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

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

前言

最近工作中,在加了事务的接口中,不同的业务或者是出现并发的时候,发现了一些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)
publicvoidcreatA() {
// 查找未锁定Pointpoint=pointMapper.selectOne(newLambdaQueryWrapper<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 (InterruptedExceptione) {
thrownewRuntimeException(e);
    }
}

然后通过两个接口,各自调用方法

@GetMapping("/t1")
publicvoidcreateA() {
pointService.creatA();
}
@GetMapping("/t2")
publicvoidcreateB() {
pointService.creatB();
}

2)、运行结果

先访问creatA,然事务卡住不提交,在访问creatB,就会发现,他们两个查到的数据都是未锁,拿到的是更新前的数据。

3)、问题分析

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

4)、解决方法

解决上述方式可以直接使用for update(排它锁),在查询数据添加for update,防止其他事务同时修改这些数据,从而保证事务的一致性。

需要注意的是,使用FOR UPDATE语句需要在事务内部执行,否则该语句不会起作用。此外,在使用FOR UPDATE时,需要保证被锁定的记录的索引列上有足够的索引,否则查询可能会导致表级锁定,从而降低并发性能。因此我们需要在表中添加索引。

Pointpoint=pointMapper.selectOne(newLambdaQueryWrapper<Point>()
        .eq(Point::getPointCode, "A")
        .eq(Point::getIsLock, false)
        .last("for update"));

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



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

相关文章
|
7月前
|
算法 前端开发
可以读通讯稿的组数
可以读通讯稿的组数
46 0
|
7月前
|
编解码 计算机视觉
读、写视频
【5月更文挑战第7天】读、写视频。
50 2
|
数据库
【并发事务会产生哪些问题】
【并发事务会产生哪些问题】
144 0
|
XML Java 数据格式
五、事务操作2
五、事务操作2
71 0
|
7月前
|
SQL 关系型数据库 MySQL
MySQL事务原理分析(ACID特性、隔离级别、锁、MVCC、并发读异常、并发死锁以及如何避免死锁)
MySQL事务原理分析(ACID特性、隔离级别、锁、MVCC、并发读异常、并发死锁以及如何避免死锁)
189 1
|
7月前
|
关系型数据库 MySQL 数据库
【mysql】当前读和快照读,幻读和可重复读
【mysql】当前读和快照读,幻读和可重复读
422 0
|
数据库
数据库事务——快照读与当前读
数据库事务——快照读与当前读
176 0
|
XML Java API
五、事务操作1
五、事务操作1
95 0
五、事务操作1
|
关系型数据库 MySQL 数据库
并发事务更新问题
并发事务更新问题
67 0