您现在的位置:首页 > >

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);
}

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


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

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



在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 inCombinedTx(Callable callable, String[] transactions) {
if (callable == null) {
return null;
}

Callable 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 inTransaction(Callable 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 inTransaction(Callable 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,否则无法看到数据库提交的相关日志信息。


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



参考链接


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


热文推荐
猜你喜欢
友情链接: