Redis : redis事务
Redis的事務(wù)功能詳解
MULTI、EXEC、DISCARD和WATCH命令是Redis事務(wù)功能的基礎(chǔ)。
Redis事務(wù)允許在一次單獨(dú)的步驟中執(zhí)行一組命令,并且可以保證如下兩個(gè)重要事項(xiàng):
>Redis會(huì)將一個(gè)事務(wù)中的所有命令序列化,然后按順序執(zhí)行。Redis不可能在一個(gè)Redis事務(wù)的執(zhí)行過(guò)程中插入執(zhí)行另一個(gè)客戶端發(fā)出的請(qǐng)求。這樣便能保證Redis將這些命令作為一個(gè)單獨(dú)的隔離操作執(zhí)行。 > 在一個(gè)Redis事務(wù)中,Redis要么執(zhí)行其中的所有命令,要么什么都不執(zhí)行。因此,Redis事務(wù)能夠保證原子性。
EXEC命令會(huì)觸發(fā)執(zhí)行事務(wù)中的所有命令。因此,當(dāng)某個(gè)客戶端正在執(zhí)行一次事務(wù)時(shí),如果它在調(diào)用EXEC命令之前就從Redis服務(wù)端斷開(kāi)連接,那么就不會(huì)執(zhí)行事務(wù)中的任何操作;相反,如果它在調(diào)用EXEC命令之后才從Redis服務(wù)端斷開(kāi)連接,那么就會(huì)執(zhí)行事務(wù)中的所有操作。(有出錯(cuò)也不會(huì)停止了,后面解釋)
當(dāng)Redis使用只增文件(AOF:Append-only File)時(shí),Redis能夠確保使用一個(gè)單獨(dú)的write(2)系統(tǒng)調(diào)用,這樣便能將事務(wù)寫(xiě)入磁盤(pán)。然而,如果Redis服務(wù)器宕機(jī),或者系統(tǒng)管理員以某種方式停止Redis服務(wù)進(jìn)程的運(yùn)行,那么Redis很有可能只執(zhí)行了事務(wù)中的一部分操作。Redis將會(huì)在重新啟動(dòng)時(shí)檢查上述狀態(tài),然后退出運(yùn)行,并且輸出報(bào)錯(cuò)信息。使用redis-check-aof工具可以修復(fù)上述的只增文件,這個(gè)工具將會(huì)從上述文件中刪除執(zhí)行不完全的事務(wù),這樣Redis服務(wù)器才能再次啟動(dòng)。
從2.2版本開(kāi)始,除了上述兩項(xiàng)保證之外,Redis還能夠以樂(lè)觀鎖的形式提供更多的保證,這種形式非常類(lèi)似于“檢查再設(shè)置”(CAS:Check And Set)操作。本文稍后會(huì)對(duì)Redis的樂(lè)觀鎖進(jìn)行描述。
一、相關(guān)命令
1. MULTI
該命令用來(lái)開(kāi)啟事務(wù),它總是返回ok結(jié)果,當(dāng)其執(zhí)行之后,客戶端可以繼續(xù)發(fā)送任意條數(shù)量的指令,這些指令不會(huì)立即被執(zhí)行,而是被放到了隊(duì)列中,直到EXEC被調(diào)用之后,所有命令才會(huì)被序列化執(zhí)行。
2. EXEC
在一個(gè)事務(wù)中執(zhí)行所有先前放入隊(duì)列的命令,然后恢復(fù)正常的連接狀態(tài)。
當(dāng)使用WATCH命令時(shí),只有當(dāng)受監(jiān)控的鍵沒(méi)有被修改時(shí),EXEC命令才會(huì)執(zhí)行事務(wù)中的命令,這種方式利用了檢查再設(shè)置(CAS)的機(jī)制。
這個(gè)命令的運(yùn)行格式如下所示:
EXEC
這個(gè)命令的返回值是一個(gè)數(shù)組,其中的每個(gè)元素分別是原子化事務(wù)中的每個(gè)命令的返回值。當(dāng)使用WATCH命令時(shí),如果事務(wù)執(zhí)行中止,那么EXEC命令就會(huì)返回一個(gè)Null值。
MULTI開(kāi)啟之后,因?yàn)槟承┰驔](méi)有成功執(zhí)行EXEC,那么事務(wù)中所有的命令都不會(huì)被執(zhí)行的。
?
3. DISCARD
清除所有先前在一個(gè)事務(wù)中放入隊(duì)列的命令,然后恢復(fù)正常的連接狀態(tài)。
如果使用了WATCH命令,那么DISCARD命令就會(huì)將當(dāng)前連接監(jiān)控的所有鍵取消監(jiān)控。
這個(gè)命令的運(yùn)行格式如下所示:
DISCARD
這個(gè)命令的返回值是一個(gè)簡(jiǎn)單的字符串,總是OK。
4. WATCH
當(dāng)某個(gè)事務(wù)需要按條件執(zhí)行時(shí),就要使用這個(gè)命令將給定的鍵設(shè)置為受監(jiān)控的。
這個(gè)命令的運(yùn)行格式如下所示:
WATCH key [key ...]
這個(gè)命令的返回值是一個(gè)簡(jiǎn)單的字符串,總是OK。
對(duì)于每個(gè)鍵來(lái)說(shuō),時(shí)間復(fù)雜度總是O(1)。
?
NOTE:
A、WATCH使得EXEC命令需要有條件的執(zhí)行,也就是事務(wù)只能在所有被監(jiān)視的鍵沒(méi)有被修改的前提下才能執(zhí)行。另外,在EXEC被執(zhí)行之后,所有的WATCH都會(huì)被取消。
?
5. UNWATCH
清除所有先前為一個(gè)事務(wù)監(jiān)控的鍵。
如果你調(diào)用了EXEC或DISCARD命令,那么就不需要手動(dòng)調(diào)用UNWATCH命令。
這個(gè)命令的運(yùn)行格式如下所示:
UNWATCH
這個(gè)命令的返回值是一個(gè)簡(jiǎn)單的字符串,總是OK。
時(shí)間復(fù)雜度總是O(1)。
二、使用方法及事務(wù)內(nèi)部的錯(cuò)誤示范
使用MULTI命令便可以進(jìn)入一個(gè)Redis事務(wù)。這個(gè)命令的返回值總是OK。此時(shí),用戶可以發(fā)出多個(gè)Redis命令。Redis會(huì)將這些命令放入隊(duì)列,而不是執(zhí)行這些命令。一旦調(diào)用EXEC命令,那么Redis就會(huì)執(zhí)行事務(wù)中的所有命令。
Redis原生使用(Redis-cli):
127.0.0.1:6379> multi?? ??// 事務(wù)開(kāi)始的動(dòng)作標(biāo)志下面即為入隊(duì)
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?? ??// 執(zhí)行事務(wù)
1) OK
2) "Thinking in Java"
3) (integer) 3
4) 1) "Thinking"
?? 2) "Programming"
?? 3) "java"
127.0.0.1:6379> discard? // 事務(wù)已執(zhí)行完畢 已經(jīng)自動(dòng)取消
(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? // 事務(wù)未執(zhí)行 可以刷新隊(duì)列指令狀態(tài) 取消執(zhí)行
OK
127.0.0.1:6379> exec? ???// 事務(wù)已經(jīng)被取消不能再執(zhí)行
(error) ERR EXEC without MULTI
四、為什么Redis不支持回滾?
如果你具備關(guān)系型數(shù)據(jù)庫(kù)的知識(shí)背景,你就會(huì)發(fā)現(xiàn)一個(gè)事實(shí):在事務(wù)運(yùn)行期間,雖然Redis命令可能會(huì)執(zhí)行失敗,但是Redis仍然會(huì)執(zhí)行事務(wù)中余下的其他命令,而不會(huì)執(zhí)行回滾操作,你可能會(huì)覺(jué)得這種行為很奇怪。
然而,這種行為也有其合理之處:
只有當(dāng)被調(diào)用的Redis命令有語(yǔ)法錯(cuò)誤時(shí),這條命令才會(huì)執(zhí)行失敗(在將這個(gè)命令放入事務(wù)隊(duì)列期間,Redis能夠發(fā)現(xiàn)此類(lèi)問(wèn)題),或者對(duì)某個(gè)鍵執(zhí)行不符合其數(shù)據(jù)類(lèi)型的操作:實(shí)際上,這就意味著只有程序錯(cuò)誤才會(huì)導(dǎo)致Redis命令執(zhí)行失敗,這種錯(cuò)誤很有可能在程序開(kāi)發(fā)期間發(fā)現(xiàn),一般很少在生產(chǎn)環(huán)境發(fā)現(xiàn)。?(語(yǔ)法錯(cuò)誤的意思吧)
?????? Redis已經(jīng)在系統(tǒng)內(nèi)部進(jìn)行功能簡(jiǎn)化,這樣可以確保更快的運(yùn)行速度,因?yàn)?/span>Redis不需要事務(wù)回滾的能力。
對(duì)于Redis事務(wù)的這種行為,有一個(gè)普遍的反對(duì)觀點(diǎn),那就是程序有可能會(huì)有缺陷(bug)。但是,你應(yīng)當(dāng)注意到:事務(wù)回滾并不能解決任何程序錯(cuò)誤。例如,如果某個(gè)查詢會(huì)將一個(gè)鍵的值遞增2,而不是1,或者遞增錯(cuò)誤的鍵,那么事務(wù)回滾機(jī)制是沒(méi)有辦法解決這些程序問(wèn)題的。請(qǐng)注意,沒(méi)有人能解決程序員自己的錯(cuò)誤,這種錯(cuò)誤可能會(huì)導(dǎo)致Redis命令執(zhí)行失敗。正因?yàn)檫@些程序錯(cuò)誤不大可能會(huì)進(jìn)入生產(chǎn)環(huán)境,所以我們?cè)陂_(kāi)發(fā)Redis時(shí)選用更加簡(jiǎn)單和快速的方法,沒(méi)有實(shí)現(xiàn)錯(cuò)誤回滾的功能。
?
鑒于沒(méi)有任何機(jī)制能避免程序員自己造成的錯(cuò)誤,并且這類(lèi)錯(cuò)誤通常不會(huì)在生產(chǎn)環(huán)境中出現(xiàn),所以 Redis 選擇了更簡(jiǎn)單、更快速的無(wú)回滾方式來(lái)處理事務(wù)。
五、丟棄命令隊(duì)列
DISCARD命令可以用來(lái)中止事務(wù)運(yùn)行。在這種情況下,不會(huì)執(zhí)行事務(wù)中的任何命令,并且會(huì)將Redis連接恢復(fù)為正常狀態(tài)。示例如下所示:
?
六、通過(guò)CAS操作實(shí)現(xiàn)樂(lè)觀鎖
1、樂(lè)觀鎖實(shí)現(xiàn)
舉個(gè)例子,假設(shè)我們需要原子性為某個(gè)鍵加1操作(假設(shè)INCR不存在),那么應(yīng)該是這樣的執(zhí)行語(yǔ)句:
SET mykey 1
val = GET mykey
val = val + 1
SET mykey ${val}
?
單個(gè)客戶端訪問(wèn)操作沒(méi)有任何問(wèn)題,如果是多個(gè)客戶端同時(shí)訪問(wèn)mykey,就會(huì)產(chǎn)生資源共享訪問(wèn)問(wèn)題,比如:現(xiàn)在有個(gè)兩個(gè)客戶端訪問(wèn)同一個(gè)鍵mykey,那么mykey的可能是2,但是我們期望的值應(yīng)該是3才對(duì),這個(gè)類(lèi)似于高并發(fā)下的sync鎖機(jī)制,所以我們需要使用WATCH來(lái)監(jiān)控被共享的鍵mykey,如下:
WATCH mykey(可監(jiān)控多個(gè)鍵)
val = GET mykey
val = val + 1
MULTI
SET mykey ${val}
EXEC
?
NOTE:
雖然大多情況下,多個(gè)客戶端訪問(wèn)操作同一個(gè)鍵的情況很少或沒(méi)有,但是不能排除這個(gè)特殊情況,所以建議在有可能產(chǎn)生鍵共享的指令中使用WATCH(有點(diǎn)類(lèi)似JAVA中的synchronzied)在EXEC執(zhí)行前對(duì)其監(jiān)管。
?
2、Redis不支持回滾(Roll Back)
Redis的事務(wù)不支持回滾,這點(diǎn)不同于關(guān)系數(shù)據(jù)庫(kù)中的事務(wù),所以它的內(nèi)部保持了簡(jiǎn)單且快速的特點(diǎn)。另外,Redis不支持回滾是這樣考慮的:Redis事務(wù)中命令之所以會(huì)失敗,是由于錯(cuò)誤的編程所造成,通過(guò)事務(wù)回滾是不能回避這個(gè)根本問(wèn)題。
?
NOTE:
Redis事務(wù)中命令執(zhí)行失敗,仍會(huì)繼續(xù)執(zhí)行后面的執(zhí)行,在沒(méi)有特殊干預(yù)前提下,直到執(zhí)行完隊(duì)列中所有指令為止。
?
3、使用事務(wù)可能遇到的問(wèn)題
A、事務(wù)在執(zhí)行?EXEC?之前,入隊(duì)的命令可能會(huì)出錯(cuò),舉個(gè)例子:命令可能會(huì)產(chǎn)生語(yǔ)法錯(cuò)誤(參數(shù)數(shù)量錯(cuò)誤,參數(shù)名錯(cuò)誤等),或者其他更嚴(yán)重的錯(cuò)誤,比如內(nèi)存不足(如果服務(wù)器使用maxmemory 設(shè)置了最大內(nèi)存限制的話)。
?
B、事務(wù)在執(zhí)行?EXEC?之前,舉個(gè)例子:事務(wù)中的命令可能處理了錯(cuò)誤類(lèi)型的鍵,比如將列表命令用在了字符串鍵上面等。
?
對(duì)于發(fā)生在?EXEC?執(zhí)行之前的錯(cuò)誤,客戶端以前的做法是檢查命令入隊(duì)所得的返回值:如果命令入隊(duì)時(shí)返回QUEUED ,那么入隊(duì)成功;否則,就是入隊(duì)失敗。如果有命令在入隊(duì)時(shí)失敗,那么大部分客戶端都會(huì)停止并取消這個(gè)事務(wù)。?
從 Redis 2.6.5 開(kāi)始,服務(wù)器會(huì)對(duì)命令入隊(duì)失敗的情況進(jìn)行記錄,并在客戶端調(diào)用?EXEC?命令時(shí),拒絕執(zhí)行并自動(dòng)放棄這個(gè)事務(wù)。
在 Redis 2.6.5 以前, Redis 只執(zhí)行事務(wù)中那些入隊(duì)成功的命令,而忽略那些入隊(duì)失敗的命令。
而新的處理方式則使得在管道技術(shù)中包含事務(wù)變得簡(jiǎn)單,因?yàn)榘l(fā)送事務(wù)和讀取事務(wù)的回復(fù)都只需要和服務(wù)器進(jìn)行一次通訊即可。
?至于那些在?EXEC?命令執(zhí)行之后所產(chǎn)生的錯(cuò)誤,并沒(méi)有對(duì)它們進(jìn)行特別處理:即使事務(wù)中有某個(gè)/某些命令在執(zhí)行時(shí)產(chǎn)生了錯(cuò)誤, 事務(wù)中的其他命令仍然會(huì)繼續(xù)執(zhí)行。
七、WATCH命令詳解
那么WATCH命令實(shí)際做了些什么呢?
這個(gè)命令會(huì)使得EXEC命令在滿足某些條件時(shí)才會(huì)運(yùn)行事務(wù):
我們要求Redis只有在所有受監(jiān)控的鍵都沒(méi)有被修改時(shí),才會(huì)執(zhí)行事務(wù)。(但是,相同的客戶端可能會(huì)在事務(wù)內(nèi)部修改這些鍵,此時(shí)這個(gè)事務(wù)不會(huì)中止運(yùn)行。內(nèi)部修改的沒(méi)關(guān)系)否則,Redis根本就不會(huì)進(jìn)入事務(wù)。(注意,如果你使用WATCH命令監(jiān)控一個(gè)易失性的鍵,然后在你監(jiān)控這個(gè)鍵之后,Redis再使這個(gè)鍵過(guò)期,那么EXEC命令仍然可以正常工作。)
WATCH命令可以被調(diào)用多次。簡(jiǎn)單說(shuō)來(lái),所有的WATCH命令都會(huì)在被調(diào)用之時(shí)立刻對(duì)相應(yīng)的鍵進(jìn)行監(jiān)控,直到EXEC命令被調(diào)用之時(shí)為止。你可以在單條的WATCH命令之中,使用任意數(shù)量的鍵作為命令參數(shù)。
當(dāng)調(diào)用EXEC命令時(shí),所有的鍵都會(huì)變?yōu)槲词鼙O(jiān)控的狀態(tài),Redis不會(huì)管事務(wù)是否被中止。當(dāng)一個(gè)客戶單連接被關(guān)閉時(shí),所有的鍵也都會(huì)變?yōu)槲词鼙O(jiān)控的狀態(tài)。(就是調(diào)用EXEC前,鍵都是受到WATCH監(jiān)控,調(diào)用后就自動(dòng)釋放監(jiān)控了)。
你還可以使用UNWATCH命令(不需要任何參數(shù)),這樣便能清除所有的受監(jiān)控鍵。當(dāng)我們對(duì)某些鍵施加樂(lè)觀鎖之后,這個(gè)命令有時(shí)會(huì)非常有用。因?yàn)?#xff0c;我們可能需要運(yùn)行一個(gè)用來(lái)修改這些鍵的事務(wù),但是在讀取這些鍵的當(dāng)前內(nèi)容之后,我們可能不打算繼續(xù)進(jìn)行操作,此時(shí)便可以使用UNWATCH命令,清除所有受監(jiān)控的鍵。在運(yùn)行UNWATCH命令之后,Redis連接便可以再次自由地用于運(yùn)行新事務(wù)。
如何使用WATCH命令實(shí)現(xiàn)ZPOP操作呢?
本文將通過(guò)一個(gè)示例,說(shuō)明如何使用WATCH命令創(chuàng)建一個(gè)新的原子化操作(Redis并不原生支持這個(gè)原子化操作),此處會(huì)以實(shí)現(xiàn)ZPOP操作為例。這個(gè)命令會(huì)以一種原子化的方式,從一個(gè)有序集合中彈出分?jǐn)?shù)最低的元素。以下源碼是最簡(jiǎn)單的實(shí)現(xiàn)方式:
WATCH zset
element = ZRANGEzset 0 0
MULTI
ZREM zset element
EXEC
如果偽碼中的EXEC命令執(zhí)行失敗(例如,返回Null值),那么我們只需要重復(fù)運(yùn)行這個(gè)操作即可。
八、Redis腳本和事務(wù)
根據(jù)定義,Redis腳本也是事務(wù)型的。因此,你可以通過(guò)Redis事務(wù)實(shí)現(xiàn)的功能,同樣也可以通過(guò)Redis腳本來(lái)實(shí)現(xiàn),而且通常腳本更簡(jiǎn)單、更快速。
由于Redis從2.6版本才開(kāi)始引入腳本特性,而事務(wù)特性是很久以前就已經(jīng)存在的,所以目前的版本才有兩個(gè)看起來(lái)重復(fù)的特性。但是,我們不太可能在短時(shí)間內(nèi)移除對(duì)事務(wù)特性的支持。因?yàn)?#xff0c;即使不用求助于Redis腳本,用戶仍然能夠規(guī)避競(jìng)爭(zhēng)狀態(tài),這從語(yǔ)義上來(lái)看是適宜的。還有另一個(gè)更重要的原因,Redis事務(wù)特性的實(shí)現(xiàn)復(fù)雜度是最小的。
但是,在相當(dāng)長(zhǎng)的一段時(shí)間之內(nèi),我們不大可能看到整個(gè)用戶群體都只使用Redis腳本。如果發(fā)生這種情況,那么我們可能會(huì)廢棄,甚至最終移除Redis事務(wù)。
?????? 腳本將在下一章節(jié)和管道一起描述。
總結(jié)
以上是生活随笔為你收集整理的Redis : redis事务的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: Redis:master/slave、s
- 下一篇: Redis:事务、管道、Lua脚本