ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

我为了准备美团的面试,爆肝60张图画明白了Spring事务!

我为了准备美团的面试,爆肝60张图画明白了Spring事务!

一、为什么 Spring 事务是美团面试的高频题

在很多互联网公司的后端面试中,Spring 事务几乎是必问项,美团也不例外。原因并不复杂:一个稍微复杂一点的业务链路,往往同时操作多张表,涉及新增、更新、删除,任何一个环节失败,都可能导致数据不一致。订单扣库存、优惠券核销、积分发放、账务流水,这些典型场景对事务正确性的要求极高。

面试官真正想考察的,从来不是你有没有背下来“ACID”这四个字母,而是下面这几个层次:

  • 你能不能把数据库事务和 Spring 事务的关系讲清楚;
  • 你知不知道一个注解背后,Spring 到底帮你做了哪些事;
  • 你在项目里是否踩过事务失效的坑,又是如何定位和解决的;
  • 你能否结合源码,讲出事务开启、提交、回滚的完整链路;
  • 你能否把事务和锁、MVCC、连接池、分布式场景串成一张完整的知识图谱。

本篇文章我会从最基础的概念讲起,逐步深入到源码实现和面试实战,目标是让你不仅“会背”,而且能把整条链路从头到尾讲明白。

二、先把数据库事务的地基打牢

在讨论 Spring 事务之前,必须先明确一个前提:Spring 本身并不真正实现“事务”,它本质上是对数据库事务能力的一层抽象和封装。真正保证数据一致性的,依然是底层的数据库事务。因此,理解数据库事务是理解 Spring 事务的必要前提。

2.1 什么是事务

事务是数据库操作的最小执行单元。一个事务中包含的一组操作,要么全部成功,要么全部失败回滚,不允许出现“一半成功一半失败”的中间状态。

举个最简单的例子:用户下单时,需要在订单表插入一条订单记录,同时在库存表扣减库存。如果没有事务,可能出现订单插入了,但库存扣减因为异常没有执行,结果就是“订单存在但库存没动”,这显然是不可接受的。

有了事务,这两个操作被绑定在一起:要么订单和库存都成功,要么都回滚,回到执行前的状态。

2.2 事务的四大特性:ACID

ACID 是事务的四个核心特性,面试中经常要求展开解释,并且要能结合数据库实现机制来讲,而不是只背定义。

特性含义常见实现机制
原子性 Atomicity事务中的操作要么全部完成,要么全部不完成,执行过程中出现错误会回滚到事务开始前的状态Undo Log
一致性 Consistency事务执行前后,数据必须从一个合法状态变到另一个合法状态,不能破坏约束和业务规则由其他三个特性共同保障
隔离性 Isolation多个事务并发执行时相互隔离,一个事务的执行不能被其他事务干扰锁机制、MVCC
持久性 Durability事务一旦提交,对数据的修改就是永久性的,即使系统崩溃也不丢失Redo Log

这里特别强调一点:很多人把“一致性”当成一个可有可无的口号,其实它是由原子性、隔离性、持久性共同支撑起来的结果。原子性保证不会出现中间状态,隔离性保证并发场景下不会读到脏数据,持久性保证提交后的结果不会丢,三者共同作用,最终才表现为“数据始终处于一致状态”。

2.3 并发事务带来的问题

如果不做隔离,多个事务并发执行会带来一系列经典问题,这也是理解隔离级别的地基:

  • 脏读:事务 A 读到了事务 B 尚未提交的数据。如果 B 之后回滚,A 读到的就是一条根本不存在的“脏数据”。
  • 不可重复读:同一个事务内,先后两次读取同一条数据,结果不一致。例如事务 A 第一次读到余额 100,事务 B 把余额改成 200 并提交,事务 A 第二次读到 200。
  • 幻读:同一个事务内,按相同条件多次查询,结果集的行数不一致。例如第一次查到 10 条,第二次查询期间有另一个事务插入了一条满足该条件的新记录,于是第二次查到 11 条。

脏读关注的是读到未提交的数据,不可重复读关注的是已提交的数据被修改,幻读关注的则是数据集的行数变化。面试时能把三者的本质区别讲清楚,是加分项。

2.4 四种隔离级别

为了解决上述问题,SQL 标准定义了四种隔离级别,从低到高依次是:

隔离级别脏读不可重复读幻读
读未提交 READ UNCOMMITTED可能可能可能
读已提交 READ COMMITTED不可能可能可能
可重复读 REPEATABLE READ不可能不可能可能
串行化 SERIALIZABLE不可能不可能不可能

隔离级别越高,数据一致性越强,但并发性能越差。需要特别记忆的是 MySQL 的默认隔离级别是“可重复读 REPEATABLE READ”,而 Oracle 和 PostgreSQL 的默认隔离级别是“读已提交 READ COMMITTED”。这个差异在面试中常被拿来考察。

补充说明:MySQL InnoDB 虽然默认是可重复读,但在一定条件下可以避免幻读,主要通过 MVCC 中的快照读和行锁、间隙锁来实现。不过严格来说 SQL 标准的可重复读级别并不能完全消除幻读,这一点在面试时要注意表述的严谨性。

2.5 事务的传播行为概念

“传播行为”是 Spring 对数据库事务的一种扩展概念。数据库本身没有传播行为,它解决的是这样一个问题:当一个事务方法被另一个事务方法调用时,事务应该怎么处理?是加入外层事务,还是新开一个事务,还是干脆以非事务方式运行?

Spring 定义了 7 种传播行为,这是 Spring 事务面试中的绝对核心。我们将在后面的章节中逐一展开。

三、Spring 事务的核心抽象

很多同学一提到 Spring 事务,脑子里只有 @Transactional 注解。但如果面试官追问“注解背后的工作机制”,往往就答不上来了。要讲清楚 Spring 事务,必须掌握它的三块核心抽象。

3.1 三大核心接口

Spring 事务模块的核心接口主要有三个:

  • TransactionDefinition:事务定义,描述事务的属性,包括传播行为、隔离级别、超时时间、是否只读、回滚规则等。
  • TransactionStatus:事务状态,描述当前事务的运行状态,例如是否是新事务、是否已完成、是否只读、是否有保存点等。
  • PlatformTransactionManager:事务管理器,负责根据事务定义开启事务、提交事务、回滚事务。

可以这样理解它们的关系:TransactionDefinition 是“你要一个什么样的事务”的说明书,TransactionStatus 是“当前事务现在处于什么状态”的快照,而 PlatformTransactionManager 是真正操纵事务的执行者。

3.2 TransactionDefinition 的关键属性

TransactionDefinition 中最重要的几个属性,正是我们在 @Transactional 注解里可以配置的那些值:

public interface TransactionDefinition { int getPropagationBehavior(); int getIsolationLevel(); int getTimeout(); boolean isReadOnly(); String getName(); }
属性说明@Transactional 对应配置
传播行为定义事务如何在方法间传播propagation
隔离级别定义事务的隔离级别isolation
超时时间事务的最长执行时间,超过则回滚timeout
只读标志标识事务内只有读操作,可做优化readOnly
回滚规则定义哪些异常触发回滚rollbackFor、noRollbackFor

3.3 PlatformTransactionManager 的工作流程

事务管理器的接口定义非常简洁:

public interface PlatformTransactionManager { TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException; void commit(TransactionStatus status) throws TransactionException; void rollback(TransactionStatus status) throws TransactionException; }

其中 getTransaction 方法承担了两个职责:如果当前已经存在事务,则依据传播行为决定是加入现有事务还是新开事务;如果当前没有事务,则按定义开启一个新事务。commit 和 rollback 则分别负责提交和回滚。

3.4 常用的事务管理器实现

Spring 并不直接和某种特定数据库打交道,而是通过不同的 PlatformTransactionManager 实现来适配各种持久化技术:

  • DataSourceTransactionManager:基于 JDBC 和单一数据源的事务管理,负责从 DataSource 获取连接并管理连接的提交和回滚。
  • HibernateTransactionManager:适用于 Hibernate 持久化场景。
  • JpaTransactionManager:适用于 JPA 持久化场景,例如 Spring Data JPA 项目。
  • JtaTransactionManager:用于分布式事务,委托给 JTA 容器。

在 Spring Boot 项目中,当我们引入了 spring-boot-starter-jdbc 或者 spring-boot-starter-data-jpa,Spring Boot 会自动配置对应的事务管理器,我们通常无需手动声明。

3.5 事务同步与资源绑定

这是一个容易被忽略但很重要的细节:Spring 在开启事务后,会把数据库连接绑定到当前线程,后续同一个事务内的所有 DAO 操作都复用这个连接,从而保证它们处于同一个数据库事务中。

Spring 通过 TransactionSynchronizationManager 维护线程级别的资源,核心是通过 ThreadLocal 绑定资源。这也解释了为什么事务方法必须通过代理调用才能生效,以及为什么同类内部自调用会失效,这两个问题我们后面会详细展开。

四、7 种传播行为逐一拆解

传播行为是 Spring 事务最经典、也是被问得最多的考点。很多同学知道 REQUIRED 和 REQUIRES_NEW,但问到 SUPPORTS、NESTED 的区别时,就开始支支吾吾。下面我们用统一的业务场景把 7 种传播行为讲透。

统一场景:订单服务中,方法 A 开启了事务,在方法 A 内部调用了方法 B。问题就是:B 的事务应该怎么处理?

4.1 REQUIRED(默认值)

如果当前存在事务,则加入当前事务;如果当前没有事务,则新建一个事务。

这是使用最广泛的传播行为。在默认情况下,@Transactional 注解就等价于 propagation = Propagation.REQUIRED。

@Transactional(propagation = Propagation.REQUIRED) public void methodA() { orderDao.insert(order); // B 会加入 A 已经开启的事务 orderService.methodB(); }

关键点:当 B 加入 A 的事务后,A 和 B 的提交或回滚是绑定的。无论 A 还是 B 中抛出异常,整个事务都会回滚。这也意味着,REQUIRED 的行为符合“同生共死”的语义,适合绝大多数业务场景。

4.2 SUPPORTS

如果当前存在事务,则加入当前事务;如果当前没有事务,则以非事务方式执行。

这个传播行为的特点是“有则加入,没有也不强求”。它通常用于那些既可能被事务方法调用,也可能被非事务方法调用的查询方法。

@Transactional(propagation = Propagation.SUPPORTS) public User queryUser(Long id) { return userDao.selectById(id); }

例如一个查询方法,它被事务方法调用时,希望能在同一个事务和同一个连接里运行;但如果被非事务方法调用,也没必要单独开启事务。

4.3 MANDATORY

如果当前存在事务,则加入当前事务;如果当前没有事务,则抛出异常。

MANDATORY 的语义是“强制要求必须有事务”。它用于那些必须在事务内执行的代码,防止开发者在不开启事务的情况下误用。

@Transactional(propagation = Propagation.MANDATORY) public void deductStock(Long skuId, int count) { stockDao.deduct(skuId, count); }

在上述代码中,如果 deductStock 被一个没有事务的方法直接调用,Spring 会抛出 IllegalTransactionStateException。

4.4 REQUIRES_NEW

无论当前是否存在事务,都新开一个事务;如果当前已有事务,则把当前事务挂起。

这是与 REQUIRED 对比最强烈的传播行为。它表示“我不和你同生共死,我要自己独立的事务”。

@Transactional(propagation = Propagation.REQUIRES_NEW) public void writeLog(String content) { logDao.insert(content); }

假设外层订单事务中调用了 writeLog 方法,此时会发生:

  • 外层事务被挂起;
  • 为新方法开启一个全新的数据库事务和新的连接;
  • 新事务独立提交或回滚,不受外层事务最终结果影响;
  • 新事务结束后,恢复之前被挂起的外层事务。

REQUIRES_NEW 常用于日志记录、审计信息等场景:即使主业务事务回滚,日志信息也希望能够单独提交下来。

注意:REQUIRES_NEW 会挂起外层事务,底层需要新建数据库连接,因此如果大量使用,会增加数据库连接消耗,甚至可能出现连接池耗尽的问题,需要谨慎使用。

4.5 NOT_SUPPORTED

无论当前是否存在事务,都以非事务方式执行;如果当前已有事务,则把当前事务挂起。

NOT_SUPPORTED 表示“我不想在事务里跑”。它适用于那些执行时间较长、或者根本不需要事务能力的操作,避免长期占用事务和数据库连接。

@Transactional(propagation = Propagation.NOT_SUPPORTED) public void exportReport() { // 执行耗时报表导出,不在事务内运行 }

和 REQUIRES_NEW 相比,NOT_SUPPORTED 直接以非事务方式运行,而 REQUIRES_NEW 会为其开启一个新的事务。

4.6 NEVER

如果当前存在事务,则抛出异常;如果当前没有事务,则以非事务方式执行。

NEVER 的语义是“绝对不允许在事务里运行”。它较少使用,一般用于明确禁止事务腐蚀的场景。

4.7 NESTED

如果当前存在事务,则在嵌套事务中执行;如果当前没有事务,则行为等同于 REQUIRED,新建一个事务。

这是所有传播行为中最容易被误解的一个。核心区别在于:

  • 嵌套事务是外层事务的一个“子事务”,复用同一个数据库连接;
  • 嵌套事务通过保存点 Savepoint 实现,而不是通过挂起外层事务和新建连接实现;
  • 内层事务可以独立回滚到保存点,而不影响外层事务;但外层事务回滚时,内层事务也必然随之回滚。

看下面这个例子:

@Transactional(propagation = Propagation.NESTED) public void processItem(Item item) { itemDao.process(item); }

假设外层事务方法调用了 processItem,并且外层捕获了内层抛出的异常:

  • 如果内层 processItem 失败,它只回滚到内层方法开始前的保存点;
  • 外层事务可以继续执行其他逻辑,并最终提交;
  • 外层提交成功时,未失败部分的数据会保留下来。

为了帮助大家把 NESTED 和 REQUIRES_NEW 彻底区分开,我们再做一个对比:NESTED 是复用同一个数据库连接,通过 Savepoint 实现内层回滚;REQUIRES_NEW 则是新建数据库连接、把外层事务挂起,来实现内外层互不影响。前者的资源开销更小,但要求底层数据库或驱动支持 Savepoint;后者的隔离程度更高,但大量使用时容易造成连接池吃紧。

五、@Transactional 注解使用详解

传播行为之后,第二个高频考点就是 @Transactional 注解本身。很多同学每天都在写这个注解,但一到面试被追问属性配置,就暴露出“会用但讲不清楚”的问题。

5.1 注解可以加在哪里

@Transactional 可以标注在接口、类和具体方法上。Spring 官方推荐只标注在类和方法上,不推荐标注接口,因为不同的动态代理机制对接口注解的识别策略不一致,容易产生误解。

当类和方法上都存在注解时,方法级别的配置会覆盖类级别的配置。这个优先级非常重要,它意味着你可以给类设置一套默认事务属性,再在个别方法上做特殊处理。

5.2 常用属性一览

属性说明备注
propagation传播行为默认 REQUIRED
isolation隔离级别默认使用数据库默认隔离级别
timeout事务超时时间,单位秒默认 -1,表示不限制
readOnly是否只读只读事务可做优化
rollbackFor指定哪些异常回滚默认只回滚 RuntimeException 和 Error
noRollbackFor指定哪些异常不回滚和 rollbackFor 相反

5.3 最容易踩坑的回滚规则

Spring 事务的默认回滚规则是:只有 RuntimeException 和 Error 会触发回滚,受检异常不会触发回滚。这是面试中非常容易被追问的点。

@Service public class OrderService { @Transactional(rollbackFor = Exception.class) public void createOrder(Order order) throws Exception { orderDao.insert(order); stockService.deduct(order.getSkuId(), order.getCount()); couponService.writeOff(order.getCouponId()); } }

上面这段代码通过 rollbackFor = Exception.class,把回滚边界扩大到了所有异常。在实际项目里,如果你的业务方法可能抛出 IOException、SQLException 等受检异常,而你又希望这些异常也能回滚,就必须显式配置 rollbackFor。

反过来,noRollbackFor 用于某些特殊场景:即使抛出了异常,也不希望事务回滚。例如发通知失败不影响主流程时,可以配置为不回滚。

六、Spring 事务失效的 8 大经典场景

事务失效是实践类面试的必考题。它的本质,往往不是 Spring 的 Bug,而是开发者对代理机制、传播机制和数据库特性理解不到位。下面 8 个场景,建议逐个对照自己项目里的代码检查一遍。

6.1 同类内部自调用导致事务失效

开发中最常见的场景,就是在一个类的方法内部通过 this 调用另一个事务方法,结果事务没有生效。

@Service public class OrderService { @Transactional public void placeOrder(Order order) { orderDao.insert(order); // 这里通过 this 调用,不会经过 Spring 代理 this.updateInventory(order); } @Transactional public void updateInventory(Order order) { stockDao.deduct(order.getSkuId(), order.getCount()); } }

原因是 Spring 事务基于 AOP 代理实现。外部调用时,调用方拿到的是代理对象,代理对象会在目标方法执行前后织入事务逻辑;而同类内部通过 this 调用时,方法直接在原始对象上执行,根本没有经过代理,所以 @Transactional 注解失效。

解决思路通常有:把被调用方法拆分到另一个 Spring Bean 中,通过注入的代理对象调用;或者通过 AopContext.currentProxy() 获取当前代理对象再调用。

6.2 方法不是 public

默认情况下,只有 public 方法才能被 Spring AOP 代理。如果事务方法被写成 private、protected 或者包级私有,事务不会生效。

如果项目使用 AspectJ 静态织入,可以支持非 public 方法,但普通 Spring Boot + CGLIB/JDK 动态代理项目,还是要老老实实使用 public。

6.3 异常被开发者捕获吞掉

@Transactional public void updateData(Order order) { try { orderDao.update(order); int i = 1 / 0; } catch (Exception e) { log.error("update error", e); // 异常在这里被吞掉,事务感知不到 } }

上面这段代码中,虽然抛出了 ArithmeticException,但它在 catch 中被捕获并吞掉,Spring 根本感知不到异常,自然也不会回滚。如果业务确实需要捕获异常,至少要在 catch 中重新抛出,或者调用 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() 手动标记回滚。

6.4 rollbackFor 配置错误

正如 5.3 小节所讲,如果方法抛出的是受检异常,而你没有配置 rollbackFor,事务同样不会回滚。反过来,如果配置了错误的异常类型,也可能导致该回滚的没回滚。

6.5 数据库引擎不支持事务

并不是所有数据库表都支持事务。例如 MySQL 的 MyISAM 引擎就不支持事务。如果目标表使用的是 MyISAM,即使 Spring 配置正确,数据操作也不会真正拥有事务语义。面试官问到这里,主要考察你是否理解“Spring 事务最终还是要落到数据库事务能力上”。

6.6 多线程场景下事务不共享

Spring 事务是基于 ThreadLocal 把连接绑定到当前线程的。如果在一个事务方法里通过线程池开启新线程去执行数据库操作,新线程拿不到外层事务的连接绑定,自然也不会加入外层事务,外层回滚也无法回滚新线程已提交的内容。

6.7 类没有被 Spring 管理

如果一个对象是开发者自己 new 出来的,或者没有加上 @Service、@Component 等注解,它根本不经过 Spring 容器管理,也不会被代理,事务自然无效。

6.8 传播行为配置不当

例如使用了 NOT_SUPPORTED、NEVER 这类传播行为,方法本身就不会真正处在事务内。需要根据业务场景选择正确的传播行为,而不是一律使用默认值。

七、Spring 事务与锁、MVCC、连接池的联动

面试到了后半段,单纯考 Spring 往往已经不够,而是会把事务、锁、MVCC、连接池串起来问。你需要建立一张完整的知识图谱。

7.1 事务与连接池

Spring 开启事务时,会从数据源连接池中获取连接,并把连接绑定到当前线程。事务方法执行期间,该连接被当前事务独占;方法结束后,连接根据提交或回滚结果重新归还到连接池。

这里引出一个重要实践结论:长事务是非常危险的。事务时间越长,连接被占用的时间就越长,连接池中的可用连接就越少。当并发量上升时,大量请求会因为拿不到连接而阻塞,甚至造成服务雪崩。所以实际开发中,应当尽量缩短事务范围,把非事务操作,比如外部接口调用、文件上传、复杂计算等,放在事务之外。

7.2 事务与锁

数据库为了保证隔离性,会在事务执行期间加锁。不同的语句和隔离级别会触发不同的锁类型,常见的有行锁、间隙锁、临键锁和表锁。

事务越久,锁持有的时间也越久,就越容易引发锁等待和死锁。死锁的直接表现是两个事务互相等待对方释放锁,谁都执行不下去。遇到死锁,数据库通常会让其中一方回滚,另一方继续执行。

优化思路上,可以从几个方面入手:尽量按固定顺序访问资源;缩小事务范围;减少长事务;合理使用索引,避免锁范围过大。

7.3 事务与 MVCC

MVCC,也就是多版本并发控制,是数据库在事务并发场景下提供一致性读的重要手段。它通过版本链让读操作读到适合自己的历史版本,从而减少读和写之间的锁冲突。

在 MySQL InnoDB 中,MVCC 主要和 Undo Log 以及 Read View 有关。普通 SELECT 通常是快照读,读取的是某个版本快照;而 SELECT ... FOR UPDATE、UPDATE、DELETE 这类操作属于当前读,读取的是最新版本并可能加锁。

Spring 事务本身不改变这些数据库特性,但事务的持续时间会影响快照和锁的持有时间。面试时能把“Spring 事务的范围”和“数据库锁或 MVCC 的影响”结合起来回答,会显得非常有工程经验。

八、分布式事务入门

美团这类公司业务量很大,服务通常会做微服务拆分,数据也会分库分表。此时本地事务已经不能覆盖跨服务、跨数据源的场景,必须了解分布式事务的基本概念。

分布式事务要解决的核心问题,就是多个参与方各自有本地事务,怎样协调它们一起成功或一起失败。

  • CAP 理论:在分布式系统中,一致性、可用性、分区容错性三者无法同时完全满足,需要根据业务取舍。
  • BASE 理论:基本可用、软状态、最终一致,是对 CAP 中强一致要求的放宽。
  • 2PC 两阶段提交:协调者先询问各参与者是否可以提交,再进行统一的提交或回滚,优点是强一致,缺点是同步阻塞、单点故障风险高。
  • TCC 补偿:把操作拆成 Try、Confirm、Cancel 三个阶段,通过显式补偿实现最终一致。
  • 最终一致性消息:通过可靠消息、本地消息表等方式,让参与方异步达成最终一致。

面试时如果被问到分布式事务,可以结合本项目规模诚实说明:小型项目优先考虑避免跨库事务,中大型项目再讨论 TCC、消息最终一致等方案的取舍。

九、美团面试高频问题与回答思路

最后,我们把前面所有知识浓缩成几道高频面试题。回答时不要只背结论,要按“概念 + 机制 + 场景 + 避坑”的结构展开。

  1. 讲讲 Spring 事务和数据库事务的关系?
  2. Spring 事务的原理是什么?
  3. 什么是事务传播行为?REQUIRED 和 REQUIRES_NEW 的区别?
  4. Spring 事务在什么情况下会失效?
  5. @Transactional 默认在哪些异常下回滚?如果希望所有异常都回滚怎么办?
  6. 事务比较长会有什么问题?
  7. MySQL 默认隔离级别是什么?可重复读是怎么实现的?
  8. 分布式事务有哪些常见方案?

这类面试题的回答框架可以归纳为:先用一两句话点出结论,然后解释底层机制,再用业务场景举例,最后补充容易踩的坑或优化建议。这样既有理论深度,又有落地经验。

十、总结:用一张图串起 Spring 事务

整篇文章到这里,完整的 Spring 事务知识框架已经形成。你可以尝试在大脑中画一张图:

  • 最底层是数据库事务,提供 ACID、隔离级别、锁和 MVCC;
  • 中间层是 Spring 的核心抽象,TransactionDefinition 定义事务属性,TransactionStatus 描述事务状态,PlatformTransactionManager 负责开启、提交和回滚;
  • 再上一层是 @Transactional 注解和 7 种传播行为;
  • 最上层是项目实战经验,包括事务失效排查、连接池优化和分布式事务选型。

面试不是在比拼谁背的名词多,而是在考察你能不能把这条从数据库到 Spring 再到业务场景的链路讲通。真正把这些内容理解到位,你会发现 Spring 事务并没有想象中那么复杂。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进