MongoDB与Node.js环境下用户关注数更新的可靠性方案咨询
社交API关注计数的可靠性问题
当前关注模型
{ sourceId: { type: Schema.Types.ObjectId, ref: "user", required: true, }, targetId: { type: Schema.Types.ObjectId, ref: "user", required: true, }, approved: { type: Boolean, required: true, default: true, }, }
当前关注逻辑
const query: any = { targetId: <some_oid>, sourceId: <some_oid>, approved: true }; await new FollowModel(query).save(); await UserModel.updateOne({ _id: targetId }, { $inc: { followerCount: 1 } }); await UserModel.updateOne({ _id: sourceId }, { $inc: { followingCount: 1 } }); sendSuccess(res, FollowStatus.FOLLOWING);
当前流程存在异常风险:比如关注记录插入成功、被关注者计数更新成功,但关注者的计数更新失败。MongoDB事务虽能解决该问题,但需要副本集及复杂配置。现提出两个核心问题:
- 简单架构下是否能实现可靠的计数方案?若可以,具体如何实现?
- 大型社交系统又是如何处理此类问题的?
解决方案
一、简单架构下的可靠实现方案
1. 幂等校验+补偿任务组合
- 前置幂等校验:插入关注记录前,先查询是否已存在
sourceId和targetId对应的有效关注关系,避免重复执行操作导致计数错误。 - 操作日志记录:每次执行关注/取消关注操作时,强制写入一条操作日志(包含操作类型、
sourceId、targetId、操作状态),确保日志写入成功后再执行后续步骤。 - 定时补偿脚本:启动定时任务(比如每分钟执行一次),扫描未标记为完成的操作日志,核对
FollowModel记录与User表的计数:- 若存在有效关注记录,但
source的followingCount未对应增加,或target的followerCount未对应增加,则手动触发计数更新。 - 确认计数同步完成后,标记日志为已完成。
- 若存在有效关注记录,但
2. 反转操作顺序+失败回滚
把计数更新前置,关注记录插入后置,配合异常捕获实现回滚:
try { // 先更新计数 await Promise.all([ UserModel.updateOne({ _id: targetId }, { $inc: { followerCount: 1 } }), UserModel.updateOne({ _id: sourceId }, { $inc: { followingCount: 1 } }) ]); // 再插入关注记录 await new FollowModel(query).save(); sendSuccess(res, FollowStatus.FOLLOWING); } catch (err) { // 插入失败则回滚计数 await Promise.all([ UserModel.updateOne({ _id: targetId }, { $inc: { followerCount: -1 } }), UserModel.updateOne({ _id: sourceId }, { $inc: { followingCount: -1 } }) ]); sendError(res, "关注失败"); }
同时给所有数据库操作增加重试机制(比如用p-retry库),处理临时网络或数据库连接异常。
3. 原子化幂等操作
利用MongoDB的findOneAndUpdate实现幂等性关注,减少重复操作风险:
// 不存在则插入,存在则不做修改 const followResult = await FollowModel.findOneAndUpdate( { sourceId, targetId }, { $setOnInsert: { approved: true } }, { upsert: true, new: true } ); // 仅当是新插入的记录时,才更新计数 if (followResult.upsertedId) { await Promise.all([ UserModel.updateOne({ _id: targetId }, { $inc: { followerCount: 1 } }), UserModel.updateOne({ _id: sourceId }, { $inc: { followingCount: 1 } }) ]); } sendSuccess(res, FollowStatus.FOLLOWING);
用Promise.all并行执行计数更新,减少耗时;若其中一个更新失败,捕获异常后记录到操作日志,后续通过补偿任务修复。
二、大型社交系统的处理方式
1. 最终一致性+异步队列
大型系统不会强求强一致性,而是采用最终一致性架构:
- 异步化操作:用户触发关注后,先写入关注记录,然后发送消息到Kafka/RabbitMQ等消息队列,异步执行计数更新。
- 消费重试机制:消息队列的消费者处理计数更新时,若失败会自动重试(配置重试次数和间隔),保证计数最终会同步。
- 定期对账任务:后台定时扫描
Follow表,统计每个用户的实际关注/粉丝数,与User表的计数对比,修正不一致的数值。
2. 缓存层前置+异步持久化
- 把计数存储在Redis等缓存中,用户触发关注时,先更新缓存中的计数,保证前端能快速获取最新数值,再异步同步到数据库。
- 即使数据库更新失败,缓存的计数也能保证用户体验,后续通过重试任务完成数据库同步。
3. 分库分表+分布式事务
针对超大规模用户量,会采用分库分表架构,此时会用TCC、XA等分布式事务方案保证跨库操作的一致性,但这会大幅增加架构复杂度,一般只在核心场景使用。
内容的提问来源于stack exchange,提问作者Shri
相关产品推荐
相关产品推荐

