記錄一次insert 死鎖
1 問題描述
支付平臺上線之後在payorder insert的時候時不時會報dead lock detected 的錯誤日誌,在流量高峰的時候更易發生,在流量不是很高的時候也有發生。
追查發現這種情況下往往是同一個業務訂單同一時刻併發支付請求,這種情況不是使用者正常的支付行為,有可能是惡意刷或者請求重發。
同一個業務訂單同一個時刻併發的支付請求會造成unique key衝突(partnerid + orderno),因為orderno是業務訂單號和時間戳的組合。
在處理這種unique key衝突支付平臺是會允許第一個獲得鎖的成功,後續key衝突的會insert ignore並且判斷如果affected rows為0直接返回系統錯誤。
那為什麼在事務開始insert 記錄的時候就發生死鎖呢?
(PS: 聯想到之前iuserlastpaytype那個死鎖問題推斷也是這個問題,userid作為的主鍵,當時解決辦法是支付成功之前不更新iuserlastpaytype,但是沒有找到死鎖原因)
2 問題原因
要回答這個問題就要理解innodb 在insert時候的加鎖機制,官方文件http://dev.mysql.com/doc/refman/5.5/en/innodb-locks-set.html
insert加鎖策略:
在插入row之前,會設定插入意向鎖(是一種區間鎖),這個意向鎖通知其他事務我在這個區間會插入一條記錄,但是其它事務如果在這個區間和我的插入位置不衝突的話就無需等待。
insert操作會對插入的行加排他鎖,首先應該明確的是innodb的鎖都是加到索引上的,並且是index-record lock也就是記錄鎖而不是區間鎖。
如果併發事務插入的記錄不產生dulicate key衝突就不會有問題,發生dulicate key衝突時候innodb會先對這個記錄加shared lock,當先獲得該記錄排他鎖的事務回滾後,其它事務申請的shared lock會被授予,然後都會同時申請該記錄的排它鎖,當2個以上事務佔有該記錄的共享鎖又要申請該記錄的排它鎖的時候死鎖就產生了。
結合業務情況,因為alipaywap,tenpaywap,微信支付,聯動優勢都會先通過服務端先向第三方支付請求下單,這個過程有失敗的概率會導致事務最終沒有提交而回滾。當發生3個以上併發請求同一個支付訂單號,而且第一個先獲得insert排他鎖的事務因為第三方支付互動失敗回滾就必然導致死鎖發生。 通常在第三方服務不穩定的時候發生。
下面來測試模擬這種死鎖情況,表結構如下,a欄位建了unique key
結果表明,無論是insert ignore還是insert ... on duplicate key update ... 都會產生死鎖。
|
- insert ignore的情況
Transaction 1 |
Transaction 2 |
Transaction 3 |
---|---|---|
roll back | ||
insert ignore into yxg (a,b) values (9, 'a'); | insert ignore into yxg (a,b) values (9, 'a'); | |
- insert ... on duplicate key update
Transaction 1 |
Transaction 2 |
Transaction 3 |
---|---|---|
insert into yxg (a,b) values (8, 'a') on duplicate key update b='b'; | insert into yxg (a,b) values (8, 'a') on duplicate key update b='b'; | |
rollback | ||
3 解決辦法
解決思路:
就是讓先獲得插入排它鎖的insert操作事務不產生回滾。
解決辦法:
insert payorder之後立刻先提交一次事務,避免其因為和第三方互動失敗而產生事務回滾。這樣大不了增加了少許payorder記錄,而這樣的payorder也應該記錄參與統計支付成功率的。
4-25 17:15該解決方案上線,觀察3天沒有再報dead lock的錯誤(之前是週末必報並且頻率不低)。
4 後續思考
4.1 為什麼innodb在insert發生duplicate key衝突後會對記錄先加shared lock然後再申請排它鎖,而不是直接申請排它鎖呢? 這樣不就不產生死鎖了嗎?