背景知識:
MySQL有三種鎖的級別:頁級、表級、行級。
MyISAM和MEMORY存儲引擎采用的是表級鎖(table-level locking);BDB存儲引擎采用的是頁面鎖(page-level locking),但也支持表級鎖;InnoDB存儲引擎既支持行級鎖(row-level locking),也支持表級鎖,但默認情況下是采用行級鎖。
MySQL這3種鎖的特性可大致歸納如下:
表級鎖:開銷小,加鎖快;不會出現死鎖;鎖定粒度大,發生鎖沖突的概率最高,並發度最低。
行級鎖:開銷大,加鎖慢;會出現死鎖;鎖定粒度最小,發生鎖沖突的概率最低,並發度也最高。
頁面鎖:開銷和加鎖時間界於表鎖和行鎖之間;會出現死鎖;鎖定粒度界於表鎖和行鎖之間,並發度一般。
行級鎖並不是直接鎖記錄,而是鎖索引,如果一條SQL語句用到了主鍵索引,mysql會鎖住主鍵索引;如果一條語句操作了非主鍵索引,mysql會先鎖住非主鍵索引,再鎖定主鍵索引。
問題現象:
線上出現錯誤日志:mysql死鎖問題。
排查過程:
請DBA協助查看mysql錯誤日志,發現確實存在死鎖問題。
發生死鎖的就是以上兩條sql,兩次請求時間僅間隔1毫秒。
涉及到的數據庫表如下:
根據背景知識里的紅色描述:如果用到了主鍵索引,mysql會鎖定主鍵索引,如果用到了非主鍵索引,msyql會先鎖定非主鍵索引,再鎖定主鍵索引。因此,在SQL(1)中先鎖定了next_consume_time這個非主鍵索引,還需要鎖定主鍵索引,此時SQL(2)直接鎖定了主鍵索引,而其update語句中set使用了next_consume_time,同時還需要next_consume_time這個非主鍵索引。因此兩條SQL就出現了對索引資源的競爭,造成死鎖。
從業務方面考慮:兩條SQL是從兩台機器發起的請求,而這兩條SQL在業務上是存在了先后順序的,先執行SQL(1)占用需要執行的表記錄,在執行SQL(2)進行業務操作。從日志中發現,54機器執行的是SQL(2),那么54機器肯定已經執行過了SQL(1),此時該條記錄已經是被占用狀態了,55機器又怎么會執行SQL(2)呢?難道是沒被占用嗎?
DBA又查看mysql的binlog,發現queue_id為283410的記錄,確實正常執行過SQL(1),那55機器為什么還要再次執行SQL(1)呢?
又查看開放平台日志,54機器線程啟動時間為:
2016-09-25 11:12:40,463] [dbMsgConsumer-3] [INFO] [taskLogger] [////] - [QueueConsumeTask started]
55機器線程啟動時間為:
2016-09-25 11:12:40,583] [dbMsgConsumer-2] [INFO] [taskLogger] [////] - [QueueConsumeTask started]
兩者僅相差120毫秒。
目前咱們mysql默認的事務隔離級別是REPETABLE READ(可重復讀),即在同一個事務內,多次查詢結果是一致的。
當54機器開啟事務,執行SQL(1)時,事務還未提交,55機器也執行SQL(1),此時55機器查詢到的記錄是更新前的,54機器提交事務,再去執行SQL(2),此時由於SQL(1)是范圍查詢,SQL(2)是主鍵查詢,SQL(2)的執行時間要遠遠少於SQL(1),會造成54機器執行SQL(2)時,55機器還未執行完成SQL(1),造成兩條機器互相搶占資源,造成死鎖。54和55兩台機器執行示意圖如下:
解決辦法:
修改SQL(1)為兩步操作,首先通過條件查詢出符合條件的記錄,然后根據查詢出的結果的主鍵id再進行update操作。
修改后sql,先執行查詢操作:
再執行修改操作,使用獲取到的主鍵ID
代碼修改如下:
經驗教訓:
電商無論前台后台的程序,都不應該存在僅根據非主鍵的幾個字段一查就要update/delete的場景。即使有,也應該改為先把要更新的記錄查出來然后逐條按主鍵id更新。
原文blog:http://blog.csdn.net/lzy_lizhiyang/article/details/52678446