MongoDB实现帖子投票功能需4次查询,如何优化执行效率?
优化方案
你当前流程的冗余点主要有两处:一是先查询投票记录再分支做新增/更新,多了一次前置查询;二是更新帖子计数后重新读文档、计算rank再保存,多了一次帖子查询。这两步都可以用MongoDB原生的原子操作合并,最终有效投票仅需2次DB操作,重复投票仅需1次DB操作,性能提升的同时数据一致性更强。
1. 先补基础索引
给Votes集合创建{ userId: 1, postId: 1 }的唯一复合索引,从数据库层面阻止同一用户对同一帖子生成多条投票记录,不需要在应用层做前置判断。
Vote.index({ userId: 1, postId: 1 }, { unique: true })
2. 投票记录判断+增量计算合并为1次原子操作
用findOneAndUpdate + upsert + 聚合更新管道,直接在数据库层完成「是否存在投票记录、票型是否重复、帖子计数需要调整的增量值」的计算,不需要提前查Votes集合:
// 入参说明:postId为目标帖子ID,userId为当前操作用户ID,voteType=1代表upvote,-1代表downvote const voteRes = await Vote.findOneAndUpdate( { userId, postId }, [ { $set: { voteType: voteType, // 直接计算帖子投票分的调整增量: // 同票型重复投:增量为0 // 切换票型:增量为 新票型 - 旧票型(比如从-1切到1,增量为2,和你原有逻辑一致) // 首次投票:增量为当前票型值 scoreDelta: { $cond: { if: { $eq: ["$voteType", voteType] }, then: 0, else: { $ifNull: [{ $subtract: [voteType, "$voteType"] }, voteType] } } } } } ], { upsert: true, new: true } ) // 增量为0直接返回重复投票错误,无需执行后续逻辑 if (voteRes.scoreDelta === 0) { throw new Error("不能重复投同类型票") }
3. 帖子计数更新+rank计算合并为1次原子操作
拿到增量值后,不需要先$inc再查帖子、算rank、save,直接用聚合更新管道在一次DB请求里完成分数调整和rank计算:
// 以下rank计算逻辑可替换为你实际使用的排序算法,示例为简化版Reddit热榜算法 const updatedPost = await Post.findOneAndUpdate( { _id: postId }, [ { $set: { voteScore: { $add: ["$voteScore", voteRes.scoreDelta] }, rank: { $let: { vars: { newScore: { $add: ["$voteScore", voteRes.scoreDelta] }, postAgeHour: { $divide: [{ $subtract: [new Date(), "$createdAt"] }, 3600000] } }, in: { $divide: [ { $log10: { $max: [{ $abs: "$$newScore" }, 1] } }, { $pow: [{ $add: ["$$postAgeHour", 2] }, 1.5] } ] } } } } } ], { new: true } )
可选进阶优化
如果你的rank计算逻辑复杂,无法在Mongo聚合管道中实现(比如需要关联其他维度数据),不需要同步阻塞计算:投票接口更新完voteScore后可以直接返回前端,把rank计算任务丢到异步消息队列里延迟更新,几百毫秒的延迟对Feed流排序的用户感知几乎为零,还能进一步降低接口响应耗时。
不建议为了追求1次DB操作把投票记录内嵌到Posts文档里,会导致后续查询「用户历史投票记录」类需求的性能大幅下降,2次原子操作已经是兼顾所有业务场景的最优解。
内容的提问来源于stack exchange,提问作者Matthew Trent
相关产品推荐
相关产品推荐

