分布式系统:CAP、ID 与分库分表

讲清 CAP/BASE 的真实含义、分布式 ID 与幂等设计,并梳理分库分表、一致性哈希和高可用的工程权衡

分布式题很容易变成名词接龙。建议抓住一条判断标准:在故障和并发下,系统愿意牺牲什么、又能保证到什么程度。下面每个问题都尽量落到这个取舍上。

1. CAP 和 BASE 到底在说什么?

C 是一致性,通常指线性一致:读到的必须是最新已完成的写;A 是可用性,每个请求都能在有限时间内得到非错误响应;P 是分区容错,网络分区时系统仍能继续工作。

关键点是:网络分区不是「要不要容忍」,而是迟早会发生。所以真实选择是「分区发生时保 C 还是保 A」,而不是平时三选二。很多回答说「我们选了 CA」,在跨机房部署下基本不成立。

BASE 是对 AP 的工程化补充:基本可用、软状态、最终一致。它不是放弃正确性,而是把一致性从「时刻」放宽到「一段时间内」。

还有一个常被追问的 PACELC:分区时在 C 和 A 之间选,没有分区时在延迟和一致性之间选。多数系统平时也要为一致性付出延迟。

2. 分布式 ID 有哪些方案?雪花算法的坑在哪?

UUID 本地生成、无依赖,但无序且较长,做主键会造成 B+ 树页分裂和索引膨胀。数据库自增简单可靠,但单点、扩展难,分库后需要步长或号段。

雪花算法生成 64 位 ID:1 位符号、41 位毫秒时间、10 位机器标识、12 位序列。趋势递增、性能高,但强依赖时钟,回拨可能产生重复 ID;机器标识的分配也需要统一管理。

常见的回拨处理:小幅回拨等待追平,大幅回拨直接报错或切换备用 workerId;也可以用「上次时间戳 + 序列」的本地状态兜底。

号段模式(如 Leaf)从数据库批量取一段 ID 在内存分配,减少数据库压力,但要处理号段耗尽时的预取和容灾。最终选型看是否需要趋势递增、能否接受中心依赖,以及前端是否会被 JS 精度截断。

3. 如何设计一个幂等接口?

幂等的本质是「同一个业务意图执行多次,结果与执行一次相同」。先确定幂等键:客户端生成的请求号、业务单号,或「用户 + 操作 + 目标」的组合。

常见实现:数据库唯一索引(最可靠,但要注意捕获重复键异常);去重表或 Redis SET NX 记录已处理键(要设置合理 TTL);状态机限制非法迁移;乐观锁版本号;Token 机制(先领 token,提交时消费)。

还要处理并发窗口:两个相同请求同时到达,都查到「未处理」,然后都执行。唯一约束或分布式锁能把窗口关掉,单纯「先查再写」不行。

设计时明确三点:幂等键由谁生成、保存多久、重复请求返回什么(成功结果、进行中还是冲突)。这些约定比具体技术更重要。

4. 分布式锁有哪些实现?怎么选?

Redis 锁性能高、实现简单,SET NX PX 加 Lua 释放,配合续期;但主从异步复制下存在锁丢失窗口,适合防重复而非强互斥。

ZooKeeper 用临时顺序节点:创建最小序号节点者获得锁,其他节点监听前一个节点。会话断开后临时节点自动删除,可靠性较好;缺点是性能低于 Redis,且依赖 ZK 集群。

数据库锁可以用唯一索引或 SELECT ... FOR UPDATE,实现最简单,但性能和扩展性最差,适合低频、强一致的场景。

选择时看失败代价:重复发一条通知,Redis 锁足够;扣减余额,应该用数据库行锁、乐观锁或共识系统,并配合 fencing token 防止旧持有者继续写。

5. 为什么要分库分表?会带来哪些问题?

单表数据量大时,索引变深、DDL 变慢、备份恢复困难;单库连接数和写入能力也有上限。分表解决数据量,分库解决连接数和写扩展。

拆分方式有垂直和水平:垂直按业务或字段拆,边界清晰但可能留下大表;水平按分片键拆,扩展性好,但跨片查询变难。

问题清单要能说出来:跨库 join、跨片分页和聚合、分布式事务、全局唯一 ID、跨片唯一约束、路由规则变更、数据迁移与扩容、热点分片。

工程上通常配合中间件(如 ShardingSphere)做路由和归并,但中间件不能消灭跨片成本。设计分片键时优先选查询必带、分布均匀的字段;如果业务必须按两个维度查,考虑冗余表或搜索引擎。

6. 一致性哈希解决了什么问题?

普通取模 hash(key) % N 在节点数变化时几乎全部 key 都要重新映射,缓存会大面积失效。一致性哈希把节点和 key 映射到同一个环上,key 顺时针找到第一个节点,增删节点只影响相邻区间。

为了解决节点分布不均,引入了虚拟节点:每个物理节点在环上放置多个副本,key 的分布更均匀,节点扩容时迁移也更平滑。

它仍有两个边界:一是热点 key 不会因为哈希而分散,需要额外拆分;二是节点性能不同,等权虚拟节点可能造成负载不均,要按权重调整。

一致性哈希常用于缓存和对象存储,数据库分片更常用范围或哈希取模,因为要兼顾范围查询和运维可控性。

7. ZooKeeper 的典型用法和原理?

ZooKeeper 是一个高可用的协调服务,数据组织成树形 znode,支持持久节点、临时节点和顺序节点。它通过 ZAB 协议在 leader 和 follower 之间复制日志,写请求由 leader 处理,多数派确认后提交。

临时节点随会话消失,因此常用于服务注册和故障感知;顺序节点配合 watch 可以实现公平锁;节点数据小、读多写少,适合存元数据和配置,不适合存大对象或高频写。

watch 是一次性的:触发后需要重新注册,客户端要处理「注册前发生变化」的窗口。会话超时时间决定了故障感知的快慢,设得太短容易误判,太长则恢复慢。

同类系统 etcd 基于 Raft,提供租约、MVCC 和更现代的 API;选型时看生态和团队经验,不要只看「谁更快」。

8. 系统高可用要做哪些事?

先定义目标:RTO(多久恢复)和 RPO(能丢多少数据),不同业务差异很大。然后从冗余、故障转移、隔离和演练四方面设计。

冗余包括多实例、多可用区、多副本;故障转移要有健康检查、选主和流量摘除,并防止脑裂——常用多数派、租约和 fencing 机制。隔离包括线程池、连接池、机房和租户隔离,避免一个依赖拖垮全局。

降级和限流是「保命」手段:非核心功能可关闭,核心链路保留最小依赖。所有降级开关要提前演练,不能等到故障时才发现配置不生效。

最后是演练和演练后的修复:混沌工程、故障注入、备份恢复演练。没有演练过的高可用方案,通常只是文档上的高可用。

易错点与自测

  • CAP 不是「三选二」,分区发生时才是真正的取舍点;BASE 是最终一致,不是「不一致也行」。
  • 雪花 ID 的趋势递增依赖时钟,回拨处理必须明确。
  • 分库分表能解决容量,但会把查询和事务复杂度推到应用层。

自测:① 解释为什么 UUID 不适合做 InnoDB 主键;② 说明一致性哈希中虚拟节点解决什么问题;③ 给出一个「并发重复请求」的幂等落地方案。

参考资料

评论

为保障内容质量,评论需站长审核通过后才会展示;提交后没立刻看到属正常,请勿重复提交。

还没有评论,来抢沙发~