天天看點

你真的懂Redis事務嗎?

譯者:黃健宏

MULTI 、 EXEC 、 DISCARD 和 WATCH 是 Redis 事務的基礎。

事務可以一次執行多個指令, 并且帶有以下兩個重要的保證:

  • 事務是一個單獨的隔離操作:事務中的所有指令都會序列化、按順序地執行。事務在執行的過程中,不會被其他用戶端發送來的指令請求所打斷。
  • 事務是一個原子操作:事務中的指令要麼全部被執行,要麼全部都不執行。

    EXEC 指令負責觸發并執行事務中的所有指令:

    當使用 AOF 方式做持久化的時候, Redis 會使用單個 

    write(2)

     指令将事務寫入到磁盤中。

    然而,如果 Redis 伺服器因為某些原因被管理者殺死,或者遇上某種硬體故障,那麼可能隻有部分事務指令會被成功寫入到磁盤中。

    如果 Redis 在重新啟動時發現 AOF 檔案出了這樣的問題,那麼它會退出,并彙報一個錯誤。

    使用 

    redis-check-aof

     程式可以修複這一問題:它會移除 AOF 檔案中不完整事務的資訊,確定伺服器可以順利啟動。
    • 如果用戶端在使用 MULTI 開啟了一個事務之後,卻因為斷線而沒有成功執行 EXEC ,那麼事務中的所有指令都不會被執行。
    • 另一方面,如果用戶端成功在開啟事務之後執行 EXEC ,那麼事務中的所有指令都會被執行。

從 2.2 版本開始,Redis 還可以通過樂觀鎖(optimistic lock)實作 CAS (check-and-set)操作,具體資訊請參考文檔的後半部分。

用法

MULTI 指令用于開啟一個事務,它總是傳回 

OK

 。

MULTI 執行之後, 用戶端可以繼續向伺服器發送任意多條指令, 這些指令不會立即被執行, 而是被放到一個隊列中, 當 EXEC 指令被調用時, 所有隊列中的指令才會被執行。

另一方面, 通過調用 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 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 , 復原是沒有辦法處理這些情況的。

鑒于沒有任何機制能避免程式員自己造成的錯誤, 并且這類錯誤通常不會在生産環境中出現, 是以 Redis 選擇了更簡單、更快速的無復原方式來處理事務。

放棄事務

當執行 DISCARD 指令時, 事務會被放棄, 事務隊列會被清空, 并且用戶端會從事務狀态中退出:

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

使用 check-and-set 操作實作樂觀鎖

WATCH 指令可以為 Redis 事務提供 check-and-set (CAS)行為。

被 WATCH 的鍵會被監視,并會發覺這些鍵是否被改動過了。 如果有至少一個被監視的鍵在 EXEC 執行之前被修改了, 那麼整個事務都會被取消, EXEC 傳回空多條批量回複(null multi-bulk 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 監視了一個帶過期時間的鍵, 那麼即使這個鍵過期了, 事務仍然可以正常執行, 關于這方面的詳細情況,請看這個文章: http://code.google.com/p/redis/issues/detail?id=270

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 的傳回值不是空多條回複(null multi-bulk reply)即可。

Redis 腳本和事務

從定義上來說, Redis 中的腳本本身就是一種事務, 是以任何在事務裡可以完成的事, 在腳本裡面也能完成。 并且一般來說, 使用腳本要來得更簡單,并且速度更快。

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

不過我們并不打算在短時間内就移除事務功能, 因為事務提供了一種即使不使用腳本, 也可以避免競争條件的方法, 而且事務本身的實作并不複雜。