作者:微信小助手
发布时间:2019-05-10T20:08
前几天,线上发生了一次数据库死锁问题,这一问题前前后后排查了比较久的时间,这个过程中自己也对数据库的锁机制有了更深的理解。
本文总结了这次死锁排查的全过程,并分析了导致死锁的原因及解决方案。希望给大家提供一个死锁的排查及解决思路。
本文涉及到 MySQL 执行引擎、数据库隔离级别、InnoDB 锁机制、索引、数据库事务等多领域知识。前车之鉴,后事之师,希望读者们都可以有所收获。
现象
某天晚上,同事正在发布,突然线上大量报警,很多是关于数据库死锁的,报警提示信息如下:
{"errorCode":"SYSTEM_ERROR","errorMsg":"nested exception is org.apache.ibatis.exceptions.PersistenceException:
Error updating database. Cause: ERR-CODE: [TDDL-4614][ERR_EXECUTE_ON_MYSQL]
Deadlock found when trying to get lock;
The error occurred while setting parameters\n### SQL:
update fund_transfer_stream set gmt_modified=now(),state = ? where fund_transfer_order_no = ? and seller_id = ? and state = 'NEW'
通过报警,我们基本可以定位到发生死锁的数据库以及数据库表。先来介绍下本文案例中涉及到的数据库相关信息。
背景情况
我们使用的数据库是 MySQL 5.7,引擎是 InnoDB,事务隔离级别是 READ-COMMITED。
数据库版本查询方法:
select version();
引擎查询方法:
show create table fund_transfer_stream;
建表语句中会显示存储引擎信息,形如:ENGINE=InnoDB。
事务隔离级别查询方法:
select @@tx_isolation;
事务隔离级别设置方法(只对当前 Session 生效):
set session transaction isolation level read committed;
PS:注意,如果数据库是分库的,以上几条 SQL 语句需要在单库上执行,不要在逻辑库执行。
发生死锁的表结构及索引情况(隐去了部分无关字段和索引):
CREATE TABLE `fund_transfer_stream` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`gmt_create` datetime NOT NULL COMMENT '创建时间',
`gmt_modified` datetime NOT NULL COMMENT '修改时间',
`pay_scene_name` varchar(256) NOT NULL COMMENT '支付场景名称',
`pay_scene_version` varchar(256) DEFAULT NULL COMMENT '支付场景版本',
`identifier` varchar(256) NOT NULL COMMENT '唯一性标识',
`seller_id` varchar(64) NOT NULL COMMENT '卖家Id',
`state` varchar(64) DEFAULT NULL COMMENT '状态', `fund_transfer_order_no` varchar(256)
DEFAULT NULL COMMENT '资金平台返回的状态',
PRIMARY KEY (`id`),UNIQUE KEY `uk_scene_identifier`
(KEY `idx_seller` (`seller_id`),
KEY `idx_seller_transNo` (`seller_id`,`fund_transfer_order_no`(20))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='资金流水';
该数据库共有三个索引,1 个聚簇索引(主键索引),2 个非聚簇索引(非主键索引)。
聚簇索引:
PRIMARY KEY (`id`)
非聚簇索引:
KEY `idx_seller` (`seller_id`),
KEY `idx_seller_transNo` (`seller_id`,`fund_transfer_order_no`(20))
以上两个索引,其实 idx_seller_transNo 已经覆盖到了 idx_seller,由于历史原因,该表以 seller_id 分表,所以是先有的 idx_seller,后有的 idx_seller_transNo。
死锁日志
当数据库发生死锁时,可以通过以下命令获取死锁日志:
show engine innodb status
发生死锁,第一时间查看死锁日志,得到死锁日志内容如下:
Transactions deadlock detected, dumping detailed information.
2019-03-19T21:44:23.516263+08:00 5877341 [Note] InnoDB:
*** (1) TRANSACTION:
TRANSACTION 173268495, ACTIVE 0 sec fetching rows
mysql tables in use 1, locked 1
LOCK WAIT 304 lock struct(s), heap size 41168, 6 row lock(s), undo log entries 1
MySQL thread id 5877358, OS thread handle 47356539049728, query id 557970181 11.183.244.150 fin_instant_app updating
update `fund_transfer_stream` set `gmt_modified` = NOW(), `state` = 'PROCESSING' where ((`state` = 'NEW') AND (`seller_id` = '38921111') AND (`fund_transfer_order_no` = '99010015000805619031958363857'))
2019-03-19T21:44:23.516321+08:00 5877341 [Note] InnoDB:
*** (1) HOLDS THE LOCK(S):
RECORD LOCKS space id 173 page no 13726 n bits 248 index idx_seller_transNo of table `xxx`.`fund_transfer_stream` trx id 173268495 lock_mode X locks rec but not gap
Record lock, heap no 168 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
2019-03-19T21:44:23.516565+08:00 5877341 [Note] InnoDB:
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 173 page no 12416 n bits 128 index PRIMARY of table `xxx`.`fund_transfer_stream` trx id 173268495 lock_mode X locks rec but not gap waiting
Record lock, heap no 56 PHYSICAL RECORD: n_fields 17; compact format; info bits 0
2019-03-19T21:44:23.517793+08:00 5877341 [Note] InnoDB:
*** (2) TRANSACTION:
TRANSACTION 173268500,&n