MongoDB聚合统计好友数异常求助:更新后未统计新好友
问题分析与解决方案
问题根源
你遇到的统计延迟/旧数据问题,主要来自三个核心原因:
- 异步中间件未等待:
post('save')回调调用了异步的calcFriends但未用await,Mongoose不会等待该操作完成就继续流程,导致聚合查询时数据库的写操作可能还未完全生效。 - Update操作不触发Save中间件:如果接受好友请求时用的是
findByIdAndUpdate/updateOne这类直接更新方法,而非文档实例的save(),post('save')根本不会触发,好友数自然不会更新。 - 全量聚合的竞态风险:依赖实时聚合统计好友数,在高并发场景下容易出现数据不一致,且每次统计都要扫描集合,效率低下。
解决方案
方案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,彻底避免统计延迟:
- 添加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(); });
- 在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
相关产品推荐
相关产品推荐

