使用的是 "illuminate/database": "^9.3"
最近業(yè)務(wù)上線,發(fā)現(xiàn)經(jīng)常產(chǎn)生死鎖。
排查過(guò)sql語(yǔ)句,并沒(méi)有發(fā)現(xiàn)什么異常
隔離級(jí)別是Read committed,
執(zhí)行update的時(shí)候也是根據(jù)主鍵ID進(jìn)行的。
隔三差五的就會(huì)導(dǎo)致mysql死鎖,通過(guò)KILL把死鎖的KILL了也沒(méi)用,后續(xù)繼續(xù)執(zhí)行事務(wù)也會(huì)導(dǎo)致死鎖。
解決方式是只能php start.php reload 然後就正常了。
請(qǐng)問(wèn)有遇過(guò)類似情況的嗎?
問(wèn)題已經(jīng)解決,感謝各位。
順帶寫清楚原因方便後者參考。
$dbCheck = $request->post('dbCheck',false);
try{
Db::beginTransaction();
$rs = Db::table('tbl_product ')->where('p_id','=',12345)->select('planID')->first();
if(!$rs) throw new Exception('not found');
$rs = Db::table('tbl_rateList')->where([
'planID'=>$rs->planID,
'bookDate'=>date('Y-m-d')
])->select('id,Price')->first();
if(!$rs){
Db::table('tbl_rateList')->insert(.....);
}else{
Db::table('tbl_rateList'))->where([
'planID'=>$rs->planID,
'bookDate'=>date('Y-m-d')
])->update(.....);
}
if($dbCheck){
Db::rollBack();
return json();
}
Db::commit();
return json([....])
}catch(\Exception $e)
{
Db::rollBack();
return json([....])
}
$dbCheck這個(gè)是後加的功能,實(shí)際代碼中有價(jià)格核驗(yàn),比底價(jià)低了就進(jìn)行二次確認(rèn)。
在單線程下進(jìn)行,是沒(méi)有問(wèn)題的,但是只要符合這三個(gè)條件,那麼就會(huì)產(chǎn)生死鎖:
1、3個(gè)或以上的INSERT操作
2、表中存在UNIQUE類型的唯一索引
3、第一個(gè)INSERT操作產(chǎn)生了rollBack
前端是進(jìn)行批量修改的時(shí)候,會(huì)傳遞DBConfirm參數(shù),而單條記錄的修改,是不會(huì)傳遞這個(gè)參數(shù)。
如果用戶首先進(jìn)行批量修改,傳遞了dbConfirm參數(shù),同時(shí)其他用戶也進(jìn)行批量/單條記錄的修改,此時(shí)會(huì)產(chǎn)生3個(gè)INSERT的transaction。
在SQL中抽象的流程為:
事務(wù)1 獲取了X鎖,並且插入成功;
事務(wù)2 嘗試插入,檢查UNIQUE索引,獲取S鎖 但失敗,繼續(xù)等待事務(wù)1
事務(wù)3 嘗試插入,檢查UNIQUE索引,獲取S鎖 但失敗,繼續(xù)等待事務(wù)1
當(dāng)事務(wù)1 進(jìn)行回滾,事務(wù)2和事務(wù)3 此時(shí)能獲取S鎖,檢查重複值之後執(zhí)行插入申請(qǐng)X鎖,但由於事務(wù)2和3都已經(jīng)獲取了S鎖,所以導(dǎo)致X鎖獲取失敗兩個(gè)事務(wù)進(jìn)入死鎖狀態(tài)。
增加多一個(gè)接口,專門處理價(jià)格檢測(cè),不通過(guò)事務(wù)回滾來(lái)進(jìn)行。
我也是第一次遇見這個(gè)問(wèn)題,最終還是通過(guò)百度和對(duì)mysql的error log還原了整個(gè)事件。
自己的不足在於太過(guò)先入為主,總覺(jué)得是update導(dǎo)致的死鎖,哪知道是特定環(huán)境下insert導(dǎo)致的死鎖。(從第一次發(fā)生,把一開始通過(guò)UNIQUE KEY進(jìn)行update操作改為 通過(guò)主鍵ID進(jìn)行update操作)包括再這裡提問(wèn),我都是傾向於問(wèn)題存在於 多線程 + UPDATE。
不用猜都知道你的代碼有問(wèn)題,建議仔細(xì)檢查代碼排查出問(wèn)題,要么改成可串行化。
runtime/logs 下的都查了,沒(méi)發(fā)現(xiàn)異常;代碼有問(wèn)題不至于隔三差五才死鎖吧?而且代碼異常的話拋出錯(cuò)誤后執(zhí)行的rollback,也不會(huì)導(dǎo)致數(shù)據(jù)庫(kù)死鎖啊?,F(xiàn)在webman是沒(méi)有任何異常,是數(shù)據(jù)庫(kù)產(chǎn)生死鎖。至于mysql的日志也查了,之前是通過(guò)索引進(jìn)行更新記錄,后來(lái)修改為根據(jù)主鍵ID更新記錄。
這個(gè)我清楚,所以排查過(guò)SQL語(yǔ)句,我是對(duì)A表進(jìn)行操作。
偽代碼:
try{
Db::beginTransaction();
$rs = Db::table('tbl_product ')->where('p_id','=',12345)->select('planID')->first();
if(!$rs) throw new Exception('not found');
$rs = Db::table('tbl_rateList')->where([
'planID'=>$rs->planID,
'bookDate'=>date('Y-m-d')
])->select('id')->first();
if(!$rs){
Db::table('tbl_rateList')->insert(.....);
}else{
Db::table('tbl_rateList'))->where([
'planID'=>$rs->planID,
'bookDate'=>date('Y-m-d')
])->update(.....);
}
Db::commit();
return json([....])
}catch(\Exception $e)
{
Db::rollBack();
return json([....])
}
但是情況也是一樣。
異常最好catch (\Throwable $e)不要用catch(\Exception $e)。
catch \Throwable 可以捕獲任何異常和Error錯(cuò)誤,catch \Exception 只能捕獲異常,不能捕獲Error