前言
最近工作中,在加了事务的接口中,不同的业务或者是出现并发的时候,发现了一些SQL读取问题,两个都被事务包裹的方法,各自是隔离的,如果一方的事务延时提交,就会导致另一方读取出来的数据相同,并不是修改后的数据。
问题描述
不同业务涉及到同一张表的时候,或者相同业务出现并发访问时,可能会导致读取到的数据是相同的。在业务a中,读取表的数据1,并且进行修改,在将数据2写回数据库,此时事务并未提交。但是业务b就从数据库中又获取了数据1(期望来说要获取的是数据2),这就导致了两个事务获取的数据相同了,这个显然就是不对的。
案例准备
本次采用springboot+mybatis-plus来做案例,基础简单就不再赘述。首先需要准备一张表,本次模拟开发业务中的情况,需要一张点位表(t_point),就包括四个字段就行,其中控制is_lock来判断是否锁定。
业务需求
本次模拟点位查找的业务,根据is_lock来判断改点是否被锁住,需要查找没有被锁住的点位,进行处理之后,将点位设置未锁定,然后写回数据库,本次用两个方法查找同一个点位来模拟业务a、业务b(creatA,creatB方法),并且两个业务需要用事务包裹。
问题回放
我们在creatA方法中在更新之后睡眠了10s,来模拟事务延时提交。当我们访问creatA和creatB,就会看到两个方法都找到了点位是没锁的。
1)、代码复现
creatA方法是从点位表中获取未锁得数据,进行判断是否锁住,没锁就上锁然后更新到数据库中,然后睡眠10s模拟事务延时提交;creatB方法也是相同的,只是不需要加上睡眠。
@Override
@Transactional(rollbackFor = Exception.class)
public void creatA() {
// 查找未锁定
Point point = pointMapper.selectOne(new LambdaQueryWrapper<Point>()
.eq(Point::getPointCode, "A")
.eq(Point::getIsLock, false));
if (ObjectUtil.isEmpty(point)) {
// 锁了
System.out.println("CreateA -> 点位已经上锁");
return;
}
// 处理一些业务......
System.out.println("CreateA正在处理相应业务");
// 锁定
point.setIsLock(true);
// 更新
pointMapper.updateById(point);
// 休眠:模拟延时提交事务
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
然后通过两个接口,各自调用方法
@GetMapping("/t1")
public void createA() {
pointService.creatA();
}
@GetMapping("/t2")
public void createB() {
pointService.creatB();
}
2)、运行结果
先访问creatA,然事务卡住不提交,在访问creatB,就会发现,他们两个查到的数据都是未锁,拿到的是更新前的数据。
3)、问题分析
首先,因为两个方法都被事务给包裹住了,这就导致需要等待事务提交之后,才算真正的写回到数据库中。Spring中使用@Transactional注解的事务是相互隔离的。Spring框架使用底层的事务管理器来管理事务,这些事务管理器支持不同的事务隔离级别。当使用@Transactional注解时,可以通过指定不同的隔离级别来控制事务的隔离程度。所以createB在createA事务提交之前,是看不到修改的数据的。
4)、解决方法
解决上述方式可以直接使用for update(排它锁),在查询数据添加for update,防止其他事务同时修改这些数据,从而保证事务的一致性。
需要注意的是,使用FOR UPDATE语句需要在事务内部执行,否则该语句不会起作用。此外,在使用FOR UPDATE时,需要保证被锁定的记录的索引列上有足够的索引,否则查询可能会导致表级锁定,从而降低并发性能。因此我们需要在表中添加索引,否则会锁全表。
Point point = pointMapper.selectOne(new LambdaQueryWrapper<Point>()
.eq(Point::getPointCode, "A")
.eq(Point::getIsLock, false)
.last("for update"));
当查询这行数据的时候,就会给记录加上排它锁,使其他事务无法同时修改或删除这条记录,直到当前事务释放锁(当事务提交、回滚、超时或被杀死)。在并发环境下,如果多个事务同时请求加锁,可能会出现环路等待的情况,导致所有事务都被阻塞无法继续执行。加锁策略还是要合理设计,否则可能出现死锁问题。
👍创作不易,如有错误请指正,感谢观看!记得点赞哦!👍