Spring中事务传播机制的理解与简单试用

目录

一,前言

二,Spring框架中的事务传播行为

三,事务的传播行为测试

Propagation.REQUIRED

Propagation.SUPPORTS

Propagation.MANDATORY

Propagation.REQUIRES_NEW

Propagation.NOT_SUPPORTED

Propagation.NEVER

Propagation.NESTED

四,事务失效场景总结

五,事务传播机制使用场景


 

一,前言

相信每个人一谈到事务就能联想到数据库中的事务,以及其对应的ACID特性。但实际上并不只有在数据库中才存在事务的说法,它的核心作用在Spring框架中也有着自己独特的作用。也许部分小伙伴对于Spring的事务在面试或学习中听说过,并没有真正理解其具体作用及使用场景,以至于每次在使用Spring事务的时候,都是直接在方法上加上 @Transactional(rollbackFor = Exception.class),之后就没再关系过了,没有细细研究事务的传播行为,导致在使用的时候不得心应手。因此今天针对于Spring事务机制方面的学习了解做些简单梳理总结。

Tips:数据库中的事务必须满足4个条件(ACID):原子性Atomicity,或称不可分割性)、一致性Consistency)、隔离性Isolation,又称独立性)、持久性Durability)。

  • 原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。

  • 一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。

  • 隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行(Serializable)。

  • 持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

二,Spring框架中的事务传播行为

在Spring框架中,事务的传播机制定义了在调用具有事务功能的方法时,如何处理已经存在的事务。

Spring提供了多种事务传播行为,每种行为都适用不同的开发场景和业务需求。以下是常见的事务传播行为及其说明:

事务传播行为类型解释说明
Requied(默认)如果当前没有事务,则创建一个新的事务,并在方法执行期间使用该事务。 如果当前存在事务,方法将加入该事务并成为其一部分。 这是最常用的事务传播行为,用于保证方法在一个事务中执行。 如果不存在事务,则创建一个新的事务;如果存在事务,则加入该事务。
Supports如果当前存在事务,则方法将在该事务中执行。 如果当前没有事务,则方法将以非事务方式执行。 这种事务传播行为适用于不强制要求方法在事务中执行的情况。
Mandatory如果当前存在事务,则方法将在该事务中执行。 如果当前没有事务,则抛出异常。这种事务传播行为适用于要求方法在事务中执行的情况,表明该方法必须在事务中执行。
Requires_New如果当前存在事务,则将当前事务挂起,并创建一个新的事务。方法在新的事务中执行。 如果当前没有事务,则方法将在新的事务中执行。 这种事务传播行为适用于要求方法在新事务中执行的情况。无论当前是否存在事务,方法都会在新的事务中执行,并将当前事务挂起。
Not_Supported方法将以非事务方式执行。 如果当前存在事务,则将其挂起。 这种事务传播行为适用于要求方法不在事务中执行的情况。无论当前是否存在事务,方法都会以非事务方式执行,并将当前事务挂起。
Never方法将以非事务方式执行。 如果当前存在事务,则抛出异常。 这种事务传播行为适用于要求方法绝不在事务中执行的情况。 如果当前存在事务,则会抛出异常。
Nested如果当前存在事务,则方法将在嵌套事务中执行。 如果当前没有事务,则创建一个新的事务,并在方法执行期间使用该新事务。 嵌套事务是外部事务的一部分,并可以回滚到事务状态的保存点。 这种事务传播行为适用于需要局部回滚的情况,外部事务可以回滚并影响内部嵌套事务的状态。

以上 7 种传播机制,可根据“是否支持当前事务”的维度分为以下 3 类:

dc5f5aad6c7b4f76a4248d83f5da8fac.png

在项目中可通过使用@Transactional这个注解来开启事务

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {
    @AliasFor("transactionManager")
    String value() default "";
​
    @AliasFor("value")
    String transactionManager() default "";
​
    Propagation propagation() default Propagation.REQUIRED;
​
    Isolation isolation() default Isolation.DEFAULT;
​
    int timeout() default -1;
​
    boolean readOnly() default false;
​
    Class<? extends Throwable>[] rollbackFor() default {};
​
    String[] rollbackForClassName() default {};
​
    Class<? extends Throwable>[] noRollbackFor() default {};
​
    String[] noRollbackForClassName() default {};
}

在这个注解源码中可以看到目前它默认的就是REQUIRED

如果需要更改使用的事务传播类型呢,可以通过Propagation这个枚举类用注解的形式标名:

package org.springframework.transaction.annotation;
​
public enum Propagation {
    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);
​
    private final int value;
​
    private Propagation(int value) {
        this.value = value;
    }
​
    public int value() {
        return this.value;
    }
}

事务传播机制的好处在于:

  • 简化代码逻辑:事务传播机制允许开发人员在需要的情况下,将多个方法调用作为一个事务进行管理,而无需手动处理事务的开始和提交等操作。

  • 提供事务隔离性:通过控制事务的传播行为,可以确保在一个事务中执行的方法能够共享相同的事务上下文,从而维护事务的隔离性和一致性。

  • 支持嵌套事务:事务传播机制中的NESTED选项允许方法在嵌套事务中执行,提供了更高级别的事务控制,并支持局部回滚。

传播机制的作用到底是什么?

笔者个人观点是,事务是数据库的一种特性,而Spring只是封装这个特性,方便我们使用,最终的执行实际上都是在数据库中完成,但是对于数据库来说,事务是单一的,没有那么多业务场景,但是对于Spring来说,会面对各种各样的业务需求,所以需要有一套可以从代码层面去控制事务来满足我们的场景需求,所以也就有了传播机制。

既然是“事务传播”,所以事务的数量应该在两个或两个以上,Spring 事务传播机制的诞生是为了规定多个事务在传播过程中的行为的。比如方法 A 开启了事务,而在执行过程中又调用了开启事务的 B 方法,那么 B 方法的事务是应该加入到 A 事务当中呢?还是两个事务相互执行互不影响,又或者是将 B 事务嵌套到 A 事务中执行呢?所以这个时候就需要一个机制来规定和约束这两个事务的行为,这就是 Spring 事务传播机制所解决的问题。

通俗点说就是当我们项目比较简单,业务逻辑也不复杂,一个业务方法处理一个数据操作时,就像简单查数据,删数据之类、一个方法内处理单个数据层操作,那就没使用这个Spring事务的必要了,因为单个数据操作在数据库中就算一个基础的数据库事务,要么成功要么失败嘛。但是如果业务处理复杂就另当别论了,比如一个处理订单业务的方法内包括订单支付完成后的扣款和扣款完成后的订单提交两步操作,就像淘宝买东西支付后,钱扣了,待收货就有订单信息这样的步骤。这时如果没有事务介入,保证扣款和订单信息跟新的一致性,就可能造成你钱付了,但是那个方法执行(两步)中间的数据库操作可能出现异常了,结果订单状态没更新,那你不是直接裂开了吗。因此在处理复杂业务逻辑时,学习加入事务处理,保证业务方法执行一致性是完全必要的。

Spring事务有是如何实现的呢?

因为Spring本身是没有事务的,只有数据库才会回有事务,而Spring的事务是借助AOP,通过动态代理的方式,在我们要操作数据库的是时候,实际是Spring通过动态代理进行功能扩展,在我们的代码操作数据库之前通过数据库客户端打开数据库事务,如果代码执行完毕没有异常信息或者是没有Spring要捕获的异常信息时,再通过数据库客户端程序提交事务,如果有异常信息或者是有Spring要捕获的异常信息,再通过数据库客户端程序回滚事务,从而达到控制数据库事务的目的。下面附上一张Spring创建事务的流程图供大家理解,有兴趣的小伙伴也可通过源码自行查看,这里并不详述:

b5d3c676e2e54deca0d6e1067055bf9f.png

下面针对每个不同的传播行为进行一个简单使用了解。

三,事务的传播行为测试

因此事务的数量应该在两个或两个以上,因此先在项目中准备两个用于测试的方法。

@Resource
    private TestUserDao testUserDao;
​
    @Resource
    private AdminDao adminDao;
/**
     * insertData作为方法调用者
     * @param testUser 新对象
     * @return 方法执行结果
     */
    public String insertData(TestUser testUser) {
        testUser.setCreateTime(new Date());
        String result=testUserDao.insert(testUser)>0?"数据插入成功":"数据插入失败";
        log.info("获取结果-》{}",result);
        log.info("更新状态");
                try {
            updateStatus(testUser.getId());
        }catch (Exception e) {
            e.printStackTrace();
        } 
        return result;
    }
​
    /**
     * 被调用方法
     * @param id 查询数据库id
     * @return 更新id对应admin的状态
     */
    public void updateStatus(Integer id) {
        log.info("更新对象id为-》{}的状态",id);
        if(adminDao.update(adminDao.selectById(id), new LambdaUpdateWrapper<Admin>().set(Admin::getStatus,1).eq(Admin::getId,id))>0){
            log.info("更新成功!" );
        }else {
            log.info("更新失败!" );
            throw new MyException(501,"模拟异常");
        }
    }

此时insertData作为调用者方法,updateSatus作为被insertData调用的方法。这样分别加入事务不同的事务传播机制后,分别测试不同的事务传播行为对方法执行的影响结果。先来看看如果不加入事务处理会怎样:

将一个admin数据库中不存在的id加入测试数据并测试。

@Autowired
    private TestUserServiceImpl testUserService;
​
    private static final TestUser tu =new TestUser();
    static {
        tu.setId(103).setName("张三");
    }
    @Test
     void requiredResult(){
        System.out.println(testUserService.insertData(tu));
    }

此时执行insertData方法时,由于它先调用更新状态的方法查询数据库并更新,但是数据库没id为103的数据,更新失败并抛出异常,那么最后数据库结果会怎样呢。。。

JDBC Connection [HikariProxyConnection@1266035080 wrapping com.mysql.cj.jdbc.ConnectionImpl@24f2608b] will not be managed by Spring
==>  Preparing: INSERT INTO test_user ( id, name, createTime ) VALUES ( ?, ?, ? )
==> Parameters: 103(Integer), 张三(String), 2023-07-06 20:01:27.666(Timestamp)
<==    Updates: 1
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@51172948]
2023-07-06 20:01:28.628  INFO 7752 --- [           main] c.y.t.service.Impl.TestUserServiceImpl   : 获取数据插入结果->数据插入成功
2023-07-06 20:01:28.628  INFO 7752 --- [           main] c.y.t.service.Impl.TestUserServiceImpl   : 更新状态
2023-07-06 20:01:28.628  INFO 7752 --- [           main] c.y.t.service.Impl.TestUserServiceImpl   : 更新对象id为-》103的状态
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@736f8837] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1233162869 wrapping com.mysql.cj.jdbc.ConnectionImpl@24f2608b] will not be managed by Spring
==>  Preparing: select id,username,pwd,avatar,status,token from admin where id= ?
==> Parameters: 103(Integer)
<==      Total: 0
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@736f8837]
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c3b0cc8] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@43924520 wrapping com.mysql.cj.jdbc.ConnectionImpl@24f2608b] will not be managed by Spring
==>  Preparing: UPDATE admin SET status=? WHERE (id = ?)
==> Parameters: 1(Integer), 103(Integer)
<==    Updates: 0
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@2c3b0cc8]
2023-07-06 20:01:28.693  INFO 7752 --- [           main] c.y.t.service.Impl.TestUserServiceImpl   : 更新失败!
​
模拟异常
MyException(code=501)

可以看到当数据插入成功后,后面的操作如果出现异常,数据库操作不会回滚,这样就好比扣钱没订单支付完成的信息,这在日常业务开发中肯定是不行的,因此需要我们用事务来处理。

Propagation.REQUIRED

这个表示没有事务就加上事务,有就按事务执行。作为事务默认的传播方式这也是最常见的一种。这里可以根据事物的这种默认传播行为考虑一下多个方法存在嵌套关系时的事务处理关系。

当类中出现这种方法的嵌套关系时,如果将被调用的方法加入事务处理是否会回滚呢?也就是一个没有事务的方法里嵌套一个有事物的方法。

/**
     * 被调用方法,加入Propagation.REQUIRED事务处理
     * @param id 查询数据库id
     * @return 更新id对应admin的状态
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = MyException.class)
    public void updateStatus(Integer id) {
        log.info("更新对象id为-》{}的状态",id);
        if(adminDao.update(adminDao.selectById(id), new LambdaUpdateWrapper<Admin>().set(Admin::getStatus,1).eq(Admin::getId,id))>0){
            log.info("更新成功!" );
        }else {
            log.info("更新失败!" );
            throw new MyException(501,"模拟异常");
        }
    }

然后再次由insertData调用该方法,测试:

@Autowired
    private TestUserServiceImpl testUserService;
​
    private static final TestUser tu =new TestUser();
    static {
        tu.setId(104).setName("张三");
    }
    @Test
     void requiredResult(){
        System.out.println("无事务处理的执行结果:"+testUserService.insertData(tu));
    }

数据库中仍然插入了id为104的数据,并没有实现回滚。这是因为没有加入事务处理的类被代理后,代理类中调用的仍然是updateStatus的原始方法。

反过来如果在有事物的方法里调用一个没有事务的方法呢?

/**
     * insertData作为方法调用者,加入事务
     * @param testUser 新对象
     * @return 方法执行结果
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = MyException.class)
    public String insertData(TestUser testUser) {
        testUser.setCreateTime(new Date());
        String result=testUserDao.insert(testUser)>0?"数据插入成功":"数据插入失败";
        log.info("获取数据插入结果->{}",result);
        log.info("更新状态");
                try {
            updateStatus(testUser.getId());
        }catch (Exception e) {
            e.printStackTrace();
        } 
        return result;
    }
​
    /**
     * 被调用方法,加入Propagation.REQUIRED事务处理
     * @param id 查询数据库id
     * @return 更新id对应admin的状态
     */
    //@Transactional(propagation = Propagation.REQUIRED,rollbackFor = MyException.class)
    public void updateStatus(Integer id) {
        log.info("更新对象id为-》{}的状态",id);
        if(adminDao.update(adminDao.selectById(id), new LambdaUpdateWrapper<Admin>().set(Admin::getStatus,1).eq(Admin::getId,id))>0){
            log.info("更新成功!" );
        }else {
            log.info("更新失败!" );
            throw new MyException(501,"模拟异常");
        }
    }

此时,进行测试,插入一个id为105的数据,虽然方法仍然执行失败,这是肯定的,毕竟有异常。

Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1fb30e5d]
JDBC Connection [HikariProxyConnection@1120514542 wrapping com.mysql.cj.jdbc.ConnectionImpl@217c6a1e] will be managed by Spring
==>  Preparing: INSERT INTO test_user ( id, name, createTime ) VALUES ( ?, ?, ? )
==> Parameters: 105(Integer), 张三(String), 2023-07-06 21:01:31.562(Timestamp)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1fb30e5d]
2023-07-06 21:01:31.675  INFO 10940 --- [           main] c.y.t.service.Impl.TestUserServiceImpl   : 获取数据插入结果->数据插入成功
2023-07-06 21:01:31.675  INFO 10940 --- [           main] c.y.t.service.Impl.TestUserServiceImpl   : 更新状态
2023-07-06 21:01:31.675  INFO 10940 --- [           main] c.y.t.service.Impl.TestUserServiceImpl   : 更新对象id为-》105的状态
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1fb30e5d] from current transaction
==>  Preparing: select id,username,pwd,avatar,status,token from admin where id= ?
==> Parameters: 105(Integer)
<==      Total: 0
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1fb30e5d]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1fb30e5d] from current transaction
==>  Preparing: UPDATE admin SET status=? WHERE (id = ?)
==> Parameters: 1(Integer), 105(Integer)
<==    Updates: 0
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1fb30e5d]
2023-07-06 21:01:31.768  INFO 10940 --- [           main] c.y.t.service.Impl.TestUserServiceImpl   : 更新失败!
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1fb30e5d]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1fb30e5d]
​
模拟异常
MyException(code=501)
    at com.yy.testUser.service.Impl.TestUserServiceImpl.updateStatus(TestUserServiceImpl.java:76)
    at com.yy.testUser.service.Impl.TestUserServiceImpl.insertData(TestUserServiceImpl.java:60)

但是,这个方法中数据库操作其实已经回滚了。因为数据库并未插入id为105的数据!

 e1692656109841b5ae72d30a9cfb2b7f.png

原因就是代理的时候,直接把没有事务的方法包在了有事务的代理方法里面了,所以事务的效果就实现了,不成功便回滚。

类似于如下形式:

//被代理后的方法
proxyInsertData(){
        //开启事务
        原始类.insertData(){
            原始类.updateStatus();
        }
    }

这样执行后的方法即使出现异常,也不会在业务方法执行失败的情况下扣你钱了。所以如果是在同一个类上进行事务嵌套的话,其结果取决于外层方法事物的传播特性。并且当内部方法发生异常情况时,即使外部方法捕获处理该异常,依然数据会被回滚。这也正对了Required的作用:外部方法存在事务且使用Propagation.REQUIRED修饰时,所有内部方法不会新建事务,直接运行在当前事务中(前提是没指定其他独有的传播特性,这点后面继续看

Propagation.SUPPORTS

如果存在事务,方法将在事务中执行;如果不存在事务,方法将以非事务方式执行。也就是说他是跟着外部方法走的,当外部方法就指定这个传播行为时,也就相当于没有事务一样。

/**
     * insertData作为方法调用者,加入SUPPORTS
     * @param testUser 新对象
     * @return 方法执行结果
     */
    @Transactional(propagation = Propagation.SUPPORTS,rollbackFor = MyException.class)
    public String insertData(TestUser testUser) {
        testUser.setCreateTime(new Date());
        String result=testUserDao.insert(testUser)>0?"数据插入成功":"数据插入失败";
        log.info("获取数据插入结果->{}",result);
        log.info("更新状态");
                try {
            updateStatus(testUser.getId());
        }catch (Exception e) {
            e.printStackTrace();
        } 
        return result;
    }
​
    
    /**
     * 被调用方法,加入Propagation.REQUIRED事务处理
     * @param id 查询数据库id
     * @return 更新id对应admin的状态
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = MyException.class)
    public void updateStatus(Integer id) {
        log.info("更新对象id为-》{}的状态",id);
        if(adminDao.update(adminDao.selectById(id), new LambdaUpdateWrapper<Admin>().set(Admin::getStatus,1).eq(Admin::getId,id))>0){
            log.info("更新成功!" );
        }else {
            log.info("更新失败!" );
            throw new MyException(501,"模拟异常");
        }
    }

此时就像第一次测试的时候,里面的updateStatus方法有Propagation.REQUIRED修饰,但是外部insertData没有事务。所以插入数据时,即使后面存在异常情况,执行操作结果不会触发事务回滚机制。

Propagation.MANDATORY

Mandatory表示被修饰的方法必须在事务中运行。很容易理解,必须在事务方法里面用,否则直接抛异常。

/**
     * insertData作为方法调用者,MANDATORY
     * @param testUser 新对象
     * @return 方法执行结果
     */
    @Transactional(propagation = Propagation.MANDATORY,rollbackFor = MyException.class)
    public String insertDataMANDATORY(TestUser testUser) {
        testUser.setCreateTime(new Date());
        String result=testUserDao.insert(testUser)>0?"数据插入成功":"数据插入失败";
        log.info("获取数据插入结果->{}",result);
        log.info("更新状态");
                try {
            updateStatus(testUser.getId());
        }catch (Exception e) {
            e.printStackTrace();
        } 
        return result;
    }
​
    
    /**
     * 被调用方法,加入Propagation.REQUIRED事务处理
     * @param id 查询数据库id
     * @return 更新id对应admin的状态
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = MyException.class)
    public void updateStatus(Integer id) {
        log.info("更新对象id为-》{}的状态",id);
        if(adminDao.update(adminDao.selectById(id), new LambdaUpdateWrapper<Admin>().set(Admin::getStatus,1).eq(Admin::getId,id))>0){
            log.info("更新成功!" );
        }else {
            log.info("更新失败!" );
            throw new MyException(501,"模拟异常");
        }
    }

这样运行测试就直接抛异常:

org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'
​
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:362)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:574)
    ……

所以如果外部有事物,里面的方法用Mandatory传播行为的话就没事,里面的方法执行时加入到外面方法的事务中,并按事务的规则执行。

Propagation.REQUIRES_NEW

表示被修饰的方法必须运行在它自己的事务中。一个新的事务会被启动。如果调用者存在当前事务,则在该方法执行期间,当前事务会被挂起。也就是说内部调用的传播行为是这个的方法都会先架空外部的方法的事务,独立再新建自己的事务执行方法,彼此隔离,最后才走外部事务:

/**
     * insertData作为方法调用者
     * @param testUser 新对象
     * @return 方法执行结果
     */
    @Transactional(propagation = Propagation.REQUIRES_NEW,rollbackFor = MyException.class)
    public String insertData(TestUser testUser) {
        testUser.setCreateTime(new Date());
        String result=testUserDao.insert(testUser)>0?"数据插入成功":"数据插入失败";
        log.info("获取数据插入结果->{}",result);
        log.info("更新状态");
        try {
            updateStatus(testUser.getId());
        }catch (Exception e) {
            e.printStackTrace();
        } 
        return result;
    }
​
    
    /**
     * 被调用方法,加入Propagation.REQUIRES_NEW
     * @param id 查询数据库id
     * @return 更新id对应admin的状态
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = MyException.class)
    public void updateStatus(Integer id) {
        log.info("更新对象id为-》{}的状态",id);
        if(adminDao.update(adminDao.selectById(id), new LambdaUpdateWrapper<Admin>().set(Admin::getStatus,1).eq(Admin::getId,id))>0){
            log.info("更新成功!" );
        }else {
            log.info("更新失败!" );
            throw new MyException(501,"模拟异常");
        }
    }

再次测试,插入一个id为107的数据,更新状态失败时抛异常:

Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1fb30e5d]
JDBC Connection [HikariProxyConnection@1120514542 wrapping com.mysql.cj.jdbc.ConnectionImpl@217c6a1e] will be managed by Spring
==>  Preparing: INSERT INTO test_user ( id, name, createTime ) VALUES ( ?, ?, ? )
==> Parameters: 107(Integer), 第107号测试(String), 2023-07-06 22:52:50.194(Timestamp)
<==    Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1fb30e5d]
2023-07-06 22:52:50.279  INFO 17768 --- [           main] c.y.t.service.Impl.TestUserServiceImpl   : 获取数据插入结果->数据插入成功
[org.apache.ibatis.session.defaults.DefaultSqlSession@1fb30e5d]
2023-07-06 22:52:50.283  INFO 17768 --- [           main] c.y.t.service.Impl.TestUserServiceImpl   : 更新状态
2023-07-06 22:52:50.283  INFO 17768 --- [           main] c.y.t.service.Impl.TestUserServiceImpl   : 更新对象id为-》107的状态
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1fb30e5d] from current transaction
==>  Preparing: select id,username,pwd,avatar,status,token from admin where id= ?
==> Parameters: 107(Integer)
<==      Total: 0
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1fb30e5d]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1fb30e5d] from current transaction
==>  Preparing: UPDATE admin SET status=? WHERE (id = ?)
==> Parameters: 1(Integer), 107(Integer)
<==    Updates: 0
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@1fb30e5d]
2023-07-06 22:52:50.357  INFO 17768 --- [           main] c.y.t.service.Impl.TestUserServiceImpl   : 更新失败!
MyException(code=501)
    at com.yy.testUser.service.Impl.TestUserServiceImpl.updateStatus(TestUserServiceImpl.java:83)
    at com.yy.testUser.service.Impl.TestUserServiceImpl.insertData(TestUserServiceImpl.java:62)

这里很明显数据库插入了id为107的测试数据,但是更新时由于没有该id所以更新失败,但是这两个事务彼此独立运行,相互隔离,因此插入操作正常执行,而异常触发回滚机制。这里就是上面使用Required说到的问题:如果内部的方法有其他事务传播行为,那么外部方法即使标名使用Required,内部也会执行它自己的传播行为

Propagation.NOT_SUPPORTED

表示被修饰的方法不会在事务中运行,如果调用的方法已经存在了事务,也会将其架空,当前的事务将被挂起。在测试这个事务传播行为之前,我们要想一下同一个类中出现事务嵌套和不同类中的事务嵌套是否是一致的问题。上面也说到了外部方法存在事务且使用Propagation.REQUIRED修饰时,所有内部方法不会新建事务,直接运行在当前事务中,但是测试时我们两个方法都在同一类中,因此其实观点有局限性。因此这次借助Not_Supported这个传播行为接着看。

新建一个外部类AdminServiceImpl并添加一个存在异常的测试方法,并指定其事务传播行为为Not_Supported

@Service
@Slf4j
public class  extends ServiceImpl<AdminDao, Admin>
    implements AdminService{
  @Resource
  private AdminDao adminDao;
  @Transactional(propagation = Propagation.NOT_SUPPORTED,rollbackFor = MyException.class)
    public void updateStatus(Integer id) {
        Admin status = Admin.builder().id(id).status((byte) 1).build();
        //更新信息
        adminDao.updateById(status);
        //模拟异常
        int a =10/0;
    }
}

稍微修改测试类TestUserServiceImpl

@Service
@Slf4j
public class TestUserServiceImpl extends ServiceImpl<TestUserDao, TestUser> implements TestUserService {
    @Resource
    private TestUserDao testUserDao;
​
    @Resource
    private AdminDao adminDao;
    @Resource
    private AdminServiceImpl adminService;
    /**
     * insertData作为方法调用者,测试同类下的事务嵌套
     * @param testUser 新对象
     * @return 方法执行结果
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = MyException.class)
    public String insertData(TestUser testUser) {
        testUser.setCreateTime(new Date());
        String result=testUserDao.insert(testUser)>0?"数据插入成功":"数据插入失败";
        log.info("获取数据插入结果->{}",result);
        log.info("更新状态");
        try {
        //同类下的updateStatus方法
            updateStatus(testUser.getId());
         //另一个类下的updateStatus方法
         //   adminService.updateStatus(testUser.getId());
        }catch (Exception e) {
            throw new MyException(501,"模拟异常");
        }
        return result;
        
    }
​
    
    /**
     * 被调用方法,加入Propagation.NOT_SUPPORTED
     * @param id 查询数据库id
     * @return 更新id对应admin的状态
     */
    @Transactional(propagation = Propagation.NOT_SUPPORTED,rollbackFor = MyException.class)
    public void updateStatus(Integer id) {
        log.info("更新对象id为-》{}的状态",id);
        adminDao.update(adminDao.selectById(id), new LambdaUpdateWrapper<Admin>().set(Admin::getStatus,1).eq(Admin::getId,id));
        //模拟异常
        int a = 10/0;
    }
    
}

此时当同一个方法内部存在事务嵌套时->insertData调用同类下的updateStatus方法,测试一个数据库中存在的id,检查数据插入以及更新后的结果:

 @Autowired
    private TestUserServiceImpl testUserService;
​
    private static final TestUser tu =new TestUser();
    static {
        tu.setId(202).setName("第202号测试");
    }
    
    @Test
     void requiredResult(){
        System.out.println("处理的执行结果:"+testUserService.insertData(tu));
    }

703b407e6b4f4bc4ad4de04be8af42ae.png

 显然虽然Not_Supported会使方法不在事务中运行,但是由于外部方法insertData存在事务,使得下面的方法仍然加入了事务,因此真个测试方法整体回滚了。因此同一个类中事务嵌套的话,最终结果取决于外层方法事务的传播特性

然后测试一下当嵌套的方法是另一个类中的方法时:

 /**
     * insertData作为方法调用者,测试不同类下的事务嵌套
     * @param testUser 新对象
     * @return 方法执行结果
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = MyException.class)
    public String insertData(TestUser testUser) {
        testUser.setCreateTime(new Date());
        String result=testUserDao.insert(testUser)>0?"数据插入成功":"数据插入失败";
        log.info("获取数据插入结果->{}",result);
        log.info("更新状态");
        try {
        //同类下的updateStatus方法
         //   updateStatus(testUser.getId());
         //另一个类下的updateStatus方法
         adminService.updateStatus(testUser.getId());
        }catch (Exception e) {
            throw new MyException(501,"模拟异常");
        }
        return result;
    }

9bce36d471174add85df491e8f057447.png

 可以看到,更新方法虽然有异常但是仍然更新成功了,insertData方法回滚。原因在于:插入数据的方法执行成功后,因为被调用的不同类下的更新操作的方法updateStatusNot_Supported修饰,因此它是以非事务的情况执行的,也就是他先更新,然后出现了异常,没有回滚。然后外部的insertData方法执行时发现了那个异常,虽然抛出了异常,但是由于外部方法事务传播行为是REQUIRED的,所以整体上它仍然走事务,所以回滚了。但是更新仍然执行成功了。

这也就是说当不同类的事务存在嵌套的时候,外层方法按照外层的事务传播行为执行,内层的方法按照内层的传播行为去执行。同类与不同类下的事务嵌套执行方式是不同的

Propagation.NEVER

这个就很显然易见了,被该传播行为修饰的方法不能运行在存在事务的上下文中,否则就会抛出异常。

@Service
@Slf4j
public class TestUserServiceImpl extends ServiceImpl<TestUserDao, TestUser> implements TestUserService {
    @Resource
    private TestUserDao testUserDao;
​
    @Resource
    private AdminDao adminDao;
    @Resource
    private AdminServiceImpl adminService;
    /**
     * insertData作为方法调用者
     * @param testUser 新对象
     * @return 方法执行结果
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = MyException.class)
    public String insertData(TestUser testUser) {
        testUser.setCreateTime(new Date());
        String result=testUserDao.insert(testUser)>0?"数据插入成功":"数据插入失败";
        log.info("获取数据插入结果->{}",result);
        log.info("更新状态");
        //这里并没有捕获可能出现的异常,不然就看不到结果了
        adminService.updateStatus(testUser.getId());
        return result;
    }
}
​
@Service
@Slf4j
public class AdminServiceImpl extends ServiceImpl<AdminDao, Admin>
    implements AdminService{
  @Resource
  private AdminDao adminDao;
    /**
     * 测试Propagation.NEVER
     * @param id
     */
    @Transactional(propagation = Propagation.NEVER,rollbackFor = MyException.class)
    public void updateStatus(Integer id) {
        Admin status = Admin.builder().id(id).status((byte) 1).build();
        adminDao.updateById(status);
        int a =10/0;
    }
}

由于外部的方法存在事务,而被调用方法不能在存在事务上下文的方法中被执行,因此直接抛出以下异常:

org.springframework.transaction.IllegalTransactionStateException: Existing transaction found for transaction marked with propagation 'never'
​
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.handleExistingTransaction(AbstractPlatformTransactionManager.java:413)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:352)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:574)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:361)
    ……

Propagation.NESTED

这个事务传播行为也称作嵌套事务,它的具体作用有如下几点:

  1. 表示当前方法已经存在一个事务,那么该方法将会在嵌套事务中运行。

  2. 嵌套的事务可以独立与当前事务进行单独地提交或者回滚。

  3. 如果当前事务不存在,那么其行为与Propagation_Required一样。

嵌套事务的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。

所以,当外部没有事务,内部方法由NESTED修饰时,就和3说的一致:

@Service
@Slf4j
public class TestUserServiceImpl extends ServiceImpl<TestUserDao, TestUser> implements TestUserService {
    @Resource
    private TestUserDao testUserDao;
​
    @Resource
    private AdminDao adminDao;
    @Resource
    private AdminServiceImpl adminService;
    /**
     * insertData作为方法调用者
     * @param testUser 新对象
     * @return 方法执行结果
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = MyException.class)
    public String insertData(TestUser testUser) {
        testUser.setCreateTime(new Date());
        String result=testUserDao.insert(testUser)>0?"数据插入成功":"数据插入失败";
        log.info("获取数据插入结果->{}",result);
        log.info("更新状态");
        try {
            adminService.updateStatus(testUser.getId());
        }catch (Exception e) {
            throw new MyException(501,"模拟异常");
        }
        return result;
    }
}
​
@Service
@Slf4j
public class AdminServiceImpl extends ServiceImpl<AdminDao, Admin>
    implements AdminService{
  @Resource
  private AdminDao adminDao;
    /**
     * 测试Propagation.NESTED
     * @param id
     */
    @Transactional(propagation = Propagation.NESTED,rollbackFor = MyException.class)
    public void updateStatus(Integer id) {
        Admin status = Admin.builder().id(id).status((byte) 1).build();
        adminDao.updateById(status);
        int a =10/0;
    }
}

6a30afc4b5d84306a8c2b894f70ebdea.png

 当外部方法有事物时,此时,如果外部方法发生异常,则内部事务一起发生回滚操作;2,如果外部无异常情况,内部被调用方法存在异常情况,则内部方法独立回滚;

这里重点测试一下第二种内部独立回滚的情况,测试条件如下:

@Service
@Slf4j
public class TestUserServiceImpl extends ServiceImpl<TestUserDao, TestUser> implements TestUserService {
    @Resource
    private TestUserDao testUserDao;
​
    @Resource
    private AdminDao adminDao;
    @Resource
    private AdminServiceImpl adminService;
    /**
     * insertData作为方法调用者
     * @param testUser 新对象
     * @return 方法执行结果
     */
    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = MyException.class)
    public String insertData(TestUser testUser) {
        testUser.setCreateTime(new Date());
        String result=testUserDao.insert(testUser)>0?"数据插入成功":"数据插入失败";
        log.info("获取数据插入结果->{}",result);
        log.info("更新状态");
        try {
        //外部捕获异常情况
            adminService.updateStatus(testUser.getId());
        }catch (Exception e) {
             e.printStackTrace();
        }
        return result;
    }
}
​
@Service
@Slf4j
public class AdminServiceImpl extends ServiceImpl<AdminDao, Admin>
    implements AdminService{
  @Resource
  private AdminDao adminDao;
    /**
     * 测试Propagation.NESTED
     * @param id
     */
    @Transactional(propagation = Propagation.NESTED,rollbackFor = MyException.class)
    public void updateStatus(Integer id) {
        Admin status = Admin.builder().id(id).status((byte) 1).build();
        adminDao.updateById(status);
        //内部方法模拟抛出异常
        throw new MyException(507,"模拟异常");
    }
}

此时外部方法存在事务,内部方法由NESTED修饰,那么执行后外部的业务会正常执行,而有异常抛出却被调用的内部方法会独立回滚。  af5230b2afce49a484a69fc01a270800.png

四,事务失效场景总结

在实际的JavaWeb开发中,可能会出现Spring事务失效的场景,一些常见情况包括:

  1. 方法未标注@Transactional注解:Spring使用注解@Transactional来声明事务,如果方法未标注该注解,那么事务将不会生效。

    解决方法:在需要开启事务的方法上添加@Transactional注解,确保事务可以正常工作。

  2. 异常未被catch处理导致事务回滚失效:当一个异常抛出时,Spring会自动回滚事务,但是如果异常被catch并进行了处理,没有再次抛出,事务将不会回滚。比如下面这样的:

    @Transactional(propagation = Propagation.REQUIRED,rollbackFor = MyException.class)
        public void updateStatus(Integer id) {
            try {
                Admin status = Admin.builder().id(id).status((byte) 1).build();
                adminDao.updateById(status);
                throw new MyException(507,"模拟异常");
            }catch (Exception e){
            }
         }

    虽然上面抛了异常,但是又被catch了,所以得再次抛出Spring事务支持的异常。(写的很蠢,仅作演示)

    解决方法:在catch块中使用throw抛出异常,以确保事务可以回滚。

  3. 事务跨越多个方法或类:当一个事务涉及到多个方法或类时,如果其中某一个方法或类没有正确配置事务,整个事务可能会失效。

    解决方法:确保涉及到事务的所有方法或类都正确配置了事务,使用合适的事务传播机制。

  4. 使用了多个数据源:当使用多个数据源时,如果事务管理器没有正确配置或没有指定使用哪个数据源,事务可能无法正常工作。

    解决方法:为每个数据源配置相应的事务管理器,确保事务管理器正确指定了使用哪个数据源。

  5. 事务方法访问修饰符不是public,否则会导致事务失效

    原因也很简单,因为上面说了,Spring的事务机制本质是动态代理实现的,对于JDK的动态代理,它只能代理实现了接口的类,而接口是默认都是public的。而Cglib对于pricate方法也是无法代理的,所以事务会失效。

  6. 事务方法是staticfinal修饰的事务也会失效

    同样因为Spring的声明式事务是基于动态代理实现的,所以无法重写final修饰的方法;不管是JDK动态代理还是Cglib的动态代理,就是要通过代理的方式获取到代理的具体对象,而static方法修饰的方法是属于类对象的,不属于任何实例对象,所以static方法不能被重写也就是说static方法也不被进行动态代理。

  7. 操作的数据库表本身不支持事务的话配置Spring事务也会失效,比如你的mysql存储引擎是MyISAM的。

五,事务传播机制使用场景

事务传播机制在以下开发场景中能够提供便利:

  • 多个方法之间的数据一致性要求高的情况,可以使用REQUIRED传播机制,确保方法在同一个事务中执行。

  • 需要在某个方法中开启新的事务,并独立于外部事务进行管理的情况,可以使用REQUIRES_NEW传播机制。

  • 需要嵌套事务,并能够独立于外部事务进行回滚的情况,可以使用NESTED传播机制。

总之,Spring框架中的事务传播机制提供了灵活的事务控制手段,可以根据具体的需求选择合适的传播行为,简化开发流程,确保数据一致性,并提供高级别的事务管理功能。不足之处欢迎指正~

 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值