天天看點

Redis---事務

和衆多其它資料庫一樣,redis作為nosql資料庫(不注重表的關系,跟關系型資料庫不同)也同樣提供了事務機制。在redis中,multi/exec/discard/watch這四個指令是我們實作事務的基石。相信對有關系型資料庫開發經驗的開發者而言這一概念并不陌生,即便如此,我們還是會簡要的列出redis中事務的實作特征:

1). 在事務中的所有指令都将會被串行化的順序執行,事務執行期間,redis不會再為其它用戶端的請求提供任何服務,進而保證了事物中的所有指令被原子的執行。

2). 和關系型資料庫中的事務相比,在redis事務中如果有某一條指令執行失敗,其後的指令仍然會被繼續執行。

3). 我們可以通過multi指令開啟一個事務,有關系型資料庫開發經驗的人可以将其了解為”begin transaction”語句。在該語句之後執行的指令都将被視為事務之内的操作,最後我們可以通過執行exec/discard指令來送出/復原該事務内的所有操作。這兩個redis指令可被視為等同于關系型資料庫中的commit/rollback語句。

4). 在事務開啟之前,如果用戶端與伺服器之間出現通訊故障并導緻網絡斷開,其後所有待執行的語句都将不會被伺服器執行。然而如果網絡中斷事件是發生在用戶端執行exec指令之後,那麼該事務中的所有指令都會被伺服器執行。

5). 當使用append-only模式時,redis會通過調用系統函數write将該事務内的所有寫操作在本次調用中全部寫入磁盤。然而如果在寫入的過程中出現系統崩潰,如電源故障導緻的當機,那麼此時也許隻有部分資料被寫入到磁盤,而另外一部分資料卻已經丢失。redis伺服器會在重新啟動時執行一系列必要的一緻性檢測,一旦發現類似問題,就會立即退出并給出相應的錯誤提示。此時,我們就要充分利用redis工具包中提供的redis-check-aof工具,該工具可以幫助我們定位到資料不一緻的錯誤,并将已經寫入的部分資料進行復原。修複之後我們就可以再次重新啟動redis伺服器了。

指令原型

時間複雜度

指令描述

傳回值

multi

用于标記事務的開始,其後執行的指令都将被存入指令隊列,直到執行exec時,這些指令才會被原子的執行。

始終傳回ok

exec

執行在一個事務内指令隊列中的所有指令,同時将目前連接配接的狀态恢複為正常狀态,即非事務狀态。如果在事務中執行了watch指令,那麼隻有當watch所監控的keys沒有被修改的前提下,exec指令才能執行事務隊列中的所有指令,否則exec将放棄目前事務中的所有指令。 原子性的傳回事務中各條指令的傳回結果。

如果在事務中使用了watch,一旦事務被放棄,exec将傳回null-multi-bulk回複。

discard

復原事務隊列中的所有指令,同時再将目前連接配接的狀态恢複為正常狀态,即非事務狀态。如果watch指令被使用,該指令将unwatch所有的keys。

始終傳回ok。

watch key [key …]

o(1)

在multi指令執行之前,可以指定待監控的keys,然而在執行exec之前,如果被監控的keys發生修改,exec将放棄執行該事務隊列中的所有指令。

unwatch

取消目前事務中指定監控的keys,如果執行了exec或discard指令,則無需再手工執行該指令了,因為在此之後,事務中所有被監控的keys都将自動取消。

這就示範了幾次事務,按道理說發生錯誤就得復原,但是如果是隐藏錯誤好像沒有復原,隻有簡單的文法錯誤才復原了,我覺得其實redis事務是一個雞肋,沒有什麼很大的用處。

Redis---事務

事務被正常執行:

在shell指令行下執行redis的用戶端工具。

/> redis-cli

在目前連接配接上啟動一個新的事務。

redis 127.0.0.1:6379> multi

ok

執行事務中的第一條指令,從該指令的傳回結果可以看出,該指令并沒有立即執行,而是存于事務的指令隊列。

redis 127.0.0.1:6379> incr t1

queued

又執行一個新的指令,從結果可以看出,該指令也被存于事務的指令隊列。

redis 127.0.0.1:6379> incr t2

執行事務指令隊列中的所有指令,從結果可以看出,隊列中指令的結果得到傳回。

redis 127.0.0.1:6379> exec

1) (integer) 1

2) (integer) 1

事務中存在失敗的指令:

開啟一個新的事務。

設定鍵a的值為string類型的3。

redis 127.0.0.1:6379> set a 3

從鍵a所關聯的值的頭部彈出元素,由于該值是字元串類型,而lpop指令僅能用于list類型,是以在執行exec指令時,該指令将會失敗。

redis 127.0.0.1:6379> lpop a

再次設定鍵a的值為字元串4。

redis 127.0.0.1:6379> set a 4

擷取鍵a的值,以便确認該值是否被事務中的第二個set指令設定成功。

redis 127.0.0.1:6379> get a

從結果中可以看出,事務中的第二條指令lpop執行失敗,而其後的set和get指令均執行成功,這一點是redis的事務與關系型資料庫中的事務之間最為重要的差别。

1) ok

2) (error) err operation against a key holding the wrong kind of value

3) ok

4) “4”

2.1 事務中存在文法錯誤的指令:

復原事務:

為鍵t2設定一個事務執行前的值。

redis 127.0.0.1:6379> set t2 tt

開啟一個事務。

在事務内為該鍵設定一個新值。

redis 127.0.0.1:6379> set t2 ttnew

放棄事務。

redis 127.0.0.1:6379> discard

檢視鍵t2的值,從結果中可以看出該鍵的值仍為事務開始之前的值。

redis 127.0.0.1:6379> get t2

“tt”

在redis的事務中,watch指令可用于提供cas(check-and-set)功能。假設我們通過watch指令在事務執行之前監控了多個keys,倘若在watch之後有任何key的值發生了變化,exec指令執行的事務都将被放棄,同時傳回null multi-bulk應答以通知調用者事務執行失敗。例如,我們再次假設redis中并未提供incr指令來完成鍵值的原子性遞增,如果要實作該功能,我們隻能自行編寫相應的代碼。其僞碼如下:

以上代碼隻有在單連接配接的情況下才可以保證執行結果是正确的,因為如果在同一時刻有多個用戶端在同時執行該段代碼,那麼就會出現多線程程式中經常出現的一種錯誤場景–競态争用(race condition)。比如,用戶端a和b都在同一時刻讀取了mykey的原有值,假設該值為10,此後兩個用戶端又均将該值加一後set回redis伺服器,這樣就會導緻mykey的結果為11,而不是我們認為的12。為了解決類似的問題,我們需要借助watch指令的幫助,見如下代碼:

和此前代碼不同的是,新代碼在擷取mykey的值之前先通過watch指令監控了該鍵,此後又将set指令包圍在事務中,這樣就可以有效的保證每個連接配接在執行exec之前,如果目前連接配接擷取的mykey的值被其它連接配接的用戶端修改,那麼目前連接配接的exec指令将執行失敗。這樣調用者在判斷傳回值後就可以獲悉val是否被重新設定成功。