Redis從入門到精通(五、Redis的事務)

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 的客戶端,這種做法叫做樂觀鎖。我們只需在客戶端執行事務失敗之后進行重試的邏輯即可。


更多詳細的資料參考:

Redis事務官方文檔

Redis實戰


?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容