01.22 高併發場景下更新數據庫報錯,記錄一次 MySQL 死鎖問題的解決

今天隔壁項目組的開發小姐姐找到我,說她們項目正在做壓力測試,更新 MySQL 數據庫的一張表時,總是發生死鎖,日誌大概是這個樣子的:

<code>org.springframework.dao.DeadlockLoserDataAccessException: 
### Error updating database. Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
### The error may involve com.taikang.biz.persist.dao.PolicyMapper.updateChdrin-Inline
### The error occurred while setting parameters/<code>

她們壓測的併發量不高,我心想 MySQL 應該還不至於這麼脆弱,應該是哪裡設置的問題,或者是代碼寫的有問題;我大概看了一下數據庫設置、表結構和代碼,大概是這樣的(不是真實的表和代碼,我按照問題的過程重新寫的):

表結構很簡單,主要是三個字段,自增主鍵、訂單號和保單號:

<code>CREATE TABLE policy
(
id INT NOT NULL AUTO_INCREMENT ,
orderno VARCHAR(50) ,
policyno VARCHAR(20) ,
PRIMARY KEY (id)
)
ENGINE=InnoDB DEFAULT CHARSET=utf8 ;/<code>

DAO 層的代碼也比較簡單,業務邏輯是通過訂單號定位到一條數據,修改其保單號:

<code>@Update("UPDATE policy set policyno = #{policyNo} WHERE orderno = #{orderNo}")
public void updatePolicy(@Param("orderNo") String orderNo, @Param("policyNo") String policyNo);/<code>

在壓測的過程中,每次的報文中,訂單號都是不相同的,也就是 where 條件中的 orderNo 是不相同的。

1. 先查看存儲引擎

MySQL 的 InnoDB 引擎在更新數據時採用的是行級鎖,而 MyISAM 引擎只有表級鎖;我們公司的數據庫都是由基礎設施的團隊搭建及維護的,MySQL 默認的 InnoDB 引擎應該不會被修改。

但是考慮到 MySQL 的存儲引擎不是在建庫時定義,而是在建表的時候聲明(同庫可以包含不同存儲引擎類型的表),萬一是誰不小心指定錯了呢,所以保險起見還是掃一眼 policy 表的存儲引擎。

ENGINE=InnoDB ,沒有問題。

順便總結一下 InnoDB 引擎和 MyISAM 引擎的區別,要不然這篇文章字數太少,覺得對不起你們...

  • MyISAM 只支持表級鎖,InnoDB 支持行級鎖和表級鎖,默認行級鎖;
  • MyISAM 不支持事務,執行速度也就更快一些;InnoDB 支持事務,可以 commit、rollback ,具有崩潰後的修復能力;
  • MyISAM 不支持外鍵,InnoDB 支持外鍵(儘管實際應用中我們很少使用,一般都是在業務上進行約束);
  • MyISAM 不支持 MVCC , InnoDB 支持 MVCC ;大家可以把 MVCC 看做是樂觀鎖的一種實現。


2. 執行計劃

直接看一下 UPDATE 語句的執行計劃。

<code>id  select_type  table   type   possible_keys  key      key_len  ref     rows    Extra        
-- ----------- ------ ----- ------------- ------- ------- ------ ------ -----------
1 SIMPLE policy index (null) PRIMARY 4 (null) 784896 Using where /<code>

我們注意其中的 "rows" 這個指標,它代表著 MySQL 認為執行這條 SQL 語句必須要逐行檢查和判斷的數據條數,而“786432”就是 policy 表的數據條數;

這也就是說:當執行 UPDATE 操作的時候,如果 where 條件不能走索引(可能是沒有索引,也可能是因為錯誤的寫法導致 SQL 不走索引),那麼 MySQL 會使用表級鎖;因此我們壓測過程中,高併發的場景下,表級鎖會導致數據庫死鎖。

找到原因之後,解決起來也就非常簡單了,那就是給 orderno 增加索引,再讓我們看看加完索引之後的執行計劃:

<code>id  select_type  table   type   possible_keys  key          key_len  ref    rows  Extra        
-- ----------- ------ ----- ------------- ----------- ------- ----- ---- -----------
1 SIMPLE policy range idx_orderno idx_orderno 153 const 1 Using where /<code>

rows = 1,變成了行級鎖,再次壓測,問題解決!

高併發場景下更新數據庫報錯,記錄一次 MySQL 死鎖問題的解決


分享到:


相關文章: