Spring 事务管理与数据一致性实践

Spring 事务管理与数据一致性实践 Spring 事务管理与数据一致性实践一、什么是事务1.1 基本概念事务Transaction是一组操作的集合这些操作要么全部成功要么全部失败回滚不存在部分成功部分失败的中间状态。类比理解银行转账A 向 B 转 100 元 操作1A 账户 -100 操作2B 账户 100 如果操作1成功但操作2失败 没有事务 → A 少了100B 没收到钱丢了 有事务 → 全部回滚A 和 B 余额都不变1.2 ACID 四大特性特性英文含义类比原子性Atomicity事务中所有操作要么全部完成要么全部不完成一个开关控制一排灯要么全亮要么全灭一致性Consistency事务前后数据满足业务规则如余额不能为负转账后两人总金额不变隔离性Isolation多个事务并发执行时互不干扰两个柜台同时办业务不会串账持久性Durability事务提交后数据永久保存即使宕机也不丢失转账成功后银行断电记录仍在注博客https://blog.csdn.net/badao_liumang_qizhi二、Spring 事务管理核心概念2.1 声明式事务TransactionalSpring 通过Transactional注解实现声明式事务管理。方法执行前开启事务正常返回则提交抛出异常则回滚。Transactional(rollbackForException.class)publicvoidtransferMoney(IntegerfromId,IntegertoId,BigDecimalamount){accountRepository.deduct(fromId,amount);// 操作1accountRepository.add(toId,amount);// 操作2// 如果操作2抛异常 → 操作1也会回滚}2.2 事务传播行为Propagation当一个事务方法调用另一个事务方法时如何处理事务边界传播行为含义使用场景REQUIRED默认有事务就加入没有就新建大多数场景REQUIRES_NEW总是新建事务暂停外层事务独立操作一条失败不影响其他NESTED嵌套事务可以独立回滚到保存点部分操作可回滚但不影响外层SUPPORTS有事务就加入没有就非事务执行查询方法NOT_SUPPORTED挂起当前事务非事务执行日志记录等不需要事务的操作MANDATORY必须在事务中否则抛异常强制要求调用方有事务NEVER不能在事务中否则抛异常防止意外被事务包裹2.3 回滚规则配置说明rollbackFor Exception.class任何异常都回滚推荐默认行为只有 RuntimeException 和 Error 才回滚noRollbackFor XxxException.class指定异常不回滚三、事务一致性的常见问题与解决方案3.1 问题1事务内发 MQ 消息❌ 错误做法 Transactional public void createOrder(OrderDto dto) { orderRepository.save(order); // 写数据库 mqSender.send(order.getId()); // 发MQ通知下游 // 如果后面还有操作抛异常 → 数据库回滚了但MQ已发出 // 下游收到消息去查订单 → 查不到数据已回滚 }解决方案事务提交后再发 MQTransactionalpublicvoidcreateOrder(OrderDtodto){orderRepository.save(order);// 注册事务提交后动作AfterTransactionActionCollectorcollectornewAfterTransactionActionCollector();TransactionSynchronizationManager.registerSynchronization(collector);collector.addCommitSyncAction(()-mqSender.send(order.getId()));// MQ 在事务提交后才发出下游一定能查到数据}3.2 问题2事务内释放分布式锁❌ 错误做法 Transactional public void deductStock(Integer itemId) { lock.tryLock(); stockRepository.deduct(itemId, 1); // 扣库存 lock.unlock(); // 释放锁 // 事务还没提交另一个线程获取锁后读到的是未提交数据 }解决方案事务提交后再释放锁TransactionalpublicvoiddeductStock(IntegeritemId){lock.tryLock();stockRepository.deduct(itemId,1);// 事务提交后再释放锁AfterTransactionActionCollectorcollectornewAfterTransactionActionCollector();TransactionSynchronizationManager.registerSynchronization(collector);collector.addCommitSyncAction(lock::unlock);// 其他线程获取锁后一定读到已提交的数据}3.3 问题3批量操作部分失败❌ 错误做法 Transactional public void batchProcess(ListOrderDto orders) { for (OrderDto order : orders) { processOrder(order); // 第3条失败 → 前2条也回滚 } }解决方案每条记录独立事务publicvoidbatchProcess(ListOrderDtoorders){for(OrderDtoorder:orders){try{processOrderInNewTransaction(order);// 独立事务}catch(Exceptione){log.warn(处理失败: {},order.getId());// 继续处理下一条}}}Transactional(propagationPropagation.REQUIRES_NEW)publicvoidprocessOrderInNewTransaction(OrderDtoorder){// 独立事务失败只回滚自己processOrder(order);}3.4 问题4调用外部接口后事务回滚❌ 错误做法 Transactional public void confirmDelivery(DeliveryDto dto) { deliveryRepository.save(delivery); // 写数据库 experienceCloudApi.sendOrder(delivery); // 调外部接口A retailApi.updateOrder(delivery); // 调外部接口B // 如果调外部接口B失败 → 数据库回滚了但外部接口A已收到订单 }解决方案外部调用放在事务提交后TransactionalpublicvoidconfirmDelivery(DeliveryDtodto){deliveryRepository.save(delivery);// 写数据库事务内AfterTransactionActionCollectorcollectornewAfterTransactionActionCollector();TransactionSynchronizationManager.registerSynchronization(collector);collector.addCommitSyncAction(()-{experienceCloudApi.sendOrder(delivery);// 事务提交后再调});}或者先调外部接口失败则不写数据库TransactionalpublicvoidconfirmDelivery(DeliveryDtodto){deliveryRepository.save(delivery);// 调体验云在事务内失败则整体回滚StringresultexperienceCloudApi.sendOrder(delivery);if(!Y.equals(result)){thrownewRuntimeException(体验云下发失败);// 触发回滚}}四、通用示例代码4.1 基本事务原子性保证ServicepublicclassOrderService{ResourceprivateOrderRepositoryorderRepository;ResourceprivateStockRepositorystockRepository;ResourceprivatePaymentRepositorypaymentRepository;/** * 创建订单原子性三张表要么都写成功要么都不写. */Transactional(rollbackForException.class)publicvoidcreateOrder(CreateOrderDtodto){// 1. 创建订单主表OrderordernewOrder();order.setOrderCode(generateOrderCode());order.setAmount(dto.getAmount());orderRepository.save(order);// 2. 扣减库存intaffectedstockRepository.deduct(dto.getItemId(),dto.getQty());if(affected0){thrownewRuntimeException(库存不足);// → 触发回滚订单主表也不会写入}// 3. 创建支付记录PaymentpaymentnewPayment();payment.setOrderId(order.getId());payment.setAmount(dto.getAmount());paymentRepository.save(payment);// 三张表要么全成功要么全回滚}}4.2 独立事务REQUIRES_NEWServicepublicclassBatchDeliveryService{ResourceprivateDeliveryServicedeliveryService;/** * 批量发货每条独立事务一条失败不影响其他. */publicStringbatchConfirmDelivery(ListDeliveryParamsDtoparamsList){StringBuildererrorsnewStringBuilder();for(DeliveryParamsDtoparams:paramsList){try{// 每条记录独立事务deliveryService.confirmDeliveryInNewTransaction(params);}catch(Exceptione){// 记录失败信息继续处理下一条errors.append(发货单).append(params.getOrderCode()).append(失败:).append(e.getMessage()).append(;);}}returnerrors.toString();// 空字符串表示全部成功}}ServicepublicclassDeliveryService{ResourceprivateDeliveryRecordMasterRepositorymasterRepository;ResourceprivateDeliveryRecordMasterSubtableRepositorysubtableRepository;ResourceprivateExperienceCloudApiexperienceCloudApi;/** * 单条确认发货独立事务. * propagation REQUIRES_NEW即使外层有事务也新建独立事务. */Transactional(propagationPropagation.REQUIRES_NEW,rollbackForException.class)publicvoidconfirmDeliveryInNewTransaction(DeliveryParamsDtoparams){// 1. 创建发货单主表DeliveryRecordMastermasternewDeliveryRecordMaster();master.setOrderCode(params.getOrderCode());master.setDeliveryWay(params.getDeliveryWay());masterRepository.saveAndFlush(master);// 2. 创建发货单扩展表DeliveryRecordMasterSubtablesubtablenewDeliveryRecordMasterSubtable();subtable.setDeliveryRecordId(master.getId());subtable.setIsFixedDeliveryDate(params.getIsFixedDeliveryDate());subtableRepository.saveAndFlush(subtable);// 3. 调外部系统接口在事务内失败则整体回滚StringresultexperienceCloudApi.sendOrder(master);if(!Y.equals(result)){// 抛异常 → 本条事务回滚主表扩展表都不会写入// 但不影响批量中的其他记录thrownewRuntimeException(发货单:master.getOrderCode()传体验云失败);}}}4.3 事务提交后执行AfterTransactionActionCollectorServicepublicclassDeliveryConfirmService{ResourceprivateDeliveryRepositorydeliveryRepository;ResourceprivateMqSendermqSender;ResourceprivateDistributedLockProviderlockProvider;/** * 确认发货 事务提交后发MQ 事务提交后释放锁. */Transactional(rollbackForException.class)publicvoidconfirmDelivery(ConfirmDeliveryDtodto){// 获取分布式锁StringlockKeydelivery_dto.getOrderCode();DistributedLocklocklockProvider.getLock(lockKey,TimeUnit.MINUTES,5);lock.tryLock(TimeUnit.MINUTES,3);// 注册事务同步器AfterTransactionActionCollectorcollectornewAfterTransactionActionCollector();TransactionSynchronizationManager.registerSynchronization(collector);// 事务内操作 // 1. 创建发货单DeliveryRecorddeliverynewDeliveryRecord();delivery.setOrderCode(dto.getOrderCode());delivery.setStatus(CREATED);deliveryRepository.save(delivery);// 2. 扣减库存stockRepository.deduct(dto.getItemId(),dto.getQty());// 注册事务提交后动作 // 事务提交后发MQ保证消费者能查到已提交的数据collector.addCommitSyncAction(()-{mqSender.sendDeliveryCreatedMessage(delivery.getId());});// 事务提交后释放锁保证其他线程读到已提交数据collector.addCommitSyncAction(lock::unlock);// 注册事务回滚后动作 collector.addRollbackSyncAction(()-{lock.unlock();// 回滚也要释放锁log.warn(发货确认回滚释放锁: {},lockKey);});}}4.4 事务回滚场景演示ServicepublicclassPaymentService{/** * 支付 调第三方 补偿. */Transactional(rollbackForException.class)publicvoidprocessPayment(PaymentDtodto){AfterTransactionActionCollectorcollectornewAfterTransactionActionCollector();TransactionSynchronizationManager.registerSynchronization(collector);// 1. 调第三方扣款已扣钱StringpaymentIdthirdPartyPayApi.charge(dto.getAmount());// 2. 保存支付记录到数据库paymentRepository.save(newPayment(paymentId,dto));// 3. 更新订单状态orderRepository.updateStatus(dto.getOrderId(),PAID);// 假设这里抛异常 → 事务回滚 → 但第三方已经扣款了// 注册回滚补偿 collector.addRollbackSyncAction(()-{// 事务回滚后调第三方退款thirdPartyPayApi.refund(paymentId);log.warn(支付回滚已调退款: {},paymentId);});// 注册提交后通知 collector.addCommitSyncAction(()-{notificationService.sendPaymentSuccess(dto.getUserId());});}}4.5 saveAndFlush 与 save 的区别ServicepublicclassSubtableService{/** * 演示 saveAndFlush 的必要性. */Transactional(rollbackForException.class)publicvoidcreateDeliveryWithSubtable(DeliveryDtodto){// 1. 保存主表DeliveryMastermasternewDeliveryMaster();master.setOrderCode(dto.getOrderCode());masterRepository.saveAndFlush(master);// saveAndFlush立即执行 INSERT SQL 并获取自增 ID// 如果用 saveID 可能还没生成下面引用 master.getId() 可能为 null// 2. 保存扩展表需要主表IDDeliverySubtablesubtablenewDeliverySubtable();subtable.setDeliveryRecordId(master.getId());// 依赖主表IDsubtable.setIsFixedDeliveryDate(dto.getIsFixedDeliveryDate());subtableRepository.saveAndFlush(subtable);// 3. 后续逻辑查询扩展表DeliverySubtablesavedsubtableRepository.findByDeliveryRecordId(master.getId());// saveAndFlush 保证这里能查到刚写入的数据// 如果用 saveJPA 可能延迟 flush这里可能查不到}}4.6 乐观锁防止并发更新/** * 实体基类包含乐观锁版本号. */MappedSuperclasspublicclassBaseEntity{IdGeneratedValue(strategyGenerationType.IDENTITY)privateIntegerid;Version// JPA 乐观锁注解privateIntegerversion;privateDatecreateTime;privateDateupdateTime;}/** * 使用示例并发更新库存. */ServicepublicclassStockService{Transactional(rollbackForException.class)publicvoiddeductStock(IntegeritemId,Integerqty){StockstockstockRepository.findByItemId(itemId);if(stock.getQty()qty){thrownewRuntimeException(库存不足);}stock.setQty(stock.getQty()-qty);stockRepository.save(stock);// save 时 JPA 会检查 version// UPDATE stock SET qty?, versionversion1 WHERE id? AND version?// 如果 version 不匹配被其他线程修改过→ 抛 OptimisticLockException// → 事务回滚调用方可重试}}五、事务相关的常见陷阱5.1 同类方法调用事务失效ServicepublicclassOrderService{// ❌ 事务不生效同类内部调用绕过了 Spring AOP 代理publicvoidcreateOrder(OrderDtodto){this.doCreate(dto);// 直接调用不走代理}TransactionalpublicvoiddoCreate(OrderDtodto){// 这里的 Transactional 不会生效}}// ✅ 正确做法通过另一个 Bean 调用ServicepublicclassOrderService{ResourceprivateOrderTransactionServicetransactionService;publicvoidcreateOrder(OrderDtodto){transactionService.doCreate(dto);// 通过代理调用事务生效}}ServicepublicclassOrderTransactionService{TransactionalpublicvoiddoCreate(OrderDtodto){// 事务正常生效}}5.2 异常被 catch 吞掉导致不回滚// ❌ 事务不回滚异常被 catch 了Transactionalpublicvoidprocess(){try{repository.save(entity);riskyOperation();// 抛异常}catch(Exceptione){log.error(失败,e);// 异常被吞掉Spring 认为方法正常返回 → 不回滚}}// ✅ 正确做法catch 后重新抛出或手动标记回滚Transactionalpublicvoidprocess(){try{repository.save(entity);riskyOperation();}catch(Exceptione){log.error(失败,e);throwe;// 重新抛出 → 事务回滚// 或者TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}}5.3 REQUIRES_NEW 的使用注意ServicepublicclassOuterService{ResourceprivateInnerServiceinnerService;TransactionalpublicvoidouterMethod(){outerRepository.save(entity1);// 外层事务// 调用独立事务方法innerService.innerMethod();// 新事务提交或回滚不影响外层outerRepository.save(entity2);// 继续外层事务// 如果这里失败 → 外层回滚entity1 回滚但 innerMethod 已提交不回滚}}ServicepublicclassInnerService{Transactional(propagationPropagation.REQUIRES_NEW)publicvoidinnerMethod(){innerRepository.save(entity);// 独立事务// 提交后即使外层回滚这里的数据也不会丢失}}六、关键设计总结场景解决方案Spring 实现多表写入原子性同一事务内操作Transactional批量操作部分失败每条独立事务Propagation.REQUIRES_NEW事务后发 MQ提交后执行TransactionSynchronization.afterCommit()事务后释放锁提交后执行AfterTransactionActionCollector外部调用失败补偿回滚后执行AfterTransactionActionCollector.addRollbackSyncAction()并发更新冲突乐观锁Version字段确保数据立即可查立即刷新saveAndFlush()调外部失败回滚事务内调用失败抛异常异常触发回滚