MongoDB并发读写处理:Mongoose下文档锁问题咨询
关于MongoDB并发访问与锁机制的解答
1. 你的当前实现无需额外加锁
你使用的findOneAndUpdate属于单文档原子操作,MongoDB会保证该操作的查询条件(numAvailableProduct:{$gt:0})与更新逻辑($inc:{numAvailableProduct:-1})是一个不可分割的整体。即便连接池(maxPoolSize)允许同时发起多个请求,MongoDB也会对同一文档的写入操作做串行化处理:
- 第一个匹配到
numAvailableProduct>0的请求会立即执行减1操作,全程不会被其他请求打断 - 后续请求查询时,
numAvailableProduct已经是更新后的值,不满足条件则不会执行更新
因此不会出现“读取旧值导致超卖”的问题,你不需要额外锁定文档、集合或数据库。
2. 何时需要手动实现锁机制?
只有当你需要处理跨文档的原子操作(比如同时修改订单和库存两个文档),或者业务逻辑要求更严格的并发控制时,才需要手动实现锁。常见的两种实现方式:
乐观锁(推荐,无阻塞)
基于版本号控制,每次更新时校验版本,不匹配则说明有其他请求修改过,需重试。在Mongoose中的实现步骤:
- 给Schema添加版本字段(Mongoose默认自带
__v字段,也可自定义):
const ProductSchema = new Schema({ name: String, numAvailableProduct: Number, customer: [{name: String}], __v: { type: Number, default: 0 } });
- 更新时带上版本号匹配:
async function purchaseProduct(productName) { let result; while (!result) { const product = await ProductModel.findOne({ name: productName, numAvailableProduct: { $gt: 0 } }); if (!product) break; // 匹配当前版本,更新库存同时递增版本号 result = await ProductModel.findOneAndUpdate( { name: productName, __v: product.__v }, { $inc: { numAvailableProduct: -1 }, $inc: { __v: 1 } }, { new: true } ); } return result; }
并发请求中只有第一个匹配到版本号的会成功,其他请求返回null,通过循环重试即可。
悲观锁(阻塞式,慎用)
通过给文档添加锁定标记,先锁定再操作,处理完成后解锁,适合并发量小但对一致性要求极高的场景:
- 给Schema添加锁定与过期字段:
const ProductSchema = new Schema({ name: String, numAvailableProduct: Number, customer: [{name: String}], isLocked: { type: Boolean, default: false }, lockExpires: { type: Date } // 防止死锁,设置锁过期时间 });
- 实现锁定-操作-解锁流程:
async function purchaseWithLock(productName) { // 尝试锁定文档,过期锁自动释放 const lockedProduct = await ProductModel.findOneAndUpdate( { name: productName, numAvailableProduct: { $gt: 0 }, $or: [ { isLocked: false }, { lockExpires: { $lt: new Date() } } ] }, { $set: { isLocked: true, lockExpires: new Date(Date.now() + 10000) }, $inc: { numAvailableProduct: -1 } }, { new: true } ); if (!lockedProduct) return null; try { // 执行业务逻辑,比如添加购买用户 await ProductModel.findOneAndUpdate( { name: productName }, { $push: { customer: { name: 'testCustomer' } } } ); } finally { // 无论操作成功与否,释放锁 await ProductModel.findOneAndUpdate( { name: productName }, { $set: { isLocked: false } } ); } return lockedProduct; }
注意:悲观锁易引发死锁,必须添加过期时间,且确保在finally块中释放锁。
总结
- 你的商品扣减场景,
findOneAndUpdate的原子操作完全满足需求,无需额外锁 - 跨文档操作或特殊业务需求时,优先选择乐观锁,悲观锁需谨慎使用
内容的提问来源于stack exchange,提问作者Nick
相关产品推荐
相关产品推荐

