ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Spring 面经系列 · 第 5 篇:事务这条线,从 ACID 到失效与多线程

Spring 面经系列 · 第 5 篇:事务这条线,从 ACID 到失效与多线程

Spring 面经系列 · 第 5 篇:事务这条线,从 ACID 到失效与多线程

💡面试里问「说一下事务」,十个人有九个上来就背 ACID、背四种隔离级别,像在答数据库的题。可面试官真正想确认的是:Spring 在这条链路里到底干了什么、没干什么。

说穿了,Spring 自己既不写 redo log,也不加行锁,它只是把「事务边界」这件事做成了一层统一抽象,再靠动态代理 +ThreadLocal复用同一个数据库连接,把 commit / rollback 翻译给底层。一旦你想让它跨线程、跨库去保证一致性,它立刻两手一摊——这不是它管的范围。

这篇文章按视频里的顺序把这条线捋一遍:ACID 归位 → 管理类型与实现方式 → 七种传播行为 → 隔离级别与数据库的对应 → 声明式事务原理 → 传播行为在源码里怎么落地 → 失效场景与多线程事务。凡是我没有对照源码逐行确认的具体类名、方法名、默认值,都会标出来,不会替 Spring 打包票。


1. ACID:Spring 只兜住「边界」,不实现「特性」

视频里先过了事务的四大特性,并且强调这不是 Spring 独有的——凡是碰数据库、碰事务的地方都得遵守它。这段本身没错,但如果面试时只把 ACID 念一遍,等于答成了数据库教科书。这里真正要说清的是每一条特性分别由谁负责。

  • 原子性(A):一组操作要么全成功要么全失败。数据库靠 undo log 回滚来兑现「全失败」,Spring 只负责把「哪几条语句算一组」这个边界圈出来,并在异常时把 rollback 指令发给底层。
  • 一致性(C):视频用的是转账例子——张三和李四加起来一共 3000 块,转一万次总额还是 3000。这种「数据前后守恒」本质上是业务规则 + 数据库约束(唯一键、外键、check)保证的,Spring 并不校验你的业务金额对不对。
  • 隔离性(I):并发事务之间互不干扰,防止脏读、不可重复读、幻影读。这是数据库引擎用锁 / MVCC 实现的,Spring 只是把隔离级别作为参数透传下去。
  • 持久性(D):一旦提交就永久保存,哪怕中途宕机也不能丢。这是数据库的 redo log / WAL 落盘在兜底,Spring 完全不管。

一张表看清职责边界:

特性谁真正兑现Spring 负责的那部分
原子性 A数据库(undo log 回滚)定义事务边界 + 异常时下发 rollback
一致性 C业务逻辑 + 数据库约束不负责,只保证「要么都提交要么都回滚」
隔离性 I数据库(锁 / MVCC)把 isolation 参数透传给底层连接
持久性 D数据库(redo log / WAL)不负责,只管把 commit 发出去

一句话理解:Spring 是「事务的编排者」,不是「事务的实现者」。它把 commit / rollback / 隔离级别这些动作统一成一套 API,底下换成 JDBC、JPA、Hibernate 还是 JTA,你写的代码不变;但 ACID 这四个字母,绝大多数是数据库引擎在替你扛。

所以视频里那句「本地事务也好、分布式事务也好,都应尽量遵循这四大特性,可能做不到百分之百,但要尽量遵循」——这句话的落点其实在分布式场景:单机里数据库帮你把 A、I、D 兜住了,一旦跨库跨服务,这些保证就散了,才需要你自己想办法。这点在第 7 节的多线程 / 分布式里会回扣。


2. 事务管理类型与实现方式:两种类型,三条实现路径

视频把这道题拆成两个小问:一是「管理类型」,二是「实现方式」。

管理类型只有两种:编程式和声明式。所谓编程式,就是你自己手动控制开启、提交、回滚。最原始的样子就是早期直接用 JDBC:拿一个Connection,setAutoCommit(false)相当于开启事务,操作做完手动commit(),出异常就rollback()。Spring 在之上也提供了编程式入口——注入一个TransactionTemplate,把数据库操作丢进它的execute()里,一旦捕获到异常,就把事务状态设成 rollback-only(只回滚),否则让它正常提交。它的好处是灵活,某些声明式满足不了、想手动控制提交时机的场景用得上。

声明式就是大家最熟的@Transactional:不用自己写提交回滚,交给 Spring。它又分两种配法——注解,或者在 XML 里用<tx:advice>这种通知的方式声明「哪些方法走什么事务」。

维度编程式事务声明式事务
边界控制自己写代码控制注解 / XML 元数据声明
代表 APITransactionTemplate.execute@Transactional/tx:advice
侵入性有,业务代码里混事务逻辑无,靠代理织入
灵活度高,可随时手动提交受注解语义限制
使用频率少数特殊场景绝大多数场景

至于实现方式,视频说得很直接:Spring 声明式事务本身就是基于 AOP 的,所以它的实现路径跟 AOP 那套几乎一模一样,分基于接口、基于 XML、基于注解三种(AspectJ 算第四种,但视频强调严格来说只讲三种)。基于接口的方式现在不建议用,但它对应的是早期的TransactionProxyFactoryBean一类 API——你得自己声明通知、自己创建动态代理。视频作者特意说,讲源码之前先普及一遍接口用法,反而更容易看懂底层。这里我把它归类为「视频里给的说法」,具体类名笔者未对照源码核实。

这里补一个视频没展开、但面试常追问的点:声明式事务真正落地时靠的是TransactionManager家族。Spring 定义了一个统一的PlatformTransactionManager接口,按你底层用什么数据访问技术,换不同实现:

实现类管的是什么典型场景
DataSourceTransactionManager一个 JDBCConnection单库、MyBatis / 原生 JDBC
JpaTransactionManagerJPA 的EntityManagerSpring Data JPA
HibernateTransactionManagerHibernateSession老项目直接用 Hibernate
JtaTransactionManager应用服务器的全局事务跨多个数据源的分布式事务

PlatformTransactionManager这层抽象,正是第 1 节「Spring 只当编排者」的技术落点:无论哪个实现,它做的都是同一件事——开启、提交、回滚、(必要时)挂起。至于 NESTED 传播、事务挂起这些高级动作,并不是每个实现都支持,这点第 3、6 节会再提。(本表为笔者依据官方文档补充,视频未点名这几个类。)

Q&A 先摆一个这里容易踩的:

Q1:Spring 是不是自己实现了原子性 / 持久性?

不是。Spring 拿不到 undo log、也写不了 redo log,它做的事是把「一组操作」绑在同一个连接上,出异常时补发一条 rollback。真正的「要么全做要么全不做」「提交后不丢」,是数据库引擎在磁盘和日志层面完成的。换句话说,你把@Transactional加在一个不支持事务的存储引擎上(比如 MySQL 的 MyISAM 表),Spring 发再多 rollback 也没用——因为它根本没有可回滚的东西。(前半句是公开事实,后半句「MyISAM 失效」是 MySQL 常识,笔者未逐条对照文档,列为补充。)


3. 七种传播行为:语义、用途与坑,全在一张表里

视频里先给了一个定义上的提醒:面试题目往往只说「说一下事务传播行为」,不会帮你把「什么是传播行为」问出来,所以你得自己先交代——传播行为只在「两个事务方法嵌套调用」时才有意义,也就是外层一个事务方法里又调了内层一个事务方法,内层到底是「融入外层」「另开一个」「还是报错」,这个规则就是传播行为。

Spring 一共定义了七种。视频反复强调前三种(REQUIRED / SUPPORTS / REQUIRES_NEW)是必须能脱口而出的,其余能记多少说多少。我把它做成一张对照表,右边两列专门写「典型用途」和「容易踩的坑」:

传播行为外层有事务时外层无事务时典型用途常见坑
REQUIRED(默认)融入外层,共用一个事务自己开一个新事务增删改,要保证一起成功失败以为「开了注解就一定是新事务」,其实它只是融入
SUPPORTS融入外层不开事务,裸跑查询方法,单独调没必要开事务单独调用时无事务,隔离级别设了也白设
REQUIRES_NEW挂起外层,另开独立新事务自己开一个新事务记日志:外层成不成都要求日志落库内外是两个事务,外层回滚带不回内层已提交的
NOT_SUPPORTED挂起外层,以非事务方式跑非事务方式跑不需要事务、且想脱离外层事务的操作别指望它有原子性,出异常不会回滚
MANDATORY融入外层直接抛异常(要求外层必须有事务)强制「必须在事务里被调用」的方法单独调用它就炸,常被误用
NEVER直接抛异常(外层不能有事务)不开事务,正常跑明确禁止在事务上下文里执行的方法一旦它出现在事务链上就报错
NESTED在外层里开嵌套事务(savepoint)相当于 REQUIRED部分回滚:子回滚不影响父提交只有特定事务管理器支持,默认多为DataSourceTransactionManager

视频里对三个高频的解释很实在:REQUIRED 适用增删改;SUPPORTS 适用查询,因为单独调用查询没必要开事务,被事务方法调用时才融入;REQUIRES_NEW 适用「内外没有业务关联」的场景,最典型就是记日志——不管主业务成功失败都想把这条日志留下,所以自己另开一个事务,不受外层影响。这两个「独立事务」的语义,直接引出了源码里的「挂起 / 恢复」机制,放到第 6 节讲。

Q2:NESTED 和 REQUIRES_NEW 都能「自己搞一个」,区别在哪?

区别在它们和外层事务的关系。REQUIRES_NEW是彻底独立的两个事务:外层事务被挂起,内层自己提交,内层回滚不回滚外层、外层回滚也不影响已经提交的内层。NESTED用的是 savepoint(保存点),内层是外层的一个「子集」——内层回滚只回到 savepoint,外层还在;但外层一旦回滚,内层跟着一起没了。视频里提到 NESTED「比较复杂,可以自己控制哪些操作需要事务、哪些不需要」,本质就是 savepoint 的部分回滚能力。要注意它的适用范围很窄。

(七种常量名与语义、NESTED 仅DataSourceTransactionManager开箱支持、REQUIRES_NEW/NOT_SUPPORTED的挂起并非所有管理器都支持——均已对照官方 Propagation 文档核实。视频里「MANDATORY 就是外部一定要有事务」「NEVER 就是我看到外部开事务直接不干了」是口语版,对应官方语义一致。)


4. 隔离级别:Spring 的五档 ↔ 数据库的三种并发问题

视频对这块的态度很有意思:作者说自己不太想讲,因为不想把基础概念再念一遍,但面试会问,所以只做「总结 + 答题技巧」。隔离性对应的是并发事务下的三类读问题——脏读、不可重复读、幻影读。Spring 的隔离级别本质是对 JDBC 隔离级别的封装,一共五档(含 DEFAULT)。

先把三种并发问题用一个「账户转账」的例子讲清楚(视频用的就是转账 / 存取款的场景):

  • 脏读:事务二把 1000 改成 800 但还没提交,事务一进来查到了 800。读到了未提交的「脏」数据,一旦二回滚,一拿到的就是错的。
  • 不可重复读:事务一先读到 1000,事务二改成 800并提交了,事务一在同一事务里再读变成 800。同一个事务两次读同一行结果不一样。
  • 幻影读:事务一按条件查总额度读到 3000,事务二插入了一条新记录并提交,事务一再查总条数 / 总额对不上。它针对的是「整张表里符合条件的行集合」,而不是某一行。

这里就是最容易搞混的地方,视频专门点了一句:不可重复读针对「一行数据」被改,幻影读针对「整张表」被插入新行。所以解法也不同——不可重复读锁到行级别就够了,幻影读得把范围 / 表锁住。

隔离级别与三类问题的对应关系:

Spring 常量脏读不可重复读幻影读数据库大致怎么兜底
READ_UNCOMMITTED可能可能可能基本不加读锁,并发问题全开
READ_COMMITTED阻止可能可能只读已提交数据,解决脏读
REPEATABLE_READ阻止阻止可能视频说核心是「加锁」,锁住读过的一行
SERIALIZABLE阻止阻止阻止视频直白说「说白了就是表锁」
DEFAULT取决于数据库——用底层数据库自己的默认级别

关键一条:Spring 的隔离级别默认是DEFAULT,也就是跟随数据库。视频给了两个具体值——MySQL 默认是「可重复读」,Oracle 默认是「读已提交」。这属于数据库公开事实,也是面试最爱顺带问的一句。

Q3:给一个 SUPPORTS 的方法设 isolation,会生效吗?

不会。这是我在官方文档里核到的一条硬性约束:隔离级别只对「新开的事务」有意义。SUPPORTS单独调用时根本不开事务,NOT_SUPPORTED更是以非事务方式跑,你给它设READ_COMMITTED也是白设。视频没点破这条,但和第 3 节 SUPPORTS 单独调时无事务的说法是自洽的。(此条已对照@Transactional官方文档核实:isolation 仅适用于新开事务,实际只对 REQUIRED / REQUIRES_NEW 这类会创建事务的行为有意义。)

补充一点边界:视频用的是标准 SQL 模型(幻影读只有 SERIALIZABLE 挡得住),而 MySQL InnoDB 在其默认的 REPEATABLE_READ 下靠 next-key / gap lock 其实也能挡大部分幻影读。这属于 MySQL 引擎的实现细节,笔者未对照 MySQL 文档逐条核实,仅作提示,面试时按「标准模型」答最稳。


5. 声明式事务原理:动态代理 + ThreadLocal 复用同一条连接

这是全套题里难度拉满的一块。视频把它归到「你得看过源码才答得出」的档次,但主线其实只有一句话:Spring 声明式事务整套机制就是套在 AOP 上跑的,而事务的本质,是「同一线程复用同一条数据库连接」。

先看怎么用:@Configuration配置类上要先加@EnableTransactionManagement,才能在各方法上用@Transactional。视频强调——不加这个开关,注解是不生效的。

原理分三大步,和 AOP 完全对齐:解析 → 创建动态代理 → 调用增强。

第一步解析。AOP 里解析的是 advisor(切面 = pointcut 切点 + advice 通知),发生在 Bean 创建前的第一个后置处理器里。但事务「没有切面、也没有前后置通知」,它解析的是什么?视频给的答案是:@EnableTransactionManagement会导入一个配置类,这个配置类帮我们注册了一个针对事务的 advisor。所以事务的 advisor 不是你用@Around之类注解写出来的,而是它内置提供的。这个 advisor 也有 pointcut,只不过切点不是切点表达式,而是「你方法 / 类 / 接口上有没有@Transactional」。

第二步创建代理。在 Bean 初始化后、由 Bean 后置处理器创建动态代理,规则和 AOP 一样:有接口走 JDK 动态代理,没接口走 CGLIB。创建前拿 advisor 的 pointcut 去匹配@Transactional,匹配方式很宽——方法上有、类上有、接口 / 抽象类 / 父类上有,满足任意一个就给你建代理。

第三步调用增强,这才是事务真正发生的地方。代理把目标方法包了一层 try / catch:进来先在增强里拿一个Connection、把它setAutoCommit(false)(开启事务),执行目标方法里的 SQL,正常结束就 commit,抛异常就 rollback。视频那句「说白了它就是在动态代理的增强类里,为你包了一层事务」非常精准。

调用 @Transactional 方法 → 代理拦截:取连接 + setAutoCommit(false) 开事务 → 把 Connection 存进 ThreadLocal(以 DataSource 为 key) → try { 执行目标方法 / SQL } → 正常 commit,异常 rollback

为什么是 ThreadLocal?视频里 P03 和 P38 反复落到这个点:外层方法把连接存进ThreadLocal,就是为了嵌套调用的内层方法能从同一个ThreadLocal里取到同一个Connection——只有用同一条连接,才能保证是同一个事务。同时内层如果发现ThreadLocal里已经有值,就不会自己去提交,而是把提交权统一交给最外层。这套「按线程绑定资源」的机制,官方实现就是TransactionSynchronizationManager(下面第 6 节源码会点名它)。

这一层我核到了官方描述:TransactionSynchronizationManager以「每线程(per-thread)」方式把资源绑到当前线程,绑定用的 key 通常就是DataSource,value 是连接持有对象;同线程同事务里后续再取,拿到的就是同一个连接;事务结束时由事务管理器clear掉、解绑。这与视频讲的「存进 ThreadLocal、以 DataSource 为 key、取出复用」完全对得上。


6. 传播行为的源码实现:挂起、恢复、新建事务的分支

第 5 节讲清楚了「融入」,但视频明确指出:传播行为是在「调用动态代理」那一刻处理的,跟解析切面、创建代理没关系——所以本节只聚焦调用阶段。源码虽然复杂,但七种传播归纳起来就两种走向:要么融入外部事务,要么新建一个事务。视频就讲这两种。

要讲清这两个走向,先记住两个贯穿始终的关键点(视频反复强调「看过源码就记这两个」):

  1. 开启事务后会返回一个事务状态对象,源码里叫TransactionInfo(视频原话),里面有个关键布尔属性newTransaction——它是不是一个新事务。外层自己创建连接开事务时它为true;内层只是融入时它是false。
  2. 提交动作永远只提交「当前ThreadLocal里那条连接」。所以谁把连接放进 / 拿出ThreadLocal,谁就决定了这次事务到底提不提交。

走向 A:内嵌融入外层。外层创建Connection、setAutoCommit(false)、存入ThreadLocal、newTransaction=true,然后执行目标方法;目标方法里调了内嵌事务方法,又走一遍内嵌的动态代理增强。内嵌增强里先判断ThreadLocal上是不是已经有连接——有,说明外面已经有事务了;再判断当前传播行为是「融入」这一类,于是不创建新连接,newTransaction=false,直接用外层连接执行 SQL。内嵌方法执行完,到提交这一步会判newTransaction:是false→不提交,方法结束、控制权回到外层。外层继续往下走,最后判newTransaction=true,从ThreadLocal拿连接提交。整个过程就是一次提交,落在最外层。

走向 B:内嵌新建事务(对应 REQUIRES_NEW / NOT_SUPPORTED 的「挂起」)。前面几步一样:外层建连接、开事务、newTransaction=true、目标方法调用内嵌。区别在内嵌判断传播行为是「新建事务」这一类时,它做了一件关键的事:把外层事务的相关信息(连接、隔离级别、是否只读等)先「暂存」起来,也就是把外层ThreadLocal里存的事务信息置空,然后自己新建一条Connection、放进ThreadLocal——因为第 5 节说过,一个线程的ThreadLocal只保证有一条连接 / 一个事务。这个「暂存 + 置空 + 自建」的动作,就是官方所谓的挂起(suspend)。内嵌newTransaction=true,执行自己的 SQL,到提交时判为 true →提交自己;提交完,它再把「之前暂存的外层事务信息」回归回ThreadLocal——这就是恢复(resume),让外层重新占回当前线程。控制权回到外层后,外层同样判newTransaction=true,拿回自己的连接提交。

融入: 外层存连接 → 内嵌查到已有连接(newTransaction=false) → 不提交 → 外层统一提交 新建/挂起:外层存连接 → 内嵌「暂存外层信息 + 置空 + 自建新连接」→ 内嵌提交自己 → 「恢复外层信息」→ 外层再提交

视频也坦诚:这块「没看过源码听着会晕」。所以面试时不用背全,抓住三句话就能立住——连接存ThreadLocal、提交只提交ThreadLocal里的连接、newTransaction决定这次提不提交;新建事务比融入多了个「暂存 / 回归(挂起 / 恢复)」的概念。至于「外部存在事务却报异常」那类(NEVER / MANDATORY),视频直接说不是重点,略过。

这里回扣第 3 节那句「挂起不是所有管理器都支持」:官方 Propagation 文档明确写了REQUIRES_NEW/NOT_SUPPORTED的事务挂起在部分管理器上开箱不支持,尤其JtaTransactionManager要看具体是否配置了相应的TransactionSynchronization。也就是说上面这套「暂存 / 置空 / 恢复」的漂亮逻辑,落到JtaTransactionManager上不一定走得通。


7. 失效场景与多线程事务:Spring 兜不住的那些坑

前面全是「怎么生效」,这一节讲「什么时候不生效」,这也是最容易在面试里被追问、被要求「说一个你踩过的坑」的地方。视频作者给答题定调:把失效原因答全,说明你真在项目里踩过。

失效场景清单。视频把 P40 和 AOP 的失效思路绑定:因为事务本来就基于 AOP,所以凡是 AOP 失效的原因,事务同样失效。我做成了可勾选的清单,并补上了视频没明说、但面试常凑齐的几条:

失效场景根因解决办法
方法是private/ 非 public代理增强默认拦不到非公开方法,事务织不进去改成public
目标类没交给 Spring 管理不是 Bean,根本没为其创建代理加上@Service/@Component等
同类自调用(内部调用)没重新走代理,增强不触发,传播行为也失效(视频列为最关键)注入自身走代理,或开exposeProxy用AopContext.currentProxy()
异常被自己 catch 后没抛出代理增强没感知到异常,不会触发 rollback别吞异常,或手动setRollbackOnly
抛的是 checked 异常默认只对RuntimeException和Error回滚,checked 异常不回滚@Transactional(rollbackFor = Exception.class)
@Transactional标在接口方法上、却用 CGLIB 代理CGLIB 基于类,接口上的注解匹配不到注解标到实现类 / 实现方法上
传播行为「吞掉」了预期事务如误用 SUPPORTS/NEVER/NOT_SUPPORTED,以为开了其实没开核对第 3 节表格语义
存储引擎不支持事务底层无可回滚的东西(如 MyISAM 表)换成支持事务的引擎(InnoDB)
多线程执行事务信息绑在ThreadLocal,子线程拿不到(下详)见第 7 节后半 / 分布式方案

自调用这一条值得多说两句,因为它是「内部调用导致传播失效」的根源。视频的例子是:本类事务方法 A 里直接调本类的 B,而你把 B 的传播行为设成「重新开一个」(REQUIRES_NEW),结果 B 根本没另开事务。原因不是传播规则坏了,而是你走的是this.B()这种本类直接调用,没经过代理对象,那段「新建事务 / 挂起」的增强代码压根没执行。两种解法视频都给了:一是把自身再注入一遍(@Resource private XxxService self;),用self.B()走代理;二是在配置里启用 AOP 并「暴露代理到当前线程」,调用时通过AopContext.currentProxy()拿代理对象再调。(exposeProxy/AopContext的精确 API 名笔者未逐字对照源码,列为视频原话。)

再看 checked 异常这条——它是被问得最多、又最反直觉的:默认情况下你try里抛了个 checked 异常,事务不回滚。官方文档给的回滚默认规则很明确:不配自定义规则时,只对RuntimeException和Error回滚,checked 异常不回滚;想让 checked 也回滚,用rollbackFor(或rollbackForClassName)。所以视频说「把异常抛出去」还不够,抛对了类型 + 配对了rollbackFor才行。

Q4:Spring 用多线程跑事务,能保证一致性吗?

不能——视频里这题是作者一个学员去面试某大厂时碰到的原题,答案斩钉截铁:保证不了一致性,也保证不了原子性。原理就是第 5 节那句话:事务信息全在ThreadLocal里,一个线程只绑一条连接、只有一个事务。你在 A 里new Thread调 B,B 在自己的线程上,查不到 A 那个ThreadLocal里的连接,于是它自己另开一条连接、另起一个事务,两人各干各的。结果就是 B 抛异常只回滚 B,A 的数据照样提交——A 插进去了、B 回滚了,数据库里只剩一半。官方也写得很直白:@Transactional不会传播到方法内部新起的线程。

视频作者给的出路有两条(P39):一是自己用编程式事务手动控制提交回滚;二是走分布式事务的思路,用二阶段提交那套。这两条都指向同一个事实:多线程一致性不在 Spring 声明式事务的能力范围内。

「2 句代码」的解法(P03)——以及它到底没解决什么。P03 给了一个看起来像银弹的技巧,作者说「只要两句代码就能解决多线程事务失效」。它的本质是把外层事务的连接手动搬进子线程的ThreadLocal:

@ResourceprivateDataSourcedataSource;@TransactionalpublicvoidmethodA()throwsInterruptedException{// 第 1 句:取出外层事务已绑定到本线程的连接持有对象Objectholder=TransactionSynchronizationManager.getResource(dataSource);Threadt=newThread(()->{// 第 2 句:把它手动绑进子线程自己的 ThreadLocalTransactionSynchronizationManager.bindResource(dataSource,holder);methodB();// 走 B 的代理,读到同一个 connection});t.start();t.join();// 关键:父线程必须等子线程跑完,再统一提交}

视频里的演示效果确实对了:B 抛异常后,A 的那条也不提交了,数据库刷新成一致(要么都有要么都无)。它「解决」的是——让子线程复用外层那一条Connection,于是内外变成同一个事务,提交权统一收回到最外层。

但这里必须把话说透,它没解决的部分恰恰才是重点:

  • 它没有做多线程并行事务。你复用的是同一条 JDBC 连接,而一条Connection不是线程安全的。要让 A 线程和 B 线程同时往同一条连接上写 SQL,结果是不可预料的。所以你看代码里那句t.join()——父线程必须串行等待子线程跑完再提交。说白了这不是「并发事务」,而是「把一个本该串行的活儿挪到别的线程去跑,再拼回同一个连接」。子线程一多、或者两个线程真并发跑,方案立刻崩。
  • 它只管「事务边界」,不管分布式一致性。库换了、连接换了、跨服务了,这套bindResource就搬不过去了。视频作者自己也留了一句「解决方案其实还有很多种」,言下之意这两句是入门技巧不是终局。
  • 它绕过了 Spring 的正常资源管理。手动bindResource若不配对unbindResource,线程复用时(比如线程池)连接会残留、脏读上一个任务的上下文。生产环境里至少要补三件事:子线程用完解绑、保证串行、评估是否根本不该用多线程。

一句话理解:这两句代码不是「让 Spring 支持了多线程事务」,而是「你自己手动把子线程塞进外层那条连接的上下文里」——它把并发问题偷偷变成了串行。真正的多线程 / 跨服务一致性,还得回到「编程式手动编排」或「分布式事务(二阶段提交 / Seata 那一类)」上去解。


最后总结

  • ACID 归位:A / I / D 主要靠数据库引擎,Spring 只做「事务边界的编排者」,负责圈范围、发 commit / rollback、透传隔离级别。
  • 两种管理类型:编程式(TransactionTemplate/ 手动 JDBC)与声明式(注解 / XML);实现上分接口 / XML / 注解三条路,底层统一走PlatformTransactionManager家族。
  • 七种传播行为:REQUIRED / SUPPORTS / REQUIRES_NEW 是必背的三件套,REQUIRES_NEW 是「独立事务」,NESTED 是「savepoint 部分回滚」,MANDATORY / NEVER 是「条件不满足就抛异常」。
  • 隔离级别五档跟数据库走:DEFAULT跟随数据库(MySQL 默认 RR、Oracle 默认 RC),且隔离级别只对新开事务有效。
  • 原理主线:AOP 代理 +ThreadLocal持连接,事务本质 =同一线程复用同一条连接;TransactionSynchronizationManager是这套绑定的官方实现。
  • 传播的源码:记住newTransaction决定提不提交、提交只提交ThreadLocal里的连接;新建事务比融入多了「挂起 / 恢复」。
  • 失效与多线程:失效原因基本 = AOP 失效原因(非 public、自调用、吞异常、checked 异常、接口注解遇 CGLIB、Bean 没管理、引擎不支持);多线程 Spring 保证不了一致性,「2 句代码」是把并发偷偷掰成串行复用同一连接的技巧,不是银弹。

我个人在面试里最看重的一条判断标准是:候选人能不能把「事务 = 同一线程复用同一连接」这句因果链讲顺。能讲顺的人,传播行为、失效场景、多线程问题基本都不会答错;讲不顺的,背再多七种传播也经不起追问。

事实说明

  • 视频里给的事实:ACID 四条与转账 / 存取款例子;管理类型 = 编程式 + 声明式、实现方式 = 接口 / XML / 注解;七种传播行为语义及 REQUIRED / SUPPORTS / REQUIRES_NEW 三件套用途;三种并发读问题与「不可重复读针对行、幻影读针对表」的区分、MySQL 默认 RR / Oracle 默认 RC、SERIALIZABLE「就是表锁」、REPEATABLE_READ「核心是加锁」;声明式事务 = 套 AOP 三步(解析 advisor、创建动态代理、调用增强)、@EnableTransactionManagement导入配置类注册事务 advisor、连接存ThreadLocal以 DataSource 为 key;传播行为在调用动态代理时处理、TransactionInfo.newTransaction决定是否提交、新建事务的「暂存 / 置空 / 恢复」;失效原因(非 public、类未交 Spring 管理、吞异常、自调用走 this 不过代理、接口注解遇 CGLIB、rollbackFor未配、多线程);P39 结论 = 多线程保证不了一致性、可用编程式或二阶段提交;P03 的getResource+bindResource两句代码演示与效果。
  • 官方文档核实过的事实(Spring Framework 7.0.9 javadoc / reference):Propagation 七枚举常量语义与 REQUIRED 为默认、NESTED 仅DataSourceTransactionManager开箱支持、REQUIRES_NEW/NOT_SUPPORTED挂起并非所有管理器支持;Isolation 五常量语义与 DEFAULT = 跟随数据库;默认回滚只对RuntimeException+Error、checked 不回滚、可用rollbackFor/noRollbackFor覆盖;isolation 仅对新开事务有效;@Transactional不传播到方法内新建的线程;自调用绕过代理不触发事务;TransactionSynchronizationManager按线程绑定资源、key = DataSource、getResource/bindResource/unbindResource;Spring 提供跨 JDBC / JPA / Hibernate / JTA 的一致抽象。
  • 笔者判断 / 未核实:TransactionTemplate、TransactionProxyFactoryBean、exposeProxy/AopContext.currentProxy()的精确类名方法名为视频原话、笔者未逐字对照源码;「MyISAM 不支持事务导致失效」是 MySQL 常识、未对照 MySQL 文档;「InnoDB 在 RR 下靠 gap lock 挡幻影读」是引擎实现细节、未核实,仅作提示;PlatformTransactionManager实现类家族表为笔者依官方文档补充、视频未点名;P03「2 句代码」代码里的t.join()、finally解绑、以及对「Connection 非线程安全 / 实为串行」的分析为笔者补充判断,视频演示未展开。

参考资料 & 致谢

  • [1] Spring面经-哔哩哔哩(本系列素材来源,分 P:P03 / P33 / P34 / P35 / P36 / P37 / P38 / P39 / P40)
  • [2] Spring Framework 官方文档 · Transactions
  • [3] Spring Propagation 枚举 javadoc
  • [4] Spring Isolation 枚举 javadoc
  • [5] Spring @Transactional javadoc
  • [6] Spring TransactionSynchronizationManager javadoc
  • [7] Spring 项目主页
  • [8] MySQL 官网 ・ Oracle Database 官网 ・ Hibernate 官网 ・ Seata 官网

本文覆盖分 P

分 P 编号视频原始标题本文对应小节
P33事务四大特性第 1 节 ACID
P34Spring 支持的事务管理类型和实现方式第 2 节 管理类型与实现方式
P35说一下 Spring 的事务传播行为第 3 节 七种传播行为
P36说一下 spring 的事务隔离第 4 节 隔离级别
P37Spring 事务实现基本原理第 5 节 声明式事务原理
P38Spring 事务传播行为实现原理第 6 节 传播行为源码实现
P40Spring 事务的失效原因?第 7 节 失效场景清单
P39Spring 多线程事务能否保证事务的一致性第 7 节 Q4 多线程一致性
P032 句代码即可解决 Spring 多线程事务失效问题第 7 节 「2 句代码」解法
RELATED READING

延伸阅读

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