SELECT FOR UPDATE 死锁分析(select for update锁表)
工作中常用的业务场景:加锁获取,有则更新,无则插入。
表定义:
CREATE TABLE number (
prefix VARCHAR(20) NOT NULL DEFAULT '' COMMENT '前缀码',
value BIGINT NOT NULL DEFAULT 0 COMMENT '流水号',
UNIQUE KEY uk_prefix(prefix)
);
业务逻辑:
@Transactional
long acquire(String prefix) {
SerialNumber current = dao.selectAndLock(prefix);
if (current == null) {
dao.insert(new Record(prefix, 1));
return 1;
}
else {
current.number++;
dao.update(current);
return current.number;
}
}
MySQL InnoDB中在Repeatable Read的隔离级别下,当通过select for update的where条件筛出记录时,上面的代码是不会有deadlock问题的。然而当select for update中的where条件无法筛选出记录时,这时在有多个线程执行上面的acquire方法时是可能会出现死锁的。
场景复现和分析
- 场景复现
初始化表
insert into number select 'bbb',2;
insert into number select 'hhh',8;
insert into number select 'yyy',25;
2个事务按照时序执行如下语句:
session 1 | session 2 |
begin; | |
begin; | |
select * from number where prefix='ddd' for update | |
select * from number where prefix='fff' for update | |
insert into number select 'ddd',1 | |
锁等待中 | insert into number select 'fff',1 |
锁等待解除 | 死锁,session 2的事务被回滚 |
- 死锁分析
1、session1 执行select for update
session1获得('bbb', 'hhh')的gap锁
2、session2 执行select for update
session2也获得('bbb', 'hhh')的gap锁
截自InnoDB的lock_rec_has_to_wait方法实现,可以看到的LOCK_GAP类型的锁只要不带有插入意向标识,不必等待其它锁(表锁除外)
3、session1尝试insert
session1尝试insert 'ddd',1时,由于发现session2已经获取了这个区间的gap锁,所以Innodb会给session1事务添加插入意向锁,锁的模式为LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION,等待session2释放gap锁
4、session2尝试insert
由于session2在插入时也发现了session1的gap锁,同样加上了插入意向锁,等待session1释放掉gap锁。因此出现死锁的情况。
- 死锁原因
两个session同时通过select for update,并且未命中任何记录的情况下,是有可能得到相同gap的锁的(要看where筛选条件是否落在同一个区间。如果上面的案例如果一个session准备插入'ddd'另一个准备插入'kkk'则不会出现冲突,因为不是同一个gap)。此时再进行并发插入,其中一个会进入锁等待,待第二个session进行插入时,会出现死锁。MySQL会根据事务权重选择一个事务进行回滚。
- 避免死锁
1、事务隔离级别降低到RC,不存在gap锁
2、业务代码捕获异常进行重试
3、RR级别下,不同的where条件(不同的gap锁)则不会存在该问题
引用自:
https://www.cnblogs.com/micrari/p/8029710.html