聊聊Spring事务控制策略以及@Transactional失效问题避坑

大家好,又见面了。

在大部分涉及到数据库操作的项目里面,事务控制、事务处理都是一个无法回避的问题。比如,需要对SQL执行过程进行事务的控制与处理的时候,其整体的处理流程会是如下的示意:

首先是要开启事务、然后执行具体SQL,如果执行异常则回滚事务,否则提交事务,最后关闭事务,完成整个处理过程。按照这个流程的逻辑,写一下对应的实现代码:


public void testJdbcTransactional(DataSource dataSource) {
    Connection conn = null;
    int result = 0;
    try {
        // 获取链接
        conn = dataSource.getConnection();
        // 禁用自动事务提交,改为手动控制
        conn.setAutoCommit(false);
        // 设置事务隔离级别
        conn.setTransactionIsolation(
            TransactionIoslationLevel.READ_COMMITTED.getLevel()
        );

        // 执行SQL
        PreparedStatement ps = 
            conn.prepareStatement("insert into user (id, name) values (?, ?)");
        ps.setString(1, "123456");
        ps.setString(2, "Tom");
        result = ps.executeUpdate();

        // 执行成功,手动提交事务
        conn.commit();
    } catch (Exception e) {
        // 出现异常,手动回滚事务
        if (conn != null) {
            try {
                conn.rollback();
            } catch (Exception e) {
                // write log...
            }
        }
    } finally {
        // 执行结束,最终不管成功还是失败,都要释放资源,断开连接
        try {
            if (conn != null && !conn.isClosed()) {
                conn.close();
            }
        } catch (Exception e) {
             // write log...
        }
    }
}

不难发现,上面大段的代码逻辑并不复杂,对于业务而言其实仅仅只是执行了一个insert操作而已。但是杂糅的事务控制代码,显然干扰了业务自身的代码处理逻辑的阅读与理解

常规项目的代码中,涉及到DB处理的场景很多,如果每个地方都有这么一段事务控制的逻辑,那么整体代码的可维护性将会比较差,想想都令人窒息。

好在,JAVA中很多项目现在都是基于Spring框架进行构建的。得益于 Spring框架的封装,业务代码中进行事务控制操作起来也很简单,直接加个 @Transactional注解即可,大大简化了对业务代码的侵入性。那么对 @Transactional事务注解了解的够全面吗?知道有哪些场景可能会导致 @Transactional注解并不会如你预期的方式生效吗?知道应该怎么使用 @Transactional才能保证对性能的影响最小化吗?

下面我们一起探讨下这些问题。

Spring声明式事务处理机制

为了简化业务开发场景对事务的处理复杂度,让开发人员可以更关注于业务自身的处理逻辑,Spring提供了声明式事务的能力支持。

Spring数据库事务约定处理逻辑流程如下图所示,对比前面示例中基于JDBC的事务处理,Spring的事务的处理操作交给了Spring框架处理,开发人员仅需要实现自己的业务逻辑即可,大大简化了事务方面的处理投入。

基于Spring事务机制,实现上述DB操作事务控制的代码,我们的代码会变得非常的简洁:


@Transactional
public void insertUser() {
    userDao.insertUser();
}

与JDBC事务实现代码相比,基于Spring的方式只需要添加一个 @Transactional注解即可,代码中只需要实现业务逻辑即可,实现了事务控制机制对业务代码的低侵入性

Spring支持的基于 Spring AOP实现的声明式事务功能,所谓声明式事务,即使用@Transactional注解进行声明标注,告诉Spring框架在什么地方启用数据库事务控制能力。@Transactional注解,可以添加在类或者方法上。如果其添加在类上时,表明此类中所有的public非静态方法都将启用事务控制能力。

既然@Transactional注解承载了Spring框架对于事务处理的相关能力,那么接下来我们就一起看下该注解的一些可选配置以及具体使用场景。

img

@Transactional主要可选配置

只读事务配置

通过readonly参数指定当前事务是否为一个只读事务。设置为true标识此事务是个只读事务,默认情况为false。

@Transactional(readOnly = true)
public DomResponse<CiCdItemDetail> queryCicdItemDetail(String appCode) {
    return null;
}

这里涉及一个概念,叫做只读事务,其含义描述如下:

在多条查询语句一起执行的场景里面会涉及到的概念。表示在事务设置的那一刻开始,到整个事务执行结束的过程中,其他事务所提交的写操作数据,对该事务都不可见。

举个例子:

现在有一个复合查询操作,包含2条SQL查询操作:先获取用户表count数,再获取用户表中所有数据。
(1) 先执行完获取用户表count数,得到结果10
(2) 在还没开始执行后一条语句的时候,另一个进程操作了DB并往用户表中插入一条新数据
(3) 复合操作的第二条SQL语句,获取用户列表的操作被执行,返回了11条记录

很明显,复合操作中的两条SQL语句获取的数据结果无法匹配上。原因就是非原子性操作导致,即2条查询操作执行的间隔内,有另一个写操作修改了目标读取的数据,导致了此问题的出现。

为了避免此情况的发生,可以给复合查询操作添加上只读事务,这样事务控制范围内,事务外的写操作就不可见,这样就保证了事务内多条查询语句执行结果的一致性。

那为什么要设置为只读事务、而不是常规的事务呢?主要是从执行效率角度的考虑。因为这个里的操作都是一些只读操作,所以设置为只读事务,数据库会为只读事务提供一些优化手段,比如不启动回滚段、不记录回滚log之类的。

img

回滚条件设定

@Transactional有提供4个不同属性,可以支持传入不同的参数,设定需要回滚的条件:

参数 含义说明
rollbackFor 用于指定需要回滚的特定异常类型,可以指定一个或者多个。当指定rollbackFor或者rollbackForClassName之后,方法执行逻辑中只有抛出指定的异常类型,才会触发事务回滚
rollbackForClassName rollbackFor相同,设置字符串格式的类名
noRollbackFor 用于指定不需要进行回滚的异常类型,当方法中抛出指定类型的异常时,不进行事务回滚。而其余的类型的异常将会触发事务回滚。
noRollbackForClassName noRollbackFor相同,设置字符串格式的类名

其中,rollbackFor支持指定单个或者多个异常类型,只要抛出指定类型的异常,事务都将被回滚掉:


// 指定单个异常
@Transactional(rollbackFor = DemoException.class)
public void insertUser() {
    // do something here
}

// 指定多个异常
@Transactional(rollbackFor = {DemoException.class, DemoException2.class})
public void insertUser2() {
    // do something here
}

rollbackForrollbackForClassName作用相同,只是提供了2个不同的指定方法,允许执行Class类型或者ClassName字符串。


// 指定异常名称
@Transactional(rollbackForClassName = {"DemoException"})
public void insertUser() {
    // do something here
}

同理,noRollbackFornoRollbackForClassName的使用与上面示意的相似,只是其含义功能点是相反的。

img

事务传播行为

propagation用于指定此事务对应的传播类型。所谓的事务传播类型,即当前已经在一个事务的上下文中时,又需要开始一个事务,这个时候来处理这个将要开启的新事务的处理策略。

主要有7种类型的事务传播类型:

传播类型 含义描述
REQUIRED 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务
SUPPORTS 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行
MANDATORY 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常
REQUIRES_NEW 创建一个新的事务,如果当前存在事务,则把当前事务挂起
NOT_SUPPORTED 以非事务方式运行,如果当前存在事务,则把当前事务挂起
NEVER 以非事务方式运行,如果当前存在事务,则抛出异常
NESTED 如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于REQUIRED

事务的传播行为,将会影响到事务控制的结果,比如最终是在同一事务中,一旦遇到异常,所有操作都会被回滚掉,而如果是在多个事务中,则某一个事务的回滚,不影响已提交的其余事务的回滚。

实际编码的时候,可以通过@Transactional注解中的 propagation参数来指定具体的传播类型,取值由 org.springframework.transaction.annotation.Propagation枚举类提供。如果不指定,则默认取值为 Propagation.REQUIRED,也即如果当前存在事务,则加入该事务,如果当前没有事务,则创建一个新的事务


/**
 * The transaction propagation type.
 * <p>Defaults to {@link Propagation#REQUIRED}.
 * @see org.springframework.transaction.interceptor.TransactionAttribute#getPropagationBehavior()
 */
Propagation propagation() default Propagation.REQUIRED;
  

img

事务超时设定

可以使用timeout属性来设置事务的超时秒数,默认值为-1,表示永不超时。

@Transactional失效场景避坑

同一个类中方法间调用

Spring的事务实现原理是AOP,而AOP的原理是动态代理。

在类内部方法之间相互调用的时候,本质上是类对象自身的调用,而不是使用代理对象去调用,也就不会触发AOP,这样其实Spring也就无法将事务控制的代码逻辑织入到调用代码流程中,所以这里的事务控制就无法生效。


public void insertUser() {
    writeDataIntoDb();
}

@Transactional
public void writeDataIntoDb() {
  // ...
}

所以遇到同一个类中多个方法之间相互调用,且调用的方法需要做事务控制的时候需要特别注意下这个问题。解决方式,可以建2个不同的类,然后将方法放到两个类中,这样跨类调用,Spring事务机制就可以生效。

添加在非public方法上

如果将@Transactional注解添加在protected、private修饰的方法上,虽然代码不会有任何的报错,但是实际上注解是不会生效的。

@Transactional
private void writeDataIntoDb() {
  // ...
}

方法内部Try Catch吞掉相关异常

这个其实很容易理解,业务代码中将所有的异常给catch并吞掉了,等同于业务代码认为被捕获的异常不需要去触发回滚。对框架而言,因为异常被捕获了,业务逻辑执行都在正常往下运行,所以也不会触发异常回滚机制。


// catch了可能的异常,导致DB操作失败的时候事务不会触发回滚
@Transactional
public void insertUser() {
    try {
        UserEntity user = new UserEntity();
        user.setWorkId("123456");
        user.setUserName("王小二");
        userRepository.save(user);
    } catch (Exception e) {
        log.error("failed to create user");

        // 直接吞掉了异常,这样不会触发事务回滚机制
    }
}

在业务处理逻辑中,如果确实需要知晓并捕获相关处理的异常进行一些额外的业务逻辑处理,如果要保证事务回滚机制生效,最后需要往外抛出 RuntimeException异常,或者是继承RuntimeException实现的业务自定义异常。如下:


// catch了可能的异常,对外抛出RuntimeException或者其子类,可触发事务回滚
@Transactional
public void insertUser() {
    try {
        UserEntity user = new UserEntity();
        user.setWorkId("123456");
        user.setUserName("王小二");
        userRepository.save(user);
    } catch (Exception e) {
        log.error("failed to create user");

        // @Transactional没有指定rollbackFor,所以抛出RuntimeException或者其子类,可触发事务回滚机制
        throw new RuntimeException(e);
    }
}

当然,如果@Transactional注解指定了 rollbackFor为某个具体的异常类型,则最终需要保证异常时对外抛出相匹配的异常类型,才可以触发事务处理逻辑。如下:


// catch了指定异常,对外抛出对应类型的异常,可触发事务回滚
@Transactional(rollbackFor = DemoException.class)
public void insertUser() {
    try {
        UserEntity user = new UserEntity();
        user.setWorkId("123456");
        user.setUserName("王小二");
        userRepository.save(user);
    } catch (Exception e) {
        log.error("failed to create user");
        // @Transactional有指定rollbackFor,抛出异常要与rollbackFor指定异常类型一致
        throw new DemoException();
    }
}

对应数据库引擎类型不支持事务

MySQL数据库而言,常见的数据库引擎有 InnoDBMyisam等类型,但是MYISAM引擎类型是不支持事务的。所以如果建表时设置的引擎类型设置为 MYISAM的话,即使代码里面添加了@Transactional最终事务也不会生效的。

@Transactional使用策略

因为事务处理对性能会有一定的影响,所以事务也不是说任何地方都可以随便添加的。对于一些性能敏感场景,需要注意几点:

  1. 仅在必要的场合添加事务控制

(1)不含有DB操作相关,无需添加事务控制
(2)单条查询语句,没必要添加事务控制
(3)仅有查询操作的多条SQL执行场景,可以添加只读事务控制
(4)单条 insert/update/delete语句,其实也不需要添加 @Transactional事务处理,因为单条语句执行其实数据库有隐性事务控制机制,如果执行失败,是属于 SQL报错,数据不会更新成功,自然也无需回滚。

  1. 尽可能缩小事务控制的代码段处理范围

主要从性能层面考虑,事务机制,类似于并发场景的加锁处理,范围越大对性能影响越明显

  1. 事务控制范围内的业务逻辑尽可能简单、避免非事务相关耗时处理逻辑

也是从性能层面考虑,尽量将耗时的逻辑放到事务控制之外执行,事务内仅保留与DB操作切实相关的逻辑

总结

好啦,关于Spring中事务控制的相关用法,以及@Transactional使用过程中可能的一些失效场景,就探讨到这里了。那么你对事务这块有哪些自己的理解呢?或者是否有遇到相关的问题呢?欢迎一起交流下咯。

img

我是悟道,聊技术、又不仅仅聊技术~

如果觉得有用,请点个关注,也可以关注下我的公众号【架构悟道】,获取更及时的更新。

期待与你一起探讨,一起成长为更好的自己。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


摘要: 原创出处 https://www.bysocket.com 「公众号:泥瓦匠BYSocket 」欢迎关注和转载,保留摘要,谢谢! 目录 连接 连接池产生原因 连接池实现原理 小结 TEMPERANCE:Eat not to dullness;drink not to elevation.节制
摘要: 原创出处 https://www.bysocket.com 「公众号:泥瓦匠BYSocket 」欢迎关注和转载,保留摘要,谢谢! 一个优秀的工程师和一个普通的工程师的区别,不是满天飞的架构图,他的功底体现在所写的每一行代码上。-- 毕玄 1. 命名风格 【书摘】类名用 UpperCamelC
今天犯了个错:“接口变动,伤筋动骨,除非你确定只有你一个人在用”。哪怕只是throw了一个新的Exception。哈哈,这是我犯的错误。一、接口和抽象类类,即一个对象。先抽象类,就是抽象出类的基础部分,即抽象基类(抽象类)。官方定义让人费解,但是记忆方法是也不错的 —包含抽象方法的类叫做抽象类。接口
Writer :BYSocket(泥沙砖瓦浆木匠)微 博:BYSocket豆 瓣:BYSocketFaceBook:BYSocketTwitter :BYSocket一、引子文件,作为常见的数据源。关于操作文件的字节流就是 —FileInputStream&amp;FileOutputStream。
作者:泥沙砖瓦浆木匠网站:http://blog.csdn.net/jeffli1993个人签名:打算起手不凡写出鸿篇巨作的人,往往坚持不了完成第一章节。交流QQ群:【编程之美 365234583】http://qm.qq.com/cgi-bin/qm/qr?k=FhFAoaWwjP29_Aonqz
本文目录 线程与多线程 线程的运行与创建 线程的状态 1 线程与多线程 线程是什么? 线程(Thread)是一个对象(Object)。用来干什么?Java 线程(也称 JVM 线程)是 Java 进程内允许多个同时进行的任务。该进程内并发的任务成为线程(Thread),一个进程里至少一个线程。 Ja
Writer :BYSocket(泥沙砖瓦浆木匠)微 博:BYSocket豆 瓣:BYSocketFaceBook:BYSocketTwitter :BYSocket在面向对象编程中,编程人员应该在意“资源”。比如?1String hello = &quot;hello&quot;; 在代码中,我们
摘要: 原创出处 https://www.bysocket.com 「公众号:泥瓦匠BYSocket 」欢迎关注和转载,保留摘要,谢谢! 这是泥瓦匠的第103篇原创 《程序兵法:Java String 源码的排序算法(一)》 文章工程:* JDK 1.8* 工程名:algorithm-core-le
摘要: 原创出处 https://www.bysocket.com 「公众号:泥瓦匠BYSocket 」欢迎关注和转载,保留摘要,谢谢! 目录 一、父子类变量名相同会咋样? 有个小故事,今天群里面有个人问下面如图输出什么? 我回答:60。但这是错的,答案结果是 40 。我知错能改,然后说了下父子类变
作者:泥瓦匠 出处:https://www.bysocket.com/2021-10-26/mac-create-files-from-the-root-directory.html Mac 操作系统挺适合开发者进行写代码,最近碰到了一个问题,问题是如何在 macOS 根目录创建文件夹。不同的 ma
作者:李强强上一篇,泥瓦匠基础地讲了下Java I/O : Bit Operation 位运算。这一讲,泥瓦匠带你走进Java中的进制详解。一、引子在Java世界里,99%的工作都是处理这高层。那么二进制,字节码这些会在哪里用到呢?自问自答:在跨平台的时候,就凸显神功了。比如说文件读写,数据通信,还
1 线程中断 1.1 什么是线程中断? 线程中断是线程的标志位属性。而不是真正终止线程,和线程的状态无关。线程中断过程表示一个运行中的线程,通过其他线程调用了该线程的 方法,使得该线程中断标志位属性改变。 深入思考下,线程中断不是去中断了线程,恰恰是用来通知该线程应该被中断了。具体是一个标志位属性,
Writer:BYSocket(泥沙砖瓦浆木匠)微博:BYSocket豆瓣:BYSocketReprint it anywhere u want需求 项目在设计表的时候,要处理并发多的一些数据,类似订单号不能重复,要保持唯一。原本以为来个时间戳,精确到毫秒应该不错了。后来觉得是错了,测试环境下很多一
纯技术交流群 每日推荐 - 技术干货推送 跟着泥瓦匠,一起问答交流 扫一扫,我邀请你入群 纯技术交流群 每日推荐 - 技术干货推送 跟着泥瓦匠,一起问答交流 扫一扫,我邀请你入群 加微信:bysocket01
Writer:BYSocket(泥沙砖瓦浆木匠)微博:BYSocket豆瓣:BYSocketReprint it anywhere u want.文章Points:1、介绍RESTful架构风格2、Spring配置CXF3、三层初设计,实现WebService接口层4、撰写HTTPClient 客户
Writer :BYSocket(泥沙砖瓦浆木匠)什么是回调?今天傻傻地截了张图问了下,然后被陈大牛回答道“就一个回调…”。此时千万个草泥马飞奔而过(逃哈哈,看着源码,享受着这种回调在代码上的作用,真是美哉。不妨总结总结。一、什么是回调回调,回调。要先有调用,才有调用者和被调用者之间的回调。所以在百
Writer :BYSocket(泥沙砖瓦浆木匠)一、什么大小端?大小端在计算机业界,Endian表示数据在存储器中的存放顺序。百度百科如下叙述之:大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加
What is a programming language? Before introducing compilation and decompilation, let&#39;s briefly introduce the Programming Language. Programming la
Writer :BYSocket(泥沙砖瓦浆木匠)微 博:BYSocket豆 瓣:BYSocketFaceBook:BYSocketTwitter :BYSocket泥瓦匠喜欢Java,文章总是扯扯Java。 I/O 基础,就是二进制,也就是Bit。一、Bit与二进制什么是Bit(位)呢?位是CPU
Writer:BYSocket(泥沙砖瓦浆木匠)微博:BYSocket豆瓣:BYSocket一、前言 泥瓦匠最近被项目搞的天昏地暗。发现有些要给自己一些目标,关于技术的目标:专注很重要。专注Java 基础 + H5(学习) 其他操作系统,算法,数据结构当成课外书博览。有时候,就是那样你越是专注方面越