Spring——事务注解@Transactional【建议收藏】

2023-11-10

在某些业务场景下,如果一个请求中,需要同时写入多张表的数据或者执行多条sql,为了保证操作的原子性(要么同时成功,要么同时失败),避免数据不一致的情况,我们一般都会用到事务;Spring框架下,我们经常会使用@Transactional注解来管理事务;

本篇介绍Spring的事务注解@Transactional相关的知识,包括事务简介、事务隔离级别、Spring声明式事务实现原理、事务的传播行为、@Transactional的用法及注意事项等,属于Spring的常用注解之一,需要掌握相关知识点;

Spring AOP是实现Spring声明式事务的基础,相关知识可参考我之前的文章《Spring AOP用到的代理模式&SpringAOP实现原理》

1. Spring事务的相关的概念

事务指逻辑上的一组操作,组成这组操作的各个单元,要不全部成功,要不全部不成功;下面介绍事务相关的基本概念;

1.1 事务基本要素

ACID,是指数据库管理系统DBMS)在写入或更新资料的过程中,为保证事务(transaction)是正确可靠的,所必须具备的四个特性:原子性(atomicity,或称不可分割性)、一致性(consistency)、隔离性(isolation,又称独立性)、持久性(durability);

在数据库系统中,一个事务是指:由一系列数据库操作组成的一个完整的逻辑过程;例如银行转帐,从原账户扣除金额,以及向目标账户添加金额,这两个数据库操作的总和,构成一个完整的逻辑过程,不可拆分;这个过程被称为一个事务,具有ACID特性;

  • 原子性(Atomicity): 事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。
  • 一致性(Consistency): 事务开始前和结束后,数据库的完整性约束没有被破坏。比如A向B转账,不可能A扣了钱,B却没收到。
  • 隔离性(Isolation): 同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。
  • 持久性(Durability): 事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。

1.2 Spring中使用事务的两种方式

Spring为事务管理提供了丰富的功能支持,Spring事务管理分为编码式和声明式的两种方式:

  • 1)编程式事务:允许用户在代码中精确定义事务的边界,通过编程代码在业务逻辑时需要时自行实现,粒度更小;

编程式事务管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager;对于编程式事务管理,Spring推荐使用TransactionTemplate;

  • 2)声明式事务:基于Spring AOP,通过注解或XML配置实现;有助于用户将操作与事务规则进行解耦;

其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务;声明式事务管理也有两种常用的方式,一种是在XML配置文件中做相关的事务规则声明,另一种是基于@Transactional注解的方式,显然基于注解的方式更简单易用,更清爽;

显然声明式事务要优于编程式事务,这正是Spring倡导的非侵入式的开发方式;声明式事务管理使业务代码不受污染,一个方法需要事务支持,只要加上注解即可;

和编程式事务相比,声明式事务也存在缺点,因为基于Spring AOP(动态代理),声明式事务最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别;当然也可以将需要进行事务管理的代码块独立为方法;此外,同类方法之间的调用不会被AOP拦截,从而导致事务注解失效;

由于编程式事务是侵入式事务管理,硬编码到项目代码中,影响业务逻辑代码,所以一般来说推荐使用声明式事务来实现事务,其中通过@Transactional注解实现事务被广泛使用;

1.3 Spring事务的几个属性

Spring事务属性对应TransactionDefinition类里面的各个方法,如下所示:

// org.springframework.transaction.TransactionDefinition
public interface TransactionDefinition {
    //...

    /**
     * 返回事务传播行为
     */
    int getPropagationBehavior();

    /**
     * 返回事务的隔离级别,事务管理器根据它来控制另外一个事务可以看到本事务内的哪些数据
     */
    int getIsolationLevel();

    /**
     * 事务超时时间,事务必须在多少秒之内完成
     */
    int getTimeout();

    /**
     * 事务是否只读,事务管理器能够根据这个返回值进行优化,确保事务是只读的
     */
    boolean isReadOnly();

    /**
     * 事务名字
     */
    @Nullable
    String getName();
}

事务属性可以理解成事务的一些基本配置,描述了事务策略如何应用到方法上;事务属性包含了几个方面:传播行为、隔离规则、回滚规则、事务超时、是否只读;

事务的创建需要依赖这些事务属性,包括下面要介绍的@Transactional注解的属性其实就是在设置这些值;

1.3.1 传播方式

如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为;例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行;Spring定义了七种传播方式:

1. TransactionDefinition.PROPAGATION_REQUIRED:
   "如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。"
 
2. TransactionDefinition.PROPAGATION_REQUIRES_NEW:
   "创建一个新的事务,如果当前存在事务,则把当前事务挂起。"
 
3. TransactionDefinition.PROPAGATION_SUPPORTS:
   "如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。"
 
4. TransactionDefinition.PROPAGATION_NOT_SUPPORTED:
   "以非事务方式运行,如果当前存在事务,则把当前事务挂起。"
 
5. TransactionDefinition.PROPAGATION_NEVER:
   "以非事务方式运行,如果当前存在事务,则抛出异常。"
 
6. TransactionDefinition.PROPAGATION_MANDATORY:
   "如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。"
 
7. TransactionDefinition.PROPAGATION_NESTED:
   "如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;"
   "如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。"

工作开发中,我们最常用的就是默认的事务传播行为:如果外层方法有事务则加入进去,没有则在当前方法创建一个事务;关于这几种事务传播行为的测试验证和介绍,可以参考这篇文章《详解Spring事务的传播机制 - 知乎》

1.3.2 隔离规则

事务的隔离级别是指若干个并发的事务之间的隔离程度,它定义了一个事务可能受其他并发事务影响的程度;多个事务并发运行,可能会导致以下的问题:

  • 脏读(Dirty reads)—— 事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据。
  • 不可重复读(Nonrepeatable read)—— 事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。
  • 幻读(Phantom read)—— 系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

为了避免事务并发状态下脏读、不可重复读、幻读的产生,Spring中定义了五种隔离规则:

1. @Transactional(isolation = Isolation.DEFAULT)
   "使用后端数据库默认的隔离级别 对于MYSQL来说就是可重复读"

1. @Transactional(isolation = Isolation.READ_UNCOMMITTED)
   "是最低的隔离级别,允许读取尚未提交的数据变更(会出现脏读,不可重复读),基本不使用"
 
2. @Transactional(isolation = Isolation.READ_COMMITTED)
   "允许读取并发事务已经提交的数据(会出现不可重复读和幻读)"
 
3. @Transactional(isolation = Isolation.REPEATABLE_READ)
   "事物开启后,对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改(会出现幻读)"
 
4. @Transactional(isolation = Isolation.SERIALIZABLE)
   "最高的隔离级别,完全服从ACID的隔离级别,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的"

说明:

  • 事务隔离级别为ISOLATION_READ_UNCOMMITTED时,写数据只会锁住相应的行;
  • 事务隔离级别为可ISOLATION_REPEATABLE_READ时,如果检索条件有索引(包括主键索引)的时候,默认加锁方式是next-key锁;如果检索条件没有索引,更新数据时会锁住整张表。一个间隙被事务加了锁,其他事务是不能在这个间隙插入记录的,这样可以防止幻读;
  • 事务隔离级别为ISOLATION_SERIALIZABLE时,读写数据都会锁住整张表;
  • 隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也就越大;

1.3.3 回滚规则

事务回滚规则定义了哪些异常会导致事务回滚而哪些不会;默认情况下,只有未检查异常(RuntimeException和Error类型的异常)会导致事务回滚,而在遇到检查型异常时不会回滚; 但是可以声明事务在遇到特定的检查型异常时像遇到运行期异常那样回滚,并且相反,还可以声明事务遇到特定的异常不回滚,即使这些异常是运行期异常;

1.3.4 事务超时

为了使应用程序很好地运行,事务不能运行太长的时间,因为事务可能涉及对后端数据库的锁定,会占用数据库资源;事务超时就是事务的一个定时器,在特定时间内事务如果没有执行完毕,那么就会自动回滚,而不是一直等待其结束;

1.3.5 是否只读

如果在一个事务中所有关于数据库的操作都是只读的,也就是说,这些操作只读取数据库中的数据,而并不更新数据, 这个时候我们应该给该事务设置只读属性,这样可以帮助数据库引擎优化事务,从而提升数据库读写效率;

2. Spring声明式事务的原理

Spring AOP将通用的功能横向抽取出来作为切面,避免非业务代码侵入到业务代码中;通过@Transactional注解就能让Spring为我们管理事务,免去了重复的事务管理逻辑,减少对业务代码的侵入,让开发人员能够专注于业务层面开发;

结合上面的图,我们关注两点:

(1)判断生成代理对象:通过@Transactional注解来标记方法(定义切点),在Bean初始化过程中判断是否要对当前Bean创建代理对象,并且拿到@Transactional注解的属性;

(2)定义代理对象的回调逻辑,即执行代理逻辑:在执行目标方法前打开事务,执行过程中捕获异常执行回滚逻辑,在执行完目标方法后提交事务;

源码分析请参考我的文章《Spring事务注解@Transactional的源码分析》,将源码的流画张图总结下:

3. @Transctional的用法介绍

下面介绍@Transctional的用法,包括:注解参数设置,事务方法调用的几种不同case,事务如何回滚,常见的事务失效的场景以及使用建议;

3.1 注解属性

// org.springframework.transaction.annotation.Transactional

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {

    /**
     * 当在配置文件中有多个 TransactionManager , 可以用该属性指定选择哪个事务管理器。
     */
    @AliasFor("transactionManager")
    String value() default "";

    /**
     * 同上。
     */
    @AliasFor("value")
    String transactionManager() default "";

    /**
     * 事务的传播行为,默认值为 REQUIRED。
     */
    Propagation propagation() default Propagation.REQUIRED;

    /**
     * 事务的隔离规则,默认值采用 DEFAULT。
     */
    Isolation isolation() default Isolation.DEFAULT;

    /**
     * 事务超时时间。
     */
    int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;

    /**
     * 是否只读事务
     */
    boolean readOnly() default false;

    /**
     * 用于指定能够触发事务回滚的异常类型。
     */
    Class<? extends Throwable>[] rollbackFor() default {};

    /**
     * 同上,指定类名。
     */
    String[] rollbackForClassName() default {};

    /**
     * 用于指定不会触发事务回滚的异常类型
     */
    Class<? extends Throwable>[] noRollbackFor() default {};

    /**
     * 同上,指定类名
     */
    String[] noRollbackForClassName() default {};

}

关于value和transactionManager属性的说明

这两个属性是一个意思,当配置了多个事务管理器时,可以使用该属性指定选择哪个事务管理器;大多数项目只需要一个事务管理器,然而,有些项目为了提高效率、或者有多个完全不同又不相干的数据源,从而使用了多个事务管理器;

Spring的Transactional管理已经考虑到了这一点,首先定义多个transactional manager,并为qualifier属性指定不同的值;然后在需要使用@Transactional注解的时候指定TransactionManager的qualifier属性值或者直接使用bean名称,配置和代码使用的例子:

    @Override
    @Transactional(transactionManager = "txManager#singleton", rollbackFor = Exception.class)
    public Boolean updateModuleStatus(Integer bizType, String moduleCode, Integer status) {
        //如果数据库无此数据,返回异常,封装返回结果Boolean.FALSE
        long moduleId = Optional.ofNullable(moduleDAO.selectByTypeAndCode(bizType, moduleCode))
                .orElseThrow(() -> new BusinessException(ResultCodeEnum.DATABASE_NO_SUCH_RECORD, Boolean.FALSE))
                .getId();
        try {
            moduleDAO.updateStatusByModuleId(moduleId, status);
            // 更新缓存
            bizCacheService.refreshModulesCache(bizType);
            return Boolean.TRUE;
        } catch (Exception e) {
            log.error("[SERIOUS_BUSINESS]update module status error! e:{}", e);
            throw new BusinessException(ResultCodeEnum.SERVER_BUSYNESS, Boolean.FALSE);
        }
    }

@Transactional注解可以作用于哪些地方?

@Transactional可以作用在接口、类、类方法;

  • 作用于类:当把@Transactional 注解放在类上时,表示所有该类的public方法都配置相同的事务属性信息,会导致事务控制的粒度太大,注解参数无法根据每个类方法的实际需求设置;因此,一般@Transactional注解都会直接添加的需要的方法上;
  • 作用于方法:当类配置了@Transactional,方法也配置了@Transactional,方法的事务会覆盖类的事务配置信息;
  • 作用于接口:不推荐这种使用方法,因为一旦标注在Interface上并且配置了Spring AOP 使用CGLib动态代理,将会导致@Transactional注解失效;

3.2 事务方法之间相互调用的case

3.2.1 同一个类中函数相互调用

假设:同一个类AClass中,有两个方法aFunction、aInnerFunction;aFunction调用aInnerFunction;aFunction函数被其他类调用

case1:两个方法都添加了@Transactional注解,aInnerFunction使用Propagation.REQUIRES_NEW传播方式;aInnerFunction抛异常;

    @Transactional(rollbackFor = Exception.class)
    public void aFunction() {
        //todo: 数据库操作A(增,删,该)
        aInnerFunction(); // 调用内部没有添加@Transactional注解的函数
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void aInnerFunction() {
        //todo: 操作数据B(做了增,删,改 操作)
        throw new RuntimeException("函数执行有异常!");
    }

结果:两个函数操作的数据都会回滚;同类调用,不涉及事务传播,相当于aInnerFunction的代码加到了aFunction方法内;

case2: aFunction不添加注解,aInnerFunction添加注解;aInnerFunction抛异常;

    public void aFunction() {
        //todo: 数据库操作A(增,删,该)
        aInnerFunction(); // 调用内部没有添加@Transactional注解的函数
    }

    @Transactional(rollbackFor = Exception.class)
    public void aInnerFunction() {
        //todo: 操作数据B(做了增,删,改 操作)
        throw new RuntimeException("函数执行有异常!");
    }

结果:两个函数对数据库的操作都不会回滚;因为同类方法调用不会调用代理对象的方法,@Transactional注解添加和没添加一样;

3.2.2 不同类中函数相互调用

假设:两个类AClass、BClass;AClass类有aFunction、BClass类有bFunction;AClass类aFunction调用BClass类bFunction;AClass类的aFunction被其他类调用;。

case1:aFunction添加注解,bFunction不添加注解;bFunction抛异常;

@Service
public class AClass {
    @Autowired
    private BClass bClass;

    @Transactional(rollbackFor = Exception.class)
    public void aFunction() {
        //todo: 数据库操作A(增,删,该)
        bClass.bFunction();
    }
}

@Service
public class BClass {

    public void bFunction() {
        //todo: 数据库操作A(增,删,该)
        throw new RuntimeException("函数执行有异常!");
    }
}

结果:两个函数对数据库的操作都回滚了;相当于aFunction执行时抛了异常;此时,bFunction如果打上事务注解并且使用默认的事务传播方式,结果也一样;因为两个方法处于同一个事务内;

case2:aFunction、bFunction两个函数都添加事务注解;bFunction抛异常;aFunction抓出异常并吞掉异常

@Service
public class AClass {
    @Autowired
    private BClass bClass;

    @Transactional(rollbackFor = Exception.class)
    public void aFunction() {
        //todo: 数据库操作A(增,删,该)
        try {
            bClass.bFunction();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

@Service
public class BClass {
    @Transactional(rollbackFor = Exception.class)
    public void bFunction() {
        //todo: 数据库操作A(增,删,该)
        throw new RuntimeException("函数执行有异常!");
    }
}

结果:两个函数数据库操作都没成功,而且还抛异常了org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only;

可以这么理解,两个函数用的是同一个事务;bFunction函数抛了异常,调了事务的rollback函数,并且事务被标记了只能rollback了;程序继续执行,aFunction函数里面把异常给抓出来了,这个时候aFunction函数没有抛出异常,既然你没有异常那事务就需要提交,会调事务的commit函数;而之前这个事务已经被标记了只能rollback-only(因为是同一个事务),因此直接就抛异常了,不让调了;

case3:aFunction、bFunction两个函数都添加注解;bFunction抛异常,aFunction抓出异常;这里要注意bFunction函数@Transactional注解我们是有变化的,加了一个参数propagation = Propagation.REQUIRES_NEW,控制事务的传播行为,表明是一个新的事务;其实情况3就是来解决情况2的问题的;

@Service
public class AClass {
    @Autowired
    private BClass bClass;

    @Transactional(rollbackFor = Exception.class)
    public void aFunction() {
        //todo: 数据库操作A(增,删,该)
        try {
            bClass.bFunction();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

}

@Service
public class BClass {
    @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)
    public void bFunction() {
        //todo: 数据库操作A(增,删,该)
        throw new RuntimeException("函数执行有异常!");
    }
}

结果:bFunction函数里面的操作回滚了,aFunction里面的操作成功了;有了前面情况2的理解。这种情况也很好解释,因为两个函数不是同一个事务了,所以bFunction抛异常只会导致bFunction的回滚,不影响aFunction所在事务的正常执行;

3.3 事务回滚

@Transactional默认只能回滚RuntimeException和RuntimeException下面的子类抛出的异常,不能回滚Exception异常;如果需要支持回滚Exception异常,需要显示的指明,如@Transactional(rollbackFor = Exception.class);

3.4 失效场景

介绍几种常见的事务实效的场景,这里引用一张图来说明;

图转自:《Spring事务(注解 @Transactional )失效的12种场景_悬浮海的博客》

3.4.1 打了@Transactional但是事务不生效

(1)@Transactional注解未打在public方法上

Java的访问权限主要有四种:private、default、protected、public;如果事务方法定义了错误的访问权限(非public方法),会导致事务失效;

原因:根据我的文章《Spring事务注解@Transactional的源码分析》里面的判断当前方法适用于事务切面,计算事务属性的AbstractFallbackTransactionAttributeSource#computeTransactionAttribute方法里有段逻辑,如下,指出"事务不作用于非public方法";

(2)目标方法用final修饰

某个方法不想被子类重写,可以将该方法定义成final的;如果将事务方法定义成final,会导致事务失效;

原因:Spring事务基于Spring AOP,通过JDK动态代理或者CGlib代理,在代理类中实现的事务功能;但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法;同样,static修饰的方法,同样无法通过动态代理,变成事务方法;

(3)同一个类中的方法直接内部调用

原因:方法被事务管理是因为Apring AOP为其生成代理了对象,但是直接this调用同类方法,调用的是目标类对象的方法,而非代理类方法,因此,在同类中的方法直接内部调用,会导致事务失效;

如果有些场景,确实想在同一个类的某个方法中,调用当前类的另外一个事务方法,该怎么办呢?

  • 方法1:新写一个Service,把事务方法挪过去,在当前类注入新的Service
  • 方法2:在当前Service注入自己;可能有些人可能会有这样的疑问:这种做法会不会出现循环依赖问题?不会,Spring的Bean初始化流程中使用三级缓存解决循环依赖问题,可参考我的文章《Spring——循环依赖&三级缓存【建议收藏】》
  • 方法3:在当前Service类中使用AopContext.currentProxy()获取当前类的代理对象,相比方法2更加直观;代码示例如下:
@Servcie
public class ServiceA {

   public void save(User user) {
         queryData1();
         queryData2();
         ((ServiceA)AopContext.currentProxy()).doSave(user);
   }

   @Transactional(rollbackFor=Exception.class)
   public void doSave(User user) {
       addData1();
       updateData2();
    }
 }

同一个类方法调用事务失效相关的文章:

Spring 事务实现原理 (同一个类方法调用事务失效

Spring AOP调用本类的事务方法失效

(4)事务方法所在的类未被Spring管理

使用Spring事务的前提是:对象要被Spring IOC容器管理,需要创建bean实例;打了注解,但是忘了在当前类加@Service注解,导致事务不生效,也是小白常见的编码错误;

(5)多线程调用

如果两个方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务;如果看过Spring事务源码,能会知道Spring的事务是通过数据库连接Connection来实现的;当前线程中保存了一个map,key是数据源,value是数据库连接;

我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚;如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务;

(6)存储引擎不支持事务

如MYSQL的myisam存储引擎不支持事务,有些老项目中,可能还在用它;在开发的过程中,如果发现某张表的事务一直都没有生效,可以检查下那张表的存储引擎,看是否支持事务;

(7)未开启事务

这个原因极其容易被忽略;

如果你使用的是Springboot项目,那么你很幸运,因为Springboot通过DataSourceTransactionManagerAutoConfiguration类,已经默默的帮你开启了事务,只需要配置数据源spring.datasource相关参数即可;

但如果你使用的还是传统的老Spring项目,则需要在applicationContext.xml文件中,手动配置事务相关参数;

3.4.2 事务未回滚

(1)使用了错误的传播特性,如新开启了一个事物,可能导致新事物和原事务不会一起回滚;

(2)自己吞了异常,忘记抛出了;或者抛出了非运行时异常,但又没有配置到注解上;因为Spring事务默认情况下只会回滚RuntimeException(运行时异常)和Error(错误);

本节参考 :《Spring事务(注解 @Transactional )失效的12种场景_悬浮海的博客》

3.5 使用建议

1. 要知道@Transactional注解里面每个属性的含义,@Transactional注解属性就是来控制事务属性的,通过这些属性来生成事务;

2. 要明确我们添加的@Transactional注解会不会起作用;如@Transactional注解在外部调用的函数上才有效果,直接内部调用无效;

3. 显示的指定rollbackFor注解属性,即使rollbackFor有默认值,但阿里巴巴开发者规范中,还是要求开发者重新指定该参数,因为如果使用默认值,一旦程序抛出了非运行时的其他Exception,事务不会回滚,这会出现很大的bug;

4. 要明确事务的作用范围,有@Transactional的函数调用有@Transactional的函数的时候,进入第二个函数的时候是新的事务,还是沿用之前的事务;稍不注意就会抛UnexpectedRollbackException异常;

5. 不要滥用事务,避免大事务,事务会影响数据库的读写性能,非必要场景不建议使用;适当的对方法里面的实务操作拆分执行;

4. 编程式事务

上面的这些内容都是基于@Transactional注解的,这种事务使用方式叫做声明式事务;其实,Spring还提供了另外一种创建事务的方式,即通过硬编码的方式使用Spring中提供的事务相关的类来控制事务,这种方式叫做编程式事务;

编程式事务主要有两种用法:1)通过事务管理器PlatformTransactionManager控制事务 和2)通过事务模板TransactionTemplate控制事务;常用的是TransactionTemplate,如下:

   @Resource
   private DataSourceTransactionManager transactionManager;
   
    // 同类方法调用 使用编程式事务
    TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
    transactionTemplate.execute(transactionStatus -> {
		queryData();
		insertData();
		return Boolean.TRUE;
    });
   });

的确,看起来比较复杂,代码中融入了大量Spring的代码;但是编程式事务有时候也能带来一些好处:

1. 避免由于Spring AOP问题导致的事务失效的问题,如同类事务方法调用;
2. 可以对代码块加事务,能够更小粒度的更精确的控制事务的范围,一定程度避免事务滥用;

建议:如果项目中有些业务逻辑比较简单,而且不经常变动,建议使用@Transactional注解开启事务,因为它更简单,开发效率更高,但是千万要小心事务失效的问题;而使用编程式事务有时可以帮我们解决@Transactional注解声明式事务解决不了的问题,如同类调用;二者可以配合使用;

本节参考:30.spring系列- 事务(编程式事务)

5. 其他方式的关于事务的使用示例

(1)编程式事务

    /**
     * 执行投递并将重试次数+1 编程式事务
     *
     * @param reqDTO
     * @param recordInDB
     */
    public void deliverMessageThenUpdateRetryTimes(MessageDeliverTaskReqDTO reqDTO, MessageDeliverTaskDO recordInDB) {

        // 同类方法调用使用 编程式事务
        TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
        transactionTemplate.execute(transactionStatus -> {
            final Long recordId = recordInDB.getId();
            // [投递消息-子类实现]
            deliverMessage(reqDTO, recordInDB);

            // 更新 不需要回执则更形成`已处理`
            final Integer messageStatus = Boolean.FALSE.equals(reqDTO.getNeedCallback()) ? MessageStatusEnum.HANDLED.getType() : null;
            final int update = messageDeliverTaskDAO.plusRetryTime(recordId, messageStatus);
            log.warn("deliverMessageThenUpdateRetryTimes_suc. [recordId={} reqDTO={}]", recordId, JSON.toJSONString(reqDTO));
            return update > 0;
        });

    }

(2)先提交事务再执行RPC(RPC需要剥离出事务)

    /**
     * 会员订单后置处理
     */
    private void postProcessHandleMemberOrder(MemberOrderDO memberOrderDO) {
        if (MemberOrderStatusEnum.PAID.getStatus().equals(memberOrderDO.getStatus())) {
            // fixme 要求一定要更新订单表和消息入库 才能发消息 极端情况RPC发消息调用瞬间 core回调 但是DB事务还没有提交完成 RPC应该剥离出事务 在事务提交后执行
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
                @Override
                public void afterCommit() {
                    // 支付成功发消息
                    Map<String, String> msgBody = buildPaySucMsgBody(memberOrderDO);
                    boolean sendMsgResult = notifyService.sendPayMsg(msgBody);
                    if (sendMsgResult) {
                        log.warn("send paySucMsg to core sus.[orderNo={} msg={}]", memberOrderDO.getOrderNo(), JSON.toJSONString(msgBody));
                    } else {
                        log.warn("send paySucMsg to core failed.[orderNo={} msg={}]", memberOrderDO.getOrderNo(), JSON.toJSONString(msgBody));
                    }
                    if (StringUtils.isNotBlank(memberOrderDO.getAgreementNo())) {
                        // 支付成功后,查询是否有需要投递的签约信息,用于处理微信纯签约,新用户购买,在收到支付回调之后,需要进行处理
                        MessageDeliverDO messageDeliverDO = messageDeliverDAO.queryByOrderAndType(memberOrderDO.getAgreementNo(), MessageDeliverOrderTypeEnum.AGREEMENT.getType());
                        if (messageDeliverDO != null) {
                            // 发送签约信息
                            Map<String, String> signedMsgBody = JSON.parseObject(messageDeliverDO.getMsgBody(), Map.class);
                            boolean sendSignedMsgResult = notifyService.sendSignMsg(signedMsgBody);
                            log.warn("pay_suc_send_signSucMsg_to_core_sus.[orderNo={} res={} msg={}]", memberOrderDO.getOrderNo(), sendSignedMsgResult, JSON.toJSONString(sendSignedMsgResult));
                        }
                    }
                }
            });
        }
    }

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Spring——事务注解@Transactional【建议收藏】 的相关文章

  • JavaMail Gmail 问题。 “准备启动 TLS”然后失败

    mailServerProperties System getProperties mailServerProperties put mail smtp port 587 mailServerProperties put mail smtp
  • 如何在 JFace 的 TableViewer 中创建复选框?

    我创建了一个包含两列的 tableViewer 我想将其中一列设为复选框 为此 我创建了一个 CheckBoxCellEditor 但我不知道为什么它不起作用 名为 tableName 的列显示其值正常 色谱柱规格如下 String COL
  • AES 加密 Java/plsql

    我需要在Java和plsql DBMS CRYPTO for Oracle 10g 上实现相同的加密 解密应用程序 两种实现都工作正常 但这里的问题是我对相同纯文本的加密得到了不同的输出 下面是用于加密 解密过程的代码 Java 和 PLS
  • 在Windows上安装Java 11 OpenJDK(系统路径问题)

    Java 11 最近发布了 众所周知 这个版本没有安装文件 当然 要在没有安装程序的情况下安装 Java 我将系统设置 PATH 和 JAVA HOME 设置为解压缩 Java 11 的文件夹的地址 根据对类似问题的已接受回复建议 唯一的事
  • 线程自动利用多个CPU核心?

    假设我的应用程序运行 2 个线程 例如渲染线程和游戏更新线程 如果它在具有多核 CPU 当今典型 的移动设备上运行 我是否可以期望线程在可能的情况下自动分配给不同的核心 我知道底层操作系统内核 Android linux内核 决定调度 我的
  • 解决错误:日志已在具有多个实例的atomikos中使用

    我仅在使用atomikos的实时服务器上遇到问题 在我的本地服务器上它工作得很好 我在服务器上面临的问题是 init 中出错 日志已在使用中 完整的异常堆栈跟踪 java lang RuntimeException Log already
  • Java8无符号算术

    据广泛报道 Java 8 具有对无符号整数的库支持 然而 似乎没有文章解释如何使用它以及有多少可能 有些函数 例如 Integer CompareUnsigned 很容易找到 并且似乎可以实现人们所期望的功能 但是 我什至无法编写一个简单的
  • 如何从表中检索特定列 --- JPA 或 CrudRepository?我只想从用户表中检索电子邮件列

    用户模型 Entity Table name user uniqueConstraints UniqueConstraint columnNames email public class User implements Serializab
  • jdbc4.MySQLSyntaxErrorException:数据库中不存在表

    我正在使用 SpringBoot 开发一个网络应用程序 这是我的application properties文件来指定访问数据库的凭据 spring datasource driverClassName com mysql jdbc Dri
  • OnClick 事件中的 finish() 如何工作?

    我有一个Activity一键退出Activity 通过layout xml我必须设置OnClick事件至cmd exit调用 this finish 效果很好 public void cmd exit View editLayout thi
  • 请求位置更新参数

    这就是 requestLocationUpdates 的样子 我使用它的方式 requestLocationUpdates String provider long minTime float minDistance LocationLis
  • 反思 Groovy 脚本中声明的函数

    有没有一种方法可以获取 Groovy 脚本中声明的函数的反射数据 该脚本已通过GroovyShell目的 具体来说 我想枚举脚本中的函数并访问附加到它们的注释 Put this到 Groovy 脚本的最后一行 它将作为脚本的返回值 a la
  • Tomcat 6找不到mysql驱动

    这里有一个类似的问题 但关于类路径 ClassNotFoundException com mysql jdbc Driver https stackoverflow com questions 1585811 classnotfoundex
  • 使用 SAX 进行 XML 解析 |如何处理特殊字符?

    我们有一个 JAVA 应用程序 可以从 SAP 系统中提取数据 解析数据并呈现给用户 使用 SAP JCo 连接器提取数据 最近我们抛出了一个异常 org xml sax SAXParseException 字符引用 是无效的 XML 字符
  • 如何在 Maven 中显示消息

    如何在 Maven 中显示消息 在ant中 我们确实有 echo 来显示消息 但是在maven中 我该怎么做呢 您可以使用 antrun 插件
  • 查看Jasper报告执行的SQL

    运行 Jasper 报表 其中 SQL 嵌入到报表文件 jrxml 中 时 是否可以看到执行的 SQL 理想情况下 我还想查看替换每个 P 占位符的值 Cheers Don JasperReports 使用 Jakarta Commons
  • android Accessibility-service 突然停止触发事件

    我有一个 AccessibilityService 工作正常 但由于开发过程中的某些原因它停止工作 我似乎找不到这个原因 请看一下我的代码并告诉我为什么它不起作用 public class MyServicee extends Access
  • 休眠以持久保存日期

    有没有办法告诉 Hibernate java util Date 应该持久保存 我需要这个来解决 MySQL 中缺少的毫秒分辨率问题 您能想到这种方法有什么缺点吗 您可以自己创建字段long 或者使用自定义的UserType 实施后User
  • java8 Collectors.toMap() 限制?

    我正在尝试使用java8Collectors toMap on a Stream of ZipEntry 这可能不是最好的想法 因为在处理过程中可能会发生异常 但我想这应该是可能的 我现在收到一个我不明白的编译错误 我猜是类型推理引擎 这是
  • Jackson 将单个项目反序列化到列表中

    我正在尝试使用一项服务 该服务为我提供了一个带有数组字段的实体 id 23233 items name item 1 name item 2 但是 当数组包含单个项目时 将返回该项目本身 而不是包含一个元素的数组 id 43567 item

随机推荐

  • 区块链草莽时代,致敬每一个实干者

    七月在野 八月在宇 九月在户 十月都在裁员 气温一点点降低 区块链行业也一步步临近冰点 区块链进入寒冬 留下的都是实干者 目前 区块链行业发展还存在各种各样的问题 比如人才储备不够 招不到人 法律的普及速度跟不上 技术应用落地还较为困难 而
  • Java程序员要掌握的前端:JavaScript篇

    第一篇HTML CSS在这里哦 Java程序员要掌握的前端 HTML CSS 第二章 Javascript 它是一种脚本语言 可以用来更改页面内容 控制多媒体 制作图像 动画等等 例子 修改页面内容 js 代码位置 引入 js 脚本 注意
  • 如何证明自己会python_如何判断python字符串出现次数?这几种方法你一定要学会...

    摘要 python应用阶段是实际的工作经验 现在如何判断python字符串出现次数 这几种方法你一定要学会 小编建议大家可以试着理解这些内容 也许对您的python学习有帮助 毕竟实践出真知 所以你要知道如何判断python字符串出现次数
  • 哲♂学三幻神带你学习ConstraintLayout(约束布局)

    哲 学三幻神带你学习ConstraintLayout 约束布局 标签 空格分隔 Tutorial Android ConstraintLayout 是什么 ConstraintLayout 约束布局 其实已经不算什么新东西了 很多同学应该知
  • 解决Gitlab不能向master分支push、merge的问题

    今天在向gitlab push代码的时候出现 remote rejected master gt master pre receive hook declined 这是因为gitlab默认开启了分支保护 以保护master分支不被其他用户随
  • 二、C语言初阶:函数

    2 函数 2 1 函数原型 函数原型通常放在头文件里面或者调用它的函数的前面 include
  • 从零开始学习Blazor

    什么是Blazor Blazor的优势和用途 简介 Blazor是一种基于WebAssembly的开源框架 它允许开发人员使用C 语言编写Web应用程序 Blazor可以在浏览器中运行C 代码 而无需使用JavaScript 该框架由微软开
  • 前端js循环修改对象属性,得到新数组数据属性是最后一个数(深拷贝与浅拷贝)

    项目场景 在前端项目的开发中 我将JSON格式的对象数组 例如 a 1 b 2 引入js文件 在js文件中通过循环修改对象数组中的数据 然后把新的数据push添加到新建数组中 这一过程涉及深拷贝与浅拷贝的问题 困扰了一下午 总算是解决了 特
  • HDMI PL驱动开发记录

    在Zynq UltraScale example的基础上 舍弃掉在PS端做驱动的方案 在PS端搭建工程 并利用串口进行配置 对于多路HDMI的不同输入输出控制起来较为方便 且利用PL来做驱动 结构更加清晰 在做HDMI驱动的时候要了解两个重
  • 回归模型的变量筛选与预测

    我眼中的回归变量筛选 变量筛选是回归建模过程关键的一步 由于变量间的相关性 必然会导致不同的筛选方法得到不同的模型 在所有变量筛选方法中 向前法 向后法以及逐步回归法的使用频率较高 因为这类方法操作简单 运算速度快 非常实用 这种方法选出的
  • java获取url锚点_定位与锚点

    文档流 普通流 上至下 左至右 浮动流 元素添加了float 定位流 添加了定位属性 定位 position static 默认值 absolute 绝对定位 脱离文档流 不占位 默认参考浏览器零点 relative 相对定位 占据文档流
  • c# 得到list符合某条件的索引值,排序

    请教 在List集合中怎么得到元素的索引值 参考 http www myexception cn c sharp 385022 html 这个可以用来读取窗口的多个textbox控件中内容 1 解决方案 2 你可以使用FindIndex方法
  • Centos8 部署 Mysql8.0及主从复制

    1 拉取镜像 root VM 24 9 centos docker pull mysql 如需拉取置指定版本镜像加上 版本号即可 root VM 24 9 centos docker pull mysql 7 6 1 创建本地文件用于挂载
  • C++ 异常处理

    C 异常 Exception 是指在程序运行时产生的特殊情况 例如 尝试除以零的操作 异常提供了一种转移程序控制权的方式 异常处理涉及到三个关键字 try catch throw throw 当问题出现时 程序会抛出一个异常 这是通过使用
  • VUE 定时请求接口数据

    1 设置定时器 mounted this timer setInterval function 执行内容 60000 2 清除定时器 beforeDestroy clearInterval this timer
  • Android平台深度学习--NNAPI

    转自 http blog sina com cn s blog 602f87700102y62v html 1 Android 8 1 API 27 NNAPI 人工智能神经网络API 如 TensorFlow 神经网络 API 能够向设备
  • Java的动态代理Proxy

    动态代理类是实现在运行时指定的接口列表的类 这样通过类实例上的一个接口的方法调用将被编码并通过统一接口分派到另一个对象 先问一个问题 数据库操作需要以下流程 获取数据库连接 gt 执行sql gt 提交事务 gt 异常回滚事务 gt 释放连
  • Java 异常之 RuntimeException和Exception的区别

    在java的异常类体系中 Error和RuntimeException是非检查型异常 其他的都是检查型异常 所有方法都可以在不声明throws的情况下抛出RuntimeException及其子类 不可以在不声明的情况下抛出非RuntimeE
  • C语言指针的赋值和初始化的看法

    https blog csdn net liuchunjie11 article details 80333224 在查看上述博客时 修改代码 include
  • Spring——事务注解@Transactional【建议收藏】

    在某些业务场景下 如果一个请求中 需要同时写入多张表的数据或者执行多条sql 为了保证操作的原子性 要么同时成功 要么同时失败 避免数据不一致的情况 我们一般都会用到事务 Spring框架下 我们经常会使用 Transactional注解来