新聞中心
你好,我是悟空。

10年積累的成都網(wǎng)站設(shè)計、網(wǎng)站建設(shè)經(jīng)驗,可以快速應(yīng)對客戶對網(wǎng)站的新想法和需求。提供各種問題對應(yīng)的解決方案。讓選擇我們的客戶得到更好、更有力的網(wǎng)絡(luò)服務(wù)。我雖然不認(rèn)識你,你也不認(rèn)識我。但先網(wǎng)站設(shè)計后付款的網(wǎng)站建設(shè)流程,更有秦皇島免費網(wǎng)站建設(shè)讓你可以放心的選擇與我們合作。
本文主要內(nèi)容如下:
一、前言
最近項目的生產(chǎn)環(huán)境遇到一個奇怪的問題:
現(xiàn)象:每天早上客服人員在后臺創(chuàng)建客服事件時,都會創(chuàng)建失敗。當(dāng)我們重啟這個微服務(wù)后,后臺就可以正常創(chuàng)建了客服事件了。到第二天早上又會創(chuàng)建失敗,又得重啟這個微服務(wù)才行。
初步排查:創(chuàng)建一個客服事件時,會用到 Redis 的遞增操作來生成一個唯一的分布式 ID 作為事件 id。代碼如下所示:
return redisTemplate.opsForValue().increment("count", 1);而恰巧每天早上這個遞增操作都會返回 null,進而導(dǎo)致后面的一系列邏輯出錯,保存客服事件失敗。當(dāng)重啟微服務(wù)后,這個遞增操作又正常了。
那么排查的方向就是 Redis 的操作為什么會返回 null 了,以及為什么重啟就又恢復(fù)正常了。
二、排查
根據(jù)上面的信息,我們先來看看 Redis 的自增操作在什么情況下會返回 null。
2.1 推測一
根據(jù)重啟后就恢復(fù)正常,我們推測晚上執(zhí)行了大量的 job,大量 Redis 連接未釋放,當(dāng)早上再來執(zhí)行 Redis 操作時,執(zhí)行失敗。重啟后,連接自動釋放了。
但是其他有使用到 Redis 的業(yè)務(wù)功能又是正常的,所以推測一的方向有問題,排除。
2.2 推測二
可能是 Redis 事務(wù)造成的問題。這個推測的依據(jù)是根據(jù)下面的代碼來排查的。
直接看 redisTemplate? 遞增的方法 increment,如下所示:
官方注釋已經(jīng)說明什么情況下會返回 null:
- 當(dāng)在 pipeline(管道)中使用這個 increment 方法時會返回 null。
- 當(dāng)在 transaction(事務(wù))中使用這個 increment 方法時會返回 null。
事務(wù)提供了一種將多個命令打包,然后一次性、有序地執(zhí)行機制.
多個命令會被入列到事務(wù)隊列中,然后按先進先出(FIFO)的順序執(zhí)行。
事務(wù)在執(zhí)行過程中不會被中斷,當(dāng)事務(wù)隊列中的所有命令都被執(zhí)行完畢之后,事務(wù)才會結(jié)束。(內(nèi)容來自 Redis 設(shè)計與實現(xiàn))
繼續(xù)看代碼,發(fā)現(xiàn)在操作 Redis 的 ServiceImpl 實現(xiàn)類的上面添加了一個 @Transactional 注解,推測是不是這個注解影響了 Redis 的操作結(jié)果。
2.3 驗證推測二
如下面的表格所示,第二行中沒有添加 Spring 的事務(wù)注解 @Transactional?時,執(zhí)行 Redis 的遞增命令肯定是正常的,而接下來要驗證的是表格中的第一行:加了 @Transactional 是否對 Redis 的命令有影響。
為了驗證上面的推論,我寫了一個 Demo 程序。
Controller 類,定義了一個 API,用來模擬前端發(fā)起的請求:
Service 實現(xiàn)類,定義了一個方法,用來遞增 Redis 中的 count 鍵,每次遞增 1,然后返回命令執(zhí)行后的結(jié)果。而且這個 Service 方法加了@Transactional 注解。
Postman 測試下,發(fā)現(xiàn)每發(fā)一次請求,count 都會遞增 1,并沒有返回 null。
然后到 Redis 中查看數(shù)據(jù),count 的值也是遞增后的值 38,也不是 null。
通過這個實驗說明在 @Transactional 注解的方法里面執(zhí)行 Redis 的操作并不會返回 null,結(jié)論我記錄到了表格中。
所以說上面的推論不成立(加了 @Transactional 注解并不影響),到這里線索似乎斷了。
2.4 推測三
然后跟當(dāng)時做這塊功能的開發(fā)人員說明了情況,告訴他可能是 Redis 事務(wù)造成的,然后問有沒有其他同學(xué)在凌晨執(zhí)行過 Redis 事務(wù)相關(guān)的 Job。
他說最近有同事加過 Redis 的事務(wù)功能,在凌晨執(zhí)行 Job 的時候用到事務(wù)。我將這位同事加的代碼簡化后如下所示:
下面是針對這段代碼的解釋,簡單來說就是開啟事務(wù),將 Redis 命令順序放到一個隊列中,然后最后一起執(zhí)行,且保證原子性。
setEnableTransactionSupport表示是否開啟事務(wù)支持,默認(rèn)不開啟。
難道開啟了 Redis 事務(wù),還能影響 Spring 事務(wù)中的 Redis 操作?
2.5 驗證推測三
如下表,序號 3 和 序號 4 的場景都是開啟了 Redis 的事務(wù)支持,兩個場景的區(qū)別是是否加了 @Transactional 注解。
為了驗證上面的場景,我們來做個實驗:
- 先開啟 Redis 事務(wù)支持,然后執(zhí)行 Redis 的事務(wù)命令 multi 和 exec 。
- 驗證場景 3:在 @Transactional 注解的方法中執(zhí)行 Redis 的遞增操作。
- 驗證場景 4:在非 @Transactional 注解的方法中執(zhí)行 Redis 的遞增操作
2.5.1 執(zhí)行 Redis 事務(wù)
首先就用 Redis 的 multi 和 exec 命令來設(shè)置兩個 key 的值。
如下圖所示,設(shè)置成功了。
2.5.2 @Transactional 中執(zhí)行 Redis 命令
接下來在標(biāo)注有 @Transactional 注解的方法中執(zhí)行 Redis 的遞增操作。
多次執(zhí)行這個命令返回的結(jié)果都是 null,這不就正好重現(xiàn)了!
再來看 Redis 中 count 的值,發(fā)現(xiàn)每執(zhí)行一次 API 請求調(diào)用,都會遞增 1,所以雖然命令返回的是 null,但最后 Redis 中存放的還是遞增后的結(jié)果。
接下來我們驗證下場景 4,先執(zhí)行 Redis 事務(wù)操作,然后在不添加 @Transactional 注解的方法中執(zhí)行 Redis 遞增操作。
用 Postman 調(diào)用這個接口后,正常返回自增后的結(jié)果,并不是返回 null。說明在非 @Transactional 中執(zhí)行 Redis 操作并沒有受到 Redis 事務(wù)的影響。
四個場景的結(jié)論如下所示,只有第三個場景下,Redis 的遞增操作才會返回 null。
問題原因找到了,說明 RedisTemplete 開啟了 Redis 事務(wù)支持后,在 @Transactional 中執(zhí)行的 Redis 命令也會被認(rèn)為是在 Redis 事務(wù)中執(zhí)行的,要執(zhí)行的遞增命令會被放到隊列中,不會立即返回執(zhí)行后的結(jié)果,返回的是一個 null,需要等待事務(wù)提交時,隊列中的命令才會順序執(zhí)行,最后 Redis 數(shù)據(jù)庫的鍵值才會遞增。
三、源碼解析
那我們就看下為什么開啟了 Redis 事務(wù)支持,效果就不一樣了。
找到 Redis 執(zhí)行命令的核心方法, execute 方法。
然后一步一步點進去看,關(guān)鍵代碼就是 211 行到 216 行,有一個邏輯判斷,當(dāng)開啟了 Redis 事務(wù)支持后,就會去綁定一個連接(bindConnection?),否則就去獲取新的 Redis 連接(getConnection?)。這里我們是開啟了的,所以再到 bindConnection方法中查看如何綁定連接的。
接著往下看,關(guān)鍵代碼如下所示,當(dāng)開啟了 Redis 事務(wù)支持,且添加了 @Transactional 注解時,就會執(zhí)行 Redis 的 mutil 命令。
關(guān)鍵代碼:conn.multi();
Redis Multi 命令用于標(biāo)記一個事務(wù)塊的開始,事務(wù)塊內(nèi)的多條命令會按照先后順序被放進一個隊列當(dāng)中,最后由 EXEC 命令原子性(atomic)地執(zhí)行。
真相大白,開啟 Redis 事務(wù)支持 + @Transactional 注解后,最后其實是標(biāo)記了一個 Redis 事務(wù)塊,后續(xù)的操作命令是在這個事務(wù)塊中執(zhí)行的。
比如下面的的遞增命令并不會返回遞增后的結(jié)果,而是返回 null。
stringRedisTemplate.opsForValue().increment("count", 1);而我們的生產(chǎn)環(huán)境重啟服務(wù)后,開啟的 Redis 事務(wù)支持又被重置為默認(rèn)值了,所以后續(xù)的 Redis 遞增操作都能正常執(zhí)行。
四、修復(fù)方案
目前想到了兩種解決方案:
方案一:每次 Redis 的事務(wù)操作完成后,關(guān)閉 Redis 事務(wù)支持,然后再執(zhí)行 @Transactional 中的 Redis 命令。(有弊端)
方案二:創(chuàng)建兩個 StringRedisTemplate,一個專門用來執(zhí)行 Redis 事務(wù),一個用來執(zhí)行普通的 Redis 命令。
4.1 方案一
方案一的寫法如下,先開啟事務(wù)支持,事務(wù)執(zhí)行之后,再關(guān)閉事務(wù)支持。
但是這種寫法有個弊端,如果在執(zhí)行 Redis 事務(wù)期間,在 @Transactional 注解的方法里面執(zhí)行 Redis 命令,則還是會造成返回結(jié)果為 null。
4.2 方案二
弄兩個 RedisTemplate Bean,一個是用來執(zhí)行 Redis 事務(wù)的,一個是用來執(zhí)行普通 Redis 命令的(不支持事務(wù))。不同的地方引入不同的 Bean 就可以了。
先創(chuàng)建一個 RedisConfig 文件,自動裝配兩個 Bean。一個 Bean 名為 stringRedisTemplate? 代表不支持事務(wù)的,執(zhí)行命令后立即返回實際的執(zhí)行結(jié)果。另外一個 Bean 名為 stringRedisTemplateTransaction,代表開啟 Redis 事務(wù)支持的。
代碼如下所示:
接下來在測試的 Service 類中注入兩個不同的 StringRedisTemplate 實例,代碼如下所示:
Redis 事務(wù)的操作改寫成這樣,且不需要手動開啟 Redis 事務(wù)支持了。用到的 StringRedisTemplate 是支持事務(wù)的那個實例。
在 Spring 的 @Tranactional 中執(zhí)行的 Redis 命令如下所示,用到的 StringRedisTemplate 是不支持事務(wù)的那個實例。
然后還是按照上面場景 3 的測試步驟,先執(zhí)行 testRedisMutil 方法,再執(zhí)行 testTransactionAnnotations 方法。
驗證結(jié)果:Redis 遞增操作正常返回 count 的值,修復(fù)完成。
另外關(guān)于 Redis 事務(wù)使用還有一個坑,就是 Redis 連接未釋放,導(dǎo)致獲取不到連接了,這是下一個話題了~
參考資料:https://blog.csdn.net/qq_34021712/article/details/79606551
標(biāo)題名稱:一次Redis事務(wù)使用不當(dāng)引發(fā)的生產(chǎn)事故
文章鏈接:http://m.fisionsoft.com.cn/article/cccjpeh.html


咨詢
建站咨詢
