高并发场景题(下):看起来简单、扛起量来极难
属于 S5 高并发系统设计 · 第四篇 上一篇:场景题(中) 下一篇:S6 Agent Backend
这一篇的场景有个共同点:单看功能都很简单,难点全在"量"上——Feed 流、点赞、排行榜、短链,随便一个做到亿级用户都极其复杂。它们的核心套路是"读写分离 + 推拉结合 + 缓存分层"。而且量越大,越依赖 Redis 这类 AP 组件,故障边界就越重要——每个场景末尾的"挂了怎么办"都要会讲(底层逻辑见一致性与故障边界)。
Feed 流:信息流怎么做
微博、朋友圈式的"关注的人发的内容按时间排序"。核心是推模式 vs 拉模式的权衡:
- 推(写扩散):发布时推给所有粉丝的收件箱,读快,但大 V 粉丝百万级,写放大严重。
- 拉(读扩散):读时聚合关注列表,写轻,但关注多时读慢。
- 推拉结合:大 V 用拉(不推),普通用户用推——这是工业界的标准答案。
收件箱用 Redis List(每条 feed 一个 key,时间倒序),冷热分离(热数据 Redis、冷数据 MySQL)。
故障与一致性边界:Feed 是典型的最终一致场景
| 故障 | 现象 | 处理 |
|---|---|---|
| 大 V 发布时 Redis 写失败 | 粉丝收件箱没刷出来 | 可容忍(Feed 本来就是最终一致):靠拉模式兜底(读时聚合大 V 的 feed 列表),Redis 恢复后回补 |
| 收件箱 Redis 主从丢写 | 部分粉丝漏了这条 feed | 用户"下拉刷新"触发重新聚合(拉模式兜底);或对账任务按"粉丝最后访问时间"补推 |
| 关注关系缓存挂 | 拉模式聚合时拿不到关注列表 | 降级查 DB 关注表;DB 是强一致锚点,缓存只是加速 |
深挖点:Feed 的一致性要求是什么档?→ 最终一致(少刷到一条 feed 用户感知不到),所以可以大胆用 Redis 推 + 拉兜底;绝不能用强一致方案(每个粉丝写都要确认,写放大直接打死系统)。一致性选档 = 业务容忍度在这里体现得最清楚。
点赞 / 计数:海量原子操作
点赞、阅读量、关注数,都是海量高频的原子自增。方案是:Redis 计数(INCR 原子、点赞用 Set 去重)→ 异步批量落库 → 定期对账(Redis 和 DB 校准,防止丢)。
要点:计数是"最终一致"的,不追求实时强一致,用对账兜底。
故障与一致性边界:计数丢了怎么回补
- Redis 主从切换丢写:最后几秒的点赞/阅读数丢失 → 对账回补:DB 有"点赞明细表"(user_id, target_id, 唯一索引),以明细为准重算计数;
- 为什么点赞要明细表而不是只存计数:计数可以重算的前提是有明细可重算——这是"最终一致能收敛"的根基(见一致性与故障边界 五、);
- 深挖链:计数 Redis 和 DB 不一致以谁为准?→ DB 明细。→ 那为什么不直接读 DB?→ 扛不住高频读。→ 对账周期内用户看到的数是错的?→ 是,但计数错几百不算事故(业务容忍度)。
排行榜:zset 的天下
实时榜单用 Redis ZSet(跳表支持 O(log n) 排名):ZADD 写分、ZREVRANGE 取 Top N、ZRANK 查排名。大数据量用分段(按分值区间拆多个 ZSet)。定时榜单用时间戳做 key(如 rank:2024-06-01)天然隔离周期。
故障与一致性边界:榜单不是"关键正确"数据
- zset 所在 Redis 挂/主从丢写:榜单短暂不准/缺失 → 榜单允许最终一致(错几分钟没人告你);处理:主从切换后从快照重建 + 增量补(对账:以"分数变更事件"或 DB 为源重建);
- 深挖点:排行榜"分数"的来源要单一(如以业务事件流为准),Redis 只是投影——投影可以重建,源头不能丢(源头在 DB/Kafka)。
短链:发号器 + 进制转换
长 URL 转短链,核心两步:发号器生成唯一 ID(Redis INCR / 雪花),62 进制转换(0-9a-zA-Z 最短表达)得到短码。存储用 Redis(热)+ MySQL(持久),访问时 301 跳转。
故障与一致性边界:发号器是"必须可用"的
- 发号器挂 → 短码发不出来,短链服务整体停摆 → 号段模式(一次取一批号段本地发,发号器挂了本地照发,见场景题(上) 分布式 ID);
- 短码唯一性边界:62 进制在 7 位内表达空间有限(62^7 ≈ 3.5 万亿),发满会冲突 → 需要DB 唯一索引兜底(重复插入报错 → 重新发号);
- 深挖点:短码为什么用 301 而不是 302?→ 301 永久重定向会被浏览器/搜索引擎缓存,减少服务压力;但改目标 URL 不生效(缓存了)→ 所以业务要能改目标 URL 时用 302。这是"缓存一致性"在短链里的体现。
海量数据:单表装不下了
单表几千万上亿,读写都慢。方案是分库分表(按 user_id 哈希分到 N 库 N 表),配合 bitmap(海量签到/在线状态,极省内存)、布隆过滤器(判断存在,防穿透)。
分库分表的关键点:分片键选查询最频繁的字段、尽量避免跨分片查询、分表后不能用自增主键(改用雪花算法)。
故障与一致性边界:分片后的故障与扩容
| 场景 | 问题 | 处理 |
|---|---|---|
| 跨分片事务 | 一次操作涉及多个分片的数据(如用户 + 订单在不同片) | 规避:按"主维"分片(订单按 user_id 分,同用户同片);跨片一致性 → 最终一致(本地消息表/SAGA,见一致性与故障边界 4.7) |
| 单分片挂 | 该片数据不可用 | 分片 + 副本(每片一主一从),主挂从提升;分片数量 = 故障爆炸半径的单位 |
| 扩容(加片) | 哈希取模会全量 rehash → 数据迁移风暴 | 一致性哈希(只迁移受影响部分);或"翻倍扩容"(2N 取模,一半数据迁移);或分片键带日期(按时间滚动,加片只影响新数据) |
| 布隆过滤器误判 | 存在性判断可能误报(不存在判成存在) | 布隆只做"第一道拦截"(不存在一定判不存在),误判的放行到 DB 二次确认——DB 是最终答案 |
深挖链:分库分表后跨片查询怎么办?→ 设计上避免(分片键选查询主维度);确实要查 → 应用层聚合(多片并行查 + 合并)或冗余存储(宽表/ES)。→ 分片键选错了能改吗?→ 极难,所以分片键是第一设计决策。
串起来
从 S1 到 S5,整套知识终于串成闭环:MySQL 管存储、Redis 管缓存和计数、Kafka 管异步、微服务管编排,而这些"看起来简单"的场景,正是四者组合运用的大舞台。每个场景都能用同一把尺子衡量:这数据允许最终一致吗?能重算/对账吗?源头在哪?——回答得了这三个问题,故障边界就讲得清(详见一致性与故障边界)。
下一篇是 S6,把这四个组件真正串成一个系统——后端 Agent 决策。