MongoDB避免更新时脏读及Node+Mongoose钱包API数据一致性方案
Node + Mongoose 钱包API数据一致性解决方案
1. 读取Wallet文档时余额被更新,确保获取最新值
避免复用Mongoose文档缓存
Mongoose的Document实例会缓存查询结果,若后续文档被其他请求更新,旧实例仍保留旧值。每次需要最新数据时,直接重新执行查询,不要复用之前的Document对象:
// 错误:复用旧实例导致读取旧值 const oldWallet = await Wallet.findById(walletId); // 其他请求更新余额后,oldWallet仍为旧数据 // 正确:每次查询获取最新数据,用lean()返回普通对象提升性能 const getLatestWallet = async (walletId) => { return await Wallet.findById(walletId).lean(); };
原子更新+返回最新值
如果是更新操作后需要最新余额,直接用findOneAndUpdate并设置new: true,一步完成原子更新和最新数据读取,避免先读后写的竞态问题:
// 原子扣减余额并返回更新后的钱包数据 const updatedWallet = await Wallet.findOneAndUpdate( { _id: walletId }, { $inc: { balance: -amount } }, // MongoDB原子操作,避免并发冲突 { new: true, runValidators: true } );
事务确保读写一致性
针对先读再写的复杂操作(如余额校验后扣减),使用Mongoose事务将查询和更新绑定在同一个会话中,确保读取的是最新数据:
const session = await mongoose.startSession(); session.startTransaction(); try { // 事务内查询,保证数据未被其他会话修改 const wallet = await Wallet.findById(walletId).session(session); if (!wallet) throw new Error('钱包不存在'); // 执行业务逻辑(如余额校验) if (wallet.balance < amount) throw new Error('余额不足'); wallet.balance -= amount; await wallet.save({ session }); await session.commitTransaction(); } catch (err) { await session.abortTransaction(); throw err; } finally { session.endSession(); }
2. 读取文档时钱包被删除,确保获取正确信息
强制检查查询结果
无论使用哪种查询方式,都必须判断返回结果是否为null,以此确认钱包是否已被删除:
const wallet = await Wallet.findById(walletId); if (!wallet) { throw new Error('钱包已删除或不存在'); } // 后续业务逻辑基于存在的钱包执行
乐观锁防止过期操作
给Wallet Schema添加version字段,每次更新时递增版本号,查询或更新时校验版本号,避免对已删除/修改的旧数据进行操作:
// 定义Schema时添加版本字段 const walletSchema = new mongoose.Schema({ balance: { type: Number, default: 0 }, version: { type: Number, default: 0 } }); // 更新时校验版本号,确保数据未被篡改或删除 const updatedWallet = await Wallet.findOneAndUpdate( { _id: walletId, version: currentVersion }, { $inc: { balance: -amount, version: 1 } }, { new: true } ); if (!updatedWallet) { throw new Error('钱包数据已过期或已删除'); }
事务内的存在性校验
在事务中先查询钱包是否存在,再执行后续操作,利用数据库事务的隔离性,避免查询后钱包被删除的情况:
const session = await mongoose.startSession(); session.startTransaction(); try { const wallet = await Wallet.findById(walletId).session(session); if (!wallet) { throw new Error('钱包已删除'); } // 执行业务操作 await wallet.save({ session }); await session.commitTransaction(); } catch (err) { await session.abortTransaction(); throw err; } finally { session.endSession(); }
内容的提问来源于stack exchange,提问作者ritwick_ _D
相关产品推荐
相关产品推荐

