一、现象或背景:分布式系统里的"一致性"有多难?
2024 年 11 月凌晨 03:12:47,我们监控到三条关键链路同时出现异常:
- Redis 缓存一致性:商品改价接口的缓存未及时更新,导致 17 分钟内价格错误率高达 2.3%;
- Kafka 消息顺序:自定义 Partitioner 导致 27 个分区空转,TPS 从 9.4 万暴跌至 1.6 万;
- 分布式锁失效:Sentinel 主从切换让 Redis 锁瞬间蒸发,引发 3 个核心服务的并发雪崩。
这背后,是分布式系统里最基础、最致命的挑战——如何在不完美世界里做到"一致性"。
1.1 一致性问题的规模与代价
- 金融系统:每秒处理数百万交易,单笔交易一致性延迟超过 100ms,可能导致百万级损失;
- 电商大促:库存扣减与缓存更新的时序错位,可能造成超卖或缓存穿透;
- 实时风控:事件流处理的顺序错乱,可能漏掉关键异常信号。
二、原理或根因分析:CAP 定理与算法选择
2.1 CAP 定理的现实困境
在分布式系统中,一致性(Consistency)、可用性(Availability)、分区容忍性(Partition Tolerance) 三者不可兼得。我们团队在 2023 年的《Kafka 3.6 vs RocketMQ 5.1 选型决策手册》中,通过 5 维对比发现:
| 维度 | Kafka 3.6 | RocketMQ 5.1 | |------|----------|-------------| | 单分区吞吐(1KB) | 12.7 万/s | 11.2 万/s | | 顺序消息能力 | 需自定义分区 | 原生支持 | | 事务消息 | 需配合 Kafka Transactional ID | 原生支持 | | 运维复杂度 | KRaft 模式需手动选主 | Dledger 自动选主 | | P99 延迟(10KB) | 15.2ms | 18.7ms |
最终选择 Kafka 3.6 是因为其在吞吐和生态上的优势,但代价是需要更精细的分区策略。
2.2 Raft 算法的实战选择
在需要强一致性的场景(如分布式锁、配置中心),我们采用 Raft 算法实现。Raft 通过以下机制保证一致性:
- Leader 选举:通过心跳机制选举 leader,确保集群始终有主;
- 日志复制:所有操作必须经过 leader 转发,确保日志一致性;
- 安全性保证:通过 term 和日志索引防止脑裂。
在《Kafka at-least-once 重复消费根因与幂等方案》一文中提到,Raft 的日志复制机制能有效避免消息重复,但需要配合消费端的幂等处理。
三、具体做法:从理论到落地
3.1 Raft 集群部署配置
使用 etcd 3.5.0 部署 Raft 集群,配置文件如下:
# etcd 配置文件 raft_config.yaml
name: member1
data_dir: /var/lib/etcd
wal_dir: /var/lib/etcd/wal
snap_dir: /var/lib/etcd/snapshot
heartbeat_interval: 100
election_timeout: 500
initial_cluster:
member1=http://10.10.1.10:2380
member2=http://10.10.1.11:2380
member3=http://10.10.1.12:2380
启动命令:
etcd --name member1 --data-dir /var/lib/etcd --config-file raft_config.yaml
3.2 分区策略优化
在《Kafka 生产者自定义 Partitioner 后 32 分区只有 5 个在干活》一文中,我们发现 key 基数太低会导致路由倾斜。解决方案是:
- 动态获取分区数:通过
ClusterMetadata获取真实分区数,避免硬编码; - 全 32 位取模:使用
(hash & 0x7FFFFFFF) % numPartitions替代位运算; - CI 预检:在 CI 中用真实 key 测试分区覆盖度。
3.3 时间同步保障
在《凌晨一次 NTP 时钟 step 了 8 秒,把分布式锁、JWT、Kafka 同时打穿》一文中,我们发现本地时钟不一致会导致严重问题。解决方案是:
- NTP 服务监控:定期检查
ntpq -p的reach字段,确保时钟同步正常; - 单调 token 作为兜底:在分布式锁、JWT 签发等场景,使用单调递增的 token 代替时间戳;
- 日志审计:在关键操作日志中记录
timestamp和sequenceId,用于事后追溯。
四、踩过的坑与反直觉之处
4.1 网络分区下的脑裂风险
在《Sentinel 主从切换让 Redis 锁瞬间蒸发》一文中,我们发现主从切换可能导致锁失效。反直觉之处在于:
- 主库 SHUTDOWN 时,Sentinel 会自动进行 failover,但此时 Redis 锁会丢失;
- 从库晋升为 master 后,原 master 的锁记录会被清除;
- 解决方案:在 Redis 锁加锁时,同时记录到数据库,用数据库事务保证最终一致性。
4.2 配置不当引发的性能问题
在《Redis 热 key 单分片被打到 98% CPU》一文中,我们发现单分片的热 key 会导致 CPU 高峰。反直觉之处在于:
- 本地缓存 + 一致性令牌 的组合比单纯扩容更有效;
- 限流兜底 可以防止击穿瞬间的洪峰;
- 监控指标 需要包含
key_hit_rate和key_miss_rate,及时发现热 key。
五、结论与可复用清单
5.1 选择一致性算法的关键因素
| 因素 | 优先级 | 说明 | |------|--------|------| | 业务场景 | ★★★★☆ | 金融系统优先强一致性,高并发场景可接受最终一致性 | | 系统规模 | ★★★★☆ | 小规模系统可选 Paxos,大规模系统推荐 Raft | | 性能要求 | ★★★☆☆ | Kafka 的吞吐优势 vs RocketMQ 的事务支持 | | 运维复杂度 | ★★☆☆☆ | KRaft 模式需手动选主,Dledger 自动选主更简单 | | 故障恢复 | ★★★★☆ | Raft 的日志复制机制能有效防止数据丢失 |
5.2 可复用清单
- 分区策略:动态获取分区数 + 全 32 位取模 + CI 预检;
- 时间同步:NTP 监控 + 单调 token + 日志审计;
- 锁保障:数据库事务 + 唯一性约束 + 重试机制;
- 热 key 处理:本地缓存 + 一致性令牌 + 限流兜底;
- 故障恢复:Raft 日志复制 + 数据库事务 + 监控告警。
5.3 开放问题引发讨论
- 在 Kafka 3.6 的 KRaft 模式下,选主延迟是否对业务 P99 有显著影响?(参考《Kafka 3.6 vs RocketMQ 5.1 选型决策手册》)
- 如何在不牺牲性能的前提下,实现 Redis 的最终一致性?(参考《Redis 热 key 单分片被打到 98% CPU》)
- 分布式锁失效后,如何快速恢复业务状态?(参考《Sentinel 主从切换让 Redis 锁瞬间蒸发》)