redis setnx原子性_不支持原子性的 Redis 事务也叫事务吗?
假設(shè)現(xiàn)在有這樣一個(gè)業(yè)務(wù),用戶獲取的某些數(shù)據(jù)來自第三方接口信息,為避免頻繁請(qǐng)求第三方接口,我們往往會(huì)加一層緩存,緩存肯定要有時(shí)效性,假設(shè)我們要存儲(chǔ)的結(jié)構(gòu)是 hash(沒有String的'SET anotherkey "will expire in a minute" EX 60'這種原子操作),我們既要批量去放入緩存,又要保證每個(gè) key 都加上過期時(shí)間(以防 key 永不過期),這時(shí)候事務(wù)操作是個(gè)比較好的選擇
為了確保連續(xù)多個(gè)操作的原子性,我們常用的數(shù)據(jù)庫(kù)都會(huì)有事務(wù)的支持,Redis 也不例外。但它又和關(guān)系型數(shù)據(jù)庫(kù)不太一樣。
每個(gè)事務(wù)的操作都有 begin、commit 和 rollback,begin 指示事務(wù)的開始,commit 指示事務(wù)的提交,rollback 指示事務(wù)的回滾。它大致的形式如下
begin(); try {command1();command2();....commit(); } catch(Exception e) {rollback(); }Redis 在形式上看起來也差不多,分為三個(gè)階段
上面的指令演示了一個(gè)完整的事務(wù)過程,所有的指令在 exec 之前不執(zhí)行,而是緩存在服務(wù)器的一個(gè)事務(wù)隊(duì)列中,服務(wù)器一旦收到 exec 指令,才開執(zhí)行整個(gè)事務(wù)隊(duì)列,執(zhí)行完畢后一次性返回所有指令的運(yùn)行結(jié)果。
Redis 事務(wù)可以一次執(zhí)行多個(gè)命令,本質(zhì)是一組命令的集合。一個(gè)事務(wù)中的所有命令都會(huì)序列化,按順序地串行化執(zhí)行而不會(huì)被其它命令插入,不許加塞。
可以保證一個(gè)隊(duì)列中,一次性、順序性、排他性的執(zhí)行一系列命令(Redis 事務(wù)的主要作用其實(shí)就是串聯(lián)多個(gè)命令防止別的命令插隊(duì))
官方文檔是這么說的
事務(wù)可以一次執(zhí)行多個(gè)命令, 并且?guī)в幸韵聝蓚€(gè)重要的保證:- 事務(wù)是一個(gè)單獨(dú)的隔離操作:事務(wù)中的所有命令都會(huì)序列化、按順序地執(zhí)行。事務(wù)在執(zhí)行的過程中,不會(huì)被其他客戶端發(fā)送來的命令請(qǐng)求所打斷。
- 事務(wù)是一個(gè)原子操作:事務(wù)中的命令要么全部被執(zhí)行,要么全部都不執(zhí)行
這個(gè)原子操作,和關(guān)系型 DB 的原子性不太一樣,它不能完全保證原子性,后邊會(huì)介紹。
Redis 事務(wù)的幾個(gè)命令
MULTI 命令用于開啟一個(gè)事務(wù),它總是返回 OK 。
MULTI 執(zhí)行之后, 客戶端可以繼續(xù)向服務(wù)器發(fā)送任意多條命令, 這些命令不會(huì)立即被執(zhí)行, 而是被放到一個(gè)隊(duì)列中, 當(dāng) EXEC 命令被調(diào)用時(shí), 所有隊(duì)列中的命令才會(huì)被執(zhí)行。
另一方面, 通過調(diào)用 DISCARD , 客戶端可以清空事務(wù)隊(duì)列, 并放棄執(zhí)行事務(wù)。
廢話不多說,直接操作起來看結(jié)果更好理解~
一帆風(fēng)順
正常執(zhí)行(可以批處理,挺爽,每條操作成功的話都會(huì)各取所需,互不影響)
放棄事務(wù)(discard 操作表示放棄事務(wù),之前的操作都不算數(shù))
思考個(gè)問題:假設(shè)我們有個(gè)有過期時(shí)間的 key,在事務(wù)操作中 key 失效了,那執(zhí)行 exec 的時(shí)候會(huì)成功嗎?
事務(wù)中的錯(cuò)誤
上邊規(guī)規(guī)矩矩的操作,看著還挺好,可是事務(wù)是為解決數(shù)據(jù)安全操作提出的,我們用 Redis 事務(wù)的時(shí)候,可能會(huì)遇上以下兩種錯(cuò)誤:
- 事務(wù)在執(zhí)行 EXEC 之前,入隊(duì)的命令可能會(huì)出錯(cuò)。比如說,命令可能會(huì)產(chǎn)生語(yǔ)法錯(cuò)誤(參數(shù)數(shù)量錯(cuò)誤,參數(shù)名錯(cuò)誤等等),或者其他更嚴(yán)重的錯(cuò)誤,比如內(nèi)存不足(如果服務(wù)器使用 maxmemory 設(shè)置了最大內(nèi)存限制的話)。
- 命令可能在 EXEC 調(diào)用之后失敗。舉個(gè)例子,事務(wù)中的命令可能處理了錯(cuò)誤類型的鍵,比如將列表命令用在了字符串鍵上面,諸如此類。
Redis 針對(duì)如上兩種錯(cuò)誤采用了不同的處理策略,對(duì)于發(fā)生在 EXEC 執(zhí)行之前的錯(cuò)誤,服務(wù)器會(huì)對(duì)命令入隊(duì)失敗的情況進(jìn)行記錄,并在客戶端調(diào)用 EXEC 命令時(shí),拒絕執(zhí)行并自動(dòng)放棄這個(gè)事務(wù)(Redis 2.6.5 之前的做法是檢查命令入隊(duì)所得的返回值:如果命令入隊(duì)時(shí)返回 QUEUED ,那么入隊(duì)成功;否則,就是入隊(duì)失敗)
對(duì)于那些在 EXEC 命令執(zhí)行之后所產(chǎn)生的錯(cuò)誤, 并沒有對(duì)它們進(jìn)行特別處理: 即使事務(wù)中有某個(gè)/某些命令在執(zhí)行時(shí)產(chǎn)生了錯(cuò)誤, 事務(wù)中的其他命令仍然會(huì)繼續(xù)執(zhí)行。
全體連坐(某一條操作記錄報(bào)錯(cuò)的話,exec 后所有操作都不會(huì)成功)
冤頭債主(示例中 k1 被設(shè)置為 String 類型,decr k1 可以放入操作隊(duì)列中,因?yàn)橹挥性趫?zhí)行的時(shí)候才可以判斷出語(yǔ)句錯(cuò)誤,其他正確的會(huì)被正常執(zhí)行)
為什么 Redis 不支持回滾
如果你有使用關(guān)系式數(shù)據(jù)庫(kù)的經(jīng)驗(yàn),那么 “Redis 在事務(wù)失敗時(shí)不進(jìn)行回滾,而是繼續(xù)執(zhí)行余下的命令”這種做法可能會(huì)讓你覺得有點(diǎn)奇怪。
以下是官方的自夸:
- Redis 命令只會(huì)因?yàn)殄e(cuò)誤的語(yǔ)法而失敗(并且這些問題不能在入隊(duì)時(shí)發(fā)現(xiàn)),或是命令用在了錯(cuò)誤類型的鍵上面:這也就是說,從實(shí)用性的角度來說,失敗的命令是由編程錯(cuò)誤造成的,而這些錯(cuò)誤應(yīng)該在開發(fā)的過程中被發(fā)現(xiàn),而不應(yīng)該出現(xiàn)在生產(chǎn)環(huán)境中。
- 因?yàn)椴恍枰獙?duì)回滾進(jìn)行支持,所以 Redis 的內(nèi)部可以保持簡(jiǎn)單且快速。
有種觀點(diǎn)認(rèn)為 Redis 處理事務(wù)的做法會(huì)產(chǎn)生 bug , 然而需要注意的是, 在通常情況下, 回滾并不能解決編程錯(cuò)誤帶來的問題。 舉個(gè)例子, 如果你本來想通過 INCR 命令將鍵的值加上 1 , 卻不小心加上了 2 , 又或者對(duì)錯(cuò)誤類型的鍵執(zhí)行了 INCR , 回滾是沒有辦法處理這些情況的。
鑒于沒有任何機(jī)制能避免程序員自己造成的錯(cuò)誤, 并且這類錯(cuò)誤通常不會(huì)在生產(chǎn)環(huán)境中出現(xiàn), 所以 Redis 選擇了更簡(jiǎn)單、更快速的無回滾方式來處理事務(wù)。
帶 Watch 的事務(wù)
WATCH 命令用于在事務(wù)開始之前監(jiān)視任意數(shù)量的鍵: 當(dāng)調(diào)用 EXEC 命令執(zhí)行事務(wù)時(shí), 如果任意一個(gè)被監(jiān)視的鍵已經(jīng)被其他客戶端修改了, 那么整個(gè)事務(wù)將被打斷,不再執(zhí)行, 直接返回失敗。
WATCH命令可以被調(diào)用多次。 對(duì)鍵的監(jiān)視從 WATCH 執(zhí)行之后開始生效, 直到調(diào)用 EXEC 為止。
用戶還可以在單個(gè) WATCH 命令中監(jiān)視任意多個(gè)鍵, 就像這樣:
redis> WATCH key1 key2 key3 OK當(dāng) EXEC 被調(diào)用時(shí), 不管事務(wù)是否成功執(zhí)行, 對(duì)所有鍵的監(jiān)視都會(huì)被取消。另外, 當(dāng)客戶端斷開連接時(shí), 該客戶端對(duì)鍵的監(jiān)視也會(huì)被取消。
我們看個(gè)簡(jiǎn)單的例子,用 watch 監(jiān)控我的賬號(hào)余額(一周100零花錢的我),正常消費(fèi)
但這個(gè)卡,還綁定了我媳婦的支付寶,如果在我消費(fèi)的時(shí)候,她也消費(fèi)了,會(huì)怎么樣呢?
犯困的我去樓下 711 買了包煙,買了瓶水,這時(shí)候我媳婦在超市直接刷了 100,此時(shí)余額不足的我還在挑口香糖來著,,,
這時(shí)候我去結(jié)賬,發(fā)現(xiàn)刷卡失敗(事務(wù)中斷),尷尬的一批
你可能沒看明白 watch 有啥用,我們?cè)賮砜聪?#xff0c;如果還是同樣的場(chǎng)景,我們沒有 watch balance ,事務(wù)不會(huì)失敗,儲(chǔ)蓄卡成負(fù)數(shù),是不不太符合業(yè)務(wù)呢
使用無參數(shù)的 UNWATCH 命令可以手動(dòng)取消對(duì)所有鍵的監(jiān)視。 對(duì)于一些需要改動(dòng)多個(gè)鍵的事務(wù),有時(shí)候程序需要同時(shí)對(duì)多個(gè)鍵進(jìn)行加鎖, 然后檢查這些鍵的當(dāng)前值是否符合程序的要求。 當(dāng)值達(dá)不到要求時(shí), 就可以使用 UNWATCH 命令來取消目前對(duì)鍵的監(jiān)視, 中途放棄這個(gè)事務(wù), 并等待事務(wù)的下次嘗試。
watch指令,類似樂觀鎖,事務(wù)提交時(shí),如果 key 的值已被別的客戶端改變,比如某個(gè) list 已被別的客戶端push/pop 過了,整個(gè)事務(wù)隊(duì)列都不會(huì)被執(zhí)行。(當(dāng)然也可以用 Redis 實(shí)現(xiàn)分布式鎖來保證安全性,屬于悲觀鎖)
通過 watch 命令在事務(wù)執(zhí)行之前監(jiān)控了多個(gè) keys,倘若在 watch 之后有任何 key 的值發(fā)生變化,exec 命令執(zhí)行的事務(wù)都將被放棄,同時(shí)返回 Null 應(yīng)答以通知調(diào)用者事務(wù)執(zhí)行失敗。
悲觀鎖悲觀鎖(Pessimistic Lock),顧名思義,就是很悲觀,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人會(huì)修改,所以每次在拿數(shù)據(jù)的時(shí)候都會(huì)上鎖,這樣別人想拿這個(gè)數(shù)據(jù)就會(huì) block 直到它拿到鎖。傳統(tǒng)的關(guān)系型數(shù)據(jù)庫(kù)里邊就用到了很多這種鎖機(jī)制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖樂觀鎖
樂觀鎖(Optimistic Lock),顧名思義,就是很樂觀,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人不會(huì)修改,所以不會(huì)上鎖,但是在更新的時(shí)候會(huì)判斷一下在此期間別人有沒有去更新這個(gè)數(shù)據(jù),可以使用版本號(hào)等機(jī)制。樂觀鎖適用于多讀的應(yīng)用類型,這樣可以提高吞吐量。樂觀鎖策略:提交版本必須大于記錄當(dāng)前版本才能執(zhí)行更新
WATCH 命令的實(shí)現(xiàn)原理[1]
在代表數(shù)據(jù)庫(kù)的 server.h/redisDb 結(jié)構(gòu)類型中, 都保存了一個(gè) watched_keys 字典, 字典的鍵是這個(gè)數(shù)據(jù)庫(kù)被監(jiān)視的鍵, 而字典的值是一個(gè)鏈表, 鏈表中保存了所有監(jiān)視這個(gè)鍵的客戶端,如下圖。
typedef struct redisDb {dict *dict; /* The keyspace for this DB */dict *expires; /* Timeout of keys with a timeout set */dict *blocking_keys; /* Keys with clients waiting for data (BLPOP)*/dict *ready_keys; /* Blocked keys that received a PUSH */dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */int id; /* Database ID */long long avg_ttl; /* Average TTL, just for stats */unsigned long expires_cursor; /* Cursor of the active expire cycle. */list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */ } redisDb;list *watched_keys; /* Keys WATCHED for MULTI/EXEC CAS */WATCH 命令的作用, 就是將當(dāng)前客戶端和要監(jiān)視的鍵在 watched_keys 中進(jìn)行關(guān)聯(lián)。
舉個(gè)例子, 如果當(dāng)前客戶端為 client99 , 那么當(dāng)客戶端執(zhí)行 WATCH key2 key3 時(shí), 前面展示的 watched_keys 將被修改成這個(gè)樣子:
通過 watched_keys 字典, 如果程序想檢查某個(gè)鍵是否被監(jiān)視, 那么它只要檢查字典中是否存在這個(gè)鍵即可; 如果程序要獲取監(jiān)視某個(gè)鍵的所有客戶端, 那么只要取出鍵的值(一個(gè)鏈表), 然后對(duì)鏈表進(jìn)行遍歷即可。
在任何對(duì)數(shù)據(jù)庫(kù)鍵空間(key space)進(jìn)行修改的命令成功執(zhí)行之后 (比如 FLUSHDB、SET 、DEL、LPUSH、 SADD,諸如此類), multi.c/touchWatchedKey 函數(shù)都會(huì)被調(diào)用 —— 它會(huì)去 watched_keys 字典, 看是否有客戶端在監(jiān)視已經(jīng)被命令修改的鍵, 如果有的話, 程序?qū)⑺斜O(jiān)視這個(gè)/這些被修改鍵的客戶端的 REDIS_DIRTY_CAS 選項(xiàng)打開:
void multiCommand(client *c) {// 不能在事務(wù)中嵌套事務(wù)if (c->flags & CLIENT_MULTI) {addReplyError(c,"MULTI calls can not be nested");return;}// 打開事務(wù) FLAGc->flags |= CLIENT_MULTI;addReply(c,shared.ok); }/* "Touch" a key, so that if this key is being WATCHed by some client the* next EXEC will fail. */ void touchWatchedKey(redisDb *db, robj *key) {list *clients;listIter li;listNode *ln;// 字典為空,沒有任何鍵被監(jiān)視if (dictSize(db->watched_keys) == 0) return;// 獲取所有監(jiān)視這個(gè)鍵的客戶端clients = dictFetchValue(db->watched_keys, key);if (!clients) return;// 遍歷所有客戶端,打開他們的 CLIENT_DIRTY_CAS 標(biāo)識(shí)listRewind(clients,&li);while((ln = listNext(&li))) {client *c = listNodeValue(ln);c->flags |= CLIENT_DIRTY_CAS;} }當(dāng)客戶端發(fā)送 EXEC 命令、觸發(fā)事務(wù)執(zhí)行時(shí), 服務(wù)器會(huì)對(duì)客戶端的狀態(tài)進(jìn)行檢查:
- 如果客戶端的 CLIENT_DIRTY_CAS 選項(xiàng)已經(jīng)被打開,那么說明被客戶端監(jiān)視的鍵至少有一個(gè)已經(jīng)被修改了,事務(wù)的安全性已經(jīng)被破壞。服務(wù)器會(huì)放棄執(zhí)行這個(gè)事務(wù),直接向客戶端返回空回復(fù),表示事務(wù)執(zhí)行失敗。
- 如果 CLIENT_DIRTY_CAS 選項(xiàng)沒有被打開,那么說明所有監(jiān)視鍵都安全,服務(wù)器正式執(zhí)行事務(wù)。
小總結(jié):
3 個(gè)階段
- 開啟:以 MULTI 開始一個(gè)事務(wù)
- 入隊(duì):將多個(gè)命令入隊(duì)到事務(wù)中,接到這些命令并不會(huì)立即執(zhí)行,而是放到等待執(zhí)行的事務(wù)隊(duì)列里面
- 執(zhí)行:由 EXEC 命令觸發(fā)事務(wù)
3 個(gè)特性
- 單獨(dú)的隔離操作:事務(wù)中的所有命令都會(huì)序列化、按順序地執(zhí)行。事務(wù)在執(zhí)行的過程中,不會(huì)被其他客戶端發(fā)送來的命令請(qǐng)求所打斷。
- 沒有隔離級(jí)別的概念:隊(duì)列中的命令沒有提交之前都不會(huì)實(shí)際的被執(zhí)行,因?yàn)槭聞?wù)提交前任何指令都不會(huì)被實(shí)際執(zhí)行,也就不存在”事務(wù)內(nèi)的查詢要看到事務(wù)里的更新,在事務(wù)外查詢不能看到”這個(gè)讓人萬分頭痛的問題
- 不保證原子性:Redis 同一個(gè)事務(wù)中如果有一條命令執(zhí)行失敗,其后的命令仍然會(huì)被執(zhí)行,沒有回滾
在傳統(tǒng)的關(guān)系式數(shù)據(jù)庫(kù)中,常常用 ACID 性質(zhì)來檢驗(yàn)事務(wù)功能的安全性。Redis 事務(wù)保證了其中的一致性(C)和隔離性(I),但并不保證原子性(A)和持久性(D)。
最后
Redis 事務(wù)在發(fā)送每個(gè)指令到事務(wù)緩存隊(duì)列時(shí)都要經(jīng)過一次網(wǎng)絡(luò)讀寫,當(dāng)一個(gè)事務(wù)內(nèi)部的指令較多時(shí),需要的網(wǎng)絡(luò) IO 時(shí)間也會(huì)線性增長(zhǎng)。所以通常 Redis 的客戶端在執(zhí)行事務(wù)時(shí)都會(huì)結(jié)合 pipeline 一起使用,這樣可以將多次 IO 操作壓縮為單次 IO 操作。
參考資料
[1]
Redis設(shè)計(jì)與實(shí)現(xiàn): https://redisbook.readthedocs.io/en/latest/feature/transaction.html#id3
總結(jié)
以上是生活随笔為你收集整理的redis setnx原子性_不支持原子性的 Redis 事务也叫事务吗?的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 用稳压管保护单片机引脚_一步一步,全程揭
- 下一篇: 【学习笔记】第四章——文件 II(基本操