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

MongoDB聚合统计好友数异常求助:更新后未统计新好友

问题分析与解决方案

问题根源

你遇到的统计延迟/旧数据问题,主要来自三个核心原因:

  1. 异步中间件未等待:post('save')回调调用了异步的calcFriends但未用await,Mongoose不会等待该操作完成就继续流程,导致聚合查询时数据库的写操作可能还未完全生效。
  2. Update操作不触发Save中间件:如果接受好友请求时用的是findByIdAndUpdate/updateOne这类直接更新方法,而非文档实例的save(),post('save')根本不会触发,好友数自然不会更新。
  3. 全量聚合的竞态风险:依赖实时聚合统计好友数,在高并发场景下容易出现数据不一致,且每次统计都要扫描集合,效率低下。

解决方案

方案1:修复Save中间件的异步等待

最直接的修复是让post('save')等待统计操作完成,确保聚合查询能拿到最新数据:

FriendSchema.post('save', async function () {
  await this.constructor.calcFriends(this.sender, this.recipient);
});

方案2:为Update操作添加对应中间件

如果你的业务中用findByIdAndUpdate这类方法更新好友状态,需要额外添加findOneAndUpdate的中间件:

// 注意:使用findByIdAndUpdate时要设置{ new: true }才能拿到更新后的文档
FriendSchema.post('findOneAndUpdate', async function (doc) {
  if (doc) {
    await doc.constructor.calcFriends(doc.sender, doc.recipient);
  }
});

方案3:改用增量更新(推荐最佳实践)

全量聚合效率低且易出问题,建议根据好友状态的变更,直接增量更新用户的friendsCount,彻底避免统计延迟:

  1. 添加Pre中间件记录状态变更
// 处理save操作的状态变更
FriendSchema.pre('save', function (next) {
  if (this.isModified('status')) {
    this._prevStatus = this._original?.status;
  }
  next();
});

// 处理findOneAndUpdate操作的状态变更
FriendSchema.pre('findOneAndUpdate', async function (next) {
  const doc = await this.model.findOne(this.getQuery());
  this._prevStatus = doc.status;
  this._sender = doc.sender;
  this._recipient = doc.recipient;
  next();
});
  1. 在Post中间件中执行增量更新
// 处理save操作的增量更新
FriendSchema.post('save', async function () {
  if (!this.isModified('status')) return;

  const { sender: senderID, recipient: recipientID, status, _prevStatus } = this;
  const updateOps = [];

  if (_prevStatus === 'pending' && status === 'accepted') {
    // 接受请求:双方好友数+1
    updateOps.push(
      { updateOne: { filter: { _id: senderID }, update: { $inc: { friendsCount: 1 } } } },
      { updateOne: { filter: { _id: recipientID }, update: { $inc: { friendsCount: 1 } } } }
    );
  } else if (_prevStatus === 'accepted' && status !== 'accepted') {
    // 取消/拒绝已接受请求:双方好友数-1
    updateOps.push(
      { updateOne: { filter: { _id: senderID }, update: { $inc: { friendsCount: -1 } } } },
      { updateOne: { filter: { _id: recipientID }, update: { $inc: { friendsCount: -1 } } } }
    );
  }

  if (updateOps.length > 0) {
    await User.bulkWrite(updateOps);
  }
});

// 处理findOneAndUpdate操作的增量更新
FriendSchema.post('findOneAndUpdate', async function () {
  const { _prevStatus, _sender: senderID, _recipient: recipientID } = this;
  const newStatus = this.getUpdate().$set?.status;
  if (!newStatus || _prevStatus === newStatus) return;

  const updateOps = [];

  if (_prevStatus === 'pending' && newStatus === 'accepted') {
    updateOps.push(
      { updateOne: { filter: { _id: senderID }, update: { $inc: { friendsCount: 1 } } } },
      { updateOne: { filter: { _id: recipientID }, update: { $inc: { friendsCount: 1 } } } }
    );
  } else if (_prevStatus === 'accepted' && newStatus !== 'accepted') {
    updateOps.push(
      { updateOne: { filter: { _id: senderID }, update: { $inc: { friendsCount: -1 } } } },
      { updateOne: { filter: { _id: recipientID }, update: { $inc: { friendsCount: -1 } } } }
    );
  }

  if (updateOps.length > 0) {
    await User.bulkWrite(updateOps);
  }
});

额外优化:初始化历史数据的好友数

如果你的数据库已有历史好友记录,需要在应用启动时执行一次全量统计,初始化所有用户的friendsCount:

async function initFriendsCount() {
  const friendStats = await Friend.aggregate([
    { $match: { status: 'accepted' } },
    {
      $group: {
        _id: { $push: ['$sender', '$recipient'] },
        count: { $sum: 1 }
      }
    },
    { $unwind: '$_id' },
    {
      $group: {
        _id: '$_id',
        total: { $sum: { $divide: ['$count', 2] } } // 每条记录被双方各统计一次,需除以2
      }
    }
  ]);

  const updateOps = friendStats.map(stat => ({
    updateOne: {
      filter: { _id: stat._id },
      update: { friendsCount: stat.total }
    }
  }));

  // 处理无好友的用户,设置friendsCount为0
  const allUserIds = await User.distinct('_id');
  const processedIds = friendStats.map(stat => stat._id.toString());
  const missingIds = allUserIds.filter(id => !processedIds.includes(id.toString()));

  missingIds.forEach(id => {
    updateOps.push({
      updateOne: {
        filter: { _id: id },
        update: { friendsCount: 0 }
      }
    });
  });

  await User.bulkWrite(updateOps);
}

// 应用启动时调用
initFriendsCount().catch(console.error);

内容的提问来源于stack exchange,提问作者Mohamed Elsobky

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.07 10:05:22