Spring 事务保证数据一致性完整详解
一、底层基础:数据库原生事务 ACID
Spring 本身不具备事务能力,只是对 JDBC 事务、JTA 分布式事务做高层封装与管理,真正原子性、隔离性由数据库 InnoDB 引擎实现。
ACID 四大特性
- 原子性 Atomic:一系列操作要么全成功、要么全回滚,依托undo 日志实现回滚。
- 一致性 Consistent:事务执行前后,业务数据约束完全合法(主键、外键、唯一索引、余额非负等),是最终目标。
- 隔离性 Isolate:多事务并发互不干扰,依靠MVCC + 锁,4 种隔离级别。
- 持久性 Durable:提交后数据永久落地,依靠redo 崩溃恢复日志。
二、Spring 事务两大实现方式
1. 编程式事务(手动控制)
TransactionTemplate/PlatformTransactionManager手动开启、提交、回滚,灵活但侵入代码。
@Autowired private TransactionTemplate transactionTemplate; public void biz() { transactionTemplate.execute(status -> { // 数据库操作 if(异常条件){ status.setRollbackOnly(); //强制回滚 } return null; }); }2. 声明式事务(@Transactional,主流)
基于AOP 动态代理实现,无侵入,分为 JDK 动态代理(接口)、CGLIB 代理(普通类)。
核心执行流程
- 调用带
@Transactional方法,Spring 生成代理对象拦截方法。 - 代理切面
TransactionInterceptor触发事务管理器。 - 获取数据库连接,关闭自动提交(
conn.setAutoCommit(false))。 - 执行业务 SQL。
- 正常走完:commit 提交;抛出指定异常:rollback 回滚。
- 释放连接、恢复自动提交。
三、Spring 如何保障一致性:分层拆解
3.1 原子性保障(全部成功 / 全部失败)
1)正常提交流程
- AOP 拦截方法,从连接池拿到 Connection,关闭自动提交。
- 执行所有 SQL 写入 InnoDB 缓冲池。
- 方法正常结束,切面触发
commit()。 - redo 日志刷盘,事务持久化;binlog 同步主从。
2)异常回滚机制(最容易踩坑)
- 默认仅对RuntimeException、Error自动回滚;Checked 受检异常不回滚。
// 指定所有异常都回滚 @Transactional(rollbackFor = Exception.class)- 手动强制回滚:
TransactionStatus.setRollbackOnly() - 回滚原理:InnoDB 根据 undo 日志,把数据修改全部撤销。
经典失效场景(直接破坏原子性,数据不一致)
- 方法内部
try-catch吃掉异常,切面感知不到异常,不会自动回滚。 - 同类内方法自调用,AOP 代理失效,注解完全无效。
- 多线程异步操作,子线程不属于主线程事务,互不影响。
- 传播行为配置错误(REQUIRES_NEW 新开独立事务)。
3.2 隔离性保障:Spring 对接数据库 4 种隔离级别
通过@Transactional(isolation = Isolation.XXX)设置,本质是调用 JDBC 底层设置:
表格
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 底层实现 |
|---|---|---|---|---|
| READ_UNCOMMITTED | 允许 | 允许 | 允许 | 无锁控制 |
| READ_COMMITTED (MySQL 默认) | 禁止 | 允许 | 允许 | MVCC 快照读 |
| REPEATABLE_READ (InnoDB 默认) | 禁止 | 禁止 | 允许 | MVCC |
| SERIALIZABLE | 全部禁止 | 全部禁止 | 全部禁止 | 全表行锁 |
MVCC 多版本并发控制:不加锁读写,通过 undo 版本链 + read-view 实现快照读取,保证同一事务多次读取数据一致。
3.3 持久性保障(宕机不丢数据)
- redo 日志:事务修改先写 redo 缓冲区,定时刷盘,数据库崩溃重启后,用 redo 重做未刷入磁盘的数据。
- binlog 二进制日志:记录所有修改语句,用于主从复制、数据恢复。
- 两阶段提交(2PC)保证 redo 与 binlog 一致性:
- 阶段 1:事务 prepare,刷 redo 日志;
- 阶段 2:全部成功,提交 binlog,正式 commit。 宕机在 prepare 阶段,直接回滚;在 commit 阶段,完成持久化。
3.4 一致性(最终业务约束合法)
Spring 不直接管控业务约束,依靠两层保证:
- 数据库层:主键、唯一约束、外键、check 约束,非法操作直接抛异常,触发事务回滚。
- 业务层:事务包裹完整业务逻辑,扣款 + 加钱、主表 + 子表同时在一个事务内,不会出现一方成功一方失败。
四、事务传播机制(多事务嵌套一致性控制)
7 种传播属性,解决多个事务方法互相调用时,共用事务还是新建独立事务:
- REQUIRED(默认):有事务就加入,没有就新建。最常用,整体一个事务,一荣俱荣一损俱损。
- REQUIRES_NEW:每次新建独立事务,外层回滚不影响内层已提交事务。
- SUPPORTS:有事务就用,没有就非事务运行。
- NOT_SUPPORTED:强制非事务执行。
- MANDATORY:必须运行在已有事务中,否则报错。
- NEVER:禁止存在事务,有事务直接抛异常。
- NESTED:嵌套事务,基于 savepoint 保存点,子事务可单独回滚,不影响父事务。
五、本地事务 vs 分布式事务(跨库一致性)
1)单一库本地事务(Spring 声明式即可完全保证)
单 MySQL 库,一个 Connection,依靠 InnoDB+Spring AOP 事务,完全满足 ACID。
2)分布式场景(多库、多微服务、跨服务调用)
单纯@Transactional完全失效,会出现局部提交、局部失败,数据不一致,需要分布式方案:
- 2PC 两段提交:Seata AT 模式,强一致性,性能差。
- TCC:手动编写 Confirm/Cancel/Try,侵入业务,高性能。
- SAGA:长事务,补偿回滚,最终一致性。
- 本地消息表 / 可靠消息队列:最终一致性,主流业务选型。
六、Spring 事务完整执行时序总结
- 客户端调用业务方法 → 进入 Spring AOP 代理
- TransactionInterceptor 拦截,向 TM 申请事务资源
- 获取 JDBC 连接,关闭自动提交
- 执行业务所有 DML
- 无异常:执行 conn.commit (),持久化数据
- 抛出指定异常:conn.rollback (),undo 日志还原数据
- 释放连接,事务结束
七、高频面试核心考点
- @Transactional 为什么同类调用失效:只有外部调用才经过代理对象,内部 this 调用不走 AOP 切面。
- 默认只回滚运行时异常,必须手动指定 rollbackFor=Exception.class 才能捕获所有异常。
- Spring 事务只是代理管控连接提交回滚,底层一致性完全依赖 InnoDB 日志机制。
- 事务超时:timeout 超时后直接抛出异常回滚,防止长事务占用连接。
- 只读事务 readonly=true:数据库优化,禁止修改操作。
Spring @Transactional 事务失效场景 + 复现代码 + 修复方案
环境:SpringBoot 2.7+/3.x + MyBatis/MyBatis-Plus + MySQL InnoDB
前提:数据库引擎必须是 InnoDB,MyISAM 不支持事务!
实体简单示例(用户表)
@Data public class User { private Long id; private String name; }Mapper
@Mapper public interface UserMapper { int insert(User user); }场景 1:同类内部调用(this 自调用,最常考)
错误代码
@Service public class UserService { @Autowired private UserMapper userMapper; public void outer() { // this调用,不走AOP代理,@Transactional失效 inner(); } @Transactional(rollbackFor = Exception.class) public void inner() { User u1 = new User(); u1.setName("张三"); userMapper.insert(u1); // 抛出异常,期望回滚,实际不会回滚! throw new RuntimeException("出错"); } }测试调用userService.outer():数据库插入成功,事务不回滚。
原因
AOP 事务依靠代理对象;this.inner()是原生对象调用,没有经过TransactionInterceptor切面拦截,事务逻辑完全不执行。
修复方案(任选其一)
方案 1:自己注入自身(推荐)
@Service public class UserService { @Autowired private UserMapper userMapper; // 注入代理对象 @Autowired private UserService self; public void outer() { self.inner(); // 使用代理调用 } @Transactional(rollbackFor = Exception.class) public void inner() { User u1 = new User(); u1.setName("张三"); userMapper.insert(u1); throw new RuntimeException("出错"); } }方案 2:拆分到不同 Service 方案 3:开启expose-proxy,使用((UserService) AopContext.currentProxy()).inner()启动类需要配置:
spring.aop.proxy-target-class=true<aop:aspectj-autoproxy expose-proxy="true"/>场景 2:异常被 try-catch 捕获,切面感知不到异常
错误代码
@Service public class UserService { @Autowired private UserMapper userMapper; @Transactional(rollbackFor = Exception.class) public void addUser() { try { User u1 = new User(); u1.setName("李四"); userMapper.insert(u1); int i = 1 / 0; // 算术异常 } catch (Exception e) { e.printStackTrace(); // 吃掉异常,没有向外抛出!事务不会回滚 } } }现象:插入成功,数据持久化,不会回滚。
修复两种方式
方式 1:catch 后重新抛出
catch (Exception e) { throw new RuntimeException(e); }方式 2:手动标记回滚(不抛出异常也能回滚)
catch (Exception e) { // 手动告知事务管理器需要回滚 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); }场景 3:只抛出受检异常(Exception,非 RuntimeException),未配置 rollbackFor
错误代码
// ❌ 没有 rollbackFor @Transactional public void test() throws Exception { User u = new User(); u.setName("王五"); userMapper.insert(u); throw new Exception("受检异常"); }Spring 默认规则:仅 RuntimeException / Error 触发自动回滚受检异常
Exception、IOException不会回滚!
修复
@Transactional(rollbackFor = Exception.class)场景 4:传播行为配置错误 REQUIRES_NEW 理解误区
@Service public class UserService { @Autowired private UserMapper userMapper; @Transactional(rollbackFor = Exception.class) public void outer() { User u1 = new User(); u1.setName("外层数据"); userMapper.insert(u1); try { inner(); } catch (Exception e) { // 捕获内层异常 } // 外层正常结束,外层事务提交 } @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class) public void inner() { User u2 = new User(); u2.setName("内层数据"); userMapper.insert(u2); throw new RuntimeException("内层异常"); } }现象:
- inner 独立事务,异常回滚,内层数据无记录
- outer 事务不受影响,外层数据成功入库
很多人误以为内层异常会让外层回滚,REQUIRES_NEW 互相隔离!
场景 5:方法访问权限不是 public
错误代码
@Service public class UserService { @Autowired private UserMapper userMapper; // private / protected / default 包访问权限,事务失效 @Transactional(rollbackFor = Exception.class) private void addUser() { User u = new User(); u.setName("赵六"); userMapper.insert(u); throw new RuntimeException(); } }原因:Spring AOP 只能拦截public方法,非 public 不会生成代理增强。 ✅ 修复:方法改成 public。
场景 6:多线程异步场景(拓展高频坑)
@Transactional(rollbackFor = Exception.class) public void testThread() { new Thread(() -> { User u = new User(); u.setName("线程数据"); userMapper.insert(u); throw new RuntimeException(); }).start(); }现象:子线程抛出异常,主线程事务不会回滚,子线程操作独立连接,不属于当前事务。
事务和数据库连接绑定 ThreadLocal,不同线程连接不同,天然无法共享事务。 解决:分布式事务方案 Seata AT / 可靠消息,不要指望本地事务跨线程。
配套测试 Controller
@RestController @RequestMapping("/tx") public class TxController { @Autowired private UserService userService; @GetMapping("/test1") public String test1(){ userService.outer(); return "ok"; } }快速排查事务失效自查清单(面试可直接背诵)
- 数据库表引擎是否 InnoDB?
- @Transactional 是否加在 public 方法?
- 是否同类内部 this 调用?
- 异常是否被 try-catch 吞掉没有外抛?
- 抛出受检异常有没有配置 rollbackFor = Exception.class?
- 是否多线程操作数据库?
- 传播行为 REQUIRES_NEW / NOT_SUPPORTED 是否误用?
- 是否使用不同数据源(多数据源没配置事务管理器)?