背景
springboot 单机项目,一个方法中涉及到多线程和多数据源修改的操作,希望保证事务的一致性。
解决方案
在使用Spring @Transactional声明的事务中,无法进行数据源的切换,此时有3种解决方案,本文采用方案3:
- 拆分成多个 Spring 事务,每个事务对应一个数据源。如果是【写】场景,可能会存在多数据源的事务不一致的问题。
- 引入 Seata 框架,提供完整的分布式事务的解决方案。
- 使用 Dynamic Datasource 提供的 @DSTransactional(opens new window)注解,支持多数据源的切换,不提供绝对可靠的多数据源的事务一致性(强于 1 弱于 2)。
动态数据源依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
单体启动类
@Slf4j
@SpringBootApplication
@EnableTransactionManagement
@EnableAspectJAutoProxy(proxyTargetClass = true, exposeProxy = true)
public class JeecgSystemApplication extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(JeecgSystemApplication.class);
}
public static void main(String[] args) throws UnknownHostException {
ConfigurableApplicationContext application = SpringApplication.run(JeecgSystemApplication.class, args);
}
// 其他代码...
}
截包方法
@Override
public Boolean capturePacket(String userName) {
// 校验
if(CollectionUtils.isNotEmpty(listDoingCapturingPacketData())){
throw new JeecgBootException("当前有正在执行的截包任务,预计1-2分钟,请稍后再试");
}
// 获取数据
List<TwBatchRecord3> twBatchRecord3s = capturePacketBailianMapper.packData();// 获取分拣机数据
String djbh1 = capturePacketBailianMapper.selectDJBH1();// 获取分拣单据编号
log.info("人工按截包功能,截包人:{}", userName);
// 插入/更新
if (CollectionUtils.isNotEmpty(twBatchRecord3s)) {
// 插入打包数据表
CapturingPacketData capturingPacketData = Optional.ofNullable(saveCpd(userName, djbh1))
.orElseThrow(() -> new JeecgBootException("插入老云集打包数据表失败"));
// 异步处理
final List<TwBatchRecord3> finalTwBatchRecord3s = twBatchRecord3s;
final String finalDjbh1 = djbh1;
final CapturingPacketData finalCapturePacketData = capturingPacketData;
ICapturingPacketDataService capturingPacketDataService = (ICapturingPacketDataService) AopContext.currentProxy();
Objects.requireNonNull(threadAsyncConfig.getAsyncExecutor()).execute(() -> {
try {
capturingPacketDataService.capturePacketTask(finalCapturePacketData, finalTwBatchRecord3s, finalDjbh1);// 截包任务
} catch (Exception e) {
baseMapper.updateReceiptStatusFailed(finalCapturePacketData.getId());
log.error("截包任务异常:" + e.getMessage());
}
});
} else {
throw new JeecgBootException("获取到的打包数据为空");
}
return Boolean.TRUE;
}
截包任务方法
/**
* 截包任务
* 插入渠道调拨单/商品异常单、打印单并更新截包数据状态
*
* @param capturePacketData
* @param finalTwBatchRecord3s
* @param finalDjbh1
* @return
*/
@DSTransactional // @DSTransactional 是在3.3.0版本中加入了多数据源的统一提交和回滚的功能
@Override
public void capturePacketTask(CapturingPacketData capturePacketData, List<TwBatchRecord3> finalTwBatchRecord3s, String finalDjbh1) {
// 插入百联,分拣机打包记录表;并更新百联,分拣机TW_SnoPno表的截包时间
capturePacketBailianService.saveProcess(finalTwBatchRecord3s, finalDjbh1);
// 其他逻辑
}
注意点
-
使用动态数据源(@DS)时,@Transactional 使用不当会导致 @DS 失效。
- 实现层上面加 @Transactional,数据源没有切换。
- 开启事务的同时,会从数据库连接池获取数据库连接。在这个事务内的所有数据库操作,都是在事务连接建立之后,所以会产生数据源没有切换的问题。
- 为了使 @DS 起作用,必须替换数据库连接,即改变事务的传播机制,产生新的事务,获取新的数据库连接。
-
多线程问题:
- 多线程情况下,直接在最外层添加事务注解是不生效的,但在子线程内的方法添加注解没有问题。
ICapturingPacketDataService capturingPacketDataService = (ICapturingPacketDataService) AopContext.currentProxy();
一方面,需要在主线程内获得,避免在子线程中无法获取代理对象的问题。另一方面,通过获取代理,避免了同方法中调用事务注解 AOP 不生效的问题。
-
@DSTransactional 使用注意点:
- 不可与 @Transactional 混用,保证该 service 的方法在整个调用链路上的所有方法都未使用 Spring 原生事务。
- @DSTransactional 目前并未实现全功能的事务,只支持统一提交和回滚。更复杂的场景要用 Seata。
- 3.3.6 版本的问题,3.4.0 版本以前使用 DSTransactional,没有 DS 会报错。