死锁
死锁是指的两个或者两个以上的事务在执行过程中,因为争夺锁资源而造成的一种互相等待的现象。
注意,必须是相互等待才会死锁,如果只是A等B,是不会发生死锁的。如果是A等B,B也等A就会死锁;或者有2个以上的事务形成一个环路等待如:A等B,B等C,C等A,也会死锁。
死锁的危害:
首先,mysql用的锁是一种自旋锁而不是普通的互斥锁,这意味着事务拿不到锁而被阻塞,等待其他事务释放锁的时候不会让出CPU,而是一直在空转。这个过程会一直消耗CPU。
所以死锁的危害很明显,一方面阻塞了命令的执行,导致高并发无法进行;另一方面死锁会导致多个事务一直等待锁释放,CPU会被占满。
所以如果在使用mysql发现CPU被占满的话,很可能是因为死锁的原因。
当然,mysql在发生锁等待的时候不会让事务一直等,而是会有一个超时时间。在InnoDB引擎中,可以通过参数:innodb_lock_wait_timeout查询:
默认超时时间是50s,超时后会自动释放锁回滚事务。
如果执行命令会发生死锁的话,mysql会直接报错:Deadlock found when trying to get lock; try restarting transaction;
意思是:尝试去获取锁的时候发现会死锁,请重新开启一个事务。
很幸运是不是,mysql这个小机灵鬼还能检测出死锁。下面说说它是怎么检测出死锁的:
目前数据库大部分采用wait-for graph(等待图,图这种数据结构)的方式来进行死锁检测,InnoDB引擎也是采用这种方式来检测死锁。
等待图的节点代表一个事务,节点与节点之间的链接代表锁的等待指向关系。
当图中节点与节点间形成环路,则证明存在死锁:
如下图中,t1和t2之间存在环路,这就证明t1和t2事务之间存在死锁
下面我举1个常见的会发生死锁的情况:
情景:模拟秒杀时超卖
我们一般会在业务层也一段这样的逻辑(伪代码):
现在假设表goods只有字段id 和 store,而且store的值是1,也就是现在刚好只有一个库存。A和B两个客户在抢这个商品。
怎么解决?其实只要给 select 加X锁而不是加S锁就可以解决。
我们重新来一次:
所以说 使用S锁来查库存剩余量无论从逻辑上还是考虑锁的方面都是错的,应该使用X锁来查。
由于mysql加锁的方式各种各样,所以出现死锁的情况也是千奇百怪。那么如果在真实项目中发生了死锁,我们该如何排查?
死锁的排查
面
1.线上错误日志报警发 现死锁异常
2.查看错误日志的堆栈信息
3.查看 MySQL 死锁相关的日志
4.根据 binlog 查看死锁相关事务的执行内容
5.根据上述信息找出两个相互死锁的事务执行的 SQL 操作,根据本系列介绍的锁相关理论知识,进行分析推断死锁原因
6.修改业务代码 对线上偶发的 MySQL 死锁问题,我的排查处理过程如下:
我们主要介绍1~4步。
首先在排查死锁之前,要先保证死锁日志和binlog日志已经开启:
查看死锁日志的开启 和 开启记录死锁
查看binlog日志开启情况和位置
查看错误日志位置
死锁日志的获取
1.show engine innodb status 命令获取死锁信息,但是该命令只能获取最近一次的死锁信息。
2.通过在错误日志查找死锁日志。只要开启了死锁日志(innodb_print_all_deadlocks),那么当发生死锁的时候,会将死锁日志一起记录到错误日志中。
请注意,死锁错误是Note级别的错误,如果在错误日志中找不到死锁的错误信息可能时因为设置了错误级别导致Note级别的错误没有记录到日志中。
我们可以查看log_error_verbosity变量,它表示要记录到错误日志的级别。这个变量有三个值:1、2、3,默认是3. 他们的意义是:
1 -- Errors Only
2 -- Errors and warnings
3 -- Errors, warnings, and notes
set global log_error_verbosity=3; 表示记录所有级别的错误。
下面我们模拟一个死锁:
客户端A、B同时执行begin
下面是在错误日志中获取的死锁日志:
2020-11-03 21:50:53 15899 [Note] InnoDB: Transactions deadlock detected, dumping detailed information.
2020-11-03 21:50:53 15899 [Note] InnoDB:
*** (1) TRANSACTION: # 1
TRANSACTION 4247, ACTIVE 60 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1
MySQL thread id 15890, OS thread handle 140010633271040, query id 1221457 localhost root Updating
update x set name='zbp111' where id=1 #2
2020-11-03 21:50:53 15899 [Note] InnoDB: *** (1) WAITING FOR THIS LOCK TO BE GRANTED: #3
RECORD LOCKS space id 50 page no 3 n bits 88 index PRIMARY of table `test`.`x` trx id 4247 lock_mode X locks rec but not gap waiting # 4
Record lock, heap no 19 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
0: len 4; hex 80000001; asc ;;
1: len 6; hex 000000000000; asc ;;
2: len 7; hex 80000000000000; asc ;;
3: len 5; hex 7a62703131; asc zbp11;;
4: len 2; hex 7831; asc x1;;
2020-11-03 21:50:53 15899 [Note] InnoDB: *** (2) TRANSACTION: # 5
TRANSACTION 421485610914136, ACTIVE 71 sec starting index read
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 15899, OS thread handle 140010632656640, query id 1221458 localhost root Statistics
select * from x where id=3 lock in share mode # 6
2020-11-03 21:50:53 15899 [Note] InnoDB: *** (2) HOLDS THE LOCK(S): #7
RECORD LOCKS space id 50 page no 3 n bits 88 index PRIMARY of table `test`.`x` trx id 421485610914136 lock mode S locks rec but not gap # 8
Record lock, heap no 19 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
0: len 4; hex 80000001; asc ;;
1: len 6; hex 000000000000; asc ;;
2: len 7; hex 80000000000000; asc ;;
3: len 5; hex 7a62703131; asc zbp11;;
4: len 2; hex 7831; asc x1;;
2020-11-03 21:50:53 15899 [Note] InnoDB: *** (2) WAITING FOR THIS LOCK TO BE GRANTED: # 9
RECORD LOCKS space id 50 page no 3 n bits 88 index PRIMARY of table `test`.`x` trx id 421485610914136 lock mode S locks rec but not gap waiting # 10
Record lock, heap no 20 PHYSICAL RECORD: n_fields 5; compact format; info bits 0
0: len 4; hex 80000003; asc ;;
1: len 6; hex 000000001097; asc ;;
2: len 7; hex 46000001c501d6; asc F ;;
3: len 6; hex 7a6270333333; asc zbp333;;
4: len 3; hex 787878; asc xxx;;
2020-11-03 21:50:53 15899 [Note] InnoDB: *** WE ROLL BACK TRANSACTION (2) #11
死锁日志中的重点部分我用 #n 标记了出来
#1 表示事务1
#2 表示死锁发生前事务1的最后一条sql(但不是事务1提交前的最后一条sql)
#3 表示事务1的update x ..... 想要获取的锁。
#4 lock_mode X locks rec but not gap waiting 表示事务1想获取一个记录锁因而等待(最后一个单词waiting)。
记录锁(LOCK_REC_NOT_GAP): lock_mode X locks rec but not gap
间隙锁(LOCK_GAP): lock_mode X locks gap before rec
Next-key 锁(LOCK_ORNIDARY): lock_mode X
插入意向锁(LOCK_INSERT_INTENTION): lock_mode X locks gap before rec insert intention
#5 事务2
#6 表示死锁发生前事务2的最后一条sql
#7 和 #8 表示事务2当时正持有的锁时一个S记录锁(是A客户端执行的第一个select持有的锁)
#9 和 #10 表示事务2想获取一个S记录锁(是A客户端第二个select想要获取的锁)
#11 表示mysql回滚了事务2。说明是事务2发生的死锁,因为发生死锁会报错,报错就会自动回滚。
现在能够初步断定的是,是事务2在执行
select * from x where id=3 lock in share mode
的时候发生了死锁,并且在死锁之前,事务2已经持有了一个共享锁。
根据以上信息,我们再在binlog日志查找更多线索,因为死锁日志只能给出2条和死锁有关的sql。想要知道死锁发生时的更多的sql只能在binlog找。
然后我们根据死锁日志发生死锁的时间:“2020-11-03 21:50:53”,在binlog中找这个时间上下的sql,而且死锁日志也告诉了你发生死锁的sql,因此也可以根据这个sql在binlog日志中找这条sql上下的相关sql,这些sql都是可能引起最后那条sql发生死锁的原因。
PS:只有commit了的事务才会记录到binlog中,rollback回滚的事务中的所有sql是不会记到binlog中的。
binlog日志只显示了客户端A的事务的sql,没有记录客户端B的事务的sql,因为客户端B的事务由于死锁报错被回滚了。
尽量将长事务拆分成多个小事务
查询时避免没有where条件语句查询,并尽可能使用索引查询
可以的话尽量使用等值查询,避免临键锁锁住间隙