coding……
但行好事 莫问前程

Spring Boot多数据源事务管理

在开发企业应用时,对于使用者的一个操作实际上对应底层数据库的多个读写。由于数据操作在顺序执行的过程中,任何一步操作都有可能发生异常,异常会导致后续操作无法完成,此时由于业务逻辑并未正确的完成,之前成功操作数据的并不可靠,会产生不一致的数据,需要在这种情况下进行回退。事务的作用就是为了保证用户的每一个操作都是可靠的,事务中的每一步操作都必须成功执行,只要有发生异常就回退到事务开始未进行操作的状态。了解事务的基本属性和隔离级别,请参考之前的一篇文章理解数据库事务的4种隔离级别。了解事务的传播属性,请看Spring Boot中使用@Transactional注解配置事务管理。对于单源数据库,只要在需要进行事务控制的方法上添加@Transactional注解就可以,但是对于多源数据库,@Transactionnal是无法管理多个数据源的。本篇文章主要介绍上篇文章多源数据库操作时,事务控制的实现方式。其次要讲的是,如果想真正实现多源数据库事务控制,肯定是需要分布式锁的,本篇文章介绍的两种方式,并没有使用分布式锁,换言之,只是多源数据库事务控制的一种变通方式。

1. 只使用主库TransactionManger

这种方式,在需要进行事务控制的方法上加@Transactional注解,并在注解上使用value属性,注明是主库事务管理器。如下:

@Transactional(value = "masterTransactionManager")
public void createUser(String userName, String description) {
    MasterUser masterUser = new MasterUser();

    /*主数据库插入*/
    masterUser.setUserName(userName);
    masterUser.setDescription(description);
    masterUser.setIsDeleted(DataStatusEnum.EXIST.getCode());
    masterUserMapper.insertSelective(masterUser);

    /*从数据库插入*/
    SlaveUser slaveUser = new SlaveUser();
    slaveUser.setUserName(userName);
    slaveUser.setDescription(description);
    slaveUser.setIsDeleted(DataStatusEnum.EXIST.getCode());
    slaveUserMapper.insertSelective(slaveUser);
}

使用主库事务管理器,也就是说事务中产生异常时,只能回滚主库数据。但是因为数据操作顺序是先主后从,所以分一下三种情况:

  1. 主库插入时异常,主库未插成功,这时候从库还没来及插入,主从数据是还是一致的
  2. 主库插入成功,从库插入时异常,这时候在主库事务管理器监测到事务中存在异常,将之前插入的主库数据插入,主从数据还是一致的
  3. 主库插入成功,从库插入成功,事务结束,主从数据一致。

当然这只是理想情况,假如存在一种情况,在数据库从库插入之后,还有其他业务逻辑的处理,假如这部分业务处理产生了异常,主库事务管理器只能回滚主库数据,但是从库数据是无法回滚的,这时候主从数据变产生了不一致。还有比如从库数据插入成功后,主库提交,这时候主库崩溃了,导致数据没插入,这时候从库数据也是无法回滚的。这种方式可以简单实现多源数据库的事务管理,但是无法处理上述情况。看一下正常处理下情况下打印的日志信息:

在createUser方法上,添加了注解@Transactional(value = “masterTransactionManager”),只开启了主库的事务管理器,从日志上看,也只有主库事务管理生效了,与预期一致。

2. 为方法添加多个事务管理器

@Transactional注解支持指定事务管理器,假如可以为一个方法添加多个注解,是不是就可以了完成对两个数据源的事务管理。但是,Spring是不支持为一个方法添加两个@Transactional注解的,所以最直接的想法是可不可以通过代码实习为一个方法添加两个事务管理器,最终找到一种解决方案,通过自定义注解方式,实现为createUser方法添加两个@Transactional注解的效果,并开启两个事务管理器。核心代码如下:

2.1 添加自定义注解MultiTransactional

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MultiTransactional {

    String[] value() default {};
}

注解使用对象是方法,@Retention(RetentionPolicy.RUNTIME)表示注解会在class字节码文件中存在,在运行时可以通过反射获取到。关于自定义注解的详细信息,请参考这篇文章

2.2 添加主从数据库事务管理器名称常量

public class DbTxConstants {

    public static final String DB1_TX = "masterTransactionManager";

    public static final String DB2_TX = "slaveTransactionManager";
}

其实这一步也可以省略,只是代码中要多次使用主从事务管理器名,所以这里定义成常量。

2.3 添加自定义拦截器

@Aspect
@Component
public class MultiTransactionAop {

    private final ComboTransaction comboTransaction;

    @Autowired
    public MultiTransactionAop(ComboTransaction comboTransaction) {
        this.comboTransaction = comboTransaction;
    }

    @Pointcut("@annotation(com.zhuoli.service.springboot.mybatis.transaction.repository.aop.MultiTransactional)")
    public void pointCut() {
    }

    @Around("pointCut() && @annotation(multiTransactional)")
    public Object inMultiTransactions(ProceedingJoinPoint pjp, MultiTransactional multiTransactional) {
        return comboTransaction.inCombinedTx(() -> {
            try {
                return pjp.proceed();
            } catch (Throwable throwable) {
                if (throwable instanceof RuntimeException) {
                    throw (RuntimeException) throwable;
                }
                throw new RuntimeException(throwable);
            }
        }, multiTransactional.value());
    }
}

功能为收集被拦截方法MultiTransactional注解,并将Callable对象 () -> createUser()作为参数传给comboTransaction.inCombinedTx方法。

2.4 ComboTransaction类

@Component
public class ComboTransaction {

    @Autowired
    private Db1TxBroker db1TxBroker;

    @Autowired
    private Db2TxBroker db2TxBroker;

    public <V> V inCombinedTx(Callable<V> callable, String[] transactions) {
        if (callable == null) {
            return null;
        }

        Callable<V> combined = Stream.of(transactions)
                .filter(ele -> !StringUtils.isEmpty(ele))
                .distinct()
                .reduce(callable, (r, tx) -> {
                    switch (tx) {
                        case DbTxConstants.DB1_TX:
                            return () -> db1TxBroker.inTransaction(r);
                        case DbTxConstants.DB2_TX:
                            return () -> db2TxBroker.inTransaction(r);
                        default:
                            return null;
                    }
                }, (r1, r2) -> r2);

        try {
            return combined.call();
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

简单讲一下方法inCombinedTx的作用,其实就是将自定义注解@MultiTransactional的参数通过Java8 Stream的Reduce操作,转化为Callable对象,不了解Reduce操作的同学,可以参考我之前的一篇文章Java8 Stream reduce操作。将最终的Callable对象调用call方法执行,得到最终结果。

2.5 Db1TxBroker & Db2TxBroker

@Component
public class Db1TxBroker {

    @Transactional(DbTxConstants.DB1_TX)
    public <V> V inTransaction(Callable<V> callable) {
        try {
            return callable.call();
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

@Component
public class Db2TxBroker {

    @Transactional(DbTxConstants.DB2_TX)
    public <V> V inTransaction(Callable<V> callable) {
        try {
            return callable.call();
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

ComboTransaction类的inCombinedTx方法,第一个参数为() -> createUser(),所以reduce执行过程如下:

1. () -> db1TxBroker.inTransaction(() -> createUser())

2. db1TxBroker.inTransaction(() -> createUser())结果为
@Transactional("masterTransactionManager")
createUser()
所以() -> db1TxBroker.inTransaction(() -> createUser())结果为
      @Transactional("masterTransactionManager")
() -> createUser()

3. db2TxBroker.inTransaction(() -> createUser())结果为
@Transactional("slaveTransactionaManager")
@Transactional("masterTransactionManager")
createUser()
所以() -> db2TxBroker.inTransaction(() -> createUser())结果为
      @Transactional("slaveTransactionaManager")
      @Transactional("masterTransactionManager")
() -> createUser()

4. combined.call()其实等价于
@Transactional("slaveTransactionaManager")
@Transactional("masterTransactionManager")
createUser()
实现为createUser()添加两个@Transactional注解

2.6 自定义注解使用

@Override
@MultiTransactional(value = {DbTxConstants.DB1_TX, DbTxConstants.DB2_TX})
public void createUserWithAnnotation(String userName, String description) {
    MasterUser masterUser = new MasterUser();

    /*主数据库插入*/
    masterUser.setUserName(userName);
    masterUser.setDescription(description);
    masterUser.setIsDeleted(DataStatusEnum.EXIST.getCode());
    masterUserMapper.insertSelective(masterUser);

    /*从数据库插入*/
    SlaveUser slaveUser = new SlaveUser();
    slaveUser.setUserName(userName);
    slaveUser.setDescription(description);
    slaveUser.setIsDeleted(DataStatusEnum.EXIST.getCode());
    slaveUserMapper.insertSelective(slaveUser);
}

2.7 日志信息

可以看到,开启了两个事务管理器,符合预期

2.8 异常模拟

@Override
@MultiTransactional(value = {DbTxConstants.DB1_TX, DbTxConstants.DB2_TX})
public void createUserWithAnnotation(String userName, String description) {
    MasterUser masterUser = new MasterUser();

    /*主数据库插入*/
    masterUser.setUserName(userName);
    masterUser.setDescription(description);
    masterUser.setIsDeleted(DataStatusEnum.EXIST.getCode());
    masterUserMapper.insertSelective(masterUser);

    /*从数据库插入*/
    SlaveUser slaveUser = new SlaveUser();
    slaveUser.setUserName(userName);
    slaveUser.setDescription(description);
    slaveUser.setIsDeleted(DataStatusEnum.EXIST.getCode());
    slaveUserMapper.insertSelective(slaveUser);

    if (true){
        throw new RuntimeException("Exception");
    }
}

日志信息如下:可以看到使用自定义注解这种方式,假如在从库插入后,还有其他业务逻辑,并且报了异常,这时候主从数据库都是可以回滚的。当然这种事务控制方式也存在不完美的地方,比如当提交时数据库崩溃这种情况,依然是无法解决的,但是这种情况可能性是相对比较小的,所以在不使用分布式锁的情况下,这种事务多元数据库事务管理方式是一种有效的方案。

另外对于本篇文章的示例代码配置需要注意一下,需要讲日志级别调到DEBUG,否则无法看到数据库提交的相关日志信息。

示例代码: 码云 – 卓立 – 多源数据库事务控制

参考链接

  1. Spring Boot 中使用 @Transactional 注解配置事务管理

赞(0) 打赏
Zhuoli's Blog » Spring Boot多数据源事务管理
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址