一、简介

1、介绍

事务,就是一组操作数据库的动作集合。事务是现代数据库理论中的核心概念之一。如果一组处理步骤或者全部发生或者一步也不执行,我们称该组处理步骤为一个事务。当所有的步骤像一个操作一样被完整地执行,我们称该事务被提交。由于其中的一部分或多步执行失败,导致没有步骤被提交,则事务必须回滚到最初的系统状态。

2、事务特点

  • 原子性(Atomicity):整个事务是一个整体,不可分割的最小工作单位。一个事务中的所有操作要么全部执行成功,要么全部都不执行。其中任何一条语句执行失败,都会导致事务回滚

  • 一致性(Consistency)数据库的记录总是从一个一致性状态转变成另一个一致性状态。这里的一致性是语义上的一致性, 并不是语法上的一致性

  • 隔离性(Isolation):一个事物的执行,不受其他事务的干扰,即并发执行的事物之间互不干扰

  • 持久性(Durability):数据一旦提交,结果就是永久性的。并不应为宕机等情况丢失。一般理解就是写入硬盘保存成功

3、事务实现方式

3.1 MySql事务实现方式

  • 原子性和持久性利用redo log(重做日志) 实现

  • 一致性利用undo log(回滚日志)实现

  • 隔离性利用锁来实现

3.2 SpringBoot实现机制

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

  • 编程式事务管理: 编程式事务管理使用 TransactionTemplate 或者直接使用底层的 PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate

  • 声明式事务管理: 建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务

  • 声明式事务管理不需要入侵代码,更快捷而且简单,推荐使用

声明式事务有两种方式:

  • 一种是在配置文件(xml)中做相关的事务规则声明

  • 另一种是基于 @Transactional 注解的方式。注释配置是目前流行的使用方式,推荐使用

在应用系统调用声明了 @Transactional 的目标方法时,Spring Framework** 默认使用 AOP 代理**,在代码运行时生成一个代理对象,根据 @Transactional 的属性配置信息,这个代理对象决定该声明 @Transactional 的目标方法是否由拦截器 TransactionInterceptor来使用拦截,在 TransactionInterceptor拦截时,会在目标方法开始执行之前创建并加入事务,并执行目标方法的逻辑,最后根据执行情况是否出现异常,利用抽象事务管理器 AbstractPlatformTransactionManager 操作数据源 DataSource 提交或回滚事务

Spring AOP 代理有 CglibAopProxyJdkDynamicAopProxy两种,以 CglibAopProxy 为例,对于 CglibAopProxy,需要调用其内部类的 DynamicAdvisedInterceptorintercept方法。对于 JdkDynamicAopProxy,需要调用其 invoke方法

二、@Transactional详解

1、@Transactional常用配置

参 数 名 称功 能 描 述
readOnly该属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。例如:@Transactional(readOnly=true)
rollbackForrollbackFor 该属性用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。例如:指定单一异常类:@Transactional(rollbackFor=RuntimeException.class)指定多个异常类:@Transactional(rollbackFor={RuntimeException.class,Exception.class})
rollbackForClassName该属性用于设置需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,则进行事务回滚。例如:指定单一异常类名称@Transactional(rollbackForClassName="RuntimeException")指定多个异常类名称:@Transactional(rollbackForClassName={"RuntimeException","Exception"})
noRollbackFor该属性用于设置不需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,不进行事务回滚。例如:指定单一异常类:@Transactional(noRollbackFor=RuntimeException.class)指定多个异常类:@Transactional(noRollbackFor={RuntimeException.class, Exception.class})
noRollbackForClassName该属性用于设置不需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,不进行事务回滚。例如:指定单一异常类名称:@Transactional(noRollbackForClassName="RuntimeException")指定多个异常类名称:@Transactional(noRollbackForClassName={"RuntimeException", "Exception"})
propagation该属性用于设置事务的传播行为。例如:@Transactional(propagation=Propagation.NOT_SUPPORTED, readOnly=true)
isolation该属性用于设置底层数据库的事务隔离级别,事务隔离级别用于处理多事务并发的情况,通常使用数据库的默认隔离级别即可,基本不需要进行设置
timeout该属性用于设置事务的超时秒数,默认值为-1表示永不超时 事物超时设置:@Transactional(timeout=30) ,设置为30秒

2、事务传播行为

Spring在TransactionDefinition接口中规定了7种类型的事务传播行为。Propagation枚举则引用了这些类型,开发过程中我们一般直接用Propagation枚举。例如@Transactional(propagation=Propagation.NOT_SUPPORTED,readOnly=true),常用的三项已经加粗

事务传播行为类型说明
PROPAGATION_REQUIRED需要事务(默认)。若当前无事务,新建一个事务;若当前有事务,加入此事务中
PROPAGATION_SUPPORTS支持事务。若当前没有事务以非事务方式执行;若当前有事务,加入此事务中
PROPAGATION_MANDATORY强制使用事务。若当前有事务,就使用当前事务;若当前没有事务,抛出IllegalTransactionStateException异常
PROPAGATION_REQUIRES_NEW新建事务。无论当前是否有事务,都新建事务运行
PROPAGATION_NOT_SUPPORTED不支持事务。若当前存在事务,把当前事务挂起,然后运行方法
PROPAGATION_NEVER不使用事务。若当前方法存在事务,则抛出IllegalTransactionStateException异常,否则继续使用无事务机制运行
PROPAGATION_NESTED嵌套。如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作

3、事务5种隔离级别

例如:@Transactional(isolation = Isolation.READ_COMMITTED)

隔离级别含义
DEFAULT这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别另外四个与JDBC的隔离级别相对应
READ_UNCOMMITTED最低的隔离级别。事实上我们不应该称其为隔离级别,因为在事务完成前,其他事务可以看到该事务所修改的数据。而在其他事务提交前,该事务也可以看到其他事务所做的修改。可能导致脏,幻,不可重复读
READ_COMMITTED大多数数据库的默认级别。在事务完成前,其他事务无法看到该事务所修改的数据。遗憾的是,在该事务提交后,你就可以查看其他事务插入或更新的数据。这意味着在事务的不同点上,如果其他事务修改了数据,你就会看到不同的数据。可防止脏读,但幻读和不可重复读仍可以发生
REPEATABLE_READISOLATION_READ_COMMITTED更严格,该隔离级别确保如果在事务中查询了某个数据集,你至少还能再次查询到相同的数据集,即使其他事务修改了所查询的数据。然而如果其他事务插入了新数据,你就可以查询到该新插入的数据。可防止脏读,不可重复读,但幻读仍可能发生
SERIALIZABLE完全服从ACID的隔离级别,确保不发生脏读、不可重复读和幻影读。这在所有隔离级别中也是最慢的,因为它通常是通过完全锁定当前事务所涉及的数据表来完成的。代价最大、可靠性最高的隔离级别,所有的事务都是按顺序一个接一个地执行。避免所有不安全读取

三、事务使用事项与场景

1、事务使用注意事项

  • 在具体的类(或类的方法)上使用 @Transactional 注解,而不要使用在类所要实现的任何接口上

  • @Transactional 注解应该只被应用在 public 修饰的方法上(注意)。 如果在 protected、private 或者 package-visible 的方法上使用 该注解,它也不会报错(IDEA会有提示), 但事务并没有生效

  • @Transactional是基于动态代理的(注意),需要一个类调用另一个类,类内调用会失效

  • 被外部调用的公共方法A有两个进行了数据操作的子方法B和子方法C的事务注解说明:

    • 被外部调用的公共方法A声明事务@Transactional,无论子方法B和C是不是本类的方法,无论子方法B和C是否声明事务,事务均由公共方法A控制

    • 被外部调用的公共方法A未声明事务@Transactional,子方法B和C若是其他类的方法且各自声明事务:事务由子方法B和C各自控制

    • 被外部调用的公共方法A未声明事务@Transactional,子方法B和C若是本类的方法,则即使子方法B和C各自声明事务,事务也不会生效,并且会报错(没有可用的transactional)

    • 被外部调用的公共方法A声明事务@Transactional,子方法运行异常,但运行异常被子方法自己 try-catch 处理了,则事务回滚是不会生效的!

      如果想要事务回滚生效,需要将子方法的事务控制交给调用的方法来处理:

      • 方案1:子方法中不用 try-catch 处理运行异常

      • 方案2:子方法的catch里面将运行异常抛出throw new RuntimeException();

  • 默认情况下,Spring会对unchecked异常进行事务回滚,也就是默认对 RuntimeException() 异常或是其子类进行事务回滚;如果是checked异常则不回滚,例如空指针异常、算数异常等会被回滚;文件读写、网络问题Spring就没法回滚。若想对所有异常(包括自定义异常)都起作用,注解上面需配置异常类型:@Transactional(rollbackFor = Exception.class)

  • 数据库要支持事务,如果是mysql,要使用innodb引擎,myisam不支持事务

  • 事务@Transactional由spring控制时,它会在抛出异常的时候进行回滚。如果自己使用try-catch捕获处理了,是不生效的。如果想事务生效可以进行手动回滚或者在catch里面将异常抛出throw new RuntimeException();有两种方法

    • 方案一:手动抛出运行时异常(缺陷是不能在catch代码块自定义返回值)

      try{
            ....  
        }catch(Exception e){
            logger.error("fail",e);
            throw new RuntimeException;
      }
      
      
    • 方案二:手动进行回滚 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();

      try{
            ...
        }catch(Exception e){
            log.error("fail",e);
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
            return false;
      }
      

2、事务使用场景

2.1 自动回滚

直接抛出,不try/catch

@Override
@Transactional(rollbackFor = Exception.class)
public Object submitOrder() throws Exception {  
     success();  
     //假如exception这个操作数据库的方法会抛出异常,方法success()对数据库的操作会回滚
     exception(); 
     return ApiReturnUtil.success();
}

2.2 手动回滚

进行try/catch,回滚并抛出

@Override
@Transactional(rollbackFor = Exception.class)
public Object submitOrder (){  
    success();  
    try {  
        exception(); 
     } catch (Exception e) {  
        e.printStackTrace();     
        //手工回滚异常
        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        return ApiReturnUtil.error();
     }  
    return ApiReturnUtil.success();
}

2.3 回滚部分异常

@Override
@Transactional(rollbackFor = Exception.class)
public Object submitOrder (){  
    success();  
    //只回滚以下异常,设置回滚点
    Object savePoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
    try {  
        exception(); 
     } catch (Exception e) {  
        e.printStackTrace();     
        //手工回滚异常,回滚到savePoint
        TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savePoint);
        return ApiReturnUtil.error();
     }  
    return ApiReturnUtil.success();
}

2.4 手动创建、提交、回滚事务

PlatformTransactionManager 这个接口中定义了三个方法 getTransaction创建事务,commit 提交事务,rollback 回滚事务。它的实现类是 AbstractPlatformTransactionManager

@Autowired
DataSourceTransactionManager dataSourceTransactionManager;
@Autowired
TransactionDefinition transactionDefinition;
 
// 手动创建事务
TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
 
// 手动提交事务
dataSourceTransactionManager.commit(transactionStatus);
 
// 手动回滚事务。(最好是放在catch 里面,防止程序异常而事务一直卡在哪里未提交)
dataSourceTransactionManager.rollback(transactionStatus);

3、事务其他情况

3.1 事务提交方式

默认情况下,数据库处于自动提交模式。每一条语句处于一个单独的事务中,在这条语句执行完毕时,如果执行成功则隐式的提交事务,如果执行失败则隐式的回滚事务。

对于正常的事务管理,是一组相关的操作处于一个事务之中,因此必须关闭数据库的自动提交模式。不过,这个我们不用担心,spring 会将底层连接的自动提交特性设置为 false 。也就是在使用 spring 进行事物管理的时候,spring 会将是否自动提交设置为false,等价于JDBC中的 connection.setAutoCommit(false); ,在执行完之后在进行提交 connection.commit();

spring事务管理器回滚一个事务的推荐方法是在当前事务的上下文内抛出异常。spring事务管理器会捕捉任何未处理的异常,然后依据规则决定是否回滚抛出异常的事务。

3.2 事务并发经典情况

  • 第一类丢失更新

    没有事务隔离的情况下,两个事务都同时更新一行数据,但是第二个事务却中途失败退出, 导致对数据的两个修改都失效了

  • 脏读

    脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。

  • 不可重复读

    在一个事务内,多次读同一数据。在这个事务还没有结束时,另外一个事务也访问该同一数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读

  • 第二类丢失更新

    不可重复读的特例,有两个并发事务同时读取同一行数据,然后其中一个对它进行修改提交,而另一个也进行了修改提交。这就会造成第一次写操作失效。

  • 幻读

    当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样

四、使用事务的一些错误

1、案例一

1.1 问题描述

两个使用Transaction注解的Service,A和B,在A中引入了B的方法用于更新数据 ,当A中捕捉到B中有异常时,回滚动作正常执行,但是当return时则出现org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only异常

@Transactional
public class ServiceA {
  @Autowired
  private ServiceB serviceB;

  public void methodA() {
    try{
      serviceB.methodB();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}

@Transactional
public class serviceB {
  public void methodB() {
    throw new RuntimeException();
  }
}

1.2 问题原因

@Transactional(propagation= Propagation.REQUIRED):如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是@Transactional的默认方式。在这种情况下,外层事务(ServiceA)和内层事务(ServiceB)就是一个事务,任何一个出现异常,都会在methodA执行完毕后回滚。

如果内层事务B抛出异常e(没有catch,继续向外层抛出),在内层事务结束时,spring会把事务B标记为"rollback-only";这时外层事务A发现了异常e,如果外层事务A catch了异常并处理掉,那么外层事务A的方法会继续执行代码,直到外层事务也结束时,这时外层事务A想commit,因为正常结束没有向外抛异常,但是内外层事务AB是同一个事务,事务B(同时也是事务A)已经被内层方法标记为"rollback-only",需要回滚,无法commit,这时spring就会抛出org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only,意思是"事务已经被标记为回滚,无法提交"

1.3 解决方案

  • 直接在外层事务的catch代码块中抛出捕获的内层事务的异常,两层事务有未捕获异常,都回滚(有时候这个异常就是交给外层处理的,抛出到更外层显得多此一举);

  • 在内层事务中做异常捕获处理,并且不向外抛异常,两层事务都不回滚

  • 最好的方式:如果希望内层事务回滚,但不影响外层事务提交,需要将内层事务的传播方式指定为@Transactional(propagation= Propagation.NESTED),外层事务的提交和回滚能够控制嵌套的内层事务回滚;而内层事务报错时,只回滚内层事务,外层事务可以继续提交。(JPA不支持NESTED,有时可以用REQUIRES_NEW替代一下)。

    详细说明参考:https://www.jianshu.com/p/8beab9f37e5b

  • 如果这个异常发生时,内层需要事务回滚的代码还没有执行,则可以@Transactional(noRollbackFor = {内层抛出的异常}.class),指定内层也不为这个异常回滚。


参考文章:

SpringBoot2异常处理回滚事务详解

事务传播机制–应用/实例/详解

mysql事务详解

spring 事务传播行为之嵌套事务NESTED细节

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐