Mysql 百问系列:死锁是怎么发生的

1,887 阅读4分钟

本文主要从共享锁(S锁)和独占锁(X锁)出发,详细说明两种锁的加锁机制,以及死锁如何产生。

问题:

  1. 什么是共享锁,什么是独占锁?
  2. 死锁怎么发生的?
  3. 发生死锁了,Mysql 如何处理的?

共享锁和独占锁

上一篇文章中我们已经讲解了共享锁和独占锁的基本概念,我这边再详细将一下。

  • 共享锁 Shared Locks (S 锁)  从名字就可以看出来它允许共享,共享的意思是当你给某个事物加上锁后,其他人还可以为它加上(仅限)共享锁。
  • 独占锁 Exclusive Locks (X 锁), 如果想要修改某个事物,必须先加上X锁,但是如果该事物已经错在了S锁或者X锁就必须等已经存在的锁释放了才能再加上去。
S锁 X锁
S锁 兼容 不兼容
X锁 不兼容 不兼容

了解了S锁和X锁,那么我们来看看什么是死锁吧。
让我们来复现下死锁的过程,以便更好理解。建议可以根据自己理解来复现死锁过程,并分析原因加深理解。不然变成:
眼睛: 我会了。
脑子: 不,你不会。

准备工作

CREATE TABLE `student` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `age` int(11) NOT NULL,
  `addresss` varchar(255) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT student VALUES (1,'张一',12,'ssss'),(2,'张二',12,'ssss'),(3,'张三',12,'ssss'),(4,'张四',12,'ssss'),(5,'张五',12,'ssss');

准备工作完成,建立表,并插入5条数据。

模拟死锁第一步
新建一个数据库连接并执行:

###事务A
begin;
select * from student where id in (1,2) lock in share mode;

步骤一 事务A 得到了id 为1,2的S锁。
再新建一个数据库连接并执行:

###事务B
begin;
select * from student where id in (1,3) lock in share mode;

步骤二 这时事务B 得到了id为1,3的S锁
然后我们在事务B 中尝试获取数据id = 2 的X锁

事务B
update student set age  =100 where id = 2

步骤三, 由于id为2 被事务A加了S锁,所以事务B 被阻塞。
最后我们在事务A中尝试获取数据id = 3 的X锁

事务A
update student set age  =100 where id = 3

#执行结果:1213 - Deadlock found when trying to get lock; try restarting transaction, Time: 0.005000s

步骤四,由于id为3 被事务B加了S锁,所以事务A被阻塞。 当然其实执行的时候被跳出死锁警告。事务A回滚撤销了。

到此,我们模拟了整个死锁过程,由于B 为了获得X锁需要等A执行完成,而A后续反而为了获得X锁需要等B执行完成。所以造成了死锁。

死锁的原因

从上面的例子中我们可以看到死锁的原因是相互寻求各自手上的资源(即锁)导致的。
但是其中有个重要的特性需要说明就是二阶段锁协议。 
二阶段锁协议很简单:

  1. 需要查询或者修改时才加锁。
  2. 事务提交的时候才释放锁。

上面例子中步骤三,B之所以会被阻塞,是因为事务A虽然读取数据结束了,但是事务还没结束,所以导致读取是加的锁没有释放。
也是由于二阶段锁协议的原因,所以我们应该尽可能避免长事务。因为`事务执行时间越长,加锁的时间越长,发生死锁的概率越大```

死锁如何处理

mysql 可以设置两种处理方式:

  1. 第一种就是上面例子锁展示的,发生死锁时,自动回滚其中一个事务,解除死锁。 可以通过innodb_deadlock_detect 参数设置为on 开启。默认情况下就是on 状态。
  2. 设置超时时间,也就是发生死锁时先等待一段时间后,再退出。可以设置innodb_lock_wait_timeout 设置超时时间。

一般情况下,我们会用第一种方式。

留个问题

现实场景中有哪些容易发生死锁的场景呢?其中最常见的一个例子是减库存了。
扣减库存我们一般有以下步骤

  1. 查看现有库存是否充足
  2. 库存充足则减去库存。 不足则返回扣除失败。

假设流程的sql语句如下

# 查询剩余库存
select last_number from stock where  id = 1000 lock in share mode;

#库存足够 --减去库存
update stock set last_number = last_number - 1 where id = 1000;

那么问题是:

  1. 当并发比较高的时候,上面这个业务功能会不会发生死锁,分析下发生死锁的原因?
  2. 为什么要用lock in share mode 加锁,不加锁行不行? 高并发下,如果不加锁会发生什么问题?
  3. 怎么修改才能不发生死锁,又能避免其他问题?

问题答案下篇文章回答。有收获,请关注下吧。