MongoDB/Mongoose如何按ID统计CommentsView数据并同步更新Posts评论浏览量
实现方案
你可以根据业务场景选择以下三种方案,也可以组合使用保证数据一致性:
方案1:Mongoose 中间件(应用层实现,最常用)
适用场景:仅当前Node.js服务会写入CommentsView集合,无其他服务直接操作MongoDB的场景。
直接在CommentsView的Schema上绑定post保存钩子,每次插入浏览记录后自动更新对应评论的浏览量:
const mongoose = require('mongoose'); // 引入Posts模型,替换为你实际的文件路径 const Post = require('./models/post'); const commentsViewSchema = new mongoose.Schema( { text: { type: String, }, uid: { type: String } } ); // 新增post save中间件 commentsViewSchema.post('save', async function (doc, next) { try { // 拿到刚插入的浏览记录对应的评论uid const targetUid = doc.uid; // 更新Posts集合中对应评论的浏览量 await Post.updateMany( // 筛选包含对应uid评论的Post文档 { 'comments.uid': targetUid }, // 对应评论的totalViews自增1 { $inc: { 'comments.$[targetComment].totalViews': 1 } }, // 数组筛选器,定位到uid匹配的评论子文档 { arrayFilters: [ { 'targetComment.uid': targetUid } ] } ); next(); } catch (err) { next(err); } }); const CommentsView = mongoose.model('CommentsView', commentsViewSchema); module.exports = CommentsView;
注意:如果业务中会用到insertMany批量插入浏览记录,需要额外添加post('insertMany')钩子,默认save钩子不会触发批量插入的更新逻辑。
方案2:MongoDB Change Streams(数据库层面监听,全场景覆盖)
适用场景:存在多个服务/多个入口会写入CommentsView集合,应用层钩子无法覆盖所有写入操作的场景。
通过MongoDB自带的变更流监听集合的所有插入操作,触发更新逻辑:
const Post = require('./models/post'); const CommentsView = require('./models/commentsView'); async function startViewSyncListener() { // 开启CommentsView集合的变更流监听 const changeStream = CommentsView.watch(); changeStream.on('change', async (changeEvent) => { // 只处理插入操作 if (changeEvent.operationType === 'insert') { const insertedRecord = changeEvent.fullDocument; const targetUid = insertedRecord.uid; // 和方案1相同的更新逻辑 await Post.updateMany( { 'comments.uid': targetUid }, { $inc: { 'comments.$[targetComment].totalViews': 1 } }, { arrayFilters: [ { 'targetComment.uid': targetUid } ] } ); } }); } // 应用启动时调用该函数开启监听 startViewSyncListener();
注意:该方案要求MongoDB运行在副本集或分片集群模式,单机部署模式不支持Change Streams特性。
方案3:定时兜底校验(保证最终一致性)
作为前两种方案的补充,避免服务宕机、中间件报错等异常场景导致的数据不一致,定期对齐两个集合的数据:
const schedule = require('node-schedule'); const Post = require('./models/post'); const CommentsView = require('./models/commentsView'); async function syncAllCommentViews() { // 统计所有评论的总浏览量 const viewStatistics = await CommentsView.aggregate([ { $group: { _id: '$uid', totalViews: { $sum: 1 } } } ]); // 批量更新Posts集合的浏览量字段 for (const stat of viewStatistics) { await Post.updateMany( { 'comments.uid': stat._id }, { $set: { 'comments.$[targetComment].totalViews': stat.totalViews } }, { arrayFilters: [ { 'targetComment.uid': stat._id } ] } ); } } // 示例:每天凌晨2点执行一次全量同步 schedule.scheduleJob('0 0 2 * * *', syncAllCommentViews);
额外注意事项
- 首次上线前如果已经存在历史数据,先执行一次
syncAllCommentViews对齐历史浏览量,再开启钩子或变更流监听。 - 对数据强一致性要求高的场景,可以将
CommentsView插入和Posts更新的逻辑放在MongoDB事务中执行,避免单步操作失败导致的数据偏差,事务同样要求MongoDB运行在副本集或分片集群模式。
内容的提问来源于stack exchange,提问作者s.khan
相关产品推荐
相关产品推荐

