本地事务与分布式事务详解,不止是数据库事务

前言

在写项目的时候往往会添加@Transactional处理数据库事务,但是很多人不理解底层逻辑,只停留在加个注解就能回滚的层面;随着业务微服务拆分,原本单一数据库的本地事务彻底失效:下单扣库存、扣用户余额、生成物流单分散在订单、账户、仓储多个独立服务、多套数据库,此时单一本地事务无法保证跨服务数据一致性,分布式事务应运而生。

本文分为两大核心板块:

  1. 本地事务底层原理:数据库原生事务、Spring @Transactional 切面实现等
  2. 分布式事务完整拆解:基于 Redis 协调者的柔性事务、主流分布式方案对比等

一、本地事务:单JVM、单数据库的数据一致性保障

1.1数据库原生事务底层:BEGIN / COMMIT / ROLLBACK

事务的本质是数据库层提供的 ACID 机制,和框架无关,MySQL InnoDB 引擎完整支持事务四大特性:

ACID 核心定义

  1. 原子性 (Atomic):一组 SQL 要么全部执行成功,要么全部回滚,不存在中间状态;
  2. 一致性 (Consistent):事务执行前后,业务数据约束不变(比如余额不能为负数);
  3. 隔离性 (Isolate):多个并发事务互相隔离,互不干扰,通过隔离级别控制;
  4. 持久性 (Durable) :事务执行COMMIT提交后,修改永久落盘,宕机不会丢失。

原生SQL执行流程

复制代码
BEGIN; -- 开启事务,数据库创建事务快照,所有修改仅存在内存缓冲
UPDATE t_order SET status=1 WHERE id=1;
UPDATE t_account SET balance=balance-100 WHERE user_id=1;
COMMIT; -- 全部SQL执行无异常,持久化修改到磁盘,外部查询可见新数据
-- 若中间SQL报错,执行 ROLLBACK; 撤销所有未提交修改

关键:如果只写BEGIN,不执行COMMIT

  • 当前事务内的查询可以看到自己的修改;
  • 其他数据库连接、其他服务完全看不到未提交数据
  • 连接断开、程序异常时,数据库自动执行ROLLBACK,丢弃所有修改。

1.2 Spring@Transactional注解:切面封装原生事务

1.2.1 核心实现机制:AOP 动态切面

@Transactional 本质是 Spring AOP 动态代理对BEGIN/COMMIT/ROLLBACK的封装,执行流程:

  1. Spring 启动扫描所有带有@Transactional的类 / 方法,生成动态代理对象
  2. 外部调用业务方法时,先走代理切面逻辑;
  3. 切面逻辑:
    • 前置:获取数据库连接,执行底层BEGIN,开启本地事务;
    • 执行目标业务方法体(你的更新、插入 SQL);
    • 后置无异常:执行COMMIT,提交事务释放连接;
    • 后置捕获异常:执行ROLLBACK,撤销全部修改。

1.2.2简化代码逻辑

java 复制代码
// AOP切面代理逻辑伪代码
public Object invoke(MethodInvocation invocation) throws Throwable {
    Connection conn = getDbConnection();
    try {
        // 等价原生 BEGIN,关闭自动提交
        conn.setAutoCommit(false);
        // 执行开发者写的业务方法体
        Object result = invocation.proceed();
        // 无异常,提交事务
        conn.commit();
        return result;
    } catch (Exception e) {
        // 抛出异常,回滚所有SQL修改
        conn.rollback();
        throw e;
    } finally {
        // 释放数据库连接回连接池
        conn.close();
    }
}

1.2.3本地事务限制:只适用于单JVM+单数据库

本地事务生效的硬性约束:

  1. 所有操作使用同一个 DataSource、同一个数据库连接
  2. 所有业务逻辑运行在同一个应用实例(单个 JVM)

一旦出现以下场景,@Transactional 本地事务直接失效:

  • 微服务拆分,调用远程服务(订单服务调用账户服务);
  • 项目分库分表,操作多个独立 MySQL 库;
  • 多线程异步执行业务,子线程使用新数据库连接。

1.3总结

本地事务是单体应用的一致性解决方案,依托数据库原生 ACID,Spring AOP 简化开发,成本极低;但架构升级为微服务、多库之后,跨服务 / 跨库操作无法共用同一个数据库连接,本地事务彻底失去约束力,此时必须引入分布式事务。

二、分布式事务:多服务多数据源下的数据一致性方案

2.1 分布式事务诞生背景

举电商下单经典场景:

  1. 订单服务:新增订单记录(库 1);
  2. 远程调用账户服务:扣减用户余额(库 2);
  3. 远程调用库存服务:扣减商品库存(库 3)。

三个操作分属三个独立服务、三套 MySQL 数据库,三个独立本地事务。此时会出现数据不一致灾难场景:

  • 订单库提交成功,账户扣减成功,库存服务网络超时宕机,库存未扣减;
  • 订单创建失败,但账户余额已经被扣减,用户资产损失。

核心矛盾 :多个独立本地事务无法互相感知执行状态,无法统一提交 / 回滚,因此需要一个全局协调者统一管控所有分支事务。

2.2Redis协调性分布式事务

本文暂时只讲解Redis为主的分布式事务,并结合上图来展开讲解

整体架构角色

  1. 分支服务:订单系统、账户系统(每个服务拥有独立数据库、独立本地事务);(相当于部门,在合作一项任务时,都得向领导汇报进度)
  2. 协调者中间件:Redis,全局存储事务唯一编号、各分支执行状态;(相当于领导,等到全部部门完成对应任务后,才能分配下一个任务)
  3. 统一增强切面:每个服务增加分布式事务 AOP 切面,拦截本地事务提交逻辑,实现阻塞等待。

完整执行分步流程

步骤 1:发起全局事务,生成全局唯一事务编号 UUID

订单系统作为事务发起方,进入分布式事务方法,切面生成全局唯一事务 ID(uuid),将该事务编号写入 Redis 协调者,标记「全局事务进行中」。

步骤 2:各分支开启本地事务,携带事务编号远程调用
  1. 订单系统切面:执行BEGIN开启自身本地事务,执行订单入库的方法体 SQL;
  2. 订单远程 HTTP/Feign 调用账户服务,把全局事务编号随请求头传递给账户系统
  3. 账户服务接收请求,切面识别携带分布式事务 ID,同样执行BEGIN开启账户本地事务,执行扣余额方法体 SQL。

此时两个服务都已经执行完业务 SQL,但都不会立刻执行 COMMIT 提交本地事务,切面会暂停、阻塞提交流程。

步骤 3:各分支上报自身执行状态到 Redis 协调者

订单、账户各自执行完业务方法体后,向 Redis 写入当前分支执行状态:

  • 状态枚举:INIT(初始化) / RUNNING(执行中) / SUCCESS(分支执行成功) / FAIL(分支执行失败)
  • 例:订单执行无异常 → Redis 写入 tx_uuid:order=SUCCESS;账户执行无异常 → Redis 写入 tx_uuid:account=SUCCESS
步骤 4:切面循环查询 Redis,等待所有分支状态完成

订单、账户的切面会阻塞线程,循环向 Redis 查询当前全局事务下所有分支的执行状态

  1. 只要存在任意分支状态为FAIL:全局事务失败,所有分支执行本地ROLLBACK,释放阻塞;
  2. 所有分支状态全部为SUCCESS:满足提交条件,放行本地事务执行COMMIT
  3. 若部分分支长时间无状态(宕机、网络中断),依靠 Redis 过期 key / 定时补偿任务做回滚兜底。
步骤 5:全部分支就绪,统一放行提交本地事务

Redis 确认所有分支执行成功后,两个服务的 AOP 切面解除阻塞,执行各自本地事务的COMMIT,数据永久落库,分布式事务执行完成。

2.3 Redis 协调方案核心底层原理

1. 全局事务 ID 保证链路串联

全局 UUID 作为唯一索引存入 Redis,所有分支操作、状态记录都绑定该编号,协调者可以精准归集同一个分布式事务下所有服务的执行情况,区分不同并发的分布式事务,互不干扰。

2. AOP 切面拦截本地 COMMIT 是核心关键点

本地事务原生执行顺序:BEGIN → 执行业务SQL → 直接COMMIT 分布式事务改造后执行顺序:BEGIN → 执行业务SQL → 切面阻塞暂停 → 等待Redis全部分支成功 → 放行COMMIT 通过切面延迟提交本地事务,实现「所有分支全部就绪再统一提交」的分布式一致性逻辑。

3. Redis 的作用:全局状态存储器

Redis 仅做轻量状态协调,不执行业务逻辑,存储三类数据:

  1. 全局事务基础信息(创建时间、过期时间);
  2. 每个分支服务的执行状态;
  3. 分布式事务锁,防止重复上报状态、重复提交。

4. 阻塞切面的线程安全

切面阻塞采用CountDownLatch(我之前专门有一篇讲过,不了解的可以去看看,是主线程阻塞,等待N个子线程全部执行完毕,主线程再继续往下走)、循环轮询 Redis 实现等待,不会提前释放数据库连接,本地事务连接持续持有,直到收到全局提交 / 回滚指令,保证未提交数据对外不可见。

2.4优缺点

优点

  1. 实现轻量化:复用项目已有 Redis 中间件,不需要额外部署 Seata、XA 事务管理器;
  2. 业务侵入低:基于 AOP 切面封装,业务代码只需要增加注解,无需手动编写协调逻辑;
  3. 一致性可控:真正做到全部提交 / 全部回滚,满足业务基础一致性需求;
  4. 易于扩展:新增仓储、支付等分支服务,只需要引入统一切面依赖即可接入分布式事务。

原生缺陷(生产必须补充兜底方案)

  1. 服务宕机阻塞风险:若分支服务上报状态后宕机,线程永久阻塞,数据库连接长期占用,需要增加 Redis 过期 key + 定时补偿任务自动回滚超时事务;
  2. 轮询 Redis 有性能损耗:大量并发分布式事务会频繁查询 Redis,高并发场景需要优化轮询间隔、增加本地缓存状态;
  3. 长事务锁库:本地事务阻塞等待期间,数据库行锁持续持有,高并发场景容易产生锁等待、死锁;
  4. 仅适合短耗时分布式事务,跨分钟级长流程业务不推荐。

2.5主流的分布式事务对比

方案 协调组件 一致性强度 性能 适用场景 缺点
本文 Redis 协调柔性事务 Redis 最终一致性(阻塞准 2PC) 中等 中小微微服务、短流程下单业务 长事务锁行、需定时补偿
Seata AT 模式(主流工业级) Seata TC 事务协调器 最终一致性 电商、支付、大规模微服务 需要独立部署 TC 服务
XA 强一致性事务(2PC) 数据库事务管理器 强一致性 极低 金融核心、低并发账务 全局锁,并发阻塞严重
TCC 事务(手动补偿) 无中间件,代码实现 强最终一致 高并发金融支付 侵入业务代码,开发成本极高

结尾

事务是数据安全的底层基石,本地事务是分布式事务的基础,所有分布式事务本质都是对多个独立本地事务做统一调度管控 。理解@Transactional切面底层、数据库原生事务机制,才能看懂分布式事务协调者、阻塞切面的设计思路。