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

【事务与并发】- 不同事务读取相同数据问题_第1张图片

不同事务读取相同数据问题

    • 前言
    • 问题描述
      • 案例准备
      • 业务需求
      • 问题回放
        • 1)、代码复现
        • 2)、运行结果
        • 3)、问题分析
        • 4)、解决方法

前言

最近工作中,在加了事务的接口中,不同的业务或者是出现并发的时候,发现了一些SQL读取问题,两个都被事务包裹的方法,各自是隔离的,如果一方的事务延时提交,就会导致另一方读取出来的数据相同,并不是修改后的数据。

问题描述

不同业务涉及到同一张表的时候,或者相同业务出现并发访问时,可能会导致读取到的数据是相同的。在业务a中,读取表的数据1,并且进行修改,在将数据2写回数据库,此时事务并未提交。但是业务b就从数据库中又获取了数据1(期望来说要获取的是数据2),这就导致了两个事务获取的数据相同了,这个显然就是不对的。

案例准备

本次采用springboot+mybatis-plus来做案例,基础简单就不再赘述。首先需要准备一张表,本次模拟开发业务中的情况,需要一张点位表(t_point),就包括四个字段就行,其中控制is_lock来判断是否锁定。
【事务与并发】- 不同事务读取相同数据问题_第2张图片

业务需求

本次模拟点位查找的业务,根据is_lock来判断改点是否被锁住,需要查找没有被锁住的点位,进行处理之后,将点位设置未锁定,然后写回数据库,本次用两个方法查找同一个点位来模拟业务a、业务b(creatA,creatB方法),并且两个业务需要用事务包裹。

问题回放

我们在creatA方法中在更新之后睡眠了10s,来模拟事务延时提交。当我们访问creatA和creatB,就会看到两个方法都找到了点位是没锁的。
【事务与并发】- 不同事务读取相同数据问题_第3张图片

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,就会发现,他们两个查到的数据都是未锁,拿到的是更新前的数据。
【事务与并发】- 不同事务读取相同数据问题_第4张图片

3)、问题分析

首先,因为两个方法都被事务给包裹住了,这就导致需要等待事务提交之后,才算真正的写回到数据库中。Spring中使用@Transactional注解的事务是相互隔离的。Spring框架使用底层的事务管理器来管理事务,这些事务管理器支持不同的事务隔离级别。当使用@Transactional注解时,可以通过指定不同的隔离级别来控制事务的隔离程度。所以createB在createA事务提交之前,是看不到修改的数据的。
【事务与并发】- 不同事务读取相同数据问题_第5张图片

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"));

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

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

你可能感兴趣的:(事务与锁,杂项,mybatis,java,spring,boot,事务)