You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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. 简单架构下是否能实现可靠的计数方案?若可以,具体如何实现?
  2. 大型社交系统又是如何处理此类问题的?

解决方案

一、简单架构下的可靠实现方案

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.22 21:30:32