分布式系统设计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 的选择本质上是这个服务在故障时伤害哪一端的决策。PACELCCAP 的细化模型CAP 的局限性在于它只考虑了发生网络分区时的取舍——但网络分区并非常态。PACELC 将场景拆分为两类场景含义权衡P(Partition)发生网络分区时tradeAvsC同 CAPE(Else)无分区、正常运行中tradeL(Latency延迟) vsC(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 做跨服务事务——性能代价过高单点故障风险不可接受。TCCTry-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进程 crashMQ 定期回查上游事务状态。生产者 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 节点集群——ALeaderterm1、BFollowerterm1、CFollowerterm1。选举超时时间随机化为 150-300ms 区间。时刻 T₀正常运行 A --[心跳 50ms/次]-- B A --[心跳 50ms/次]-- C B 和 C 每次收到心跳重置自己的选举超时计时器 时刻 T₁A 宕机 心跳停止。B 和 C 的选举超时计时器开始倒计时 时刻 T₂B 的选举超时触发B 随机到了 180msC 的计时器还剩 40ms B 的角色Follower → Candidate B 的 term1 → 2 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)→ 否相等 - 相等时比较 lastLogIndexB (10) C (10)→ 是 → 日志足够新合格 C 回复{term: 2, voteGranted: true} C 重置选举超时计时器因为收到了更大 term 的 RPC 时刻 T₄B 收到 C 的投票 B 的投票数1自己 1C 2 2 3/2 → 超过半数 → B 当选为 term 2 的 Leader 时刻 T₅B 开始发送心跳 B --[心跳term2]-- C C 收到心跳确认 B 为 Leader C 重置选举超时计时器竞争选举Split Vote若 B 和 C 几乎同时超时两者都变为 Candidate各自投票给自己各得 1 票——均未超过半数。两个 Candidate 各自进入下一轮随机超时等待先超时者赢得下一轮选举。随机化超时时间是 Raft 选举机制避免活锁的关键设计。选举关键规则① 一个 term 内每个节点最多投一票② Candidate 的日志必须不比投票者旧先比较 lastLogTermterm 相同再比较 lastLogIndex③ 收到 term 大于自身 term 的任何 RPC → 立即转为 Follower更新自身 term。② 日志复制10 步完整时序Raft 的日志复制是多数派确认机制最核心的体现前提条件B 是 term 2 的 LeaderA 已宕机C 是 Follower Leader B 的日志[idx1,t1] [idx2,t1] [idx3,t1] committedIndex3 Follower C 的日志[idx1,t1] [idx2,t1] [idx3,t1] committedIndex3 Step 1: 客户端向 Leader B 发送写请求 → SET X 5 Step 2: Leader B 将命令追加到本地日志 B 的新日志条目{index: 4, term: 2, command: SET X5} 尚未提交——committedIndex 仍为 3 Step 3: Leader B 向所有 FollowerC 和 A并行发送 AppendEntries RPC 内容{term: 2, leaderId: B, prevLogIndex: 3, prevLogTerm: 1, ← 用于一致性检查 entries: [{index: 4, term: 2, command: SET X5}], leaderCommit: 3} Step 4: Follower C 收到 AppendEntries执行一致性检查 C 检查自己的日志在 index3 处的 term 是否为 1prevLogTerm → 是 → 一致性检查通过 → C 将 entry {index:4, term:2, cmd:SET X5} 追加到自己的日志 → 回复{term: 2, success: true} Step 5: Follower A 已宕机无回复Leader B 会持续重试 AppendEntries Step 6: Leader B 收到 C 的确认 现在拥有 entry {index:4, term:2} 的节点BLeader CFollower 2 个 2 3/2 → 超过半数 → 可以提交 Step 7: Leader B 提交 entry index4 B 将 entry 应用到状态机X 5 B 更新 committedIndex 4 Step 8: Leader B 返回写入成功给客户端 客户端得到响应——此时 Follower C 尚未提交但不影响正确性 Step 9: 下一次心跳中Leader B 通知 committedIndex4 Follower C 收到心跳发现 committedIndex4 自己的 committedIndex3 → C 将 entry index4 应用到状态机X 5 → C 更新 committedIndex 4 Step 10: 完成。3 个节点中有 2 个持久化了 entry index4 后续 A 恢复后Leader B 会通过 AppendEntries 补齐 A 缺失的日志日志复制的核心一致性检查。AppendEntries 中的prevLogIndex和prevLogTerm是两个关键的校验字段——Follower 会检查自己日志在prevLogIndex位置的 term 是否等于prevLogTerm。若不等说明 Follower 的日志与 Leader 在某个位置出现了分叉Leader 会递减prevLogIndex逐条回退直到找到一个一致性交汇点后开始覆盖写入。这个简单的设计保证了一个重要属性如果两个节点的日志在同一个 index 上有相同的 term那么它们在这个 index 之前的所有条目完全一致。③ 安全性选举限制Candidate 的日志必须至少和投票者同样新——先比较 lastLogTermterm 相同再比较 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。业界三种应对策略阻塞等待美团 Leaf、默认雪花算法若回拨时间较短 5ms阻塞等待时钟追上回拨前的时间点后继续生成。简单有效但不适用于较大回拨。备用 workerId 位百度 UidGeneratorRingBuffer 预先生成一批 ID 并缓存。若检测到时钟回拨在原有 workerId 的备用位上补偿一个增量生成不同的序列起点。避免了对外部时钟的强依赖。抛异常拒绝服务若时钟回拨超过阈值如 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位只需存IDMD5截取前7位❌ 碰撞需处理7位需额外重试逻辑UUID截取✅22位URL本身太长存储与重定向链路用户访问 http://short.cn/3dK3k9M → Nginx 负载均衡 → 短链服务 ① Caffeine 本地缓存热点短链1万条5min过期 ② 未命中 → Redis短链→长URL ③ 未命中 → MySQLBTree索引在shortCode列→ 回写RedisCaffeine ④ 返回 302 Location: 长URL问题方案发号器单点Snowflake 天然分布式各机器不同 workerId 独立发号时钟回拨阻塞等待 备用位补偿 抛异常人工介入热点短链Caffeine Redis 主从 CDN 多级缓存过期清理定时任务标记过期 → 归档冷存储防恶意扫描Sentinel 令牌桶单 IP 每秒最多 100 次5.2 秒杀系统秒杀与普通下单的核心差异并发量从几千到几十万 QPS、流量从均匀分布变为瞬时峰值、库存竞争从低到极高。漏斗模型四层架构第一层CDN 静态化99% 流量挡在这里 ├── 秒杀页面纯静态 HTMLCDN 缓存 └── 秒杀按钮到时间后 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 而不是 MySQLMySQL 单行更新的行级锁竞争上限约 1000-2000 TPS秒杀场景下成为全局瓶颈。Redis 单线程模型天然免疫——单机 10 万 QPS。为什么不用 JVM 锁秒杀通常是多实例部署synchronized/ReentrantLock 是进程级别锁跨实例无效。Redis 是共享中间件天然支持分布式。防重复下单
交易跨10个库、日均千万订单——选错一次分布式事务方案,加班三个月重写
分布式系统设计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 的选择本质上是这个服务在故障时伤害哪一端的决策。PACELCCAP 的细化模型CAP 的局限性在于它只考虑了发生网络分区时的取舍——但网络分区并非常态。PACELC 将场景拆分为两类场景含义权衡P(Partition)发生网络分区时tradeAvsC同 CAPE(Else)无分区、正常运行中tradeL(Latency延迟) vsC(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 做跨服务事务——性能代价过高单点故障风险不可接受。TCCTry-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进程 crashMQ 定期回查上游事务状态。生产者 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 节点集群——ALeaderterm1、BFollowerterm1、CFollowerterm1。选举超时时间随机化为 150-300ms 区间。时刻 T₀正常运行 A --[心跳 50ms/次]-- B A --[心跳 50ms/次]-- C B 和 C 每次收到心跳重置自己的选举超时计时器 时刻 T₁A 宕机 心跳停止。B 和 C 的选举超时计时器开始倒计时 时刻 T₂B 的选举超时触发B 随机到了 180msC 的计时器还剩 40ms B 的角色Follower → Candidate B 的 term1 → 2 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)→ 否相等 - 相等时比较 lastLogIndexB (10) C (10)→ 是 → 日志足够新合格 C 回复{term: 2, voteGranted: true} C 重置选举超时计时器因为收到了更大 term 的 RPC 时刻 T₄B 收到 C 的投票 B 的投票数1自己 1C 2 2 3/2 → 超过半数 → B 当选为 term 2 的 Leader 时刻 T₅B 开始发送心跳 B --[心跳term2]-- C C 收到心跳确认 B 为 Leader C 重置选举超时计时器竞争选举Split Vote若 B 和 C 几乎同时超时两者都变为 Candidate各自投票给自己各得 1 票——均未超过半数。两个 Candidate 各自进入下一轮随机超时等待先超时者赢得下一轮选举。随机化超时时间是 Raft 选举机制避免活锁的关键设计。选举关键规则① 一个 term 内每个节点最多投一票② Candidate 的日志必须不比投票者旧先比较 lastLogTermterm 相同再比较 lastLogIndex③ 收到 term 大于自身 term 的任何 RPC → 立即转为 Follower更新自身 term。② 日志复制10 步完整时序Raft 的日志复制是多数派确认机制最核心的体现前提条件B 是 term 2 的 LeaderA 已宕机C 是 Follower Leader B 的日志[idx1,t1] [idx2,t1] [idx3,t1] committedIndex3 Follower C 的日志[idx1,t1] [idx2,t1] [idx3,t1] committedIndex3 Step 1: 客户端向 Leader B 发送写请求 → SET X 5 Step 2: Leader B 将命令追加到本地日志 B 的新日志条目{index: 4, term: 2, command: SET X5} 尚未提交——committedIndex 仍为 3 Step 3: Leader B 向所有 FollowerC 和 A并行发送 AppendEntries RPC 内容{term: 2, leaderId: B, prevLogIndex: 3, prevLogTerm: 1, ← 用于一致性检查 entries: [{index: 4, term: 2, command: SET X5}], leaderCommit: 3} Step 4: Follower C 收到 AppendEntries执行一致性检查 C 检查自己的日志在 index3 处的 term 是否为 1prevLogTerm → 是 → 一致性检查通过 → C 将 entry {index:4, term:2, cmd:SET X5} 追加到自己的日志 → 回复{term: 2, success: true} Step 5: Follower A 已宕机无回复Leader B 会持续重试 AppendEntries Step 6: Leader B 收到 C 的确认 现在拥有 entry {index:4, term:2} 的节点BLeader CFollower 2 个 2 3/2 → 超过半数 → 可以提交 Step 7: Leader B 提交 entry index4 B 将 entry 应用到状态机X 5 B 更新 committedIndex 4 Step 8: Leader B 返回写入成功给客户端 客户端得到响应——此时 Follower C 尚未提交但不影响正确性 Step 9: 下一次心跳中Leader B 通知 committedIndex4 Follower C 收到心跳发现 committedIndex4 自己的 committedIndex3 → C 将 entry index4 应用到状态机X 5 → C 更新 committedIndex 4 Step 10: 完成。3 个节点中有 2 个持久化了 entry index4 后续 A 恢复后Leader B 会通过 AppendEntries 补齐 A 缺失的日志日志复制的核心一致性检查。AppendEntries 中的prevLogIndex和prevLogTerm是两个关键的校验字段——Follower 会检查自己日志在prevLogIndex位置的 term 是否等于prevLogTerm。若不等说明 Follower 的日志与 Leader 在某个位置出现了分叉Leader 会递减prevLogIndex逐条回退直到找到一个一致性交汇点后开始覆盖写入。这个简单的设计保证了一个重要属性如果两个节点的日志在同一个 index 上有相同的 term那么它们在这个 index 之前的所有条目完全一致。③ 安全性选举限制Candidate 的日志必须至少和投票者同样新——先比较 lastLogTermterm 相同再比较 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。业界三种应对策略阻塞等待美团 Leaf、默认雪花算法若回拨时间较短 5ms阻塞等待时钟追上回拨前的时间点后继续生成。简单有效但不适用于较大回拨。备用 workerId 位百度 UidGeneratorRingBuffer 预先生成一批 ID 并缓存。若检测到时钟回拨在原有 workerId 的备用位上补偿一个增量生成不同的序列起点。避免了对外部时钟的强依赖。抛异常拒绝服务若时钟回拨超过阈值如 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位只需存IDMD5截取前7位❌ 碰撞需处理7位需额外重试逻辑UUID截取✅22位URL本身太长存储与重定向链路用户访问 http://short.cn/3dK3k9M → Nginx 负载均衡 → 短链服务 ① Caffeine 本地缓存热点短链1万条5min过期 ② 未命中 → Redis短链→长URL ③ 未命中 → MySQLBTree索引在shortCode列→ 回写RedisCaffeine ④ 返回 302 Location: 长URL问题方案发号器单点Snowflake 天然分布式各机器不同 workerId 独立发号时钟回拨阻塞等待 备用位补偿 抛异常人工介入热点短链Caffeine Redis 主从 CDN 多级缓存过期清理定时任务标记过期 → 归档冷存储防恶意扫描Sentinel 令牌桶单 IP 每秒最多 100 次5.2 秒杀系统秒杀与普通下单的核心差异并发量从几千到几十万 QPS、流量从均匀分布变为瞬时峰值、库存竞争从低到极高。漏斗模型四层架构第一层CDN 静态化99% 流量挡在这里 ├── 秒杀页面纯静态 HTMLCDN 缓存 └── 秒杀按钮到时间后 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 而不是 MySQLMySQL 单行更新的行级锁竞争上限约 1000-2000 TPS秒杀场景下成为全局瓶颈。Redis 单线程模型天然免疫——单机 10 万 QPS。为什么不用 JVM 锁秒杀通常是多实例部署synchronized/ReentrantLock 是进程级别锁跨实例无效。Redis 是共享中间件天然支持分布式。防重复下单