1. 程式人生 > >Redis : redis事務

Redis : redis事務

Redis的事務功能詳解


MULTIEXECDISCARDWATCH命令是Redis事務功能的基礎。

Redis事務允許在一次單獨的步驟中執行一組命令,並且可以保證如下兩個重要事項:

>Redis會將一個事務中的所有命令序列化,然後按順序執行。Redis不可能在一個Redis事務的執行過程中插入執行另一個客戶端發出的請求。這樣便能保證Redis將這些命令作為一個單獨的隔離操作執行 > 在一個Redis事務中,Redis要麼執行其中的所有命令,要麼什麼都不執行。因此,Redis事務能夠保證原子性

EXEC命令會觸發執行事務中的所有命令。因此,當某個客戶端正在執行一次事務時,

如果它在呼叫EXEC命令之前就從Redis服務端斷開連線,那麼就不會執行事務中的任何操作;相反,如果它在呼叫EXEC命令之後才從Redis服務端斷開連線,那麼就會執行事務中的所有操作。(有出錯也不會停止了,後面解釋)

Redis使用只增檔案(AOFAppend-only File)時,Redis能夠確保使用一個單獨的write(2)系統呼叫,這樣便能將事務寫入磁碟。然而,如果Redis伺服器宕機,或者系統管理員以某種方式停止Redis服務程序的執行,那麼Redis很有可能只執行了事務中的一部分操作。Redis將會在重新啟動時檢查上述狀態,然後退出執行,並且輸出報錯資訊。使用redis-check-aof

工具可以修復上述的只增檔案,這個工具將會從上述檔案中刪除執行不完全的事務,這樣Redis伺服器才能再次啟動。

2.2版本開始,除了上述兩項保證之外,Redis還能夠以樂觀鎖的形式提供更多的保證,這種形式非常類似於檢查再設定CASCheck And Set)操作。本文稍後會對Redis的樂觀鎖進行描述。

一、相關命令

1. MULTI

該命令用來開啟事務,它總是返回ok結果,當其執行之後,客戶端可以繼續傳送任意條數量的指令,這些指令不會立即被執行,而是被放到了佇列中,直到EXEC被呼叫之後,所有命令才會被序列化執行

2. EXEC

在一個事務中執行所有先前放入佇列的命令,然後恢復正常的連線狀態。

當使用WATCH命令時,只有當受監控的鍵沒有被修改時,EXEC命令才會執行事務中的命令,這種方式利用了檢查再設定(CAS)的機制

這個命令的執行格式如下所示:

EXEC

這個命令的返回值是一個數組,其中的每個元素分別是原子化事務中的每個命令的返回值。當使用WATCH命令時,如果事務執行中止,那麼EXEC命令就會返回一個Null值。

MULTI開啟之後,因為某些原因沒有成功執行EXEC,那麼事務中所有的命令都不會被執行的。

 

3. DISCARD

清除所有先前在一個事務中放入佇列的命令,然後恢復正常的連線狀態。

如果使用了WATCH命令,那麼DISCARD命令就會將當前連線監控的所有鍵取消監控。

這個命令的執行格式如下所示:

DISCARD

這個命令的返回值是一個簡單的字串,總是OK

4. WATCH

當某個事務需要按條件執行時,就要使用這個命令將給定的鍵設定為受監控的

這個命令的執行格式如下所示:

WATCH key [key ...]

這個命令的返回值是一個簡單的字串,總是OK

對於每個鍵來說,時間複雜度總是O(1)

 

NOTE:

A、WATCH使得EXEC命令需要有條件的執行,也就是事務只能在所有被監視的鍵沒有被修改的前提下才能執行。另外,在EXEC被執行之後,所有的WATCH都會被取消。

 

5. UNWATCH

清除所有先前為一個事務監控的鍵。

如果你呼叫了EXECDISCARD命令,那麼就不需要手動呼叫UNWATCH命令。

這個命令的執行格式如下所示:

UNWATCH

這個命令的返回值是一個簡單的字串,總是OK

時間複雜度總是O(1)

二、使用方法及事務內部的錯誤示範

使用MULTI命令便可以進入一個Redis事務。這個命令的返回值總是OK。此時,使用者可以發出多個Redis命令。Redis會將這些命令放入佇列,而不是執行這些命令。一旦呼叫EXEC命令,那麼Redis就會執行事務中的所有命令。

Redis原生使用(Redis-cli):

127.0.0.1:6379> multi     // 事務開始的動作標誌下面即為入隊

OK

127.0.0.1:6379> set book-name "Thinking in Java"

QUEUED

127.0.0.1:6379> get book-name

QUEUED

127.0.0.1:6379> sadd tag "java""Programming""Thinking"

QUEUED

127.0.0.1:6379> smembers tag

QUEUED

127.0.0.1:6379> exec     // 執行事務

1) OK

2) "Thinking in Java"

3) (integer) 3

4) 1) "Thinking"

   2) "Programming"

   3) "java"

127.0.0.1:6379> discard  // 事務已執行完畢 已經自動取消

(error) ERR DISCARD without MULTI

127.0.0.1:6379> multi

OK

127.0.0.1:6379> set book-name "Patterns in Java"

QUEUED

127.0.0.1:6379> get book-name

QUEUED

127.0.0.1:6379> sadd tag "Java""Thinking""Programming"

QUEUED

127.0.0.1:6379> smembers tag

QUEUED

127.0.0.1:6379> discard  // 事務未執行 可以重新整理佇列指令狀態 取消執行

OK

127.0.0.1:6379> exec     // 事務已經被取消不能再執行

(error) ERR EXEC without MULTI

四、為什麼Redis不支援回滾?

如果你具備關係型資料庫的知識背景,你就會發現一個事實:在事務執行期間,雖然Redis命令可能會執行失敗,但是Redis仍然會執行事務中餘下的其他命令,而不會執行回滾操作,你可能會覺得這種行為很奇怪。

然而,這種行為也有其合理之處:

只有當被呼叫的Redis命令有語法錯誤時,這條命令才會執行失敗(在將這個命令放入事務佇列期間,Redis能夠發現此類問題),或者對某個鍵執行不符合其資料型別的操作:實際上,這就意味著只有程式錯誤才會導致Redis命令執行失敗,這種錯誤很有可能在程式開發期間發現,一般很少在生產環境發現。 (語法錯誤的意思吧)
       Redis已經在系統內部進行功能簡化,這樣可以確保更快的執行速度,因為Redis不需要事務回滾的能力。

對於Redis事務的這種行為,有一個普遍的反對觀點,那就是程式有可能會有缺陷(bug)。但是,你應當注意到:事務回滾並不能解決任何程式錯誤。例如,如果某個查詢會將一個鍵的值遞增2,而不是1,或者遞增錯誤的鍵,那麼事務回滾機制是沒有辦法解決這些程式問題的。請注意,沒有人能解決程式設計師自己的錯誤,這種錯誤可能會導致Redis命令執行失敗。正因為這些程式錯誤不大可能會進入生產環境,所以我們在開發Redis時選用更加簡單和快速的方法,沒有實現錯誤回滾的功能。

 

鑑於沒有任何機制能避免程式設計師自己造成的錯誤,並且這類錯誤通常不會在生產環境中出現,所以 Redis 選擇了更簡單、更快速的無回滾方式來處理事務。

五、丟棄命令佇列

DISCARD命令可以用來中止事務執行。在這種情況下,不會執行事務中的任何命令,並且會將Redis連線恢復為正常狀態。示例如下所示:

 

六、通過CAS操作實現樂觀鎖

1、樂觀鎖實現

舉個例子,假設我們需要原子性為某個鍵加1操作(假設INCR不存在),那麼應該是這樣的執行語句:

SET mykey 1

val = GET mykey

val = val + 1

SET mykey ${val}

 

單個客戶端訪問操作沒有任何問題,如果是多個客戶端同時訪問mykey,就會產生資源共享訪問問題,比如:現在有個兩個客戶端訪問同一個鍵mykey,那麼mykey的可能是2,但是我們期望的值應該是3才對,這個類似於高併發下的sync鎖機制,所以我們需要使用WATCH來監控被共享的鍵mykey,如下:

WATCH mykey(可監控多個鍵)

val = GET mykey

val = val + 1

MULTI

SET mykey ${val}

EXEC

 

NOTE:

雖然大多情況下,多個客戶端訪問操作同一個鍵的情況很少或沒有,但是不能排除這個特殊情況,所以建議在有可能產生鍵共享的指令中使用WATCH(有點類似JAVA中的synchronzied)在EXEC執行前對其監管。

 

2、Redis不支援回滾(Roll Back)

Redis的事務不支援回滾,這點不同於關係資料庫中的事務,所以它的內部保持了簡單且快速的特點。另外,Redis不支援回滾是這樣考慮的:Redis事務中命令之所以會失敗,是由於錯誤的程式設計所造成,通過事務回滾是不能迴避這個根本問題。

 

NOTE:

Redis事務中命令執行失敗,仍會繼續執行後面的執行,在沒有特殊干預前提下,直到執行完佇列中所有指令為止。

 

3、使用事務可能遇到的問題

A、事務在執行 EXEC 之前,入隊的命令可能會出錯,舉個例子:命令可能會產生語法錯誤(引數數量錯誤,引數名錯誤等),或者其他更嚴重的錯誤,比如記憶體不足(如果伺服器使用maxmemory 設定了最大記憶體限制的話)。

 

B、事務在執行 EXEC 之前,舉個例子:事務中的命令可能處理了錯誤型別的鍵,比如將列表命令用在了字串鍵上面等。

 

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

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

在 Redis 2.6.5 以前, Redis 只執行事務中那些入隊成功的命令,而忽略那些入隊失敗的命令。

而新的處理方式則使得在管道技術中包含事務變得簡單,因為傳送事務和讀取事務的回覆都只需要和伺服器進行一次通訊即可

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

七、WATCH命令詳解

那麼WATCH命令實際做了些什麼呢?

這個命令會使得EXEC命令在滿足某些條件時才會執行事務:

我們要求Redis只有在所有受監控的鍵都沒有被修改時,才會執行事務。(但是,相同的客戶端可能會在事務內部修改這些鍵,此時這個事務不會中止執行。內部修改的沒關係)否則,Redis根本就不會進入事務。(注意,如果你使用WATCH命令監控一個易失性的鍵,然後在你監控這個鍵之後,Redis再使這個鍵過期,那麼EXEC命令仍然可以正常工作。

WATCH命令可以被呼叫多次。簡單說來,所有的WATCH命令都會在被呼叫之時立刻對相應的鍵進行監控,直到EXEC命令被呼叫之時為止。你可以在單條的WATCH命令之中,使用任意數量的鍵作為命令引數。

當呼叫EXEC命令時,所有的鍵都會變為未受監控的狀態,Redis不會管事務是否被中止。當一個客戶單連線被關閉時,所有的鍵也都會變為未受監控的狀態。(就是呼叫EXEC前,鍵都是受到WATCH監控,呼叫後就自動釋放監控了)。

你還可以使用UNWATCH命令(不需要任何引數),這樣便能清除所有的受監控鍵。當我們對某些鍵施加樂觀鎖之後,這個命令有時會非常有用。因為,我們可能需要執行一個用來修改這些鍵的事務,但是在讀取這些鍵的當前內容之後,我們可能不打算繼續進行操作,此時便可以使用UNWATCH命令,清除所有受監控的鍵。在執行UNWATCH命令之後,Redis連線便可以再次自由地用於執行新事務。

如何使用WATCH命令實現ZPOP操作呢?

本文將通過一個示例,說明如何使用WATCH命令建立一個新的原子化操作(Redis並不原生支援這個原子化操作),此處會以實現ZPOP操作為例。這個命令會以一種原子化的方式,從一個有序集合中彈出分數最低的元素。以下原始碼是最簡單的實現方式:

WATCH zset

element = ZRANGEzset 0 0

MULTI

ZREM zset element

EXEC

如果偽碼中的EXEC命令執行失敗(例如,返回Null值),那麼我們只需要重複執行這個操作即可。

八、Redis指令碼和事務

根據定義,Redis指令碼也是事務型的。因此,你可以通過Redis事務實現的功能,同樣也可以通過Redis指令碼來實現,而且通常指令碼更簡單、更快速。

由於Redis2.6版本才開始引入指令碼特性,而事務特性是很久以前就已經存在的,所以目前的版本才有兩個看起來重複的特性。但是,我們不太可能在短時間內移除對事務特性的支援。因為,即使不用求助於Redis指令碼,使用者仍然能夠規避競爭狀態,這從語義上來看是適宜的。還有另一個更重要的原因,Redis事務特性的實現複雜度是最小的。

但是,在相當長的一段時間之內,我們不大可能看到整個使用者群體都只使用Redis指令碼。如果發生這種情況,那麼我們可能會廢棄,甚至最終移除Redis事務。

       將在下一章節和管道一起描述。