演进全景
让我们回顾小明的二手书平台从诞生到成熟的完整演进路径:
第一阶段:单机时代
└── 问题:并发修改导致数据错乱
└── 方案:数据库事务、隔离级别、乐观锁/悲观锁
第二阶段:读写分离
└── 问题:从库延迟导致读取过期数据
└── 方案:强制读主、时间窗口、会话级路由、同步复制
第三阶段:引入缓存
└── 问题:数据库与缓存数据不一致
└── 方案:Cache-Aside、延迟双删、CDC、LISTEN/NOTIFY
第四阶段:数据分片
└── 问题:跨分片查询和事务
└── 方案:并行查询、全局索引、CQRS、Saga、消息驱动
第五阶段:服务拆分
└── 问题:跨服务操作无法保证原子性
└── 方案:TCC、Saga、本地消息表、AT 模式
第六阶段:消息驱动
└── 问题:消息丢失、重复、乱序
└── 方案:本地消息表、幂等消费、顺序保证、死信队列
每一个阶段都是为了解决上一阶段的瓶颈,但同时也引入了新的一致性挑战。没有银弹,只有权衡。
一致性问题分类
按数据存储分类
| 类型 | 问题描述 | 典型场景 | 章节 |
|---|---|---|---|
| 单存储并发 | 多个请求同时修改同一数据 | 库存扣减、余额变更 | 第一章 |
| 主从复制延迟 | 从库数据落后于主库 | 读写分离架构 | 第二章 |
| 多存储不一致 | 数据库与缓存数据不同步 | 缓存架构 | 第三章 |
| 跨分片一致性 | 分片间数据操作不一致 | 分库分表架构 | 第四章 |
| 跨服务事务 | 多服务操作无法原子提交 | 微服务架构 | 第五章 |
| 消息可靠性 | 消息丢失/重复/乱序 | 事件驱动架构 | 第六章 |
按一致性强度分类
强一致性 最终一致性
│ │
▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐
│ 单机事务 │ │ 同步复制 │ │ TCC │ │ Saga │ │ 消息驱动 │
└─────────┘ └─────────┘ └─────────┘ └─────────┘ └─────────┘
│ │ │ │ │
性能最低 性能较低 性能中等 性能较高 性能最高
解决方案速查表
单机并发问题
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 数据库事务 | ACID 保证 | 简单可靠 | 单机限制 | 单数据库场景 |
| 悲观锁 | SELECT FOR UPDATE | 强一致 | 阻塞等待 | 冲突频繁 |
| 乐观锁 | 版本号检查 | 无阻塞 | 需要重试 | 冲突较少 |
| SERIALIZABLE | 最高隔离级别 | 无并发问题 | 性能差 | 金融核心 |
读写分离一致性
| 方案 | 原理 | 延迟容忍 | 性能影响 | 适用场景 |
|---|---|---|---|---|
| 强制读主 | 关键操作读主库 | 零延迟 | 高 | 余额查询 |
| 时间窗口 | 写后短时间读主 | 秒级 | 中 | 普通业务 |
| 会话级路由 | 同会话读主 | 会话内零延迟 | 中 | 用户体验敏感 |
| 同步复制 | 等待从库确认 | 零延迟 | 高 | 强一致要求 |
| 业务容忍 | 接受延迟 | 秒级 | 无 | 非关键数据 |
缓存一致性
| 方案 | 原理 | 一致性强度 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| Cache-Aside | 先更DB再删缓存 | 最终一致 | 低 | 通用场景 |
| 延迟双删 | 二次删除 | 最终一致(更强) | 中 | 高一致要求 |
| Write-Through | 同时写DB和缓存 | 最终一致 | 中 | 缓存托管 |
| Write-Behind | 异步批量写DB | 弱一致 | 高 | 高写入场景 |
| CDC | 监听DB变更 | 最终一致 | 高 | 无侵入需求 |
| LISTEN/NOTIFY | PG 通知机制 | 最终一致 | 低 | PostgreSQL |
跨服务事务
| 方案 | 原理 | 隔离性 | 性能 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|---|
| TCC | 预留-确认-取消 | 好 | 中 | 高 | 短事务、金融 |
| Saga | 正向+补偿 | 差 | 高 | 中 | 长事务 |
| 本地消息表 | 事务写消息 | 差 | 高 | 低 | 异步场景 |
| AT 模式 | 自动undo log | 中 | 低 | 高 | 低侵入需求 |
消息可靠性
| 问题 | 方案 | 原理 |
|---|---|---|
| 消息丢失 | 本地消息表 | 事务保证写入 |
| 消息丢失 | 事务消息 | 半消息机制 |
| 消息重复 | 幂等消费 | 去重+业务幂等 |
| 消息乱序 | 单分区 | 同 key 同分区 |
| 消息乱序 | 序列号检查 | 消费端校验 |
| 消费失败 | 死信队列 | 隔离问题消息 |
跨分片一致性
| 问题 | 方案 | 一致性 | 性能 |
|---|---|---|---|
| 跨分片查询 | 并行查询 | 强一致 | 差 |
| 跨分片查询 | 全局索引 | 最终一致 | 中 |
| 跨分片查询 | CQRS | 最终一致 | 好 |
| 跨分片事务 | 2PC | 强一致 | 差 |
| 跨分片事务 | Saga | 最终一致 | 好 |
| 跨分片事务 | 消息驱动 | 最终一致 | 好 |
选型决策树
总体决策流程
遇到一致性问题
│
├─ 1. 先问:真的需要强一致吗?
│ ├─ 金融、交易、核心业务 → 需要
│ └─ 统计、展示、非核心 → 最终一致即可
│
├─ 2. 再问:能简化架构吗?
│ ├─ 能不拆服务就不拆
│ ├─ 能不分库就不分
│ └─ 能单机解决就单机
│
└─ 3. 最后:选择合适的方案
└─ 见下方详细决策树
详细决策树
┌─────────────────────────────────────────────────────────────┐
│ 一致性问题决策树 │
└─────────────────────────────────────────────────────────────┘
问题类型是什么?
│
├─ 单库并发问题
│ ├─ 冲突频率高? → 悲观锁 (SELECT FOR UPDATE)
│ ├─ 冲突频率低? → 乐观锁 (版本号)
│ └─ 极端要求? → SERIALIZABLE 隔离级别
│
├─ 读写分离延迟
│ ├─ 能容忍秒级延迟? → 业务容忍
│ ├─ 需要写后即读一致? → 时间窗口读主
│ ├─ 整个会话需要一致? → 会话级路由
│ └─ 绝对不能延迟? → 同步复制(慎用)
│
├─ 缓存不一致
│ ├─ 通用场景? → Cache-Aside
│ ├─ 需要更强一致? → 延迟双删
│ ├─ 不能改代码? → CDC
│ └─ 用 PostgreSQL? → LISTEN/NOTIFY
│
├─ 跨服务事务
│ ├─ 需要资源隔离? → TCC
│ ├─ 是长事务? → Saga
│ ├─ 可以异步? → 本地消息表
│ └─ 要低侵入? → AT 模式(如 Seata)
│
├─ 消息可靠性
│ ├─ 担心丢失? → 本地消息表 + At-Least-Once
│ ├─ 担心重复? → 幂等消费
│ ├─ 担心乱序? → 单分区 + 序列号
│ └─ 担心堆积? → 死信队列 + 告警
│
└─ 跨分片问题
├─ 查询问题
│ ├─ 查询少? → 并行查询所有分片
│ ├─ 查询复杂? → CQRS + 查询库
│ └─ 需要实时? → 全局索引
└─ 事务问题
├─ 能避免跨分片? → 调整分片策略
├─ 必须强一致? → 2PC(慎用)
└─ 可最终一致? → Saga 或消息驱动
常见误区
误区一:追求强一致性
很多时候,最终一致性就够了。强一致性的代价是:
- 性能下降
- 可用性降低
- 实现复杂
建议:先问业务能否容忍短暂不一致,再选方案。
误区二:过早分布式
分布式是为了解决规模问题,不是为了炫技。
错误路径:
单机 → 微服务 → 分库分表 → 问题缠身
正确路径:
单机 → 优化单机 → 读写分离 → 必要时才拆服务 → 必要时才分库
建议:能单机解决的,不要分布式。
误区三:忽视幂等性
分布式系统中,任何操作都可能被执行多次:
- 网络重试
- 消息重复
- 用户重复点击
建议:设计 API 时就考虑幂等,而不是事后补救。
误区四:缺少兜底机制
再完美的系统也会出问题,需要考虑:
- 补偿失败怎么办?
- 消息消费失败怎么办?
- 分布式事务超时怎么办?
建议:
- 记录完整的操作日志
- 设计死信队列和告警
- 提供人工介入入口
实战检查清单
上线前检查
单机并发:
- 关键操作有事务保护
- 并发修改有锁机制
- 隔离级别设置合理
读写分离:
- 关键读操作走主库
- 有延迟监控告警
- 有降级方案(主库承载所有)
缓存:
- 缓存有过期时间
- 更新操作删除缓存
- 缓存失效有重试
跨服务事务:
- 每个操作都有补偿
- 补偿操作幂等
- 有事务状态记录
消息:
- 生产端可靠发送
- 消费端幂等处理
- 有死信队列
- 有消费监控
分片:
- 分片键选择合理
- 跨分片查询有方案
- 扩容方案已规划
故障排查检查清单
数据不一致时:
- 检查事务边界是否正确
- 检查缓存是否已失效
- 检查消息是否已消费
- 检查补偿是否执行成功
- 检查是否有并发竞争
性能下降时:
- 检查锁等待时间
- 检查跨分片查询数量
- 检查消息积压情况
- 检查同步复制延迟
技术栈推荐
Rust 生态
| 需求 | 推荐库 |
|---|---|
| PostgreSQL | sqlx |
| Redis | redis-rs |
| Kafka | rdkafka |
| 序列化 | serde + serde_json |
| 异步运行时 | tokio |
| HTTP 客户端 | reqwest |
| 日志 | tracing |
基础设施
| 需求 | 推荐方案 |
|---|---|
| 数据库 | PostgreSQL |
| 缓存 | Redis |
| 消息队列 | Kafka / RabbitMQ |
| CDC | Debezium |
| 监控 | Prometheus + Grafana |
| 链路追踪 | Jaeger |
写在最后
分布式一致性没有完美方案,只有适合的方案。
小明从一个简单的二手书平台开始,随着业务增长,一步步解决了各种一致性问题:
- 单机时代:学会了用事务和锁
- 读写分离:学会了处理主从延迟
- 引入缓存:学会了多存储同步
- 数据分片:学会了跨分片一致性
- 服务拆分:学会了分布式事务
- 消息驱动:学会了可靠消息
这个过程中,小明总结出几条原则:
- 简单优先:能简单解决的,不要复杂化
- 权衡取舍:没有银弹,只有权衡
- 做好兜底:考虑失败场景和人工介入
- 持续演进:架构随业务演进,不要过度设计
希望这个系列对你有所帮助。当你遇到一致性问题时,可以回来翻翻这份指南,找到适合你场景的方案。
记住:最好的架构不是最复杂的,而是刚好能解决问题的。
上一篇:消息驱动:最终一致性
本系列:
感谢阅读!