必须用referralCode双向索引并建invitedBy普通索引;三层以上邀请链需预存ancestors数组;rewardLogs须独立分片,奖励统计用原子更新totalInviteRewards字段;所有关系变更须事务强一致。

邀请码关联必须用 referralCode 字段做双向索引
用户注册时填的邀请码,不能只存进被邀请人文档里就完事。mongodb 没有外键约束,父子关系靠字段维系,一旦漏建索引或字段命名不统一,后续查“谁邀请了谁”或“某人邀请了几个人”会极慢甚至查不准。
实操建议:
- 在
users集合中固定两个字段:referralCode(本用户的唯一邀请码)、invitedBy(字符串,存上级的referralCode) - 为
invitedBy建普通索引:db.users.createIndex({ invitedBy: 1 });若常按邀请人查全路径,再加一个复合索引:{ invitedBy: 1, createdAt: -1 } - 避免用
userId存invitedBy—— 用户注销或 ID 格式变更时,关系链直接断裂;而referralCode是业务层生成、稳定不变的标识 - 注册时校验
invitedBy是否真实存在且未禁用,否则写入空值或拒绝,别让脏数据进库
三层以内裂变用递归聚合,超三层必须预计算 ancestors 数组
MongoDB 的 $graphLookup 能查邀请链,但深度超过 3 层后性能陡降,尤其集合大了以后容易超内存或超时。真实业务里,奖励结算、排行榜、风控扫描都要求毫秒级响应,不能每次请求都现场遍历。
实操建议:
- 新用户注册/邀请关系变更时,同步更新其文档的
ancestors字段(字符串数组),例如:["A123", "B456", "C789"]表示 A 邀 B、B 邀 C、C 邀当前用户 - 这个数组长度建议硬性限制为 3 或 5(根据业务规则),避免无限蔓延;超出部分截断,不补全
- 查“某人所有下级”时,不再用
$graphLookup,改用:{ ancestors: { $in: ["X999"] } }或{ invitedBy: "X999" }分层查,快且可控 - 注意:
ancestors必须在事务内更新(如果用 MongoDB 4.0+),否则并发注册可能导致父链错位
rewardLogs 集合必须分片 + 写时分离,别和 users 混在一起
奖励发放是高频写操作,每笔都要记日志、扣余额、更新统计。如果把奖励记录嵌在 users 文档里,很快会触发 BSON 16MB 限制;如果全堆在单个集合,写入瓶颈和查询压力都会集中爆发。
实操建议:
- 单独建
rewardLogs集合,字段精简:必含userId、type(如"invite_bonus")、amount、triggeredBy(谁触发的)、createdAt - 按
userId分片(shard key),避免热点;如果用副本集且量不大,至少加 TTL 索引自动清理半年前日志:db.rewardLogs.createIndex({ createdAt: 1 }, { expireAfterSeconds: 3600 * 24 * 180 }) - 统计“某人累计获得邀请奖励”时,别实时
$sum全表,而是维护一个totalInviteRewards字段在users里,每次发奖后原子更新:db.users.updateOne({ _id: userId }, { $inc: { totalInviteRewards: 10 } }) - 切记:奖励金额必须用整数单位(如“分”),别存浮点型
NumberDecimal—— 多次加减后精度漂移,对账时哭都来不及
父子层级变更时,ancestors 和 totalInviteRewards 必须强一致性更新
用户注销、邀请码作废、关系申诉重置……这些操作会动到层级结构。如果只删 invitedBy 却不清理 ancestors,或者只清日志不回滚统计,奖励就会多发或少发,而且很难追溯。
实操建议:
- 任何影响邀请关系的操作,必须走统一 service 方法,内部用
session.startTransaction()包裹:先查原链,再删子树ancestors,再递减各级totalInviteRewards,最后删对应rewardLogs - 不要依赖定时任务“事后修正”——延迟高、难调试、易漏;修复逻辑比正向逻辑更复杂,优先堵住源头
- 上线前用脚本抽样验证:随机选 100 个三级邀请链,检查
ancestors长度、内容、对应用户的totalInviteRewards是否与rewardLogs总和一致
层级深了之后,最麻烦的不是怎么存,而是怎么保证每次写都把所有相关字段同步改对。字段分散在不同集合、不同文档里,一个漏掉,整个链就偏了。