首先死锁产生的原因:两个及以上事务争夺资源导致互相等待造成的
比如事务A先修改id为1的数据再去修改id为2的数据,事务B先修改id为2的数据再去修改id为1的数据,因为事务A先拿到id1的锁再去拿id2的锁,而事务B先拿到id2的锁又去拿id1的锁,此时死锁就产生了
下面我们来演示一下(以下所有操作都是在MySQL5.7.43下进行,操作工具为Navicat)
先建一张表
CREATE TABLE `t_test` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(32) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`update_time` datetime DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
往里面加两条数据
然后我们打开两个查询窗口
窗口一
START TRANSACTION; # 开启事务
UPDATE t_test SET age = 21 WHERE id = 1
UPDATE t_test SET age = 31 WHERE id = 2
COMMIT; # 提交事务
ROLLBACK; # 回滚
窗口二
START TRANSACTION; # 开启事务
UPDATE t_test SET age = 32 WHERE id = 2
UPDATE t_test SET age = 22 WHERE id = 1
COMMIT; # 提交事务
ROLLBACK; # 回滚
我们先去窗口一启动事务(执行第一行)
然后去窗口二开启事务(执行第一行)
回到窗口一,执行修改id1的数据
刷新表格可以看到因为开启了事务数据没有更新
再去窗口二,执行修改id2的数据
一样的刷新表格,数据此时没有更新进来
回到窗口一,执行修改id2的数据
可以发现窗口上显示正在处理,下面信息里面也显示UPDATE t_test SET age = 31 WHERE id = 2,但是并没有显示执行完成,因为在等待id2的锁,而此时id2的锁在窗口二那里
这个时候我们再去窗口二,执行修改id1的数据
可以看到已经提示死锁Deadlock found when trying to get lock; try restarting transaction
此时我们提交窗口二的事务
刷新数据,因为已经异常了数据不会更新的
我们再回到窗口一,提交事务
刷新数据,显示数据提交成功
看到没有,这就是死锁产生的原因,两个事务按照不同的顺序去拿锁产生了冲突
那如果两个事务按相同的顺序拿锁会死锁吗?我们改一下两个窗口的SQL执行顺序
窗口一
START TRANSACTION; # 开启事务
UPDATE t_test SET age = 40 WHERE id = 1
UPDATE t_test SET age = 50 WHERE id = 2
COMMIT; # 提交事务
ROLLBACK; # 回滚
窗口二
START TRANSACTION; # 开启事务
UPDATE t_test SET age = 42 WHERE id = 1
UPDATE t_test SET age = 52 WHERE id = 2
COMMIT; # 提交事务
ROLLBACK; # 回滚
开启窗口一的事务
开启窗口二的事务
去窗口一修改id1的数据
刷新数据,数据未更新
去窗口二执行修改id1的数据
此时窗口处于等待状态,因为id1的锁在窗口一那里
去窗口一执行修改id2的数据
刷新数据未更新
去窗口二执行修改id2的数据
发现没有反应,下面显示的还是UPDATE t_test SET age = 42 WHERE id = 1执行中,因为上面一个id1还没拿到锁,所以此时无法执行id2的修改,但是可以发现不会报错,只是处于等待获取锁的状态
提交窗口一的事务
刷新数据,数据更新成功
回到窗口二
发现id1执行了,因为窗口一提交了事务,所以窗口二拿到了锁
继续执行修改id2数据
刷新数据未更新,因为窗口二的事务未提交
提交窗口二的事务
刷新数据,数据更新成功
到这里可以发现,如果两个事务按照相同的顺序获取锁是不会产生死锁的,但是会出现如果一个事务一直不提交导致另一个事务一直等待锁的情况,最后会出现等待超时异常问题
接着我要说的是正常的查询语句是不涉及到死锁问题的,因为查询用的是S锁也就是共享锁或读锁,当然你要是在后面加 FOR UPDATE(悲观锁) 当我没说
这里我们可以验证一下查询语句是不是会导致死锁的问题
我在窗口二加了一条查询语句
START TRANSACTION; # 开启事务
SELECT * FROM t_test WHERE id = 1
UPDATE t_test SET age = 42 WHERE id = 1
UPDATE t_test SET age = 52 WHERE id = 2
COMMIT; # 提交事务
ROLLBACK; # 回滚
我们还是先开启窗口一和窗口二的事务
执行窗口一修改id1的数据,此时窗口一拿到了id1的锁
我们再去窗口二执行查询语句
可以看到不受影响能查询出数据
在继续执行窗口二修改id1数据
可以看到此时是等待获取锁的状态
为了证明查询语句也不影响修改语句,我们继续
之前的运行直接回滚或提交都行
还是先开启两个窗口的事务
这次我们先执行窗口二的查询
可以看到拿到结果了
再去窗口一执行修改id1的数据
可以看到能到拿到锁,说明窗口二的查询语句不会影响到窗口一修改语句获取锁
可以得出一个结论,死锁不会发生在查询语句上,查询语句不会影响其它事务获取锁的操作,注意这里的查询语句不带 FOR UPDATE
还有就是删除也是需要获取锁的,跟修改一下,这里就不演示了
这里还有一点很重要的地方需要注意,那就是表锁和行锁的问题,就是MySQL在什么情况下是表锁,什么情况下是行锁
重点要注意的就是索引的问题,如果查询条件没有命中索引,那么就是表锁
我们调整一下窗口一的语句
START TRANSACTION; # 开启事务
# DELETE FROM t_test WHERE id = 1
UPDATE t_test SET age = 42 WHERE name = 'Z3'
# UPDATE t_test SET age = 40 WHERE id = 1
# UPDATE t_test SET age = 50 WHERE id = 2
COMMIT; # 提交事务
ROLLBACK; # 回滚
再调整一下窗口二的语句
START TRANSACTION; # 开启事务
# SELECT * FROM t_test WHERE id = 1
# UPDATE t_test SET age = 42 WHERE id = 1
UPDATE t_test SET age = 52 WHERE id = 2
COMMIT; # 提交事务
ROLLBACK; # 回滚
窗口一修改的是id1 name='Z3’的,窗口二修改的是id2的
还是先开启两个窗口的事务
我们先去执行窗口一修改name为Z3的数据
再去窗口二执行修改id1的数据
发现窗口二处在等待获取锁的状态,但是窗口一修改的是id1的数据,窗口二修改的是id2的数据,这就说明窗口一其实锁的是整张表
全部回滚一下
我们再重新开启两个窗口的事务
这次我们先执行窗口二修改id2的数据
再去窗口一执行修改name Z3的数据
发现窗口一处在等待获取锁的状态,因为id2的锁在窗口二那里,这也证明了窗口一需要锁全表
为了证明这个锁表是索引导致的,我们去给name加上索引
这里加的就是NORMAL普通索引,记得保存
回滚重新开启两个窗口的事务
一样的我们先执行窗口一修改name为Z3的数据
再去窗口二执行修改id2的数据
可以看到没有进入等待获取锁状态,它拿到锁了,说明这次窗口一没有锁表,因为我们给name字段加了索引,所以才没有锁表
这里告诫大家,经常用来做查询条件的字段一定要建索引!!!索引很重要!!!不然一不小心就会锁表!!!
最后再讲一点,就是修改 name=Z3 和修改id1的锁有没有区别的问题
我们先改一下窗口二的语句
START TRANSACTION; # 开启事务
# SELECT * FROM t_test WHERE id = 1
UPDATE t_test SET age = 42 WHERE id = 1
# UPDATE t_test SET age = 52 WHERE id = 2
COMMIT; # 提交事务
ROLLBACK; # 回滚
重新开启两个窗口的事务
我们先去窗口一执行修改name=Z3的语句
可以看到已经拿到锁了
再去窗口二执行修改id1的数据
可以看到处在获取锁的状态,说明nameZ3和id1其实是同一个锁
这里深处逻辑我就不研究了,我猜的是MySQL所有的行锁都是用主键ID锁的,即使你用其它索引查询的,最终还是回表查询拿到这条数据的主键id,然后再根据主键id锁住这行