Spring 事务保证数据一致性

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)

  2. 手动强制回滚:TransactionStatus.setRollbackOnly()

  3. 回滚原理: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. 是否使用不同数据源(多数据源没配置事务管理器)?
相关推荐
foo1st2 小时前
MySQL 8.0(Windows)升级笔记
数据库·笔记·mysql
张人玉2 小时前
基于 Vue 3 + ECharts + Express + SQLite 的可视化大屏与业务管理系统——YOLOv8 高精度车辆行人检测与计数系统
数据库·vue.js·yolo·sqlite·echarts·express
Lakers-242 小时前
1987年5月29日晚上19-21点出生性格、运势和命运
java
吴声子夜歌2 小时前
MongoDB 4.x——高级特性(Change Stream和事务)
数据库·mongodb
是店小二呀2 小时前
NanoPi R4S怎么搭私人云盘?iStoreOS与WebDAV完整教程
数据库·人工智能
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题 第194题】【08_Kafka篇】第10题:简述 Kafka 的 Rebalance 机制?
java·kafka·消费者组·rebalance·分布式消息队列
__log2 小时前
幂等性设计:从“重复提交“到“稳如磐石“的系统防护
java·开发语言·spring boot
spider_xcxc2 小时前
Helm 部署 K8s 集群完整笔记
java·开发语言·kubernetes
Database_Cool_3 小时前
云数据库备份是怎么实现的、支持时间点恢复吗:阿里云 RDS MySQL 备份与 PITR 详解
数据库·mysql·阿里云