2335 字
约 7 分钟
6
S18-A06 · 中间件与分布式:缓存、消息、存储、容器

适用读者:要在一堆中间件里做选型的人;被告警追着跑、分不清"是缓存还是 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. 自测

  1. 缓存三模式与四类故障?为什么"删缓存"优于"更新缓存"?
  2. 消息三种语义?"至少一次"下幂等键怎么选?
  3. outbox 模式解决什么?
  4. 分片键三约束?分库分表的三个触发线?
  5. 容器化解决什么、不解决什么?
答案
  1. Cache-Aside/读写穿透/写回;穿透/击穿/雪崩/一致性;删除让下次读回源,避免并发写旧值。
  2. 至多/至少/恰好一次;业务单号或消息 ID + 去重表。
  3. 业务写库与消息发送的原子性(本地事务 + 独立投递)。
  4. 高基数均匀、查询必带、不可变;2,000 万行/1TB/写 5k。
  5. 解决环境一致性;不解决领域边界、一致性与容量。

9. 下一篇

补编第 3 篇 · 行业架构案例讲评 —— 电商、营销、社交、外卖、约车、支付、信贷、物流八套架构:同一批组件为何配出不同结构,以及每套里"不可照抄"的部分。

S18-A06 · 中间件与分布式:缓存、消息、存储、容器
https://www.clxhxhhr.top/posts/5446/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。