1、場景描述
今天開發中遇到一個場景,在一個事務中的操作邏輯是:需要先刪除A表的某個記錄,然后多線程往A表里插入多條數據。
begin
delete from A where age=2;
以下多線程操作>>>
insert into A valus(5);
......
insert into A valus(5);
<<<
commit;
但是這樣操作會會導致delete from A where age=2;執行完后,后續的插入獲取鎖超時,從而導致整個事務的失敗回滾。
出現:Error updating database. Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
2、問題分析(結合本人真實業務代碼分析)
核心代碼
問題分析
-
applicationDirMapper.deleteByExample(exampleQuery);
這段代碼執行后會在當前事務發起索引表效應。因為當前delete后面跟的條件字段applicationId他不是一個主鍵或者普通索引,所以會導致鎖表。
-
applicationServerDTO.getFolders().parallelStream().forEach(dirId -> {
ApplicationDir applicationDir = new ApplicationDir();
applicationDir.setApplicationId(applicationServer.getId());
applicationDir.setDirId(dirId);
applicationDirMapper.insertSelective(applicationDir);
});這段代碼分析parallelStream是一個多線程操作。Spring這時候會給這里的每個線程分配一個共享的SqlSession,為什么確定說分配了一個新的SqlSession呢?我們以日志來證明。
這個代碼實際可以看成里面有5個過程。
第1步:80行是個查詢操作
第2步:82行是個更新操作
第3步:93行是個查詢操作
第4步:94行是個刪除操作
第5步:95行是個多線程插入操作
從上面的日志可以看出來過了第4步之后Spring就重新又Creating a new SqlSession了,導致后面的多線程里操作數據庫的SqlSession和當前事務中的SqlSession脫離了,多線程脫軌到另外一個獨立的事務中去了。
當前事務的SqlSession
org.apache.ibatis.session.defaults.DefaultSqlSession@7aac05e2
多線程事務的SqlSession
org.apache.ibatis.session.defaults.DefaultSqlSession@2337080f
由此可見由於當前事務第4步刪除操作導致鎖表,但是多線程的插入操作有獲取不到當前事務的鎖而當前事務被阻塞無法釋放鎖,導致多線程獲取鎖超時永遠等待中所以超時出現Lock wait timeout exceeded; try restarting transaction。
3、解決方案
a. 如果當前操作沒有書屋要求可以去掉@Transactional,這樣事務就可以自動提交不會導致鎖的獲取超時。
b.可以將多線程里面的代碼塊抽離出來通過異步消息去處理。
c.大家有什么好的想法可以幫忙指導一下。
4、總結問題
開發中需要了解mysqld的鎖的類型,鎖的原理,鎖的算法,事務的隔離級別,事務的傳播屬性這樣才能更安全的介入開發中。