事务Transactional

2023年 9月 27日 22.6k 0

一. 什么是事务

简单的说事务是逻辑上的一组操作,要么都执行,要么都不执行;

在我们的日常开发中,我们系统的每个业务方法可能包括了多个原子性的数据库操作,例如下面的saveUserInfo(),就包含多个原子性的数据库操作,这些原子性的数据库操作,要么都执行,要么都不执行。

public void saveUserInfo() {
  userInfoDao.save(userBase);
  10 / 0;
  userInfoDetail.save(userInfoDetail);
}

但是,我们需要注意的是:事务能否生效的关键是数据库引擎是否支持。比如我们常用的MySQL数据库,默认使用支持事务的innodb引擎,如果我们使用myisam引擎,那么就不再支持事务了。

事务举例:

比如张三要给李四转一万块钱,这个转账会涉及两个关键操作

1.张三的银行账户余额减少10000

2.李四的银行账户余额增加10000

如果这两个操作之间突然出现了错误,比如银行系统崩溃、网络故障、服务宕机;导致张三余额减少,而李四的余额没有增加,那么这样就不对了。事务就是要保证这两个关键操作要么都成功,要么都失败。

public class TransferAccountsServiceImpl{
  @Resource
  private ITransferAccountsDao transferAccountsDao;
  
  @Transactional(propagation = Propagation.REQUIRED)
  public void transferAccount() {
    transferAccountsDao.addMonney(10000, "lisi");
    10 / 0;
    transferAccountsDao.reduceMonney(10000, "zhangsan");
  }
}

二. 事务的特点ACID

  • 原子性(Atomicity):事务最小的执行单位,不允许分割,事务的原子性确保动作要么全部完成,要么完全失败。
  • 一致性(Consistency):执行事务前后,数据保持一致,例如在上面的转账例子中,无论事务是否成功,转账者和收款人的总额应该是不变的。
  • 隔离性(Isolation):并发访问数据库时,一个用户的事务不被其它事务干扰,各并发事务之间的数据库是独立的。
  • 持久性(Durability):一个事务被提交后,它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
  • 三. Spring对事务的支持

    程序是否支持事务的先决条件是数据库,比如使用MySQL的话,如果选择的是innodb,那么支持事务,如果选择的是myisam,那么从根上就不支持事务了

    思考1:MySQL怎么保证原子性?

    如果要保证原子性,就需要在发生异常时,对已经执行的操作进行回滚,在MySQL中,恢复机制是通过回滚日志实现的,所有事务进行的修改,都会先记录到这个回滚日志中,然后再执行相关的操作。

    如果在执行过程中遇到异常,我们直接利用回滚日志中的信息将数据回滚到修改之前的样子即可,并且回滚日志会先将数据持久化到磁盘上,这样就可以保证即便在遇到数据库突然宕机,当用户再次重启数据库时,数据库还是能够通过查回滚日志来回滚之前未完成的事务。

    1. Spring支持两种事务管理

    编程事务管理

    通过TransactionTemplate或者TransactionManager手动管理事务,在实际应用中却很少使用,下面通过代码来演示,使用TransactionTemplate进行编程式事务管理

    @Autowired
    private TransactionTemplate transactionTemplate;
    ​
    public void testTransactionTemplate() {
      transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        protected void doInTransactionWithoutResult(final TransactionStatus transactionStatus) {
          try {
            //TODO 业务代码
          } catch (final Exception e) {
            // 异常时回滚
            transactionStatus.setRollbackOnly();
          }
        }
      });
    }
    

    使用TransactionManager进行编程式事务管理

    @Resource
    private PlatformTransactionManager transactionManager;
    ​
    public void testTransactionManager() {
      final TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
      try {
        //TODO 业务代码
        transactionManager.commit(status);
      } catch (final Exception e) {
        // 异常时回滚
        transactionManager.rollback(status);
      }
    }
    

    声明式事务管理

    声明式事务管理,实际上是通过AOP实现,基于@Transactional的注解使用最多

    使用@Transactional注解进行事务管理

    @Transactional
    public void testTransactional() {
      userInfoDao.save(userInfo);
      userInfoDetailDao.save(userInfoDetail);
    }
    

    2. Spring事务管理接口介绍

    Spring框架中,事务管理最重要的3个接口:

  • PlatformTransactionManager:平台事务管理器,Spring事务策略核心。
  • TransactionDefinition:事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)。
  • TransactionStatus:事务运行状态。
  • 可以将PlatformTransactionManager接口看作是事务上层管理者,而TransactionDefinitionTransactionStatus这两个接口可以看作是事务的描述。

    PlatformTransactionManager会根据TransactionDefinition定义的事务超时时间、隔离级别、传播行为等来进行事务管理,TransactionStatus接口则提供一些方法来获取事务相应的状态(比如是否是新事务、是否可以回滚等)。

    事务管理器:PlatformTransactionManager

    PlatformTransactionManager:通过这个接口,Spring为各个数据库持久层框架,例如JDBC(DataSourceTransactionManager)Hibernate(HibernateTransactionManager)JPA(JpaTransactionManager)等提供了对应的事务管理器,具体如何实现,就根据各个框架的特性实际出发。

    SpringBootPlatformTransactionManager接口定义如下:

    package org.springframework.transaction;
    ​
    import org.springframework.lang.Nullable;
    ​
    public interface PlatformTransactionManager extends TransactionManager {
        TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
    ​
        void commit(TransactionStatus status) throws TransactionException;
    ​
        void rollback(TransactionStatus status) throws TransactionException;
    }
    

    SpringPlatformTransactionManager接口定义如下:

    package org.springframework.transaction;
    ​
    public interface PlatformTransactionManager {
        TransactionStatus getTransaction(TransactionDefinition var1) throws TransactionException;
    ​
        void commit(TransactionStatus var1) throws TransactionException;
    ​
        void rollback(TransactionStatus var1) throws TransactionException;
    }
    

    思考2:为什么要将PlatformTransactionManager定义成接口呢?

    将事务管理行为抽象出来,然后不同的平台去实现这个接口,这样我们可以保证提供给外部的行为不变,方便扩展。

    事务属性:TransactionDefinition

    事务管理器接口PlatformTransactionManager通过getTransaction(TransactionDefinition var1)方法来得到事务,这个方法的参数是TransactionDefinition类型,这个类定义了一些基本的事务属性

    事务属性包含了以下5个方面:

    • 隔离级别
    • 传播行为
    • 回滚规则
    • 是否只读
    • 事务超时

    TransactionDefinition接口定义如下:

    package org.springframework.transaction;
    ​
    public interface TransactionDefinition {
        int PROPAGATION_REQUIRED = 0;
        int PROPAGATION_SUPPORTS = 1;
        int PROPAGATION_MANDATORY = 2;
        int PROPAGATION_REQUIRES_NEW = 3;
        int PROPAGATION_NOT_SUPPORTED = 4;
        int PROPAGATION_NEVER = 5;
        int PROPAGATION_NESTED = 6;
        int ISOLATION_DEFAULT = -1;
        int ISOLATION_READ_UNCOMMITTED = 1;
        int ISOLATION_READ_COMMITTED = 2;
        int ISOLATION_REPEATABLE_READ = 4;
        int ISOLATION_SERIALIZABLE = 8;
        int TIMEOUT_DEFAULT = -1;
    ​
        int getPropagationBehavior();
    ​
        int getIsolationLevel();
    ​
        int getTimeout();
    ​
        boolean isReadOnly();
    ​
        String getName();
    }
    

    事务状态:TransactionStatus

    TransactionStatus接口用来记录事务的状态,这个接口定义了一组方法,用来获取或判断事务相应的状态信息,PlatformTransactionManager.getTransaction(TransactionDefinition var1)方法返回一个TransactionStatus对象。

    TransactionStatus接口定义如下:

    package org.springframework.transaction;
    
    import java.io.Flushable;
    
    public interface TransactionStatus extends SavepointManager, Flushable {
        boolean isNewTransaction();
    
        boolean hasSavepoint();
    
        void setRollbackOnly();
    
        boolean isRollbackOnly();
    
        void flush();
    
        boolean isCompleted();
    }
    

    3. 事务注解详解

    在实际的业务开发中,大家一般使用@Transactional注解来开启事务,但很多人并不是很清楚这个注解中的参数是什么意思?有什么用?下面我将通过一些样例代码来介绍这个注解。

    事务传播行为是为了解决业务层方法之间互相调用的事务问题

    当事务方法被另一个事务方法调用时,必须指定事务应该如何传播:方法可能继续在现有的事务中运行,也有可能开启一个新事务,并在自己的事务中运行

    例如:

    DemoService1类中的service1()方法中调用了DemoService2类中的service2()方法。这就涉及到了业务层方法之间互相调用的事务问题了,如果在service2方法中出现了异常需要回滚,如何配置事务传播行为才能让service1也跟着回滚呢?

    @Service
    public class DemoService1 {
      @Resource
      private DemoService2 demoService2;
      
      public void service1() {
        demoService2.service2();
        // 
      }
    }
    
    public class DemoService2 {
      @Transactional(propagation = Propagation.XXX)
      public void service2() {
        //TOOD
        
      }
    }
    

    TransactionDefinition中,定义了7个表示传播行为的常量

    public interface TransactionDefinition {
        int PROPAGATION_REQUIRED = 0;
        int PROPAGATION_SUPPORTS = 1;
        int PROPAGATION_MANDATORY = 2;
        int PROPAGATION_REQUIRES_NEW = 3;
        int PROPAGATION_NOT_SUPPORTED = 4;
        int PROPAGATION_NEVER = 5;
        int PROPAGATION_NESTED = 6;
    }
    

    为了方便使用,Spring又定义了一个枚举类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;
        }
    }
    

    事务传播行为可能的值

  • TransactionDefinition.PROPAGATION_REQUIRED 使用最多的一个事务传播行为,平常我们使用的@Transactional注解默认使用的就是这个事务传播行为。
  • 如果外部方法没有开启事务,Propagation.REQUIRED修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。如果外部方法开启了事务,且使用了Propagation.REQUIRED修饰,所有Propagation.REQUIRED修饰的内部方法和外部方法均属于同一个事务,只要一个方法回滚,整个事务就回滚。

    例如:

    上面的service1()service2()方法都使用PROPAGATION_REQUIRED传播事务行为,两者使用的是同一个事务,只要其中的一个方法回滚,整个事务就回滚

    @Service
    public class DemoService1 {
      @Resource
      private DemoService2 demoService2;
      
      @Transactional(propagation = Propagation.REQUIRED)
      public void service1() {
        
        demoService2.service2();
        
      }
    }
    
    public class DemoService2 {
      @Transactional(propagation = Propagation.REQUIRED)
      public void service2() {
        //TOOD
      }
    }
    
  • TransactionDefinition.PROPAGATION_REQUIRES_NEW 创建一个新事务,如果当前存在事务,把当前挂起,也就是说无论外部方法是否开启事务,Propagation.REQUIRES_NEW修饰的内部方法会新开启自己的事务,且事务相互独立,互不打扰。
  • 例如:上面的service2()使用PROPAGATION_REQUIRES_NEW事务传播行为修饰,service1()还是使用PROPAGATION_REQUIRED修饰,如果service1()发生异常回滚,service2()不会跟着回滚,因为service2()开启了独立的事务。但是service2()抛出了未捕获的异常且该异常满足事务回滚规则,service2()也同样会回滚,因为这个异常被service1()是事务管理机制检测到了。

    @Service
    public class DemoService1 {
      @Resource
      private DemoService2 demoService2;
      
      @Transactional(propagation = Propagation.REQUIRED)
      public void service1() {
        demoService2.service2();
        10 / 0
      }
    }
    
    public class DemoService2 {
      @Transactional(propagation = Propagation.REQUIRES_NEW)
      public void service2() {
        //TOOD
      }
    }
    
  • TransactionDefinition.PROPAGATION_NESTED 如果当前存在事务,就在嵌套事务内执行,如果当前没有事务,就执行与TransactionDefinition.PROPAGATION_REQUIRED类似的操作。
  • 在外部方法开启事务的情况下,在内部开启一个新的事务,作为嵌套事务存在;如果外部方法没有开启事务,则单独开启一个事务,与PROPAGATION_REQUIRED类似。

    例如:如果service2()回滚,service1()不会回滚,如果service1()回滚,service2()也回滚

    @Service
    public class DemoService1 {
      @Resource
      private DemoService2 demoService2;
      
      @Transactional(propagation = Propagation.REQUIRED)
      public void service1() {
        demoService2.service2();
      }
    }
    
    public class DemoService2 {
      @Transactional(propagation = Propagation.NESTED)
      public void service2() {
        //TOOD
      }
    }
    
  • TransactionDefinition.PROPAGATION_MANDATORY 如果当前存在事务,则加入该事务,如果当前没有事务,则抛出异常。
  • TransactionDefinition.PROPAGATION_SUPPORTS 如果当前存在事务,则加入该事务,如果当前没有事务,则以非事务方式继续运行。
  • TransactionDefinition.PROPAGATION_NOT_SUPPORTED 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
  • TransactionDefinition.PROPAGATION_NEVER 以非事务方式运行,如果当前存在事务,则抛出异常。
  • 事务隔离级别

    TransactionDefinition接口中定义了5个表示隔离级别的常量

    package org.springframework.transaction;
    
    public interface TransactionDefinition {
    
      int ISOLATION_DEFAULT = -1;
      int ISOLATION_READ_UNCOMMITTED = 1;
      int ISOLATION_READ_COMMITTED = 2;
      int ISOLATION_REPEATABLE_READ = 4;
      int ISOLATION_SERIALIZABLE = 8;
      
    }
    

    和事务传播行为一样,Spring又定义了一个枚举类Isolation

    package org.springframework.transaction.annotation;
    
    public enum Isolation {
      DEFAULT(-1),
      READ_UNCOMMITTED(1),
      READ_COMMITTED(2),
      REPEATABLE_READ(4),
      SERIALIZABLE(8);
    
      private final int value;
    
      private Isolation(int value) {
        this.value = value;
      }
    
      public int value() {
        return this.value;
      }
    }
    

    事务隔离级别介绍:

    隔离级别 描述
    TransactionDefinition.ISOLATION_DEFAULT 使用数据库默认的隔离级别,MySQL默认采用REPEATABLE_READOracle默认采用READ_COMMITTED
    TransactionDefinition.ISOLATION_READ_UNCOMMITTED 最低隔离级别,这个隔离级别使用比较少,因为它允许读取尚未提交的数据变更,可能会导致脏读、幻读、不可重复读
    TransactionDefinition.ISOLATION_READ_COMMITTED 允许读取并发事务已经提交的数据,可以有效阻止脏读,但幻读、不可重复读仍然有可能发生
    TransactionDefinition.ISOLATION_REPEATABLE_READ 对同一个字段多次读取结果都是一致的,除非数据被本身事务修改,可以阻止脏读、不可重复读,但有可能发生幻读
    TransactionDefinition.ISOLATION_SERIALIZABLE 最高的隔离级别,完全遵从ACID的隔离级别,所有的事务逐个执行,保证事务之间互不干扰,可以有效防止脏读、幻读、不可重复读,这种隔离级别会影响程序性能,通常情况下也不会用到该级别

    事务超时属性

    所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒,默认值为-1,这表示事务的超时时间取决于底层事务系统或者没有超时时间。

    事务只读属性

    TransactionDefinition接口中定义了isReadOnly()

    package org.springframework.transaction;
    
    public interface TransactionDefinition {
    
      boolean isReadOnly();
    
    }
    

    对于只有读取数据查询的事务,可以指定事务类型为 readonly,即只读事务。只读事务不涉及数据的修改,数据库会提供一些优化手段,适用在有多条数据库查询操作的方法中。

    思考3:为什么数据查询操作还要启用事务支持呢?

    MySQL 默认对每一个新建立的连接都启用了autocommit模式。在该模式下,每一个发送到 MySQL 服务器的sql语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务,并开启一个新的事务。如果在方法加上了Transactional注解,这个方法执行的所有sql会被放在一个事务中。如果声明了只读事务的话,数据库就会去优化它的执行,并不会带来其他的效率问题。如果不加Transactional,每条sql会开启一个单独的事务,期间被其它事务改了数据,都会实时读取到最新值。

    事务回滚规则

    定义哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行期异常(RuntimeException 的子类)才会回滚,Error 也会导致事务回滚,但是,在遇到检查型(Checked)异常时不会回滚。

    如果想要回滚自定义的异常,代码可以如下这样写:

    @Transactional(rollbackFor= MyException.class)
    

    Transactional注解

    @Transactional 的作用范围
    • 方法:推荐将注解用于方法上,不过需要注意的是:该注解只能应用到 public 方法上,否则不生效。
    • 类:如果将注解用在类上,表明该类中所有的 public 方法都生效。
    • 接口:不推荐在接口上使用。
    @Transactional 的参数

    @Transactional注解源码如下,里面包含了基本事务属性的配置:

    package org.springframework.transaction.annotation;
    
    import java.lang.annotation.Documented;
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Inherited;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Inherited
    @Documented
    public @interface Transactional {
      String value() default "";
    
      Propagation propagation() default Propagation.REQUIRED;
    
      Isolation isolation() default Isolation.DEFAULT;
    
      int timeout() default -1;
    
      boolean readOnly() default false;
    
      Class targetClass = config.getTargetClass();
          if (targetClass == null) {
            throw new AopConfigException("TargetSource cannot determine target class: Either an interface or a target is required for proxy creation.");
          } else {
            return (AopProxy)(!targetClass.isInterface() && !Proxy.isProxyClass(targetClass) ? new ObjenesisCglibAopProxy(config) : new JdkDynamicAopProxy(config));
          }
        }
      }
    
      private boolean hasNoUserSuppliedProxyInterfaces(AdvisedSupport config) {
        Class[] ifcs = config.getProxiedInterfaces();
        return ifcs.length == 0 || ifcs.length == 1 && SpringProxy.class.isAssignableFrom(ifcs[0]);
      }
    }
    

    如果一个类或者一个类中的 public 方法上被@Transactional 注解修饰,Spring 容器就会在启动的时候为其创建一个代理类,在调用被@Transactional 注解修饰的 public 方法时,实际调用的是TransactionInterceptor 类中的 invoke()方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。

    Spring AOP 自调用问题

    当方法被@Transactional 注解修饰时,Spring 事务管理器只会在被其他类方法调用的时候生效,而不会在一个类中方法调用生效。

    这是由Spring AOP 工作原理决定的。Spring AOP 使用动态代理来实现事务的管理,它会在运行的时候为带有 @Transactional 注解的方法生成代理对象,并在方法调用的前后应用事物逻辑。如果该方法被其他类调用,代理对象就会拦截方法调用并处理事务。但是在同类中的其他方法内部调用的时候,我们代理对象就无法拦截到这个内部调用,因此事务也就失效了。

    DemoService3 类中的method1()调用method2()就会导致method2()的事务失效。解决办法就是避免同一类中自调用或者使用 AspectJ 取代 Spring AOP 代理

    样例:

    @Service
    public class DemoService3 {
    
    private void method1() {
         method2();
         //TODO
    }
    @Transactional
     public void method2() {
         //TODO
      }
    }
    

    在自调用的时候开启事务,这是因为使用了 AopContext.currentProxy() 方法来获取当前类的代理对象,然后通过代理对象调用 method2()。这样就相当于从外部调用了 method2(),因此事务注解会生效。

    但是我们一般也不会在代码中这么写

    样例(不推荐):

    @Service
    public class DemoService4 {
    
    private void method1() {
      // 先获取该类的代理对象,然后通过代理对象调用method2。
      ((MyService)AopContext.currentProxy()).method2(); 
      //TODO
    }
      
    @Transactional
    	public void method2() {
      	//TODO
     	}
    }
    

    四. 总结

    • @Transactional 注解只有作用在 public 修饰的方法上才生效,不推荐在接口上使用。
    • 避免同一个类中调用 @Transactional 注解的方法,这样会导致事务失效。
    • 正确的设置 @TransactionalrollbackForpropagation 属性,否则事务可能会回滚失败;
    • 使用 @Transactional 注解的方法所在的类必须被 Spring 管理,否则不生效;
    • 底层使用的数据库必须支持事务机制,否则不生效;

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论