交易跨10个库、日均千万订单——选错一次分布式事务方案,加班三个月重写

173 阅读22分钟

分布式系统设计:CAP/BASE 选型 + 分布式事务四方案 + Raft 共识

分布式事务方案选错,代价不是改几行代码——是重写整个交易链路。我从一个日均千万订单的项目里总结出一张四方案决策树:Seata AT(改得少→性能掉 30%)→TCC(代码 3 倍→但一致性最强)→Saga(长流程)→RocketMQ 事务消息(最简单→只适用异步)。选方案前先想清楚一个问题——你这个场景真的需要强一致吗?

阅读约 16 分钟 | 系列第 11/17 篇


一、CAP 定理:P 必须选,C 与 A 的权衡

CAP 定理:一个分布式系统最多同时满足 Consistency(一致性)、Availability(可用性)、Partition Tolerance(分区容忍性)中的两个。

含义违反后果
C(一致性)所有节点同一时刻看到相同数据读到旧数据
A(可用性)每个请求都能获得非错误响应服务不可用
P(分区容忍性)节点间网络断开后系统仍可运作系统瘫痪

P 必须选——网络分区一定会发生(交换机故障、网络延迟、机房断电)。剩下在 C 和 A 之间权衡:

  • 金融核心场景(账户余额、订单状态)→ 选 CP,宁可短暂不可用也不能数据不一致
  • 非交易场景(用户昵称、商品描述)→ 选 AP,最终一致即可
  • 不同服务可以有不同的 CAP 选择——核心服务偏 C,外围服务偏 A

场景案例:支付系统的双链路 CAP 选择

同一个支付系统内,不同服务的 CAP 选择可以不同:

服务CAP 选择网络分区时的行为业务理由
订单服务(下单、查询订单)AP继续接受订单,返回"订单处理中";下游恢复后异步同步"系统能用"比"数据即时准确"更重要——用户看到"处理中"可以接受,看到"500 错误"会流失
账户服务(余额扣减、记账)CP拒绝写入,返回"系统繁忙,请稍后重试"余额不准是资金安全底线——宁可暂时不可用,绝不能出现"扣了钱但没记账"

"同一个系统内,不同服务可以做不同的 CAP 选择——核心资金链路选 CP 保一致性,外围展示链路选 AP 保可用性。这不是技术妥协,而是业务建模:CAP 的选择本质上是'这个服务在故障时伤害哪一端'的决策。"

PACELC:CAP 的细化模型

CAP 的局限性在于它只考虑了"发生网络分区时"的取舍——但网络分区并非常态。PACELC 将场景拆分为两类:

场景含义权衡
P (Partition)发生网络分区时trade A vs C(同 CAP)
E (Else)无分区、正常运行中trade L (Latency,延迟) vs C (Consistency,一致性)

典型系统的 PACELC 分类:

系统PACELC 类型含义
DynamoDB、CassandraPA/EL分区时选可用性;正常时选低延迟(允许读到旧数据)
HBase、ZooKeeperPC/EC分区时选一致性;正常时也选一致性(读操作可能等待同步)
MongoDB(默认配置)PA/EC分区时选可用性(允许主从切换短暂不一致);正常时选一致性(读主节点)

"PACELC 的真正价值在于提醒我们:CAP 的 C/A 权衡不是唯一的——即使系统正常运行,延迟与一致性之间也存在取舍。将数据复制到三个副本后才返回成功(强一致),和写入主节点后立即返回(低延迟但可能读到旧数据),是每天都在发生的选择。"

BASE 理论(AP 的实践指导)

Basically Available(基本可用)→ Soft State(允许中间态)→ Eventually Consistent(最终一致性)。做不到强一致性(C),那就保障基本可用 + 最终一致性。


二、分布式事务:四种方案递进

2PC(两阶段提交)

两阶段提交的核心流程——协调者(Coordinator)先问所有参与者"能提交吗?"(Prepare),全票通过后再发"正式提交"(Commit):

客户端                 协调者                 参与者A         参与者B         参与者C
  |                      |                      |               |               |
  |--- 提交请求 -------->|                      |               |               |
  |                      |                      |               |               |
  |                  [Phase 1: Prepare]         |               |               |
  |                      |--- Prepare --------->|               |               |
  |                      |--- Prepare ----------|-------------->|               |
  |                      |--- Prepare ----------|---------------|-------------->|
  |                      |                      |               |               |
  |                      |<-- YES --------------|               |               |
  |                      |<-- YES --------------|-------------->|               |
  |                      |<-- YES --------------|---------------|-------------->|
  |                      |                      |               |               |
  |                  [Phase 2: Commit]          |               |               |
  |                      |--- Commit ---------->|               |               |
  |                      |--- Commit -----------|-------------->|               |
  |                      |--- Commit -----------|---------------|-------------->|
  |                      |                      |               |               |
  |                      |<-- ACK --------------|               |               |
  |                      |<-- ACK --------------|-------------->|               |
  |                      |<-- ACK --------------|---------------|-------------->|
  |                      |                      |               |               |
  |<-- 提交成功 ---------|                      |               |               |

故障场景:任一参与者返回 NO 或超时 → 协调者发 Rollback 给所有参与者。

致命缺陷:① 同步阻塞——Prepare 阶段锁住资源,整个提交完成前其他事务无法操作同一行数据;② 协调者单点故障——Prepare 阶段全部通过后协调者宕机,参与者不知道该提交还是回滚,资源锁永久持有;③ 数据不一致——Commit 阶段协调者仅发出了部分 Commit 请求就宕机,部分参与者提交了、部分没有。

XA 协议是 2PC 的标准实现。金融系统通常不采用 2PC 做跨服务事务——性能代价过高,单点故障风险不可接受。

TCC(Try-Confirm-Cancel)

业务层提供三个方法,将事务控制权从数据库层上移到应用层:

阶段动作要求
Try预留资源 + 校验各服务并行执行,锁定本次事务所需的全部资源
Confirm确认执行使用 Try 阶段预留的资源完成业务操作。必须幂等——Confirm 可能被重试
Cancel取消释放回滚 Try 阶段的资源预留。必须幂等——Cancel 可能被重试

转账示例:账户 A 向账户 B 转账 100 元(TCC 伪代码)

// ============ Try 阶段 ============
// 账户服务 A:冻结资金
public void tryDecrease(String accountA, BigDecimal amount) {
    // UPDATE account SET available = available - 100, frozen = frozen + 100
    // WHERE account_id = 'A' AND available >= 100
    // 若 available < 100 → 抛出异常,触发全局 Cancel
}

// 账户服务 B:校验账户状态(不做实际资金变动)
public void tryIncrease(String accountB, BigDecimal amount) {
    // SELECT status FROM account WHERE account_id = 'B'
    // 若账户不存在或已冻结 → 抛出异常,触发全局 Cancel
}

// ============ Confirm 阶段 ============
// 账户服务 A:实际扣减冻结资金
public void confirmDecrease(String accountA, BigDecimal amount) {
    // UPDATE account SET frozen = frozen - 100
    // WHERE account_id = 'A' AND frozen >= 100
    // 返回受影响行数:若为 0 说明已 Confirm 过(幂等保障),直接返回成功
}

// 账户服务 B:实际增加余额
public void confirmIncrease(String accountB, BigDecimal amount) {
    // UPDATE account SET balance = balance + 100 WHERE account_id = 'B'
    // 返回受影响行数:若为 0 说明已 Confirm 过(幂等保障),直接返回成功
}

// ============ Cancel 阶段 ============
// 账户服务 A:解冻资金
public void cancelDecrease(String accountA, BigDecimal amount) {
    // UPDATE account SET available = available + 100, frozen = frozen - 100
    // WHERE account_id = 'A' AND frozen >= 100
    // 返回受影响行数:若为 0 说明已 Cancel 过或 Try 未执行(幂等保障),直接返回成功
}

// 账户服务 B:无操作(Try 阶段未冻结任何资源)
public void cancelIncrease(String accountB, BigDecimal amount) {
    // 空操作——直接返回成功
}

TCC 核心要点

  • 优点:性能高(各服务并行 Try,无数据库长事务锁);不依赖底层数据库的分布式事务支持
  • 缺点:侵入大(每个服务写三套代码);Confirm/Cancel 必须幂等(通过事务状态表 + 唯一约束实现)
  • 空回滚:Cancel 先于 Try 到达时,需判断 Try 是否执行过,未执行则直接返回成功——避免对一笔从未开始的事务执行回滚
  • 防悬挂:Cancel 比 Try 先执行时,Cancel 记录一条"Cancel 已执行"标记,后续迟到的 Try 检查到此标记后直接拒绝执行

MQ 最终一致性

基于 RocketMQ 事务消息:发送 half 消息(消费者不可见)→ 执行本地事务 → commit/rollback。如果本地事务执行完但未发送 commit(进程 crash),MQ 定期回查上游事务状态。

生产者                                         RocketMQ                                     消费者
  |                                               |                                            |
  |--- 发送 half 消息 --------------------------->|  (消息暂存,消费者不可见)                    |
  |                                               |                                            |
  |--- 执行本地事务(如:扣减库存)                 |                                            |
  |   成功 → commit / 失败 → rollback             |                                            |
  |                                               |                                            |
  |         【异常分支:本地事务执行完但 commit 未发出——进程 crash】                               |
  |                                               |                                            |
  |                                    MQ 回查:调用生产者提供的 check 回调                       |
  |                                    <-- checkLocalTransaction(txId) --|                      |
  |                                    -- 返回 COMMIT/ROLLBACK -------->|                      |
  |                                               |                                            |
  |                                               |--- 消息对消费者可见 ------------------------>|
  |                                               |                              消费者执行本地事务 |

适用场景:非核心链路——下单成功后发短信、更新统计表、同步搜索索引等。"允许短暂不一致,但不允许永久不一致。"

Seata AT 模式

一阶段提交业务 SQL + 记录 undo_log → 二阶段全局提交(异步删除 undo_log)或回滚(反向补偿 SQL)。对业务侵入最小——只需在方法上添加 @GlobalTransactional 注解,Seata 自动代理数据源,拦截 SQL 并记录回滚信息。

代价:① 隔离性较弱——一阶段提交后、二阶段完成前,其他事务可能读到未全局确认的数据(可通过 @GlobalLock + SELECT FOR UPDATE 解决);② 存在性能开销——每个写操作额外生成 undo_log,全局锁在 TC(事务协调者)侧维护。

金融系统选型

链路方案原因
核心交易(下单/资金扣划)TCC性能最高,强一致性,Confirm/Cancel 幂等可保障资金安全
非核心(通知/日志/统计)MQ 最终一致 + Seata AT侵入小,最终一致即可,允许短暂延迟

"分布式事务没有银弹,核心是理解每种方案的代价——强一致性 = 性能代价 + 复杂度代价。TCC 最强但也最重,MQ 最终一致最轻但也最弱,Seata AT 居中。选型就是在这根轴上调位置。"


三、Raft 共识算法

Raft 要解决的问题:分布式系统中多个节点如何对一个值达成一致。Raft 将共识问题拆解为三个子问题——Leader 选举日志复制安全性

Raft 集群中每个节点处于三种角色之一:Leader(处理所有写请求)、Follower(被动响应)、Candidate(选举中的临时角色)。

① Leader 选举:具体场景还原

初始状态:3 节点集群——A(Leader,term=1)、B(Follower,term=1)、C(Follower,term=1)。选举超时时间随机化为 150-300ms 区间。

时刻 T₀:正常运行
  A --[心跳 50ms/次]--> B
  A --[心跳 50ms/次]--> C
  B 和 C 每次收到心跳,重置自己的选举超时计时器

时刻 T₁:A 宕机
  心跳停止。B 和 C 的选举超时计时器开始倒计时

时刻 T₂:B 的选举超时触发(B 随机到了 180ms,C 的计时器还剩 40msB 的角色:Follower → Candidate
  B 的 term:12
  B 投票给自己:voteCount = 1
  B 向 C 发送 RequestVote RPC:
    {term: 2, candidateId: B, lastLogIndex: 10, lastLogTerm: 1}

时刻 T₃:C 收到 B 的 RequestVote
  C 的判断逻辑:
    ① B.term (2) >= C.currentTerm (1)?→ 是,C 更新 currentTerm = 2
    ② C 在当前 term (2) 投过票吗?→ 没有
    ③ B 的日志至少和自己一样新吗?
       - B.lastLogTerm (1) > C.lastLogTerm (1)?→ 否(相等)
       - 相等时比较 lastLogIndex:B (10) >= C (10)?→ 是
       → 日志足够新,合格
  C 回复:{term: 2, voteGranted: true}
  C 重置选举超时计时器(因为收到了更大 term 的 RPC)

时刻 T₄:B 收到 C 的投票
  B 的投票数:1(自己)+ 1(C)= 2
  2 > 3/2 → 超过半数 → B 当选为 term 2 的 Leader

时刻 T₅:B 开始发送心跳
  B --[心跳,term=2]--> C
  C 收到心跳,确认 B 为 Leader
  C 重置选举超时计时器

竞争选举(Split Vote):若 B 和 C 几乎同时超时,两者都变为 Candidate,各自投票给自己,各得 1 票——均未超过半数。两个 Candidate 各自进入下一轮随机超时等待,先超时者赢得下一轮选举。随机化超时时间是 Raft 选举机制避免活锁的关键设计。

选举关键规则:① 一个 term 内,每个节点最多投一票;② Candidate 的日志必须不比投票者旧(先比较 lastLogTerm,term 相同再比较 lastLogIndex);③ 收到 term 大于自身 term 的任何 RPC → 立即转为 Follower,更新自身 term。

② 日志复制:10 步完整时序

Raft 的日志复制是"多数派确认"机制最核心的体现:

前提条件:Bterm 2LeaderA 已宕机,CFollower
Leader B 的日志:[idx1,t1] [idx2,t1] [idx3,t1]   committedIndex=3
Follower C 的日志:[idx1,t1] [idx2,t1] [idx3,t1]  committedIndex=3

Step 1: 客户端向 Leader B 发送写请求 → SET X = 5

Step 2: Leader B 将命令追加到本地日志
  B 的新日志条目:{index: 4, term: 2, command: "SET X=5"}
  (尚未提交——committedIndex 仍为 3Step 3: Leader B 向所有 FollowerCA)并行发送 AppendEntries RPC
  内容:{term: 2, leaderId: B,
         prevLogIndex: 3, prevLogTerm: 1,    ← 用于一致性检查
         entries: [{index: 4, term: 2, command: "SET X=5"}],
         leaderCommit: 3}

Step 4: Follower C 收到 AppendEntries,执行一致性检查
  C 检查自己的日志在 index=3 处的 term 是否为 1prevLogTerm)
  → 是 → 一致性检查通过
  → Centry {index:4, term:2, cmd:"SET X=5"} 追加到自己的日志
  → 回复:{term: 2, success: true}

Step 5: Follower A 已宕机,无回复(Leader B 会持续重试 AppendEntriesStep 6: Leader B 收到 C 的确认
  现在拥有 entry {index:4, term:2} 的节点:BLeader)+ CFollower)= 22 > 3/2 → 超过半数 → 可以提交!

Step 7: Leader B 提交 entry index=4
  Bentry 应用到状态机:X = 5
  B 更新 committedIndex = 4

Step 8: Leader B 返回"写入成功"给客户端
  (客户端得到响应——此时 Follower C 尚未提交,但不影响正确性)

Step 9: 下一次心跳中,Leader B 通知 committedIndex=4
  Follower C 收到心跳,发现 committedIndex=4 > 自己的 committedIndex=3Centry index=4 应用到状态机:X = 5C 更新 committedIndex = 4

Step 10: 完成。3 个节点中有 2 个持久化了 entry index=4
  后续 A 恢复后,Leader B 会通过 AppendEntries 补齐 A 缺失的日志

日志复制的核心:一致性检查。AppendEntries 中的 prevLogIndexprevLogTerm 是两个关键的校验字段——Follower 会检查自己日志在 prevLogIndex 位置的 term 是否等于 prevLogTerm。若不等,说明 Follower 的日志与 Leader 在某个位置出现了分叉,Leader 会递减 prevLogIndex 逐条回退,直到找到一个一致性交汇点后开始覆盖写入。这个简单的设计保证了一个重要属性:如果两个节点的日志在同一个 index 上有相同的 term,那么它们在这个 index 之前的所有条目完全一致。

③ 安全性

  • 选举限制:Candidate 的日志必须"至少和投票者同样新"——先比较 lastLogTerm,term 相同再比较 lastLogIndex。这保证了已提交的 entry 不会在后续任期中丢失
  • 提交规则:Leader 只能提交当前 term的 entry——不能通过提交旧 term 的 entry 来间接提交。这防止了"已提交的 entry 在后续 term 中被覆盖"的异常情况

Paxos vs Raft

维度Multi-PaxosRaft
可理解性难,论文晦涩易,明确拆为三子问题
Leader可有多个 Proposer严格单一 Leader(强 Leader 模型)
日志允许不严格连续(可并发提交,存在空洞)严格连续递增,不允许空洞
成员变更需单独处理(Joint Consensus / 单步变更)内置 Joint Consensus 机制,更工程化
业界采用Google Spanner、OceanBase、Chubbyetcd、Consul、TiKV、Nacos

为什么 OceanBase 选 Multi-Paxos? ① OceanBase 起步早(2010 年),Raft 论文 2013 年才发表——时间窗口决定了技术栈起点 ② Multi-Paxos 允许日志不严格连续(空洞),适合分布式数据库的并发事务提交——多个事务可以在不同 index 位置并发写入,性能上限更高 ③ Google Spanner 也基于 Paxos——金融级数据库更信任已有大规模生产验证的 Paxos 系算法。这不是"Paxos 比 Raft 好",而是"已有基础设施和团队经验决定了技术路线"。

"Paxos 和 Raft 是等价的——都能实现分布式共识,Raft 更易懂。选型是历史的,原理是共通的——多数派确认 + Leader 协调 + 日志复制。理解了一个,另一个的核心思想也能看懂。"


四、分布式设计常用方案

设计方案关键点
分布式 ID雪花算法1bit + 41bit时间戳 + 10bit机器 + 12bit序列。时钟回拨→阻塞等待或使用 sequence 上限
幂等msgId 去重 + 状态机 + 唯一约束消息可重投,消费必须幂等
分布式锁Redisson 看门狗SET NX EX → Redisson 自动续期 → RedLock 争议大
分布式 SessionRedis 集中存储Spring Session + Redis,各节点无状态

雪花算法时钟回拨处理

时钟回拨是雪花算法的经典难题——无论 NTP 校时、虚拟机迁移还是手动调整,时钟都可能"跳回"过去的时间点,导致生成重复 ID。业界三种应对策略:

  1. 阻塞等待(美团 Leaf、默认雪花算法):若回拨时间较短(< 5ms),阻塞等待时钟追上回拨前的时间点后继续生成。简单有效,但不适用于较大回拨。
  2. 备用 workerId 位(百度 UidGenerator):RingBuffer 预先生成一批 ID 并缓存。若检测到时钟回拨,在原有 workerId 的备用位上补偿一个增量,生成不同的序列起点。避免了对外部时钟的强依赖。
  3. 抛异常拒绝服务:若时钟回拨超过阈值(如 100ms),直接拒绝生成 ID,等待人工介入。适用于对 ID 重复零容忍的强一致性场景。

"生产环境的雪花算法实现不能忽视时钟回拨——它不会每天发生,但发生一次且未处理,造成的 ID 重复就是数据事故。"


五、从理论到实践:三个经典系统设计推演

以下三个经典系统设计题将本节所讲的分布式 ID 生成、Redis 数据结构、MQ 削峰、幂等设计等知识点串联为完整方案,展现"原理→架构→代码"的完整链路。

5.1 短链系统(TinyURL)

需求澄清与数据量估算(BOTEC)
  • 功能:长 URL → 7 位短链;访问短链 → 302 重定向
  • 预估:每天 100 万新短链,读 QPS 约 10 万
  • 存储(5 年):18 亿条 × 131B/条 ≈ 236 GB
核心设计:发号器 + Base62

不要用 Hash(碰撞需处理)或 UUID(太长)。用 Snowflake 生成 64 位唯一 ID → 转 62 进制(0-9a-zA-Z)→ 7 位短码:

Snowflake ID: 6852435064793841664    Base62: 3dK3k9M
方案唯一性长度说明
发号器+Base62✅ 天然唯一7位只需存ID
MD5截取前7位❌ 碰撞需处理7位需额外重试逻辑
UUID截取22+位URL本身太长
存储与重定向链路
用户访问 http://short.cn/3dK3k9M
  → Nginx 负载均衡
  → 短链服务:
      ① Caffeine 本地缓存(热点短链,1万条,5min过期)
      ② 未命中 → Redis(短链→长URL)
      ③ 未命中 → MySQL(B+Tree索引在shortCode列)→ 回写Redis+Caffeine
      ④ 返回 302 Location: 长URL
问题方案
发号器单点Snowflake 天然分布式,各机器不同 workerId 独立发号
时钟回拨阻塞等待 < 备用位补偿 < 抛异常+人工介入
热点短链Caffeine + Redis 主从 + CDN 多级缓存
过期清理定时任务标记过期 → 归档冷存储
防恶意扫描Sentinel 令牌桶,单 IP 每秒最多 100 次

5.2 秒杀系统

秒杀与普通下单的核心差异:并发量从几千到几十万 QPS、流量从均匀分布变为瞬时峰值、库存竞争从低到极高。

漏斗模型四层架构
第一层:CDN + 静态化(99% 流量挡在这里)
  ├── 秒杀页面纯静态 HTML,CDN 缓存
  └── 秒杀按钮到时间后 JS 发请求

第二层:网关限流(Nginx / Gateway)
  ├── 令牌桶限流:单 IP 每秒 10 次
  └── 验证码/答题:分散请求(人机识别+手动减速)

第三层:应用层削峰(MQ 异步)
  ├── 请求直接发 MQ → 快速返回"排队中"
  └── 消费者逐条处理 → Redis 扣库存 → 成功则创建订单

第四层:数据库层(最终落地)
  ├── 库存扣减用 Redis Lua 脚本保证原子性
  └── 订单持久化到 MySQL(异步写入)
核心问题:怎么保证不超卖?
-- Redis Lua 脚本:原子扣减库存
local stock = tonumber(redis.call('get', KEYS[1]))
if stock == nil or stock <= 0 then return -1 end
if stock < tonumber(ARGV[1]) then return -1 end
redis.call('decrby', KEYS[1], ARGV[1])
return stock - tonumber(ARGV[1])

为什么用 Redis 而不是 MySQL? MySQL 单行更新的行级锁竞争上限约 1000-2000 TPS,秒杀场景下成为全局瓶颈。Redis 单线程模型天然免疫——单机 10 万+ QPS。

为什么不用 JVM 锁? 秒杀通常是多实例部署,synchronized/ReentrantLock 是进程级别锁,跨实例无效。Redis 是共享中间件,天然支持分布式。

防重复下单

Redis 分布式锁:SET order:lock:{userId}:{productId} NX EX 30——拿到锁才能进入下单流程。仅用于瞬时去重(秒杀窗口 30s),不是长期幂等方案。

秒杀结束后同步数据库

MQ 消费者创建订单 → 写入 MySQL。定时任务对账:Redis 总扣减数 vs MySQL 订单数,发现差异自动补单/退款。

5.3 实时排行榜系统

需求:100 万用户,实时更新积分,支持 Top 100 查询 + 查个人排名
存储选型
方案Top 100查个人排名更新积分
MySQL ORDER BY score DESC LIMIT 100100万行排序→慢全表扫描→慢单行UPDATE→快
Redis ZSet ZADD/ZREVRANGE/ZREVRANKO(log N+M)≈毫秒级O(log N)≈毫秒级O(log N)≈微秒级

果断选 Redis ZSet。跳表按 score 排序天然适合排行榜场景——取 Top 100 只需访问最高分段的最右侧 100 个节点。MySQL 即使给 score 加索引,ORDER BY score DESC LIMIT 100 仍需全索引扫描。

积分相同怎么排?

Redis ZSet score 相同时默认按 member 字典序。如需"先达到该分数的排前面",将 timestamp 编码到 score 的小数部分:

score = 积分 × 10^10 + (Long.MAX_VALUE - timestamp)
架构
用户完成交易 → MQ(积分变动事件)
  → 消费者:
      ① ZADD rank:daily 积分 userId(每日榜)
      ② ZINCRBY rank:weekly 积分 userId(每周榜累加)
      ③ ZINCRBY rank:monthly 积分 userId(月度榜累加)
  → 定时任务(每天凌晨):
      ④ 每日榜数据归档 MySQL → 清空每日榜

查询接口:
  - Top 100: ZREVRANGE rank:daily 0 99 WITHSCORES
  - 个人排名: ZREVRANK rank:daily userId
  - 个人积分: ZSCORE rank:daily userId
Redis 故障数据丢失

AOF everysec + 每天定时 BGSAVE 快照。更稳妥:核心积分变更同时写 MySQL 和 Redis,Redis 故障后用 MySQL 重建排行榜。

三个设计题的共同思路:需求澄清→数据量估算(BOTEC)→核心算法选型→存储方案→架构分层→边界问题处理。核心价值不在于方案本身,而在于从约束条件一步步推导出方案的过程。


核心要点回顾

CAP 定理的工程实践结论:P(分区容忍性)必须选择——网络分区是物理现实而非小概率事件;剩下在 C(一致性)和 A(可用性)之间根据业务语义权衡。金融核心链路选 CP——宁可短暂不可用也不能数据不一致;非核心链路选 AP——最终一致即可。同一系统内不同服务可以有不同的 CAP 选择:支付系统中订单服务选 AP(继续接受订单返回"处理中")、账户服务选 CP(拒绝写入返回"系统繁忙")。PACELC 模型进一步细化:即使无网络分区,正常运行时仍存在延迟(L)与一致性(C)的取舍。

分布式事务按一致性强度递进:2PC 的致命缺陷是同步阻塞锁资源加协调者单点故障,金融系统基本不使用;TCC(Try-Confirm-Cancel)将事务控制权上移到业务层——Try 预留资源校验、Confirm 确认执行、Cancel 取消释放,性能最高但侵入最大;MQ 最终一致性基于 RocketMQ 事务消息(half 消息→本地事务→commit/rollback→回查兜底),适用于非核心链路;Seata AT 模式侵入最小(@GlobalTransactional 注解),代价是隔离性较弱。核心交易链路用 TCC,非核心链路用 MQ 最终一致加 Seata AT。

Raft 共识拆解为三个子问题。Leader 选举:随机化超时时间(150-300ms)避免 Split Vote 活锁,Candidate 的日志必须不比投票者旧,每个 term 内每节点最多投一票。日志复制:AppendEntries RPC 通过 prevLogIndex 和 prevLogTerm 进行一致性检查——不匹配则 Leader 逐条回退直到找到一致点后覆盖写入。Raft 比 Paxos 易懂且功能等价,但 OceanBase 因起步早(Raft 论文 2013 年才发表)和 Multi-Paxos 允许日志不严格连续(适合分布式数据库的并发事务提交)而选择了 Paxos 系。

三大系统设计的共同思路是需求澄清→数据量估算(BOTEC)→核心算法选型→存储方案→架构分层→边界处理。短链系统:Snowflake 生成唯一 ID→Base62 转 7 位短码→Caffeine+Redis+MySQL 三级缓存→302 重定向。秒杀系统:CDN 静态化挡 99% 流量→网关令牌桶限流→MQ 削峰异步→Redis Lua 脚本原子扣库存(单线程模型免疫锁竞争)→异步写 MySQL→定时对账。排行榜系统:Redis ZSet 的 skiplist O(log N) 天然适合排名场景,积分相同时将 timestamp 编码到 score 小数部分实现"先达到排前面"。


能最终一致就别强一致——省下的不只是性能,还有开发成本和运维复杂度。收藏这张四方案决策树,下次架构评审直接翻出来对照。

下一篇:《计算机网络基础:TCP三次握手为什么不是两次?TLS 1.3怎么做到1-RTT?》 系列合集掘金Java合集