不能只用 parent_id 做盖楼,因其导致n次查询和扫描;双层桶(root_id+path)支持索引查询与前缀匹配,配合写时计算hot_score和批量修复path,才能扛住高并发。

为什么不能只用 parent_id 做盖楼?
很多团队一开始直接照搬 MySQL 邻接表思路,在 MongoDB 里加个 parent_id 字段,以为能复用递归逻辑。结果一压测就崩:单条热门视频下 5 万+ 子评论,前端每次展开一个二级回复,后端就得查一次 parent_id,再查它的 parent_id……最后变成 N 次网络往返 + N 次集合扫描。MongoDB 的 $graphLookup 虽然支持递归,但不支持分页、不走索引、无法按热度排序,线上根本不敢开。
root_id + path 是双层桶的核心字段
真正扛住抖音级并发的建模,关键不是“能不能嵌套”,而是“能不能预判加载路径”。双层桶本质是把树形结构扁平化为两条可索引的线性路径:
-
root_id:强制所有子评论都存一级评论的 ID(不是父评论 ID),确保“查某条主评的所有回复”是一次等值查询 -
path:字符串数组,存完整祖先链,比如["r_123", "c_456", "c_789"],其中r_*是 root,c_*是 comment。它让“查某条子评的完整盖楼”变成前缀匹配查询
示例文档:
{"_id": "c_789", "root_id": "r_123", "path": ["r_123", "c_456", "c_789"], "content": "回复 @张三", "videoId": "v_001", "userId": "u_999", "createdAt": {"$date": "2026-04-09T14:22:00Z"}}
对应索引必须建两个:
-
db.comments.createIndex({root_id: 1, createdAt: -1})—— 加载主评全部子回复用 -
db.comments.createIndex({path: 1, createdAt: -1})—— 展开某条子评盖楼时用{path: {$regex: "^r_123,c_456"}}
热度排序不能靠 $sort 实时算
抖音评论区默认按“点赞数 + 时间衰减”排序,但 MongoDB 不适合在每次查询时对全量子评论做 $addFields + $sort。真实做法是写时计算:
- 新增/更新评论时,用原子操作
findOneAndUpdate同步更新hot_score字段(公式如:like_count * 1000 + Math.floor((Date.now() - createdAt.getTime()) / 3600000)) - 查询时直接
sort({hot_score: -1}),配合root_id索引,毫秒出结果 - 点赞数变更走异步队列更新
hot_score,避免阻塞主流程
注意:hot_score 必须是数字类型,不能是表达式字段,否则无法走索引。
删评论时 path 字段不能留孤儿
用户删除中间某条子评(比如 c_456),它下面所有子孙评论的 path 就失效了。不能靠应用层遍历修复——高并发下会锁表。正确做法是:
- 删前先查出所有子孙:
db.comments.find({path: {$regex: "^r_123,c_456,"}}) - 用批量更新把它们的
path替换为跳过被删节点的新路径(如把["r_123","c_456","c_789"]改成["r_123","c_789"]) - 这一步必须用
bulkWrite原子执行,且path索引要提前建好,否则更新过程极慢
最易忽略的是:path 字段长度没限制,但 MongoDB 单文档最大 16MB,深度超过 20 层的极端盖楼必须截断或转存到独立集合。











