Redis 事務
先來看看 MySQL 事務的四大特性:
- 原子性:將事務里的多個操作打包成一個整體,要么全部成功,要么全部失敗,失敗后會進行回滾操作。
- 一致性:確保事務執行前后,其數據的整體變化一致。
- 隔離性:并發執行事務或其他操作時,對數據訪問的隔離限制。
- 持久性:事務完成后,對數據的修改持久化存儲。
Redis 的事務與 MySQL 的事務相比,是稍遜一籌的。Redis 事務的特性可以說只有一個:“原子性”。
且 Redis 是否是真正的原子性還持有爭議。如果原子性按照 MySQL 事務的原子性定義,那么 Redis 事務其實是沒有原子性的。
Redis 事務的原子性,就行將多個操作打包成一個整體,要么全部執行,要么全都不執行。這里的全部執行是不保證成功的。
這和 MySQL 事務原子性是不同的,除了執行過程中出現錯誤的命令外,其他命令都能正常執行,并且,Redis 事務是不支持回滾(roll back)操作的。這與 MySQL 的略有不同。
舊版本 Redis 官網上對于事務的介紹中有這么一句話:
Either all of the commands or none are processed, so Redis transactions is also atomic. The EXEC command…
但現在 Redis 官網把這句話給刪掉了,可見官方對于 Redis 事務的原子性也是比較虛的…
根據 MySQL 事務的四大特性,總結 Redis 事務的特性:
- 弱化的原子性:Redis 沒有回滾機制,只能做到這些操作批量執行,不能做到某條操作失敗后就回到事務執行的初始狀態。
- 不保證一致性:Redis 沒有回滾機制,也沒有 “約束”。MySQL 的一致性體現的是運行事務前和運行后,結果都是合理有效的,不會出現中間非法狀態。
- 不需要隔離性:Redis 是單線程模型,并不存在并發執行事務的情況。
- 不需要持久性:Redis 的數據主要還是保存在內存的,是否開啟持久化和 redis-server 有關,不在 Redis 事務的考慮范圍內。
Redis 事務的主要意義就是提供了一種將多個命令請求打包的功能。然后,再按順序執行打包的所有命令,并且不會被中途打斷。也就是當有其他客戶端也執行命令時,不會出現 “插隊” 的情況。
Redis 是如何實現事務的?在開啟事務時,Redis 會為當前客戶端額外創建一個隊列。后續的命令操作都將進入這個隊列,知道后續執行事務時,才將這些命令操作從隊列中取出,執行。
在 Redis 主線程執行隊列里的命令操作時,是不會處理其他客戶端的命令請求的,知道隊列的命令操作全部執行完畢。
MySQL 的事務確實非常強大,但是其在空間和時間上都花費了不小的資源和開銷,是做出了一定的犧牲的。
而 Redis 主要的特點就是操作簡單,速度快,性能高,常常作為 MySQL 服務器上游的緩存來使用。所以,其實并不需要像 MySQL 事務那樣的強大。
Redis 事務實際開發中使用的非常少,功能比較雞肋。往往用在那些會出現線程安全問題的地方,比如計數器等。且 Redis 是支持通過編寫 Lua 腳本來執行一系列的操作,這也看出是一種事務。
事務命令
使用 MULTI
開啟事務,使用 EXEC
執行事務,使用 DISCARD
取消事務。
注意:開啟事務后,將一些命令發送給服務器時,這些命令并不會立即執行,只有輸入 EXEC
命令時,才會依此執行這些命令。
取消事務后,之前的發送給服務器的命令就都不會執行。
如果在開啟事務時,且已經有幾個命令發送給服務器了,此時服務器宕機了,那么就和 DISCARD
一樣,取消了事務。
redis 127.0.0.1:6379> MULTI
OK
redis 127.0.0.1:6379(TX)> SET name redis
QUEUED
redis 127.0.0.1:6379(TX)> SET age 18
QUEUED
redis 127.0.0.1:6379(TX)> EXEC
1) OK
2) OK
redis 127.0.0.1:6379>
來看這么一個場景:
- 客戶端一:
redis 127.0.0.1:6379> SET k1 111 # 0??
redis 127.0.0.1:6379> MULTI # 1??
OK
redis 127.0.0.1:6379(TX)> SET k1 222 # 2??
QUEUED
redis 127.0.0.1:6379(TX)> EXEC # 4??
1) OK
redis 127.0.0.1:6379>
- 客戶端二:
redis 127.0.0.1:6379> SET k1 333 # 3??
有上述兩個客戶端,按照后續的序號執行命令,也就是客戶端一在開啟事務后,發送服務器將 k1
設為 222 的命令,隨即客戶端二發送了 SET k1 333
命令并且執行了。最后客戶端一執行了事務。
直接看的話,感覺最后 k1
的結果是 333。但結果卻是 222。這是因為只有執行了 EXEC
命令,才會真正執行事務,之前的命令都只是進入了事務隊列,還沒有真正執行。所以,將 k1
設為 222 的命令是發生在客戶端二執行之后的。
針對上述場景,會導致最后的結果產生歧義。我們可以在執行事務前通過 WATCH
監聽 k1
。
WATCH
可以監聽某個 key
是否在執行事務之間,與開啟事務前的值有差異。如果監聽到值有變化,則不執行修改該 key
的命令。
UNWATCH
可以取消對某個 key
的監聽。
- 客戶端一:
redis 127.0.0.1:6379> WATCH k1 # 0??
OK
redis 127.0.0.1:6379> SET k1 111 # 1??
OK
redis 127.0.0.1:6379> MULTI # 2??
OK
redis 127.0.0.1:6379(TX)> SET k1 222 # 3??
QUEUED
redis 127.0.0.1:6379(TX)> EXEC # 5??
(nil)
redis 127.0.0.1:6379>
- 客戶端二:
redis 127.0.0.1:6379> SET k1 333 # 4??
OK
redis 127.0.0.1:6379>
通過上述操作,可以看到,客戶端一執行 EXEC
了后,返回 (nil)
,說明該事務沒有執行任何操作。最后 k1
的值也是 333。
這是因為其監聽到了 k1
的值發生了變化,所以不執行修改 k1
的命令。
WATCH 的實現
WATCH
的實現是基于 "樂觀鎖"的,通過 CAS
原子操作判斷 key
是否被修改。
在監聽某個 key
時,會為該 key
生成一個 “版本號”,后續有其他客戶端對該 key
進行修改時,會將該 key
的版本號增加。
在執行事務時,出現了對 key
進行修改的命令,會通過 CAS
操作判斷其版本號與最初 WATCH
時的版本號是否一致。如果一致,則執行命令,否則,放棄執行該命令。
至于為什么要是有版本號而不是直接使用 key
的值判斷,是為了避免 ABA 問題的出現。
Redis 中的 Lua 腳本,也能起到類似于事務的效果。官網上說,事務這里的任何能實現的效果,都可以使用 Lua 腳本替代。