分布式事务

目录

分布式事务概述

[ACID 特性](#ACID 特性)

[一、ACID 概述](#一、ACID 概述)

二、原子性(Atomicity)

[2.1 定义与出处](#2.1 定义与出处)

[2.2 核心机制](#2.2 核心机制)

[2.3 应用场景](#2.3 应用场景)

[2.4 详细示例](#2.4 详细示例)

三、一致性(Consistency)

[3.1 定义与出处](#3.1 定义与出处)

[3.2 核心机制](#3.2 核心机制)

[3.3 应用场景](#3.3 应用场景)

[3.4 详细示例](#3.4 详细示例)

四、隔离性(Isolation)

[4.1 定义与出处](#4.1 定义与出处)

[4.2 核心机制](#4.2 核心机制)

[4.3 应用场景](#4.3 应用场景)

[4.4 详细示例](#4.4 详细示例)

五、持久性(Durability)

[5.1 定义与出处](#5.1 定义与出处)

[5.2 核心机制](#5.2 核心机制)

[5.3 应用场景](#5.3 应用场景)

[5.4 详细示例](#5.4 详细示例)

[六、ACID 各特性的权衡与总结](#六、ACID 各特性的权衡与总结)

事务的隔离级别 (读未提交、读已提交、可重复读、串行化)

[1. 读未提交(READ UNCOMMITTED)](#1. 读未提交(READ UNCOMMITTED))

[2. 读已提交(READ COMMITTED)](#2. 读已提交(READ COMMITTED))

[3. 可重复读(REPEATABLE READ)](#3. 可重复读(REPEATABLE READ))

一、基本概念

二、为什么需要它

三、典型流程

四、协调模式

1)编排式(Orchestration)

2)协同式(Choreography)

五、设计要点

1)补偿操作必须幂等

2)补偿不保证完全对称

3)需要持久化事务日志

4)隔离性问题

六、适用场景

七、一句话总结

[4. 串行化(SERIALIZABLE)](#4. 串行化(SERIALIZABLE))

四种隔离级别对比总结

各数据库默认隔离级别

如何选择合适的隔离级别

[悲观锁(Pessimistic Locking)](#悲观锁(Pessimistic Locking))

[乐观锁(Optimistic Locking)](#乐观锁(Optimistic Locking))

[悲观锁 vs 乐观锁对比](#悲观锁 vs 乐观锁对比)

并发控制:乐观锁、悲观锁

一、并发控制的背景与出处

二、并发控制的两大思路

[三、悲观锁(Pessimistic Locking)](#三、悲观锁(Pessimistic Locking))

[3.1 定义](#3.1 定义)

[3.2 常见实现方式](#3.2 常见实现方式)

[3.3 应用场景](#3.3 应用场景)

[3.4 详细示例:库存扣减](#3.4 详细示例:库存扣减)

[3.5 优缺点](#3.5 优缺点)

[四、乐观锁(Optimistic Locking)](#四、乐观锁(Optimistic Locking))

[4.1 定义](#4.1 定义)

[4.2 常见实现方式](#4.2 常见实现方式)

[4.3 应用场景](#4.3 应用场景)

[4.4 详细示例:账户余额更新(版本号)](#4.4 详细示例:账户余额更新(版本号))

[4.5 优缺点](#4.5 优缺点)

[五、悲观锁 vs 乐观锁对比](#五、悲观锁 vs 乐观锁对比)

六、如何选择

适合悲观锁的情况

适合乐观锁的情况

七、工程实践建议

八、一句话总结

分布式事务基础理论

[2.1 CAP 定理详解-三选二的误区 (实际是 CP vs AP)](#2.1 CAP 定理详解-三选二的误区 (实际是 CP vs AP))

一、为什么说"三选二"是误区

[二、为什么实际更像是 CP vs AP](#二、为什么实际更像是 CP vs AP)

三、怎么理解这个取舍

[CP 系统](#CP 系统)

[AP 系统](#AP 系统)

四、一句话总结

[五、典型 CP 系统](#五、典型 CP 系统)

ZooKeeper

HBase

[六、典型 AP 系统](#六、典型 AP 系统)

Eureka

Cassandra

[七、CP vs AP 简单对比](#七、CP vs AP 简单对比)

[2.2 BASE 理论](#2.2 BASE 理论)

一、如何从强一致性降级为最终一致性

二、软状态与异步确保

[2.3 一致性模型分类](#2.3 一致性模型分类)

[一、强一致性(Strong Consistency)](#一、强一致性(Strong Consistency))

[二、弱一致性(Weak Consistency)](#二、弱一致性(Weak Consistency))

[三、最终一致性(Eventual Consistency)](#三、最终一致性(Eventual Consistency))

[四、因果一致性(Causal Consistency)](#四、因果一致性(Causal Consistency))

[五、会话一致性(Session Consistency)](#五、会话一致性(Session Consistency))

六、一致性模型对比

经典解决方案模式

[3.1 两阶段提交 (2PC)](#3.1 两阶段提交 (2PC))

[一、2PC 的基本概念](#一、2PC 的基本概念)

[二、2PC 的角色](#二、2PC 的角色)

1)协调者(Coordinator)

[2)参与者(Participant / Resource Manager)](#2)参与者(Participant / Resource Manager))

[三、2PC 的流程](#三、2PC 的流程)

[第一阶段:准备阶段(Prepare Phase)](#第一阶段:准备阶段(Prepare Phase))

[第二阶段:提交阶段(Commit / Rollback Phase)](#第二阶段:提交阶段(Commit / Rollback Phase))

四、简单示例

第一阶段

第二阶段

[五、2PC 的优点](#五、2PC 的优点)

1)实现思路清晰

2)能保证强一致性

3)适合关键事务场景

[六、2PC 的缺点](#六、2PC 的缺点)

1)同步阻塞严重

2)协调者单点问题

3)存在阻塞风险

4)性能较差

七、一句话总结

[3.2 三阶段提交 (3PC)](#3.2 三阶段提交 (3PC))

[一、3PC 的基本概念](#一、3PC 的基本概念)

[二、3PC 相比 2PC 的改进点](#二、3PC 相比 2PC 的改进点)

1)把原来的两阶段拆成三阶段

2)引入超时机制

[三、3PC 的流程](#三、3PC 的流程)

第一阶段:CanCommit

第二阶段:PreCommit

第三阶段:DoCommit

[四、3PC 的简单理解](#四、3PC 的简单理解)

[五、3PC 与 2PC 的对比](#五、3PC 与 2PC 的对比)

[2PC 的问题](#2PC 的问题)

[3PC 的改进](#3PC 的改进)

[六、3PC 的局限性](#六、3PC 的局限性)

1)仍然依赖协调者

2)网络分区下仍可能出错

3)工程上应用不多

七、一句话总结

[3.3 TCC (Try-Confirm-Cancel)](#3.3 TCC (Try-Confirm-Cancel))

[一、TCC 的基本概念](#一、TCC 的基本概念)

[二、TCC 的三阶段含义](#二、TCC 的三阶段含义)

[1)Try 阶段](#1)Try 阶段)

[2)Confirm 阶段](#2)Confirm 阶段)

[3)Cancel 阶段](#3)Cancel 阶段)

[三、TCC 的典型流程](#三、TCC 的典型流程)

第一步:Try

第二步:Confirm

第三步:Cancel(异常时执行)

[四、TCC 的优点](#四、TCC 的优点)

1)不依赖底层数据库事务

2)控制力强

3)适合高一致性业务

[五、TCC 的缺点](#五、TCC 的缺点)

1)业务侵入性强

2)设计复杂

3)对业务建模要求高

六、适用场景

[七、与 2PC / 可靠消息的区别](#七、与 2PC / 可靠消息的区别)

[和 2PC 的区别](#和 2PC 的区别)

和最终一致性消息方案的区别

八、一句话总结

[3.4 可靠消息最终一致性 (本地消息表/事务消息)](#3.4 可靠消息最终一致性 (本地消息表/事务消息))

一、基本概念

二、为什么需要它

三、本地消息表方案

1)核心思路

2)典型流程

3)优点

4)缺点

四、事务消息方案

1)核心思路

2)典型流程

3)回查机制

4)优点

5)缺点

[五、本地消息表 vs 事务消息](#五、本地消息表 vs 事务消息)

六、适用场景

七、一句话总结

[3.5 最大努力通知](#3.5 最大努力通知)

一、基本概念

二、为什么需要它

三、典型流程

四、设计要点

1)通知必须幂等

2)通知内容应精简

3)重试策略要合理

4)需要监控和告警

五、适用场景

六、一句话总结

[3.6 SAGA 事务](#3.6 SAGA 事务)

一、基本概念

二、为什么需要它

三、典型流程

四、协调模式

1)编排式(Orchestration)

2)协同式(Choreography)

五、设计要点

1)补偿操作必须幂等

2)补偿不保证完全对称

3)需要持久化事务日志

4)隔离性问题

六、适用场景

七、一句话总结

主流框架与实现

[4.1 Seata 详解](#4.1 Seata 详解)

[一、Seata 简介](#一、Seata 简介)

[二、Seata 的四种事务模式](#二、Seata 的四种事务模式)

[1)AT 模式(默认)](#1)AT 模式(默认))

[2)TCC 模式](#2)TCC 模式)

[3)SAGA 模式](#3)SAGA 模式)

[4)XA 模式](#4)XA 模式)

[三、Seata 架构三组件](#三、Seata 架构三组件)

[4.2 本地消息表 + 定时任务实现](#4.2 本地消息表 + 定时任务实现)

一、方案概述

二、表结构设计

三、核心流程

1)写入消息(与业务同事务)

2)定时任务扫描投递

四、优点与缺点

五、与事务消息的区别

核心技术难点

[5.1 幂等性设计](#5.1 幂等性设计)

一、为什么需要幂等性

二、幂等性方案

1)唯一索引

2)乐观锁版本号

3)去重表

4)状态机约束

三、方案选择建议

[5.2 空回滚与防悬挂 (TCC 特有)](#5.2 空回滚与防悬挂 (TCC 特有))

一、空回滚

什么是空回滚

解决方案

二、防悬挂

什么是悬挂

解决方案

三、总结记忆

[5.3 事务状态存储与恢复](#5.3 事务状态存储与恢复)

一、为什么要存储事务状态

二、状态存储方案

1)数据库存储(推荐)

2)日志存储

[3)Redis 存储](#3)Redis 存储)

三、事务恢复流程

四、注意事项

[4.1 Seata 详解](#4.1 Seata 详解)

[一、Seata 简介](#一、Seata 简介)

[二、Seata 的四种事务模式](#二、Seata 的四种事务模式)

[1)AT 模式(默认)](#1)AT 模式(默认))

[2)TCC 模式](#2)TCC 模式)

[3)SAGA 模式](#3)SAGA 模式)

[4)XA 模式](#4)XA 模式)

[三、Seata 架构三组件](#三、Seata 架构三组件)

全局锁导致的写隔离

一、基本概念

二、它如何实现写隔离

三、典型执行流程

四、重试与清理机制

五、优缺点

[读隔离:快照读 vs 当前读(Seata AT 模式分析)](#读隔离:快照读 vs 当前读(Seata AT 模式分析))

一、快照读与当前读

[二、Seata AT 与读隔离的关系](#二、Seata AT 与读隔离的关系)

三、可能出现的风险

四、常见处理手段

五、一句话总结

不可重复读与脏读风险控制

一、问题来源

二、脏读如何控制

三、不可重复读如何控制

四、全局层面的工程做法

五、一句话总结

[7.1 降级与熔断](#7.1 降级与熔断)

一、为什么要降级

二、何时触发降级

三、典型方案

[四、Fallback 设计](#四、Fallback 设计)

五、权衡点

[7.2 规避分布式事务的长路径](#7.2 规避分布式事务的长路径)

一、为什么长路径危险

二、设计原则

三、常见优化方式

四、事件驱动替代

五、一句话总结

[7.3 异步化与最终一致性设计](#7.3 异步化与最终一致性设计)

一、核心思路

二、典型实现模式

三、设计要点

四、何时适合最终一致性

五、不适用场景

[8.1 电商下单场景(订单+库存+积分+优惠券)](#8.1 电商下单场景(订单+库存+积分+优惠券))

一、推荐拆分方式

二、一个可落地流程

三、适合的事务模式

四、异常处理

五、一句话总结

[8.2 银行转账跨行场景](#8.2 银行转账跨行场景)

一、业务特点

二、常见方案

三、对账与恢复

四、异常处理

五、一句话总结

[8.3 支付回调处理](#8.3 支付回调处理)

一、核心问题

二、典型处理流程

三、重试与通知

四、常见风险控制

五、一句话总结

总结与选型建议

一、选型核心原则

二、常见方案对比

三、如何决策

四、工程上的真实建议

五、一句话总结

延伸阅读与进阶方向

[共识算法(Paxos, Raft)与分布式事务的区别](#共识算法(Paxos, Raft)与分布式事务的区别)

一、解决的问题不同

二、关注点不同

三、关系

分布式事务与分布式锁的位置关系

一、能力边界不同

二、不要混用概念

三、典型例子

[云原生下的事务趋势:Transaction Mesh(事务网格)](#云原生下的事务趋势:Transaction Mesh(事务网格))

一、基本思路

二、潜在价值

三、现实挑战


分布式事务概述

ACID 特性

一、ACID 概述

ACID 是数据库事务正确执行的四个基本特性的缩写,由以下学者在 1983 年正式提出并标准化:

  • Theo Härder 和 Andreas Reuter 在论文 "Principles of Transaction-Oriented Database Recovery" 中系统定义了这些概念

  • 其思想源于 Jim Gray 在 1970 年代对事务处理系统的开创性研究(图灵奖得主)

本质目的:保证数据库在并发访问和系统故障下,数据的完整性和一致性。


二、原子性(Atomicity)

2.1 定义与出处
  • 定义:事务中的所有操作,要么全部成功提交(Commit),要么全部失败回滚(Rollback),不存在部分执行的状态。

  • 英文原意:"All or nothing"

2.2 核心机制

|----------------|----------------------------------------|
| 机制 | 说明 |
| Undo Log(回滚日志) | 记录修改前的旧值,回滚时用于恢复 |
| 事务状态表 | 记录事务处于 Active / Committed / Aborted 状态 |
| 崩溃恢复流程 | 启动时扫描日志,未提交的事务全部回滚 |

2.3 应用场景

✅ 必须使用原子性的场景:

  • 银行转账(A 账户扣款 + B 账户加款,必须同时成功或失败)

  • 电商下单(扣库存 + 创建订单 + 扣积分 --- 要么全做,要么全不做)

  • 账务系统记账(借方 + 贷方 + 流水记录,三者一致)

❌ 可容忍非原子性的场景(通常是业务层面补偿):

  • 日志记录系统(丢失几条日志可接受)

  • 数据仓库 ETL 的中间临时表

2.4 详细示例

场景:用户 A 向用户 B 转账 100 元

复制代码
-- 开启事务 BEGIN TRANSACTION; 
-- 操作1:从 A 账户扣款 
UPDATE account SET balance = balance - 100 WHERE user = 'A'; 
-- 操作2:向 B 账户加款 
UPDATE account SET balance = balance + 100 WHERE user = 'B'; 
-- 操作3:记录流水 
INSERT INTO transfer_log (from_user, to_user, amount, time) VALUES ('A', 'B', 100, NOW()); 

-- 提交事务 COMMIT;

原子性如何保证:

|-----------------------------|----------------------------------|
| 情况 | 数据库行为 |
| 全部语句执行成功 → COMMIT | 所有修改持久化生效 |
| 执行 UPDATE account 后,数据库突然断电 | 重启后 Undo Log 回滚已扣款,账户 A 余额恢复原值 |
| 操作2执行遇到约束错误(如 B 账户不存在) | 自动执行 ROLLBACK,A 账户恢复原值,流水记录也不会写入 |
| 开发人员主动执行 ROLLBACK | 所有修改撤销 |

不满足原子性的后果:

  • A 扣了 100 元,B 没增加 → 资金丢失

  • B 增加了 100 元,A 没扣 → 银行亏钱


三、一致性(Consistency)

3.1 定义与出处
  • 定义:事务执行前后,数据库从一个一致的状态转变为另一个一致的状态。即所有定义的规则(约束、触发器、级联、业务规则)都满足。

  • 重要澄清:这是数据库 ACID 中的一致性,与 CAP 中的 C(线性一致性)不同。DB 一致性靠业务逻辑 + 约束保证。

3.2 核心机制

|----------|---------------------------|
| 机制 | 说明 |
| 完整性约束 | 主键、外键、唯一索引、CHECK、NOT NULL |
| 业务层逻辑 | 原子性 + 隔离性 共同服务业务一致性 |
| 触发器/存储过程 | 自动维护关联数据的一致性 |

3.3 应用场景

✅ 强一致性要求的场景:

  • 金融系统:资产不能凭空产生或消失(借贷平衡)

  • 订单系统:订单总金额 = sum(商品价格 × 数量) + 运费 - 优惠

  • 库存系统:库存数 >= 0,不允许负数

❌ 弱一致性可接受的场景:

  • 社交媒体的点赞数(允许短暂不精确)

  • 网站的 PV/UV 统计(最终一致即可)

3.4 详细示例

场景:电商订单表 + 订单明细表保持一致性

sql

复制代码
-- 建表时定义约束(数据库层面保证数据一致性)
CREATE TABLE orders (
    order_id   VARCHAR(32) PRIMARY KEY,
    total_amount DECIMAL(10,2) NOT NULL CHECK (total_amount >= 0),
    status     VARCHAR(20) NOT NULL,
    user_id    INT NOT NULL
);

事务操作:

复制代码
BEGIN;

INSERT INTO orders (order_id, total_amount, status, user_id) 
VALUES ('ORD001', 250.00, 'PAID', 1001);

INSERT INTO order_items (item_id, order_id, product_name, price, quantity) 
VALUES (1, 'ORD001', '手机', 1000.00, 2);   -- 总价2000,但订单总金额250?

-- 此时违反业务一致性:订单总金额 ≠ 明细总金额
-- 应当禁止提交

COMMIT;

一致性保证方式:

  • 数据库层面:CHECK、外键约束可拦截明显错误

  • 应用层面:在 COMMIT 前验证 SUM(price*quantity) = total_amount,否则 ROLLBACK

  • 事务边界内:隔离性避免了中途看到不一致中间态

一致性的核心思想:事务执行前后,所有定义的规则必须成立。


四、隔离性(Isolation)

4.1 定义与出处
  • 定义:并发执行的事务之间不会互相干扰。事务 A 在提交前,其修改对其他事务是不可见的(取决于隔离级别)。

  • 标准定义者:ANSI SQL-92 标准中正式定义了四种隔离级别及对应的并发现象。

4.2 核心机制

|---------------|-----------------------------------------------------------------|
| 技术 | 说明 |
| 锁机制 | 共享锁(读锁)、排他锁(写锁)、意向锁、间隙锁 |
| MVCC(多版本并发控制) | 每个读操作看到的是快照版本,不加锁,提高并发 |
| 隔离级别 | READ UNCOMMITTED, READ COMMITTED, REPEATABLE READ, SERIALIZABLE |

三种并发问题:

|-------|----------------------|
| 现象 | 说明 |
| 脏读 | 读到未提交的数据(万一回滚就不存在) |
| 不可重复读 | 同一事务内两次读取同一行,数据不一致 |
| 幻读 | 同一事务内两次范围查询,结果集行数不一致 |

四种隔离级别对比:

|----------------------------|----|-------|------------------------|------|
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 并发性能 |
| READ UNCOMMITTED | ✅ | ✅ | ✅ | 最高 |
| READ COMMITTED (Oracle 默认) | ❌ | ✅ | ✅ | 高 |
| REPEATABLE READ (MySQL 默认) | ❌ | ❌ | ❌* (InnoDB 通过间隙锁避免幻读) | 中 |
| SERIALIZABLE | ❌ | ❌ | ❌ | 最低 |

4.3 应用场景
  • 读已提交(RC):大多数 OLTP 系统,可接受不可重复读(如订单查询)

  • 可重复读(RR):需要对账、统计分析、多次读取数据一致的场景

  • 可串行化:银行日终结算、票务超卖严格控制场景(但并发极低)

4.4 详细示例

场景:两个事务同时操作同一账户余额,判断隔离性的影响。

初始数据:账户 A 余额 = 100 元


脏读示例(READ UNCOMMITTED):

|----|--------------------------------------|--------------------------------------|------------------|
| 时间 | 事务 1 | 事务 2 | 现象 |
| T1 | BEGIN | | |
| T2 | UPDATE A SET balance = balance - 100 | | 余额变为 0(未提交) |
| T3 | | SELECT balance FROM A WHERE user='A' | 读到余额 0(脏数据) |
| T4 | ROLLBACK(事务1回滚) | | |
| T5 | | 基于余额 0 做转账 | ❌ 使用了不存在的数据,资金错误 |

解决方案:升级到 READ COMMITTED,事务 2 只能读取已提交的数据。


不可重复读示例(READ COMMITTED):

|----|-------------------------------------|-----------------------------------|
| 时间 | 事务 1 | 事务 2 |
| T1 | BEGIN | |
| T2 | SELECT balance FROM A → 100 | |
| T3 | | UPDATE A SET balance = 200;COMMIT |
| T4 | SELECT balance FROM A → 200(两次结果不同) | |
| T5 | 基于第一次读的 100 做逻辑,但数据已变 | |

解决方案:升级到 REPEATABLE READ,事务 1 始终读取快照版本 100。


五、持久性(Durability)

5.1 定义与出处
  • 定义:一旦事务提交,所做的修改是永久性的,即使数据库系统立即崩溃,数据也不会丢失。

  • 出处:Jim Gray 在 1978 年的 System R 论文及后续恢复理论中强调。

5.2 核心机制

|--------------------------|----------------|
| 技术 | 说明 |
| Redo Log(重做日志) | 记录修改后的新值,崩溃后重放 |
| WAL(Write-Ahead Logging) | 日志先落盘,数据页后写 |
| 双写缓冲区 / 组提交 | 提高性能同时保证持久性 |
| RAID / 主从复制 | 硬件级别冗余 |

关键原则(WAL):

在对数据页本身进行修改之前,必须先将修改记录(Redo Log)写入持久存储(磁盘)。

5.3 应用场景
  • 必须持久:金融交易、用户注册、订单创建、日志审计

  • 可弱化持久性:测试环境、临时缓存(Redis 可配置 AOF 同步策略)

5.4 详细示例

场景:用户在 ATM 取款 500 元,成功扣款后 ATM 机停电。

复制代码
BEGIN;
UPDATE account SET balance = balance - 500 WHERE card_no = '6222****';
INSERT INTO withdraw_log (card_no, amount, time) VALUES ('6222****', 500, NOW());
COMMIT;

持久性保证流程:

复制代码
1. 执行 UPDATE → 生成 Redo Log(包含新余额 = 旧余额-500)
2. 执行 INSERT → 生成 Redo Log
3. COMMIT 执行
   → 首先将 Redo Log 从 Buffer 强制 fsync() 到磁盘 ✅
   → 然后返回客户端"提交成功"
4. 此时若数据库立即崩溃
   → 数据页可能还未写入磁盘(仍在 Buffer Pool)
   → 数据库重启时,自动扫描 Redo Log
   → 重现(重做)已提交事务的所有修改
   → 账户余额正确回放为"已扣500"

为何 Redo Log 比数据页先落盘?

  • 数据页随机写(慢)

  • Redo Log 顺序追加写(快)

  • WAL 机制保证:日志落盘 → 就算数据页丢了 → 也能恢复

不满足持久性的后果:

  • 银行系统:用户转账成功,但数据库崩溃后余额丢失(灾难性后果)

  • 企业系统:财务单据录入后消失,审计无法追溯


六、ACID 各特性的权衡与总结

|-----|-------------------------|----------------------|
| 特性 | 对性能的影响 | 常见牺牲手段(如 NoSQL / 缓存) |
| 原子性 | 需要 Undo Log + 崩溃恢复,开销中等 | 不支持事务,依靠业务补偿或应用层重试 |
| 一致性 | 依赖约束和业务校验,影响小 | 提供 最终一致性,短期内允许不一致 |
| 隔离性 | 影响最大(锁 + MVCC 竞争) | 降低隔离级别(RC),或放弃强隔离 |
| 持久性 | 需要 fsync() 刷盘,开销较高 | 异步落盘,批量写入,主从复制 |

关系图:

复制代码
  持久性 ←────────→ 原子性  
     ↕                      ↕
  一致性 ←───────── 隔离性

一句口诀记忆:

原子做到底,一致守规矩; 隔离拒干扰,持久不后悔。

事务的隔离级别 (读未提交、读已提交、可重复读、串行化)

1. 读未提交(READ UNCOMMITTED)

出处: ANSI SQL-92 标准定义的最低隔离级别。 定义: 允许事务读取其他事务未提交的数据修改。这是最低的隔离级别,几乎没有任何并发保护。 可能发生的问题:

  • ❌ 脏读(Dirty Read):读取了未提交的修改

  • ❌ 不可重复读(Non-repeatable Read)

  • ❌ 幻读(Phantom Read)

应用场景:

  • ⚠️ 极少在实际业务中使用

  • 适用于对数据准确性要求极低、追求极致读取性能的场景

  • 例如:实时监控系统中的近似值展示,允许短暂不一致

示例:

复制代码
-- 事务1
BEGIN;
UPDATE account SET balance = balance - 100 WHERE user = 'A';
-- 此时余额减少了100,但尚未提交

-- 事务2(READ UNCOMMITTED)
BEGIN;
SELECT balance FROM account WHERE user = 'A';
-- 读到了事务1未提交的修改值(脏数据)!

-- 事务1 回滚
ROLLBACK;
-- 事务2读到的数据实际上从未存在过

⚠️ 危险: 读未提交级别下,事务可能基于"幽灵数据"做出错误决策,实际业务中几乎不使用。


2. 读已提交(READ COMMITTED)

出处: ANSI SQL-92 标准定义。Oracle、SQL Server、PostgreSQL 的默认隔离级别。 定义: 事务只能读取其他事务已提交的数据。这是大多数数据库的默认隔离级别。 可能发生的问题:

  • ✅ 防止脏读

  • ❌ 不可重复读(Non-repeatable Read):同一事务内两次读取同一数据,结果可能不同

  • ❌ 幻读(Phantom Read)

核心机制:

  • 每次读取时,只看到在该读操作之前已提交的事务的修改

  • 在同一个事务内,不同时间点的读操作可能看到不同的数据快照

应用场景:

  • ✅ 大多数 OLTP 业务系统的默认选择

  • 适用于对数据一致性要求不是特别严格、但需要防止脏读的场景

  • 电商商品浏览、用户信息查询等

示例:

复制代码
-- 事务1
BEGIN;
SELECT balance FROM account WHERE user = 'A';
-- 结果:100

-- 事务2(另一个会话)
BEGIN;
UPDATE account SET balance = 200 WHERE user = 'A';
COMMIT;

-- 事务1 再次读取(READ COMMITTED)
SELECT balance FROM account WHERE user = 'A';
-- 结果:200(两次读取结果不同!)

-- 如果事务1基于第一次读取的100做了业务判断
-- 而第二次读取变成了200,可能导致逻辑错误

💡 不可重复读的本质: 同一事务内的读操作看到的是不同时间点的数据快照。


3. 可重复读(REPEATABLE READ)

出处: ANSI SQL-92 标准定义。MySQL InnoDB 的默认隔离级别。 定义: 确保同一事务内多次读取同一数据的结果一致,即在整个事务期间看到的是事务开始时的快照。 可能发生的问题:

  • ✅ 防止脏读

  • ✅ 防止不可重复读

  • ❌ 可能幻读(Phantom Read)(MySQL InnoDB 通过 MVCC 已能防止大部分幻读)

核心机制:

  • MVCC(多版本并发控制): 通过 Undo Log 维护数据的多个历史版本,事务读取时获取事务开始时的快照版本

  • 当前读 vs 快照读:

  • 快照读(普通 SELECT):读取快照版本,不受其他事务影响

  • 当前读(SELECT ... FOR UPDATE、UPDATE、DELETE):读取最新已提交的数据

应用场景:

  • ✅ 报表生成、对账、审计等需要事务内数据一致性的场景

  • 金融系统中的账务核对

  • 批量数据处理

示例:

复制代码
-- 事务1(REPEATABLE READ)
BEGIN;
SELECT balance FROM account WHERE user = 'A';
-- 结果:100(快照版本)

-- 事务2(另一个会话)
BEGIN;
UPDATE account SET balance = 200 WHERE user = 'A';
COMMIT;

-- 事务1 再次读取
SELECT balance FROM account WHERE user = 'A';
-- 结果:仍然是 100(快照版本,不受事务2影响)

-- 事务1 提交后,后续查询才能看到新值
COMMIT;
SELECT balance FROM account WHERE user = 'A';
-- 结果:200(新事务,读取最新数据)

幻读示例(理论上的):

复制代码
-- 事务1(REPEATABLE READ)
BEGIN;
SELECT COUNT(*) FROM users WHERE age > 20;
-- 结果:10

-- 事务2(另一个会话)
BEGIN;
INSERT INTO users (name, age) VALUES ('张三', 25);
COMMIT;

-- 事务1 再次查询
SELECT COUNT(*) FROM users WHERE age > 20;
-- 理论结果:在某些数据库中为 11(发生幻读)
-- MySQL InnoDB:仍为 10(通过 Next-Key Lock 防止了幻读)

一、基本概念

SAGA 事务 是分布式事务中的一种 长事务 解决方案。 它的核心思想是: 把一个跨多个服务的长事务,拆成一系列 本地短事务,每个服务独立提交,通过 补偿操作 来回滚失败的部分。 它属于一种 最终一致性 方案,重点不在立刻一致,而在 失败后能回退到一致状态。


二、为什么需要它

在一些业务流程中,事务跨度长、涉及多个服务: 如果用 2PC,整个链路要一直持有锁,性能极差; 如果用 TCC,每个服务都要写 Try/Confirm/Cancel 三套逻辑,成本高。 SAGA 的折中方式是: 每个步骤只做本地事务,立即提交释放资源,失败时按逆序执行补偿。


三、典型流程

以电商下单为例(创建订单 → 扣库存 → 扣优惠券 → 发积分): 步骤 1:正向执行 订单服务创建订单 → 库存服务扣库存 → 优惠券服务核销优惠券 → 积分服务加积分。 每个步骤都是独立的本地事务,提交后立刻释放锁。 步骤 2:失败时补偿 如果某个步骤失败(比如积分服务报错),SAGA 编排器会 按逆序 触发补偿: 积分服务补偿(取消加积分) → 优惠券服务补偿(恢复优惠券) → 库存服务补偿(恢复库存) → 订单服务补偿(取消订单)。 步骤 3:全部补偿完成后,事务回滚到一致状态。


四、协调模式

SAGA 有两种协调方式:

1)编排式(Orchestration)

由一个 SAGA 编排器(Orchestrator)统一管理流程。 编排器负责调用各服务、记录状态、触发补偿。 优点:流程集中可控,容易扩展; 缺点:编排器是额外组件,有一定复杂度。

2)协同式(Choreography)

各服务通过事件驱动,自己决定下一步做什么。 服务 A 完成后发布事件,服务 B 订阅并执行。 失败时服务发布补偿事件,其他服务订阅后回滚。 优点:去中心化,没有单点; 缺点:流程分散,调试困难。


五、设计要点

1)补偿操作必须幂等

补偿可能被多次触发,需要保证重复执行结果一致。

2)补偿不保证完全对称

正向操作和补偿操作不一定是镜像关系,需要根据业务语义设计。

3)需要持久化事务日志

记录每个步骤的执行状态,服务重启后能恢复或继续补偿。

4)隔离性问题

SAGA 不保证隔离性,中间状态可能被其他事务读到。 需要通过业务设计来处理,比如标记"进行中"状态。


六、适用场景

适合 长链路业务流程 且各步骤 可补偿 的场景:

  • 电商下单(订单→库存→优惠券→积分)

  • 差旅报销审批

  • 会员注册送权益

不适合 强一致性 要求的场景:

  • 金融转账类业务应该用 2PC 或 TCC。

七、一句话总结

SAGA 的本质是: 长事务拆短,正向逐步提交,失败逆序补偿,靠业务语义保证最终一致。 它比 2PC 更轻量,比 TCC 更灵活,是长链路业务的常用方案。

🚀 MySQL InnoDB 通过 Next-Key Lock(间隙锁 + 记录锁)在 REPEATABLE READ 级别下已经能防止大部分幻读。


4. 串行化(SERIALIZABLE)

出处: ANSI SQL-92 标准定义的最高隔离级别。 定义: 强制事务串行执行,完全避免所有并发问题。通过锁定读取的所有数据范围来实现。 可能发生的问题:

  • ✅ 防止脏读

  • ✅ 防止不可重复读

  • ✅ 防止幻读

核心机制:

  • 所有 SELECT 语句自动转换为 SELECT ... FOR SHARE(共享锁)

  • 读写操作互相阻塞,事务只能串行执行

  • 最大的性能代价:并发度极低

应用场景:

  • ✅ 对数据一致性要求极高的关键业务

  • 财务结算、清算系统

  • 法规合规要求严格一致的场景

  • ⚠️ 不推荐在高并发系统中使用

示例:

复制代码
-- 设置隔离级别
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- 事务1
BEGIN;
SELECT * FROM account WHERE user = 'A';
-- 此时 account 表中 user='A' 的行被加共享锁

-- 事务2(尝试修改)
BEGIN;
UPDATE account SET balance = 200 WHERE user = 'A';
-- 被阻塞,等待事务1释放锁

-- 事务1 提交
COMMIT;
-- 事务2 的 UPDATE 才能执行

⚠️ 代价: 串行化级别下,并发性能可能下降几个数量级,仅在必要时使用。

四种隔离级别对比总结

|-------|------------------|--------------------------------|-----------------|--------------|
| 特性 | READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE |
| 脏读 | ❌ 可能 | ✅ 防止 | ✅ 防止 | ✅ 防止 |
| 不可重复读 | ❌ 可能 | ❌ 可能 | ✅ 防止 | ✅ 防止 |
| 幻读 | ❌ 可能 | ❌ 可能 | ⚠️ 可能* | ✅ 防止 |
| 性能 | 最高 | 较高 | 中等 | 最低 |
| 并发度 | 最高 | 较高 | 中等 | 最低 |
| 默认数据库 | 无 | Oracle, PostgreSQL, SQL Server | MySQL InnoDB | 无 |

*MySQL InnoDB 通过 Next-Key Lock 在 REPEATABLE READ 下已能防止大部分幻读。

各数据库默认隔离级别

  • MySQL InnoDB: REPEATABLE READ

  • Oracle: READ COMMITTED

  • PostgreSQL: READ COMMITTED

  • SQL Server: READ COMMITTED

  • SQLite: SERIALIZABLE

如何选择合适的隔离级别

|-----------------|-----------------|-------------|
| 场景 | 推荐级别 | 理由 |
| 高并发 OLTP(电商、社交) | READ COMMITTED | 性能优先,脏读可防即可 |
| 报表生成、对账 | REPEATABLE READ | 需要事务内一致性 |
| 金融结算 | SERIALIZABLE | 一致性优先 |
| 数据统计分析 | READ COMMITTED | 允许轻微不一致 |
| 批量数据处理 | REPEATABLE READ | 避免处理过程中数据变化 |

💡 工程实践建议: 大多数业务使用 READ COMMITTED 即可,需要事务内一致性时再升级到 REPEATABLE READ。SERIALIZABLE 仅在极端情况下使用。

悲观锁(Pessimistic Locking)

核心理念: 假设并发冲突一定会发生,所以在读取数据时就加锁,防止其他事务修改。 实现方式:

  • SELECT ... FOR UPDATE(排他锁):锁定读取的行,其他事务无法修改

  • SELECT ... FOR SHARE(共享锁):锁定读取的行,其他事务可读但不可写

  • 表锁、行锁、页锁

适用场景:

  • ✅ 写操作频繁、冲突概率高的场景

  • 库存扣减(防止超卖)

  • 账户余额修改

  • 资源抢占(抢票、秒杀)

示例:

复制代码
-- 悲观锁:查询时直接加锁
BEGIN;
SELECT balance FROM account WHERE user = 'A' FOR UPDATE;
-- 加排他锁,其他事务无法修改该记录

-- 基于读取的值做计算
UPDATE account SET balance = 50 WHERE user = 'A';
COMMIT;
-- 释放锁,其他事务才能操作

缺点:

  • 锁竞争激烈时,大量事务阻塞等待,性能下降严重

  • 可能出现死锁(Deadlock)


乐观锁(Optimistic Locking)

核心理念: 假设并发冲突很少发生,不加锁,在提交时检查数据是否被其他事务修改过。 实现方式:

  • 版本号(Version): 每次更新时检查版本号是否变化

  • 时间戳(Timestamp): 每次更新时检查时间戳是否变化

  • CAS(Compare-And-Swap): 比较并交换

适用场景:

  • ✅ 读多写少、冲突概率低的场景

  • 用户信息修改

  • 配置管理

  • 文章编辑(冲突时提示重新编辑)

示例(版本号方式):

复制代码
-- 读取时获取当前版本号
SELECT balance, version FROM account WHERE user = 'A';
-- 结果:balance=100, version=3

-- 更新时检查版本号
UPDATE account 
SET balance = 50, version = version + 1 
WHERE user = 'A' AND version = 3;
-- 如果 version 已被其他事务修改为 4,则 UPDATE 影响行数为 0,更新失败
-- 应用层需要重试:重新读取、重新计算、重新提交

示例(CAS 方式):

复制代码
-- CAS:直接基于旧值更新,数据库自动检查
UPDATE account 
SET balance = balance - 100 
WHERE user = 'A' AND balance >= 100;
-- 数据库原子性保证:要么更新成功(余额足够),要么不更新(余额不足)
-- 不需要额外的版本号字段

悲观锁 vs 乐观锁对比

|---------|-------------|------------|
| 特性 | 悲观锁 | 乐观锁 |
| 假设 | 冲突一定会发生 | 冲突很少发生 |
| 加锁时机 | 读取时加锁 | 提交时检查 |
| 性能(低冲突) | 较差(锁开销) | 较好(无锁开销) |
| 性能(高冲突) | 较好(直接阻塞) | 较差(大量重试) |
| 死锁风险 | 有 | 无 |
| 实现复杂度 | 较低(数据库原生支持) | 较高(需应用层配合) |
| 适用场景 | 写多读少、高冲突 | 读多写少、低冲突 |

💡 选择建议: 读多写少选乐观锁,写多读少选悲观锁。不确定时,先从悲观锁开始,发现性能瓶颈后再考虑乐观锁。

并发控制:乐观锁、悲观锁

一、并发控制的背景与出处

并发控制(Concurrency Control) 是数据库为了解决多个事务同时访问同一份数据时可能产生的数据冲突问题而提出的一组机制。 其理论基础主要来自数据库事务处理领域的经典研究:

  • Jim Gray 在 1970s~1980s 对事务、锁、恢复机制进行了系统研究

  • Philip A. Bernstein、Vassos Hadzilacos、Nathan Goodman 在经典著作 Concurrency Control and Recovery in Database Systems(1987)中系统总结了并发控制理论

  • ANSI SQL-92 标准进一步将事务隔离级别、锁行为等内容标准化

核心目标:

  • 保证并发事务执行结果尽可能等价于某种串行执行结果

  • 在数据正确性和系统性能之间做平衡

  • 避免脏读、不可重复读、幻读、丢失更新等并发异常


二、并发控制的两大思路

数据库并发控制最常见的两种策略是:

  • 悲观锁(Pessimistic Locking):假设冲突经常发生,所以先加锁再操作

  • 乐观锁(Optimistic Locking):假设冲突很少发生,所以先执行,提交时再检查冲突

可以简单记忆为:

  • 悲观锁:先锁后改

  • 乐观锁:先改后验


三、悲观锁(Pessimistic Locking)

3.1 定义

悲观锁的核心思想是:既然并发修改很可能发生,那我在读取数据时就先把它锁住,防止别人同时修改。 也就是说,它对并发持"悲观"态度,认为冲突是高概率事件,因此优先通过数据库锁机制保障正确性。

3.2 常见实现方式

在关系型数据库中,悲观锁通常依赖数据库原生锁:

  • 排他锁(X Lock):一个事务持有后,其他事务不能读也不能写(不同数据库细节略有差异)

  • 共享锁(S Lock):允许其他事务读,但不允许写

  • 行锁(Row Lock):锁单行数据

  • 表锁(Table Lock):锁整张表

  • 间隙锁 / Next-Key Lock:MySQL InnoDB 为避免幻读使用的范围锁

最常见 SQL 写法:

复制代码
SELECT * FROM account WHERE id = 1 FOR UPDATE;

这表示:读取这条记录时就加排他锁,其他事务必须等待当前事务提交或回滚后才能继续修改。

3.3 应用场景

悲观锁适用于冲突概率高、写多、资源竞争强的场景:

  • 库存扣减、秒杀抢购

  • 银行账户扣款

  • 订单状态流转(防止重复处理)

  • 有限资源分配(优惠券、座位、名额)

  • 调度系统中的任务抢占

3.4 详细示例:库存扣减

场景:商品库存只剩 1 件,两个用户同时下单。

如果不加锁:

复制代码
-- 事务1
SELECT stock FROM product WHERE id = 1001; -- stock = 1

-- 事务2
SELECT stock FROM product WHERE id = 1001; -- stock = 1

-- 事务1
UPDATE product SET stock = stock - 1 WHERE id = 1001;

-- 事务2
UPDATE product SET stock = stock - 1 WHERE id = 1001;

结果可能出现:库存被扣成 -1,发生超卖。 使用悲观锁:

复制代码
-- 事务1
BEGIN;
SELECT stock FROM product WHERE id = 1001 FOR UPDATE;
-- 此时事务2如果也想锁这条记录,会被阻塞

UPDATE product 
SET stock = stock - 1 
WHERE id = 1001 AND stock > 0;
COMMIT;

-- 事务2(等待事务1提交后才继续)
BEGIN;
SELECT stock FROM product WHERE id = 1001 FOR UPDATE;
-- 读取到 stock = 0
-- 不能继续扣减
ROLLBACK;
3.5 优缺点

优点:

  • 逻辑直观,数据库直接保证互斥

  • 适合高冲突场景

  • 能较强地保证数据正确性

缺点:

  • 容易阻塞,吞吐下降

  • 高并发下锁竞争严重

  • 可能出现死锁

  • 用户等待时间更长

四、乐观锁(Optimistic Locking)

4.1 定义

乐观锁的核心思想是:先假设并发冲突不常发生,因此不急着加锁,而是在更新提交时检查数据是否被别人改过。 也就是说,它对并发持"乐观"态度,认为大部分情况下事务之间不会真正冲突。

4.2 常见实现方式

乐观锁通常不依赖数据库原生锁,而是依赖应用层字段检查。最常见的两类实现:

  • 版本号(version)机制

  • 时间戳(timestamp)机制

最经典的是版本号方案:

  1. 查询数据时带出当前 version

  2. 更新时附带条件 where version = old_version

  3. 更新成功则说明期间无人修改

  4. 更新失败则说明发生并发冲突,需要重试或提示失败

示例:

复制代码
UPDATE account
SET balance = 80, version = version + 1
WHERE id = 1 AND version = 5;

如果影响行数为 0,说明这条记录已经被其他事务更新过,当前更新失败。

4.3 应用场景

乐观锁适用于读多写少、冲突概率低、追求吞吐量的场景:

  • 用户资料修改

  • 后台配置管理

  • CMS 内容编辑

  • 购物车、草稿箱、表单类业务

  • 低频更新但并发读很多的业务

4.4 详细示例:账户余额更新(版本号)

假设表结构如下:

复制代码
CREATE TABLE account (
    id BIGINT PRIMARY KEY,
    balance DECIMAL(10,2) NOT NULL,
    version INT NOT NULL DEFAULT 0
);

初始数据:

复制代码
id = 1, balance = 100.00, version = 3

两个事务同时读取:

复制代码
-- 事务1读取
SELECT balance, version FROM account WHERE id = 1;
-- 结果:balance=100, version=3

-- 事务2读取
SELECT balance, version FROM account WHERE id = 1;
-- 结果:balance=100, version=3

事务1先更新成功:

复制代码
UPDATE account
SET balance = 80, version = 4
WHERE id = 1 AND version = 3;
-- 成功,影响 1 行

事务2再更新:

复制代码
UPDATE account
SET balance = 50, version = 4
WHERE id = 1 AND version = 3;
-- 失败,影响 0 行

原因是 version 已经被事务1改成 4,事务2基于旧版本做更新,数据库拒绝这次写入。 这样就避免了**丢失更新(Lost Update)**问题。

4.5 优缺点

优点:

  • 不容易阻塞

  • 并发吞吐高

  • 无死锁问题

  • 更适合高读低写系统

缺点:

  • 需要应用层配合实现

  • 冲突严重时,重试成本高

  • 不适合强竞争写场景

  • 业务代码复杂度更高

五、悲观锁 vs 乐观锁对比

|--------|---------|--------------|
| 对比项 | 悲观锁 | 乐观锁 |
| 核心假设 | 冲突经常发生 | 冲突很少发生 |
| 是否先加锁 | 是 | 否 |
| 冲突处理方式 | 阻塞其他事务 | 提交时检测失败并重试 |
| 实现位置 | 数据库层 | 应用层 + SQL 条件 |
| 吞吐量 | 较低 | 较高 |
| 死锁风险 | 有 | 无 |
| 适合场景 | 写多、竞争激烈 | 读多、竞争较低 |
| 开发复杂度 | 中 | 较高 |


六、如何选择

经验上可以这样选:

适合悲观锁的情况
  • 热点资源竞争非常强

  • 不允许失败重试

  • 一次失败代价很大

  • 业务必须立刻串行化处理

比如:

  • 秒杀库存

  • 账户扣款

  • 订单抢占

  • 排班/座位分配

适合乐观锁的情况
  • 读远多于写

  • 数据冲突概率低

  • 允许失败后重试

  • 更关注吞吐量和响应速度

比如:

  • 用户资料更新

  • 配置编辑

  • 内容管理后台

  • 普通运营台系统


七、工程实践建议

  1. 不要把乐观锁和悲观锁理解成数据库二选一。

很多系统会混合使用:核心扣减链路用悲观锁,普通编辑链路用乐观锁。

  1. 能用数据库原子条件更新时,优先考虑原子 SQL。

例如:

复制代码
UPDATE product
SET stock = stock - 1
WHERE id = 1001 AND stock > 0;

这类写法本质上也是一种轻量级并发控制,很多时候比显式悲观锁更高效。

  1. 高并发系统中,锁只是手段,不是全部。

实际工程里常常还会结合:

  • 队列削峰

  • Redis 分布式锁

  • 幂等控制

  • 去重表

  • 本地消息表

  • 限流与熔断

  1. 先看冲突概率,再选锁策略。

选错锁比不用锁更糟------ 低冲突场景上悲观锁会白白损失性能; 高冲突场景上乐观锁会导致大量失败重试。


八、一句话总结

  • 悲观锁:我先锁住,你们别动。

  • 乐观锁:你们先改,提交时我再检查。

如果一句话判断:

资源抢占选悲观锁,状态编辑选乐观锁。

分布式事务基础理论

2.1 CAP 定理详解-三选二的误区 (实际是 CP vs AP)

一、为什么说"三选二"是误区

CAP 定理常被简化成:一致性(C)、可用性(A)、分区容错性(P)三者只能选两个。这个说法便于记忆,但容易让人误解成系统上线时可以自由做 "CA / CP / AP" 三选二。 更准确的理解是:

  • 没有网络分区时,系统通常既可以做到较高一致性,也可以保持可用

  • 一旦发生网络分区(P) ,系统就必须在 C 和 A 之间做取舍

所以,CAP 讨论的重点不是平时怎么"三选二",而是:

分区 发生时,你的系统更偏向保一致,还是更偏向保可用。


二、为什么实际更像是 CP vs AP

在分布式系统里,P 基本是必须接受的前提。 原因很简单:

  • 网络延迟会抖动

  • 机器会宕机

  • 机房之间会丢包

  • 跨地域链路可能中断

也就是说,分区不是"会不会发生",而是"早晚会发生"。因此在工程实践里,P 通常不是可选项,而是默认存在的现实约束。 所以实际问题就变成:

  • CP: 发生分区时,优先保证一致性,牺牲部分可用性

  • AP: 发生分区时,优先保证可用性,接受短暂不一致

这就是为什么大家常说:CAP 在实践里,本质上是 CP vs AP。


三、怎么理解这个取舍

CP 系统

分区发生后,如果无法确认数据是否一致,就宁可拒绝部分请求。 特点:

  • 数据更可靠

  • 可能出现部分节点不可用

  • 适合对正确性要求极高的系统

典型场景:

  • 分布式协调

  • 元数据管理

  • 金融核心状态

AP 系统

分区发生后,系统优先返回结果,保证服务不断,但不同节点上的数据可能暂时不一致。 特点:

  • 服务连续性更强

  • 数据可能延迟收敛

  • 适合高可用优先的业务

典型场景:

  • 服务注册发现

  • 社交 feed

  • 商品评论、点赞、浏览量


四、一句话总结

CAP 不是字面意义上的固定"三选二",而是:

在分布式系统必须面对网络分区的前提下,分区发生时到底选一致性还是选可用性。

所以更准确的表达应当是:

CAP 的核心不是 CA / CP / AP 三选二,而是 P 发生时的 CP vs AP 取舍。

五、典型 CP 系统

CP 系统在网络分区发生时,优先保证一致性,宁可拒绝部分请求也不返回过期或不一致的数据。

ZooKeeper

定位: 分布式协调服务,用于配置管理、服务注册、分布式锁等。 为什么是 CP:

  • 使用 ZAB 协议(类似 Paxos/Raft)保证数据强一致

  • 写入需要多数节点确认(quorum)

  • 分区发生时,少数派节点会拒绝服务,确保不会出现脑裂

  • 读操作也支持强一致性(sync 模式)

取舍: 分区时部分节点不可写甚至不可读,但保证所有存活的节点数据一致。

HBase

定位: 分布式列式数据库,构建在 HDFS 之上。 为什么是 CP:

  • 依赖 ZooKeeper 进行 Region 管理

  • 写入走 WAL(预写日志),需同步到 HDFS

  • 分区时 Region 可能暂时不可用,等待恢复后继续

取舍: 高可用不如 AP 系统,但数据可靠性更强。


六、典型 AP 系统

AP 系统在网络分区发生时,优先保证可用性,允许不同节点之间短暂不一致,通过最终一致性手段收敛。

Eureka

定位: 服务注册与发现,Netflix 开源,常用于 Spring Cloud 生态。 为什么是 AP:

  • 采用 Peer-to-Peer 复制架构,没有主从之分

  • 分区时各节点可以继续接受注册和查询

  • 不要求实时一致性,允许短暂返回过期服务列表

  • 通过心跳机制和自我保护机制维持可用性

取舍: 可能在分区期间返回不完整或过期的服务列表,但服务发现不会中断。

Cassandra

定位: 分布式 NoSQL 数据库,高可用、高吞吐。 为什么是 AP:

  • 无中心架构,节点对等

  • 写入可配置一致性级别(ONE / QUORUM / ALL),默认偏向可用

  • 支持多数据中心跨机房复制,容忍分区

  • 使用 Gossip 协议进行节点间通信

取舍: 允许读写操作返回暂时不一致的数据,但系统不会因分区而中断。


七、CP vs AP 简单对比

|--------|-------------|-----------|
| 特性 | CP 系统 | AP 系统 |
| 分区时的选择 | 保一致,部分不可用 | 保可用,允许不一致 |
| 一致性模型 | 强一致 | 最终一致 |
| 典型架构 | 主从 / Quorum | 无中心 / P2P |
| 适用场景 | 协调、配置、核心状态 | |

一句话:CP 系统宁可"停一下"也不能错,AP 系统宁可"差点准"也不能停。

2.2 BASE 理论

一、如何从强一致性降级为最终一致性

传统 ACID 系统追求的是"提交即一致",即强一致性。这在单数据库内很容易做到,但在分布式系统中,跨节点保证强一致的代价极高------需要协调、加锁、等待,导致延迟飙升甚至部分节点不可用。 BASE 的思路是:先保证系统可用,允许短期不一致,再通过异步手段让数据最终收敛到一致状态。 具体降级过程可以理解为三个步骤:

  1. 接受不一致的时间窗口

在分区或高负载发生时,系统不再要求所有节点立刻看到相同的数据,而是允许不同节点之间存在短暂差异。

  1. 保证核心流程可用

关键操作(如用户下单)优先返回成功,不阻塞等待所有节点同步完成。

  1. 通过补偿或异步同步实现最终一致

后台通过消息队列、定时任务、重试机制等手段,把未同步完成的操作逐步补齐,最终所有节点数据达到一致。

一句话概括:从"提交即一致"降级为"先可用,再收敛"。


二、软状态与异步确保

软状态(Soft state): 指系统中的数据状态可以在一段时间内不是最新、不是完全一致的。也就是说,我们接受系统存在中间状态。 举例:用户提交订单后,库存扣减和订单记录可能不会立刻在所有节点上同步,但最终会通过后续机制补齐。 异步确保(Eventual consistency): 最终一致性不是一种"不管它"的策略,而是通过可靠的异步手段来保证:

  • 消息队列: 将需要跨系统执行的操作放入队列,逐步消费

  • 定时补偿任务: 定期检查数据差异并修复

  • 重试机制: 对失败的操作进行指数退避重试

  • 本地消息表: 在本地数据库中记录操作日志,确保消息不会丢失

通过这些手段,系统可以在不阻塞主流程的前提下,保证数据最终达到一致状态。

与 ACID 的关系: - ACID 是"先一致,后提交"------事务不满足条件就不提交 - BASE 是"先提交,后一致"------先保证业务可用,再让数据慢慢收敛

BASE 本质上是在 CAP 的 CP vs AP 选择中,偏向 AP 一侧的实践方法论。

2.3 一致性模型分类

分布式系统中常见的数据一致性模型,从最强到最弱可以这样理解:

一、强一致性(Strong Consistency)

定义: 写操作完成后,后续所有读操作都能立刻读到最新值。 特点:

  • 读写路径严格同步,数据"立即可见"

  • 是最严格的一致性模型

  • 代价:延迟高、吞吐量低、部分场景不可用

典型系统:

  • 关系型数据库主从同步(同步复制)

  • ZooKeeper、etcd(基于共识算法)

一句话: "写完就能立刻读到。"


二、弱一致性(Weak Consistency)

定义: 系统不保证写操作后读操作能立刻读到最新值,甚至不保证最终会读到最新值。 特点:

  • 最宽松的一致性约束

  • 性能最好

  • 适用于对准确性要求极低的场景

典型场景:

  • CDN 缓存(不同节点返回不同内容可接受)

  • 某些日志聚合系统

一句话: "写到哪算哪,读到什么就是什么。"


三、最终一致性(Eventual Consistency)

定义: 保证在没有新写入的情况下,经过一段时间后,所有节点的读操作最终都能读到相同的最新值。 特点:

  • 允许中间状态存在不一致

  • 通过异步复制、补偿等手段最终收敛

  • 是弱一致性的"有保证版本"

典型系统:

  • DNS 系统

  • Amazon DynamoDB

  • Cassandra

一句话: "先不一致,但最终一定会一致。"


四、因果一致性(Causal Consistency)

定义: 有因果关系(happens-before)的操作,必须按因果顺序在所有节点上可见;没有因果关系的操作,顺序可以不保证。 特点:

  • 比强一致性弱,比最终一致性强

  • 保留了"逻辑上有关联的操作"的顺序

  • 不保证全局顺序,只保证因果顺序

示例:

  • 用户 A 发了评论 1

  • 用户 B 回复了评论 1(因果依赖评论 1)

  • 因果一致性保证:任何用户看到回复时,一定也能看到评论 1

  • 但如果用户 C 和用户 D 分别发了两条无关评论,它们在不同节点上的顺序可以不统一

典型场景:

  • 社交网络动态流

  • 聊天消息

  • 评论系统

一句话: "有关联的操作顺序不能乱,无关的不管。"


五、会话一致性(Session Consistency)

定义: 保证同一个会话(Session)内的用户,能保持一致性视角。不同会话之间可以存在不一致。 特点:

  • 用户体验视角的折中方案

  • 同一用户不会看到"时间倒流"

  • 跨用户可以接受不一致

实现方式:

  • 将同一用户的读写路由到同一节点(Session 粘性)

  • 在该节点内保证读写一致

  • 跨节点异步同步

典型场景:

  • 购物车系统(同一用户的操作路由到同一服务实例)

  • 用户个人配置管理

  • 会话级数据读写

一句话: "我自己看到的数据是一致的就行,别人看到的不影响我。"


六、一致性模型对比

|-------|-------|----|----------|
| 模型 | 一致性强度 | 性能 | 适用场景 |
| 强一致性 | 最强 | 最低 | 金融、协调服务 |
| 因果一致性 | 强 | 较低 | 社交动态、评论 |
| 会话一致性 | 中 | 中 | 用户会话、购物车 |
| 最终一致性 | 较弱 | 高 | 电商库存、DNS |
| 弱一致性 | 最弱 | 最高 | CDN、缓存 |

一句话总结:一致性越强,代价越高;选模型时看业务真正需要的是哪种一致性,而不是一味追求最强。

经典解决方案模式

3.1 两阶段提交 (2PC)

一、2PC 的基本概念

两阶段提交(Two-Phase Commit, 2PC) 是分布式事务中最经典的强一致性方案之一,用来保证多个参与节点要么全部提交成功,要么全部回滚。 它的核心思想很简单:

先统一询问大家"能不能提交",如果都可以,再统一要求正式提交。

所以叫"两阶段提交"。


二、2PC 的角色

2PC 中通常有两个核心角色:

1)协调者(Coordinator)

负责发起事务、收集各参与者的执行结果,并最终决定是提交还是回滚。 可以理解为:总指挥

2)参与者(Participant / Resource Manager)

真正执行业务操作的节点,比如订单库、库存库、账户库等。 可以理解为:具体干活的人


三、2PC 的流程

2PC 分为两个阶段:

第一阶段:准备阶段(Prepare Phase)

协调者向所有参与者发送 prepare 请求,询问:

"这笔事务你能不能执行成功?"

参与者收到后:

  • 执行业务操作,但不真正提交

  • 写入 Undo / Redo Log

  • 锁住相关资源

  • 如果本地执行成功,返回 YES

  • 如果本地执行失败,返回 NO

这一阶段结束后,参与者处于:

已执行、未提交、资源已锁定 的状态。

第二阶段:提交阶段(Commit / Rollback Phase)

协调者根据第一阶段的结果做决定:

  • 如果所有参与者都返回 YES → 发送 COMMIT

  • 只要有一个返回 NO → 发送 ROLLBACK

参与者收到后再真正执行:

  • 收到 COMMIT → 提交本地事务,释放锁

  • 收到 ROLLBACK → 回滚本地事务,释放锁


四、简单示例

场景:用户下单,需要同时完成:

  • 订单库:创建订单

  • 库存库:扣减库存

第一阶段
  • 协调者通知订单库:准备创建订单

  • 协调者通知库存库:准备扣减库存

  • 两边都执行成功,但先不提交,只返回 YES

第二阶段
  • 如果两边都 YES:协调者发送 COMMIT

  • 订单正式提交

  • 库存正式提交

  • 如果库存扣减失败:协调者发送 ROLLBACK

  • 订单回滚

  • 库存回滚

这样就保证了:

不会出现"订单创建了但库存没扣"这种中间错误状态。


五、2PC 的优点

1)实现思路清晰

2PC 模型简单直观,容易理解,是很多分布式事务方案的理论基础。

2)能保证强一致性

所有参与节点最终结果一致:要么都成功,要么都失败。

3)适合关键事务场景

在对正确性要求很高的场景中,2PC 能提供较强的数据一致性保障。


六、2PC 的缺点

1)同步阻塞严重

第一阶段后,参与者会一直持有锁,直到第二阶段结束。若事务链路长,系统吞吐会明显下降。

2)协调者单点问题

协调者一旦宕机,参与者可能不知道该提交还是回滚,事务进入不确定状态。

3)存在阻塞风险

如果协调者在第二阶段发送指令前崩溃,参与者会一直卡住,资源无法释放。

4)性能较差

2PC 需要多次网络通信 + 日志落盘 + 锁持有,延迟高,不适合高并发长链路业务。


七、一句话总结

2PC 本质上是:

先投票,再统一执行。

它的优点是强一致、逻辑清楚;缺点是阻塞严重、性能差、协调者故障影响大。 所以在现代高并发分布式系统里,2PC 更多作为理论基础或在少量关键链路中使用,很多业务系统会转向 TCC、SAGA、可靠消息最终一致性等方案。

3.2 三阶段提交 (3PC)

一、3PC 的基本概念

三阶段提交(Three-Phase Commit, 3PC) 可以看作是在 2PC 基础上的改进方案。 它的核心目标是:

尽量降低 2PC 中协调者故障带来的阻塞问题。

2PC 的问题在于:参与者在第一阶段完成后,可能长时间卡在"既不能提交、也不能回滚"的中间状态。 3PC 的思路是把 2PC 的"提交前决策"再拆细一点,让参与者知道事务已经推进到了哪个阶段,从而在协调者出故障时有更明确的默认处理策略。


二、3PC 相比 2PC 的改进点

3PC 主要有两个改进点:

1)把原来的两阶段拆成三阶段

2PC:

  • Prepare

  • Commit / Rollback

3PC:

  • CanCommit

  • PreCommit

  • DoCommit

这样可以把"询问能不能做"和"真正进入提交前状态"分开,减少不确定性。

2)引入超时机制

3PC 假设:如果某个阶段等待太久没有收到协调者指令,就按照预设规则自动继续或自动回滚,而不是无限阻塞。 这就是它试图解决 2PC 阻塞问题的关键。


三、3PC 的流程

第一阶段:CanCommit

协调者询问所有参与者:

"你有没有能力执行这笔事务?"

此时参与者只做检查,不真正执行事务。

  • 能执行 → 返回 YES

  • 不能执行 → 返回 NO

这一阶段类似 2PC 中的"先投票",但还没有真正写事务数据。


第二阶段:PreCommit

如果所有参与者都返回 YES,协调者发送 PreCommit 指令。 参与者收到后:

  • 真正执行业务操作

  • 写日志

  • 但仍然不正式提交

  • 返回 ACK 给协调者

这时参与者已经进入一种"提交前的稳定状态"。 可以理解为:

"活已经干了,随时可以正式提交。"

如果这一阶段协调者出问题,参与者因为知道自己已经进入 PreCommit 状态,所以可以结合超时机制做下一步判断,而不是像 2PC 那样一直卡死。


第三阶段:DoCommit

如果协调者收到了所有参与者的 ACK,就发送 DoCommit。 参与者收到后:

  • 正式提交事务

  • 释放资源

如果在前面任意阶段出现失败,协调者则通知所有参与者 Abort / Rollback


四、3PC 的简单理解

如果用一句话概括 3PC 的流程:

先问能不能做,再让大家先准备好,最后正式提交。

相比 2PC,3PC 多出来的就是中间那一步:

PreCommit(提交前确认)

这一步的意义在于:

  • 让参与者明确知道自己是不是已经进入"接近提交"的阶段

  • 避免协调者故障时,参与者完全不知道该怎么办


五、3PC 与 2PC 的对比

|---------------|---------|----------------|
| 对比项 | 2PC | 3PC |
| 阶段数 | 两阶段 | 三阶段 |
| 是否有 PreCommit | 没有 | 有 |
| 是否容易阻塞 | 较容易 | 相对降低 |
| 是否有超时机制 | 通常没有强依赖 | 有 |
| 实现复杂度 | 较低 | 更高 |
| 一致性保障 | 强 | 理论上更灵活,但现实中仍有限 |

2PC 的问题
  • 参与者容易卡在不确定状态

  • 协调者宕机会导致长时间阻塞

  • 所有人只能被动等待

3PC 的改进
  • 通过 CanCommit + PreCommit + DoCommit 细化事务状态

  • 通过超时减少无限阻塞

  • 让参与者在故障时有更多自处理能力


六、3PC 的局限性

虽然 3PC 是对 2PC 的改进,但它并没有彻底解决问题。 原因在于:

1)仍然依赖协调者

协调者仍然是核心决策点,一旦故障,系统仍然会变复杂。

2)网络分区下仍可能出错

3PC 假设网络延迟是有界的,并依赖超时做决定。但现实分布式系统里,网络分区和长时间抖动很常见,因此超时并不一定可靠。

3)工程上应用不多

相比 2PC,3PC 理论更优雅,但实现复杂、收益有限,所以实际生产中使用得远少于 TCC、SAGA、可靠消息等方案。


七、一句话总结

3PC 本质上是:

在 2PC 的基础上,加一个"提交前确认"阶段,并引入超时机制,试图减少阻塞。

它比 2PC 更进一步,但并没有成为主流工业方案。现实系统里,3PC 更多是一个经典理论模型,用来帮助理解分布式事务的演进思路。

3.3 TCC (Try-Confirm-Cancel)

一、TCC 的基本概念

TCC(Try-Confirm-Cancel) 是一种典型的分布式事务补偿模型。 它的核心思想不是像 2PC 那样依赖数据库层的锁和统一提交,而是把一个业务操作拆成三个明确的业务阶段:

  • Try:尝试执行 / 预留资源

  • Confirm:确认提交

  • Cancel:取消并回滚

可以把它理解成:

先占坑,再确认;如果不行,就主动撤销。


二、TCC 的三阶段含义

1)Try 阶段

Try 不是最终提交,而是做业务检查 + 资源预留。 例如:

  • 检查账户余额是否充足

  • 冻结一部分金额

  • 预扣库存但不正式出库

  • 锁定优惠券但不真正核销

这一阶段的目标是:

先确保后续事务有条件成功。

2)Confirm 阶段

当所有参与方的 Try 都成功后,进入 Confirm。 这一阶段负责:

  • 正式提交业务结果

  • 把冻结资源转成实际消耗

  • 完成最终状态落地

例如:

  • 冻结金额正式扣减

  • 预扣库存正式生效

  • 优惠券正式核销

3)Cancel 阶段

如果任意一个参与方 Try 失败,或者后续流程出错,就进入 Cancel。 这一阶段负责:

  • 释放 Try 阶段预留的资源

  • 将业务状态恢复到执行前

  • 保证事务整体回退

例如:

  • 解冻金额

  • 恢复预扣库存

  • 取消优惠券锁定


三、TCC 的典型流程

以"下单 + 扣库存 + 扣余额"为例:

第一步:Try
  • 订单服务:创建待确认订单

  • 库存服务:冻结库存

  • 账户服务:冻结余额

如果三者都成功,说明事务具备继续提交的条件。

第二步:Confirm

协调器通知各服务执行 Confirm:

  • 订单变为正式订单

  • 库存正式扣减

  • 余额正式扣款

第三步:Cancel(异常时执行)

如果某个服务在 Try 阶段失败,或者在中途流程异常,则执行 Cancel:

  • 删除待确认订单

  • 释放冻结库存

  • 解冻余额

最终效果是:

要么全部真正生效,要么全部业务补偿回去。


四、TCC 的优点

1)不依赖底层数据库事务

TCC 是业务层分布式事务方案,不要求多个系统共享一个数据库事务能力。

2)控制力强

Try / Confirm / Cancel 都由业务自己定义,能精确表达复杂业务语义。

3)适合高一致性业务

相比消息最终一致性方案,TCC 对事务过程的控制更强,更适合资金、库存等核心场景。


五、TCC 的缺点

1)业务侵入性强

每个参与服务都要实现 Try / Confirm / Cancel 三套接口,开发成本明显更高。

2)设计复杂

必须自己处理:

  • 幂等

  • 空回滚

  • 悬挂问题

  • 重试补偿

  • 状态一致性

3)对业务建模要求高

不是所有业务都适合拆成 Try / Confirm / Cancel。 如果资源无法"预留"或"取消",实现就会很困难。


六、适用场景

TCC 适合以下业务:

  • 支付扣款

  • 账户转账

  • 库存冻结与扣减

  • 优惠券占用与核销

  • 核心订单链路

这些业务的共同特点是:

  • 资源可以先冻结 / 预占

  • 后续可以确认或撤销

  • 对一致性要求高


七、与 2PC / 可靠消息的区别

和 2PC 的区别
  • 2PC 更偏数据库层协调

  • TCC 更偏业务层补偿控制

2PC 是"数据库先锁住,再统一提交",TCC 是"业务先预留,再确认或取消"。

和最终一致性消息方案的区别
  • 消息最终一致性 更关注"先完成主流程,再异步补齐"

  • TCC 更关注"主流程内就把成功和回滚路径都设计清楚"

所以 TCC 通常一致性更强,但开发成本也更高。


八、一句话总结

TCC 本质上是:

把一个分布式事务拆成"先预留、再确认、失败则取消"的三段式业务控制流程。

它适合高一致性核心业务,但代价是业务侵入强、实现复杂。

3.4 可靠消息最终一致性 (本地消息表/事务消息)

一、基本概念

可靠消息最终一致性 是分布式事务里非常常见的一类方案。 它的核心思想不是要求多个系统在同一个时刻一起提交,而是:

先把本地事务做成功,再通过可靠消息把后续动作异步通知出去,最终让多个系统的数据收敛一致。

它属于一种 最终一致性 方案,重点不在"立刻一致",而在"消息不能丢、下游最终要执行成功"。


二、为什么需要它

在分布式系统里,经常会遇到这样的场景:

  • 订单系统下单成功后,要通知库存系统扣库存

  • 支付成功后,要通知积分系统加积分

  • 用户注册成功后,要通知消息系统发欢迎通知

如果直接在一个大事务里把这些系统都绑在一起,会有几个问题:

  • 耦合太高

  • 性能差

  • 容易阻塞

  • 某个下游挂了会影响主流程

所以很多系统会采用一种更实用的方式:

主业务先提交,本地保存好"待发送消息",然后异步通知下游。

这样既保证了主流程成功,又能通过重试和补偿机制把后续链路补齐。


三、本地消息表方案

1)核心思路

本地消息表方案的关键在于:

业务数据和消息记录放在同一个本地事务里提交。

也就是说,在订单系统中,会同时做两件事:

  • 写订单表

  • 写消息表(例如 message_outbox)

两者一起提交,要么都成功,要么都失败。 这样可以保证:

  • 如果订单成功了,消息一定也被记录下来了

  • 如果订单失败了,消息也不会"假发送"

2)典型流程

以"下单后扣库存"为例: 步骤 1:主业务本地事务提交

复制代码
订单服务:
- 写入订单表
- 写入本地消息表(内容:订单已创建,待通知库存)
- 一起提交

步骤 2:异步投递消息

后台任务或消息投递器扫描本地消息表,把未发送的消息投递到 MQ。

步骤 3:下游消费处理

库存服务收到消息后执行扣库存逻辑。

步骤 4:失败重试 / 补偿

如果消息发送失败,重试; 如果库存扣减失败,也可以继续重试或人工补偿。

3)优点
  • 主业务事务可靠

  • 消息不会凭空丢失

  • 实现相对容易理解

  • 对业务侵入比 TCC 小

4)缺点
  • 需要额外维护消息表、扫描任务、重试机制

  • 消息投递存在延迟

  • 只能保证最终一致,不能保证实时一致


四、事务消息方案

1)核心思路

事务消息方案和本地消息表很像,但消息可靠性更多交给 MQ 中间件 来保证。 典型代表是 RocketMQ 的事务消息。 它的思路可以概括为:

先发送半消息(Half Message),本地事务成功后再确认提交消息,否则回滚消息。

2)典型流程

以"支付成功后加积分"为例: 步骤 1:先发半消息 支付服务先往 MQ 发一条"暂不可见"的半消息。 步骤 2:执行本地事务 支付服务本地更新支付状态。 步骤 3:提交或回滚消息

  • 如果本地事务成功 → 告诉 MQ 提交消息

  • 如果本地事务失败 → 告诉 MQ 回滚消息

步骤 4:下游消费 积分服务消费这条正式消息,给用户加积分。

3)回查机制

如果发送半消息后,生产者宕机了,MQ 不知道这条消息该提交还是回滚,就会发起 事务回查

"你这条本地事务到底成功了没有?"

生产者收到回查请求后,根据本地事务状态返回结果。 这就是事务消息比普通异步消息更可靠的关键。

4)优点
  • 可靠性更强

  • MQ 帮忙承担一部分事务协调能力

  • 不必自己维护完整的消息扫描逻辑

5)缺点
  • 依赖特定 MQ 能力(不是所有 MQ 都支持事务消息)

  • 业务实现仍然需要处理幂等、重试、回查

  • 一致性仍然是最终一致,不是强一致


五、本地消息表 vs 事务消息

|-------|-------------|------------------|
| 对比项 | 本地消息表 | 事务消息 |
| 核心保障方 | 业务系统自己 | MQ 中间件 + 业务系统 |
| 可靠性实现 | 本地事务 + 扫表补发 | 半消息 + 提交/回滚 + 回查 |
| 复杂度 | 偏业务侧复杂 | 偏 MQ 侧复杂 |
| 适用性 | 更通用 | 依赖特定 MQ |
| 一致性级别 | 最终一致 | 最终一致 |

可以简单理解为:

  • 本地消息表:自己兜底

  • 事务消息:让 MQ 帮你兜一部分底


六、适用场景

这类方案特别适合:

  • 订单 → 库存

  • 支付 → 积分

  • 注册 → 发券 / 发通知

  • 主业务完成后触发多个异步动作

它们的共同特点是:

  • 主流程要尽快成功返回

  • 下游允许稍后完成

  • 能接受短时间内不一致

  • 但最终必须一致


七、一句话总结

可靠消息最终一致性的本质是:

先把本地事务做成功,再保证消息可靠投递,靠异步重试和补偿让各系统最终一致。

它比 2PC / TCC 更轻量,更适合高并发互联网业务,也是实际工程里非常主流的一类分布式事务方案。

3.5 最大努力通知

一、基本概念

最大努力通知 是分布式事务中用于 通知下游系统事务结果 的一种方案。 它的核心思想是: 上游系统在事务完成后,尽最大努力多次通知下游,但 不保证 下游一定能收到或处理成功。 它属于一种 最终一致性 方案,重点不在立刻成功,而在 尽可能提高通知成功率。


二、为什么需要它

在一些业务场景中,上游系统需要把事务结果通知给下游,但: 下游系统可能暂时不可用,或网络偶尔抖动,单次通知很容易失败。 如果直接发一次通知就结束,下游很可能漏处理。 但如果像 2PC 那样强要求确认,又会把系统耦合得太紧。 所以折中方案是: 上游多次通知,逐步降低频率,直到下游收到为止。


三、典型流程

以支付成功后通知订单系统发货为例: 步骤 1:事务提交后立即第一次通知 支付服务在本地事务提交后,立即调用订单系统的通知接口。 步骤 2:失败后定时重试 如果通知失败(超时、报错等),启动定时任务重试。 重试策略通常按 阶梯式间隔 进行:

  • 第 1 次失败后,等 30 秒再试

  • 第 2 次失败后,等 1 分钟再试

  • 第 3 次失败后,等 5 分钟再试

  • 第 4 次失败后,等 10 分钟再试

  • 第 5 次失败后,等 30 分钟再试

步骤 3:达到最大次数后停止 重试到最大次数(如 5~10 次)后仍失败,停止通知,记录告警日志,等待人工介入。


四、设计要点

1)通知必须幂等

下游收到同一条通知多次时,处理结果应该一样。 下游需要通过业务唯一标识(如订单号)做幂等校验。

2)通知内容应精简

通知消息只包含 必要字段(如订单号、状态),不要传太多冗余数据。

3)重试策略要合理

间隔逐步拉长,避免在下游宕机时造成雪崩。

4)需要监控和告警

达到最大重试次数后,应该有明确的告警机制,不能静默丢弃。


五、适用场景

适合 通知型 场景,上游完成业务后只需告知下游,下游处理成功与否 不影响 上游事务结果:

  • 支付成功后通知发货

  • 订单完成后通知发积分

  • 审批通过后通知发送消息

不适合 强依赖型 场景,下游必须处理成功且需要确认:

  • 这类场景应该用 TCC 或可靠消息最终一致性。

六、一句话总结

最大努力通知的本质是: 事务完成后,按阶梯间隔多次重试通知下游,尽力而为,但兜底靠人工。 它比 2PC / TCC 更简单,比可靠消息更轻量,适合通知类场景。

3.6 SAGA 事务

一、基本概念

SAGA 事务 是分布式事务中的一种 长事务 解决方案。 它的核心思想是: 把一个跨多个服务的长事务,拆成一系列 本地短事务,每个服务独立提交,通过 补偿操作 来回滚失败的部分。 它属于一种 最终一致性 方案,重点不在立刻一致,而在 失败后能回退到一致状态。


二、为什么需要它

在一些业务流程中,事务跨度长、涉及多个服务: 如果用 2PC,整个链路要一直持有锁,性能极差; 如果用 TCC,每个服务都要写 Try/Confirm/Cancel 三套逻辑,成本高。 SAGA 的折中方式是: 每个步骤只做本地事务,立即提交释放资源,失败时按逆序执行补偿。


三、典型流程

以电商下单为例(创建订单 → 扣库存 → 扣优惠券 → 发积分): 步骤 1:正向执行 订单服务创建订单 → 库存服务扣库存 → 优惠券服务核销优惠券 → 积分服务加积分。 每个步骤都是独立的本地事务,提交后立刻释放锁。 步骤 2:失败时补偿 如果某个步骤失败(比如积分服务报错),SAGA 编排器会 按逆序 触发补偿: 积分服务补偿(取消加积分) → 优惠券服务补偿(恢复优惠券) → 库存服务补偿(恢复库存) → 订单服务补偿(取消订单)。 步骤 3:全部补偿完成后,事务回滚到一致状态。


四、协调模式

SAGA 有两种协调方式:

1)编排式(Orchestration)

由一个 SAGA 编排器(Orchestrator)统一管理流程。 编排器负责调用各服务、记录状态、触发补偿。 优点:流程集中可控,容易扩展; 缺点:编排器是额外组件,有一定复杂度。

2)协同式(Choreography)

各服务通过事件驱动,自己决定下一步做什么。 服务 A 完成后发布事件,服务 B 订阅并执行。 失败时服务发布补偿事件,其他服务订阅后回滚。 优点:去中心化,没有单点; 缺点:流程分散,调试困难。


五、设计要点

1)补偿操作必须幂等

补偿可能被多次触发,需要保证重复执行结果一致。

2)补偿不保证完全对称

正向操作和补偿操作不一定是镜像关系,需要根据业务语义设计。 例如正向是扣库存,补偿可能是加库存,但数量可能不同(比如有损场景)。

3)需要持久化事务日志

记录每个步骤的执行状态,服务重启后能恢复或继续补偿。

4)隔离性问题

SAGA 不保证隔离性,中间状态可能被其他事务读到。 需要通过业务设计来处理,比如标记进行中状态。


六、适用场景

适合 长链路业务流程 且各步骤 可补偿 的场景:

  • 电商下单(订单→库存→优惠券→积分)

  • 差旅报销审批

  • 会员注册送权益

不适合 强一致性 要求的场景:

  • 金融转账类业务应该用 2PC 或 TCC。

七、一句话总结

SAGA 的本质是: 长事务拆短,正向逐步提交,失败逆序补偿,靠业务语义保证最终一致。 它比 2PC 更轻量,比 TCC 更灵活,是长链路业务的常用方案。

主流框架与实现

4.1 Seata 详解

一、Seata 简介

Seata(Simple Extensible Autonomous Transaction Architecture)是阿里巴巴开源的分布式事务解决方案。 它提供了一站式的事务框架,支持多种事务模式,开发者只需少量配置即可在微服务中实现分布式事务。 官网:https://seata.io

二、Seata 的四种事务模式

1)AT 模式(默认)

AT 模式是 Seata 最常用的模式,对业务代码 零侵入。 原理: Seata 在执行 SQL 时自动生成 undo log,记录数据修改前后的快照。 提交时正常执行,回滚时利用 undo log 恢复数据。 流程:

  • 一阶段:业务 SQL 正常执行,Seata 拦截并解析 SQL,生成 undo log,本地事务提交

  • 二阶段提交:收到 TC 提交指令,异步删除 undo log

  • 二阶段回滚:收到 TC 回滚指令,利用 undo log 恢复数据

优点:

  • 业务代码不需要改动

  • 开发成本低

缺点:

  • 依赖关系型数据库(需要支持本地事务)

  • undo log 有额外开销

2)TCC 模式

TCC 模式需要业务自己实现 Try/Confirm/Cancel 三个接口。 适用场景:

  • 非关系型数据库场景

  • 需要精细控制的事务场景

  • 跨系统调用

优点:

  • 性能比 AT 模式高

  • 灵活度强

缺点:

  • 业务侵入性强,每个接口要写三套逻辑
3)SAGA 模式

Seata 内置了 SAGA 状态机,可以可视化编排事务流程。 适用场景:

  • 长链路业务流程

  • 遗留系统接入

优点:

  • 有可视化的状态机设计器

  • 支持 JSON 定义流程

4)XA 模式

XA 模式基于数据库的 XA 协议,强一致性。 适用场景:

  • 对一致性要求极高的场景

  • 传统银行/金融系统

优点:

  • 强一致性

缺点:

  • 性能差,锁持有时间长

三、Seata 架构三组件

|----|-------------------------|----------------------|
| 组件 | 全称 | 职责 |
| TC | Transaction Coordinator | 事务协调器,管理全局事务状态 |
| TM | Transaction Manager | 事务管理器,开启/提交/回滚全局事务 |
| RM | Resource Manager | 资源管理器,管理分支事务,向 TC 汇报 |

典型交互流程:

  1. TM 向 TC 申请开启全局事务

  2. RM 注册分支事务到 TC

  3. 业务执行,RM 向 TC 汇报状态

  4. TM 通知 TC 提交或回滚

  5. TC 通知所有 RM 执行二阶段操作


4.2 本地消息表 + 定时任务实现

一、方案概述

本地消息表 + 定时任务 是最简单、最实用的分布式事务实现方式之一。 它的核心思路是: 业务数据和消息记录放在同一个本地事务里,定时任务扫描未投递的消息并重试投递。

二、表结构设计

复制代码
-- 业务表
CREATE TABLE orders (
    order_id VARCHAR(32) PRIMARY KEY,
    amount DECIMAL(10,2),
    status VARCHAR(20)
);

-- 本地消息表
CREATE TABLE local_message (
    msg_id VARCHAR(32) PRIMARY KEY,      -- 消息唯一标识
    biz_type VARCHAR(32),                -- 业务类型
    content TEXT,                        -- 消息内容(JSON)
    status VARCHAR(10),                  -- 状态:PENDING/SUCCESS/FAILED
    retry_count INT DEFAULT 0,           -- 重试次数
    max_retry INT DEFAULT 5,             -- 最大重试次数
    next_retry_time DATETIME,            -- 下次重试时间
    create_time DATETIME,
    update_time DATETIME
);

三、核心流程

1)写入消息(与业务同事务)
复制代码
@Transactional
public void createOrder(Order order) {
    // 1. 写入订单表
    orderMapper.insert(order);
    
    // 2. 写入本地消息表(同一个本地事务)
    LocalMessage msg = new LocalMessage();
    msg.setMsgId(UUID.randomUUID().toString());
    msg.setBizType("ORDER_CREATED");
    msg.setContent(JSON.toJSONString(order));
    msg.setStatus("PENDING");
    msg.setNextRetryTime(new Date());
    localMessageMapper.insert(msg);
    
    // 一起提交,保证原子性
}
2)定时任务扫描投递
复制代码
@Scheduled(fixedDelay = 5000)
public void scanAndSend() {
    // 扫描待投递且到重试时间的消息
    List<LocalMessage> messages = localMessageMapper
        .selectPendingMessages(new Date());
    
    for (LocalMessage msg : messages) {
        try {
            // 投递到 MQ
            mqProducer.send(msg.getContent());
            
            // 更新状态为成功
            msg.setStatus("SUCCESS");
            localMessageMapper.update(msg);
        } catch (Exception e) {
            // 更新重试次数和下次重试时间
            msg.setRetryCount(msg.getRetryCount() + 1);
            if (msg.getRetryCount() >= msg.getMaxRetry()) {
                msg.setStatus("FAILED");
            } else {
                msg.setNextRetryTime(calculateNextRetry(msg.getRetryCount()));
            }
            localMessageMapper.update(msg);
        }
    }
}

四、优点与缺点

优点:

  • 实现简单,不依赖特殊组件

  • 适用于任何消息中间件

  • 消息可靠性高

缺点:

  • 业务代码需要维护消息表

  • 定时任务有延迟,不是实时投递

  • 消息表和主业务表在同一个数据库时可能成为瓶颈

五、与事务消息的区别

|------|---------|----------------|
| 对比项 | 本地消息表 | 事务消息(RocketMQ) |
| 实现方 | 业务系统自己 | MQ 中间件 |
| 投递延迟 | 定时扫描有延迟 | 半消息即时确认 |
| 依赖 | 通用 | 依赖支持事务消息的 MQ |
| 复杂度 | 偏业务侧 | 偏 MQ 侧 |


核心技术难点

5.1 幂等性设计

一、为什么需要幂等性

在分布式系统中,同一个请求可能被重复发送多次:

  • 网络超时后重试

  • MQ 消息重复消费

  • 定时任务重复执行

  • 用户重复点击

如果不做幂等,重复请求会导致数据错误(重复扣款、重复发货等)。

二、幂等性方案

1)唯一索引

在数据库层面使用唯一索引防止重复插入:

复制代码

CREATE UNIQUE INDEX uk_order_id ON order_detail(order_id);

最可靠,推荐作为第一道防线。

2)乐观锁版本号
复制代码
CREATE UNIQUE INDEX uk_order_id ON order_detail(order_id);

版本号匹配才执行更新,天然防重复。

3)去重表

专门维护一张去重表,记录已处理的请求 ID:

复制代码
UPDATE account 
SET balance = balance - 100, version = version + 1
WHERE id = 1 AND version = 5;
4)状态机约束

利用状态流转的不可逆性:

复制代码
INSERT IGNORE INTO idempotent_table (request_id, biz_type)
VALUES ('req_001', 'PAYMENT');
-- INSERT IGNORE:如果 request_id 已存在,不重复插入

三、方案选择建议

|------|-----------|
| 场景 | 推荐方案 |
| 插入操作 | 唯一索引 |
| 更新操作 | 乐观锁 / 状态机 |
| 消息消费 | |
| | |


5.2 空回滚与防悬挂 (TCC 特有)

一、空回滚

什么是空回滚

TCC 的 Cancel 阶段在没有执行过 Try 的情况下就被调用。 场景:

  1. TM 发起全局事务

  2. 协调器调用服务 A 的 Try

  3. Try 请求因网络超时到达服务 A

  4. 协调器等不到 Try 响应,认为 Try 失败

  5. 协调器发起全局回滚,调用服务 A 的 Cancel

  6. Cancel 先于 Try 到达服务 A

此时 Cancel 发现 Try 还没执行,这就是空回滚。

解决方案

维护一个事务 ID 记录表,Cancel 时检查 Try 是否执行过:

复制代码
public boolean cancel(String xid) {
    // 检查 Try 是否已执行
    if (!tryRecordDao.exists(xid)) {
        // Try 未执行,记录空回滚,直接返回成功
        tryRecordDao.insert(xid, "CANCEL_SKIPPED");
        return true;
    }
    // Try 已执行,正常回滚
    doCancel(xid);
    return true;
}

二、防悬挂

什么是悬挂

TCC 的 Try 请求在网络延迟后,在 Cancel 之后才到达服务。 场景:

  1. Cancel 先到达,执行了空回滚处理

  2. Try 请求在网络中延迟后到达

  3. Try 被执行,但全局事务已经回滚

这导致 Try 成功执行了,但事务已经回滚,数据不一致。

解决方案

在 Cancel 执行空回滚时,也要记录一条事务记录,后续 Try 到达时检查:

复制代码
public boolean try(String xid) {
    // 检查是否已经执行过 Cancel(空回滚)
    if (tryRecordDao.exists(xid)) {
        // Cancel 已执行过,Try 不能再执行,直接返回失败
        return false;
    }
    // 正常执行 Try
    doTry(xid);
    tryRecordDao.insert(xid, "TRY_DONE");
    return true;
}

三、总结记忆

  • 空回滚:Cancel 先到,Try 还没做 → 记录空回滚标记

  • 防悬挂:Try 迟到,Cancel 已做完 → 阻止迟到 Try 执行

  • 关键:维护一张事务记录表,Try 和 Cancel 都先查表


5.3 事务状态存储与恢复

一、为什么要存储事务状态

分布式事务执行过程中可能发生故障:

  • TC 协调器宕机

  • RM 分支事务执行一半

  • 网络分区导致状态不一致

如果没有持久化状态记录,故障恢复后将不知道事务进行到哪一步。

二、状态存储方案

1)数据库存储(推荐)

将全局事务和分支事务的状态持久化到数据库:

复制代码
-- 全局事务表
CREATE TABLE global_transaction (
    xid VARCHAR(128) PRIMARY KEY,
    status VARCHAR(20),        -- BEGIN/COMMITTING/COMMITTED/ROLLBACKING/ROLLBACKED
    begin_time DATETIME,
    timeout INT,
    application_id VARCHAR(64),
    tx_service_group VARCHAR(64)
);

-- 分支事务表
CREATE TABLE branch_transaction (
    branch_id BIGINT PRIMARY KEY,
    xid VARCHAR(128),          -- 关联全局事务
    resource_id VARCHAR(128),  -- 数据源标识
    lock_key VARCHAR(256),     -- 全局锁
    status VARCHAR(20),        -- REGISTERED/PHASE_ONE_DONE/PHASE_TWO_COMMITTED/PHASE_TWO_ROLLBACKED
    client_ip VARCHAR(64)
);
2)日志存储

使用 WAL(Write-Ahead Log)机制,先写日志再执行操作。

3)Redis 存储

适用于对性能要求高、可容忍少量数据丢失的场景。

三、事务恢复流程

TC 重启后的恢复流程:

  1. 加载数据库中的事务状态

  2. 对状态为 COMMITTING 的全局事务:通知所有 RM 提交

  3. 对状态为 ROLLBACKING 的全局事务:通知所有 RM 回滚

  4. 对超时未完成的事务:根据策略回滚或告警

四、注意事项

  • 状态表要有唯一索引和重试机制

  • 恢复操作本身也要幂等

  • 定期清理已完成的事务记录,避免表膨胀

4.1 Seata 详解

一、Seata 简介

Seata(Simple Extensible Autonomous Transaction Architecture)是阿里巴巴开源的分布式事务解决方案。 它提供了一站式的事务框架,支持多种事务模式,开发者只需少量配置即可在微服务中实现分布式事务。 官网:https://seata.io

二、Seata 的四种事务模式

1)AT 模式(默认)

AT 模式是 Seata 最常用的模式,对业务代码零侵入。 原理: Seata 在执行 SQL 时自动生成 undo log,记录数据修改前后的快照。 提交时正常执行,回滚时利用 undo log 恢复数据。 流程:

  • 一阶段:业务 SQL 正常执行,Seata 拦截并解析 SQL,生成 undo log,本地事务提交

  • 二阶段提交:收到 TC 提交指令,异步删除 undo log

  • 二阶段回滚:收到 TC 回滚指令,利用 undo log 恢复数据

优点:

  • 业务代码不需要改动

  • 开发成本低

缺点:

  • 依赖关系型数据库

  • undo log 有额外开销

2)TCC 模式

TCC 模式需要业务自己实现 Try/Confirm/Cancel 三个接口。 适用场景:

  • 非关系型数据库场景

  • 需要精细控制的事务场景

  • 跨系统调用

3)SAGA 模式

Seata 内置了 SAGA 状态机,可以可视化编排事务流程。 适用场景:

  • 长链路业务流程

  • 遗留系统接入

4)XA 模式

XA 模式基于数据库的 XA 协议,强一致性。 适用场景:

  • 对一致性要求极高的场景

  • 传统银行/金融系统

三、Seata 架构三组件

|----|-------------------------|----------------------|
| 组件 | 全称 | 职责 |
| TC | Transaction Coordinator | 事务协调器,管理全局事务状态 |
| TM | Transaction Manager | 事务管理器,开启/提交/回滚全局事务 |
| RM | Resource Manager | 资源管理器,管理分支事务,向 TC 汇报 |

典型交互流程:

  1. TM 向 TC 申请开启全局事务

  2. RM 注册分支事务到 TC

  3. 业务执行,RM 向 TC 汇报状态

  4. TM 通知 TC 提交或回滚

  5. TC 通知所有 RM 执行二阶段操作

全局锁导致的写隔离

一、基本概念

在 Seata AT 模式中,本地事务提交前会向 TC 注册分支事务,并上报 lock_keylock_key 通常由"表名 + 主键值"组成,用来表达本次事务改动了哪些业务记录。

二、它如何实现写隔离

当另一个全局事务也想修改同一批记录时,TC 会发现 lock_key 冲突,拒绝加锁或让其重试。这样就避免了两个全局事务同时提交,导致覆盖写、库存超扣等问题。

三、典型执行流程

  1. 一阶段执行业务 SQL,并生成 undo log。

  2. 分支事务提交前注册全局锁。

  3. TC 记录 lock_key → xid / branchId 的映射。

  4. 二阶段提交后释放锁;若回滚,也在回滚完成后释放锁。

四、重试与清理机制

获取全局锁失败时,RM 会按配置进行锁重试;超过次数后抛出异常,由上层决定回滚或降级。异常中断后,TC 会依赖分支状态、超时回收与正常二阶段清理释放陈旧锁。

五、优缺点

优点是无需业务侵入即可提供跨服务"写串行化"效果;缺点是热点行会形成锁竞争,长事务会放大全局锁持有时间,影响吞吐量。

读隔离:快照读 vs 当前读(Seata AT 模式分析)

一、快照读与当前读

快照读依赖数据库 MVCC,普通 SELECT 读取的是一致性视图;当前读如 SELECT ... FOR UPDATEUPDATEDELETE 会读取最新版本并尝试加锁。

二、Seata AT 与读隔离的关系

Seata AT 的核心是 undo log + 全局锁。undo log 主要服务于回滚恢复,不直接改变数据库原生的读隔离级别;因此普通查询仍主要遵循底层数据库的 RC / RR 语义。

三、可能出现的风险

如果一个分支事务一阶段已提交本地数据、但全局事务最终要回滚,那么在二阶段回滚前,其他事务可能短暂读到这份"将来会被撤销"的中间数据。这就是 AT 模式常说的脏读窗口。

四、常见处理手段

  1. 关键读场景改为当前读并配合全局锁校验。

  2. 对强一致查询走主流程串行化,避免旁路读取。

  3. 给业务对象增加"处理中/冻结中"状态,屏蔽中间态。

  4. 对账、结算等高敏感流程尽量选 XA / TCC,而不是单靠 AT。

五、一句话总结

AT 模式对写隔离更强,对读隔离更多是"借用数据库本身能力 + 业务规避中间态风险"。

不可重复读与脏读风险控制

一、问题来源

分布式事务跨多个库、多个服务时,单库隔离级别只能保护本地读写,无法天然覆盖"全局事务中间态"。因此不可重复读、脏读控制,往往需要数据库能力和事务协调器共同完成。

二、脏读如何控制

脏读的核心是读到了未最终确认的数据。常见控制手段包括:

  • 强一致链路使用 XA / TCC,减少中间态暴露;

  • AT 模式下对关键查询加锁或延后读取;

  • 使用业务状态位(冻结、处理中、待确认)隔离未完成数据。

三、不可重复读如何控制

不可重复读本质是同一事务两次读取结果不同。解决方法通常有两类:

  1. 依赖数据库 RR / Serializable;

  2. 在服务层缓存事务上下文快照,保证同一流程内读取口径一致。

四、全局层面的工程做法

真正落地时,更常见的是"风险分层":

  • 资金、库存类:优先保证强一致,必要时牺牲性能;

  • 查询、报表类:接受短时不一致,通过异步校正和对账修复。

五、一句话总结

分布式事务无法只靠一个开关解决隔离问题,通常要把"数据库隔离 + 全局锁/协调器 + 业务状态设计"组合起来使用。

7.1 降级与熔断

一、为什么要降级

分布式事务链路长、依赖多,一旦 TC、库存、优惠券等下游抖动,整条事务会被放大为级联失败。此时继续强行走全局事务,往往会让系统雪崩。

二、何时触发降级

常见信号包括:事务超时显著升高、锁冲突激增、下游接口错误率飙升、TC 不可用、核心线程池或连接池耗尽。

三、典型方案

可以用 Sentinel / Resilience4j 做熔断、限流和舱壁隔离:

  1. 非核心链路关闭分布式事务,改走本地事务 + 异步补偿;

  2. 对高失败依赖直接快速失败,避免长时间阻塞;

  3. 给用户返回"受理中"而不是"立即完成"。

四、Fallback 设计

Fallback 不是简单报错,而是给出可接受退路,例如:下单成功但积分稍后到账、优惠券稍后核销、对账后自动补发。

五、权衡点

降级提升可用性,但会降低一致性与实时性,所以只应在系统压力过高或非核心能力异常时启用。

7.2 规避分布式事务的长路径

一、为什么长路径危险

一个事务经过的服务越多,失败概率越高、锁持有时间越长、排查成本越大。链路一长,任何一个慢节点都会把整条事务拖慢。

二、设计原则

应尽量把"必须同步成功"的步骤压缩到最短,只保留订单落库、库存预占这类核心动作;积分、消息、营销等边缘动作尽量后置。

三、常见优化方式

  1. 本地优先:先完成核心本地事务,再发布事件。

  2. 能合并的服务尽量合并边界,减少跨库跳转。

  3. 预校验前置,把资格、风控、库存检查放到事务外。

  4. 用可靠消息、SAGA 代替同步串行调用。

四、事件驱动替代

很多"必须一步到位"的需求,实际可拆成"核心成功 + 事件通知 + 异步消费",只要业务允许分钟级或秒级收敛,就不必强上长链路全局事务。

五、一句话总结

最好的分布式事务优化,往往不是把事务做快,而是把不该放进事务的步骤先拿出去。

7.3 异步化与最终一致性设计

一、核心思路

把同步分布式事务改造成"本地提交 + 事件投递 + 异步消费",本质上是用时间换吞吐量,用补偿换耦合度下降。

二、典型实现模式

常见做法包括:可靠消息、Outbox、本地消息表、事件总线、Event Sourcing。核心要求是"业务状态变更"和"事件发出"不能丢,也不能重复失控。

三、设计要点

  1. 事件必须有唯一业务键,保证消费幂等。

  2. 失败可重试,且有死信队列或人工补偿通道。

  3. 关键状态要可查询,让用户知道系统处于"处理中"而不是黑盒失败。

四、何时适合最终一致性

当业务允许短暂延迟、允许补偿、且不要求用户在毫秒级看到所有结果统一时,就适合异步化。例如积分发放、消息通知、券包发放、报表更新。

五、不适用场景

资金扣减、账户余额、核心库存确认等强一致场景,不应只依赖最终一致性,而应配合 XA、TCC 或更强约束方案。

8.1 电商下单场景(订单+库存+积分+优惠券)

一、推荐拆分方式

电商下单通常不建议把"订单、库存、优惠券、积分"全部放进一个强一致大事务。更稳妥的方案是:订单创建 + 库存预占走强一致,优惠券核销和积分发放走异步补偿。

二、一个可落地流程

  1. 订单服务创建待支付订单;

  2. 库存服务预占库存;

  3. 优惠券服务冻结优惠券;

  4. 支付成功后,确认订单并异步触发积分发放;

  5. 任一步失败则按规则释放库存、解冻优惠券、关闭订单。

三、适合的事务模式

  • 下单/预占阶段:TCC 或 Seata AT;

  • 支付后续扩展动作:可靠消息 / SAGA;

  • 通知类动作:最大努力通知。

四、异常处理

若库存失败,订单直接关闭;若优惠券核销失败,可回滚订单或给出重新领取;若积分发放失败,记录任务并重试,不阻塞主下单链路。

五、一句话总结

电商下单的关键是"核心链路强一致,附属权益最终一致"。

8.2 银行转账跨行场景

一、业务特点

跨行转账对金额正确性、状态可追溯、失败可回滚要求极高,比普通互联网交易更强调强一致和审计能力。

二、常见方案

若两边都能参与标准事务协议,可用 XA / 2PC;若系统异构严重,更常见的是 TCC:

  • Try:冻结转出账户金额、登记待入账;

  • Confirm:正式扣减转出并入账转入;

  • Cancel:释放冻结金额、撤销待处理状态。

三、对账与恢复

即使主流程设计严谨,也必须有日终对账、流水核对、异常补单机制。因为跨行链路常涉及网络抖动、核心账务系统延迟、第三方确认报文丢失等问题。

四、异常处理

若扣款成功但入账确认超时,系统不能简单重复转账,而应先查事务日志、查冻结状态,再决定 confirm / cancel / 人工介入。

五、一句话总结

银行跨行转账更像"高可靠事务编排 + 强对账体系",不能只看事务协议本身。

8.3 支付回调处理

一、核心问题

支付平台回调天然具有"重复通知、乱序到达、网络超时后重试"的特点,因此回调处理首先不是事务问题,而是幂等问题。

二、典型处理流程

  1. 验签并校验订单号、金额、商户号;

  2. 根据支付流水号或业务单号做幂等去重;

  3. 首次处理时更新订单状态、记录支付流水;

  4. 后续触发发货、积分、通知等异步动作。

三、重试与通知

回调处理完成后要尽快返回平台成功响应,避免平台继续重试。内部下游失败则交给消息队列、任务表或补偿任务处理,而不是卡在回调接口里。

四、常见风险控制

  • 同一支付单多次回调:用唯一索引 + 幂等表解决;

  • 先退款后回调成功:以状态机限制非法状态流转;

  • 下游通知失败:走最大努力通知并记录重试。

五、一句话总结

支付回调的关键原则是:回调主链路短、状态流转幂等、扩展动作异步化。

总结与选型建议

一、选型核心原则

没有"最好的分布式事务方案",只有"最适合当前业务约束的方案"。通常从三个维度判断:

  1. 一致性要求有多强;

  2. 性能与吞吐压力有多高;

  3. 业务是否愿意承担补偿与复杂度。

二、常见方案对比

|----------|------------|----|------|---------------|
| 方案 | 一致性 | 性能 | 业务侵入 | 适合场景 |
| 2PC / XA | 强一致 | 较低 | 低~中 | 银行、账务、核心资金 |
| TCC | 强一致 | 中 | 高 | 核心链路、可预留资源业务 |
| SAGA | 最终一致 | 较高 | 中 | 长流程、可补偿业务 |
| 可靠消息 | 最终一致 | 高 | 中 | 订单后置动作、异步协同 |
| 最大努力通知 | 弱一致/最终一致 | 高 | 低 | 短信、回调、非核心通知 |
| Seata AT | 近强一致(偏写一致) | 中 | 低 | 关系型数据库、快速接入场景 |

三、如何决策

  • 要强一致,且数据库/中间件支持标准协议:优先 XA。

  • 要强一致,但业务可设计冻结/确认/取消:优先 TCC。

  • 流程长、服务多、允许补偿:优先 SAGA。

  • 核心交易完成后还有很多边缘动作:优先可靠消息。

  • 只是通知对方"你来补一下":最大努力通知即可。

  • Java 微服务 + 单条 SQL 改造少 + 想快速落地:Seata AT 很有性价比。

四、工程上的真实建议

真正成熟的系统往往不是只选一种,而是混合使用:资金主链路用 TCC / XA,营销和积分用消息最终一致,通知链路用最大努力通知。

五、一句话总结

选型本质不是比较概念,而是在"一致性、性能、复杂度"三角形里找到业务能长期承受的那个点。

延伸阅读与进阶方向

共识算法(Paxos, Raft)与分布式事务的区别

一、解决的问题不同

共识算法解决的是"多个节点如何对同一份日志/状态达成一致";分布式事务解决的是"多个资源操作如何作为一个业务单元成功或失败"。

二、关注点不同

Paxos / Raft 关注主从选举、日志复制、状态机一致;XA / TCC / SAGA 关注提交、回滚、补偿、业务一致性。

三、关系

它们不是替代关系,而是上下层关系:很多事务协调器、元数据存储、分布式数据库底层,都会借助 Raft 保证自身状态一致。

分布式事务与分布式锁的位置关系

一、能力边界不同

分布式锁解决的是"同一时刻只允许一个执行者进入临界区";分布式事务解决的是"多个操作要么都成功,要么都失败"。

二、不要混用概念

加了分布式锁,不等于事务就安全;用了分布式事务,也不一定能避免高层业务并发冲突。很多场景两者要配合使用。

三、典型例子

比如优惠券抢占可先用分布式锁限并发,再用本地/分布式事务保证扣减和记录原子提交。

云原生下的事务趋势:Transaction Mesh(事务网格)

一、基本思路

Transaction Mesh 试图把事务协调能力从业务代码中抽离出来,像 Service Mesh 一样,通过 Sidecar / Proxy 承担协议拦截、状态上报、补偿协调。

二、潜在价值

这样做的好处是语言无关、接入统一、治理能力集中,特别适合多语言微服务和云原生环境。

三、现实挑战

难点在于事务语义比流量转发复杂得多,涉及业务状态、回滚语义、幂等与补偿,当前仍处于探索期,离大规模标准化还有距离。

相关推荐
覆东流1 小时前
Java开发环境搭建
java·开发语言·后端
阿洛学长2 小时前
VMware安装虚拟机教程(超详细)
java·linux·开发语言
coder Ethan2 小时前
Spring AI 入门:(3)快速搭建一个简单的问答助手
java·人工智能·spring
屋外雨大,惊蛰出没2 小时前
starter的创建与引用
java·stater
小同志002 小时前
Spring Boot ⽇志概述(简单了解)
java·java-ee·日志
小马爱打代码2 小时前
SpringBoot + 延迟消息 + 时间轮:订单超时、优惠券过期等场景的高效实现方案
java·spring boot·后端
就叫_这个吧2 小时前
Java普通类、抽象类、接口的应用和区别
java·开发语言
梅孔立2 小时前
解决Nginx缓存不写入响应体问题:浏览器强制不缓存配置教程
java·开发语言·nginx·spring
方也_arkling2 小时前
【Java-Day18】API篇-Arrays
java·算法·排序算法