Spring 事务

Spring 支持两种事务方式,分别是编程式事务和声明式事务,后者最常见,通常情况下只需要一个 @Transactional 就搞定了(代码侵入性降到了最低)。

一、Spring 支持事务类型

1.2 声明式事务

声明式事务将事务管理代码从业务方法中抽离了出来,以声明式的方式来实现事务管理,对于开发者来说,声明式事务显然比编程式事务更易用、更好用。
要想实现事务管理和业务代码的抽离,就必须得用到 Spring 当中最关键最核心的技术之一,AOP,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。
声明式事务虽然优于编程式事务,但也有不足,声明式事务管理的粒度是方法级别,而编程式事务是可以精确到代码块级别的。

声明式事务代码:

@Transactional
public void savePosts(PostsParam postsParam) {
	// 保存文章
	save(posts);
	// 处理标签
  insertOrUpdateTag(postsParam, posts);
}

1.2 编程式事务

编程式事务是指将事务管理代码嵌入嵌入到业务代码中,来控制事务的提交和回滚。
在编程式事务中,必须在每个业务操作中包含额外的事务管理代码,就导致代码看起来非常的臃肿,但对理解 Spring 的事务管理模型非常有帮助。
Spring 更推荐使用 TransactionTemplate

编程式事务代码:

@Autowired
private TransactionTemplate transactionTemplate;
public void testTransaction() {

    transactionTemplate.execute(new TransactionCallbackWithoutResult() {
        @Override
        protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
            try {

                // ....  业务代码
            } catch (Exception e){
                //回滚
                transactionStatus.setRollbackOnly();
            }
        }
    });

    // java 8 lambada
    transactionTemplate.execute(status->{
        
    });   
}

TransactionManager 管理事务:

@Autowired
private PlatformTransactionManager transactionManager;

public void testTransaction() {

  TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
          try {
               // ....  业务代码
              transactionManager.commit(status);
          } catch (Exception e) {
              transactionManager.rollback(status);
          }
}

二、事务模型

Spring 将事务管理的核心抽象为一个事务管理器(TransactionManager),它的源码只有一个简单的接口定义,属于一个标记接口:

public interface TransactionManager {

}

该接口有两个子接口,分别是编程式事务接口 ReactiveTransactionManager 和声明式事务接口 PlatformTransactionManager

PlatformTransactionManager,该接口定义了 3 个接口方法:

interface PlatformTransactionManager extends TransactionManager{
    // 根据事务定义获取事务状态
    TransactionStatus getTransaction(TransactionDefinition definition)
            throws TransactionException;

    // 提交事务
    void commit(TransactionStatus status) throws TransactionException;

    // 事务回滚
    void rollback(TransactionStatus status) throws TransactionException;
}

通过 PlatformTransactionManager 这个接口,Spring 为各个平台如 JDBC(DataSourceTransactionManager)、Hibernate(HibernateTransactionManager)、JPA(JpaTransactionManager)等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。

参数 TransactionDefinition@Transactional 注解是对应的,比如说 @Transactional 注解中定义的事务传播行为、隔离级别、事务超时时间、事务是否只读等属性,在 TransactionDefinition 都可以找得到。

返回类型 TransactionStatus 主要用来存储当前事务的一些状态和数据,比如说事务资源(connection)、回滚状态等。

TransactionDefinition.java:

public interface TransactionDefinition {

	// 事务的传播行为
	default int getPropagationBehavior() {
		return PROPAGATION_REQUIRED;
	}

	// 事务的隔离级别
	default int getIsolationLevel() {
		return ISOLATION_DEFAULT;
	}

  // 事务超时时间
  default int getTimeout() {
		return TIMEOUT_DEFAULT;
	}

  // 事务是否只读
  default boolean isReadOnly() {
		return false;
	}
}

Transactional.java

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

	Propagation propagation() default Propagation.REQUIRED;
	Isolation isolation() default Isolation.DEFAULT;
  int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
  boolean readOnly() default false;

}
  • @Transactional 注解中的 propagation 对应 TransactionDefinition 中的 getPropagationBehavior,默认值为 Propagation.REQUIRED(TransactionDefinition.PROPAGATION_REQUIRED)。
  • @Transactional 注解中的 isolation 对应 TransactionDefinition 中的 getIsolationLevel,默认值为 DEFAULT(TransactionDefinition.ISOLATION_DEFAULT)。
  • @Transactional 注解中的 timeout 对应 TransactionDefinition 中的 getTimeout,默认值为TransactionDefinition.TIMEOUT_DEFAULT。
  • @Transactional 注解中的 readOnly 对应 TransactionDefinition 中的 isReadOnly,默认值为 false。

2.1 事务传播行为

当事务方法被另外一个事务方法调用时,必须指定事务应该如何传播,例如,方法可能继续在当前事务中执行,也可以开启一个新的事务,在自己的事务中执行。

声明式事务的传播行为可以通过 @Transactional 注解中的 propagation 属性来定义,比如说:

@Transactional(propagation = Propagation.REQUIRED)
public void savePosts(PostsParam postsParam) {

}

TransactionDefinition 一共定义了 7 种事务传播行为: 只需要重点看 PROPAGATION_REQUIREDPROPAGATION_REQUIRES_NEW就可以。

2.1.1 PROPAGATION_REQUIRED :

@Transactional` 默认的事务传播行为,指的是如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。更确切地意思是:

  • 如果外部方法没有开启事务的话,Propagation.REQUIRED 修饰的内部方法会开启自己的事务,且开启的事务相互独立,互不干扰。
  • 如果外部方法开启事务并且是 Propagation.REQUIRED 的话,所有 Propagation.REQUIRED 修饰的内部方法和外部方法均属于同一事务 ,只要一个方法回滚,整个事务都需要回滚。
Class A {
    @Transactional(propagation=Propagation.PROPAGATION_REQUIRED)
    public void aMethod {
        //do something
        B b = new B();
        b.bMethod();
    }
}

Class B {
    @Transactional(propagation=Propagation.PROPAGATION_REQUIRED)
    public void bMethod {
       //do something
    }
}

这个传播行为也最好理解,aMethod 调用了 bMethod,只要其中一个方法回滚,整个事务均回滚。

2.1.2 PROPAGATION_REQUIRES_NEW

创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法都会开启自己的事务,且开启的事务与外部的事务相互独立,互不干扰。

Class A {
    @Transactional(propagation=Propagation.PROPAGATION_REQUIRED)
    public void aMethod {
        //do something
        B b = new B();
        b.bMethod();
    }
}

Class B {
    @Transactional(propagation=Propagation.REQUIRES_NEW)
    public void bMethod {
       //do something
    }
}

如果 aMethod()发生异常回滚,bMethod()不会跟着回滚,因为 bMethod()开启了独立的事务。但是,如果 bMethod()抛出了未被捕获的异常并且这个异常满足事务回滚规则的话,aMethod()同样也会回滚。

2.1.3 PROPAGATION_NESTED

如果当前存在事务,就在当前事务内执行;否则,就执行与 PROPAGATION_REQUIRED 类似的操作。

2.1.4 PROPAGATION_MANDATORY

如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。

2.1.5 PROPAGATION_SUPPORTS

如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。

2.1.6 PROPAGATION_NOT_SUPPORTED

以非事务方式运行,如果当前存在事务,则把当前事务挂起。

2.1.7 PROPAGATION_NEVER

以非事务方式运行,如果当前存在事务,则抛出异常。

2.2 事务隔离级别

TransactionDefinition 中一共定义了 5 种事务隔离级别:

  • ISOLATION_DEFAULT,使用数据库默认的隔离级别,MySql 默认采用的是 REPEATABLE_READ,也就是可重复读。
  • ISOLATION_READ_UNCOMMITTED,最低的隔离级别,可能会出现脏读、幻读或者不可重复读。
  • ISOLATION_READ_COMMITTED,允许读取并发事务提交的数据,可以防止脏读,但幻读和不可重复读仍然有可能发生。
  • ISOLATION_REPEATABLE_READ,对同一字段的多次读取结果都是一致的,除非数据是被自身事务所修改的,可以阻止脏读和不可重复读,但幻读仍有可能发生。
  • ISOLATION_SERIALIZABLE,最高的隔离级别,虽然可以阻止脏读、幻读和不可重复读,但会严重影响程序性能。

2.3 数据库并发问题

2.3.1 幻读

幻读(Phantom Read)是一种数据库并发问题,通常出现在事务隔离级别低于可重复读(如读已提交或未提交的隔离级别)时。在这种情况下,事务在两次查询之间,可能会“读到”另一事务新增或删除的数据行,导致前后查询结果不一致。

举个例子
假设有一个银行系统,管理员需要查询账户余额总和,以了解当前的存款情况。

  1. 事务A:管理员启动一个事务,执行第一次查询操作,得到所有账户余额的总和为1000元。
  2. 事务B:在事务A还未结束时,另一个管理员在事务B中创建了一个新账户,并存入200元。
  3. 事务A:再次查询账户余额总和,这次结果变成了1200元。
    在事务A内部,管理员希望每次查询到的数据一致,但因为在查询间隙插入了新账户,导致数据“发生了变化”。这种情况就被称为幻读。

解决办法
为了解决幻读问题,可以将事务隔离级别提高到 可重复读 或 可串行化。在可重复读级别下,数据库通常会使用 MVCC(多版本并发控制)来防止幻读,而在串行化级别下,数据库会通过加锁来保证事务的严格隔离,彻底避免幻读问题。

2.3.2 不可重复读

不可重复读(Non-Repeatable Read)是数据库中的一种并发问题,通常发生在隔离级别低于可重复读(如“读已提交”或“未提交读”)时。它指的是同一事务在两次读取同一数据时,数据内容发生了变化。这种情况通常是因为在两个读取操作之间,另一事务对数据进行了更新或删除。

举个例子
假设有一个购物系统,管理员想要在一笔事务内查询某商品的库存数量。

  1. 事务A:管理员开启一个事务,第一次查询商品库存为100。
  2. 事务B:在事务A还未结束时,另一个管理员在事务B中对该商品的库存进行了更新,将库存减少到90。
  3. 事务A:再次查询该商品库存时,结果变成了90。
    在事务A中,管理员希望查询到的商品库存数量是一致的,但因为在两次查询之间,另一个事务修改了库存,所以两次查询结果不一致。这就是不可重复读现象。

在事务A中,管理员希望查询到的商品库存数量是一致的,但因为在两次查询之间,另一个事务修改了库存,所以两次查询结果不一致。这就是不可重复读现象。

解决方法
可以将隔离级别提高到可重复读(Repeatable Read),在这种隔离级别下,数据库可以保证同一事务内多次读取的数据内容一致,从而避免不可重复读问题。

不可重复读 vs. 幻读

  • 不可重复读:指同一事务内读取的数据内容发生了变化,通常是因为数据被修改或删除。
  • 幻读:指同一事务内多次查询时,数据行的数量发生了变化,通常是因为有新数据插入或数据删除,导致查询结果的行数不一致。

2.4 事务的超时时间

事务超时,也就是指一个事务所允许执行的最长时间,如果在超时时间内还没有完成的话,就自动回滚。
假如事务的执行时间格外的长,由于事务涉及到对数据库的锁定,就会导致长时间运行的事务占用数据库资源。

2.5 事务的只读属性

如果一个事务只是对数据库执行读操作,那么该数据库就可以利用事务的只读属性,采取优化措施,适用于多条数据库查询操作中。

为什么一个查询操作还要启用事务支持呢?

这是因为 MySql(innodb)默认对每一个连接都启用了 autocommit 模式,在该模式下,每一个发送到 MySql 服务器的 SQL 语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务。
那如果我们给方法加上了 @Transactional 注解,那这个方法中所有的 SQL 都会放在一个事务里。否则,每条 SQL 都会单独开启一个事务,中间被其他事务修改了数据,都会实时读取到。
有些情况下,当一次执行多条查询语句时,需要保证数据一致性时,就需要启用事务支持。否则上一条 SQL 查询后,被其他用户改变了数据,那么下一个 SQL 查询可能就会出现不一致的状态。

2.6 事务的回滚策略

默认情况下,事务只在出现运行时异常(Runtime Exception)时回滚,以及 Error,出现检查异常(checked exception,需要主动捕获处理或者向上抛出)时不回滚。
如果你想要回滚特定的异常类型的话,可以这样设置:

@Transactional(rollbackFor= MyException.class)

三、Spring 事务失效场景

在 Spring 中,事务是通过 @Transactional 注解以及底层的 AOP(Aspect-Oriented Programming)机制来实现的。下面详细介绍其原理、使用方式以及常见的失效场景。

Spring 事务的实现原理:

  1. AOP 切面实现
    • @Transactional 注解基于 AOP 实现,通过代理对象在方法执行前后切入。
    • Spring 为标注了 @Transactional 的方法生成代理对象,并在方法调用前后管理事务。
    • 代理对象在方法调用前创建一个事务,在方法执行成功后提交事务,或在方法抛出异常时回滚事务。
  2. 事务管理器
    • Spring 提供了不同类型的事务管理器,例如 DataSourceTransactionManager 用于关系型数据库,JpaTransactionManager 用于 JPA。
    • 事务管理器负责具体事务的开启、提交、回滚等操作。

3.1 方法是 privatefinal

Spring 事务依赖 AOP 代理,而代理不能拦截 private 或 final 方法,因此这些方法上的 @Transactional 注解不会生效。
解决办法:将事务方法定义为 public,并避免使用 final 修饰。

3.2 自调用(self-invocation)

当一个 @Transactional 方法在同一类中被另一个方法直接调用时,不会通过代理对象进行调用,事务无法生效。
解决办法:将事务方法抽取到另一个类中,通过依赖注入调用,或者在当前类中使用代理对象调用。

3.3 非 RuntimeException 异常不回滚

Spring 默认仅在 RuntimeException(如 NullPointerException)或 Error 时回滚事务,而 checked exception(如 IOException)不会触发事务回滚。
解决办法:通过 rollbackFor 属性指定回滚的异常类型,例如 @Transactional(rollbackFor = Exception.class)。

3.4 多线程环境

Spring 事务仅在当前线程内有效,若在一个事务方法中启动新线程,则新线程不受事务控制。
解决办法:避免在事务中创建新线程,或使用异步事务管理。

3.5 嵌套事务与传播行为

某些传播行为(如 Propagation.REQUIRES_NEW)会导致嵌套事务失效,因为它会开启一个新的独立事务,外部事务的回滚不影响内部事务。
解决办法:在确定事务传播行为时,需根据具体场景合理设置传播属性。

3.6 未使用 Spring 管理的类

若在未被 Spring 管理的类中使用 @Transactional,事务不会生效,因为 Spring 无法对该类生成代理。
解决办法:确保 @Transactional 注解用于由 Spring 管理的 Bean(如 @Service 或 @Component)。

3.7 事务超时设置不当

若事务执行时间超过 timeout 配置,Spring 会自动回滚事务。这种场景较常见于执行较长时间的数据库操作。
解决办法:为长时间任务合理设置事务超时,或在配置中将事务管理器的 timeout 调大。

3.8 数据库不支持事务

某些数据库操作或存储引擎不支持事务(如 MySQL 的 MyISAM 引擎),在这些情况下 @Transactional 注解也不会生效。
解决办法:使用支持事务的存储引擎(如 InnoDB)。

checked exception 和 unchecked exception 区别

Checked Exception(已检查异常)

  • 定义:编译时被强制检查的异常,通常是继承自 Exception 类(但不是 RuntimeException)。
  • 处理要求:方法声明中必须使用 throws 抛出该异常,或者在方法体内使用 try-catch 进行捕获。编译器会检查这些异常的处理。
  • 常见场景:通常用于那些无法预知或控制的情况,比如 I/O 操作(文件读取、网络连接)等。

Unchecked Exception(未检查异常)

  • 定义:在运行时抛出的异常,不会在编译时强制检查,继承自 RuntimeException。
  • 处理要求:编译器不要求必须使用 try-catch 捕获或声明 throws 抛出该异常,程序员可以选择处理或不处理。
  • 常见场景:一般用于编码错误(如 NullPointerException、ArrayIndexOutOfBoundsException 等),表示程序员的逻辑错误或疏忽,通常可以通过改进代码逻辑避免。
  • 举个例子:
public int divide(int a, int b) {
    return a / b; // 当b为0时会抛出 ArithmeticException
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值