Skip to content

高并发高可用设计 ​

Java 架构师 · 高并发与架构

高并发关注"扛得住流量"(吞吐、延迟),高可用关注"挂不掉、挂了能恢复"(冗余、隔离、容灾)。二者是架构设计的两条主线:前者靠缓存、拆分、异步削峰,后者靠冗余、隔离、容灾。本质上,二者统一于同一个工程哲学——"假设任何组件都会失败,并把故障影响圈在最小范围"。本章从缓存全链路(穿透/击穿/雪崩)讲到读写分离与主从延迟,再到削峰、预热与异地多活单元化,给出可落地的设计权衡。

一、整体思路:用"摊平"与"圈住"对抗流量与故障 ​

设计分两层:

  • 扛流量:多级缓存(本地→Redis)、读写分离、分库分表、异步化、CDN。
  • 保可用:冗余副本、超时/重试/熔断、降级、限流、容灾演练。

二、缓存全链路:穿透 / 击穿 / 雪崩 ​

2.1 缓存穿透(查不存在的 key) ​

恶意/失误查询不存在的数据,缓存与 DB 都没有 → 每次都打 DB。解法:

  • 布隆过滤器(Bloom Filter):在缓存前拦截"一定不存在"的 key(有误判但无漏判),空间极小。
  • 空值缓存:查不到也写 null 并短 TTL,防重复穿透(注意对热点不存在 key 单独设短过期避免占内存)。

2.2 缓存击穿(热点 key 失效瞬间) ​

某个极热 key 过期瞬间,大量并发同时击穿到 DB,DB 被打爆。解法:

  • 互斥重建:首个线程拿到"重建锁"(如 Redis 分布式锁),其余线程等待/返回旧值,只有一个去查 DB 并回填。
  • 逻辑过期:key 不物理过期,值里带 expire 字段;发现逻辑过期后异步重建,期间返回旧值(用户无感知)。适合"允许短暂旧值"的场景。

2.3 缓存雪崩(大量 key 同时失效 / Redis 宕机) ​

大量 key 同一时刻过期,或 Redis 集群整体不可用 → 流量全部压到 DB。解法:

  • 过期时间加随机抖动:base + random(0, jitter),避免同时失效。
  • 多级缓存:本地 Caffeine 兜底,Redis 挂时本地仍能扛部分读。
  • Redis 高可用:集群 + 哨兵,避免单点。
  • 限流+降级:DB 层限流,失败返回默认/排队。

三、缓存与数据库一致性:Cache-Aside 与并发缺陷 ​

最常用 Cache-Aside(旁路缓存):

  • 读:缓存命中则返回;未命中查 DB 再写缓存。
  • 写:先更新 DB,再删除缓存(而非更新缓存——避免并发写导致的缓存脏值)。

但"先更新 DB 再删缓存"仍有并发窗口:删缓存失败 → 缓存脏。更稳妥:

  • 延迟双删:更新 DB → 删缓存 → 休眠几百 ms → 再删一次(覆盖删除失败 + 并发读回填的脏值)。
  • binlog 订阅(Canal):监听 DB binlog,由独立消费者删/更新缓存,与业务解耦,保证"DB 变 → 缓存最终一致",是大型系统更可靠的做法。

两种写顺序的并发缺陷对比

  • 先删缓存再更新 DB:并发 A 删缓存 → B 读(缓存空)读 DB(旧值)写缓存(脏值) → A 更新 DB。结果缓存是旧值,直到下次过期。缺陷:读请求易把旧值回填。
  • 先更新 DB 再删缓存:A 更新 DB → B 读缓存(旧值, 此时还没删) → A 删缓存。B 拿到旧值但下次读即新值,影响更小;除非 B 读发生在 A 删之前且 B 慢。综合更优,配合延迟双删/Canal 可基本消除。

四、读写分离与主从延迟 ​

一主多从,写走主库、读走从库,分摊读压力。但主从异步复制有延迟(毫秒到秒级),引发:

  • 读己之写缺失:用户刚提交订单就刷新,读从库看到"没有" → 体验 bug。
  • 强一致读错乱:金融类查询读到旧余额。

解法:

  • 会话一致 / 读主:写后短暂读主库(如 1s 内该用户读主),或"写后强制读主"。
  • 半同步复制:主库等至少一个从库 ACK 再返回,降低延迟但牺牲写性能(RPO 与性能的权衡)。
  • 监控主从延迟(Seconds_Behind_Master),超阈值把读切回主或告警。

五、削峰填谷:用 MQ 把尖峰变平稳 ​

大促下单:瞬时万级 QPS 但后端只能平稳消费千级。架构:写请求入 MQ,前端返回"排队中",后端按自身能力消费。

要点:MQ 是削峰缓冲,但MQ 本身也会成单点——需限流兜底(MQ 满则拒绝新请求)、多级缓冲(本地队列 + MQ)。前端用"排队中/异步结果"降低用户预期。

六、缓存预热与灰度放量 ​

新实例上线即被打满,往往因为"缓存是冷的"(本地/Redis 都没热点数据)。预热:

  • 上线前把热点数据批量加载到 Redis(预热脚本)。
  • 本地缓存(Caffeine)在启动后主动加载热点。
  • 灰度放量:新实例先接 1% 流量,缓存逐步升温,再全量,避免冷启动击穿 DB。

七、异地多活与单元化:容灾的终极形态 ​

单机房有上限且有"机房级故障"风险(光纤挖断、断电)。异地多活把流量分散到多个机房。核心是单元化(Unitization):

  • 单元封闭:用户按路由规则(如 userId 哈希)固定落在某机房,该用户的"写"全在本单元内闭环,跨机房调用最小化。
  • 数据多活:单元内数据独立写入,跨单元同步(最终一致)。难点是冲突合并——同一条数据若两单元都可能写(如全局配置),需单写或冲突解决策略。
  • 脑裂(Split-Brain):两单元网络断开后各自写入,恢复时数据冲突。对策:明确"哪些数据单写(全局唯一源)、哪些可多活(需冲突合并)";单写数据用租约/选主保证只有一个单元可写。

多活的设计红线

  • 不能"所有数据都多活":强一致要求的全局数据(如全局库存、账户总账)必须单写,否则脑裂即数据错乱。
  • 容灾演练缺失 = 虚假多活:从没演练过"断一个机房流量切另一个",真故障时切换多半失败。演练(混沌工程)比冗余更重要。
  • RTO/RPO 要量化:切流目标(恢复时间 RTO、数据丢失 RPO)必须事先定义并演练验证。

八、最佳实践 ​

  • 多级缓存(本地 Caffeine + Redis)+ 热点 key 逻辑过期 + 布隆防穿透。
  • 一致性优先 binlog 订阅(Canal),写后删缓存 + 延迟双删兜底。
  • 容量规划基于压测,留 30%+ 余量应对突发;核心链路全异步化。
  • 容灾演练常态化(关机房、杀节点),验证 RTO/RPO 与单元化闭环。

本节小结

高并发靠"缓存、拆分、异步削峰"把流量摊平——多级缓存抗读、读写分离抗读压力、MQ 削峰抗写尖峰;高可用靠"冗余、隔离、容灾"把故障圈住。缓存三大问题中:穿透用布隆+空值、击穿用互斥重建/逻辑过期、雪崩用过期抖动+多级缓存+集群HA。一致性用 Cache-Aside(先更 DB 再删缓存)+ Canal 订阅兜底。主从延迟用读主/半同步缓解。异地多活靠单元化闭环 + 明确单写/多活边界以避脑裂。铁律:高可用不是堆机器,而是"假设任何组件都会挂"并用演练验证冗余。架构师的价值在于按业务价值分配这些手段的投入强度。

📚 官方文档与参考资料 ​

✏️ 练习与自测 ​

  1. 设计题:设计一个秒杀系统:1 万 QPS 抢 1000 件商品,画出架构(缓存/锁/MQ/限流)并说明如何防超卖(含 Redis Lua 原子扣减与库存预热)。
  2. 辨析题:对比"先删缓存再更新 DB"与"先更新 DB 再删缓存"的并发缺陷,给出"延迟双删 + Canal 订阅"如何消除脏缓存。
  3. 设计题:两地三中心多活方案,说明哪些数据必须单写(避免脑裂)、哪些可多活,并给出网络分区恢复后的冲突合并策略。
  4. 排查题:主从延迟导致"用户刚下单刷新看不到",给出三种不同一致强度(会话一致/读主/半同步)的取舍与实现。