?在Spring事務管理下,Synchronized為啥還執行緒不安全?

六脈神劍發表於2019-11-26

前言

文字已收錄至我的GitHub倉庫,歡迎Star:github.com/bin39232820…
種一棵樹最好的時間是十年前,其次是現在
我知道很多人不玩qq了,但是懷舊一下,歡迎加入六脈神劍Java菜鳥學習群,群聊號碼:549684836 鼓勵大家在技術的路上寫部落格

synchronized 鎖住方法的情況下,竟然出現了髒寫

Tips
昨天本來打算是準備著一支菸 一杯咖啡 一個bug寫一天的,突然我們組長跟我們說線上環境報錯了,
還出現了"伺服器異常,請聯絡管理員"
這特麼不是一級事故嗎?雖然有測試再前面扛槍。但是是我負責的直播模組,心理慌的一批(ps 報錯圖當時沒儲存了)

分析事故原因

因為是報錯(因為我做這條資料查詢的時候是selectOne 所以會報出現了sql異常) 原因到是很快找到了 資料庫出現了髒寫如圖:

?在Spring事務管理下,Synchronized為啥還執行緒不安全?

我負責的是直播模組 其中的一個業務是直播結束後第三方會通知我去拉取直播的回放,
但是這個回放有可能一條,也有可能是多條,但是我們的業務要求是隻需要儲存一條直播回放所以我這會做如下操作:

?在Spring事務管理下,Synchronized為啥還執行緒不安全?
我再做插入之前我會做一個校驗,並且我還加了一個方法級別的鎖 並且線上我們只有一個副本,
竟然還出現了髒寫 我的fuck,我這是見了鬼了吧

解決問題的過程

我懷著百私不得其解的心理打算去找答案

首先我模擬了一個併發環境:

    @Test
    public void TEST_TX() throws Exception {

        int N = 2;
        CountDownLatch latch = new CountDownLatch(N);
        for (int i = 0; i < N; i++) {
            Thread.sleep(100L);
            new Thread(() -> {
                try {
                    latch.await();
                    System.out.println("---> start " + Thread.currentThread().getName());
                    Thread.sleep(1000L);
                    CourseChapterLiveRecord courseChapterLiveRecord = new CourseChapterLiveRecord();
                    courseChapterLiveRecord.setCourseChapterId(9785454l);
                    courseChapterLiveRecord.setCreateTime(new Date());
                    courseChapterLiveRecord.setRecordEndTime(new Date());
                    courseChapterLiveRecord.setDuration("aaa");
                    courseChapterLiveRecord.setSiteDomain("ada");
                    courseChapterLiveRecord.setRecordId("aaaaaaaaa");
                    courseChapterLiveRecordServiceImpl.saveCourseChapterLiveRecord(courseChapterLiveRecord);
                    System.out.println("---> end " + Thread.currentThread().getName());
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }).start();
            latch.countDown();
        }

    }
複製程式碼

通過CountDownLatch 去模擬併發看看資料是否會有問題:結果測試線的資料如下:

?在Spring事務管理下,Synchronized為啥還執行緒不安全?

我去還真出現了 而且是一部分出現髒寫,一部分沒有成功,我特麼 fuck 心理一萬次想說這特麼我怎麼找
測了十來次 然後覺得肯定是有問題的 然後冷靜下來 因為我打了日誌 發現2個執行緒確實是順序執行的(這裡的截圖就沒有貼了)
眾所周知,synchronized方法能夠保證所修飾的程式碼塊、方法保證有序性、原子性、可見性。 那麼這說明什麼呢 我一想肯定Synchronized 它是起到它的作用的 一個執行緒執行完成之後,另外一個執行緒再來執行, 突然靈光一閃 是不是下一個執行緒再做冪等校驗的時候 讀到了上一次還沒有提交的事務 所以造成了髒讀髒寫的原因呢 然後我把再類上的 @Transactional 註解去掉

?在Spring事務管理下,Synchronized為啥還執行緒不安全?

果然後面測了幾次 再也沒出現上面的情況了

Tips 特別感謝一位不願透露姓名的大佬的指出說我沒有把標題的內容說清楚和後面的解決問題的收場的時候有點草率

在這裡 我再好好的說一下我標題是 在Spring事務管理下,Synchronized為啥還執行緒不安全? 其實有是自己並沒有用Synchronized 鎖住 Spring 的事務
因為我的列子上的@Transaction註解是再類上面(也就是再方法上面)Spring的宣告事事務他是利用了aop的思想
我雖然鎖住了第一個執行緒 但是等到第一個執行緒的事務 還沒提交的時候,第二個執行緒就去查詢了 所以就會導致執行緒不安全問題

解決問題

方案1 很簡單 那就是不開事務就行了,再這個方法上不加事務就行 因為 Synchronized 可以保證執行緒安全。 這個方案的意思就是說不要再同一個方法上用@Transaction 和 Synchronized 例子圖就沒有貼了 就像我前面的 把註解去掉就好了 (但是前提你這個方案確定是不需要事務)

方案2 再這個裡面再呼叫一層service 讓那個方法提交事務,這樣的話加上Synchronized 也能保證執行緒安全。 方案2我貼下程式碼吧

    @Override
    public synchronized void saveCourseChapterLiveRecord(CourseChapterLiveRecord courseChapterLiveRecord) {
        saveRecord(courseChapterLiveRecord);
    }

    @Transactional
    public void saveRecord(CourseChapterLiveRecord courseChapterLiveRecord) {
        //先查資料看是否已經存了
        if (findOrder(courseChapterLiveRecord)){ return;}
        int row = this.insertSelective(courseChapterLiveRecord);
        if (row<1){
    log.info("把錄播的資訊插入資料庫失敗 引數是->{}", JSON.toJSONString(courseChapterLiveRecord));
    throw new RRException("把錄播的資訊插入資料庫失敗");
        } 
    }
複製程式碼

其實也就是說把事務包裹在Synchronized 裡面

先自我批評一下
在技術的道路上真的不要自己覺得是什麼就是什麼 上面的程式碼是錯誤的 其實我並沒有測試過 就貼到文章上了 這是一個大忌 為什麼很多技術文章有問題 因為很多就像我上面的一樣 所以敦促自己以後做事情還是要紮紮實實

感謝 紫雨飛星 讀者提出我的錯誤 具體錯誤的原因是因為呼叫savRecord方法的時候使用的是this物件,其實是沒有被AOP處理的,也就是這個Transactional不會生效~~~

修改後的程式碼 自己注入自己

    @Override
    public synchronized void saveCourseChapterLiveRecord(CourseChapterLiveRecord courseChapterLiveRecord) {
        courseChapterLiveRecordServiceImpl.saveRecord(courseChapterLiveRecord);
    }

    @Transactional
    public void saveRecord(CourseChapterLiveRecord courseChapterLiveRecord) {
        //先查資料看是否已經存了
        if (findOrder(courseChapterLiveRecord)){ return;}
        int row = this.insertSelective(courseChapterLiveRecord);
        if (row<1){
    log.info("把錄播的資訊插入資料庫失敗 引數是->{}", JSON.toJSONString(courseChapterLiveRecord));
    throw new RRException("把錄播的資訊插入資料庫失敗");
        } 
    }

複製程式碼

利用中午的時間測了幾次 確實是不會出現執行緒安全問題了

方案3 用redis 分散式鎖 也是可以的 就算是多個副本也是能保證執行緒安全。這個後面的文章會有寫到

結論

在多執行緒環境下,就可能會出現:方法執行完了(synchronized程式碼塊執行完了),事務還沒提交,別的執行緒可以進入被synchronized修飾的方法,再讀取的時候,讀到的是還沒提交事務的資料,這個資料不是最新的,所以就出現了這個問題。

參考了一位讀者的結論

Synchronized 失效關鍵原因:是因為Synchronized鎖定的是當前呼叫方法物件,而Spring AOP 處理事務會進行生成一個代理物件,並在代理物件執行方法前的事務開啟,方法執行完的事務提交,所以說,事務的開啟和提交併不是在 Synchronized 鎖定的範圍內。出現同步鎖失效的原因是:當A(執行緒) 執行完insertSelective()方法,會進行釋放同步鎖,去做提交事務,但在A(執行緒)還沒有提交完事務之前,B(執行緒)進行執行findOrder() 方法,執行完畢之後和A(執行緒)一起提交事務, 這時候就會出現執行緒安全問題。

日常求贊

好了各位,以上就是這篇文章的全部內容了,能看到這裡的人呀,都是人才

創作不易,各位的支援和認可,就是我創作的最大動力,我們下篇文章見

六脈神劍 | 文 【原創】如果本篇部落格有任何錯誤,請批評指教,不勝感激 !

參考連結

特別鳴謝

感謝各位大佬對文章的點評 我會繼續努力採坑的 來自一個小白的真實內心獨白

相關文章