追根溯源,有點意思,關於MySQL更新「丟失」問題

2021-10-19 17:32:00 字數 3098 閱讀 7882

人生苦短,不如養狗

趁著這幾天過節,覆盤了一下去年的一些歷史遺留問題,其中有這樣乙個關於資料庫的小問題讓我忍不住翻出來又回味了一下,下面就讓我們一起品味品味。

首先,先來看下問題現場,運算元據庫的執行流程如下圖:   

這裡對原有的業務邏輯進行簡化,簡化後的**實現如下:

public void finishsubtask(subtask subtask)

}

乍一看好像邏輯和**沒有什麼問題,但是在實際執行過程中有時會出現查詢語句查出來的結果集是更新前的結果集,就好像更新沒有生效或者「丟失」了,導致沒有成功將對應的主任務終止。

在開始查案之前先說一下環境情況,mysql版本為5.6(阿里雲高可用版本,即一主一備,事務隔離級別為讀已提交),服務端使用的是springboot和mybatis框架。

1. 現場檢視

遇到問題的第一時間是去檢視了一下資料庫是不是更新出了問題,但是查詢之後發現資料確實是更新了,接著再去檢視了一下當時機器的網路問題,並沒有報資料庫連線異常等問題。到這裡,**異常問題和網路抖動問題基本可以排除。

2. 嘗試復現

在無法從問題現場獲取更多線索的情況下,我開始嘗試在本地進行復現,但是在進行多次嘗試之後,發現本地無法復現出當時的場景,上述的流程總是能正確的執行。

3. 謹慎推理

在本地復現失敗之後,結合最初的問題現場排查,問題大致出在系統內orm框架(這裡即mybatis)的sql執行流程或是mysql服務端的sql執行流程上。

mybatis的sql執行過程:

假定問題出現在mybatis的sql執行過程,那麼可能導致的原因有兩個:

由於當前的專案中mybatis相關的配置的基本都是預設,所以mybatis只使用了一級快取,且只在session級別進行共享。為了確定sqlsession的生命週期,這裡具體看下mybatis當中執行**方法的邏輯(spring框架版本):

private class sqlsessioninterceptor implements invocationhandler 

return result;

} catch (throwable t) finally }}

}}

這裡具體看下獲取sqlsession方法 getsqlsession :

public static sqlsession getsqlsession(sqlsessionfactory sessionfactory, executortype executortype,

persistenceexceptiontranslator exceptiontranslator)

logger.debug(() -> "creating a new sqlsession");

session = sessionfactory.opensession(executortype);

// 只有當事務託管給spring,才會將新建立的session註冊到sessionholder集合中當中,這裡為了執行緒隔離,使用threadlocal進行儲存

registersessionholder(sessionfactory, executortype, exceptiontranslator, session);

return session;

}

再次觀察原先的**,這裡沒有使用spring相關的事務管理方法,即沒有將事務託管給spring,所以在 getsqlsession 方法中每次獲取都是乙個新的sqlsession,這也就代表不會存在查詢語句查詢的結果為上次查詢保留在sqlsession中的快取,即該問題不會是由於mybatis的一級快取導致的。

那麼是否是由於事務隔離性導致的呢?上面說過,專案基本所有的配置都是保持預設,這裡spring的事務隔離級別和資料庫保持一致(雖然沒有使用到spring事務管理)。觀察上面的sqlsession執行方法,由於沒有將事務託管給spring,在進行**方法之後後,sqlsession會主動強制做一次 commit 操作,無論當前是否有髒頁。按照上面的執行順序來講,查詢事務是在更新事務提交之後才開始的,理論上不應該出現查詢到更新事務提交之前的資料。

分析mybatis執行過程無果,只能將目光投向mysql伺服器的內部執行過程。

mysql的sql執行過程:

在mysql服務內部,一條從客戶端發起的sql請求會經過聯結器、查詢快取、分析器、優化器以及最終進行實際執行的執行器。這裡我們具體看下執行器是如何執行一條 update 語句:                

在執行更新語句的過程中會將與目標表相關的快取清空,按照上面的請求順序是不會出現查詢語句查詢到快取的情況。

再一次,問題被指向了事務隔離性,難道真的是事務隔離性搞的鬼?

推論&驗證:

再次檢查mybatis中最後一步提交事務的方法 sqlsession.commit() ,發現該方法內部只是呼叫了 jdbctransaction/springmanagedtransaction 的 commit 方法 ,但是該方法並沒有返回值,也就是說這裡的呼叫並不能表明提交的事務真正意義上被提交完成了。那麼就會有一定可能出現更新的事務還沒有提交完成,查詢的事務開始執行了,此時根據當前mysql服務的事務隔離級別讀已提交來看,這裡的查詢只能查詢更新事務提交之前的結果集。

想到這裡,我再一次檢視了一下幾條問題資料當時更新請求和查詢請求的間隔時間,間隔時間確實非常短,平均在十幾毫秒左右(有些更短)。

為了進一步驗證猜想,在測試環境我使用了 thread.sleep() **,讓執行緒在執行完更新語句後先休眠500毫秒,然後再進行下面的查詢語句。在經過數天的壓測之後,發現確實沒有再出現過執行結束主任務失敗的情況,此時基本可以確定應該是事務隔離性導致的。

根據上面的分析,最終我設計了三種解決方案:

老話說得好:一行bug改一天。回味完之後再來看這個問題,確實不是那麼的複雜,但是學習的樂趣(改bug的樂趣)不就在於探尋問題根源的過程和找尋解決方案的過程。

最後,值此新春佳節,祝大家新年快樂,身體健康,早日暴富,哈哈哈~

追根溯源清除rootkit

你在這篇技術指南講座中將學到 rootkit很難檢測,並且能夠讓黑客完全控制你的系統。搞清楚這些黑客工具是如何使用的,並且知道如何找出隱藏在你的系統中的rootkit。假設你是乙個黑客。你剛好發現乙個系統不是你的 leet skillz 工具軟體的對手,並且獲得了根 訪問許可權 系統管理員早晚會發現...

引數尋優 梯度下降 牛頓下降法 追根溯源

引數尋優背景 引數尋優問題隨處可見,舉幾個例子。1.小明假期結束回校,可以坐火車,可以坐汽車,可以坐飛機,還可以走著,小明從哪條路去學校更好呢?2.簡單的數學,一元二次方程求根。3.高深的數學,七橋問題,怎麼才能通過所有的橋各自一次走回七點所在的岸邊。4.機器學習中,求代價函式在約束條件下的最優解問...

追根究底之追本溯源 游標

傳說中的佛來到了微軟的作業系統,這次沒跟觀音商量,沒幾天變出了個猴子,這個猴子便是游標,莫非也學會了轉殖?游標可是麻雀雖小,武藝俱全。1.從造型上來說,它本是 空 悟空 單它連悟都不用悟,就空了。就叫它 不悟而空 吧,天天頂個 bmp 的造型,變來變去。若探知它如何變,便可知道windows如何處理...