
在Java后端开发里Transactional是每天都要打交道的注解。不少人对事务失效的印象还停留在「同类调用、方法私有、异常类型不匹配」这老三样直到在生产环境接连踩坑才发现真正容易引发线上问题的全是那些容易被忽略的非典型场景。今天整理了5个隐蔽性极强的事务失效场景每个都附代码复现、底层原理和可直接复用的修复代码。看完你会发现事务失效这事细节里全是坑。场景一异常被try-catch悄悄吞掉事务假装没看见踩坑现场这是最高发、也最隐蔽的事务失效场景。代码逻辑看起来天衣无缝加了事务注解方法里也抛了异常可数据库数据就是没回滚。查了半天代理、方法权限全没问题最后发现——异常被try-catch默默吃掉了。错误示例代码Service public class OrderService { Autowired private OrderMapper orderMapper; Autowired private StockMapper stockMapper; Transactional(rollbackFor Exception.class) public void createOrder(Order order) { try { // 1. 插入订单 orderMapper.insert(order); // 2. 扣减库存 stockMapper.reduce(order.getProductId(), order.getNum()); // 3. 模拟业务异常 int i 1 / 0; } catch (Exception e) { // 只打印了日志没把异常抛出去 log.error(下单失败, e); } } }失效原理Spring事务的回滚逻辑本质是通过AOP切面捕获方法抛出的异常来触发的。一旦你在方法内部用try-catch把异常捕获并消化掉了切面就感知不到异常发生自然也就不会触发回滚操作。在Spring看来这个方法是「正常执行完成」的。解决方案两种常用修复方式按需选择方案1手动标记事务回滚捕获异常后手动通知Spring当前事务需要回滚适合不想破坏上层调用链路的场景。Service public class OrderService { Autowired private OrderMapper orderMapper; Autowired private StockMapper stockMapper; Transactional(rollbackFor Exception.class) public void createOrder(Order order) { try { orderMapper.insert(order); stockMapper.reduce(order.getProductId(), order.getNum()); // 业务逻辑... int i 1 / 0; } catch (Exception e) { log.error(下单失败订单号{}, order.getOrderNo(), e); // 手动标记当前事务为回滚状态 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); } } }注意该方式仅标记回滚方法仍会正常结束标记后不可再提交新的写操作否则会抛出异常。方案2捕获后重新抛出异常推荐生产环境使用异常链路完整便于全局异常处理和日志排查。Service public class OrderService { Autowired private OrderMapper orderMapper; Autowired private StockMapper stockMapper; Transactional(rollbackFor Exception.class) public void createOrder(Order order) { try { orderMapper.insert(order); stockMapper.reduce(order.getProductId(), order.getNum()); // 业务逻辑... int i 1 / 0; } catch (Exception e) { log.error(下单失败订单号{}, order.getOrderNo(), e); // 抛出异常让Spring切面感知并触发回滚 throw new BusinessException(下单失败请稍后重试, e); } } }场景二传播级别配错事务悄悄「裸奔」了踩坑现场很多同学为了灵活配置事务会特意指定propagation传播级别但很容易踩SUPPORTS的坑。开发者以为加了注解就有事务实际上在特定调用方式下方法全程没有事务保护异常了数据也不会回滚。错误示例代码Service public class StockService { Autowired private StockMapper stockMapper; // 错误写操作使用 SUPPORTS上层无事务时自身也无事务 Transactional(propagation Propagation.SUPPORTS, rollbackFor Exception.class) public void updateStock(Long productId, Integer num) { stockMapper.reduce(productId, num); // 模拟异常 int i 1 / 0; } } // 调用方自身未开启事务 Service public class ProductService { Autowired private StockService stockService; public void updateProductStock(Long productId, Integer num) { // 无事务环境下调用 SUPPORTS 方法全程无事务 stockService.updateStock(productId, num); } }失效原理Propagation.SUPPORTS的官方含义是如果当前存在事务就加入事务如果没有事务就以非事务方式执行。很多人误以为它是「支持事务」默认会开启事务实则恰恰相反——当调用方没有事务时这个方法就直接以无事务状态运行异常了当然不会回滚。解决方案方案1写操作统一使用默认传播级别写操作强制使用REQUIREDSpring默认值可省略不写确保有事务保护。Service public class StockService { Autowired private StockMapper stockMapper; // 写操作默认 REQUIRED无事务则新建有事务则加入 Transactional(rollbackFor Exception.class) public void updateStock(Long productId, Integer num) { stockMapper.reduce(productId, num); // 业务逻辑... int i 1 / 0; } }方案2SUPPORTS 仅用于纯查询场景SUPPORTS的正确定位是优化查询性能仅在纯读方法中使用避免不必要的事务开销。Service public class StockService { Autowired private StockMapper stockMapper; // 正确用法纯查询方法使用 SUPPORTS兼顾性能与事务兼容 Transactional(propagation Propagation.SUPPORTS, readOnly true) public Stock getStockInfo(Long productId) { return stockMapper.selectByProductId(productId); } }场景三开个线程去调用事务直接失效踩坑现场为了提升接口性能把一些次要逻辑放到异步线程里执行结果发现数据库操作异常了完全不回滚。很多人排查半天代理、异常都没问题却忽略了「线程」这个关键因素。错误示例代码Service public class OrderService { Autowired private OrderMapper orderMapper; Autowired private LogMapper logMapper; Transactional(rollbackFor Exception.class) public void createOrder(Order order) throws InterruptedException { orderMapper.insert(order); // 错误1手动new线程跨线程无法共享事务 // 错误2同类调用this.方法绕过代理注解本身失效 new Thread(() - this.saveOperateLog(order)).start(); Thread.sleep(100); // 主业务异常 int i 1 / 0; } Transactional(rollbackFor Exception.class) public void saveOperateLog(Order order) { logMapper.insertLog(order); int i 1 / 0; } }失效原理这里是双重原因叠加导致事务失效1.同类调用this.saveOperateLog()直接调用本类方法绕过了Spring的代理对象注解本身就不生效2.跨线程传递失效Spring的事务信息通过ThreadLocal存储天然线程隔离。子线程无法获取主线程的事务上下文既不会加入主事务自身的事务也不会生效解决方案核心原则异步逻辑抽取到独立类通过Spring代理调用跨线程无法实现本地事务一致性异步方法独立管理自身事务。步骤1抽取异步逻辑到独立ServiceService public class OperateLogService { Autowired private LogMapper logMapper; // 异步方法独立开启自己的事务 Async(taskExecutor) Transactional(rollbackFor Exception.class) public void saveOrderLog(Order order) { logMapper.insertLog(order); // 业务逻辑... } }步骤2主线程中注入Bean调用Service public class OrderService { Autowired private OrderMapper orderMapper; Autowired private OperateLogService operateLogService; Transactional(rollbackFor Exception.class) public void createOrder(Order order) { // 主业务事务 orderMapper.insert(order); // 正确通过注入的代理Bean调用异步方法 // 注意主线程与异步线程事务相互独立不会一起回滚 operateLogService.saveOrderLog(order); // 主业务逻辑... int i 1 / 0; } }重要提醒本地事务无法跨线程传递。如果需要强一致性不要用异步做写操作确需跨服务/跨线程一致性请引入Seata等分布式事务方案。场景四表引擎选错了写再多注解也白搭踩坑现场这个坑在老项目迁移里格外常见。新功能写完测试怎么测事务都不回滚代码翻了三遍都找不到问题。最后连上数据库一看——表用的是 MyISAM 引擎。失效原理Transactional再强大也是基于数据库本身的事务能力实现的。- MySQL 的 MyISAM 存储引擎天生不支持事务只支持表锁- 只有 InnoDB 引擎才支持事务、行锁和外键代码层面无论配置得多完美数据库底层不支持一切都是空谈。这种问题隐蔽性极强因为代码不会报任何错只是事务默默不生效。解决方案附完整SQL1. 单张表修改引擎-- 修改单个表为InnoDB引擎 ALTER TABLE t_order ENGINE InnoDB;2. 批量查询当前库所有非InnoDB表-- 查询当前数据库中所有MyISAM引擎的表 SELECT TABLE_NAME, ENGINE FROM information_schema.TABLES WHERE TABLE_SCHEMA 你的数据库名 AND ENGINE ! InnoDB;3. 建表规范写法建表时显式指定CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(64) NOT NULL COMMENT 订单号, amount DECIMAL(10,2) NOT NULL COMMENT 订单金额, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE InnoDB DEFAULT CHARSET utf8mb4 COMMENT 订单表;场景五只读事务里写数据事务直接「罢工」踩坑现场很多同学知道readOnly true可以优化查询性能就随手给一些「看起来是查询」的方法加上。结果方法里有少量写操作时要么直接报错要么事务行为和预期不符。错误示例代码Service public class OrderService { Autowired private OrderMapper orderMapper; // 错误只读事务中包含写操作 Transactional(readOnly true, rollbackFor Exception.class) public OrderVO getOrderDetail(Long orderId) { Order order orderMapper.selectById(orderId); // 写操作更新查看次数 orderMapper.updateViewCount(orderId); return convert(order); } }失效原理readOnly true不是一个「优化提示」这么简单它会将数据库连接设置为只读模式。在只读事务中执行INSERT、UPDATE、DELETE等写操作数据库会直接抛出错误部分场景下还会导致事务状态异常出现不回滚、提交失败等问题。很多人踩坑就是因为方法主体是查询夹杂了一两句更新操作又忘了去掉只读配置排查时很难联想到是这个参数的锅。解决方案核心思路读写职责拆分纯查询用只读事务写操作单独走普通事务。Service public class OrderService { Autowired private OrderMapper orderMapper; // 纯查询使用只读事务优化性能 Transactional(readOnly true) public OrderVO getOrderDetail(Long orderId) { Order order orderMapper.selectById(orderId); return convert(order); } // 写操作独立方法普通事务 Transactional(rollbackFor Exception.class) public void incrementViewCount(Long orderId) { orderMapper.updateViewCount(orderId); } // 上层调用按需组合 public OrderVO queryDetailAndAddView(Long orderId) { // 先更新浏览量 incrementViewCount(orderId); // 再查询详情 return getOrderDetail(orderId); } }实践建议绝大多数普通业务场景下单表查询的只读优化感知极弱不要为了「看起来专业」盲目加readOnly反而容易引入坑。最后事务失效速查清单遇到事务不生效别上来就瞎改代码按这个顺序排查99%的问题都能定位排查顺序检查项常见坑点1方法是不是public、是不是同类调用最经典的AOP代理绕过问题2异常有没有被try-catch吞掉最高发的非典型场景3传播级别是不是配错了SUPPORTS、NOT_SUPPORTED 易踩坑4异常类型是不是匹配默认只回滚RuntimeException5是不是跨线程调用ThreadLocal 事务不共享6数据库表引擎是不是InnoDB历史遗留表容易中招7是不是只读事务里执行了写操作readOnly 配置误用开发做久了会发现很多技术点不在于难而在于细。事务这个东西入门觉得简单真正在生产环境摸爬滚打下来才会发现处处都是细节坑。希望这篇文章能帮你避开几个生产事故。你们还遇到过什么奇葩的事务失效场景欢迎评论区一起聊聊