尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

05-SpringBoot事务管理:声明式事务、事务传播机制

05-SpringBoot事务管理:声明式事务、事务传播机制 SpringBoot事务管理声明式事务、事务传播机制黒漂技术佬 · 2026年7月前言先给你讲个真实的事故只是化名。某天深夜无人售货柜系统的服务器报警库存扣了款没收到订单状态还是待支付。排查日志发现——扣库存的 SQL 执行成功了写订单的 SQL 也执行成功了但支付接口超时报了异常偏偏只有扣库存那步没回滚。结果是货柜上的可乐少了一瓶钱没进账财务对账对到怀疑人生。根因就一句话没开事务。事务这个词你肯定听过但到底是加个Transactional就完事那么简单吗加在哪一级异常怎么处理传播行为选哪个这篇把 SpringBoot 事务管理从入门到避坑给你讲透。一、事务是什么用大白话说数据库事务遵循ACID四大特性特性全称白话翻译Atomicity 原子性要么全做要么全不做下单这件事扣库存、扣款、写订单三条 SQL要么全成功要么全回滚Consistency 一致性操作前后数据库状态合法库存不能变成负数账户余额不能凭空多出钱Isolation 隔离性并发事务之间互不干扰A 正在下单B 也在下单同一商品各自看到各自的库存快照Durability 持久性提交了就永久生效订单提交后服务器宕机重启订单还在不是内存里的幻影Spring 的事务管理不是自己发明的它只是对 JDBC 事务的一层封装。本质还是调conn.setAutoCommit(false)、conn.commit()、conn.rollback()。但 Spring 把这套手动操作变成了注解驱动让你聚焦业务逻辑。二、Spring 事务管理的两种方式2.1 编程式事务古老但灵活AutowiredprivateTransactionTemplatetransactionTemplate;publicvoidplaceOrder(OrderCreateDTOdto){transactionTemplate.execute(status-{// 扣库存productMapper.deductStock(dto.getProductId(),dto.getQuantity());// 创建订单orderMapper.insert(orderDO);returnnull;});}优点事务边界精确可以在事务内部做复杂分支判断。缺点代码侵入性强和业务逻辑耦合在一起写着写着就一堆 try-catch可读性一塌糊涂。什么时候用极少场景——比如要在事务内根据某个中间结果决定继续还是回滚声明式事务做不到这么细粒度。2.2 声明式事务主流本篇文章的重点TransactionalpublicvoidplaceOrder(OrderCreateDTOdto){productMapper.deductStock(dto.getProductId(),dto.getQuantity());orderMapper.insert(orderDO);}一个注解搞定。Spring 在背后用的是 AOP 代理调用你的方法前它先调用TransactionManager开启事务你的方法执行完后它决定 commit 还是 rollback。全程不需要你写一行事务管理代码。99% 的场景用声明式事务就够了。三、Transactional注解详解3.1 常用属性Transactional(propagationPropagation.REQUIRED,// 传播行为isolationIsolation.REPEATABLE_READ,// 隔离级别timeout30,// 超时时间(秒)readOnlyfalse,// 是否只读rollbackFor{Exception.class},// 触发回滚的异常noRollbackFor{BusinessException.class}// 不回滚的异常)publicvoidplaceOrder(OrderCreateDTOdto){...}逐一解释propagation传播行为决定事务如何跨方法传播下面第三节细讲isolation隔离级别MySQL 默认REPEATABLE_READOracle 默认READ_COMMITTEDtimeout事务超时时间。如果一个事务跑了 30 秒还没 commitSpring 会自动帮它 rollback防止长事务占着连接不放readOnly告诉数据库这是只读操作便于数据库做优化比如 MySQL InnoDB 对只读事务不加间隙锁rollbackFor重点中的重点默认只回滚RuntimeException和Error受检异常Exception不会回滚noRollbackFor指定哪些异常不回滚比如业务异常你可以自己处理3.2 回滚规则的坑看这段代码TransactionalpublicvoidcreateOrder(OrderCreateDTOdto)throwsException{orderMapper.insert(orderDO);// 远程调用支付抛出了 IOExceptionpaymentService.pay(dto);// throws IOException}IOException是受检异常checked exceptionTransactional默认不回滚。结果订单写进去了支付失败了数据不一致。解决方案有两种// 方案一指定回滚所有异常Transactional(rollbackForException.class)// 方案二把受检异常包装成运行时异常更推荐try{paymentService.pay(dto);}catch(IOExceptione){thrownewRuntimeException(支付失败,e);}我的建议一律加上rollbackFor Exception.class然后对于确实不需要回滚的业务异常比如参数校验失败用noRollbackFor单独处理。四、事务传播行为7种但你只需要记住3种传播行为Propagation的定义当前方法被调用时它和已有事务之间的关系是什么是加入现有事务还是另开一个新事务Spring 定义了 7 种传播行为但工作中常用的就 3 种4.1 REQUIRED默认最常用Transactional(propagationPropagation.REQUIRED)publicvoidplaceOrder(OrderCreateDTOdto){// 当前有事务则加入没有则新建deductStock(dto);// 同样加了 TransactionalcreatePayment(dto);// 同样加了 Transactional}效果deductStock和createPayment都在placeOrder开启的事务中执行。任何一个子方法抛异常整个事务回滚。这就像一锅火锅所有菜都在一个锅里涮——涮坏了某道菜抛异常整个锅都要倒掉回滚。这就是原子性的体现。4.2 REQUIRES_NEW独立开新事务Transactional(propagationPropagation.REQUIRES_NEW)publicvoidlogError(ErrorLogDOlog){errorLogMapper.insert(log);// 记录错误日志绝对不能回滚}效果无论外部事务是否提交/回滚这个方法的操作都在独立的事务中执行。外部事务回滚了这条日志已经写到数据库里了。典型场景记录操作日志。用户下单失败需要回滚订单数据但错误日志必须保留好方便事后排查。4.3 NESTED嵌套事务需要数据库支持SavepointTransactional(propagationPropagation.NESTED)publicvoidprocessItem(OrderItemDTOitem){// 处理单个订单项失败只回滚这一项productMapper.deductStock(item.getProductId(),item.getQuantity());// 如果这里抛异常只回滚到 savepoint外部事务继续}效果嵌套事务失败只回滚自身不影响外部事务继续执行。但外部事务回滚时嵌套事务也会一起回滚。NESTED的实现依赖数据库的 Savepoint 机制只有支持 Savepoint 的数据库MySQL InnoDB、PostgreSQL才能用。它和REQUIRES_NEW的区别在于REQUIRES_NEW两个事务完全独立外部事务回滚不影响内部事务NESTED外部事务回滚嵌套事务也会回滚其余 4 种速览传播行为含义实际使用频率SUPPORTS有事务就加入没有就非事务执行极低NOT_SUPPORTED挂起当前事务非事务执行极低MANDATORY必须在已有事务中执行否则抛异常低少数业务要求事务上下文NEVER必须非事务执行否则抛异常几乎不用五、事务隔离级别隔离级别决定了一个事务能看到另一个并发事务的哪些数据。四种标准级别级别脏读不可重复读幻读并发性能READ_UNCOMMITTED有有有最高READ_COMMITTED无有有较高REPEATABLE_READ无无有*中等SERIALIZABLE无无无最低* MySQL InnoDB 的REPEATABLE_READ通过间隙锁Gap Lock也解决了幻读。Transactional(isolationIsolation.REPEATABLE_READ)publicvoiddeductStock(LongproductId,intqty){// 查询库存 → 判断够不够 → 扣减ProductproductproductMapper.selectById(productId);if(product.getStock()qty){thrownewBusinessException(库存不足);}productMapper.deductStock(productId,qty);}MySQL 默认REPEATABLE_READ绝大多数场景都不用改。除非你的业务对数据一致性的要求极低日志记录类或极高金融交易才需要调整。无人售货柜的订单场景REPEATABLE_READ完全够用——你不需要为了多读一个商品库存就去降级到READ_UNCOMMITTED那不叫激进叫鲁莽。六、事务失效的常见场景踩坑指南面试经典题Transactional 什么时候失效——但这里不讲面试讲真实踩过的坑。6.1 同类方法自调用ServicepublicclassOrderService{publicvoidcheckout(OrderCreateDTOdto){// 直接调用本类方法事务不生效this.placeOrder(dto);}TransactionalpublicvoidplaceOrder(OrderCreateDTOdto){// ...}}原因Spring 事务靠 AOP 代理实现。this.placeOrder()绕过了代理直接调用了原始对象的方法事务拦截器根本没机会介入。解决方案把事务方法挪到另一个 Service 里或者用AopContext.currentProxy()获取代理对象来调用。// 方案一拆到另一个 ServiceServicepublicclassTransactionOrderService{AutowiredprivateOrderPersistenceServicepersistenceService;publicvoidcheckout(OrderCreateDTOdto){persistenceService.placeOrder(dto);// 跨类调用事务生效}}// 方案二AopContext 获取代理需开启 exposeProxyEnableAspectJAutoProxy(exposeProxytrue)publicclassOrderService{publicvoidcheckout(OrderCreateDTOdto){((OrderService)AopContext.currentProxy()).placeOrder(dto);}}6.2 非 public 方法TransactionalprivatevoidplaceOrder(OrderCreateDTOdto){// private 不生效// ...}Spring 基于 JDK 动态代理或 CGLIB 代理实现事务都需要目标方法是public的。protected和包级可见也不行。如果你用的是 CGLIBprotected理论上可以但别依赖这个——统一写public。6.3 异常被吞掉TransactionalpublicvoidplaceOrder(OrderCreateDTOdto){try{paymentService.pay(dto);}catch(Exceptione){log.error(支付失败,e);// 没有把异常抛出去事务感知不到异常正常提交}orderMapper.insert(orderDO);}异常被 catch 且未重新抛出Spring 感知不到异常事务照常提交——订单写进去了款没扣到。改正要么让异常抛出要么手动标记回滚TransactionalpublicvoidplaceOrder(OrderCreateDTOdto){try{paymentService.pay(dto);}catch(Exceptione){log.error(支付失败,e);TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();thrownewRuntimeException(支付失败,e);}orderMapper.insert(orderDO);}6.4 数据库引擎不支持事务CREATETABLEt_log(idBIGINTAUTO_INCREMENTPRIMARYKEY,contentTEXT)ENGINEMyISAM;--MyISAM不支持事务MySQL 中只有 InnoDB 引擎支持事务。MyISAM 的ROLLBACK就是空气。建表时一定用ENGINE InnoDB。七、完整实战无人售货柜下单的事务设计现在把前面的知识整合成一个真实的下单场景。业务流程查询商品库存扣减库存创建订单主记录创建订单明细记录调用支付接口更新订单状态为已支付其中步骤 1-4 必须在同一个事务中——任何一个失败前面都回滚。步骤 5 的支付调用可能超时失败时订单保留为待支付状态不是回滚。步骤 6 在支付回调中完成不在下单事务内。代码实现ServicepublicclassOrderServiceImplimplementsOrderService{AutowiredprivateProductMapperproductMapper;AutowiredprivateOrderMapperorderMapper;AutowiredprivateOrderDetailMapperorderDetailMapper;AutowiredprivateErrorLogServiceerrorLogService;/** * 下单主流程 * 注意rollbackForException.class 保证受检异常也能回滚 */OverrideTransactional(rollbackForException.class,timeout10)publicOrderVOplaceOrder(OrderCreateDTOdto){// 1. 扣库存带乐观锁防超卖for(OrderItemDTOitem:dto.getItems()){intaffectedproductMapper.deductStock(item.getProductId(),item.getQuantity());if(affected0){thrownewBusinessException(商品 [item.getProductId()] 库存不足);}}// 2. 创建订单主记录OrderDOorderDOnewOrderDO();orderDO.setUserId(dto.getUserId());orderDO.setDeviceId(dto.getDeviceId());orderDO.setStatus(OrderStatus.PENDING.getCode());orderDO.setTotalAmount(calculateTotal(dto.getItems()));orderMapper.insert(orderDO);// 3. 批量插入订单明细for(OrderItemDTOitem:dto.getItems()){OrderDetailDOdetailOrderDetailDO.builder().orderId(orderDO.getId()).productId(item.getProductId()).quantity(item.getQuantity()).unitPrice(productMapper.selectById(item.getProductId()).getPrice()).build();orderDetailMapper.insert(detail);}// 4. 返回 VO此时订单为待支付returnbuildOrderVO(orderDO);}/** * 支付结果处理由支付回调触发独立事务 */Transactional(rollbackForException.class)publicvoidhandlePaymentCallback(LongorderId,StringtransactionId,booleansuccess){OrderDOorderorderMapper.selectById(orderId);if(ordernull||!order.getStatus().equals(OrderStatus.PENDING.getCode())){thrownewBusinessException(订单状态异常无法处理支付);}if(success){order.setStatus(OrderStatus.PAID.getCode());}else{// 支付失败恢复库存用独立事务不回滚不影响主逻辑restoreStock(orderId);order.setStatus(OrderStatus.CANCELLED.getCode());}orderMapper.updateById(order);}/** * 恢复库存独立事务失败不影响主逻辑 */Transactional(propagationPropagation.REQUIRES_NEW,rollbackForException.class)publicvoidrestoreStock(LongorderId){ListOrderDetailDOdetailsorderDetailMapper.selectByOrderId(orderId);for(OrderDetailDOdetail:details){productMapper.restoreStock(detail.getProductId(),detail.getQuantity());}}/** * 记录错误日志独立事务外部事务回滚时日志也保留 */Transactional(propagationPropagation.REQUIRES_NEW,rollbackForException.class)publicvoidlogError(Stringmessage,StringstackTrace){errorLogService.save(newErrorLog(message,stackTrace));}}为什么这样设计placeOrder用REQUIRED默认扣库存和写订单必须原子化要么都成功要么都回滚restoreStock用REQUIRES_NEW退款失败不应影响支付状态更新——日志里记一笔告警就行错误日志用REQUIRES_NEW外部事务可能因为业务异常回滚但错误日志必须留下来支付接口不在主事务内支付是 RPC 调用可能超时几十秒放在事务里会长时间占用连接。订单创建后立即提交事务支付独立完成八、事务监控和排查技巧线上出了事务问题怎么排查几个实用技巧1. 开启事务日志logging:level:org.springframework.jdbc.datasource:DEBUGorg.springframework.transaction:DEBUG能看到事务的开启、提交、回滚全过程。2. 检查长事务EventListenerpublicvoidhandleTransactionEvent(TransactionPhaseEventevent){longdurationevent.getDuration();if(duration5000){log.warn(事务执行超过 {} ms可能存在长事务问题,duration);}}3. Druid 监控面板的SQL 监控也能看到活跃连接数、事务时间分布非常方便。总结事务管理的核心要点就这几条声明式事务用Transactional(rollbackFor Exception.class)别依赖默认回滚规则传播行为记住三种REQUIRED凑一锅、REQUIRES_NEW另起一桌、NESTED嵌套回滚点避免同类方法自调用跨类调用才能触发 AOP 代理别吞异常catch 了又不抛事务以为一切正常就提交了长事务是大忌RPC 调用、文件上传这类操作不要放在事务内先提交事务再做隔离级别别乱调MySQL 的REPEATABLE_READ在 99% 的场景下够用事务写对了凌晨的告警电话会少一半。写错了——可能就多了个写博客素材的惨痛经历。
返回列表