【事务与并发】- 不同事务读取相同数据问题

文章讨论了在并发或不同业务中,由于事务隔离级别导致的同一数据重复读取问题。通过一个SpringBoot+MyBatis-Plus的示例展示了如何在更新后延迟提交事务,从而引发数据不一致。解决方案是使用`forupdate`排它锁来确保事务的一致性,但需要注意潜在的死锁风险和索引优化。
摘要由CSDN通过智能技术生成

事务2.jpg

前言

最近工作中,在加了事务的接口中,不同的业务或者是出现并发的时候,发现了一些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"));

当查询这行数据的时候,就会给记录加上排它锁,使其他事务无法同时修改或删除这条记录,直到当前事务释放锁(当事务提交、回滚、超时或被杀死)。在并发环境下,如果多个事务同时请求加锁,可能会出现环路等待的情况,导致所有事务都被阻塞无法继续执行。加锁策略还是要合理设计,否则可能出现死锁问题。
在这里插入图片描述

👍创作不易,如有错误请指正,感谢观看!记得点赞哦!👍

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一个有梦有戏的人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值