数据库问题中,由于SQL问题导致的数据库故障是最为常见的,本文针对曾经负责的一个核心系统在上线新业务功能抛出了许多 MySQL 死锁导致事务回滚的异常,给出了详细的排查流程:
某天晚上,某核心应用在生产环境正在发布,突然线上大量报警,很多异常信息都是关于数据库死锁的
Deadlock found when trying to get lock; try restarting transaction
Mysql数据库基础信息:
去除业务中非重要字段,出现死锁异常的数据库表结构为:
CREATE TABLE `bs_customer_sass_cache` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`gmt_create` datetime NOT NULL COMMENT '创建时间',
`gmt_modified` datetime NOT NULL COMMENT '修改时间',
`cpb_mc` varchar(256) NOT NULL COMMENT '产品说明',
`cpb_id` varchar(64) NOT NULL COMMENT '产品Id',
`state` varchar(64) DEFAULT NULL COMMENT '状态',
`cpb_no` varchar(256) DEFAULT NULL COMMENT '产品订单号',
PRIMARY KEY (`id`),
KEY `idx_cpbId_cpbNo` (`cpb_id`,`cpb_no`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8 COMMENT='sass缓存'
bs_customer_sass_cache
表中包含两个索引:主键索引和idx_cpbId_cpbNo
由 (cpb_id
,cpb_no
) 字段构成的非主键联合索引。
两个或更多个列上的索引被称作联合索引,也称为复合索引。对于复合索引:Mysql从左到右的使用索引中的字段,一个查询可以只使用索引中的一部份,但只能是最左侧部分(最左前缀原则)。例如索引是key index (a,b,c)。可以支持a | a,b| a,b,c 3种组合进行索引。
默认,MySQL session开启自动提交模式(变量autocommit为ON)。只要你执行DML操作的语句, MySQL会立即隐式提交事务(Implicit Commit)。变量autocommit分会话系统变量与全局系统变量:
$Mysql> show session variables like 'autocommit';
$Mysql> show global variables like 'autocommit';
上述SQL修改会话系统变量或全局系统变量,只对当前实例有效,如果MySQL服务重启的话,这些设置就会丢失:
$Mysql> set session autocommit=0;
$Mysql> set global autocommit=0;
$Mysql> show engine innodb status
上诉命令显示的不是当前状态,而是过去某个时间范围内InnoDB存储引擎的状态。
Per second averages calculated from the last 8 seconds
死锁日志主要包含以下几个部分:
Content | Description |
---|---|
BACKGROUND THREAD | 后台Master线程 |
SEMAPHORES | 信号量信息 |
LATEST DETECTED DEADLOCK | 最近一次死锁信息,只有产生过死锁才会有 |
TRANSACTIONS | 事物信息 |
FILE I/O | IO Thread信息 |
INSERT BUFFER AND ADAPTIVE HASH INDEX | INSERT BUFFER和自适应HASH索引 |
LOG | 日志 |
BUFFER POOL AND MEMORY | BUFFER POOL和内存 |
INDIVIDUAL BUFFER POOL INFO | 如果设置了多个BUFFER POOL实例,这里显示每个BUFFER POOL信息。可通过innodb_buffer_pool_instances参数设置 |
ROW OPERATIONS | 行操作统计信息 |
END OF INNODB MONITOR OUTPU | 日志 |
TRANSACTIONS
包含了InnoDB事务(transaction)的统计信息。
$Mysql> select * from information_schema.innodb_trx;
innodb_trx ## 当前运行的所有事务
innodb_locks ## 当前出现的锁
innodb_lock_waits ## 锁等待的对应关系
查出innodb_trx中死锁事务的trx_mysql_thread_id,然后kill掉。
$Mysql> show processlist;
$Mysql> kill thread_id
查询全局等待事务锁超时时间
$Mysql> SHOW GLOBAL VARIABLES LIKE ‘innodb_lock_wait_timeout’;
设置全局等待事务锁超时时间
$Mysql> SET GLOBAL innodb_lock_wait_timeout=100;
查询当前会话等待事务锁超时时间
$Mysql> SHOW VARIABLES LIKE ‘innodb_lock_wait_timeout’;
记录最近一次死锁信息,只有产生过死锁才会有记录。查看死锁日志:
------------------------
LATEST DETECTED DEADLOCK
------------------------
2020-04-26 17:36:23 0x7fb82c9b6700
*** (1) TRANSACTION:
TRANSACTION 124829516, ACTIVE 11 sec fetching rows
mysql tables in use 1, locked 1
LOCK WAIT 4 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 1
MySQL thread id 513161, OS thread handle 140429259630336, query id 11331899 10.200.7.9 hswy_basic Searching rows for update
/* ApplicationName=IntelliJ IDEA 2019.3.4 */ update `bs_customer_sass_cache`
set `gmt_modified` = sysdate(), `state` = ''1''
where (`state` = ''0'' AND `cpb_id` = ''20200426'' AND `cpb_no` = ''2020042615158064425'')
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 5810 page no 3 n bits 72 index PRIMARY of table `eagle_dev`.`bs_customer_sass_cache` trx id 124829516 lock_mode X locks rec but not gap waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 9; compact format; info bits 0
0: len 8; hex 0000000000000002; asc ;;
1: len 6; hex 00000770bf4d; asc p M;;
2: len 7; hex 3a0000013728e0; asc : 7( ;;
3: len 5; hex 99a63513f5; asc 5 ;;
4: len 5; hex 99a635190d; asc 5 ;;
5: len 10; hex e4baa7e59381e58c8532; asc 2;;
6: len 8; hex 3230323030343236; asc 20200426;;
7: len 1; hex 30; asc 0;;
8: len 19; hex 32303230303432363135313538303634343235; asc 2020042615158064425;;
*** (2) TRANSACTION:
TRANSACTION 124829517, ACTIVE 10 sec starting index read
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 513162, OS thread handle 140428999091968, query id 11331918 10.200.7.9 hswy_basic Searching rows for update
/* ApplicationName=IntelliJ IDEA 2019.3.4 */ update `bs_customer_sass_cache`
set `gmt_modified` = sysdate(), `state` = ''1''
where (`state` = ''0'' AND `cpb_id` = ''20200426'' AND `cpb_no` = ''2020042615158064425'')
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 5810 page no 3 n bits 72 index PRIMARY of table `eagle_dev`.`bs_customer_sass_cache` trx id 124829517 lock_mode X locks rec but not gap
Record lock, heap no 3 PHYSICAL RECORD: n_fields 9; compact format; info bits 0
0: len 8; hex 0000000000000002; asc ;;
1: len 6; hex 00000770bf4d; asc p M;;
2: len 7; hex 3a0000013728e0; asc : 7( ;;
3: len 5; hex 99a63513f5; asc 5 ;;
4: len 5; hex 99a635190d; asc 5 ;;
5: len 10; hex e4baa7e59381e58c8532; asc 2;;
6: len 8; hex 3230323030343236; asc 20200426;;
7: len 1; hex 30; asc 0;;
8: len 19; hex 32303230303432363135313538303634343235; asc 2020042615158064425;;
*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 5810 page no 4 n bits 72 index idx_cpbId_cpbNo of table `eagle_dev`.`bs_customer_sass_cache` trx id 124829517 lock_mode X waiting
Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
0: len 8; hex 3230323030343236; asc 20200426;;
1: len 19; hex 32303230303432363135313538303634343235; asc 2020042615158064425;;
2: len 8; hex 0000000000000001; asc ;;
*** WE ROLL BACK TRANSACTION (2)
TRANSACTION 124829516, ACTIVE 11 sec fetching rows
事务编号为 124829516 ,活跃了11秒,fetching rows 表示事务状态为正在查询记录。常见的其他状态:
updating or deleting
:表示事物已经真正进入了update/delete的函数逻辑(row_update_for_mysql)
starting index read
:表示事务状态为根据索引读取数据。
mysql tables in use 1, locked 1
mysql tables in use 1
有一个表被使用(trx->n_mysql_tables_in_use) ,在函数ha_innobase::external_lock中trx->n_mysql_tables_in_use被递增。
locked 1
表示表上有一个表锁(加锁函数为lock_table,trx->mysql_n_tables_locked),对于DML语句为LOCK_IX
LOCK WAIT 4 lock struct(s), heap size 1136, 4 row lock(s), undo log entries 1
此事务处于LOCK WAIT状态,拥有4个锁结构(4个行锁结构,锁结构???),heap size是为了存储锁结构而申请的内存大小(可以忽略),其中有4个行锁的结构。undo log entries 1
表示当前事务有 1个 undo log 记录。
MySQL thread id 513161, OS thread handle 140429259630336, query id 11331899 10.200.7.9 customer_basic_user Searching rows for update
本事务所在MySQL线程的id是513161,该线程在操作系统级别的id就是140429259630336,当前查询的id为11331899(MySQL内部使用,可以忽略),还有用户名主机信息。
update
bs_customer_sass_cache
setgmt_modified
= sysdate(),state
= ‘‘1’’
where (state
= ‘‘0’’ ANDcpb_id
= ‘‘20200426’’ ANDcpb_no
= ‘‘2020042615158064425’’)
本事物发生阻塞的SQL语句
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
本事务当前在等待获取的锁:
RECORD LOCKS space id 5810 page no 3 n bits 72 index PRIMARY of table
eagle_dev
.bs_customer_sass_cache
trx id 124829516 lock_mode X locks rec but not gap waiting
等待获取的表空间ID为5810 ,页号为3,n bits 72表示这个聚集索引记录锁结构上留有72个Bit位。该锁的类型是X型记录锁(rec but not gap)。
lock_mode X表示该记录锁为排他锁:lock->type_mode & LOCK_MODE_MASK
其他还有:
” locks gap before rec”表示为gap锁:lock->type_mode & LOCK_GAP
” locks rec but not gap”表示为记录锁,非gap锁:lock->type_mode & LOCK_REC_NOT_GAP
” insert intention”表示为插入意向锁:lock->type_mode & LOCK_INSERT_INTENTION
“ waiting” 表示锁等待:lock->type_mode & LOCK_WAIT
继续来分析应用中发生死锁的事物代码位置:
@Transactional
public void updateStatus(Long id, String cpbId, String cpbNo) {
customerSassCacheDAO.updateCpbNo(id, cpbId, cpbNo);
customerSassCacheDAO.updateStatus(cpbId, cpbNo, '0');
}
上诉代码的目的是为了修改同一条记录的两个字段
updateCpbNo 对应的mybatis SQL:
update bs_customer_sass_cache set gmt_modified=sysdate(), cpb_no = #{cpbNo} where id =#{id} and cpb_id = #{cpbId};
updateStatus对应的mybatis SQL:
update `bs_customer_sass_cache` set `gmt_modified` = sysdate(), `state` = '1' where `state` = #{state} AND `cpb_id` = #{cpbId} AND `cpb_no` = #{cpbNo};
updateCpbNo用到的是主键索引,updateStatus用的的是idx_cpbId_cpbNo
索引。
通过MySQL begin/commit语句模拟两个并发的数据库事物:
事务T1:
begin
update bs_customer_sass_cache
set gmt_modified=sysdate(), cpb_no = '2020042615158064425'
where id = 1 and cpb_id = '20200426';
select sleep(10);
update `bs_customer_sass_cache`
set `gmt_modified` = sysdate(), `state` = '1'
where (`state` = '0' AND `cpb_id` = '20200426' AND `cpb_no` = '2020042615158064425');
select sleep(50);
commit;
事务T2:
begin;
update bs_customer_sass_cache
set gmt_modified=sysdate(), cpb_no = '2020042615158064425'
where id = 2 and cpb_id = '20200426';
select sleep(10);
update `bs_customer_sass_cache`
set `gmt_modified` = sysdate(), `state` = '1'
where (`state` = '0' AND `cpb_id` = '20200426' AND `cpb_no` = '2020042615158064425');
select sleep(50);
commit;
InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!
Mysql行级锁并不是直接锁记录,而是锁索引,如果一条SQL语句用到了主键索引,mysql会锁住主键索引;如果一条语句操作了非主键索引,mysql会先锁住非主键索引,再锁定主键索引。
事物T1执行第一条SQL语句获得了primary id=1主键索引对应的锁,事物T2执行第一条SQL语句时获取了primary id=2主键索引对应的锁;事物T1执行第二条SQL语句时,占有了idx_cpbId_cpbNo
(20200426, 2020042615158064425)非主键索引,尝试占有primary id=2主键索引对应的锁失败;事物T2执行第二条SQL语句时,占有idx_cpbId_cpbNo
(20200426, 2020042615158064425)非主键索引对应的锁失败。
主键索引: Mysql数据库InnoDB引擎使用B+Tree作为索引结构,InnoDB的数据文件本身就是索引文件,树的叶节点data域保存了完整的数据记录,这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。B+Tree叶节点包含了完整的数据记录,这种索引叫做聚集索引。
辅助索引: InnoDB的辅助索引data域存储相应记录主键的值而不是地址。换句话说,InnoDB的所有辅助索引都引用主键作为data域。
上诉索引简化的结构如下:
死锁 是并发系统中常见的问题,同样也会出现在Innodb系统中。当两个及以上的事务,双方都在等待对方释放已经持有的锁或者因为加锁顺序不一致造成循环等待锁资源,就会出现"死锁"。举例来说A 事务持有x1锁 ,申请x2锁,B 事务持有x2锁,申请x1 锁。A和B 事务持有锁并且申请对方持有的锁进入循环等待,就造成死锁。
READ UNCOMMITTED(读未提交数据)
允许事务读取未被其他事务提交的变更,脏读、不可重复读和幻读的问题都会出现
READ COMMITED(读已提交数据)
只允许事务读取已经被其他事务提交的变更,可以避免脏读,但不可重复读和幻读问题仍然会出现
REPEATABLE READ(可重复读)
确保事务可以多次从一个字段中读取相同的值,在这个事务持续期间,禁止其他事务对这个字段进行更新,可以避免脏读和不可重复读,但幻读的问题依然存在
SERIALIZABLE(串行化)
确保事务可以从一个表中读取相同的行,在这个事务持续期间,禁止其他事务对该表执行插入、更新和删除操作,所有并发问题都可以避免,但性能十分低
共享锁又称为读锁,简称S锁,顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。
排他锁又称为写锁,简称X锁,顾名思义,排他锁就是不能与其他所并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。
间隙锁(Gap Lock)是Innodb在提交下为了解决幻读问题时引入的锁机制,(下面的所有案例没有特意强调都使用可重复读隔离级别)幻读的问题存在是因为新增或者更新操作,这时如果进行范围查询的时候(加锁查询),会出现不一致的问题,这时使用不同的行锁已经没有办法满足要求,需要对一定范围内的数据进行加锁,间隙锁就是解决这类问题的。
死锁在操作系统中指的是两个或两个以上的进程在执行的过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或者系统产生了死锁,这些永远在互相等待的进程称为死锁进程。
死锁出现的原因排查过程已详细解释了死锁背后发生的原理,我们来解决这个问题。
更新事物时尽可能带上主键
在一个transaction中,更新update操作尽可能不要操作同一条记录
死锁是指两个或两个以上的进程在执行过程中,因争夺资源而造成的一种互相等待的现象。正常死锁会自动释放,innodb有一个内在的死锁检测工具,当死锁超过一定时间后,会回滚其中一个事务,innodb_lock_wait_timeout可配置死锁等待超时时间。
死锁在两情况下最容易产生:
高并发同时操作同一条数据
存在主键和辅助索引,加锁顺序相反
避免死锁方法即降低并发,操作数据时使加锁顺序相同。
个人博客:http://www.geek-make.com。