事务管理,一个被说烂的也被看烂的话题,还是八股文中的根底股之一。
本文会从设计角度,一步步的分析 Spring 事务管理的设计思路(都会设计事务管理器了,还能玩不转?)
为什么须要事务管理?
先看看如果没有事务管理器的话,如果 想让多个操作(办法 / 类)处在一个事务里 应该怎么做:
// MethodA:
public void methodA(){Connection connection = acquireConnection();
try{int updated = connection.prepareStatement().executeUpdate();
methodB(connection);
connection.commit();}catch (Exception e){rollback(connection);
}finally {releaseConnection(connection);
}
}
// MethodB:
public void methodB(Connection connection){int updated = connection.prepareStatement().executeUpdate();}
或者用 ThreadLocal 存储 Connection?
static ThreadLocal<Connection> connHolder = new ThreadLocal<>();
// MethodA:
public void methodA(){Connection connection = acquireConnection();
connHolder.set(connection);
try{int updated = connection.prepareStatement().executeUpdate();
methodB();
connection.commit();}catch (Exception e){rollback(connection);
}finally {releaseConnection(connection);
connHolder.remove();}
}
// MethodB:
public void methodB(){Connection connection = connHolder.get();
int updated = connection.prepareStatement().executeUpdate();
}
还是有点恶心,再形象一下?将绑定 Connection 的操作提取为公共办法:
static ThreadLocal<Connection> connHolder = new ThreadLocal<>();
private void bindConnection(){Connection connection = acquireConnection();
connHolder.set(connection);
}
private void unbindConnection(){releaseConnection(connection);
connHolder.remove();}
// MethodA:
public void methodA(){
try{bindConnection();
int updated = connection.prepareStatement().executeUpdate();
methoB();
connection.commit();}catch (Exception e){rollback(connection);
}finally {unbindConnection();
}
}
// MethodB:
public void methodB(){Connection connection = connHolder.get();
int updated = connection.prepareStatement().executeUpdate();
}
当初看起来好点了,不过我有一个新的需要:想让 methodB 独立一个新事务,独自提交和回滚,不影响 methodA
这……可就有点难搞了,ThreadLocal 中曾经绑定了一个 Connection,再新事务的话就不好办了
那如果再简单点呢,methodB 中须要调用 methodC,methodC 也须要一个独立事务……
而且,每次 bind/unbind 的操作也有点太傻了,万一哪个办法忘了写 unbind,最初来一个连贯泄露那不是完蛋了!
好在 Spring 提供了事务管理器,帮咱们解决了这一系列痛点。
Spring 事务管理解决了什么问题?
Spring 提供的事务管理能够帮咱们治理事务相干的资源,比方 JDBC 的 Connection、Hibernate 的 Session、Mybatis 的 SqlSession。如说下面的 Connection 绑定到 ThreadLocal 来解决共享一个事务的这种形式,Spring 事务管理就曾经帮咱们做好了。
还能够帮咱们解决简单场景下的嵌套事务,比方后面说到的 methodB/methodC 独立事务。
什么是嵌套事务?
还是拿下面的例子来说,methodA 中调用了 methodB,两个办法都有对数据库的操作,而且都须要事务:
// MethodA:
public void methodA(){int updated = connection.prepareStatement().executeUpdate();
methodB();
// ...
}
// MethodB:
public void methodB(){// ...}
这种多个办法调用链中都有事务的场景,就是嵌套事务。不过要留神的是,并不是说多个办法应用一个事务才叫嵌套,哪怕是不同的事务,只有在这个办法的调用链中,都是嵌套事务。
什么是事务流传行为?
那调用链中的子办法,是用一个新事务,还是应用以后事务呢?这个子办法决定应用新事务还是以后事务(或不应用事务)的策略,就叫事务流传。
在 Spring 的事务管理中,这个子办法的事务处理策略叫做事务流传行为(Propogation Behavior)。
有哪些事务流传行为?
Spring 的事务管理反对多种流传行为,这里就不贴了,八股文里啥都有。
但给这些流传行为分类之后,无非是以下三种:
- 优先应用以后事务
- 不应用以后事务,新建事务
- 不应用任何事务
比方下面的例子中,methodB/methodC 独立事务,就属于第 2 种流传行为 – 不应用以后事务,新建事务
看个栗子
以 Spring JDBC + Spring注解版 的事务举例。在默认的事务流传行为下,methodA 和 methodB 会应用同一个 Connection,在一个事务中
@Transactional
public void methodA(){jdbcTemplate.batchUpdate(updateSql, params);
methodB();}
@Transactional
public void methodB(){jdbcTemplate.batchUpdate(updateSql, params);
}
如果我想让 methodB 不应用 methodA 的事务,本人新建一个连贯 / 事务呢?只须要简略的配置一下 @Transactional 注解:
@Transactional
public void methodA(){jdbcTemplate.batchUpdate(updateSql, params);
methodB();}
// 流传行为配置为 - 形式 2,不应用以后事务,独立一个新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB(){jdbcTemplate.batchUpdate(updateSql, params);
}
就是这么简略,获取 Connection/ 多办法共享 Connection/ 多办法共享 + 独享 Connection/ 提交 / 开释连贯之类的操作,齐全不须要咱们操心,Spring 都替咱们做好了。
怎么回滚?
在注解版的事务管理中,默认的的回滚策略是:抛出异样就回滚。这个默认策略挺好,连回滚都帮咱们解决了,再也不必手动回滚。
然而如果在嵌套事务中,子办法独立新事务呢?这个时候哪怕抛出异样,也只能回滚子事务,不能间接影响前一个事务
可如果这个抛出的异样不是 sql 导致的,比方校验不通过或者其余的异样,此时应该将以后的事务回滚吗?
这个还真不肯定,谁说抛异样就要回滚,异样也不回滚行不行?
当然能够!抛异样和回滚事务原本就是两个问题,能够连在一起,也能够离开解决
// 流传行为配置为 - 形式 2,不应用以后事务,独立一个新事务
// 指定 Exception 也不会滚
@Transactional(propagation = Propagation.REQUIRES_NEW, noRollbackFor = Exception.class)
public void methodB(){jdbcTemplate.batchUpdate(updateSql, params);
}
每个事务 / 连贯应用不同配置
除了流传和回滚之外,还能够给每个事务 / 连贯应用不同的配置,比方不同的隔离级别:
@Transactional
public void methodA(){jdbcTemplate.batchUpdate(updateSql, params);
methodB();}
// 流传行为配置为 - 形式 2,不应用以后事务,独立一个新事务
// 这个事务 / 连贯中应用 RC 隔离级别,而不是默认的 RR
@Transactional(propagation = Propagation.REQUIRES_NEW, isolation = Isolation.READ_UNCOMMITTED)
public void methodB(){jdbcTemplate.batchUpdate(updateSql, params);
}
除了隔离级别之外,其余的 JDBC Connection 配置当然也是反对的,比方 readOnly。这样一来,尽管咱们不必显示的获取 connection/session,但还是能够给嵌套中的每一个事务配置不同的参数,非常灵活。
性能总结
好了,当初曾经理解了 Spring 事务管理的所有外围性能,来总结一下这些外围性能点:
- 连贯 / 资源管理 – 无需手动获取资源、共享资源、开释资源
- 嵌套事务的反对 – 反对嵌套事务中应用不同的资源策略、回滚策略
- 每个事务 / 连贯应用不同的配置
事务管理器(TransactionManager)模型
其实认真想想,事务管理的外围操作只有两个:提交和回滚。后面所谓的流传、嵌套、回滚之类的,都是基于这两个操作。
所以 Spring 将事务管理的外围性能形象为一个 事务管理器(Transaction Manager),基于这个事务管理器外围,能够实现多种事务管理的形式。
这个外围的事务管理器只有三个性能接口:
- 获取事务资源,资源能够是任意的,比方 jdbc connection/hibernate mybatis session 之类,而后绑定并存储
- 提交事务– 提交指定的事务资源
- 回滚事务– 回滚指定的事务资源
interface PlatformTransactionManager{
// 获取事务资源,资源能够是任意的,比方 jdbc connection/hibernate mybatis session 之类
TransactionStatus getTransaction(TransactionDefinition definition)
throws TransactionException;
// 提交事务
void commit(TransactionStatus status) throws TransactionException;
// 回滚事务
void rollback(TransactionStatus status) throws TransactionException;
}
事务定义 – TransactionDefinition
还记得下面的 @Transactional 注解吗,外面定义了流传行为、隔离级别、回滚策略、只读之类的属性,这个就是一次事务操作的定义。
在获取事务资源时,须要依据这个事务的定义来进行不同的配置:
- 比方配置了应用新事务,那么在获取事务资源时就须要创立一个新的,而不是已有的
- 比方配置了隔离级别,那么在首次创立资源(Connection)时,就须要给 Connection 设置 propagation
- 比方配置了只读属性,那么在首次创立资源(Connection)时,就须要给 Connection 设置 readOnly
为什么要独自用一个 TransactionDefinition 来存储事务定义,间接用注解的属性不行吗?
当然能够,但注解的事务管理只是 Spring 提供的自动挡,还有适宜老司机的手动挡事务管理(前面会介绍);手动挡可用不了注解,所以独自建一个事务定义的模型,这样就能够实现通用。
事务状态 – TransactionStatus
那既然嵌套事务下,每个子办法的事务可能不同,所以还得有一个子办法事务的状态 – TransactionStatus,用来存储以后事务的一些数据和状态,比方事务资源(Connection)、回滚状态等。
获取事务资源
事务管理器的第一步,就是依据事务定义来获取 / 创立资源了,这一步最麻烦的是要辨别流传行为,不同流传行为下的逻辑不太一样。
“默认的流传行为下,应用以后事务”,怎么算有以后事务呢?
把事务资源存起来嘛,只有曾经存在那就是有以后事务,间接获取已存储的事务资源就行。文中结尾的例子也演示了,如果想让多个办法无感的应用同一个事务,能够用 ThreadLocal 存储起来,简略粗犷。
Spring 也是这么做的,不过它实现的更简单一些,形象了一层 事务资源同步管理器 – TransactionSynchronizationManager(本文前面会简称 TxSyncMgr),在这个同步管理器里应用 ThreadLocal 存储了事务资源(本文为了不便了解,尽可能的不贴非关键源码)。
剩下的就是依据不同流传行为,执行不同的策略了,分类之后只有 3 个条件分支:
- 以后有事务 – 依据不同流传行为解决不同
- 以后没事务,但须要开启新事务
- 彻底不必事务 – 这个很少用
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 即可:
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 却只能存一个事务资源。
挂起(Suspend)和复原(Resume)
Spring 采纳了一种挂起 (Suspend) – 复原(Resume) 的设计来解决这个嵌套资源解决的问题。当子办法须要独立事务时,就将以后事务挂起,从 TxSyncMgr 中移除以后事务资源,创立新事务的状态时,将挂起的事务资源保留至新的事务状态 TransactionStatus 中;在子办法完结时,只须要再从子办法的事务状态中,再次拿出挂起的事务资源,从新绑定至 TxSyncMgr 即可实现复原的操作。
整个挂起 – 复原的流程,如下图所示:
留神:挂起操作是在获取事务资源这一步做的,而复原的操作是在子办法完结时(提交或者回滚)中进行的。
这样一来,每个 TransactionStatus 都会保留挂起的前置事务资源,如果办法调用链很长,每次都是新事务的话,那这个 TransactionStatus 看起来就会像一个链表:
提交事务
获取资源、操作结束起初到了提交事务这一步,这个提交操作比较简单,只有两步:
- 以后是新事务才提交
- 解决挂起资源
怎么晓得是新事务?
每通过一次事务嵌套,都会创立一个新的 TransactionStatus,这个事务状态里会记录以后是否是新事务。如果多个子办法都应用一个事务资源,那么除了第一个创立事务资源的 TransactionStatus 之外,其余都不是新事务。
如下图所示,A -> B -> C 时,因为 BC 都应用以后事务,那么尽管 ABC 所应用的事务资源是一样的,然而只有 A 的 TransactionStatus 是新事务,BC 并不是;那么在 BC 提交事务时,就不会真正的调用提交,只有回到 A 执行 commit 操作时,才会真正的调用提交操作。
这里再解释下,为什么新事务才须要提交,而曾经有事务却什么都不必做:
因为对于新事务来说,这里的提交操作曾经是事务实现了;而对于非新事务的场景,前置事务(即以后事务)还没有执行完,可能前面还有其余数据库操作,所以这个提交的操作得让以后事务创立方去做,这里并不能提交。
回滚事务
除了提交,还有回滚呢,回滚事务的逻辑和提交事务相似:
- 如果是新事务才回滚,起因下面曾经介绍过了
- 如果不是新事务则只设置回滚标记
- 解决挂起资源
留神:事务管理器是不蕴含回滚策略这个货色的,回滚策略是 AOP 版的事务管理加强的性能,但这个性能并不属于外围的事务管理器
自动挡与手动挡
Spring 的事务管理性能都是围绕着下面这个事务管理器运行的,提供了三种治理事务的形式,别离是:
- XML AOP 的事务管理 – 比拟古老当初用的不多
- 注解版本的事务管理 – @Transactional
- TransactionTemplate – 手动挡的事务管理,也称编程式事务管理
自动挡
XML/@Transactional 两种基于 AOP 的注解治理,其入口类是 TransactionInterceptor,是一个 AOP 的 Interceptor,负责调用事务管理器来实现事务管理。
因为外围性能都在事务管理器里实现,所以这个 AOP Interceptor 很简略,只是调用一下事务管理器,外围(伪)代码如下:
public Object invoke(MethodInvocation invocation) throws Throwable {
// 获取事务资源
Object transaction = transactionManager.getTransaction(txAttr);
Object retVal;
try {
// 执行业务代码
retVal = invocation.proceedWithInvocation();
// 提交事务
transactionManager.commit(txStatus);
} catch (Throwable ex){
// 先判断异样回滚策略,而后调用事务管理器的 rollback
rollbackOn(ex, txStatus);
}
}
并且 AOP 这种自动挡的事务管理还减少了一个回滚策略的玩法,这个是手动挡 TransactionTemplate 所没有的,但这个性能并不在事务管理器中,只是 AOP 版事务的一个加强。
手动挡
TransactionTemplate
这个是手动挡的事务管理,尽管没有注解的不便,然而好在灵便,异样 / 回滚啥的都能够本人管制。
所以这个实现更简略,连异样回滚策略都没有,非凡的回滚形式还要本人设置(默认是任何异样都会回滚),外围(伪)代码如下:
public <T> T execute(TransactionCallback<T> action) throws TransactionException {
// 获取事务资源
TransactionStatus status = this.transactionManager.getTransaction(this);
T result;
try {
// 执行 callback 业务代码
result = action.doInTransaction(status);
}
catch (Throwable ex) {
// 调用事务管理器的 rollback
rollbackOnException(status, ex);
}
提交事务
this.transactionManager.commit(status);
}
}
为什么有这么不便的自动挡,还要手动挡?
因为手动挡更灵便啊,想怎么玩就怎么玩,比方我能够在一个办法中,执行多个数据库操作,但应用不同的事务资源:
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;
}
});
在下面这个例子里,通过 TransactionTemplate 咱们能够准确的管制 update0/update1 应用同一个事务资源和隔离级别,而 update2 独自应用一个事务资源,并且不须要新建类加注解的形式。
手自一体能够吗?
当然能够,只有咱们应用的是同一个事务管理器的实例,因为绑定资源到同步资源管理器这个操作是在事务管理器中进行的。
AOP 版本的事务管理里,同样能够应用手动挡的事务管理持续操作,而且还能够应用同一个事务资源。
比方上面这段代码,update1/update2 依然在一个事务内,并且 update2 的 callback 完结后并不会提交事务,事务最终会在 methodA 完结时,TransactionInterceptor 中才会提交
@Transactional
public void methodA(){
// update 1
jdbcTemplate.update(...);
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;
}
});
}
总结
Spring 的事务管理,其外围是一个形象的事务管理器,XML/@Transactional/TransactionTemplate 几种形式都是基于这个事务管理器的,三中形式的外围实现区别并不大,只是入口不同而已。
本文为了不便了解,省略了大量的非关键实现细节,可能会导致有局部形容不谨严的中央,如有问题欢送评论区留言。
作者:京东保险 蒋信
起源:京东云开发者社区 转载请注明起源