
1. 事务管理不是八股文是一个生产事故换来的教训1.1 一个凌晨两点半的线上事故先说个我亲身经历的事故。那是一个支付类系统的割接日凌晨两点半运维电话直接把我从床上拽起来用户充值之后余额变了但充值流水没生成客服工单已经堆了十几条。我打开数据库一看用户账户表确实多了一笔钱但对应的流水表里空空如也。代码逻辑很清晰余额更新、流水插入、通知下游三步都在同一个 Service 方法里。问题就出在这这个方法当时没有加任何事务控制。第一步 update 成功之后第二步 insert 因为一个字段长度超限抛了 SQLException后续逻辑全部中断。最终数据库里只剩一笔孤零零的余额变动账对不上用户投诉运营抓狂。后来我给这个方法补上了Transactional同时排查了异常处理逻辑才把这类问题压下去。但这件事给我的触动很大我们天天写 CRUD天天聊 ACID可真到了线上能把手里的Transactional讲清楚、配置对、排查快的人其实没那么多。这篇文章就围绕事务管理展开从 ACID 理论讲到 Spring 事务的落地实现再讲到实际生产里的踩坑记录。我把这些年处理事务问题的经验、工具选型思路、排查手法都整理出来希望对正在学习 Spring 的读者或者正在被线上事务问题折磨的同行有一点实质帮助。1.2 事务管理到底在解决什么问题事务的本质很简单一组数据库操作要么全部成功要么全部失败。但真正把它放到并发环境、异常环境、分布式环境里事情就复杂了。举个例子你去银行转账扣你账户 100 块、加对方账户 100 块这两步必须同时成功或者同时失败。如果只扣不加银行天天亏损只加不扣用户就能无限套利。这是最经典的“原子性”场景。但除了故障场景还有并发场景。两个人同时抢购最后一件商品库存只有 1却卖出去 2 单这就是事务隔离性没处理好。再比如同一时间有人读数据、有人写数据读的人看到了写入一半的状态那业务就乱了套。所以事务管理管的是两件事一是故障发生时不留下半成品数据二是并发执行时不产生脏乱差的结果。Spring 在这里扮演的角色不是重新发明了一套事务机制而是把 JDBC、JPA、JTA 这些底层事务能力抽象出一套统一 API再通过 AOP 把事务逻辑织入你的业务方法。你写代码时只需要一个注解Spring 在运行时帮你完成开启事务、执行 SQL、提交或回滚这一整套流程。理解这层抽象是使用 Spring 事务的前提。1.3 这篇文章适合谁看如果你是刚接触 Spring Boot 的初学者建议从第 2 章 ACID 开始把基础打牢如果你已经写了两年 CRUD但被事务传播行为、事务失效问题折磨过可以直接跳到第 3 章和第 6 章那里有大量实操总结如果你正在做订单、支付、库存这类高一致性要求的系统第 5 章的订单场景完整落地和第 6 章的失效排查应该能帮你少走不少弯路。我不打算写成教科书式的概念堆砌而是尽量还原真实开发中的决策过程和排查过程把每个关键选择的“为什么”讲清楚。现在先从理论根基 ACID 讲起。2. 把 ACID 讲透这四个字母分别挡住什么灾难2.1 原子性要么全做要么全不做原子性Atomicity是事务最基础的保证。它解决的是“操作中途失败”的问题。数据库实现原子性的手段是undo log回滚日志事务执行过程中每修改一条数据先把修改前的旧值记录到 undo log 里如果事务中途失败就根据 undo log 把数据还原到修改前的状态。生活化的类比是网购下单你提交订单、扣减库存、生成支付单任何一个环节失败都不应该让系统里留下“订单不存在但库存少了”的状态。数据库不是靠“预测未来”来保证原子性而是靠“反悔重来”记住旧值随时回滚。但这里有个容易忽视的点原子性的边界只覆盖数据库操作。如果你的业务方法里既有数据库操作又有调用外部接口、发送 MQ 消息、写文件这些动作数据库事务管不到这些“外部副作用”。比如你扣了库存发了短信然后数据库回滚了短信已经发出去了用户收到一条“购买成功”的通知但订单实际不存在。这就是事务边界设计的问题后面我会细说。2.2 一致性业务规则的守护者一致性Consistency是四个特性里最抽象、也最容易被误解的一个。数据库层面的“一致性”指的是事务执行前后数据必须满足所有预定义的约束包括主键唯一、外键有效、字段非空、CHECK 约束等。如果某个 INSERT 违反约束整个事务直接回滚数据库绝不会留下违反规则的数据。但业务上的一致性远不止约束这么简单。转账场景里“钱的总量守恒”是一个数据库约束感知不到的规则它需要开发者在代码里保证。所以业内常有一句话数据库保证 AIDC 要靠业务自己写。原子性、隔离性、持久性都是数据库机制层面的承诺唯独一致性一部分靠数据库约束兜底一部分靠应用层逻辑保障。我在实际开发中对一致性的理解是在动手写事务之前先把业务规则列成清单比如“余额不能为负”“库存不能超卖”“订单金额等于商品单价乘以数量”然后决定哪些用数据库约束实现哪些用代码校验实现哪些放到事务提交后再做对账。这样设计出来的事务才不是只管“回不回滚”的空壳。2.3 隔离性并发窗口下的数据边界隔离性Isolation解决的是多个事务同时执行时彼此之间能“看到”多少对方未提交数据的问题。业界按隔离程度从低到高划分了四种隔离级别读未提交、读已提交、可重复读、串行化。隔离级别越低并发性能越好但脏读、不可重复读、幻读等问题越容易发生。很多人把隔离性当作 ACID 里“必须严格满足”的一条但主流数据库默认都没有选最高的串行化这说明隔离性是一种折中方案业务可以主动放宽隔离要求换取更高的并发吞吐。具体怎么选下一章详细说。这里先记住核心隔离性不是越高越好而是刚好满足业务需求最好。2.4 持久性落盘才算数持久性Durability保证已提交的事务不会因为宕机、断电而丢失。数据库依赖redo log重做日志实现事务提交时把修改操作记录到 redo log 并强制刷盘一旦 redo log 落盘即使数据页还没来得及写回磁盘系统重启后也能靠 redo log 恢复事务结果。这里有个工程上的权衡点redo log刷盘策略可以配置。MySQL 的innodb_flush_log_at_trx_commit参数设为 1每条事务提交都刷盘最安全但性能损耗大设为 0 或 2由系统定时刷盘性能好但极端情况下可能丢失最近一秒或更长时间的数据。业务系统选哪种取决于数据的重要程度和性能压测结果。我自己做支付类系统时绝不敢把innodb_flush_log_at_trx_commit调成 0宁愿多花点磁盘 IO 成本。但换成日志、埋点这类允许少量丢失的数据调成 2 能明显提升吞吐。理解持久性的这层工程含义比死记“提交后不会丢失”这句话有用得多。3. 从 ACID 往前走隔离级别与传播行为怎么选3.1 隔离级别不是背诵词典是取舍策略四种隔离级别对应的问题大多数人都能手写出来读未提交会有脏读读已提交避免了脏读但不可重复读依然存在可重复读进一步解决不可重复读但幻读要靠间隙锁或串行化解决串行化彻底杜绝三类问题但并发能力断崖式下跌。我摘录一个常用的对照表方便你直接参考隔离级别脏读不可重复读幻读典型实现数据库读未提交 (READ_UNCOMMITTED)可能可能可能极少使用读已提交 (READ_COMMITTED)避免可能可能Oracle 默认、SQL Server 默认可重复读 (REPEATABLE_READ)避免避免可能MySQL InnoDB 通过间隙锁基本避免MySQL 默认串行化 (SERIALIZABLE)避免避免避免并发吞吐最低光背表没用得理解场景。说个最常见的问题在 MySQL 默认的可重复读级别下两个事务同时操作同一张表A 事务先查了一遍满足条件的订单B 事务插入了一条新订单并提交A 事务再查一遍查出来的结果集和第一次一样不会出现幻读。这是 InnoDB 的间隙锁Gap Lock在起作用可重复读级别下InnoDB 会给查询范围加锁阻止其他事务在这个范围内插入新数据。但注意间隙锁只对 InnoDB 的特定索引范围查询生效。如果你的查询条件没有走索引锁会升级为表锁性能影响很大。所以很多老 DBA 会强调事务内的查询尽量走索引不仅是查询性能问题也关系到锁的范围。3.2 事务传播行为同一个方法怎么套进另一个事务隔离级别解决的是“事务和事务并行执行”的问题。传播行为解决的是“事务和方法嵌套调用”的问题。Spring 定义了 7 种传播级别其中 3 种就够覆盖绝大多数业务场景剩下 4 种更多是特殊场景的补充。先看最常用的REQUIRED如果当前没有事务就新建一个如果当前已经存在事务就加入当前事务。这是 Spring 的默认值。一个 Service 方法调另一个 Service 方法默认两个方法在同一个事务里只要有一个方法抛异常所有操作全部回滚。再看REQUIRES_NEW无论如何都新建一个独立事务外层事务的回滚不影响内层事务已经提交的结果。这个传播级别适合什么场景最典型的是“记录审计日志”和“发消息”。主业务失败了但“失败日志”本身要记录下来这个日志不能跟着主事务一起回滚。另一个场景是“扣减库存并发送库存变更消息”消息发送必须成功如果和主业务放同一个事务主业务回滚会把消息连坐回滚导致下游永远不知道发生了什么。NESTED和REQUIRES_NEW经常被混淆。NESTED不是新开事务而是基于数据库的保存点Savepoint机制在已有事务内部画出一个“子事务区域”。子事务回滚不会影响外层事务但外层事务一旦最终回滚所有子事务的操作一起回滚。REQUIRES_NEW则是彻底开新事务外层事务回滚不影响已经提交的内层事务。区别一句话总结NESTED是“局部反悔”REQUIRES_NEW是“独立人生”。我把三种常用传播级别整理成一张速查表传播级别当前无事务时当前有事务时典型使用场景REQUIRED新建事务加入当前事务默认选择绝大多数业务场景REQUIRES_NEW新建事务挂起当前事务新建独立事务审计日志、MQ 发送、外部调用NESTED新建事务基于保存点嵌套分段回滚例如批量导入失败时跳过坏数据4. Spring 事务实现剖析从 TransactionManager 到 Transactional4.1 事务管理器Spring 事务架构的地基Spring 事务的顶层抽象是PlatformTransactionManager接口它只定义了三个方法getTransaction、commit、rollback。不管底层是 MySQL、Oracle、PostgreSQL还是 JPA、MyBatis、JTA上层代码面对的都是同一个接口。这就是 Spring 事务设计的高明之处事务管理器的差异被封装在具体实现类里业务代码无感知。按数据源和持久层框架Spring 提供了几种常见实现。DataSourceTransactionManager是最常用的一种它基于java.sql.Connection管理事务配合 MyBatis、JdbcTemplate 使用。底层流程是从数据源获取连接设置autoCommit为 false事务提交时connection.commit()回滚时connection.rollback()。这里有个关键细节事务期间所有数据库操作必须使用同一个连接Spring 通过TransactionSynchronizationManager把连接绑定到当前线程保证一个线程内多次数据库操作共享同一个事务连接。JpaTransactionManager针对 JPA/Hibernate管理的是EntityManager的事务。JtaTransactionManager则用于跨多个数据源的分布式事务它依赖 Java EE 容器的 JTA 实现能协调多个数据库资源处于同一个全局事务下。但从实际落地看JTA 在微服务架构下并不好用性能损耗大、协调复杂所以现在跨库业务更多采用最终一致性方案比如本地消息表 对账任务而不是强一致性的分布式事务。4.2 Transactional 背后那套 AOP 机制当我还在用 SSH 框架写代码那会儿事务是写在 XML 配置里的。Spring Boot 时代一个Transactional注解几乎成了标配。但要真正理解这个注解为什么失效、什么时候生效必须明白它背后的 AOP 原理。Transactional的核心机制是动态代理。Spring 在启动时会扫描带有Transactional的 Bean为它们生成代理对象。当你调用目标方法时实际执行的是代理对象里的拦截逻辑开启事务、执行目标方法、根据异常情况提交或回滚、最后关闭事务。这个过程对业务代码完全透明。Spring 默认的代理方式是 JDK 动态代理要求目标类实现接口如果目标类没有接口则自动切换到 CGLIB 代理通过生成子类实现。这正是引出一个经典大坑的前提如果目标方法被 private 修饰或者目标类被 final 修饰代理机制就无法生效。CGLIB 无法继承 final 类无法覆写 private 方法所以Transactional静默失效。Spring 的事务拦截器是TransactionInterceptor。方法执行前它通过TransactionAspectSupport读取注解上的属性比如传播级别、隔离级别、回滚规则然后调用TransactionManager.getTransaction()开启事务方法正常返回后调用commit()提交事务方法抛出异常后根据回滚规则决定是否调用rollback()。这里有一条很多新手踩过的规则默认情况下只有抛出 RuntimeException 或 Error 才会触发回滚受检异常Checked Exception不会回滚。原因也简单Spring 认为受检异常代表业务可处理的异常情况业务代码 catch 住之后还可以正常工作只有运行时异常才代表系统级故障。但实际情况中很多团队的业务异常喜欢继承 Exception这就要在注解上显式声明rollbackFor。4.3 三级缓存和事务代理的关系这里顺带回应一下很多人在学 Spring 时困惑的问题Spring 三级缓存和事务代理有什么关系三级缓存解决的是循环依赖问题也就是 Bean A 依赖 Bean BBean B 又依赖 Bean A。Spring 在处理这种情况时会提前暴露一个早期的对象引用让双方都能完成注入。问题在于如果 Bean A 需要事务增强生成代理对象提前暴露的对象是原始 Bean 还是代理 BeanSpring 的处理方式是在三级缓存里存放一个ObjectFactory如果 Bean 上有切面就从工厂里取出代理对象如果没有切面就直接返回原始 Bean。所以事务代理和循环依赖是有交集的行为也受代理创建时机的影响。理解了这一层你再看一些项目里“循环依赖 事务失效”并发出现的问题排查思路会清晰很多。4.4 事务的真实时序从连接获取到资源清理展开一个事务方法的完整生命线我认为比背诵源码更重要。假设你调用了userService.createUser()这个方法标了Transactional底层走的是DataSourceTransactionManager。完整时序大概是代理对象拦截方法调用TransactionInterceptor开始处理。调用DataSourceUtils.getConnection(dataSource)从连接池拿一个连接。将连接的autoCommit设为 false保存原来的 autoCommit 值。把连接绑定到TransactionSynchronizationManager的线程局部变量里。执行目标方法你的 SQL 通过 MyBatis/JdbcTemplate 执行时拿到的是同一个绑定连接。方法正常返回调用connection.commit()提交事务。还原 autoCommit 值从线程局部变量解绑连接归还连接池。这里面最容易出问题的就是第 5 步。如果目标方法内部开启了新线程去执行数据库操作新线程拿不到主线程绑定的连接那个子线程的 SQL 就不在事务里。所以我一直跟团队强调事务方法内部严禁开新线程处理事务性数据操作异步任务要么用REQUIRES_NEW独立管理事务要么放到事务提交后再执行。5. 实操落地一个电商订单场景的完整事务实现5.1 场景设定与表结构设计为了把理论串起来我设计了一个贴近真实业务的场景用户购买商品后端需要同时完成“扣减用户余额”“扣减商品库存”“创建订单主表”“插入订单明细”四件事。任何一步失败都不能留下余额扣了但订单没建成的状态。核心表可以抽象成四张user_account用户余额核心字段balance。product_stock商品库存核心字段stock。order_info订单主表记录订单号、用户 ID、总金额、状态。order_item订单明细表记录每个商品的 ID、数量、单价。这是一个典型的单体应用内事务场景用DataSourceTransactionManager配合 MySQL InnoDB、MyBatis 就能满足需求不需要引入分布式事务。建表时有个细节所有涉及金额、库存的字段都用精确数值类型DECIMAL(12, 2)不要用FLOAT或DOUBLE浮点数在金额计算里会埋下精度炸弹。5.2 写一个功能完整的事务 Service核心方法实现如下我贴的是简化但可运行的骨架Service public class OrderServiceImpl implements OrderService { Resource private UserAccountMapper userAccountMapper; Resource private ProductStockMapper productStockMapper; Resource private OrderInfoMapper orderInfoMapper; Resource private OrderItemMapper orderItemMapper; Override Transactional(rollbackFor Exception.class) public Long createOrder(Long userId, Long productId, Integer quantity) { // 1. 查询用户余额和商品信息 UserAccount account userAccountMapper.selectByUserIdForUpdate(userId); ProductStock stock productStockMapper.selectByProductIdForUpdate(productId); BigDecimal totalAmount stock.getPrice().multiply(BigDecimal.valueOf(quantity)); // 2. 业务校验余额是否足够、库存是否充足 if (account.getBalance().compareTo(totalAmount) 0) { throw new BusinessException(余额不足); } if (stock.getStock() quantity) { throw new BusinessException(库存不足); } // 3. 依次执行数据变更 userAccountMapper.deductBalance(userId, totalAmount); productStockMapper.deductStock(productId, quantity); OrderInfo order new OrderInfo(); order.setUserId(userId); order.setTotalAmount(totalAmount); order.setStatus(CREATED); orderInfoMapper.insert(order); OrderItem item new OrderItem(); item.setOrderId(order.getId()); item.setProductId(productId); item.setQuantity(quantity); item.setPrice(stock.getPrice()); orderItemMapper.insert(item); return order.getId(); } }这里有几个关键点值得展开说。第一查询余额和库存我用了selectByUserIdForUpdate这就是SELECT ... FOR UPDATE行级锁在事务里先锁定用户行和商品行。这样做是为了防止并发问题两个人同时下单同时读到余额 100都判断可以购买都去扣余额最后余额变成负数。加锁之后第二个事务必须等第一个事务提交才能读到最新数据从而避免超卖和超扣。第二rollbackFor Exception.class是必写的。为什么因为BusinessException如果继承的是RuntimeException那默认回滚规则也能兜住但如果你的团队异常体系里业务异常继承的是Exception不写rollbackFor就会碰到“代码抛异常了但事务提交了”的诡异事件。为了统一规范我所有Transactional注解都强制写rollbackFor Exception.class。第三事务方法和普通方法要分开层次。createOrder的事务方法里原则上只放数据库操作和内存中的业务计算绝对不能出现外部 HTTP 调用、MQ 同步发送、文件写入等耗时操作。事务期间持有的数据库连接和行锁会随着这些外部调用时长被无限拉长锁等待和连接池耗尽就是这么来的。真实场景里如果需要在下单后发短信通知用户正确做法是事务提交之后异步发送或者用TransactionSynchronizationManager注册afterCommit回调。5.3 事务提交后的收尾动作怎么设计我见过不少团队把“发送通知”“写入审计日志”“推送消息到 MQ”直接写进事务方法里。短期看没出问题长期看全是雷。试想一个场景事务方法里发送 MQ 消息成功但数据库事务随后回滚了下游消费者处理消息时查不到订单数据直接报错重试一个错误消息顶着一个幽灵订单在系统里来回转。解决方案是把收尾动作放到事务提交之后。Spring 提供了TransactionSynchronizationManager.registerSynchronization你可以注册一个TransactionSynchronization在afterCommit回调里发送消息。如果要保证消息一定发送成功更稳妥的做法是“本地消息表”事务内在数据库里插入一条消息记录事务提交后由一个定时任务扫描消息表把未发送的消息投递到 MQ投递成功后标记状态。这种方式把消息发送的可靠性和业务事务解耦是目前单体应用里比较成熟可靠的最终一致性方案。6. Transactional 失效的六个经典场景与排查记录6.1 失效问题速查表我把这些年遇到的事务失效情况集中整理成一张速查表内容都是可以直接抄作业的排查方向失效现象根本原因解决方案方法正常执行但不生效方法用 private/package 修饰代理对象无法拦截把方法改成 public同类内自调用第二个方法失效对象内部this.method()不经过代理注入自身引用或拆到不同 Service或用TransactionTemplate异常被 catch 住事务不回滚异常没有传播到事务拦截器捕获后重新抛出或手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()抛了受检异常但不回滚默认只回滚 RuntimeException 和 Error注解加rollbackFor Exception.class方法所在的类被 final 修饰CGLIB 无法继承 final 类去掉 final改为普通类多个数据源操作只回滚了第一个单事务只绑定一个数据源连接引入分布式事务方案或改为最终一致性6.2 一次真实的“扣库存成功但订单没建”排查回到开头那个事故的后续。当时我接手排查时数据库里已经积累了一批“有扣款记录但无订单”的脏数据。我先看了代码业务方法确实加了Transactional表面看没有任何问题。然后我查了服务日志发现异常日志是这样一段org.springframework.dao.DataIntegrityViolationException: ...但后面没有任何回滚记录。问题出在哪我继续往下追发现这个方法是个private方法。它从另一个 public 方法内部被调用调用方没有加事务注解而 Spring 的 AOP 代理无法拦截 private 方法所以整个调用链上根本没有事务存在。Transactional注解写在 private 方法上相当于一个摆设。这个方法能在测试环境跑通是因为数据没到触发异常的地步真到线上并发量一上来字段长度超限、唯一键冲突一起来残缺数据就批量暴露了。这个案例给我的启发是事务方法必须声明为 public且必须通过 Spring 容器管理的外部调用入口进入。任何内部自调用、私有方法调用都会绕过代理注解形同虚设。6.3 你可以直接抄的“事务体检清单”每次新项目或大版本上线前我习惯按下面这份清单过一遍事务代码。把它固定在团队评审模板里能减少大量线上扯皮。凡是标记Transactional的方法确认是 public且由外部 Bean 调用而不是内部this调用。注解上统一写rollbackFor Exception.class不依赖默认回滚规则。事务方法内部不 catch 掉异常如果必须 catchcatch 之后要么重新抛出要么手动标记rollbackOnly。事务方法内部不做外部 IO、不发送 MQ、不调用 RPC收尾动作移到afterCommit或本地消息表。确认事务操作的表都是 InnoDBMyISAM 引擎不支持事务。留意小事务超时问题必要时配置timeout属性防止死锁和慢 SQL 拖住整个事务。6.4 事务日志怎么开、怎么读排查事务问题最直接的手段是打开事务日志。Spring 的TransactionInterceptor默认使用commons-logging输出日志你在application.yml里把日志级别调低就能看到完整的事务生命周期logging: level: org.springframework.transaction: DEBUG org.springframework.jdbc.datasource.DataSourceTransactionManager: DEBUG打开之后日志里会依次出现Getting transaction for ...、Initiating transaction commit、Initiating transaction rollback等关键信息。如果你发现异常堆栈都打出来了但日志里没有rollback字样基本可以断定事务没生效或者异常没传播到拦截器。这个方法我在线上排查时帮了大忙比对着代码猜高效得多。7. 事务代码的可测性提升三个值得坚持的习惯7.1 TransactionTemplate细粒度控制的另一种选择Transactional用起来方便但它是声明式的一旦事务边界选错了改动成本很高。对于需要精确控制事务范围的代码我更喜欢用编程式事务TransactionTemplate。它的写法更直白事务边界清晰可见也不存在自调用失效的陷阱Resource private TransactionTemplate transactionTemplate; public void createOrderWithTemplate(...) { Long orderId transactionTemplate.execute(status - { // 这里面的代码处于同一事务中 userAccountMapper.deductBalance(...); productStockMapper.deductStock(...); orderInfoMapper.insert(...); return orderInfo.getId(); }); }TransactionTemplate适合用在事务边界很细、或者需要在一个方法里分段控制多个独立事务的场景。它的缺点是代码比注解繁琐但对于“事务失效”这类初级问题高度免疫排查成本低。我推荐的策略是常规业务方法用Transactional复杂流程或自调用场景优先考虑TransactionTemplate。7.2 事务测试的 Rollback 约定写事务相关的单元测试时一个常见的坑是测试数据污染数据库。比如你在测试环境跑createOrder每次跑都会真的插入一条订单和扣减余额跑几次测试环境数据就乱了。我的做法是给测试类加TransactionalSpring 测试框架默认会在每个测试方法结束时回滚事务这样测试产生的数据不会真正落库既验证了业务逻辑又保护了环境数据干净。但这里有个例外如果被测方法内部使用了REQUIRES_NEW内层事务的提交不受外层测试事务回滚影响数据还是会被写进去。遇到这种情况要么测试结束后手动清理要么用 Mock 隔离外层依赖。7.3 事务监控与日志的结合最后聊一个偏运维向的技巧把事务执行时间和连接占用情况纳入监控。我之前在一个项目中给DataSource包了一层代理记录每条 SQL 的耗时和连接获取时间再配合TransactionInterceptor的日志只要事务执行超过 500ms 就告警。上线三个月抓出了好几个“事务内做远程调用”的低级问题。实际上 Spring AOP 本身就是实现日志记录的利器事务监控本质上也是 AOP 的一种应用。理解了事务代理的机制你对 Spring AOP 的理解会自然加深一层。事务管理这件事理论到实践的距离没有想象中那么大关键是把原理吃透、把边界画清、把日志打开大多数问题都能在半小时内定位。我自己在这条路上踩过的坑都写在这篇文章里了希望你能少走一截。