Redis通過 MULTI,EXEC,DISCARD,WATCH.UNWATCH
來實現事務功能。
Redis 事務介紹
提到事務,我們可能馬上會想到傳統的關系型數據庫中的事務,客戶端首先向服務器發送 BEGIN
開啟事務,然后執行讀寫操作,最后用戶發送 COMMIT
或者 ROLLBACK
來提交或者回滾之前的操作。但是Redis中的事務與關系型數據庫是不一樣的,Redis 通過 MULTI
命令開始,之后輸入一連串的操作,最終以 EXEC
結束,在這之間輸入的所有的命令都會在 EXEC
之后一起發給Redis執行,所以在這之間用戶無法通過讀取到的結果做處理,這與關系型數據庫的事務是由很大的不同的。Redis會在執行完成之后返回一組執行結果。Redis中并沒有回滾的操作,這一點會在后面說到。
Redis的這種延遲執行事務會有助于提升性能,客戶端會在收到 EXEC
命令之后再將這一系列的命令一起發給Redis,然后等待Redis的回復,這種 一次性發送多條指令,然后等待回復
的做法稱為流水線(pipeline)模式,它可以通過減少客戶端與服務器之間的網絡通信次數來提高Redis執行多個命令的性能。
Redis通過以下兩點保證事務:
- 事務中的所有命令都被序列化并按順序執行,在執行事務的過程中不會去執行其他客戶端的命令,保證命令作為單個隔離操作進行
- 要么處理所有命令,要么不處理。保證原子性。如果開啟了AOF,Redis會使用單個write命令將事務寫入文件中,如果因為某些原因導致AOF寫入被截斷,在重啟時redis會報錯,使用
redis-check-aof
工具可以修復這個錯誤(刪除掉這個事務相關的命令),保證Redis能夠重新啟動
Redis 事務示例
下面我們來看一些示例:
MULTI EXEC
127.0.0.1:6379[2]> set foo 1
OK
127.0.0.1:6379[2]> set bar 1
OK
127.0.0.1:6379[2]> MULTI
OK
127.0.0.1:6379[2]> INCR foo
QUEUED
127.0.0.1:6379[2]> INCR bar
QUEUED
127.0.0.1:6379[2]> EXEC
1) (integer) 2
2) (integer) 2
127.0.0.1:6379[2]>
可以看到在執行 MULTI
之后會返回 OK
表示狀態回復,然后執行兩個 INCR
操作,會返回 QUEUED
表示已經進入到隊列當中,最后執行 EXEC
命令,上述所有命令會一起發送到Redis,然后收到Redis的一組回復。
DISCARD
127.0.0.1:6379[2]> MULTI
OK
127.0.0.1:6379[2]> set test 09876
QUEUED
127.0.0.1:6379[2]> DISCARD
OK
127.0.0.1:6379[2]> get test
"1234"
127.0.0.1:6379[2]>
DICARD
可以取消事務
命令出現語法錯誤
下面來看以下如果這其中有語法錯誤的命令會怎么樣:
127.0.0.1:6379[2]> MULTI
OK
127.0.0.1:6379[2]> set test 1234
QUEUED
127.0.0.1:6379[2]> lpush test 12345
QUEUED
127.0.0.1:6379[2]> EXEC
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379[2]> get test
"1234"
可以看到,最終返回結果是set
命令執行成功,而 lpush
命令執行失敗,通過 get test
命令,可以看到它的值是1234
。可以看到,即使后續的命令出現了錯誤,前面已經執行成功的命令也不會回滾,同樣也不會影響后續命令。
Redis事務不支持回滾
Redis認為只有語法出現錯誤時才會導致事務的失敗,并且Redis的速度夠快,不需要回滾的能力。Redis官方給出的解釋是(我做了一下翻譯):
如果你有關系型數據庫的相關經驗,實際上Redis命令在事務期間可能會出現失敗的情況,但是Redis仍然執行了事務中剩余的命令而不是回滾,在你看來這可能很荒謬。
但是對于這種操作有以下很好的見解:
- Redis 命令只有在出現語法錯的情況下才會導致失敗(這個問題沒辦法再入隊列期間檢測到),或者這個key是錯誤的數據類型: 這意味著是編程錯誤造成的命令失敗,在開發過程中就應該檢查到這種錯誤中,而不是到生產中才發現
- Redis 內部簡單而且速度很快,不需要回滾的能力
一種反對Redis的觀點是bug是會發生的,但是通常回滾并不能解決編程錯誤所造成的結果.例如,如果查詢一個key并遞增了2而不是1,或者遞增了錯誤的key,回滾機制將沒辦法提供幫助.考慮到沒有人解決編程錯誤,而且Redis命令的失敗并不太可能進入生產環境,所以我們選擇了不支持事務回滾的更快,更簡單的做法.
WATCH命令的使用
Redis使用WATCH
來解決key的競爭問題,類似于 CAS
操作,來保證多個客戶端同時修改一個key的情況,只能有一個客戶端修改成功。
我用下面的示例演示一下A,B兩個客戶端競爭一個Key的情況:
Client A
127.0.0.1:6379[2]> GET count
"1"
127.0.0.1:6379[2]> WATCH count
OK
127.0.0.1:6379[2]> MULTI
OK
127.0.0.1:6379[2]> incr count
QUEUED
127.0.0.1:6379[2]> incr count
QUEUED
127.0.0.1:6379[2]> EXEC
(nil)
Client B
127.0.0.1:6379[2]> incr count
(integer) 2
在A客戶端WATCH count
之后,如果B客戶端執行了修改count
這個key的操作,那么A客戶端在 EXEC
之后會返回 nil
沒有進行任何操作。
我們在來看一組沒有競爭的情況:
127.0.0.1:6379[2]> get count
"3"
127.0.0.1:6379[2]> WATCH count
OK
127.0.0.1:6379[2]> MULTI
OK
127.0.0.1:6379[2]> INCR count
QUEUED
127.0.0.1:6379[2]> INCR count
QUEUED
127.0.0.1:6379[2]> EXEC
1) (integer) 4
2) (integer) 5
在沒有多個客戶端競爭的情況下,事務正常執行。
Redis并沒有用典型的加鎖功能來解決key的競爭問題,主要原因是出于性能的考慮。回顧一下關系型數據庫中的事務,在訪問以寫入為目的的數據時,數據庫會對被訪問的數據加鎖,直到提交或回滾之后才釋放鎖,如果此時另一個客戶端也這部分數據進行寫入操作,客戶端將會被阻塞,直到上一個事務結束。這種加鎖的方式稱為悲觀鎖,它的缺點在于持有鎖的客戶端持有鎖的時間越長,其它客戶端被阻塞的時間就越長。Redis為了減少客戶端等待的時間,并不會在執行WATCH
命令后對數據進行加鎖,而是如果有其他客戶端搶先修改了數據的情況下通知執行了 WATCH
的客戶端,這種做法叫做樂觀鎖。我們只需在客戶端執行事務失敗之后進行重試的邏輯即可。
更多詳細的資料參考: