最新下载
热门教程
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
MongoDB如何设计活动报名系统:通过唯一索引与乐观锁防止名额超征
时间:2026-06-30 09:36:47 编辑:袖梨 来源:一聚教程网
必须为 activity_registration 集合创建复合唯一索引 { user_id: 1, activity_id: 1 },以原子性防止同一用户重复报名同一活动;单字段索引无效,且建索引前须清理历史重复数据,否则 createIndex 会因 E11000 错误失败。
MongoDB 本身不支持行级锁或 SELECT FOR UPDATE,所谓“乐观锁”在报名场景中不能照搬 MySQL 那套逻辑;真正起作用的是唯一索引 + 原子写入 + 应用层重试机制。别指望靠 findAndModify 或版本号字段拦住并发超报——它拦不住,除非你把约束提前压到索引层。
为什么必须用唯一索引防重复报名
活动报名最核心的并发问题不是“抢到最后一个名额”,而是“同一用户报同一活动多次”。MongoDB 没有外键、没有事务回滚(跨文档)、也没有隔离级别控制,唯一能靠得住的数据库级防线就是唯一索引。
-
activity_registration集合必须建复合唯一索引:{ user_id: 1, activity_id: 1 },而不是只对user_id或activity_id单独建索引 - 如果漏掉这个索引,高并发下插入两条相同
{ user_id: 123, activity_id: 456 }的文档,MongoDB 默认允许,直到应用层查重才发现——此时名额已超,且数据已脏 - 注意:索引创建前,必须确保集合里没有历史重复数据,否则
createIndex(..., { unique: true })会直接失败并报错E11000 duplicate key error
如何用原子操作校验并扣减剩余名额
“扣减名额”不是先读再改,而是用 updateOne 的 $inc 和 $gte 条件一次性完成判断与更新,避免竞态。MongoDB 的原子性只保证单文档操作,所以名额字段必须存在活动主文档里(activity 集合),不能拆到报名表中计算。
- 活动文档结构示例:
{ _id: ObjectId("..."), name: "春游", capacity: 100, registered_count: 42, status: "published" } - 报名时执行:
db.activity.updateOne({ _id: activityId, status: "published", registered_count: { $lt: capacity } }, { $inc: { registered_count: 1 } }) - 检查返回的
result.matchedCount:为 1 表示名额充足且已扣减;为 0 表示已满或状态异常,此时不应插入报名记录 - 切勿用
find+updateOne两步走——中间可能被其他请求抢占
报名写入与唯一索引冲突的处理方式
即使做了上述校验,仍可能因网络重试、前端重复提交等导致插入 activity_registration 时触发唯一索引报错。这不是 bug,是预期行为,必须在代码里显式捕获并处理。
- 错误信息固定为:
WriteError: E11000 duplicate key error collection: db.activity_registration index: user_id_1_activity_id_1 dup key: { user_id: 123, activity_id: 456 } - 捕获该错误后,应直接返回“您已报名”,而不是抛异常或提示“系统错误”
- 不要尝试先
findOne再插入来“规避”错误——这又回到非原子操作的老路,且增加一次查询开销 - 如果业务允许“取消后重报”,则插入前可加
upsert: true并设置status: "registered",但需确保更新操作也带条件(如仅当当前状态非canceled时才生效)
分片集群下唯一索引的特殊限制
如果你的 activity 或 activity_registration 集合启用了分片,唯一索引就不是无脑加了。MongoDB 要求:任何唯一索引,其字段组合必须包含分片键(shard key)作为前缀,否则创建失败。
- 例如,若
activity_registration按activity_id分片,则{ user_id: 1, activity_id: 1 }可以建唯一索引(因为activity_id是前缀);但{ user_id: 1 }单字段索引会被拒绝 - 这意味着:如果你计划按用户维度分片,那
user_id就必须出现在所有唯一索引中——设计初期就得定死分片策略,后期无法变更 - 线上环境建唯一索引时,务必确认是否在副本集/分片集群上;如果是,必须停写或使用滚动构建(rolling build),否则可能阻塞整个集合的写入
真正难的不是写几行 createIndex,而是在分片、扩容、多服务共写同一个集合时,依然让唯一性约束不被绕过。索引建错一次,修复成本远高于初期多想五分钟。
相关文章
- 3.3 生成创新点:稳妥不夸张 07-02
- AI概念短片 07-02
- 皮影戏AI动画 07-02
- 农村旧房子原基础改造 07-02
- 用精准的语言来描述图片 07-02
- GPT human author 07-02