如何在MongoDB中设计自增注册号系统并规避并发风险
MongoDB 并发安全的自增注册号实现方案
MongoDB 本身没有内置类似关系型数据库的自增主键字段能力,很多人一开始图省事直接在业务代码里先查当前最大注册号、加1再写入,这种写法在并发场景下极容易出现重复注册号的问题——两个请求同时查到当前最大值是1,都会生成2写入,直接产生脏数据。
正确实现思路
核心是靠MongoDB的单文档原子更新能力,单独维护一个计数器集合,把「取值+加1」两个动作合并成一个原子操作完成,从引擎层面避免并发冲突。
- 第一步:创建专用计数器集合
单独建一个counters集合存所有业务的自增序列值,初始化对应注册号序列的初始文档:
// 初始化注册号序列,初始已分配的最大注册号为1 db.counters.insertOne({ _id: "reg_no", // 序列唯一标识,不同业务用不同id区分 seq: 1 })
- 第二步:原子获取下一个注册号
每次生成新注册号前,调用findOneAndUpdate原子方法,在服务端一次性完成序列号加1、返回新值的操作,这个过程是服务端原子执行的,不会被其他请求打断,哪怕高并发场景下也不会生成重复值:
// 生成下一个可用注册号 function getNextRegNo() { const result = db.counters.findOneAndUpdate( { _id: "reg_no" }, { $inc: { seq: 1 } }, // 原子自增1 { returnNewDocument: true } // 直接返回自增后的新值 ) return result.seq }
对应需求示例场景:当前计数器里seq值是1,第一次调用方法会直接把seq更新为2,返回2给新插入的注册记录使用,全程没有并发冲突的可能。
避坑提醒
- 不要用「先查最大记录+1再插入」的写法:查询和写入是两个独立操作,中间没有任何锁保护,并发下必然出现重复值。
- 不要分开写更新和查询:先执行
$inc更新再单独查seq值,两个操作中间可能被其他请求插入,拿到的序列号不准确。 - 多业务场景不需要建多个计数器集合:只要在
counters里给不同业务分配不同的_id即可,比如订单号、用户ID都可以存在同一个集合里,序列之间互不影响。
如果使用MongoDB 4.0+的事务能力,也可以在事务里完成取值和写入,但单文档原子更新的方案性能比事务高3~5倍,是生产环境最常用的实现方式。
内容的提问来源于stack exchange,提问作者TechOverflow
相关产品推荐
相关产品推荐

