爲了確保連續多個操作的原子性,一個成熟的數據庫通常都會有事務支持,Redis也不例外。Redis 的事務使用非常簡單,不同於關係數據庫,我們無須理解那麼多複雜的事務模 型,就可以直接使用。不過也正是因爲這種簡單性,它的事務模型很不嚴格,這要求我們不 能像使用關係數據庫的事務一樣來使用 Redis。
一、Redis 事務的基本使用
每個事務的操作都有 begin、commit 和 rollback,begin 指示事務的開始,commit 指示
事務的提交,rollback 指示事務的回滾。它大致的形式如下。
begin(); try {
command1(); command2(); ....
commit();
} catch(Exception e) { rollback();
}
Redis 在形式上看起來也差不多,分別是 multi/exec/discard。multi 指示事務的開始,
exec 指示事務的執行,discard 指示事務的丟棄。
> multi
OK
> incr books QUEUED
> incr books QUEUED
> exec (integer) 1
(integer) 2
上面的指令演示了一個完整的事務過程,所有的指令在 exec 之前不執行,而是緩存在 服務器的一個事務隊列中,服務器一旦收到 exec 指令,纔開執行整個事務隊列,執行完畢 後一次性返回所有指令的運行結果。因爲 Redis 的單線程特性,它不用擔心自己在執行隊列 的時候被其它指令打攪,可以保證他們能得到的「原子性」執行。
上圖顯示了以上事務過程完整的交互效果。QUEUED 是一個簡單字符串,同 OK 是一 個形式,它表示指令已經被服務器緩存到隊列裏了。
二、Redis事務原子性
事務的原子性是指要麼事務全部成功,要麼全部失敗,那麼 Redis 事務執行是原子性的 麼?
下面我們來看一個特別的例子。
> multi
OK
> set books iamastring
QUEUED
> incr books
QUEUED
> set poorman iamdesperate
QUEUED
> exec
1) OK
2) (error) ERR value is not an integer or out of range 3) OK
> get books
"iamastring"
> get poorman
"iamdesperate
上面的例子是事務執行到中間遇到失敗了,因爲我們不能對一個字符串進行數學運算, 事務在遇到指令執行失敗後,後面的指令還繼續執行,所以 poorman 的值能繼續得到設置。
到這裏,你應該明白 Redis 的事務根本不能算「原子性」,而僅僅是滿足了事務的「隔 離性」,隔離性中的串行化——當前執行的事務有着不被其它事務打斷的權利。
三、Redis事務discard(丟棄)
Redis 爲事務提供了一個 discard 指令,用於丟棄事務緩存隊列中的所有指令,在 exec執行之前。
> get books (nil)
> multi OK
> incr books QUEUED
> incr books QUEUED
> discard OK
> get books (nil)
我們可以看到 discard 之後,隊列中的所有指令都沒執行,就好像 multi 和 discard 中 間的所有指令從未發生過一樣。
四、Redis事務優化
上面的 Redis 事務在發送每個指令到事務緩存隊列時都要經過一次網絡讀寫,當一個事 務內部的指令較多時,需要的網絡 IO 時間也會線性增長。所以通常 Redis 的客戶端在執行 事務時都會結合 pipeline 一起使用,這樣可以將多次 IO 操作壓縮爲單次 IO 操作。比如我 們在使用 Python 的 Redis 客戶端時執行事務時是要強制使用 pipeline 的。
pipe = redis.pipeline(transaction=true) pipe.multi()
pipe.incr("books")
pipe.incr("books")
values = pipe.execute()
五、Redis事務命令Watch
考慮到一個業務場景,Redis 存儲了我們的賬戶餘額數據,它是一個整數。現在有兩個 併發的客戶端要對賬戶餘額進行修改操作,這個修改不是一個簡單的 incrby 指令,而是要對
> watch books OK
> incr books (integer) 1
# 被修改了
> multi
OK
> incr books
QUEUED
> exec # 事務執行失敗 (nil)
當服務器給 exec 指令返回一個 null 回覆時,客戶端知道了事務執行是失敗的,通常客 戶端 (redis-py) 都會拋出一個 WatchError 這種錯誤,不過也有些語言 (jedis) 不會拋出異
餘額乘以一個倍數。Redis 可沒有提供 multiplyby 這樣的指令。我們需要先取出餘額然後在 內存裏乘以倍數,再將結果寫回 Redis。
這就會出現併發問題,因爲有多個客戶端會併發進行操作。我們可以通過 Redis 的分佈 式鎖來避免衝突,這是一個很好的解決方案。分佈式鎖是一種悲觀鎖,那是不是可以使用樂 觀鎖的方式來解決衝突呢?
Redis 提供了這種 watch 的機制,它就是一種樂觀鎖。有了 watch 我們又多了一種可以 用來解決併發修改的方法。 watch 的使用方式如下:
while True: do_watch()
commands() multi() send_commands() try:
exec()
break
except WatchError:
continue
watch 會在事務開始之前盯住 1 個或多個關鍵變量,當事務執行時,也就是服務器收到 了 exec 指令要順序執行緩存的事務隊列時,Redis 會檢查關鍵變量自 watch 之後,是否被 修改了 (包括當前事務所在的客戶端)。如果關鍵變量被人動過了,exec 指令就會返回 null 回覆告知客戶端事務執行失敗,這個時候客戶端一般會選擇重試。
常,而是通過在 exec 方法裏返回一個 null,這樣客戶端需要檢查一下返回結果是否爲 null 來確定事務是否執行失敗。
注意事項
Redis 禁止在 multi 和 exec 之間執行 watch 指令,而必須在 multi 之前做好盯住關鍵 變量,否則會出錯。