B站二面:设计一个“评论盖楼”系统,我只答了“递归”,面试官笑了:你连索引都不会建?

📅 2026/7/15 9:47:03
B站二面:设计一个“评论盖楼”系统,我只答了“递归”,面试官笑了:你连索引都不会建?
上周有个三年开发经验的粉丝找我哭诉说去面 B 站二面挂得莫名其妙。 面试官让他设计一个类似 B 站/抖音的评论系统。他心想这不就是建张表存个parent_id搞自关联嘛结果面试官一连串“夺命追问”“如果一个评论下面有 1 万条子评论你递归查库数据库不崩吗”“按热度排序怎么做实时 count 点赞表”“中间层的评论被删了挂在它下面的子孙评论怎么办树断了”这兄弟当场自闭支支吾吾半天没答上来。其实评论系统的核心不在于“存”而在于“查”。尤其是 B 站这种“二层盖楼”模式和传统的无限层级论坛完全是两码事。今天 Fox 带你把这道题拆得底裤都不剩下次面试直接拿去“降维打击”。一、 青铜回答邻接表 (Adjacency List) —— 面试官的陷阱很多人的第一反应也是教科书里的标准答案是设计成这样面试官追问 1“我要查一个一级评论下的所有子孙评论包括回复的回复你怎么查”你的回答“代码里写个递归啊或者循环查。”Fox 解析错大错特错这是典型的N1 问题。如果一个楼层盖了 1000 层你难道要查 1000 次数据库虽然 MySQL 8.0 支持 CTE (递归查询)但在千万级数据的高并发场景下让数据库做递归计算就是找死CPU 会瞬间飙高。哪怕你加了缓存缓存穿透的那一瞬间DB 也就挂了。二、 黄金回答路径枚举 (Materialized Path) —— 还是不够完美为了解决递归稍微有点经验的会说加个path字段 比如1/23/456代表 ID 为 456 的评论父亲是 23爷爷是 1。面试官追问 2“索引怎么建LIKE 查询会失效吗”答建普通索引。查询用LIKE 1/23/%。这走的是最左前缀匹配索引不会失效性能是可以的。“如果中间楼层ID 23被删了子评论456怎么办”答呃... 那得把所有以1/23/开头的path全找出来update 一遍...面试官“如果子评论有 10 万条你为了删 1 条数据引发 10 万次更新这是数据库死锁的温床”三、 王者回答反范式设计 扁平化存储B站/抖音模式到了这一步你要先反问面试官一句“我们的业务形态是‘无限层级’类似 Reddit/旧版贴吧还是‘两层结构’类似 B站/抖音/小红书”现在主流的 App 为了移动端体验都是两层结构一级评论对视频/文章的直接评论。二级评论所有的回复不管你是回复谁的在 UI 上全部扁平化展示在一级评论下面。满分表结构设计放弃范式我们需要引入冗余字段。1. 评论主表 (comment)root_id核心永远存储最顶层那个一级评论的 ID。如果是 ID 101一级评论root_id就是 0。如果是 ID 102回复 101root_id是 101。如果是 ID 103回复 102root_id还是 101而不是 102。reply_to_id仅用于前端展示“回复 某某”不参与核心查询逻辑。查询神技不管楼盖得有多高查询 ID 为 101 的一级评论下的所有回复只需要一条 SQLSELECT * FROM comment WHERE root_id 101 ORDER BY create_time ASC;根本不需要递归没有任何 N1 问题一个索引搞定效率极高四、 追问地狱热度排序、一致性与 用户别急架构设计完了面试官真正的高并发考验才开始。这三个回答能帮你拿 S 卡。Q1怎么按“热度”排序点赞数 回复数面试官“点赞一直在变每次查数据库都ORDER BY (like_count reply_count)数据库扛得住吗”Fox 破解Redis ZSet 排行榜千万不要在 MySQL 里实时排序这是计算密集型操作。Redis Key:comment:hot:{video_id}:{root_id}Value (Member):comment_idScore:热度值(公式点赞数 * 10 回复数 * 20 时间衰减因子)流程用户点赞 - 异步写入 MQ - 消费者更新 MySQL - 同时更新 Redis ZSet 的 Score。查询时 - 先查 Redis ZSet 拿到排好序的 ID List (Top 10) - 再去 MySQL (或缓存) 里WHERE id IN (...)查具体内容。Q2Redis 和 MySQL 一致性怎么保面试官“Redis 更新了MySQL 还没落库用户刷新没变怎么办”Fox 破解允许短暂不一致评论的点赞数不是“银行余额”不需要强一致性策略用户点赞后前端直接 1骗过用户眼睛给足体验。后台走 MQ 异步慢慢落库。哪怕 Redis 里的热度比 MySQL 里的真实数据快了 1 秒谁在乎兜底就算 Redis 挂了只是排序暂时乱了数据都在 MySQL 里不会丢。Q3用户 怎么设计面试官“评论里可以 Fox怎么解析怎么存怎么发通知”Fox 破解结构化存储 异步通知千万别直接存文本“Fox 你好”万一我想改名叫FoxPlus你以前的评论显示的还是旧名字存储格式建议存 JSON 或者特殊标记。数据库存{user_id:888, name:Fox} 你好前端解析看到特定的格式渲染成超链接。解耦通知保存时后端解析出user_id:888。解耦扔个消息到 MQtopic: at_notification。消费通知服务消费消息给 ID 为 888 的用户发站内信/Push。千万不要在保存评论的主线程里发通知会卡死五、 面试满分模板直接背诵下次遇到这个问题别犹豫直接按这个套路输出“对于 B 站这种高并发评论系统传统的树形结构是死路必须用‘两级扁平化’设计。存储结构放弃递归引入root_id冗余字段。所有子子孙孙评论的root_id都指向同一个一级评论。这样查询时WHERE root_id ?一次索引扫描就能拉出整楼数据性能 O(1)。热度排序数据库只存数据排序交给Redis ZSet。用(点赞回复)计算 Score实现高性能 TopN 排序。高并发优化引入 MQ 做削峰。点赞、回复、通知全部异步化。架构思想核心是‘读写分离动静分离’。接受最终一致性利用 Redis 做前置读缓存MySQL 做兜底存储。这就是一个生产级的、能抗千万 QPS 的评论系统架构。”写在最后技术面试考的从来不是你能不能写出代码而是你“见过多少世面”。能用扁平化解决的绝不搞递归能用 MQ 异步的绝不搞同步阻塞。这套扁平化存储 Redis ZSet MQ 异步的组合拳不仅能解决评论系统帖子的回复、IM 群聊消息通通都能用