Spring事务原理解析

 更新时间:2022年12月20日 09:22:10   作者:码畜c  
Spring事务有可能会提交,回滚、挂起、恢复,所以Spring事务提供了一种机制,可以让程序员来监听当前Spring事务所处于的状态,这篇文章主要介绍了Spring底层事务原理,需要的朋友可以参考下

前言

最近在编写公司APP产品的商品砍价功能,其中有一个接口涉及并发访问。自测时通过ApiFox接口管理工具进行压测,落地数据时出现了"锁失效"的情景。十分感谢后端小伙伴的帮助排查,解决了这个问题。

问题描述

并发接口中,先对主表数据进行读取,进行业务判断后,新增、修改它表的数据。在理应串行执行的情况下发生了多个请求线程读取到了相同的主表数据,导致数据处理异常。也正是前言中所说的"锁失效"了。(实际情况加锁操作是有效的)

代码复现

@RequestMapping("/test")
@Transactional(rollbackFor = Exception.class)
public String test() {
    DistributedLock.lock("ct_lock");
    try {
        Map<String, Object> resultMap = jdbcTemplate.queryForMap("select * from concurrent_read_uncommit");
        int num = Integer.parseInt(resultMap.get("num").toString());
        num++;
        jdbcTemplate.update("update concurrent_read_uncommit set num = " + num);
    } finally {
        DistributedLock.unlock("ct_lock");
    }
    return "success";
}
  • 最少的代码进行演示,Controller方法体中的内容应是Service中的代码
  • DistributedLock中封装的Redission
  • 通过将先读后改的方式演示,实际中本质就是进行了这样的操作,但会存在更多的业务代码(不演示新增的情况)

ApiFox中通过创建100个请求线程进行压测,最终concurrent_read_uncommit表中的num字段值为94,而非100。

排查

1. 锁失效

新编写了两个简单接口,第一个接口加锁,并线程休眠30秒后释放锁。另一个接口加同样的锁,打印一条语句后直接返回。先调用第一个接口,在调用第二个接口。Debug中发现锁是有效的,在redis中存有锁Key。并且访问第二个接口时,线程被阻塞在了加锁行代码。

2. 事务隔离级别

查询数据库事务默认隔离级别:

select @@tx_isolation;

结果

REPEATABLE-READ

就是默认的RR级别,那么说明同个事务内多次读取数据都会是一样的,不会读取到脏数据。

3. 修改Spring事务传播配置

@Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)

与这个并没有关系,八竿子打不着。当时的想法时是多个并发请求在进入到了同个事务内,并一起读取到了没有被修改前的数据。细想想:

  • 事务传播配置一般用在不同事务方法间产生调用时的事务决策,是共用事务还是新创建事务,亦或是其他的方式进行处理
  • test方法本身为根方法,也没有调用其他的事务方法,所以无需配置事务传播配置
  • 即便不在同一事务内,依旧能查询到其他事务修改但未提交的相同数据

解决方案

在锁代码块中调用事务方法,而不是在事务方法中进行加锁。

原因为:并发情境下,执行速度过快,很有可能发生:请求线程在释放锁后没有来得及提交事务,另一个请求线程在加锁处被唤醒,继而读取到了事务未提交的数据。即读取到了脏数据,产生了"锁失效"的效果。

修正代码:

@RequestMapping("/test2")
public String test2() {
    ConcurrentTransactionalController proxyBean = SpringContextUtils.getBean(this.getClass());
    proxyBean.doTest2();
    return "success";
}
@Transactional(rollbackFor = Exception.class)
public void doTest2() {
    DistributedLock.lock("ct_lock");
    try {
        Map<String, Object> resultMap = jdbcTemplate.queryForMap("select * from concurrent_read_uncommit");
        int num = Integer.parseInt(resultMap.get("num").toString());
        num++;
        jdbcTemplate.update("update concurrent_read_uncommit set num = " + num);
        Thread.sleep(500);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    } finally {
        DistributedLock.unlock("ct_lock");
    }
}
  • 将需要加锁的事务代码进行提取另一个方法
  • 调用方法中进行加锁,并且必须要去掉事务注解
  • 因为是在非事务方法调用事务方法,为了保证事务生效,需要通过事务代理Bean进行调用

这样就保证了不会读取到事务未提交的数据,同时又具有锁的排他性。

其实锁一直都是有效的,本质原因就在于Spring的事务代理Bean屏蔽了事务代码。我们不能手动的进行控制,也就是说你变更了不了事务代码的顺序。如果能将提交事务的行代码写到释放锁之前,就不会存在这个问题了。所以,也可以通过编程式事务解决这个问题,关于编程式事务,Spring也有做代码封装。如果不通过编程式事务,那么就只能通过上述代码变相的来实现。

到此这篇关于Spring事务原理解析的文章就介绍到这了,更多相关Spring事务内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • maven 删除下载失败的包的方法

    maven 删除下载失败的包的方法

    本文介绍了当Maven包报红时,使用删除相关文件的方法来解决该问题,具有一定的参考价值,感兴趣的可以了解一下
    2023-09-09
  • IDEA中maven依赖报红的问题解决办法

    IDEA中maven依赖报红的问题解决办法

    这篇文章主要给大家介绍了关于IDEA中maven依赖报红的问题解决办法,在使用IDEA过程中,经常会出现maven依赖报红的问题,文中通过图文介绍的非常详细,需要的朋友可以参考下
    2023-07-07
  • SpringBoot获取配置文件的简单实现方法

    SpringBoot获取配置文件的简单实现方法

    这篇文章主要给大家介绍了关于SpringBoot如何获取配置文件的简单实现方法,文中通过示例代码介绍的非常详细,对大家学习或者使用Spring Boot具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2020-05-05
  • springboot线程池监控的简单实现

    springboot线程池监控的简单实现

    本文主要介绍了springboot线程池监控的简单实现,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2022-01-01
  • Java连接超时的几种情况以及读取代码

    Java连接超时的几种情况以及读取代码

    在Java编程中连接超时异常是指在建立网络连接时,无法在给定的时间内成功建立连接的异常,这篇文章主要给大家介绍了关于Java连接超时的几种情况以及读取的相关资料,需要的朋友可以参考下
    2024-02-02
  • Spring WebFlux怎么进行异常处理源码解析

    Spring WebFlux怎么进行异常处理源码解析

    这篇文章主要为大家介绍了Spring WebFlux怎么进行异常处理源码解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-08-08
  • ThreadPoolExecutor线程池的使用方法

    ThreadPoolExecutor线程池的使用方法

    这篇文章主要为大家详细介绍了ThreadPoolExecutor线程池的使用方法,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2019-09-09
  • Java微服务Filter过滤器集成Sentinel实现网关限流过程详解

    Java微服务Filter过滤器集成Sentinel实现网关限流过程详解

    这篇文章主要介绍了Java微服务Filter过滤器集成Sentinel实现网关限流过程,首先Sentinel规则的存储默认是存储在内存的,应用重启之后规则会丢失。因此我们通过配置中心Nacos保存规则,然后通过定时拉取Nacos数据来获取规则配置,可以做到动态实时的刷新规则
    2023-02-02
  • java数据类型与二进制详细介绍

    java数据类型与二进制详细介绍

    这篇文章主要介绍了java数据类型与二进制详细介绍的相关资料,这里对数据类型进行了一一介绍分析,并说明自动转换和强制转换,需要的朋友可以参考下
    2017-07-07
  • Java中数据转换及字符串的“+”操作方法

    Java中数据转换及字符串的“+”操作方法

    本文主要介绍了Java中的数据类型转换,包括隐式转换和强制转换,隐式转换通常用于将范围较小的数据类型转换为范围较大的数据类型,而强制转换则是将范围较大的数据类型转换为范围较小的数据类型,本文介绍Java中数据转换以及字符串的“+”操作,感兴趣的朋友一起看看吧
    2024-10-10

最新评论