好友关系必须用引用式建模,即单独建立friendships集合并创建user_id和friend_id双索引,否则会导致查询卡死、更新不一致、文档超限;$graphlookup查二度好友需严格配对字段、设maxdepth:1、避免复杂计算;共同好友数等高频指标须离线预计算并建复合索引。

好友关系必须用引用式建模,嵌入式写法在真实业务中会直接导致查询卡死、更新不一致、文档超限三连崩。
为什么不能把 friends 数组直接塞进用户文档里
看似省事,实则埋雷:
- 单个用户有 5000+ 好友时,
friends数组就可能撑爆 16MB 文档上限 - 加好友要同时更新
u1的friends和u2的friends,跨文档写操作无法原子保证 - 查“谁关注了 u2”得全表扫描所有用户文档——
db.users.find({ friends: "u2" })没索引可建,性能归零 - 删除好友时得从两个数组里分别
$pull,失败一个就数据不一致
正确建模:friendships 集合 + 双索引
建独立集合,每条记录表示一次单向关系:
db.friendships.insertOne({
user_id: "u1",
friend_id: "u2",
created_at: new Date()
})
必须立刻建两个索引:
-
db.friendships.createIndex({ user_id: 1 })—— 查 u1 关注了谁 -
db.friendships.createIndex({ friend_id: 1 })—— 查谁关注了 u2
漏掉任一索引,反向查询就会退化成 collection scan,QPS 上百就明显抖动。
$graphLookup 查二度好友的坑与写法
它不是万能图遍历工具,没配对参数或深度限制,聚合阶段会 OOM 或超时:
-
connectFromField和connectToField必须严格对应字段名,比如你存的是user_id/friend_id,就不能写成from/to -
maxDepth必须显式设值:二度好友(u1 → u2 → u3)是maxDepth: 1,不是 2;不设等于无限递归 -
startWith: "$friend_id"是起点值,不是字段路径;如果写成"$friends"(数组)会静默失败 - 高频调用时,
$graphLookup本身延迟高,别在里面塞复杂权重计算,否则 P99 轻松破 300ms
典型写法(查 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,
restrictSearchWithMatch: { friend_id: { $ne: "u1" } }
}
}
])
冗余存储 common_friends_count 是线上推荐的底线
实时跑 $graphLookup + 共同好友计数 + 时间衰减加权,根本扛不住并发请求:
- 离线任务每天跑一次,用 Spark 或 MapReduce 统计每对用户之间的
common_friends_count,写入recommend_scores集合 -
recommend_scores必须建复合索引:{ user_id: 1, target_id: 1 },支撑毫秒级点查 - 线上接口只做轻量修正:比如加入最近 24 小时互动行为(点赞/私信),用
$inc实时更新分值 - 地理推荐必须另起炉灶:靠
2dsphere索引 +$geoWithin,别指望$graphLookup带地理位置过滤
真正难的不是写出 $graphLookup,而是判断哪些关系路径值得预热、哪些用户对需要降级走缓存、以及当 restrictSearchWithMatch 里混入时间范围时,索引是否还能生效——这些细节不压测根本暴露不出来。











