使用.map()批量生成订单ID重复,异步DB更新未等待问题
生成唯一订单ID的并发问题及解决方法
问题背景
需要为cartData中的每个商品生成唯一orderId,但使用.map()结合Promise.all迭代时,.map()不会等待updateOrderNo()完成数据库的orderNo值更新,导致第二次迭代获取到的orderNo与第一次相同,生成重复订单ID。
相关代码
createOrder 函数
export const createOrder = () => { const result = new Promise(async (resolve, reject) => { try { const cartData = { cartId: 1234, items: [ { name: "One" }, { name: "Two" } ] }; await Promise.all(cartData.items.map(async (item, index) => { const orderNumber = await getNewOrderNo(); console.log("index", index, "orderNumber", orderNumber); const OrderData = { orderId: orderNumber, name: item.name, subTotalAmount: 1000, discountAmount: 0, shippingChargeAmount: 10, totalAmount: 1010, }; const newOrderData = new Order(OrderData); const addedOrder = await newOrderData.save(); console.log(`addedOrder - index ${index} ::::`, addedOrder.orderId); if (addedOrder) { const updatedOrderNo = await updateOrderNo(); } })); resolve("success"); } catch (error) { console.error('createOrder catch error', error); reject(new Error(error)); } }); return result; };
getNewOrderNo 函数
export const getNewOrderNo = async () => { try { const order = await Setting.findOne({ key: "order_no" }); var str = "" + (order.value + 1); var pad = "0000"; var ans = pad.substring(0, pad.length - str.length) + str; return order.prefix + "-" + ans; } catch (error) { console.error('getOrderNo catch error :::', error); } };
updateOrderNo 函数
export const updateOrderNo = async () => { const result = new Promise(async (resolve, reject) => { try { const order = await Setting.findOne({ key: "order_no" }); const newOrderNumber = order.value + 1; const updateOrderNumber = await Setting.updateOne( { key: "order_no" }, { $set: { value: newOrderNumber } } ); if (updateOrderNumber.nModified == 1) { resolve(newOrderNumber); } else { console.error(`order_no is not updated in db`); } } catch (error) { console.error('updateOrderNo catch error :::', error); reject('updateOrderNo failed', error); } }); return result; };
MongoDB Setting集合数据
{ "_id": "607840696826c3ca20a491fb", "key": "order_no", "value": 1000, "prefix": "ABC", }
当前控制台输出
index 0 orderNumber ABC-1001 index 1 orderNumber ABC-1001 ::: addedOrder - index 0 :::: ABC-1001 ::: addedOrder - index 1 :::: ABC-1001
期望控制台输出
index 0 orderNumber ABC-1001 index 1 orderNumber ABC-1002 ::: addedOrder - index 0 :::: ABC-1001 ::: addedOrder - index 1 :::: ABC-1002
问题原因
Promise.all会同时触发所有.map()中的异步函数,两个请求几乎同时调用getNewOrderNo,此时数据库中的orderNo还未被第一个请求更新,导致两个请求都读取到value=1000,生成相同的ABC-1001订单号,之后才各自执行更新操作,最终出现重复ID。
解决方案
核心优化:原子操作替代拆分读写
将getNewOrderNo和updateOrderNo合并为一个原子操作,使用MongoDB的findOneAndUpdate方法,保证读取订单号和更新订单号的操作是原子性的,避免并发场景下的重复读取。
优化后的原子操作函数
export const getAndUpdateOrderNo = async () => { try { // 原子操作:找到文档并自增value,返回更新前的文档 const order = await Setting.findOneAndUpdate( { key: "order_no" }, { $inc: { value: 1 } }, // 原子自增,避免并发冲突 { new: false } // 返回更新前的文档,用于计算新订单号 ); if (!order) { throw new Error("未找到order_no配置项"); } const nextValue = order.value + 1; const str = "" + nextValue; const pad = "0000"; const ans = pad.substring(0, pad.length - str.length) + str; return order.prefix + "-" + ans; } catch (error) { console.error('getAndUpdateOrderNo 错误 :::', error); throw error; } };
方案一:保持并行处理(原子操作支持并发)
修改createOrder函数,直接使用原子操作函数,此时即使并行处理也不会生成重复订单ID:
export const createOrder = async () => { try { const cartData = { cartId: 1234, items: [ { name: "One" }, { name: "Two" } ] }; await Promise.all(cartData.items.map(async (item, index) => { const orderNumber = await getAndUpdateOrderNo(); console.log("index", index, "orderNumber", orderNumber); const OrderData = { orderId: orderNumber, name: item.name, subTotalAmount: 1000, discountAmount: 0, shippingChargeAmount: 10, totalAmount: 1010, }; const newOrderData = new Order(OrderData); const addedOrder = await newOrderData.save(); console.log(`addedOrder - index ${index} ::::`, addedOrder.orderId); })); return "success"; } catch (error) { console.error('createOrder 错误', error); throw new Error(error); } };
方案二:串行处理(严格按顺序生成订单号)
如果需要严格按照商品顺序生成递增的订单号,可以改用for...of循环串行处理:
export const createOrder = async () => { try { const cartData = { cartId: 1234, items: [ { name: "One" }, { name: "Two" } ] }; for (const [index, item] of cartData.items.entries()) { const orderNumber = await getAndUpdateOrderNo(); console.log("index", index, "orderNumber", orderNumber); const OrderData = { orderId: orderNumber, name: item.name, subTotalAmount: 1000, discountAmount: 0, shippingChargeAmount: 10, totalAmount: 1010, }; const newOrderData = new Order(OrderData); const addedOrder = await newOrderData.save(); console.log(`addedOrder - index ${index} ::::`, addedOrder.orderId); } return "success"; } catch (error) { console.error('createOrder 错误', error); throw new Error(error); } };
说明
- 原代码的问题在于读取和更新订单号是两个独立操作,并发时会出现竞态条件。
- 使用
findOneAndUpdate的$inc操作符可以保证数据库层面的原子性,无论多少并发请求,每次获取的订单号都是唯一递增的。
内容的提问来源于stack exchange,提问作者Gowtham
相关产品推荐
相关产品推荐

