0
  • 聊天消息
  • 系統(tǒng)消息
  • 評(píng)論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線(xiàn)課程
  • 觀看技術(shù)視頻
  • 寫(xiě)文章/發(fā)帖/加入社區(qū)
會(huì)員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識(shí)你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

Redis 遞增修復(fù)操作

jf_ro2CN3Fa ? 來(lái)源:芋道源碼 ? 2023-06-14 09:47 ? 次閱讀

一、前言

二、排查

三、源碼解析

四、修復(fù)方案

一、前言

最近項(xiàng)目的生產(chǎn)環(huán)境遇到一個(gè)奇怪的問(wèn)題:

現(xiàn)象 :每天早上客服人員在后臺(tái)創(chuàng)建客服事件時(shí),都會(huì)創(chuàng)建失敗 。當(dāng)我們重啟 這個(gè)微服務(wù)后,后臺(tái)就可以正常創(chuàng)建了客服事件了。到第二天早上又會(huì)創(chuàng)建失敗,又得重啟這個(gè)微服務(wù)才行。

初步排查 :創(chuàng)建一個(gè)客服事件時(shí),會(huì)用到 Redis 的遞增操作來(lái)生成一個(gè)唯一的分布式 ID 作為事件 id。代碼如下所示:

returnredisTemplate.opsForValue().increment("count",1);

而恰巧每天早上這個(gè)遞增操作都會(huì)返回 null,進(jìn)而導(dǎo)致后面的一系列邏輯出錯(cuò),保存客服事件失敗。當(dāng)重啟微服務(wù)后,這個(gè)遞增操作又正常了。

那么排查的方向就是 Redis 的操作為什么會(huì)返回 null 了,以及為什么重啟就又恢復(fù)正常了。

基于 Spring Boot + MyBatis Plus + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶(hù)小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶(hù)、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能

項(xiàng)目地址:https://github.com/YunaiV/ruoyi-vue-pro

視頻教程:https://doc.iocoder.cn/video/

二、排查

根據(jù)上面的信息,我們先來(lái)看看 Redis 的自增操作在什么情況下會(huì)返回 null。

2.1 推測(cè)一

根據(jù)重啟后就恢復(fù)正常,我們推測(cè)晚上執(zhí)行了大量的 job,大量 Redis 連接未釋放,當(dāng)早上再來(lái)執(zhí)行 Redis 操作時(shí),執(zhí)行失敗。重啟后,連接自動(dòng)釋放了。

但是其他有使用到 Redis 的業(yè)務(wù)功能又是正常的,所以推測(cè)一的方向有問(wèn)題,排除 。

2.2 推測(cè)二

可能是 Redis 事務(wù)造成的問(wèn)題。這個(gè)推測(cè)的依據(jù)是根據(jù)下面的代碼來(lái)排查的。

直接看 redisTemplate 遞增的方法 increment,如下所示:

7757a1ba-0a53-11ee-962d-dac502259ad0.png

官方注釋已經(jīng)說(shuō)明什么情況下會(huì)返回 null:

當(dāng)在 pipeline(管道)中使用這個(gè) increment 方法時(shí)會(huì)返回 null。

當(dāng)在 transaction(事務(wù))中使用這個(gè) increment 方法時(shí)會(huì)返回 null。

事務(wù) 提供了一種將多個(gè)命令打包,然后一次性、有序地執(zhí)行機(jī)制.

多個(gè)命令會(huì)被入列到事務(wù)隊(duì)列中,然后按先進(jìn)先出(FIFO)的順序執(zhí)行。

事務(wù)在執(zhí)行過(guò)程中不會(huì)被中斷,當(dāng)事務(wù)隊(duì)列中的所有命令都被執(zhí)行完畢之后,事務(wù)才會(huì)結(jié)束。(內(nèi)容來(lái)自 Redis 設(shè)計(jì)與實(shí)現(xiàn))

繼續(xù)看代碼,發(fā)現(xiàn)在操作 Redis 的 ServiceImpl 實(shí)現(xiàn)類(lèi)的上面添加了一個(gè) @Transactional 注解,推測(cè)是不是這個(gè)注解影響了 Redis 的操作結(jié)果。

2.3 驗(yàn)證推測(cè)二

如下面的表格所示,第二行中沒(méi)有添加 Spring 的事務(wù)注解 @Transactional時(shí),執(zhí)行 Redis 的遞增命令肯定是正常的,而接下來(lái)要驗(yàn)證的是表格中的第一行:加了 @Transactional 是否對(duì) Redis 的命令有影響。

77660b60-0a53-11ee-962d-dac502259ad0.png

為了驗(yàn)證上面的推論,我寫(xiě)了一個(gè) Demo 程序。

Controller 類(lèi) ,定義了一個(gè) API,用來(lái)模擬前端發(fā)起的請(qǐng)求:

777f4a12-0a53-11ee-962d-dac502259ad0.png

Service 實(shí)現(xiàn)類(lèi) ,定義了一個(gè)方法,用來(lái)遞增 Redis 中的 count 鍵,每次遞增 1,然后返回命令執(zhí)行后的結(jié)果。而且這個(gè) Service 方法加了@Transactional 注解。

779b3696-0a53-11ee-962d-dac502259ad0.png

Postman 測(cè)試下,發(fā)現(xiàn)每發(fā)一次請(qǐng)求,count 都會(huì)遞增 1,并沒(méi)有返回 null。

77ac9562-0a53-11ee-962d-dac502259ad0.png

然后到 Redis 中查看數(shù)據(jù),count 的值也是遞增后的值 38,也不是 null。

77d27232-0a53-11ee-962d-dac502259ad0.png

通過(guò)這個(gè)實(shí)驗(yàn)說(shuō)明在 @Transactional 注解的方法里面執(zhí)行 Redis 的操作并不會(huì)返回 null,結(jié)論我記錄到了表格中。

77d83820-0a53-11ee-962d-dac502259ad0.png

所以說(shuō)上面的推論不成立(加了 @Transactional 注解并不影響),到這里線(xiàn)索似乎斷了

2.4 推測(cè)三

然后跟當(dāng)時(shí)做這塊功能的開(kāi)發(fā)人員說(shuō)明了情況,告訴他可能是 Redis 事務(wù)造成的,然后問(wèn)有沒(méi)有其他同學(xué)在凌晨執(zhí)行過(guò) Redis 事務(wù)相關(guān)的 Job。

他說(shuō)最近有同事加過(guò) Redis 的事務(wù)功能,在凌晨執(zhí)行 Job 的時(shí)候用到事務(wù)。我將這位同事加的代碼簡(jiǎn)化后如下所示:

77f786e4-0a53-11ee-962d-dac502259ad0.png

下面是針對(duì)這段代碼的解釋?zhuān)?jiǎn)單來(lái)說(shuō)就是開(kāi)啟事務(wù),將 Redis 命令順序放到一個(gè)隊(duì)列中,然后最后一起執(zhí)行,且保證原子性。

setEnableTransactionSupport表示是否開(kāi)啟事務(wù)支持,默認(rèn)不開(kāi)啟。

782042f0-0a53-11ee-962d-dac502259ad0.png

難道開(kāi)啟了 Redis 事務(wù),還能影響 Spring 事務(wù)中的 Redis 操作?

2.5 驗(yàn)證推測(cè)三

如下表,序號(hào) 3 和 序號(hào) 4 的場(chǎng)景都是開(kāi)啟了 Redis 的事務(wù)支持 ,兩個(gè)場(chǎng)景的區(qū)別是是否加了 @Transactional 注解 。

78389c42-0a53-11ee-962d-dac502259ad0.png

為了驗(yàn)證上面的場(chǎng)景,我們來(lái)做個(gè)實(shí)驗(yàn):

先開(kāi)啟 Redis 事務(wù)支持,然后執(zhí)行 Redis 的事務(wù)命令 multi 和 exec 。

驗(yàn)證場(chǎng)景 3:在 @Transactional 注解的方法中執(zhí)行 Redis 的遞增操作。

驗(yàn)證場(chǎng)景 4:在非 @Transactional 注解的方法中執(zhí)行 Redis 的遞增操作

2.5.1 執(zhí)行 Redis 事務(wù)

首先就用 Redis 的 multi 和 exec 命令來(lái)設(shè)置兩個(gè) key 的值。

785b039a-0a53-11ee-962d-dac502259ad0.png

如下圖所示,設(shè)置成功了。

7871127a-0a53-11ee-962d-dac502259ad0.png

2.5.2 @Transactional 中執(zhí)行 Redis 命令

接下來(lái)在標(biāo)注有 @Transactional 注解的方法中執(zhí)行 Redis 的遞增操作。

78a66524-0a53-11ee-962d-dac502259ad0.png

多次執(zhí)行這個(gè)命令返回的結(jié)果都是 null,這不就正好重現(xiàn)了!

78d62e30-0a53-11ee-962d-dac502259ad0.png

再來(lái)看 Redis 中 count 的值,發(fā)現(xiàn)每執(zhí)行一次 API 請(qǐng)求調(diào)用,都會(huì)遞增 1,所以雖然命令返回的是 null,但最后 Redis 中存放的還是遞增后的結(jié)果。

78e95b5e-0a53-11ee-962d-dac502259ad0.png78f7d882-0a53-11ee-962d-dac502259ad0.png

接下來(lái)我們驗(yàn)證下場(chǎng)景 4,先執(zhí)行 Redis 事務(wù)操作,然后在不添加 @Transactional 注解的方法中執(zhí)行 Redis 遞增操作。

790e7bc8-0a53-11ee-962d-dac502259ad0.png

用 Postman 調(diào)用這個(gè)接口后,正常返回自增后的結(jié)果,并不是返回 null。說(shuō)明在非 @Transactional 中執(zhí)行 Redis 操作并沒(méi)有受到 Redis 事務(wù)的影響。

7932b3e4-0a53-11ee-962d-dac502259ad0.png

四個(gè)場(chǎng)景的結(jié)論如下所示,只有第三個(gè)場(chǎng)景下,Redis 的遞增操作才會(huì)返回 null。

795da496-0a53-11ee-962d-dac502259ad0.png

問(wèn)題原因找到了,說(shuō)明 RedisTemplete 開(kāi)啟了 Redis 事務(wù)支持后,在 @Transactional 中執(zhí)行的 Redis 命令也會(huì)被認(rèn)為是在 Redis 事務(wù)中執(zhí)行的,要執(zhí)行的遞增命令會(huì)被放到隊(duì)列中,不會(huì)立即返回執(zhí)行后的結(jié)果,返回的是一個(gè) null,需要等待事務(wù)提交時(shí),隊(duì)列中的命令才會(huì)順序執(zhí)行,最后 Redis 數(shù)據(jù)庫(kù)的鍵值才會(huì)遞增。

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 實(shí)現(xiàn)的后臺(tái)管理系統(tǒng) + 用戶(hù)小程序,支持 RBAC 動(dòng)態(tài)權(quán)限、多租戶(hù)、數(shù)據(jù)權(quán)限、工作流、三方登錄、支付、短信、商城等功能

項(xiàng)目地址:https://github.com/YunaiV/yudao-cloud

視頻教程:https://doc.iocoder.cn/video/

三、源碼解析

那我們就看下為什么開(kāi)啟了 Redis 事務(wù)支持,效果就不一樣了。

找到 Redis 執(zhí)行命令的核心方法, execute 方法。

7972aeae-0a53-11ee-962d-dac502259ad0.png

然后一步一步點(diǎn)進(jìn)去看,關(guān)鍵代碼就是 211 行到 216 行,有一個(gè)邏輯判斷,當(dāng)開(kāi)啟了 Redis 事務(wù)支持后,就會(huì)去綁定一個(gè)連接(bindConnection),否則就去獲取新的 Redis 連接(getConnection)。這里我們是開(kāi)啟了的,所以再到 bindConnection方法中查看如何綁定連接的。

79951e62-0a53-11ee-962d-dac502259ad0.png

接著往下看,關(guān)鍵代碼如下所示,當(dāng)開(kāi)啟了 Redis 事務(wù)支持,且添加了 @Transactional 注解時(shí),就會(huì)執(zhí)行 Redis 的 mutil 命令。

關(guān)鍵代碼:conn.multi();

79b52cc0-0a53-11ee-962d-dac502259ad0.png

Redis Multi 命令 用于標(biāo)記一個(gè)事務(wù)塊的開(kāi)始,事務(wù)塊內(nèi)的多條命令會(huì)按照先后順序被放進(jìn)一個(gè)隊(duì)列當(dāng)中,最后由 EXEC 命令原子性(atomic)地執(zhí)行。

真相大白,開(kāi)啟 Redis 事務(wù)支持 + @Transactional 注解后,最后其實(shí)是標(biāo)記了一個(gè) Redis 事務(wù)塊,后續(xù)的操作命令是在這個(gè)事務(wù)塊中執(zhí)行的。

比如下面的的遞增命令并不會(huì)返回遞增后的結(jié)果,而是返回 null。

stringRedisTemplate.opsForValue().increment("count",1);

而我們的生產(chǎn)環(huán)境重啟服務(wù)后,開(kāi)啟的 Redis 事務(wù)支持又被重置為默認(rèn)值了,所以后續(xù)的 Redis 遞增操作都能正常執(zhí)行。

四、修復(fù)方案

目前想到了兩種解決方案:

方案一:每次 Redis 的事務(wù)操作完成后,關(guān)閉 Redis 事務(wù)支持,然后再執(zhí)行 @Transactional 中的 Redis 命令。(有弊端

方案二:創(chuàng)建兩個(gè) StringRedisTemplate,一個(gè)專(zhuān)門(mén)用來(lái)執(zhí)行 Redis 事務(wù),一個(gè)用來(lái)執(zhí)行普通的 Redis 命令。

4.1 方案一

方案一的寫(xiě)法如下,先開(kāi)啟事務(wù)支持,事務(wù)執(zhí)行之后,再關(guān)閉事務(wù)支持。

7a0274bc-0a53-11ee-962d-dac502259ad0.png

但是這種寫(xiě)法有個(gè)弊端 ,如果在執(zhí)行 Redis 事務(wù)期間,在 @Transactional 注解的方法里面執(zhí)行 Redis 命令,則還是會(huì)造成返回結(jié)果為 null。

7a2d12e4-0a53-11ee-962d-dac502259ad0.png

4.2 方案二

弄兩個(gè) RedisTemplate Bean,一個(gè)是用來(lái)執(zhí)行 Redis 事務(wù)的,一個(gè)是用來(lái)執(zhí)行普通 Redis 命令的(不支持事務(wù))。不同的地方引入不同的 Bean 就可以了。

先創(chuàng)建一個(gè) RedisConfig 文件,自動(dòng)裝配兩個(gè) Bean。一個(gè) Bean 名為 stringRedisTemplate 代表不支持事務(wù)的,執(zhí)行命令后立即返回實(shí)際的執(zhí)行結(jié)果。另外一個(gè) Bean 名為 stringRedisTemplateTransaction,代表開(kāi)啟 Redis 事務(wù)支持的。

代碼如下所示:

7a427f76-0a53-11ee-962d-dac502259ad0.png

接下來(lái)在測(cè)試的 Service 類(lèi)中注入兩個(gè)不同的 StringRedisTemplate 實(shí)例,代碼如下所示:

7a5d9518-0a53-11ee-962d-dac502259ad0.png

Redis 事務(wù)的操作改寫(xiě)成這樣,且不需要手動(dòng)開(kāi)啟 Redis 事務(wù)支持了。用到的 StringRedisTemplate 是支持事務(wù)的那個(gè)實(shí)例。

7a7a695e-0a53-11ee-962d-dac502259ad0.png

在 Spring 的 @Tranactional 中執(zhí)行的 Redis 命令如下所示,用到的 StringRedisTemplate 是不支持事務(wù)的那個(gè)實(shí)例。

7ac18d98-0a53-11ee-962d-dac502259ad0.png

然后還是按照上面場(chǎng)景 3 的測(cè)試步驟,先執(zhí)行 testRedisMutil 方法,再執(zhí)行 testTransactionAnnotations 方法。

驗(yàn)證結(jié)果 :Redis 遞增操作正常返回 count 的值,修復(fù)完成。

責(zé)任編輯:彭菁

聲明:本文內(nèi)容及配圖由入駐作者撰寫(xiě)或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點(diǎn)僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場(chǎng)。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問(wèn)題,請(qǐng)聯(lián)系本站處理。 舉報(bào)投訴
  • 源碼
    +關(guān)注

    關(guān)注

    8

    文章

    630

    瀏覽量

    29074
  • 小程序
    +關(guān)注

    關(guān)注

    1

    文章

    233

    瀏覽量

    12065
  • Redis
    +關(guān)注

    關(guān)注

    0

    文章

    370

    瀏覽量

    10810

原文標(biāo)題:當(dāng) Redis 碰上 @Transactional,有大坑,要注意!

文章出處:【微信號(hào):芋道源碼,微信公眾號(hào):芋道源碼】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。

收藏 人收藏

    評(píng)論

    相關(guān)推薦

    如何使用Rust連接Redis

    Rust操作Redis。 Redis依賴(lài)庫(kù) 在Rust中有很多Redis的客戶(hù)端庫(kù)可以選擇,這里我們選擇使用redis-rs庫(kù)。在Cargo
    的頭像 發(fā)表于 09-19 16:22 ?2118次閱讀

    Redis Stream應(yīng)用案例

    摘要: Redis Stream Redis最新的大版本5.0已經(jīng)RC1了,其中最重要的Feature莫過(guò)于Redis Stream了,關(guān)于Redis Stream的基本使用介紹和設(shè)計(jì)
    發(fā)表于 06-26 17:15

    laravel使用redis

    laravel操作redis筆記!
    發(fā)表于 09-24 09:40

    Redis的安裝和使用步驟

    Python操作Redis之安裝和使用(一)
    發(fā)表于 09-29 09:29

    labview讀寫(xiě)操作REDIS

    本帖最后由 SevenLi8408 于 2022-9-15 08:07 編輯 分享一個(gè)好用的非關(guān)系型緩存數(shù)據(jù)庫(kù)的使用方法。REDIS桌面管理軟件https://github.com
    發(fā)表于 08-15 10:32

    電池修復(fù)的基礎(chǔ)操作

    電池修復(fù)的基礎(chǔ)操作 在這里介紹的電動(dòng)車(chē)蓄/電池修復(fù)/之基礎(chǔ)操作步驟,就是指常規(guī)使用的修復(fù)方法,修復(fù)
    發(fā)表于 11-10 13:44 ?1029次閱讀

    python操作redis

    --有序集合)和hash(哈希類(lèi)型)。這些數(shù)據(jù)類(lèi)型都支持push/pop、add/remove及取交集并集和差集及更豐富的操作,而且這些操作都是原子性的。在此基礎(chǔ)上,redis支持各種不同方式的排序。與memcached一樣,
    發(fā)表于 11-28 11:02 ?738次閱讀
    python<b class='flag-5'>操作</b><b class='flag-5'>redis</b>

    基于多線(xiàn)程環(huán)境下值的遞增操作--原子操作

    因此在多線(xiàn)程環(huán)境中對(duì)一個(gè)變量進(jìn)行讀寫(xiě)時(shí),我們需要有一種方法能夠保證對(duì)一個(gè)值的遞增操作是原子操作——即不可打斷性,一個(gè)線(xiàn)程在執(zhí)行原子操作時(shí),其它線(xiàn)程必須等待它完成之后才能開(kāi)始執(zhí)行該原子
    的頭像 發(fā)表于 01-10 11:16 ?6114次閱讀
    基于多線(xiàn)程環(huán)境下值的<b class='flag-5'>遞增</b><b class='flag-5'>操作</b>--原子<b class='flag-5'>操作</b>

    Springboot+redis操作多種實(shí)現(xiàn)

    一、Jedis,Redisson,Lettuce三者的區(qū)別共同點(diǎn):都提供了基于Redis操作的Java API,只是封裝程度,具體實(shí)現(xiàn)稍有不同。 不同點(diǎn): 1.1、Jedis 是Redis的Java
    的頭像 發(fā)表于 09-22 10:48 ?1767次閱讀
    Springboot+<b class='flag-5'>redis</b><b class='flag-5'>操作</b>多種實(shí)現(xiàn)

    mysql_redis在MySQL中操作Redis?

    ./oschina_soft/gitee-mysql_redis.zip
    發(fā)表于 06-22 14:35 ?2次下載
    mysql_<b class='flag-5'>redis</b>在MySQL中<b class='flag-5'>操作</b><b class='flag-5'>Redis</b>?

    Redis數(shù)據(jù)同步解決方案—NineData

    NineData(https://www.ninedata.cloud/)在Redis的同步上,提供了穩(wěn)定和高效的解決方案,并且性能上也領(lǐng)先其他同步工具,特別是在同步的動(dòng)態(tài)限流、數(shù)據(jù)對(duì)比修復(fù)和限流
    的頭像 發(fā)表于 06-05 15:31 ?778次閱讀
    <b class='flag-5'>Redis</b>數(shù)據(jù)同步解決方案—NineData

    Redis是什么?簡(jiǎn)述它的優(yōu)缺點(diǎn)?

    Redis是什么?簡(jiǎn)述它的優(yōu)缺點(diǎn)? Redis本質(zhì)上是一個(gè)Key-Value類(lèi)型的內(nèi)存數(shù)據(jù)庫(kù),很像Memcached,整個(gè)數(shù)據(jù)庫(kù)加載在內(nèi)存當(dāng)中操作,定期通過(guò)異步操作把數(shù)據(jù)庫(kù)中的數(shù)據(jù)fl
    的頭像 發(fā)表于 10-09 10:37 ?741次閱讀

    redis的increment方法

    實(shí)現(xiàn)對(duì)存儲(chǔ)在數(shù)據(jù)庫(kù)中的特定鍵的遞增操作。在本文中,我們將詳細(xì)介紹Redis的 INCR 方法,包括其原理、使用方法以及一些常見(jiàn)的應(yīng)用場(chǎng)景。 首先,我們來(lái)看看Redis的 INCR 方法
    的頭像 發(fā)表于 12-05 09:57 ?1039次閱讀

    redis的主要方法

    Redis是一種基于內(nèi)存的開(kāi)源鍵值對(duì)存儲(chǔ)系統(tǒng),常用于緩存、消息中間件、數(shù)據(jù)庫(kù)等場(chǎng)景。作為一個(gè)高性能的NoSQL存儲(chǔ)解決方案,Redis提供了豐富的方法用于操作數(shù)據(jù)。本文將詳細(xì)介紹Redis
    的頭像 發(fā)表于 12-05 09:59 ?738次閱讀

    redis使用多線(xiàn)程處理操作命令

    Redis 是一個(gè)使用多線(xiàn)程處理操作命令的開(kāi)源內(nèi)存數(shù)據(jù)庫(kù)系統(tǒng)。它以其高性能、可擴(kuò)展性和靈活性而聞名,通常被用作緩存、消息代理和數(shù)據(jù)存儲(chǔ)等各種應(yīng)用場(chǎng)景。在本文中,我們將詳盡、詳實(shí)、細(xì)致地探
    的頭像 發(fā)表于 12-05 10:25 ?504次閱讀