1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        沒(méi)想到,Redis 遇到 @Transactional注解,有這么大個(gè)坑

        共 13987字,需瀏覽 28分鐘

         ·

        2022-11-14 00:24

        大家好,我是小富~

        最近項(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。代碼如下所示:

                
                return?redisTemplate.opsForValue().increment("count",?1);

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

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

        二、排查

        根據(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,如下所示:

        ea6a62a00d7ab216e7fa0ccfa25ae471.webp

        官方注釋已經(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)類的上面添加了一個(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 的命令有影響。

        39c841eb3c299aca92340ae14d32f766.webp

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

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

        9eb3322545b2d9bb067f9059d3f453a5.webp

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

        25ccbb1d387b687ec54254827f1604b4.webp

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

        7208ead6b7f25eadc5585a7f3676a200.webp

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

        1838123f4e04ecf8f170389d5cddaea4.webp

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

        0fb41092d173feaf2a723b6c5d0fa7d5.webp

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

        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)化后如下所示:

        5ce1ff9de0b6580ec0a4002d0f27cc29.webp

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

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

        8f349d69961332d06c0ad426dbba790b.webp

        難道開(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 注解。

        5936bf4c306758c99cb3db00219a5444.webp

        為了驗(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 的值。

        33c233b1f52866b311008e87fdd434c9.webp

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

        fe28dc3c835cab503ac2a8d567985835.webp

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

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

        dfb4d5f221a95750fb5104cdd0d015b5.webp

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

        93cc7cda4625292e6f5cf5c3b5a92b7d.webp

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

        1d1edb0ce33ab865ed38565851e48886.webpd66450f4c6627f21ad88fcb3bc30b428.webp

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

        4392fd1fb7df8ec9f4686551fafe68eb.webp

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

        96628a1804acb67e081a0da5287e14d1.webp

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

        3b8d5fe6e54b5afa461dcbb11ccd041e.webp

        問(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ì)遞增。

        三、源碼解析

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

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

        0627171bbb8f9e6387da3d94e0327af0.webp

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

        09db4f4afb280f5d12d6a2096f55fd70.webp

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

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

        a389081b5b662d4138b7474f1a111a65.webp

        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è)專門用來(lái)執(zhí)行 Redis 事務(wù),一個(gè)用來(lái)執(zhí)行普通的 Redis 命令。

        4.1 方案一

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

        266bfbc0d8e8aaca9f81307c4bbf33c6.webp

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

        8c79d33793970dd943fd3d53d7173a61.webp

        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ù)支持的。

        代碼如下所示:

        a161e0b6bf713b5b3efd605d90aa59e3.webp

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

        630726f35f8fde8f8fba0979d2da552e.webp

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

        d9bb851a7a8ad6d4e5298179af07d694.webp

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

        ac13b777346d9eeb15b8ee8f8de8d250.webp

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

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

        另外關(guān)于 Redis 事務(wù)使用還有一個(gè)坑,就是 Redis 連接未釋放,導(dǎo)致獲取不到連接了,這是下一個(gè)話題了~

        參考資料:https://blog.csdn.net/qq_34021712/article/details/79606551

        - END -

        說(shuō)在最后



        841eba21b705007c6ac88a4f23823063.webp

        一年一度的消費(fèi)日雙十一即將到來(lái)!

        841eba21b705007c6ac88a4f23823063.webp

        為了讓盡可能多的喜歡Java的人學(xué)習(xí) 到有趣和有用知識(shí), 我們現(xiàn)在推出年度套餐!

        在11月13日前,年度訂閱Code Gym

        可獲得 5折以上 的優(yōu)惠 哦!

        除了打折價(jià),還有一份個(gè)人獎(jiǎng)金在等著你。

        機(jī)不可失,先到先得!

        9a6abc3b99f26ae88d0e112eacff5c14.webp










        C odeGym 背后的小故

        Code Gym 由充滿激情的 Java開(kāi)發(fā)人員于 2018年創(chuàng)立。2019年, Code Gym 在全球擁有十 萬(wàn)名用戶。 從那時(shí)起, 我們的團(tuán)隊(duì)不斷 創(chuàng)新 Code Gym 課程,使我們的用戶人數(shù)達(dá)到 95萬(wàn)人。我們的任務(wù)是為大家提供最新的 Java學(xué)習(xí)體驗(yàn)。


        Java是一種最常用也最容易學(xué)習(xí)的編程語(yǔ)言,掌握了它,不僅能夠輕松炫技,更打開(kāi)了高薪的大門。Java 語(yǔ)言程序員的平均薪資為 7.5萬(wàn)美元。


        Code Gym 是一門面向?qū)嵺`的交互式編程課程,其中80%的內(nèi)容為實(shí)踐,20%的內(nèi)容為基本Java理論。這才是成為一名真正的Java開(kāi)發(fā)人員所需要的東西。本學(xué)習(xí)課程非常適合初學(xué)者以及想要擴(kuò)展技能的軟件開(kāi)發(fā)人員。


        我們相信,只要用正確的方法,每個(gè)人都能掌握編碼技。學(xué)習(xí)代碼應(yīng)該是以實(shí)踐為重點(diǎn)。事實(shí)上,任何人都可以學(xué)習(xí)編程——你不需要成為數(shù)學(xué)天才,只 要有學(xué)習(xí)的 欲望就足夠?qū)W會(huì)編程,程序員并不是天生的。


        加入 Code Gym , 點(diǎn)代 碼!







        • 大量的練習(xí)讓你為真正的工作做好準(zhǔn)備


        要學(xué)習(xí) ?Java? 語(yǔ)言并成為一名程序員,你需 要大量編寫(xiě)代碼。采用實(shí)踐第一的方法是 Code Gym 的顯著特點(diǎn)。有超過(guò)1200個(gè)不同難度的任務(wù),你可以對(duì)所學(xué)的每個(gè)主要Java主題進(jìn)行大量的練習(xí)。這些數(shù)量足以讓你獲得足夠的經(jīng)驗(yàn)找到一份工作。Code Gym 還為大家?guī)?lái)許多妙趣橫生的Java實(shí)踐體驗(yàn)——聊天應(yīng)用程序、自動(dòng)化餐廳應(yīng)用程序、HTML 編輯器、ATM模擬器等。和我們一起學(xué) Java,別跟丟了~!


        • 邊玩游戲邊學(xué)習(xí)


        學(xué)習(xí)不應(yīng)該是一個(gè)無(wú)聊的事情!Code Gym 教程使用最新技術(shù)讓你的學(xué)習(xí)更輕松、更有趣和更富有成效:可視化的課程進(jìn)展、有趣的編程故事、激勵(lì)機(jī)制、玩代碼游戲……聽(tīng)起來(lái)很有趣,是不是?但 在這里不都是編碼游戲,而是一個(gè)很酷的工具來(lái)創(chuàng)建你自己的游戲,或者更準(zhǔn)確地說(shuō),創(chuàng)建自己的一些老派經(jīng)典游戲的版本。


        • 即使解決方法驗(yàn)證


        普通課堂上,你需要等很長(zhǎng)時(shí)間老師才有空檢查你的作業(yè)。 不要再浪費(fèi)時(shí)間等待了! 我們的全能虛擬導(dǎo)師會(huì)在眨眼間來(lái)檢查你所有的解決方法并將給出針對(duì)此代碼的評(píng)論列表,能精確地告訴你哪個(gè)地方不滿足要求。


        • 不再使學(xué)習(xí)Java一件孤單的事情


        如果你在學(xué)習(xí)中遇到了瓶頸和困難,可以在Code Gym 社區(qū)中發(fā)帖交流,大家會(huì)熱心地幫助你。在社區(qū)中,你也可以和來(lái)自全世界的學(xué)習(xí)者一起分享你的學(xué)習(xí)收獲。在這里不僅有機(jī)會(huì)學(xué)習(xí)Java編程技術(shù),還能鍛煉自己的英語(yǔ)能力,相輔相成。

        9df5d42149dd23ac5166a2aababea90c.webp

        Code Gym

        想換個(gè)新工作或者開(kāi)啟新副業(yè)?

        想成為家族群里最亮眼的明星?

        想成為一名 Java 開(kāi)發(fā)人員?

        事不宜遲,你知道該怎么做。

        抓住機(jī)會(huì)開(kāi)始學(xué)習(xí) Java!

        用最低的價(jià)格購(gòu)最優(yōu)的課程,這樣福利怎能錯(cuò)過(guò)!


        acf58fdb6267f02432cf06b7d9416a22.webp
        d8f2d44b26556f139a31384eff3201c2.webp


        即日起 ?至? 11 月13日

        CodeGym超低限時(shí)折扣

        一年期高級(jí)版訂閱服務(wù)

        僅需? ¥391? 登陸即享

        ? ?93f0b01d500c30701df6b8dfeee2e879.webp


        acf58fdb6267f02432cf06b7d9416a22.webp

        未注冊(cè)過(guò)的新?戶,先注冊(cè),再訂閱

        1.? 點(diǎn)擊鏈接注冊(cè)

        https://codegym.cc/zh/

        2.? 點(diǎn)擊 課程 尋找菜單

        3.? 點(diǎn)擊菜單享受折扣優(yōu)惠

        已經(jīng)注冊(cè)的 ? ,

        在登陸狀態(tài)下打開(kāi)鏈接直接訂閱

        https://codegym.cc/zh/sale


        如有問(wèn)題可以通過(guò)微信公眾號(hào)后臺(tái)發(fā)私信溝通,或者到官?社區(qū)留?。


        點(diǎn)擊閱讀原文?即刻獲取課程!

        瀏覽 70
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            黄色录像一级 | 尤蜜粉嫩av国产一区二区三区 | 暗呦网一区二区三区 | 色婷婷视频一区二区 | 亚洲欧美激情小说另类 | 甘雨大战史莱姆视频动画免费观看 | 国产亚州精品女人久久久久久 | 日韩欧美国产三级 | 狠狠躁日日躁夜夜躁A片2022 | 色色播播 |