大家误以为「插入意向锁」是意向锁,也就是表锁,确实这个名字很让人误解。
但是,实际上「插入意向锁」不是意向锁,而是特殊的间隙锁,属于行级锁,注意是「特殊」的间隙锁,并不是我们常说的间隙锁。
所以,我在原来文章的基础上补充了两个知识点:
- 什么是插入意向锁?
- insert 语句是怎么加锁的?
提纲如下:
有个业务主要逻辑就是新增订单、修改订单、查询订单等操作。然后因为订单是不能重复的,所以当时在新增订单的时候做了幂等性校验,做法就是在新增订单记录之前,先通过 select ... for update 语句查询订单是否存在,如果不存在才插入订单记录。
而正是因为这样的操作,当业务量很大的时候,就可能会出现死锁。
接下来跟大家聊下为什么会发生死锁,以及怎么避免死锁。
死锁的发生
本次案例使用存储引擎 Innodb,隔离级别为可重复读(RR)。
接下来,我用实战的方式来带大家看看死锁是怎么发生的。
我建了一张订单表,其中 id 字段为主键索引,order_no 字段普通索引,也就是非唯一索引:
CREATE TABLE `t_order` ( `id` int NOT NULL AUTO_INCREMENT, `order_no` int DEFAULT NULL, `create_date` datetime DEFAULT NULL, PRIMARY KEY (`id`), KEY `index_order` (`order_no`) USING BTREE ) ENGINE=InnoDB ;
然后,先 t_order 表里现在已经有了 6 条记录:
假设这时有两事务,一个事务要插入订单 1007 ,另外一个事务要插入订单 1008,因为需要对订单做幂等性校验,所以两个事务先要查询该订单是否存在,不存在才插入记录,过程如下:
可以看到,两个事务都陷入了等待状态(前提没有打开死锁检测),也就是发生了死锁,因为都在相互等待对方释放锁。
这里在查询记录是否存在的时候,使用了 select ... for update 语句,目的为了防止事务执行的过程中,有其他事务插入了记录,而出现幻读的问题。
如果没有使用 select ... for update 语句,而使用了单纯的 select 语句,如果是两个订单号一样的请求同时进来,就会出现两个重复的订单,有可能出现幻读,如下图:
为什么会产生死锁?
可重复读隔离级别下,是存在幻读的问题。
Innodb 引擎为了解决「可重复读」隔离级别下的幻读问题,就引出了 next-key 锁,它是记录锁和间隙锁的组合。
- Record Loc,记录锁,锁的是记录本身;
- Gap Lock,间隙锁,锁的就是两个值之间的空隙,以防止其他事务在这个空隙间插入新的数据,从而避免幻读现象。
普通的 select 语句是不会对记录加锁的,因为它是通过 MVCC 的机制实现的快照读,如果要在查询时对记录加行锁,可以使用下面这两个方式:
begin; //对读取的记录加共享锁 select ... lock in share mode; commit; //锁释放 begin; //对读取的记录加排他锁 select ... for update; commit; //锁释放
行锁的释放时机是在事务提交(commit)后,锁就会被释放,并不是一条语句执行完就释放行锁。
比如,下面事务 A 查询语句会锁住(2, +∞]范围的记录&#x