2019.11.20 賬戶系統死鎖問題排查分析。
原文入口: http://www.qianshan.tech
問題出現
項目出現死鎖告警,日誌中出現報錯:
Error updating database. Cause: com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
聯繫dba查看死鎖日誌如下:
mysql查看死鎖日誌命令: show engine innodb status;
背景
對業務邏輯不做贅述,簡單理解爲賬戶交易採用TCC的模式,先凍結,再確認入賬或撤銷。
對應的有三個事務,
- 請求處理事務: 鎖定賬戶、請求記錄入庫、更新賬戶(凍結金額+ 可用餘額 -)
- 入賬確認事務: 鎖定請求記錄、鎖定賬戶、更新賬戶(凍結金額-)
- 入賬取消事務: 鎖定請求記錄、鎖定賬戶、更新賬戶(凍結金額- 可用餘額 +)
對應主要有兩張表:賬戶主體表(account)和請求記錄表(trans_log),因爲問題主要出在賬戶表操作上,在此只貼出賬戶表關鍵字段。如下
CREATE TABLE `account` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵字段',
`account_no` varchar(32) NOT NULL COMMENT '賬戶號',
`customer_no` varchar(32) NOT NULL COMMENT '客戶號',
`balance` decimal(15,2) NOT NULL DEFAULT '0.00' COMMENT '有效餘額',
`frozen_amount` decimal(15,2) NOT NULL DEFAULT '0.00' COMMENT '凍結餘額',
`status` varchar(32) NOT NULL COMMENT '賬戶狀態(正常:NORMAL 凍結:FROZEN 失效:DISABLED)',
`account_type` varchar(32) NOT NULL COMMENT '賬戶類型碼',
PRIMARY KEY (`id`) COMMENT '主鍵',
UNIQUE KEY `uk_account_no` (`account_no`),
UNIQUE KEY `uk_customer_acctype` (`customer_no`,`account_type`)
) ENGINE=InnoDB AUTO_INCREMENT=4491 DEFAULT CHARSET=utf8mb4 COMMENT='賬戶主體表';
INSERT INTO `account`(`id`,`account_no`,`customer_no`,`balance`, `frozen_amount`, `status`, `account_type`)
VALUES (1, '888888','123456',10.00, 0.00, 'NORMAL', 'BALANCE');
賬戶表有兩個唯一索引,一是根據賬戶號索引,二是根據客戶id和客戶類型聯合索引。
問題排查
檢測到死鎖拋出異常的地方在請求處理事務的update的時候,根據死鎖日誌查看引起死鎖的是PRIMARY主鍵索引和uk_uk_account_no索引。(雖然insert動作也會有加鎖動作,但根據死鎖日誌排除,以下對insert不用關注)
採用以下時序復現了死鎖場景:
時序分析:
- 請求處理事務: 先根據uk_customer_acctype索引進行鎖定,實際上鎖定了uk_customer_acctype索引簇記錄和主鍵索引簇上對應的記錄。
- 入賬確認事務: 根據唯一索引uk_account_no鎖定,先對uk_account_no索引簇記錄加X鎖成功,對主鍵記錄上鎖時阻塞等待
- 請求處理事務: update更新時因爲是根據account_no條件進行更新,所以嘗試對uk_account_no索引簇加鎖,而對應的鎖已經被入賬確認事務佔用,形成死鎖環。
總結
注意for update對二級索引(非聚簇索引)加X鎖時,也會同時對主鍵索引(聚簇索引)記錄加鎖。