在事务操作中,通常会面临这么一个场景,当数据落库之后需要推送至其它系统。
秉承着最小事务化的逻辑,针对非数据库操作且耗时动作,在设计上通常需要独立于事务之外,避免造成性能损耗。
针对此类业务逻辑,当然你可以 DataSourceTransactionManager
手动开启事务并提交。但显然如何一来也将提高代码复杂度,那在基于 @Transactiona
注解的前提下,如何实现事务的回调处理呢?
下面就让我们一同探究如何在 Spring Boot
中优化的实现事务回调。
1. 回调事件
让我们直接开门见山,在 Spring Boot
中提供 TransactionSynchronizationManager
可实现对事务的回调注册。
在回调事件注册中,会涉及如下两个方法,具体描述参考表格。
方法 | 作用 |
---|---|
isActualTransactionActive() | 是否当前线程是否处于事务中。 |
registerSynchronization() | 事务同步注册接口。 |
基于上述描述,我们便可判断当前线程是否处于事务中,若是则注册回调逻辑。
由此,事务回调的基本实现思路并已确定,对应下述实现代码:
public static void callback(Runnable runnable) {
// 判断事务处于事务中
if (TransactionSynchronizationManager.isActualTransactionActive()) {
// 若是,则注册回调事件
TransactionSynchronizationManager.registerSynchronization(new CallBackTask(runnable));
}
}
2. 同步注册
在 registerSynchronization()
注册回调接口时,其要求输入 TransactionSynchronization
接口实例。
接口中针对事务的不同阶段提供了不同的钩子,同时可以获取事务处理状态,所涉及的方法如下:
方法 | 作用 |
---|---|
beforeCommit() | 事务提交之前事件。 |
beforeCompletion() | 事务完成之前事件。 |
afterCommit() | 事务提交之后事件。 |
afterCompletion() | 事务完成之后事件。 |
针对表中所提到的不同事务阶段,为了更直观的展示让我们以图示进行说明。
其中 doSomthing()
为具体的业务逻辑,图示流程即从开启事务后最终提交并完成一个完整的事务周期。
接着之前的 registerSynchronization()
逻辑,让我们编写具体的回调处理逻辑。
新增 CallBackTask
并实现 TransactionSynchronization
接口,因此需处理事务完成后的回调逻辑,故此实现 afterCompletion()
钩子。
而在 afterCompletion()
中可以获取当前事务的完成状态,分别包含下述三类:
方法 | 作用 |
---|---|
STATUS_UNKNOWN | 未知状态。 |
STATUS_COMMITTED | 事务提交成功。 |
STATUS_ROLLED_BACK | 事务失败回滚。 |
基于上述内容,我们便可在指定事务状态下发起不同的回调逻辑。
以下述代码为例,传入 Runnable
线程回调逻辑,并在状态为 STATUS_COMMITTED
即事务成功提交后出发回调。
public class CallBackTask implements TransactionSynchronization {
private final Runnable runnable;
public CallBackTask(Runnable runnable) {
this.runnable = runnable;
}
@Override
public void afterCompletion(int status) {
switch (status) {
case TransactionSynchronization.STATUS_UNKNOWN:
System.out.println("Unknown");
break;
case TransactionSynchronization.STATUS_COMMITTED:
System.out.println("Transaction is committed");
// 事务提交成功,执行回调
runnable.run();
break;
case TransactionSynchronization.STATUS_ROLLED_BACK:
System.out.println("Transaction is rolled back");
break;
}
}
}
3. 集成示例
下面让我们来看一下如何在工程中进行集成使用。
让我们先假定业务场景,假如功能需要保存某业务数据,在保存成功后将数据同步至下游系统,而数据同步的总体耗时约 30s
左右。
基于此场景,最原始的方式即通过 @Transactional
注解针对整个方法开始事务。但如此设计存在一个问题,推送数据耗时导致事务迟迟无法提交,不仅降低服务性能且长时间事务挂起存在数据一致性问题。
@Transactional(rollbackFor = Exception.class)
public void directInsert(SysUser user) {
// 数据入库
sysUserDao.insert(user);
try {
// 模拟耗时
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
对于涉及事务处理的逻辑而言,永远要保持一个原则:事务最小化。
因此,对于上述的代码而言可以轻易改造为下述方式,即针对数据库操作部分单独抽象为事务操作,既实现了事务又可取得相对不错的性能。
public void manualCallback(SysUser user) {
// 数据入库, insert() 需获取代理对象操作,此处略去
insert(user);
try {
// 模拟耗时
TimeUnit.SECONDS.sleep(30);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
@Transactional(rollbackFor = Exception.class)
public void insert(SysUser user) {
sysUserDao.insert(user);
}
在第二个示例中,虽然实现了最终预期,但从代码上可以看出极易操作事务失效的情况,需通过 AopContext
创建代理对象处理。
再次改造上述的示例,同样是在一个方法内基于 @Transactional
开启事务。但通过之前定义的 TransactionManager
传入事务回调,当事务完成时将触发回调内容,实现与上述同样效果但代码更为精简。
@Transactional(rollbackFor = Exception.class)
public void callbackInsert(SysUser user) {
// 数据入库
sysUserDao.insert(user);
// 事务回调
TransactionManager.callback(() -> {
try {
// 模拟耗时
TimeUnit.SECONDS.sleep(30);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}