Spring事务管理

推荐看这篇文章,写的很好!

基本概念

Spring事务的本质其实就是数据库对事务的支持,没有数据库的事务支持,spring是无法提供事务功能的。对于纯JDBC操作数据库,想要用到事务,可以按照以下步骤进行:

  1. 获取连接 Connection con = DriverManager.getConnection()
  2. 开启事务con.setAutoCommit(true/false);
  3. 执行CRUD
  4. 提交事务/回滚事务 con.commit() / con.rollback();
  5. 关闭连接 conn.close();

使用Spring的事务管理功能后,我们可以不再写步骤 2 和 4 的代码,而是由Spirng 自动完成。那么Spring是如何在我们书写的 CRUD 之前和之后开启事务和关闭事务的呢?解决这个问题,也就可以从整体上理解Spring的事务管理实现原理了。

事务管理器模型

统一一致的事务抽象是Spring框架的一大优势,无论是全局事务还是本地事务,JTA、JDBC、Hibernate还是JPA,Spring都使用统一的编程模型,使得应用程序可以很容易地在全局事务与本地事务,或者不同的事务框架之间进行切换。下图是Spring事务抽象的核心类图:

  • PlatformTransactionManager:事务管理器,只有三个功能接口:
    • 获取事务资源getTransaction,资源可以是任意的,比如jdbc connection/hibernate mybatis session之类,然后绑定并存储
    • 提交事务commit提交指定的事务资源
    • 回滚事务rollback回滚指定的事务资源
  • TransactionDefinition:事务的定义信息,包括隔离级别、传播机制、是否只读等等
  • TransactionStatus:存储事务的状态,比如回滚状态、是否是新事务、事务是否已经完成等等

获取事务

事务管理器的第一步,就是根据事务定义来获取/创建资源了,这一步最麻烦的是要区分传播行为,不同传播行为下的逻辑不太一样。

“默认的传播行为下,使用当前事务”,怎么算有当前事务呢?

把事务资源存起来嘛,只要已经存在那就是有当前事务,直接获取已存储的事务资源就行。文中开头的例子也演示了,如果想让多个方法无感的使用同一个事务,可以用 ThreadLocal 存储起来,简单粗暴。

Spring 也是这么做的,不过它实现的更复杂一些,抽象了一层事务资源同步管理器 - TransactionSynchronizationManager(本文后面会简称 TxSyncMgr),在这个同步管理器里使用 ThreadLocal 存储了事务资源(本文为了方便理解,尽可能的不贴非关键源码)。

剩下的就是根据不同传播行为,执行不同的策略了,分类之后只有 3 个条件分支:

  1. 当前有事务 - 根据不同传播行为处理不同
  2. 当前没事务,但需要开启新事务
  3. 彻底不用事务 - 这个很少用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public final TransactionStatus getTransaction(TransactionDefinition definition) {
//创建事务资源 - 比如 Connection
Object transaction = doGetTransaction();

if (isExistingTransaction(transaction)) {
// 处理当前已有事务的场景
return handleExistingTransaction(def, transaction, debugEnabled);
}else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED){

// 开启新事务
return startTransaction(def, transaction, debugEnabled, suspendedResources);
}else {
// 彻底不用事务
}

// ...
}

先介绍一下分支 2 - 当前没事务,但需要开启新事务,这个逻辑相对简单一些。只需要新建事务资源,然后绑定到 ThreadLocal 即可:

1
2
3
4
5
6
7
8
9
10
private TransactionStatus startTransaction(TransactionDefinition definition, Object transaction,
boolean debugEnabled, SuspendedResourcesHolder suspendedResources) {

// 创建事务
DefaultTransactionStatus status = newTransactionStatus(
definition, transaction, true, newSynchronization, debugEnabled, suspendedResources);

// 开启事务(beginTx或者setAutoCommit之类的操作)
// 然后将事务资源绑定到事务资源管理器 TransactionSynchronizationManager
doBegin(transaction, definition);

现在回到分支 1 - 当前有事务 - 根据不同传播行为处理不同,这个就稍微有点麻烦了。因为有子方法独立事务的需求,可是 TransactionSynchronizationManager 却只能存一个事务资源。

挂起和恢复

Spring 采用了一种**挂起(Suspend) - 恢复(Resume)**的设计来解决这个嵌套资源处理的问题。当子方法需要独立事务时,就将当前事务挂起,从 TxSyncMgr 中移除当前事务资源,创建新事务的状态时,将挂起的事务资源保存至新的事务状态 TransactionStatus 中;在子方法结束时,只需要再从子方法的事务状态中,再次拿出挂起的事务资源,重新绑定至 TxSyncMgr 即可完成恢复的操作。

整个挂起 - 恢复的流程,如下图所示:

注意:挂起操作是在获取事务资源这一步做的,而恢复的操作是在子方法结束时(提交或者回滚)中进行的。

这样一来,每个 TransactionStatus 都会保存挂起的前置事务资源,如果方法调用链很长,每次都是新事务的话,那这个 TransactionStatus 看起来就会像一个链表:

提交事务

获取资源、操作完毕后来到了提交事务这一步,这个提交操作比较简单,只有两步:

  1. 当前是新事务才提交
  2. 处理挂起资源

怎么知道是新事务?

每经过一次事务嵌套,都会创建一个新的 TransactionStatus,这个事务状态里会记录当前是否是新事务。如果多个子方法都使用一个事务资源,那么除了第一个创建事务资源的 TransactionStatus 之外,其他都不是新事务。

如下图所示,A -> B -> C 时,由于 BC 都使用当前事务,那么虽然 ABC 所使用的事务资源是一样的,但是只有 A 的 TransactionStatus 是新事务,BC 并不是;那么在 BC 提交事务时,就不会真正的调用提交,只有回到 A 执行 commit 操作时,才会真正的调用提交操作。

回滚事务

除了提交,还有回滚呢,回滚事务的逻辑和提交事务类似:

  1. 如果是新事务才回滚,原因上面已经介绍过了
  2. 如果不是新事务则只设置回滚标记
  3. 处理挂起资源

注意:事务管理器是不包含回滚策略这个东西的,回滚策略是 AOP 版的事务管理增强的功能,但这个功能并不属于核心的事务管理器

事务隔离级别

① ISOLATION_DEFAULT:这是个 PlatfromTransactionManager 默认的隔离级别,使用数据库默认的事务隔离级别。

② ISOLATION_READ_UNCOMMITTED:读未提交,允许事务在执行过程中,读取其他事务未提交的数据。

③ ISOLATION_READ_COMMITTED:读已提交,允许事务在执行过程中,读取其他事务已经提交的数据。

④ ISOLATION_REPEATABLE_READ:可重复读,在同一个事务内,任意时刻的查询结果都是一致的。

⑤ ISOLATION_SERIALIZABLE:所有事务逐个依次执行。

事务的传播机制

spring事务的传播机制说的是,当多个事务同时存在的时候,spring如何处理这些事务的行为。事务传播机制实际上是使用简单的ThreadLocal实现的,所以,如果调用的方法是在新线程调用的,事务传播实际上是会失效的。

① PROPAGATION_REQUIRED:(默认传播行为)如果当前没有事务,就创建一个新事务;如果当前存在事务,就加入该事务。

② PROPAGATION_REQUIRES_NEW:无论当前存不存在事务,都创建新事务进行执行。

③ PROPAGATION_SUPPORTS:如果当前存在事务,就加入该事务;如果当前不存在事务,就以非事务执行。‘

④ PROPAGATION_NOT_SUPPORTED:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。

⑤ PROPAGATION_NESTED:如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则按REQUIRED属性执行。

⑥ PROPAGATION_MANDATORY:如果当前存在事务,就加入该事务;如果当前不存在事务,就抛出异常。

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

总结来说,就3种:

  • 如果存在事务,用存在的事务或创建新事务
  • 如果不存在事务,就创建事务
  • 不适用任何事务

事务实现方式

spring支持编程式事务管理和声明式事务管理两种方式:

  • 编程式事务管理使用TransactionTemplate。

    • 编程式事务操作更加复杂,但是更加灵活,可控性更好。
  • 声明式事务管理建立在AOP之上的。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前启动一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

    • 声明式事务最大的优点就是不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明或通过@Transactional注解的方式,便可以将事务规则应用到业务逻辑中,减少业务代码的污染。

声明式事务

Spring对方法的增强有五种方式:

  • 前置增强(org.springframework.aop.BeforeAdvice):在目标方法执行之前进行增强;
  • 后置增强(org.springframework.aop.AfterReturningAdvice):在目标方法执行之后进行增强;
  • 环绕增强(org.aopalliance.intercept.MethodInterceptor):在目标方法执行前后都执行增强;
  • 异常抛出增强(org.springframework.aop.ThrowsAdvice):在目标方法抛出异常后执行增强;
  • 引介增强(org.springframework.aop.IntroductionInterceptor):为目标类添加新的方法和属性。

声明式事务的实现就是通过环绕增强的方式。spring 中的事务实现从原理上说比较简单,通过 aop 在方法执行前后增加数据库事务的操作。

1、在方法开始时判断是否开启新事务,需要开启事务则设置事务手动提交 set autocommit=0;

2、在方法执行完成后手动提交事务 commit;

3、在方法抛出指定异常后调用 rollback 回滚事务

声明式事务实现起来非常简单,只需要使用@Transactional注解就可以,比如:

1
2
3
4
@Transactional(propagation=propagation.PROPAGATION_REQUIRED)
public void someMethod {
//do something
}

我们通常可以用@Transactional来修饰类和方法:

  1. 方法 :推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到 public 方法上,否则不生效。
  2. :如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效。

编程式事务

编程式事务需要依赖TransactionTemplate来实现,下面是一个简单例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Integer rows = new TransactionTemplate((PlatformTransactionManager) transactionManager,
new DefaultTransactionDefinition(TransactionDefinition.ISOLATION_READ_UNCOMMITTED))
.execute(new TransactionCallback<Integer>() {
@Override
public Integer doInTransaction(TransactionStatus status) {
// update 0
int rows0 = jdbcTemplate.update(...);

// update 1
int rows1 = jdbcTemplate.update(...);
return rows0 + rows1;
}
});
Integer rows2 = new TransactionTemplate((PlatformTransactionManager) transactionManager,
new DefaultTransactionDefinition(TransactionDefinition.ISOLATION_READ_UNCOMMITTED))
.execute(new TransactionCallback<Integer>() {
@Override
public Integer doInTransaction(TransactionStatus status) {

// update 2
int rows2 = jdbcTemplate.update(...);
return rows2;
}
});

事务失效场景总结

  • 数据库是否支持事务(Mysql的MyIsam不支持事务) ,行锁才支持事务

  • 是否发生多线程调用?因为事务底层依赖于ThreadLocal,不同线程获取到的数据库连接不一样

  • 注解所在类是否被加载成Bean

  • 入口函数是否是public的

    • private方法不会生效,JDK中必须是接口,接口中不可能有private方法,protect方法的话,也不生效。原因是spring内部判断方法修饰符如果不是public不生成事务拦截代理类。
  • 方法是否是final?

    • spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。 但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,而添加事务功能。
    • 如果某个方法是static的,同样无法通过动态代理,变成事务方法。
  • 是否发生了自调用

    • 发生自调用时,不会经过代理类,事务自然也就失效了

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      @Service
      public class OrderServiceImpl implements OrderService {
      @Transactional
      public void update(Order order) {
      updateOrder(order);
      }
      @Transactional(propagation = Propagation.REQUIRES_NEW)
      public void updateOrder(Order order) {
      // update order
      }
      }
  • 所有数据源是否加载了事务管理器

  • @Transactional的传播策略是否配置正确

  • 异常有没有被正常抛出,是不是被子方法吃掉了

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2022 Yin Peng
  • 引擎: Hexo   |  主题:修改自 Ayer
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信