适用读者:要在一堆中间件里做选型的人;被告警追着跑、分不清"是缓存还是 DB 的锅"的人。
前置:S18-A05、S11(数据库与 SQL)、S12(Redis 与缓存)、S17(分布式)、S18(云原生基础见 S26)。
覆盖旧文:3109(第 7 章 分布式系统基础)、3110(第 8 章 负载均衡)、3111(第 9 章 缓存架构)、3112(第 10 章 消息队列)、3113(第 11 章 数据库架构)、3114(第 12 章 NoSQL)、3116(第 14 章 容器化)、3117(第 15 章 云计算)、3119(第 17 章 架构演进)、3121(第 19 章 安全,部分)→ 10 篇 / 约 5 万字符。
扩展点:E-S18-034(缓存三种模式的失败面:穿透/击穿/雪崩 + 一致性顺序)、E-S18-035(消息语义三选一与幂等落点)、E-S18-036(数据层四件套:读写分离、分库分表、NoSQL 选型、归档)。
上一篇:S18-A05 · 架构风格与质量属性:从单体到微服务的选择
下一篇:补编第 3 篇 · 行业架构案例讲评(待写)。
1. 一句话结论
中间件不是"加上就变强",每个都带来一种新的失败模式:缓存带来不一致、消息带来重复与乱序、分库分表带来跨片查询、容器带来状态与网络复杂度。选型 = 挑一种你能运维的失败模式。
出处:
A-3111(缓存)、A-3112(MQ)、A-3113/A-3114(存储)、A-3116(容器)。
2. 缓存三模式与四类故障(E-S18-034)
| 模式 | 读路径 | 写路径 | 一致性窗口 | 适用 |
|---|---|---|---|---|
| Cache-Aside(旁路,最常用) | 先读缓存,miss 读 DB 回填 | 先写 DB,再删除缓存 | 短暂(删除前的读可能是旧值) | 通用 |
| Read/Write-Through | 缓存层代理读写 | 写穿到 DB | 小 | 缓存组件支持时 |
| Write-Behind | 缓存为主 | 异步批量落库 | 大(可能丢) | 高写吞吐、可容忍丢失 |
四类故障与对策:
① 穿透:查询不存在的数据 → 缓存永不命中,压力全落 DB
→ 缓存空值(短 TTL)+ 参数校验 + 布隆过滤器
② 击穿:热点 key 过期瞬间大量请求打到 DB
→ 逻辑过期(不设 TTL,后台异步刷新)或 单飞(singleflight)
③ 雪崩:大量 key 同时过期 / 缓存整体不可用
→ TTL 加随机抖动 + 多级缓存 + 限流降级 + 集群高可用
④ 一致性:DB 与缓存双写顺序错 → 长期脏数据
→ 先写库再删缓存(不是更新缓存)+ 延迟双删 + 订阅 binlog 兜底
判据:"删除缓存"优于"更新缓存"——并发写时更新缓存容易写入旧值;删除让下次读自然回源。不接受"缓存和 DB 强一致"这个需求,它只能靠分布式锁把性能打回 DB 水平。
3. 消息队列:三种语义与幂等落点(E-S18-035)
| 语义 | 保证 | 代价 | 适用 |
|---|---|---|---|
| 至多一次(at-most-once) | 可能丢 | 最低 | 可丢的埋点/日志 |
| 至少一次(at-least-once,默认现实) | 不丢但可能重复 | 消费端必须幂等 | 绝大多数业务 |
| 恰好一次(exactly-once) | 理论不重不丢 | 事务消息/两阶段,性能与复杂度高 | 金融记账等强要求 |
五个必答问题(选 MQ 时):
① 顺序性:需要全局有序还是分区有序?(全局有序 = 单分区 = 放弃并行)
② 重复:消费端幂等键是什么?(业务单号 / 消息 ID + 去重表)
③ 积压:峰值积压多少?消费能力能否横向扩(分区数是上限)?
④ 可靠:需要事务消息吗?outbox 表 vs 事务消息谁更简单?
⑤ 运维:DLQ(死信)、重试策略、消息轨迹、监控谁来做?
可靠投递的标准组合(outbox 模式):
业务写库 + 写 outbox 表(同事务)→ 独立投递进程扫 outbox → 发送 MQ → 标记已发
消费端:幂等表去重 + 失败重试(指数退避)→ 超限进 DLQ → 人工/补偿任务
判据:"至少一次 + 幂等"是工程默认解,别为"恰好一次"付出事务消息的复杂度,除非对账成本更高。
4. 数据层四件套(E-S18-036)
| 手段 | 解决 | 引入的问题 | 触发线(经验值) |
|---|---|---|---|
| 读写分离 | 读多写少、报表压力 | 主从延迟(读到旧数据) | 读 QPS 明显高于写,且可容忍毫秒级延迟 |
| 分库分表 | 单表过大、写入瓶颈 | 跨片 join/分页/事务 | 单表 >2,000 万行 或 容量 >1TB 或 写 QPS >5k |
| NoSQL | 灵活 schema、超高吞吐、检索 | 一致性弱、运维与技能栈 | 文档型(Mongo)、检索(ES)、KV(Redis)、宽列(Cassandra/HBase) |
| 归档/分区 | 历史数据拖慢查询 | 查询要带时间条件 | 冷数据占比 >50% |
分片键三约束(与 S11-A04 一致):
① 高基数且分布均匀(避免热点)
② 绝大多数查询都带它(否则广播查询)
③ 不可变(变更分片键 = 数据搬迁)
NoSQL 选型的反例:把 ES 当主库(无事务、写放大、reindex 风险)、把 Redis 当唯一存储(持久化不等于可靠)、把 Mongo 用于强事务记账(可用但需事务与索引设计)。
5. 负载均衡与容器化
| 层 | 手段 | 特点 |
|---|---|---|
| DNS | GeoDNS / 加权 | 粗粒度、受 TTL 影响 |
| 四层(LVS/ELB) | 按 IP+端口转发 | 性能高、不懂业务 |
| 七层(nginx/Envoy/ALB) | 按路径/头/权重 | 灵活、可做灰度与限流 |
| 客户端(gRPC/Ribbon) | 服务发现 + 本地负载 | 少一跳,但需治理能力 |
算法取舍:轮询(均匀但忽略负载)、加权轮询(按容量)、最少连接(长连接友好)、一致性 Hash(缓存亲和,扩容需虚拟节点)、P2C(随机两个取较小负载,大规模下最实用)。
容器化的边界:
适合容器:无状态服务、批处理、CI 构建、可水平扩的 API
不适合(或需特殊处理):有状态数据库(用 Operator/托管服务)、
需要内核参数的场景、GPU 直通、强 IO 本地盘的中间件
判据:"容器化不是架构演进,是交付方式的改变"——它降低环境差异成本,但不解决领域边界、一致性与容量问题。
6. 架构演进的四个阶段(3119 的重写)
① 单体 + 单库(验证业务)
② 模块化单体 + 读写分离 + 缓存(扛住增长)
③ 按限界上下文拆服务 + 消息解耦(多团队)
④ 平台化:服务网格/统一可观测/多活(规模化)
每阶段的门槛(不满足就别跳):
| 跳到 | 门槛 |
|---|---|
| ② | 有慢查询监控与索引治理;压测脚本可复现 |
| ③ | 有 CI/CD 与回滚;有链路追踪;领域边界稳定 |
| ④ | 有多活容量预算;有 SRE 值班与故障演练机制 |
7. 反例:六种"中间件用错"
| 现象 | 根因 |
|---|---|
| 缓存与库长期不一致 | 更新缓存而非删除、无 binlog 兜底 |
| 消息重复导致重复扣款 | 消费端无幂等键 |
| 消费积压越积越多 | 分区数不足,无法横向扩 |
| 分库分表后报表跑不动 | 跨片聚合未走汇总表/数仓 |
| 容器里跑数据库频繁 OOM | 有状态服务未用 Operator/托管 |
| 微服务后偶发 504 | 缺超时/重试预算,重试风暴放大故障 |
8. 自测
- 缓存三模式与四类故障?为什么"删缓存"优于"更新缓存"?
- 消息三种语义?"至少一次"下幂等键怎么选?
- outbox 模式解决什么?
- 分片键三约束?分库分表的三个触发线?
- 容器化解决什么、不解决什么?
答案
- Cache-Aside/读写穿透/写回;穿透/击穿/雪崩/一致性;删除让下次读回源,避免并发写旧值。
- 至多/至少/恰好一次;业务单号或消息 ID + 去重表。
- 业务写库与消息发送的原子性(本地事务 + 独立投递)。
- 高基数均匀、查询必带、不可变;2,000 万行/1TB/写 5k。
- 解决环境一致性;不解决领域边界、一致性与容量。
9. 下一篇
补编第 3 篇 · 行业架构案例讲评 —— 电商、营销、社交、外卖、约车、支付、信贷、物流八套架构:同一批组件为何配出不同结构,以及每套里"不可照抄"的部分。