1. 程式人生 > >Redis——事務詳解

Redis——事務詳解

一.關係型資料庫中的事務

關係型資料庫有一個重要的特性就是事務,而事務有四個要素:1.原子性(atomicity)2.一致性(consistency)3.隔離性(isolation)4.永續性(durability)。簡稱ACID。我們可以這麼認為:關係型資料庫中的事務就是ACID,只有當一個完整的操作同時滿足原子性,一致性,隔離性和永續性時,那麼就可以說這個操作是在一個事務(事務的書面化的解釋是訪問並可能更新資料庫中各種資料項的一個程式執行單元)。現在我們已經知道了什麼是關係型資料庫中的事務,那麼事務的這個4要素具體又代表什麼意思呢?我下面給出一個簡要精煉的解釋

要素 定義
atomicity 整個事務中的所有操作,要麼全部完成,要麼全部不完成,不可能停滯在中間某個環節。事務在執行過程中發生錯誤,會被回滾(Rollback)到事務開始前的狀態,就像這個事務從來沒有執行過一樣
consistency 事務必須是使資料庫從一個一致性狀態變到另一個一致性狀態
isolation 一個事務的執行不能被其他事務干擾。即一個事務內部的操作及使用的資料對併發的其他事務是隔離的,併發執行的各個事務之間不能互相干擾
durability 指一個事務一旦提交,它對資料庫中資料的改變就應該是永久性的。接下來的其他操作或故障不應該對其有任何影響

原子性:同時執行4個操作,一個修改a的值,一個修改b的值,一個修改c的值,一個修改d的值。修改a,c,d的值都成功了,但是修改b的值的時候出錯了,那麼這4個操作都不會執行,同時會被回滾回來。

一致性:a有500塊錢,b有500塊錢,現在a轉給b200塊錢需要兩個動作,一個是從自己的賬號上扣200,另一個是在b的賬號上增兩百。不能a扣了200而b沒有增200或者不能a的200沒扣而b的200卻增了,這樣會導致總額從原來的1000變為800或者1200,這就是不具備一致性。而只有當一開始一共是1000,操作完之後總和還是1000時才具有一致性。

隔離性:有一個蘋果,我吃了一口放在了桌子上然後出去了,回來發現蘋果被吃了兩口,說明我走的這一段時間有其他人也來吃了一口。現在,我又吃了一口放在桌子上喊了一句:這個蘋果已經被隔離了大家都不能動了!然後又出去了。期間我弟過來看到了這個蘋果準備吃一口,但準備吃的時候發現這個蘋果已經被我隔離了,只有等我解除隔離才能吃到,所以只能在哪等我回來,等我回來之後,我又啃了兩口後已經吃膩了,於是喊了一句:解除隔離!於是在旁邊的弟弟便可以得到這個已經被解除隔離的蘋果,繼續吃了起來!這個就是隔離性。(好SB的栗子)

永續性:就是當事務成功提交後將操作的資料儲存到資料庫中,這麼理解就行了,沒那麼重要。

二.Redis中的事務

Redis中的事務(transaction)是一組命令的集合。包括MULTI, EXEC, DISCARD ,WATCH ,UNWATCH.當然,這些指令的作用就是實現一個類似於關係型資料庫的事務。

1.MULTI和EXEC

從1.2.0版本開始,redis引入了MULTI和EXEC指令,MULTI標誌著一個事務的開始,後面的命令暫時不會執行,而是會存到佇列中,等到 EXEC 執行之後,佇列中的命令才會依次序執行,下面給出案例:

[root@localhost redis-4.0.1]# ./src/redis-cli -p 10000 -a 123456
127.0.0.1:10000> multi
OK
127.0.0.1:10000> set liu1 1
QUEUED
127.0.0.1:10000> set liu2 2
QUEUED
127.0.0.1:10000> set liu3 3
QUEUED
127.0.0.1:10000> exec
1) OK
2) OK
3) OK
127.0.0.1:10000> get liu1
"1"
127.0.0.1:10000> get liu2
"2"
127.0.0.1:10000> get liu3
"3"

鍵入MULTI總是會返回OK,標示一個事務開始了,後面進來的指令並不會馬上執行,而是返回”QUEUED”,這表示命令已經被伺服器接受並且暫時儲存起來,最後輸入EXEC命令後,本次事務中的所有命令才會被依次執行,可以看到最後伺服器一次性返回了三個OK,這裡返回的結果與傳送的命令是按順序一一對應的,這說明這次事務中的命令全都執行成功了。

2.DISCARD

從2.0.0版本開始redis引入了DISCARD命令,其作用是重新整理事務中先前排隊的所有命令,並將連線狀態恢復正常。就是清除之前存在佇列中的所有指令,然後直接結束該事務。下面給出案例:

[root@localhost redis-4.0.1]# ./src/redis-cli -p 10000 -a 123456
127.0.0.1:10000> multi
OK
127.0.0.1:10000> set liu1 1
QUEUED
127.0.0.1:10000> set liu2 2
QUEUED
127.0.0.1:10000> discard
OK
127.0.0.1:10000> exec
(error) ERR EXEC without MULTI

可以看出當我們執行EXEC指令時報錯了:在沒有MULTI的前提下執行了EXEC命令。代表當前環境中已經沒有了事務,而這正是DISCARD做到的。

3.WATCH和UNWATCH

下面讓我看一下Redis事務的另一個重要成員WATCH(類似於樂觀鎖)

Redis官方:WATCH is used to provide a check-and-set (CAS) behavior to Redis transactions.

從2.2.0版本開始redis引入了WATCH和UNWATCH命令。WATCH命令可以監控一個或多個鍵,一旦其中有一個鍵被修改(或刪除),之後的事務就不會執行,監控一直持續到EXEC命令(事務中的命令是在EXEC之後才執行的,EXEC命令執行完之後被監控的鍵會自動被UNWATCH)。UNWATCH的作用是取消WATCH命令對多有key的監控,所有監控鎖將會被取消。
下面舉出網上對樂觀鎖總結的比較好的解釋:

樂觀鎖:就像他的名字,不會認為資料不會出錯,他不會為資料上鎖,但是為了保證資料的一致性,他會在每條記錄的後面新增一個標記(類似於版本號),假設A 獲取K1這條標記,得到了k1的版本號是1,並對其進行修改,這個時候B也獲取了k1這個資料,當然,B獲取的版本號也是1,同樣也對k1進行修改,這個時候,如果B先提交了,那麼k1的版本號將會改變成2,這個時候,如果A提交資料,他會發現自己的版本號與最新的版本號不一致,這個時候A的提交將不會成功,A的做法是重新獲取最新的k1的資料,重複修改資料、提交資料。
悲觀鎖:這個模式將認定資料一定會出錯,所以她的做法是將整張表鎖起來,這樣會有很強的一致性,但是同時會有極低的併發性(常用語資料庫備份工作,類似於表鎖)。

解釋永遠不足以說明,上詳細的案例:

案例一: 在事務開始後使用WATCH

127.0.0.1:10000> multi
OK
127.0.0.1:10000> watch a
(error) ERR WATCH inside MULTI is not allowed
//(錯誤) 不允許在事務內部使用WATCH
127.0.0.1:10000> set a a
QUEUED
127.0.0.1:10000> exec
1) OK

從上例可以看到Redis不允許在事務內部使用WATCH命令,會報錯,但是即使使用了WATCH也不會因為這個錯誤導致事務中止,事務照常執行。

案例二: 同一客戶端下,在WATCH之後MULTI之前改變被監視的key

127.0.0.1:10000> watch a
OK
127.0.0.1:10000> set a a
OK
127.0.0.1:10000> multi
OK
127.0.0.1:10000> set a b
QUEUED
127.0.0.1:10000> exec
(nil)
// 沒有命令被執行
127.0.0.1:10000> get a
"a"

可以看出在a被監視後,執行事務中set a b的命令之前a的值已經被改變(set a a),所以事務中的所有命令得不到執行(nil)。

案例三: 不同客戶端下,一個客戶端先監控一個鍵進入事務,然後另一個客戶端改變這個別監控鍵的值
(此處用圖片展示,騷年們不能複製貼上了)執行順序是圖一,圖二,圖三

圖一:左邊的客戶端先監控一個鍵進入事務,右邊的客戶端不做操作
圖一:一個客戶端先監控一個鍵進入事務,另一個客戶端不做操作

圖二:右邊的客戶端改變這個別監控鍵的值
圖二:另一個客戶端改變這個別監控鍵的值

圖三:左邊的客戶端執行事務exec
圖三:第一個客戶端執行事務exec
由於WATCH命令的作用只是當被監控的鍵被修改後取消之後的事務,並不能保證其他客戶端不修改監控的值,所以當EXEC命令執行失敗之後需要手動重新執行整個事務

案例四: 用WATCH實現Redis的命令INCR(這裡用虛擬碼)

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

案例五: 用WATCH實現Redis的命令ZPOP(這裡用虛擬碼)

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

那麼怎樣取消對鍵的監控呢?

1. WATCH 對 key 的監視從呼叫 WATCH 開始生效,直到呼叫 EXEC 為止。EXEC 被呼叫的時候不管事務是否執行,都會取消對 key 的監視。
2.另外當客戶端斷開連線後也會取消監視。
3.使用無引數的 UNWATCH 可以取消對所有 key 的監視。

三.Redis事務錯誤時的處理

Redis官網是是這樣說的:

During a transaction it is possible to encounter two kind of command errors:
1. A command may fail to be queued, so there may be an error before EXEC is called. For instance the command may be syntactically wrong (wrong number of arguments, wrong command name, …), or there may be some critical condition like an out of memory condition (if the server is configured to have a memory limit using the maxmemory directive).
2. A command may fail after EXEC is called, for instance since we performed an operation against a key with the wrong value (like calling a list operation against a string value).

譯文如下:

在事務中,可能會遇到兩種型別的命令錯誤
1. 命令可能無法排隊,因此在呼叫EXEC之前可能會出現錯誤。 例如,命令可能在語法上是錯誤的(錯誤的引數數量,錯誤的命令名稱,…),或者可能存在一些關鍵條件,如記憶體不足條件(如果伺服器配置為使用maxmemory指令具有記憶體限制)
2. 呼叫EXEC後,命令可能會失敗,例如因為我們對一個具有錯誤值的鍵進行了一個操作(比如針對一個字串呼叫一個列表操作)

博主對其進行一個劃分:

1. 入隊錯誤:命令不存在,命令格式不正確(例如引數錯誤)等。這種情況需要區分Redis的版本,Redis 2.6.5之前的版本會忽略錯誤的命令,執行其他正確的命令,2.6.5之後的版本會忽略這個事務中的所有命令,都不執行。博主用的Redis版本為redis-4.0.1,舉例如下:

127.0.0.1:10000> multi
OK
127.0.0.1:10000> get a
QUEUED
127.0.0.1:10000> get b
QUEUED
127.0.0.1:10000> get a a
(error) ERR wrong number of arguments for 'get' command
127.0.0.1:10000> exec
(error) EXECABORT Transaction discarded because of previous errors.
//(錯誤) 執行中止,由於之前的錯誤導致事務被放棄

2. 執行錯誤:執行錯誤表示命令在執行過程中出現錯誤,比如用GET命令獲取一個散列表型別的鍵值。這種錯誤在命令執行之前Redis是無法發現的,所以在事務裡這樣的命令會被Redis接受並執行。如果食物裡有一條命令執行錯誤,其他命令依舊會執行(包括出錯之後的命令)。值得注意的是執行錯誤的邏輯是不分版本的,舉例如下:

127.0.0.1:10000> multi
OK
127.0.0.1:10000> hset a a a
QUEUED
127.0.0.1:10000> get a
QUEUED
127.0.0.1:10000> exec
1) (error) WRONGTYPE Operation against a key holding the wrong kind of value
2) "a"
//(錯誤) 對持有錯誤型別的鍵的操作

注意: 當引數個數錯誤時是入隊錯誤還是執行錯誤呢?redis官方是這麼說的:A command may fail to be queued, so there may be an error before EXEC is called. For instance the command may be syntactically wrong (wrong number of arguments, wrong command name, …),他們把錯誤的引數個數歸類為入隊錯誤。那麼下面我們做個測試:

測試一:讓命令少一個引數

127.0.0.1:10000> multi
OK
127.0.0.1:10000> set a
(error) ERR wrong number of arguments for 'set' command     
127.0.0.1:10000> exec
(error) EXECABORT Transaction discarded because of previous errors.
//(錯誤) 執行中止,由於之前的錯誤導致事務被放棄

可以看出入隊時就報錯了,那麼這是一個入隊錯誤。

測試二:讓命令多幾個引數

127.0.0.1:10000> multi
OK
127.0.0.1:10000> set a a a a a a
QUEUED
127.0.0.1:10000> exec
1) (error) ERR syntax error
//(錯誤) 語法錯誤

返回QUEUED,入隊成功,執行錯誤。

那麼,引數個數的錯誤究竟是屬於哪一種錯誤呢?是官方文件錯了嗎?有獨到見解的騷年歡迎與博主私信交流:QQ354311909

四.Redis為什麼不支援回滾

Redis命令在事務中可能會失敗,但是Redis將執行事務中其餘的命令,而不是回滾。如果你有關係型資料庫的背景你肯定會對此感到奇怪!是的,redis不支援回滾!換一句話說,redis的事務在遇到博主上述所說的執行錯誤時是不具有原子性的(atomicity )。那麼redis為什麼不支援回滾呢?

官方是這樣解釋的:

1. Redis commands can fail only if called with a wrong syntax (and the problem is not detectable during the command queueing), or against keys holding the wrong data type: this means that in practical terms a failing command is the result of a programming errors, and a kind of error that is very likely to be detected during development, and not in production.
2. Redis is internally simplified and faster because it does not need the ability to roll back.

譯文如下:

1. 只有在使用錯誤的語法呼叫時,Redis命令才會失敗(在命令佇列中,問題是無法檢測到的),或者是針對持有錯誤資料型別的鍵:這意味著在實際操作中,失敗的命令是程式設計錯誤的結果。
2. 正是因為它不需要回滾的能力,所以Redis內部才如此簡化和更快。

簡單來說就是:執行時發生錯誤其實都是我們程式設計師的程式碼程式設計出現了BUG,跟他們Redis沒有半毛錢關係,只要語法對了那麼就不會出現異常。同時沒有rollback也使得redis更加優秀。那麼出現執行錯誤怎麼辦呢?捕獲異常處理唄。所以,我是同意redis的這種做法的。

五.總結

我們最開始跟各位騷年們提到了ACID,那麼Redis的事務具有ACID嗎?

1.原子性:當出現佇列錯誤時,2.6.5之後的版本會忽略這個事務中的所有命令,都不執行,所以此時redis是符合原子性的,但是當出現執行錯誤時由於redis沒有rollback機制,所以不具有一致性,需要我們自己動手處理異常,保證事務的原子性。
2.一致性:Redis 通過謹慎的錯誤檢測和簡單的設計保證事務的一致性。
3.隔離性:Redis的WATCH命令保證了事務的隔離性。
4.永續性:(用列表展示)

型別 是否具有永續性
無持久化機制 事務不具有永續性,伺服器停機重啟後資料丟失,故而不具有永續性
RDB 機制 在特定條件下才會儲存資料集快照,不能保證資料在第一時間被儲存在硬碟中,故而不具有永續性
AOF機制appendfsync = always 時 程式總會在執行命令之後呼叫同步函式,將命令資料存在硬碟中,這種情況下的事務具有永續性
AOF 機制appendfsync = everysec 程式會每秒同步一次資料到硬碟。因為停機可能就發生在命令執行完畢但是尚未同步的那一秒鐘內,這樣會造成事務資料丟失,故而不具有永續性
AOF 機制appendfsync = no 由作業系統決定何時將資料同步到硬碟。因為事務資料可能在等待同步的過程中丟失,這樣會造成事務資料丟失,故而不具有永續性

總結得出:Redis的事務原生具有一致性,需要我們通過程式保證原子性,通過WATCH指令保證隔離性,通過設定持久話機制為AOF機制appendfsync = always 保證永續性。所以,Redis最終可以做到ACID!

話外音: Redis官方表示事務遲早會被Redis指令碼替代,讓我們拭目以待

Redis scripting and transactions
A Redis script is transactional by definition, so everything you can do with a Redis transaction, you can also do with a script, and usually the script will be both simpler and faster.
This duplication is due to the fact that scripting was introduced in Redis 2.6 while transactions already existed long before. However we are unlikely to remove the support for transactions in the short time because it seems semantically opportune that even without resorting to Redis scripting it is still possible to avoid race conditions, especially since the implementation complexity of Redis transactions is minimal.
However it is not impossible that in a non immediate future we’ll see that the whole user base is just using scripts. If this happens we may deprecate and finally remove transactions.
譯文:
Redis指令碼和事務
Redis指令碼通過定義進行事務處理,所以您可以使用Redis事務處理所有事情,您還可以使用指令碼,通常指令碼會更簡單,更快。這種重複是由於在Redis 2.6中引入了指令碼,而事務早已存在。 然而,我們不太可能在短時間內消除對事務的支援,因為它似乎在語義上是適時的,即使不使用Redis指令碼,仍然有可能避免競爭條件,特別是因為Redis事務的實現複雜度很小。然而,在不久的將來,我們將看到整個使用者群只是使用指令碼,這並不是不可能的。 如果發生這種情況,我們可能會棄用並最終刪除事務。

剛剛我們提到了Redis的持久化機制,那麼下一章博主就和大家說一說Redis的持久化機制!博主將不定時頻繁更新部落格,歡迎互相交流!