MULTI 、 EXEC 、 DISCARD 和 WATCH 是 Redis 事務相關的命令。事務能夠一次執行多個命令, 而且帶有如下兩個重要的保證:html
事務是一個單獨的隔離操做:事務中的全部命令都會序列化、按順序地執行。事務在執行的過程當中,不會被其餘客戶端發送來的命令請求所打斷。redis
事務是一個原子操做:事務中的命令要麼所有被執行,要麼所有都不執行。數據庫
EXEC 命令負責觸發並執行事務中的全部命令:編程
當使用 AOF 方式作持久化的時候, Redis 會使用單個 write(2) 命令將事務寫入到磁盤中。數組
然而,若是 Redis 服務器由於某些緣由被管理員殺死,或者趕上某種硬件故障,那麼可能只有部分事務命令會被成功寫入到磁盤中。服務器
若是 Redis 在從新啓動時發現 AOF 文件出了這樣的問題,那麼它會退出,並彙報一個錯誤。google
使用redis-check-aof
程序能夠修復這一問題:它會移除 AOF 文件中不完整事務的信息,確保服務器能夠順利啓動。spa
從 2.2 版本開始,Redis 還能夠經過樂觀鎖(optimistic lock)實現 CAS (check-and-set)操做,具體信息請參考文檔的後半部分。code
MULTI 命令用於開啓一個事務,它老是返回 OK
。 MULTI 執行以後, 客戶端能夠繼續向服務器發送任意多條命令, 這些命令不會當即被執行, 而是被放到一個隊列中, 當 EXEC命令被調用時, 全部隊列中的命令纔會被執行。htm
另外一方面, 經過調用 DISCARD , 客戶端能夠清空事務隊列, 並放棄執行事務。
如下是一個事務例子, 它原子地增長了 foo
和 bar
兩個鍵的值:
> MULTI OK > INCR foo QUEUED > INCR bar QUEUED > EXEC 1) (integer) 1 2) (integer) 1
EXEC 命令的回覆是一個數組, 數組中的每一個元素都是執行事務中的命令所產生的回覆。 其中, 回覆元素的前後順序和命令發送的前後順序一致。
當客戶端處於事務狀態時, 全部傳入的命令都會返回一個內容爲 QUEUED
的狀態回覆(status reply), 這些被入隊的命令將在 EXEC 命令被調用時執行。
使用事務時可能會趕上如下兩種錯誤:
對於發生在 EXEC 執行以前的錯誤,客戶端之前的作法是檢查命令入隊所得的返回值:若是命令入隊時返回 QUEUED
,那麼入隊成功;不然,就是入隊失敗。若是有命令在入隊時失敗,那麼大部分客戶端都會中止並取消這個事務。
不過,從 Redis 2.6.5 開始,服務器會對命令入隊失敗的狀況進行記錄,並在客戶端調用 EXEC 命令時,拒絕執行並自動放棄這個事務。
在 Redis 2.6.5 之前, Redis 只執行事務中那些入隊成功的命令,而忽略那些入隊失敗的命令。 而新的處理方式則使得在流水線(pipeline)中包含事務變得簡單,由於發送事務和讀取事務的回覆都只須要和服務器進行一次通信。
至於那些在 EXEC 命令執行以後所產生的錯誤, 並無對它們進行特別處理: 即便事務中有某個/某些命令在執行時產生了錯誤, 事務中的其餘命令仍然會繼續執行。
從協議的角度來看這個問題,會更容易理解一些。 如下例子中, LPOP 命令的執行將出錯, 儘管調用它的語法是正確的:
Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. MULTI +OK SET a 3 abc +QUEUED LPOP a +QUEUED EXEC *2 +OK -ERR Operation against a key holding the wrong kind of value
EXEC 返回兩條bulk-string-reply: 第一條是 OK
,而第二條是 -ERR
。 至於怎樣用合適的方法來表示事務中的錯誤, 則是由客戶端本身決定的。
最重要的是記住這樣一條, 即便事務中有某條/某些命令執行失敗了, 事務隊列中的其餘命令仍然會繼續執行 —— Redis 不會中止執行事務中的命令。
如下例子展現的是另外一種狀況, 當命令在入隊時產生錯誤, 錯誤會當即被返回給客戶端:
MULTI +OK INCR a b c -ERR wrong number of arguments for 'incr' command
由於調用 INCR 命令的參數格式不正確, 因此這個 INCR 命令入隊失敗。
若是你有使用關係式數據庫的經驗, 那麼 「Redis 在事務失敗時不進行回滾,而是繼續執行餘下的命令」這種作法可能會讓你以爲有點奇怪。
如下是這種作法的優勢:
- Redis 命令只會由於錯誤的語法而失敗(而且這些問題不能在入隊時發現),或是命令用在了錯誤類型的鍵上面:這也就是說,從實用性的角度來講,失敗的命令是由編程錯誤形成的,而這些錯誤應該在開發的過程當中被發現,而不該該出如今生產環境中。
- 由於不須要對回滾進行支持,因此 Redis 的內部能夠保持簡單且快速。
有種觀點認爲 Redis 處理事務的作法會產生 bug , 然而須要注意的是, 在一般狀況下, 回滾並不能解決編程錯誤帶來的問題。 舉個例子, 若是你原本想經過 INCR 命令將鍵的值加上 1 , 卻不當心加上了 2 , 又或者對錯誤類型的鍵執行了 INCR , 回滾是沒有辦法處理這些狀況的。
當執行 DISCARD 命令時, 事務會被放棄, 事務隊列會被清空, 而且客戶端會從事務狀態中退出:
> SET foo 1 OK > MULTI OK > INCR foo QUEUED > DISCARD OK > GET foo "1"
WATCH 命令能夠爲 Redis 事務提供 check-and-set (CAS)行爲。
被 WATCH 的鍵會被監視,並會發覺這些鍵是否被改動過了。 若是有至少一個被監視的鍵在 EXEC 執行以前被修改了, 那麼整個事務都會被取消, EXEC 返回nil-reply來表示事務已經失敗。
舉個例子, 假設咱們須要原子性地爲某個值進行增 1 操做(假設 INCR 不存在)。
首先咱們可能會這樣作:
val = GET mykey val = val + 1 SET mykey $val
上面的這個實如今只有一個客戶端的時候能夠執行得很好。 可是, 當多個客戶端同時對同一個鍵進行這樣的操做時, 就會產生競爭條件。舉個例子, 若是客戶端 A 和 B 都讀取了鍵原來的值, 好比 10 , 那麼兩個客戶端都會將鍵的值設爲 11 , 但正確的結果應該是 12 纔對。
有了 WATCH , 咱們就能夠輕鬆地解決這類問題了:
WATCH mykey val = GET mykey val = val + 1 MULTI SET mykey $val EXEC
使用上面的代碼, 若是在 WATCH 執行以後, EXEC 執行以前, 有其餘客戶端修改了 mykey
的值, 那麼當前客戶端的事務就會失敗。 程序須要作的, 就是不斷重試這個操做, 直到沒有發生碰撞爲止。
這種形式的鎖被稱做樂觀鎖, 它是一種很是強大的鎖機制。 而且由於大多數狀況下, 不一樣的客戶端會訪問不一樣的鍵, 碰撞的狀況通常都不多, 因此一般並不須要進行重試。
WATCH
WATCH 使得 EXEC 命令須要有條件地執行: 事務只能在全部被監視鍵都沒有被修改的前提下執行, 若是這個前提不能知足的話,事務就不會被執行。 瞭解更多->
WATCH 命令能夠被調用屢次。 對鍵的監視從 WATCH 執行以後開始生效, 直到調用 EXEC 爲止。
用戶還能夠在單個 WATCH 命令中監視任意多個鍵, 就像這樣:
redis> WATCH key1 key2 key3 OK
當 EXEC 被調用時, 無論事務是否成功執行, 對全部鍵的監視都會被取消。
另外, 當客戶端斷開鏈接時, 該客戶端對鍵的監視也會被取消。
使用無參數的 UNWATCH 命令能夠手動取消對全部鍵的監視。 對於一些須要改動多個鍵的事務, 有時候程序須要同時對多個鍵進行加鎖, 而後檢查這些鍵的當前值是否符合程序的要求。 當值達不到要求時, 就能夠使用 UNWATCH 命令來取消目前對鍵的監視, 中途放棄這個事務, 並等待事務的下次嘗試。
WATCH 能夠用於建立 Redis 沒有內置的原子操做。舉個例子, 如下代碼實現了原創的 ZPOP 命令, 它能夠原子地彈出有序集合中分值(score)最小的元素:
WATCH zset element = ZRANGE zset 0 0 MULTI ZREM zset element EXEC
程序只要重複執行這段代碼, 直到 EXEC 的返回值不是nil-reply回覆便可。
從定義上來講, Redis 中的腳本自己就是一種事務, 因此任何在事務裏能夠完成的事, 在腳本里面也能完成。 而且通常來講, 使用腳本要來得更簡單,而且速度更快。
由於腳本功能是 Redis 2.6 才引入的, 而事務功能則更早以前就存在了, 因此 Redis 纔會同時存在兩種處理事務的方法。
不過咱們並不打算在短期內就移除事務功能, 由於事務提供了一種即便不使用腳本, 也能夠避免競爭條件的方法, 並且事務自己的實現並不複雜。
不過在不遠的未來, 可能全部用戶都會只使用腳原本實現事務也說不定。 若是真的發生這種狀況的話, 那麼咱們將廢棄並最終移除事務功能。