一、前言最近項目的生產(chǎn)環(huán)境遇到一個奇怪的問題: 現(xiàn)象 :每天早上客服人員在后臺創(chuàng)建客服事件時,,都會創(chuàng)建失敗 ,。當(dāng)我們重啟 這個微服務(wù)后,后臺就可以正常創(chuàng)建了客服事件了,。到第二天早上又會創(chuàng)建失敗,,又得重啟這個微服務(wù)才行。 初步排查 :創(chuàng)建一個客服事件時,,會用到 Redis 的遞增操作來生成一個唯一的分布式 ID 作為事件 id,。代碼如下所示:
而恰巧每天早上這個遞增操作都會返回
二,、排查根據(jù)上面的信息,,我們先來看看 Redis 的自增操作在什么情況下會返回 null。 2.1 推測一根據(jù)重啟后就恢復(fù)正常,,我們推測晚上執(zhí)行了大量的 job,,大量 Redis 連接未釋放,當(dāng)早上再來執(zhí)行 Redis 操作時,,執(zhí)行失敗,。重啟后,連接自動釋放了,。 但是其他有使用到 Redis 的業(yè)務(wù)功能又是正常的,,所以推測一的方向有問題,排除 ,。 2.2 推測二可能是 Redis 事務(wù)造成的問題,。這個推測的依據(jù)是根據(jù)下面的代碼來排查的。 直接看 官方注釋已經(jīng)說明什么情況下會返回 null:
繼續(xù)看代碼,,發(fā)現(xiàn)在操作 Redis 的 ServiceImpl 實現(xiàn)類的上面添加了一個 @Transactional 注解,,推測是不是這個注解影響了 Redis 的操作結(jié)果。 2.3 驗證推測二如下面的表格所示,,第二行中沒有添加 Spring 的事務(wù)注解 為了驗證上面的推論,,我寫了一個 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í)行,,且保證原子性,。
2.5 驗證推測三如下表,,序號 3 和 序號 4 的場景都是開啟了 Redis 的事務(wù)支持 ,,兩個場景的區(qū)別是是否加了 @Transactional 注解 。 為了驗證上面的場景,,我們來做個實驗:
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 方法,。 然后一步一步點進(jìn)去看,,關(guān)鍵代碼就是 211 行到 216 行,有一個邏輯判斷,,當(dāng)開啟了 Redis 事務(wù)支持后,,就會去綁定一個連接( 接著往下看,,關(guān)鍵代碼如下所示,,當(dāng)開啟了 Redis 事務(wù)支持,且添加了 @Transactional 注解時,,就會執(zhí)行 Redis 的 mutil 命令,。 關(guān)鍵代碼:conn.multi(); Redis Multi 命令 用于標(biāo)記一個事務(wù)塊的開始,事務(wù)塊內(nèi)的多條命令會按照先后順序被放進(jìn)一個隊列當(dāng)中,,最后由 EXEC 命令原子性(atomic)地執(zhí)行,。
比如下面的的遞增命令并不會返回遞增后的結(jié)果,,而是返回 null,。
而我們的生產(chǎn)環(huán)境重啟服務(wù)后,開啟的 Redis 事務(wù)支持又被重置為默認(rèn)值了,,所以后續(xù)的 Redis 遞增操作都能正常執(zhí)行。 四、修復(fù)方案目前想到了兩種解決方案:
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 名為 代碼如下所示: 接下來在測試的 Service 類中注入兩個不同的 StringRedisTemplate 實例,,代碼如下所示: Redis 事務(wù)的操作改寫成這樣,,且不需要手動開啟 Redis 事務(wù)支持了。用到的 StringRedisTemplate 是支持事務(wù)的那個實例,。 在 Spring 的 @Tranactional 中執(zhí)行的 Redis 命令如下所示,,用到的 StringRedisTemplate 是不支持事務(wù)的那個實例。 然后還是按照上面場景 3 的測試步驟,,先執(zhí)行 testRedisMutil 方法,,再執(zhí)行 testTransactionAnnotations 方法。 驗證結(jié)果 :Redis 遞增操作正常返回 count 的值,,修復(fù)完成,。 |
|