Spring 事务回调管理


在事务操作中,通常会面临这么一个场景,当数据落库之后需要推送至其它系统。

秉承着最小事务化的逻辑,针对非数据库操作且耗时动作,在设计上通常需要独立于事务之外,避免造成性能损耗。

针对此类业务逻辑,当然你可以 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);
        }
    });
}

文章作者: 烽火戏诸诸诸侯
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 烽火戏诸诸诸侯 !
  目录