Redis 事務:將一組命令放在同一個事務中進行處理

事務

MULTI 、 EXEC 、 DISCARD 和 WATCH 是 Redis 事務相關的命令。事務能夠一次執行多個命令, 而且帶有如下兩個重要的保證:html

  • 事務是一個單獨的隔離操做:事務中的全部命令都會序列化、按順序地執行。事務在執行的過程當中,不會被其餘客戶端發送來的命令請求所打斷。redis

  • 事務是一個原子操做:事務中的命令要麼所有被執行,要麼所有都不執行。數據庫

EXEC 命令負責觸發並執行事務中的全部命令:編程

  • 若是客戶端在使用 MULTI 開啓了一個事務以後,卻由於斷線而沒有成功執行 EXEC ,那麼事務中的全部命令都不會被執行。
  • 另外一方面,若是客戶端成功在開啓事務以後執行 EXEC ,那麼事務中的全部命令都會被執行。

當使用 AOF 方式作持久化的時候, Redis 會使用單個 write(2) 命令將事務寫入到磁盤中。數組

然而,若是 Redis 服務器由於某些緣由被管理員殺死,或者趕上某種硬件故障,那麼可能只有部分事務命令會被成功寫入到磁盤中。服務器

若是 Redis 在從新啓動時發現 AOF 文件出了這樣的問題,那麼它會退出,並彙報一個錯誤。google

使用redis-check-aof程序能夠修復這一問題:它會移除 AOF 文件中不完整事務的信息,確保服務器能夠順利啓動。spa

從 2.2 版本開始,Redis 還能夠經過樂觀鎖(optimistic lock)實現 CAS (check-and-set)操做,具體信息請參考文檔的後半部分。code

用法

MULTI 命令用於開啓一個事務,它老是返回 OK 。 MULTI 執行以後, 客戶端能夠繼續向服務器發送任意多條命令, 這些命令不會當即被執行, 而是被放到一個隊列中, 當 EXEC命令被調用時, 全部隊列中的命令纔會被執行。htm

另外一方面, 經過調用 DISCARD , 客戶端能夠清空事務隊列, 並放棄執行事務。

如下是一個事務例子, 它原子地增長了 foo 和 bar 兩個鍵的值:

> MULTI
OK
> INCR foo
QUEUED
> INCR bar
QUEUED
> EXEC
1) (integer) 1
2) (integer) 1

EXEC 命令的回覆是一個數組, 數組中的每一個元素都是執行事務中的命令所產生的回覆。 其中, 回覆元素的前後順序和命令發送的前後順序一致。

當客戶端處於事務狀態時, 全部傳入的命令都會返回一個內容爲 QUEUED 的狀態回覆(status reply), 這些被入隊的命令將在 EXEC 命令被調用時執行。

事務中的錯誤

使用事務時可能會趕上如下兩種錯誤:

  • 事務在執行 EXEC 以前,入隊的命令可能會出錯。好比說,命令可能會產生語法錯誤(參數數量錯誤,參數名錯誤,等等),或者其餘更嚴重的錯誤,好比內存不足(若是服務器使用 maxmemory 設置了最大內存限制的話)。
  • 命令可能在 EXEC 調用以後失敗。舉個例子,事務中的命令可能處理了錯誤類型的鍵,好比將列表命令用在了字符串鍵上面,諸如此類。

對於發生在 EXEC 執行以前的錯誤,客戶端之前的作法是檢查命令入隊所得的返回值:若是命令入隊時返回 QUEUED ,那麼入隊成功;不然,就是入隊失敗。若是有命令在入隊時失敗,那麼大部分客戶端都會中止並取消這個事務。

不過,從 Redis 2.6.5 開始,服務器會對命令入隊失敗的狀況進行記錄,並在客戶端調用 EXEC 命令時,拒絕執行並自動放棄這個事務。

在 Redis 2.6.5 之前, Redis 只執行事務中那些入隊成功的命令,而忽略那些入隊失敗的命令。 而新的處理方式則使得在流水線(pipeline)中包含事務變得簡單,由於發送事務和讀取事務的回覆都只須要和服務器進行一次通信。

至於那些在 EXEC 命令執行以後所產生的錯誤, 並無對它們進行特別處理: 即便事務中有某個/某些命令在執行時產生了錯誤, 事務中的其餘命令仍然會繼續執行。

從協議的角度來看這個問題,會更容易理解一些。 如下例子中, LPOP 命令的執行將出錯, 儘管調用它的語法是正確的:

Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
MULTI
+OK
SET a 3
abc
+QUEUED
LPOP a
+QUEUED
EXEC
*2
+OK
-ERR Operation against a key holding the wrong kind of value

EXEC 返回兩條bulk-string-reply: 第一條是 OK ,而第二條是 -ERR 。 至於怎樣用合適的方法來表示事務中的錯誤, 則是由客戶端本身決定的。

最重要的是記住這樣一條, 即便事務中有某條/某些命令執行失敗了, 事務隊列中的其餘命令仍然會繼續執行 —— Redis 不會中止執行事務中的命令。

如下例子展現的是另外一種狀況, 當命令在入隊時產生錯誤, 錯誤會當即被返回給客戶端:

MULTI
+OK
INCR a b c
-ERR wrong number of arguments for 'incr' command

由於調用 INCR 命令的參數格式不正確, 因此這個 INCR 命令入隊失敗。

爲何 Redis 不支持回滾(roll back)

若是你有使用關係式數據庫的經驗, 那麼 「Redis 在事務失敗時不進行回滾,而是繼續執行餘下的命令」這種作法可能會讓你以爲有點奇怪。

如下是這種作法的優勢:

  • Redis 命令只會由於錯誤的語法而失敗(而且這些問題不能在入隊時發現),或是命令用在了錯誤類型的鍵上面:這也就是說,從實用性的角度來講,失敗的命令是由編程錯誤形成的,而這些錯誤應該在開發的過程當中被發現,而不該該出如今生產環境中。
  • 由於不須要對回滾進行支持,因此 Redis 的內部能夠保持簡單且快速

有種觀點認爲 Redis 處理事務的作法會產生 bug , 然而須要注意的是, 在一般狀況下, 回滾並不能解決編程錯誤帶來的問題。 舉個例子, 若是你原本想經過 INCR 命令將鍵的值加上 1 , 卻不當心加上了 2 , 又或者對錯誤類型的鍵執行了 INCR , 回滾是沒有辦法處理這些狀況的。

放棄事務

當執行 DISCARD 命令時, 事務會被放棄, 事務隊列會被清空, 而且客戶端會從事務狀態中退出:

> SET foo 1
OK
> MULTI
OK
> INCR foo
QUEUED
> DISCARD
OK
> GET foo
"1"

使用 check-and-set 操做實現樂觀鎖

WATCH 命令能夠爲 Redis 事務提供 check-and-set (CAS)行爲。

被 WATCH 的鍵會被監視,並會發覺這些鍵是否被改動過了。 若是有至少一個被監視的鍵在 EXEC 執行以前被修改了, 那麼整個事務都會被取消, EXEC 返回nil-reply來表示事務已經失敗。

舉個例子, 假設咱們須要原子性地爲某個值進行增 1 操做(假設 INCR 不存在)。

首先咱們可能會這樣作:

val = GET mykey
val = val + 1
SET mykey $val

上面的這個實如今只有一個客戶端的時候能夠執行得很好。 可是, 當多個客戶端同時對同一個鍵進行這樣的操做時, 就會產生競爭條件。舉個例子, 若是客戶端 A 和 B 都讀取了鍵原來的值, 好比 10 , 那麼兩個客戶端都會將鍵的值設爲 11 , 但正確的結果應該是 12 纔對。

有了 WATCH , 咱們就能夠輕鬆地解決這類問題了:

WATCH mykey
val = GET mykey
val = val + 1
MULTI
SET mykey $val
EXEC

使用上面的代碼, 若是在 WATCH 執行以後, EXEC 執行以前, 有其餘客戶端修改了 mykey 的值, 那麼當前客戶端的事務就會失敗。 程序須要作的, 就是不斷重試這個操做, 直到沒有發生碰撞爲止

這種形式的鎖被稱做樂觀鎖, 它是一種很是強大的鎖機制。 而且由於大多數狀況下, 不一樣的客戶端會訪問不一樣的鍵, 碰撞的狀況通常都不多, 因此一般並不須要進行重試。

瞭解 WATCH

WATCH 使得 EXEC 命令須要有條件地執行: 事務只能在全部被監視鍵都沒有被修改的前提下執行, 若是這個前提不能知足的話,事務就不會被執行。 瞭解更多->

WATCH 命令能夠被調用屢次。 對鍵的監視從 WATCH 執行以後開始生效, 直到調用 EXEC 爲止。

用戶還能夠在單個 WATCH 命令中監視任意多個鍵, 就像這樣:

redis> WATCH key1 key2 key3
OK

當 EXEC 被調用時, 無論事務是否成功執行, 對全部鍵的監視都會被取消。

另外, 當客戶端斷開鏈接時, 該客戶端對鍵的監視也會被取消。

使用無參數的 UNWATCH 命令能夠手動取消對全部鍵的監視。 對於一些須要改動多個鍵的事務, 有時候程序須要同時對多個鍵進行加鎖, 而後檢查這些鍵的當前值是否符合程序的要求。 當值達不到要求時, 就能夠使用 UNWATCH 命令來取消目前對鍵的監視, 中途放棄這個事務, 並等待事務的下次嘗試

使用 WATCH 實現 ZPOP

WATCH 能夠用於建立 Redis 沒有內置的原子操做。舉個例子, 如下代碼實現了原創的 ZPOP 命令, 它能夠原子地彈出有序集合中分值(score)最小的元素:

WATCH zset
element = ZRANGE zset 0 0
MULTI
ZREM zset element
EXEC

程序只要重複執行這段代碼, 直到 EXEC 的返回值不是nil-reply回覆便可。

Redis 腳本和事務

從定義上來講, Redis 中的腳本自己就是一種事務, 因此任何在事務裏能夠完成的事, 在腳本里面也能完成。 而且通常來講, 使用腳本要來得更簡單,而且速度更快

由於腳本功能是 Redis 2.6 才引入的, 而事務功能則更早以前就存在了, 因此 Redis 纔會同時存在兩種處理事務的方法。

不過咱們並不打算在短期內就移除事務功能, 由於事務提供了一種即便不使用腳本, 也能夠避免競爭條件的方法, 並且事務自己的實現並不複雜。

不過在不遠的未來, 可能全部用戶都會只使用腳原本實現事務也說不定。 若是真的發生這種狀況的話, 那麼咱們將廢棄並最終移除事務功能。

相關文章
相關標籤/搜索