Spring 的声明式事务在多线程的场景当中会失效,该怎么解决呢?

Spring声明式事务在多线程中失效,核心原因是事务上下文(ThreadLocal绑定)无法跨线程传递,导致子线程无法继承主线程的事务状态。解决需围绕"确保事务上下文共享 "或"避免跨线程事务依赖"展开,具体方案如下:

1. 方案一:手动管理事务(编程式事务)

放弃声明式事务的注解(@Transactional),通过PlatformTransactionManager手动控制事务边界,主动将事务上下文传递到子线程。

核心逻辑:主线程开启事务后,将TransactionStatus对象传递给子线程,子线程通过事务管理器手动提交/回滚。

代码示例:

scss 复制代码
public class PlatformTransactionMangerDemo {
    //手动事务管理
    @Autowired
    private PlatformTransactionManager transactionManager;

    public void doTask() {
        DefaultTransactionDefinition def = new DefaultTransactionDefinition();
        // 注意:在实际应用中,PlatformTransactionManager 通常是由 Spring 容器自动注入的
        TransactionStatus status = transactionManager.getTransaction(def);
        CountDownLatch latch = new CountDownLatch(1); // 用于等待子线程完成

        try {
            //子线程执行任务,传入事务状态
            new Thread(() -> {
                try {
                    // 模拟一些操作
                    System.out.println("子线程正在执行任务...");
                    latch.countDown(); // 子线程任务开始,通知主线程
                    doSonTask(); // 执行子线程任务
                    System.out.println("子线程任务完成");
                } catch (Exception e) {

                    // 如果子线程被中断,抛出异常
//                    throw new RuntimeException("子线程执行异常", e);
                    transactionManager.rollback(status); // 回滚事务
                }
            }).start();
            // 主线程等待子线程完成
            latch.await();
            System.out.println("主线程等待子线程完成");
            //主线程业务逻辑
            doMianTask();
            System.out.println("主线程任务完成");
            // 如果操作成功,提交事务
            transactionManager.commit(status);
        } catch (Exception e) {
            // 如果发生异常,回滚事务
            transactionManager.rollback(status);
        }
    }

    private void doMianTask() {
//        System.out.println("主线程任务完成");
        // 模拟主线程任务执行
        try {
            Thread.sleep(1000); // 模拟耗时操作
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // 恢复中断状态
            System.out.println("主线程被中断");
        }
        System.out.println("主线程任务完成");
    }

    private void doSonTask() {
        System.out.println("子线程任务完成");
        // 模拟子线程任务执行
        try {
            Thread.sleep(1000); // 模拟耗时操作
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt(); // 恢复中断状态
            System.out.println("子线程被中断");
        }
    }

    public static void main(String[] args) {
        PlatformTransactionMangerDemo demo = new PlatformTransactionMangerDemo();
        demo.doTask(); // 执行任务
    }

}

注意:需确保主线程等待子线程执行完成(如用CountDownLatch),否则主线程提前提交/回滚会导致子线程事务失效。

2. 方案二:使用ThreadLocal传递事务上下文(谨慎使用)

Spring事务默认通过TransactionSynchronizationManager(基于ThreadLocal)存储上下文,可手动将主线程的上下文复制到子线程。

核心步骤:

1.主线程在开启事务后,获取TransactionSynchronizationManager中的上下文(如事务连接、事务状态)。

2.子线程启动前,将主线程的上下文手动设置到子线程的ThreadLocal中。

3.子线程执行完毕后,清除子线程的ThreadLocal(避免内存泄漏)。

风险提示:

  • 破坏Spring事务的线程隔离设计,可能导致连接泄漏、事务状态混乱。

  • 仅适用于简单场景(如单数据源、无嵌套事务),不推荐生产环境使用。

3. 方案三:重构业务逻辑,避免跨线程事务依赖

这是最推荐的方案------从设计层面消除"多线程共享事务"的需求,通过业务拆分避免事务跨线程

常见拆分思路:

  • 单线程事务+异步通知:主线程先完成核心事务(如数据入库),子线程通过异步任务(如@Async)处理非核心逻辑(如日志、通知),两者无需共享事务。

  • 分布式事务:若多线程需操作不同数据源/服务,改用分布式事务框架(如Seata、Saga),而非依赖本地事务跨线程。

  • 状态标记+补偿机制:多线程任务通过"状态字段"(如task_status)标记执行结果,主线程根据所有子线程的状态,统一进行提交/回滚(或补偿操作)。

代码示例: 实体类:

vbnet 复制代码
@Data
public class Order {
    private Long orderId;
    private String userId;
    private String productId;
    private int quantity;
    private String status;
}

dao 层:

kotlin 复制代码
@Repository
public class OrderDao {
    //模拟数据库插入动作
    public Long insertOrder(Order order) {
        // 模拟数据库插入操作
        Long orderId = System.currentTimeMillis(); // 使用当前时间戳作为订单ID
        order.setOrderId(orderId);
        System.out.println("【dao】订单已插入到数据库,订单消息: " + order);
        return orderId;
    }
}

service层:

OrderService //模拟订单的核心逻辑

kotlin 复制代码
@Service
public class OrderService {
    //模拟核心业务逻辑
    private final OrderDao orderDao;
    //注入异步通知
    private final AsyncNotificationService asyncNotificationService;

    //构造器注入
    @Autowired
    public OrderService(OrderDao orderDao, AsyncNotificationService asyncNotificationService) {
        this.orderDao = orderDao;
        this.asyncNotificationService = asyncNotificationService;
    }

    /**
     * 核心业务创建订单(单线程事务)
     */
    @Transactional(rollbackFor = Exception.class)
    public Long createOrder(String userId, String productId, int quantity) {

        try {
            // 创建订单对象
            Order order = new Order();
            order.setUserId(userId);
            order.setProductId(productId);
            order.setQuantity(quantity);
            order.setStatus("已创建");
//            int a = 1/0; // 模拟异常,测试事务回滚

            // 插入订单到数据库
            Long orderId = orderDao.insertOrder(order);
            System.out.println("【主线程事务】订单创建成功,订单 ID: " + orderId );
            // 异步发送通知

            asyncNotificationService.sendOrderNotification(orderId, userId);
            return orderId;
        } catch (Exception e) {
            // 处理异常,可能是记录日志或抛出自定义异常
            System.err.println("【主线程事务】创建订单失败, 事务回滚 错误信息: " + e.getMessage());
            throw e; // 重新抛出异常以触发事务回滚
        }
    }
}

//开启异步支持

less 复制代码
@EnableAsync
@Configuration
public class AsyncConfig {
}

异步处理非核心,无事务依赖的操作

kotlin 复制代码
@Service
public class AsyncNotificationService {
    // 发送通知
    @Async
    public void sendOrderNotification(Long orderId, String userId) {
        try {
            // 模拟发送通知的逻辑
            System.out.println("【异步线程】开始发送订单通知, 订单 ID: " + orderId + " 用户 ID: " + userId);
            sleep(1000); // 模拟耗时操作
            //模拟异步线程失败
            //int b = 1/0; // 模拟异常,测试异步线程失败
            System.out.println("【异步线程】发送订单成功,订单 ID: " + orderId );
        } catch (InterruptedException e) {
            System.err.println("【异步线程】发送订单通知失败,订单 ID: " + orderId + ",错误信息: " + e.getMessage());
        }


    }
}

//进行测试

java 复制代码
@Component
public class OrderTestRunner implements CommandLineRunner {
    private final OrderService orderService;

    // 构造器注入 OrderService
    public OrderTestRunner(OrderService orderService) {
        this.orderService = orderService;
    }
    @Override
    public void run(String... args) throws Exception {
        //测试创建订单 核心操作入库在事务中,通知在异步线程执行
        orderService.createOrder("user123", "product456", 2);
        //主线程等待2秒
        try {
            Thread.sleep(2000); // 等待异步操作完成
            System.out.println("【主线程】等待完成,异步操作可能已执行完毕。");
        } catch (InterruptedException e) {
            System.err.println("【主线程】等待中断,错误信息: " + e.getMessage());
        }
    }
}

启动application:

typescript 复制代码
@SpringBootApplication
public class RedisDataDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(RedisDataDemoApplication.class, args);
    }
}

运行后结果:

yaml 复制代码
【dao】订单已插入到数据库,订单消息: Order(orderId=1755825346090, userId=user123, productId=product456, quantity=2, status=已创建)
【主线程事务】订单创建成功,订单 ID: 1755825346090
【异步线程】开始发送订单通知, 订单 ID: 1755825346090 用户 ID: user123
【异步线程】发送订单成功,订单 ID: 1755825346090
【主线程】等待完成,异步操作可能已执行完毕。

总结

  • 优先选择方案三(重构业务),从根源避免问题,保证系统稳定性。

  • 若必须跨线程共享事务,短期可尝试方案一(编程式事务),需严格控制线程同步(如等待子线程完成)。

  • 方案二(手动传递ThreadLocal) 风险高,仅作为临时调试手段,不推荐生产使用。

相关推荐
阿杆14 分钟前
零成本 Redis 实战:用Amazon免费套餐练手 + 缓存优化
redis·后端
舒一笑44 分钟前
如何优雅统计知识库文件个数与子集下不同文件夹文件个数
后端·mysql·程序员
IT果果日记1 小时前
flink+dolphinscheduler+dinky打造自动化数仓平台
大数据·后端·flink
Java技术小馆1 小时前
InheritableThreadLoca90%开发者踩过的坑
后端·面试·github
寒士obj1 小时前
Spring容器Bean的创建流程
java·后端·spring
掉鱼的猫1 小时前
Spring AOP 与 Solon AOP 有什么区别?
java·spring
诗和远方14939562327342 小时前
iOS 异常捕获原理详解
面试
数字人直播2 小时前
视频号数字人直播带货,青否数字人提供全套解决方案!
前端·javascript·后端
shark_chili2 小时前
提升Java开发效率的秘密武器:Jadx反编译工具详解
后端
武子康2 小时前
大数据-75 Kafka 高水位线 HW 与日志末端 LEO 全面解析:副本同步与消费一致性核心
大数据·后端·kafka