必须用独立 friendships 集合建模:嵌入式数组易触文档超限、跨文档更新不一致、反向查询全表扫描三连崩;正确做法是建 friendships 集合+双向单字段索引,$graphlookup 查二度好友需严控参数,高频指标须离线预计算并实时联动更新。

必须用独立 friendships 集合建模,嵌入式数组(如 friends: ["u2", "u3"])在真实业务中会直接触发文档超限、更新不一致、反向查询全表扫描三连崩。
为什么不能把关注关系嵌进用户文档里
看似省一个集合,实则埋了三个硬伤:
-
16MB 文档上限极易撞线:用户有 5000+ 关注者时,仅 ID 数组就接近 100KB;若再加时间戳、状态、备注等字段,撑爆是常态 - 加关注/取关需跨文档原子写:要同时
$push到 u1 的following和 u2 的followers,MongoDB 不支持跨文档事务(即使开启 multi-document transaction,高并发下性能断崖下跌) - 查“谁关注了 u2”无法建索引:
db.users.find({ following: "u2" })是 collection scan,QPS 过百就抖动,P99 延迟秒级起步
正确建模:friendships 集合 + 双向单字段索引
每条记录表示一次单向关注行为,结构极简:
db.friendships.insertOne({
user_id: "u1",
friend_id: "u2",
created_at: new Date(),
status: "active" // 可扩展:pending / blocked / muted
})
立刻执行以下两条索引命令,缺一不可:
-
db.friendships.createIndex({ user_id: 1 })—— 支持查 “u1 关注了谁”,走索引 range scan -
db.friendships.createIndex({ friend_id: 1 })—— 支持查 “谁关注了 u2”,否则find({ friend_id: "u2" })就是全表扫
漏掉任一索引,反向查询性能归零;复合索引(如 {user_id: 1, friend_id: 1})对反向查询无效,别被误导。
$graphLookup 查二度好友的写法与致命坑
它不是图数据库,$graphLookup 是聚合阶段模拟遍历,极易因参数错配 OOM 或超时:
-
maxDepth必须显式设值:二度好友(u1 → u2 → u3)对应maxDepth: 1,不是 2;不设等于无限递归 -
connectFromField和connectToField必须严格匹配字段名:存的是user_id/friend_id,就不能写成from/to -
startWith是起点值,不是字段路径:写"$friend_id"(字符串值),不是"$friends"(数组会静默失败) - 高频接口别塞权重计算:共同好友数、最近互动时间等逻辑放聚合里,P99 轻松破 300ms;应离线预计算并存到
recommend_scores集合
典型安全写法(查 u1 的二度好友,排除自己和一度好友):
db.friendships.aggregate([
{ $match: { user_id: "u1" } },
{
$graphLookup: {
from: "friendships",
startWith: "$friend_id",
connectFromField: "friend_id",
connectToField: "user_id",
as: "second_degree",
maxDepth: 1,
depthField: "depth"
}
},
{ $unwind: "$second_degree" },
{ $match: { "second_degree.friend_id": { $ne: "u1" } } },
{ $group: { _id: "$second_degree.friend_id" } }
])
高频场景必须预计算,别依赖实时聚合
共同好友数、互关率、最近互动天数这些指标,线上请求中实时算 $graphLookup + $lookup + $setIntersection,延迟不可控:
- 离线任务每小时跑一次,把结果写入
common_friends集合:{ user_id: "u1", target_id: "u2", count: 42 } - 为该集合建复合索引:
db.common_friends.createIndex({ user_id: 1, target_id: 1 }, { unique: true }) - 线上接口直接
findOne({ user_id: "u1", target_id: "u2" }),毫秒级返回 - 用户发生新互动(点赞/评论)时,发消息触发轻量更新,不阻塞主流程
真正容易被忽略的点是:预计算不是“一劳永逸”,它必须和实时事件联动;只靠定时任务,会丢失秒级热度变化,比如突发热搜话题带来的临时关注激增。











