乐趣区

关于后端:聊聊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 框架对于事务处理的相干能力,那么接下来咱们就一起看下该注解的一些可选配置以及具体应用场景。

@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 之类的。

回滚条件设定

@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 的应用与下面示意的类似,只是其含意性能点是相同的。

事务流传行为

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;
  

事务超时设定

能够应用 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 应用过程中可能的一些生效场景,就探讨到这里了。那么你对事务这块有哪些本人的了解呢?或者是否有遇到相干的问题呢?欢送一起交换下咯。

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

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

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

退出移动版