点赞收藏必须用唯一索引+upsert,否则并发下会重复插入;计数应通过$inc原子更新分离行为与统计;收藏列表宜嵌入式存储并限制规模,超限改用引用模式。

直接用 updateOne + upsert: true 做点赞/收藏,高并发下大概率产生重复记录或丢失计数——这不是 bug,是设计没对齐 MongoDB 的原子性边界。
为什么点赞收藏不能只靠 upsert?
很多人把「用户 A 点赞文章 B」抽象成一条 { userId: "A", articleId: "B", type: "like" } 记录,然后用 upsert 插入。问题在于:upsert 的「判断存在」和「插入」不是原子的——两个并发请求同时查不到记录,就会各自插入一条。
更隐蔽的问题是:如果业务要求「同一用户只能点一次赞」,那必须依赖唯一索引强制约束,否则靠应用层 if-else 毫无意义。
- 错误做法:先
find再insert或update—— 中间存在竞态窗口 - 错误做法:只建普通索引,不建唯一索引 ——
upsert无法阻止重复插入 - 正确前提:唯一索引必须覆盖所有判重字段,例如
{ userId: 1, articleId: 1, type: 1 }
点赞计数怎么保证实时且不锁表?
别在每次点赞时去 find 当前数量再 $inc。直接用 $inc 更新计数字段,MongoDB 对单文档的 $inc 是原子的,且不依赖事务。
关键设计是把「用户行为」和「聚合统计」分离:
- 行为记录存单独集合(如
user_actions),带唯一索引{ userId: 1, articleId: 1, type: 1 } - 计数字段放在文章文档里,例如
articles集合中的likeCount字段 - 用户点赞时,先尝试插入行为记录(
upsert+ 唯一索引),成功后再updateOne({ _id: articleId }, { $inc: { likeCount: 1 } })
注意:第二个更新操作要加 upsert: false,避免意外创建空文档;同时建议设置 writeConcern: "majority" 防止主从切换丢计数。
收藏夹列表查询慢?试试嵌入式 + 分页游标
如果用户收藏了上万条内容,用 skip(1000).limit(20) 分页会越来越慢——MongoDB 要跳过前 1000 条才能取 20 条。
替代方案是把收藏 ID 列表嵌入用户文档:{ _id: "userId", favorites: ["articleId1", "articleId2", ...] },但要注意数组不能无限增长。
- 限制单个用户最多收藏 5000 条,超限时走引用模式(另建
user_favorites集合) - 查询时用
$lookup关联文章信息,但仅限小规模分页( - 大规模场景改用游标分页:
find({ userId: "A", _id: { $gt: lastId } }).limit(20),依赖_id或时间戳有序字段
别忘了给游标字段建索引,比如 { userId: 1, createdAt: -1 }。
高并发下最容易被忽略的三个点
一是唯一索引的字段顺序影响查询效率——{ userId: 1, type: 1, articleId: 1 } 和 { userId: 1, articleId: 1, type: 1 } 在不同查询条件下性能差异可达 10 倍;
二是 updateOne 的 upsert 选项在高并发下失败率会上升,需捕获 DuplicateKeyError 并降级处理(比如只返回「已存在」);
三是不要在事务里做点赞+发通知这类跨服务操作——事务内调外部 API 是死锁高发区,应改为异步消息队列解耦。











