_028_Redis_Redis的事務功能詳解
轉自https://www.cnblogs.com/kyrin/p/5967620.html,感謝作者的無私分享。
MULTI、EXEC、DISCARD和WATCH命令是Redis事務功能的基礎。Redis事務允許在一次單獨的步驟中執行一組命令,並且可以保證如下兩個重要事項:
>Redis會將一個事務中的所有命令序列化,然後按順序執行。Redis不可能在一個Redis事務的執行過程中插入執行另一個客戶端發出的請求。這樣便能保證Redis將這些命令作為一個單獨的隔離操作執行。 > 在一個Redis事務中,Redis要麼執行其中的所有命令,要麼什麼都不執行。因此,Redis事務能夠保證原子性。EXEC命令會觸發執行事務中的所有命令。因此,當某個客戶端正在執行一次事務時,如果它在呼叫MULTI命令之前就從Redis服務端斷開連線,那麼就不會執行事務中的任何操作;相反,如果它在呼叫EXEC命令之後才從Redis服務端斷開連線,那麼就會執行事務中的所有操作。當Redis使用只增檔案(AOF:Append-only File)時,Redis能夠確保使用一個單獨的write(2)系統呼叫,這樣便能將事務寫入磁碟。然而,如果Redis伺服器宕機,或者系統管理員以某種方式停止Redis服務程序的執行,那麼Redis很有可能只執行了事務中的一部分操作。Redis將會在重新啟動時檢查上述狀態,然後退出執行,並且輸出報錯資訊。使用redis-check-aof工具可以修復上述的只增檔案,這個工具將會從上述檔案中刪除執行不完全的事務,這樣Redis伺服器才能再次啟動。
從2.2版本開始,除了上述兩項保證之外,Redis還能夠以樂觀鎖的形式提供更多的保證,這種形式非常類似於“檢查再設定”(CAS:Check And Set)操作。本文稍後會對Redis的樂觀鎖進行描述。
一、相關命令
1. MULTI
用於標記事務塊的開始。Redis會將後續的命令逐個放入佇列中,然後才能使用EXEC命令原子化地執行這個命令序列。
這個命令的執行格式如下所示:
MULTI
這個命令的返回值是一個簡單的字串,總是OK。
2. EXEC
在一個事務中執行所有先前放入佇列的命令,然後恢復正常的連線狀態。
當使用WATCH命令時,只有當受監控的鍵沒有被修改時,EXEC命令才會執行事務中的命令,這種方式利用了檢查再設定(CAS)的機制。
這個命令的執行格式如下所示:
EXEC
這個命令的返回值是一個數組,其中的每個元素分別是原子化事務中的每個命令的返回值。 當使用WATCH命令時,如果事務執行中止,那麼EXEC命令就會返回一個Null值。
3. DISCARD
清除所有先前在一個事務中放入佇列的命令,然後恢復正常的連線狀態。
如果使用了WATCH命令,那麼DISCARD命令就會將當前連線監控的所有鍵取消監控。
這個命令的執行格式如下所示:
DISCARD
這個命令的返回值是一個簡單的字串,總是OK。
4. WATCH
當某個事務需要按條件執行時,就要使用這個命令將給定的鍵設定為受監控的。
這個命令的執行格式如下所示:
WATCH key [key ...]
這個命令的返回值是一個簡單的字串,總是OK。
對於每個鍵來說,時間複雜度總是O(1)。
5. UNWATCH
清除所有先前為一個事務監控的鍵。
如果你呼叫了EXEC或DISCARD命令,那麼就不需要手動呼叫UNWATCH命令。
這個命令的執行格式如下所示:
UNWATCH
這個命令的返回值是一個簡單的字串,總是OK。
時間複雜度總是O(1)。
二、使用方法
使用MULTI命令便可以進入一個Redis事務。這個命令的返回值總是OK。此時,使用者可以發出多個Redis命令。Redis會將這些命令放入佇列,而不是執行這些命令。一旦呼叫EXEC命令,那麼Redis就會執行事務中的所有命令。
相反,呼叫DISCARD命令將會清除事務佇列,然後退出事務。
以下示例會原子化地遞增foo鍵和bar鍵的值:
正如從上面的會話所看到的一樣,EXEC命令的返回值是一個數組,其中的每個元素都分別是事務中的每個命令的返回值,返回值的順序和命令的發出順序是相同的。
當一個Redis連線正處於MULTI請求的上下文中時,通過這個連線發出的所有命令的返回值都是QUEUE字串(從Redis協議的角度來看,返回值是作為狀態回覆(Status Reply)來發送的)。當呼叫EXEC命令時,Redis會簡單地排程執行事務佇列中的命令。
三、事務內部的錯誤
在一個事務的執行期間,可能會遇到兩種型別的命令錯誤:
一個命令可能會在被放入佇列時失敗。因此,事務有可能在呼叫EXEC命令之前就發生錯誤。例如,這個命令可能會有語法錯誤(引數的數量錯誤、命令名稱錯誤,等等),或者可能會有某些臨界條件(例如:如果使用maxmemory指令,為Redis伺服器配置記憶體限制,那麼就可能會有記憶體溢位條件)。
在呼叫EXEC命令之後,事務中的某個命令可能會執行失敗。例如,我們對某個鍵執行了錯誤型別的操作(例如,對一個字串(String)型別的鍵執行列表(List)型別的操作)。
可以使用Redis客戶端檢測第一種型別的錯誤,在呼叫EXEC命令之前,這些客戶端可以檢查被放入佇列的命令的返回值:如果命令的返回值是QUEUE字串,那麼就表示已經正確地將這個命令放入佇列;否則,Redis將返回一個錯誤。如果將某個命令放入佇列時發生錯誤,那麼大多數客戶端將會中止事務,並且丟棄這個事務。
然而,從Redis 2.6.5版本開始,伺服器會記住事務積累命令期間發生的錯誤。然後,Redis會拒絕執行這個事務,在執行EXEC命令之後,便會返回一個錯誤訊息。最後,Redis會自動丟棄這個事務。
在Redis 2.6.5版本之前,如果發生了上述的錯誤,那麼在客戶端呼叫了EXEC命令之後,Redis還是會執行這個出錯的事務,執行已經成功放入事務佇列的命令,而不會關心先前發生的錯誤。從2.6.5版本開始,Redis在遭遇上述錯誤時,會採用先前描述的新行為,這樣便能輕鬆地混合使用事務和管道。在這種情況下,客戶端可以一次性地將整個事務傳送至Redis伺服器,稍後再一次性地讀取所有的返回值。
相反,在呼叫EXEC命令之後發生的事務錯誤,Redis不會進行任何特殊處理:在事務執行期間,即使某個命令執行失敗,所有其他的命令也將會繼續執行。
這種行為在協議層面上更加清晰。在以下示例中,當事務正在執行時,有一條命令將會執行失敗,即使這條命令的語法是正確的:
上述示例的EXEC命令的返回值是批量的字串,包含兩個元素,一個是OK程式碼,另一個是-ERR錯誤訊息。客戶端會根據自身的程式庫,選擇一種合適的方式,將錯誤資訊提供給使用者
需要注意的是,即使某個命令執行失敗,事務佇列中的所有其他命令仍然會執行 —— Redis不會停止執行事務中的命令。
再看另一個示例,再次使用telnet通訊協議,觀察命令的語法錯誤是如何儘快報告給使用者的:
這一次,由於INCR命令的語法錯誤,Redis根本就沒有將這個命令放入事務佇列。
四、為什麼Redis不支援回滾?
如果你具備關係型資料庫的知識背景,你就會發現一個事實:在事務執行期間,雖然Redis命令可能會執行失敗,但是Redis仍然會執行事務中餘下的其他命令,而不會執行回滾操作,你可能會覺得這種行為很奇怪。
然而,這種行為也有其合理之處:
只有當被呼叫的Redis命令有語法錯誤時,這條命令才會執行失敗(在將這個命令放入事務佇列期間,Redis能夠發現此類問題),或者對某個鍵執行不符合其資料型別的操作:實際上,這就意味著只有程式錯誤才會導致Redis命令執行失敗,這種錯誤很有可能在程式開發期間發現,一般很少在生產環境發現。
Redis已經在系統內部進行功能簡化,這樣可以確保更快的執行速度,因為Redis不需要事務回滾的能力。
對於Redis事務的這種行為,有一個普遍的反對觀點,那就是程式有可能會有缺陷(bug)。但是,你應當注意到:事務回滾並不能解決任何程式錯誤。例如,如果某個查詢會將一個鍵的值遞增2,而不是1,或者遞增錯誤的鍵,那麼事務回滾機制是沒有辦法解決這些程式問題的。請注意,沒有人能解決程式設計師自己的錯誤,這種錯誤可能會導致Redis命令執行失敗。正因為這些程式錯誤不大可能會進入生產環境,所以我們在開發Redis時選用更加簡單和快速的方法,沒有實現錯誤回滾的功能。
五、丟棄命令佇列
DISCARD命令可以用來中止事務執行。在這種情況下,不會執行事務中的任何命令,並且會將Redis連線恢復為正常狀態。示例如下所示:
六、通過CAS操作實現樂觀鎖
Redis使用WATCH命令實現事務的“檢查再設定”(CAS)行為。
作為WATCH命令的引數的鍵會受到Redis的監控,Redis能夠檢測到它們的變化。在執行EXEC命令之前,如果Redis檢測到至少有一個鍵被修改了,那麼整個事務便會中止執行,然後EXEC命令會返回一個Null值,提醒使用者事務執行失敗。
例如,設想我們需要將某個鍵的值自動遞增1(假設Redis沒有INCR命令)。
首次嘗試的偽碼可能如下所示:
val = GET mykey
val = val + 1
SET mykey $val
如果我們只有一個Redis客戶端在一段指定的時間之內執行上述偽碼的操作,那麼這段偽碼將能夠可靠的工作。如果有多個客戶端大約在同一時間嘗試遞增這個鍵的值,那麼將會產生競爭狀態。例如,客戶端-A和客戶端-B都會讀取這個鍵的舊值(例如:10)。這兩個客戶端都會將這個鍵的值遞增至11,最後使用SET命令將這個鍵的新值設定為11。因此,這個鍵的最終值是11,而不是12。
現在,我們可以使用WATCH命令完美地解決上述的問題,偽碼如下所示:
WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC
由上述偽碼可知,如果存在競爭狀態,並且有另一個客戶端在我們呼叫WATCH命令和EXEC命令之間的時間內修改了val變數的結果,那麼事務將會執行失敗。
我們只需要重複執行上述偽碼的操作,希望此次執行不會再出現競爭狀態。這種形式的鎖就被稱為樂觀鎖,它是一種非常強大的鎖。在許多用例中,多個客戶端可能會訪問不同的鍵,因此不太可能發生衝突 —— 也就是說,通常沒有必要重複執行上述偽碼的操作。
七、WATCH命令詳解
那麼WATCH命令實際做了些什麼呢?這個命令會使得EXEC命令在滿足某些條件時才會執行事務:我們要求Redis只有在所有受監控的鍵都沒有被修改時,才會執行事務。(但是,相同的客戶端可能會在事務內部修改這些鍵,此時這個事務不會中止執行。)否則,Redis根本就不會進入事務。(注意,如果你使用WATCH命令監控一個易失性的鍵,然後在你監控這個鍵之後,Redis再使這個鍵過期,那麼EXEC命令仍然可以正常工作。)
WATCH命令可以被呼叫多次。簡單說來,所有的WATCH命令都會在被呼叫之時立刻對相應的鍵進行監控,直到EXEC命令被呼叫之時為止。你可以在單條的WATCH命令之中,使用任意數量的鍵作為命令引數。
當呼叫EXEC命令時,所有的鍵都會變為未受監控的狀態,Redis不會管事務是否被中止。當一個客戶單連線被關閉時,所有的鍵也都會變為未受監控的狀態。
你還可以使用UNWATCH命令(不需要任何引數),這樣便能清除所有的受監控鍵。當我們對某些鍵施加樂觀鎖之後,這個命令有時會非常有用。因為,我們可能需要執行一個用來修改這些鍵的事務,但是在讀取這些鍵的當前內容之後,我們可能不打算繼續進行操作,此時便可以使用UNWATCH命令,清除所有受監控的鍵。在執行UNWATCH命令之後,Redis連線便可以再次自由地用於執行新事務。
如何使用WATCH命令實現ZPOP操作呢?
本文將通過一個示例,說明如何使用WATCH命令建立一個新的原子化操作(Redis並不原生支援這個原子化操作),此處會以實現ZPOP操作為例。這個命令會以一種原子化的方式,從一個有序集合中彈出分數最低的元素。以下原始碼是最簡單的實現方式:
WATCH zset
element = ZRANGE zset 0 0
MULTI
ZREM zset element
EXEC
如果偽碼中的EXEC命令執行失敗(例如,返回Null值),那麼我們只需要重複執行這個操作即可。
八、Redis指令碼和事務
根據定義,Redis指令碼也是事務型的。因此,你可以通過Redis事務實現的功能,同樣也可以通過Redis指令碼來實現,而且通常指令碼更簡單、更快速。
由於Redis從2.6版本才開始引入指令碼特性,而事務特性是很久以前就已經存在的,所以目前的版本才有兩個看起來重複的特性。但是,我們不太可能在短時間內移除對事務特性的支援。因為,即使不用求助於Redis指令碼,使用者仍然能夠規避競爭狀態,這從語義上來看是適宜的。還有另一個更重要的原因,Redis事務特性的實現複雜度是最小的。
但是,在相當長的一段時間之內,我們不大可能看到整個使用者群體都只使用Redis指令碼。如果發生這種情況,那麼我們可能會廢棄,甚至最終移除Redis事務。