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 是否误用?
- 是否使用不同数据源(多数据源没配置事务管理器)?