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的樂觀鎖進行描述。redis
用於標記事務塊的開始。Redis會將後續的命令逐個放入隊列中,而後才能使用EXEC命令原子化地執行這個命令序列。數據庫
這個命令的運行格式以下所示:數組
MULTI這個命令的返回值是一個簡單的字符串,老是OK。服務器
在一個事務中執行全部先前放入隊列的命令,而後恢復正常的鏈接狀態。markdown
當使用WATCH命令時,只有當受監控的鍵沒有被修改時,EXEC命令纔會執行事務中的命令,這種方式利用了檢查再設置(CAS)的機制。工具
這個命令的運行格式以下所示:code
EXEC這個命令的返回值是一個數組,其中的每一個元素分別是原子化事務中的每一個命令的返回值。 當使用WATCH命令時,若是事務執行停止,那麼EXEC命令就會返回一個Null值。 隊列
清除全部先前在一個事務中放入隊列的命令,而後恢復正常的鏈接狀態。進程
若是使用了WATCH命令,那麼DISCARD命令就會將當前鏈接監控的全部鍵取消監控。
這個命令的運行格式以下所示:
DISCARD
這個命令的返回值是一個簡單的字符串,老是OK。
當某個事務須要按條件執行時,就要使用這個命令將給定的鍵設置爲受監控的。
這個命令的運行格式以下所示:
WATCH key [key ...]
這個命令的返回值是一個簡單的字符串,老是OK。
對於每一個鍵來講,時間複雜度老是O(1)。
清除全部先前爲一個事務監控的鍵。
若是你調用了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事務的這種行爲,有一個廣泛的反對觀點,那就是程序有可能會有缺陷(bug)。可是,你應當注意到:事務回滾並不能解決任何程序錯誤。例如,若是某個查詢會將一個鍵的值遞增2,而不是1,或者遞增錯誤的鍵,那麼事務回滾機制是沒有辦法解決這些程序問題的。請注意,沒有人能解決程序員本身的錯誤,這種錯誤可能會致使Redis命令執行失敗。正由於這些程序錯誤不大可能會進入生產環境,因此咱們在開發Redis時選用更加簡單和快速的方法,沒有實現錯誤回滾的功能。
DISCARD命令能夠用來停止事務運行。在這種狀況下,不會執行事務中的任何命令,而且會將Redis鏈接恢復爲正常狀態。示例以下所示:
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命令實際作了些什麼呢?這個命令會使得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從2.6版本纔開始引入腳本特性,而事務特性是好久之前就已經存在的,因此目前的版本纔有兩個看起來重複的特性。可是,咱們不太可能在短期內移除對事務特性的支持。由於,即便不用求助於Redis腳本,用戶仍然可以規避競爭狀態,這從語義上來看是適宜的。還有另外一個更重要的緣由,Redis事務特性的實現複雜度是最小的。
可是,在至關長的一段時間以內,咱們不大可能看到整個用戶羣體都只使用Redis腳本。若是發生這種狀況,那麼咱們可能會廢棄,甚至最終移除Redis事務。