Spring 事务保证数据一致性
2026/7/24 19:27:46 网站建设 项目流程

Spring 事务保证数据一致性完整详解

一、底层基础:数据库原生事务 ACID

Spring 本身不具备事务能力,只是对 JDBC 事务、JTA 分布式事务做高层封装与管理,真正原子性、隔离性由数据库 InnoDB 引擎实现。

ACID 四大特性

  1. 原子性 Atomic:一系列操作要么全成功、要么全回滚,依托undo 日志实现回滚。
  2. 一致性 Consistent:事务执行前后,业务数据约束完全合法(主键、外键、唯一索引、余额非负等),是最终目标。
  3. 隔离性 Isolate:多事务并发互不干扰,依靠MVCC + 锁,4 种隔离级别。
  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 代理(普通类)。

核心执行流程
  1. 调用带@Transactional方法,Spring 生成代理对象拦截方法。
  2. 代理切面TransactionInterceptor触发事务管理器。
  3. 获取数据库连接,关闭自动提交(conn.setAutoCommit(false))。
  4. 执行业务 SQL。
  5. 正常走完:commit 提交;抛出指定异常:rollback 回滚
  6. 释放连接、恢复自动提交。

三、Spring 如何保障一致性:分层拆解

3.1 原子性保障(全部成功 / 全部失败)

1)正常提交流程
  1. AOP 拦截方法,从连接池拿到 Connection,关闭自动提交。
  2. 执行所有 SQL 写入 InnoDB 缓冲池。
  3. 方法正常结束,切面触发commit()
  4. redo 日志刷盘,事务持久化;binlog 同步主从。
2)异常回滚机制(最容易踩坑)
  1. 默认仅对RuntimeException、Error自动回滚;Checked 受检异常不回滚
// 指定所有异常都回滚 @Transactional(rollbackFor = Exception.class)
  1. 手动强制回滚:TransactionStatus.setRollbackOnly()
  2. 回滚原理: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 持久性保障(宕机不丢数据)

  1. redo 日志:事务修改先写 redo 缓冲区,定时刷盘,数据库崩溃重启后,用 redo 重做未刷入磁盘的数据。
  2. binlog 二进制日志:记录所有修改语句,用于主从复制、数据恢复。
  3. 两阶段提交(2PC)保证 redo 与 binlog 一致性:
  • 阶段 1:事务 prepare,刷 redo 日志;
  • 阶段 2:全部成功,提交 binlog,正式 commit。 宕机在 prepare 阶段,直接回滚;在 commit 阶段,完成持久化。

3.4 一致性(最终业务约束合法)

Spring 不直接管控业务约束,依靠两层保证:

  1. 数据库层:主键、唯一约束、外键、check 约束,非法操作直接抛异常,触发事务回滚。
  2. 业务层:事务包裹完整业务逻辑,扣款 + 加钱、主表 + 子表同时在一个事务内,不会出现一方成功一方失败。

四、事务传播机制(多事务嵌套一致性控制)

7 种传播属性,解决多个事务方法互相调用时,共用事务还是新建独立事务:

  1. REQUIRED(默认):有事务就加入,没有就新建。最常用,整体一个事务,一荣俱荣一损俱损。
  2. REQUIRES_NEW:每次新建独立事务,外层回滚不影响内层已提交事务。
  3. SUPPORTS:有事务就用,没有就非事务运行。
  4. NOT_SUPPORTED:强制非事务执行。
  5. MANDATORY:必须运行在已有事务中,否则报错。
  6. NEVER:禁止存在事务,有事务直接抛异常。
  7. NESTED:嵌套事务,基于 savepoint 保存点,子事务可单独回滚,不影响父事务。

五、本地事务 vs 分布式事务(跨库一致性)

1)单一库本地事务(Spring 声明式即可完全保证)

单 MySQL 库,一个 Connection,依靠 InnoDB+Spring AOP 事务,完全满足 ACID。

2)分布式场景(多库、多微服务、跨服务调用)

单纯@Transactional完全失效,会出现局部提交、局部失败,数据不一致,需要分布式方案:

  1. 2PC 两段提交:Seata AT 模式,强一致性,性能差。
  2. TCC:手动编写 Confirm/Cancel/Try,侵入业务,高性能。
  3. SAGA:长事务,补偿回滚,最终一致性。
  4. 本地消息表 / 可靠消息队列:最终一致性,主流业务选型。

六、Spring 事务完整执行时序总结

  1. 客户端调用业务方法 → 进入 Spring AOP 代理
  2. TransactionInterceptor 拦截,向 TM 申请事务资源
  3. 获取 JDBC 连接,关闭自动提交
  4. 执行业务所有 DML
  5. 无异常:执行 conn.commit (),持久化数据
  6. 抛出指定异常:conn.rollback (),undo 日志还原数据
  7. 释放连接,事务结束

七、高频面试核心考点

  1. @Transactional 为什么同类调用失效:只有外部调用才经过代理对象,内部 this 调用不走 AOP 切面。
  2. 默认只回滚运行时异常,必须手动指定 rollbackFor=Exception.class 才能捕获所有异常。
  3. Spring 事务只是代理管控连接提交回滚,底层一致性完全依赖 InnoDB 日志机制。
  4. 事务超时:timeout 超时后直接抛出异常回滚,防止长事务占用连接。
  5. 只读事务 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 触发自动回滚受检异常ExceptionIOException不会回滚!

修复

@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"; } }

快速排查事务失效自查清单(面试可直接背诵)

  1. 数据库表引擎是否 InnoDB?
  2. @Transactional 是否加在 public 方法?
  3. 是否同类内部 this 调用?
  4. 异常是否被 try-catch 吞掉没有外抛?
  5. 抛出受检异常有没有配置 rollbackFor = Exception.class?
  6. 是否多线程操作数据库?
  7. 传播行为 REQUIRES_NEW / NOT_SUPPORTED 是否误用?
  8. 是否使用不同数据源(多数据源没配置事务管理器)?

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询