Redis從菜鳥到大神-事務處理

爲了確保連續多個操作的原子性,一個成熟的數據庫通常都會有事務支持,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 之前做好盯住關鍵 變量,否則會出錯。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章