单机多数据源多线程情况下如何保证事务的一致性

398 阅读3分钟

背景

springboot 单机项目,一个方法中涉及到多线程和多数据源修改的操作,希望保证事务的一致性。

解决方案

在使用Spring @Transactional声明的事务中,无法进行数据源的切换,此时有3种解决方案,本文采用方案3:

  1. 拆分成多个 Spring 事务,每个事务对应一个数据源。如果是【写】场景,可能会存在多数据源的事务不一致的问题。
  2. 引入 Seata 框架,提供完整的分布式事务的解决方案。
  3. 使用 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);

    // 其他逻辑
}

注意点

  1. 使用动态数据源(@DS)时,@Transactional 使用不当会导致 @DS 失效。

    • 实现层上面加 @Transactional,数据源没有切换。
    • 开启事务的同时,会从数据库连接池获取数据库连接。在这个事务内的所有数据库操作,都是在事务连接建立之后,所以会产生数据源没有切换的问题。
    • 为了使 @DS 起作用,必须替换数据库连接,即改变事务的传播机制,产生新的事务,获取新的数据库连接。
  2. 多线程问题:

    • 多线程情况下,直接在最外层添加事务注解是不生效的,但在子线程内的方法添加注解没有问题。
    • ICapturingPacketDataService capturingPacketDataService = (ICapturingPacketDataService) AopContext.currentProxy(); 一方面,需要在主线程内获得,避免在子线程中无法获取代理对象的问题。另一方面,通过获取代理,避免了同方法中调用事务注解 AOP 不生效的问题。
  3. @DSTransactional 使用注意点:

    • 不可与 @Transactional 混用,保证该 service 的方法在整个调用链路上的所有方法都未使用 Spring 原生事务。
    • @DSTransactional 目前并未实现全功能的事务,只支持统一提交和回滚。更复杂的场景要用 Seata。
    • 3.3.6 版本的问题,3.4.0 版本以前使用 DSTransactional,没有 DS 会报错。