更多优质内容
请关注公众号

Mysql事务和锁(四) 死锁-张柏沛IT博客

正文内容

Mysql事务和锁(四) 死锁

栏目:数据库 系列:Mysql事务和锁系列 发布时间:2020-11-03 23:01 浏览量:2767

死锁 

死锁是指的两个或者两个以上的事务在执行过程中,因为争夺锁资源而造成的一种互相等待的现象。

注意,必须是相互等待才会死锁,如果只是AB,是不会发生死锁的。如果是ABB也等A就会死锁;或者有2个以上的事务形成一个环路等待如:ABBCCA,也会死锁。


死锁的危害:

首先,mysql用的锁是一种自旋锁而不是普通的互斥锁,这意味着事务拿不到锁而被阻塞,等待其他事务释放锁的时候不会让出CPU,而是一直在空转。这个过程会一直消耗CPU

所以死锁的危害很明显,一方面阻塞了命令的执行,导致高并发无法进行;另一方面死锁会导致多个事务一直等待锁释放,CPU会被占满


所以如果在使用mysql发现CPU被占满的话,很可能是因为死锁的原因。


当然,mysql在发生锁等待的时候不会让事务一直等,而是会有一个超时时间。InnoDB引擎中,可以通过参数:innodb_lock_wait_timeout查询:

SHOW VARIABLES LIKE '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个常见的会发生死锁的情况:

情景:模拟秒杀时超卖

我们一般会在业务层也一段这样的逻辑(伪代码)

Begin	# 开始事务
store = select `store` from goods where id = 100 lock in share mode;		# 查询id为100的商品的库存, 这里必须加锁,否则查到的库存是历史库存(快照读),而不是最新的库存。
If store > 0 :
    Update goods set `store`=`store` - 1;		# 减少库存
    Insert into `order` values (xxxx);			# 添加订单
Commit;

现在假设表goods只有字段id store,而且store的值是1,也就是现在刚好只有一个库存。AB两个客户在抢这个商品。

# A和B同时begin:
# 客户端A执行
select `store` from goods where id = 100 lock in share mode;	# 查到是1
# 还未commit

# 恰好此时客户端B也执行到了select
select `store` from goods where id = 100 lock in share mode;	# 查到也是1
# 还未commit

# 此时A看到库存是1,于是开始减库存
Update goods set `store`=`store` - 1;

# 此时A发生阻塞,因为B已经对id为100的行上了一个S锁。但是此时还没有发生死锁,因为只要客户端B在此时执行commit或者rollback就可以解除A的阻塞,让A拿到X锁对商品的库存进行修改。

# 但是此时客户端B也去减库存,因为B也读到库存是1
Update goods set `store`=`store` - 1;

# 此时B也发生了阻塞,因为A在select的时候对id上了一个S锁。此时A在等B释放S锁,而B也在等待A释放S锁,于是发生死锁。这个时候A和B都无法执行commit或rollback结束事务和释放锁,只能一直相互等下去。

怎么解决?其实只要给 select X锁而不是加S锁就可以解决。


我们重新来一次:

# 客户端A执行
select `store` from goods where id = 100 for update;	# 查到是1
# 还未commit

# 恰好此时客户端B也执行到了select
select `store` from goods where id = 100  for update;  	# 被A的X锁阻塞

# 此时A看到库存是1,于是开始减库存
Update goods set `store`=`store` - 1;		# 库存变为0

# A执行commit

# 客户端B的阻塞被解除,但是查到了库存已经是0了。于是放弃减库存的update操作。


所以说 使用S锁来查库存剩余量无论从逻辑上还是考虑锁的方面都是错的,应该使用X锁来查。

由于mysql加锁的方式各种各样,所以出现死锁的情况也是千奇百怪。那么如果在真实项目中发生了死锁,我们该如何排查?

 


死锁的排   

 对线上偶发的 MySQL 死锁问题,我的排查处理过程如下:
1.线上错误日志报警发 现死锁异常
2.查看错误日志的堆栈信息
3.查看 MySQL 死锁相关的日志
4.根据 binlog 查看死锁相关事务的执行内容
5.根据上述信息找出两个相互死锁的事务执行的 SQL 操作,根据本系列介绍的锁相关理论知识,进行分析推断死锁原因
6.修改业务代码 

我们主要介绍1~4步。


首先在排查死锁之前,要先保证死锁日志和binlog日志已经开启: 


查看死锁日志的开启 和 开启记录死锁

show variables like "%innodb_print_all_deadlocks%";
set global innodb_print_all_deadlocks=1


查看binlog日志开启情况和位置

MariaDB [(none)]> show variables like "%log_bin%";
+---------------------------------+--------------------------------+
| Variable_name                   | Value                          |
+---------------------------------+--------------------------------+
| log_bin                         | ON                             |
| log_bin_basename                | /var/lib/mysql/mysql-bin       |
| log_bin_compress                | OFF                            |
| log_bin_compress_min_len        | 256                            |
| log_bin_index                   | /var/lib/mysql/mysql-bin.index |
| log_bin_trust_function_creators | OFF                            |
| sql_log_bin                     | ON                             |
+---------------------------------+--------------------------------+


查看错误日志位置

MariaDB [(none)]> show variables like "%log_error%";
+---------------+------------------------------+
| Variable_name | Value                        |
+---------------+------------------------------+
| log_error     | /var/log/mariadb/mariadb.log |
+---------------+------------------------------+


死锁日志的获取
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;  表示记录所有级别的错误。


下面我们模拟一个死锁:

客户端AB同时执行begin

客户端A先执行:
select * from x where id=1 lock in share mode;

客户端B再执行:
update x set name='zbp333' where id=3;
update x set name='zbp111' where id=1; # 阻塞, 等待A释放对id=1的共享锁

最后客户端A执行:
select * from x where id=3 lock in share mode; # 会等待B释放id=3的排他锁,此时A和B互相等待,发生死锁


下面是在错误日志中获取的死锁日志:

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 表示事务1update 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找。

# 执行下面的语句将binlog日志的内容写入到一个txt
mysqlbinlog -uroot -p mysql-bin.000059 --base64-output=decode-rows -v > /var/www/binlog.txt

然后我们根据死锁日志发生死锁的时间:2020-11-03 21:50:53”,在binlog中找这个时间上下的sql,而且死锁日志也告诉了你发生死锁的sql,因此也可以根据这个sqlbinlog日志中找这条sql上下的相关sql,这些sql都是可能引起最后那条sql发生死锁的原因。

PS:只有commit了的事务才会记录到binlog中,rollback回滚的事务中的所有sql是不会记到binlog中的。


binlog日志只显示了客户端A的事务的sql,没有记录客户端B的事务的sql,因为客户端B的事务由于死锁报错被回滚了。


​​​​​​​

死锁的避免

尽量将长事务拆分成多个小事务

查询时避免没有where条件语句查询,并尽可能使用索引查询

可以的话尽量使用等值查询,避免临键锁锁住间隙





更多内容请关注微信公众号
zbpblog微信公众号

如果您需要转载,可以点击下方按钮可以进行复制粘贴;本站博客文章为原创,请转载时注明以下信息

张柏沛IT技术博客 > Mysql事务和锁(四) 死锁

热门推荐
推荐新闻