在Promise.all中批量创建Invoice时如何实现编号递增?
解决批量创建Invoice时编号重复的问题
问题核心在于Promise.all的并发特性:所有创建Invoice的请求同时发起,会同时读取数据库中同一个"最后发票编号",后续的number++只是内存中的操作,无法同步到并发的其他请求,最终导致所有发票使用相同编号。
以下是几种可行的解决方法:
1. 依赖数据库自增机制(最推荐)
直接利用数据库的自增特性生成唯一编号,避免手动维护编号的并发问题。如果需要带格式的编号(如INV-2024-0001),可以通过模型钩子结合自增ID生成:
示例代码:
// Invoice 模型定义 const Invoice = sequelize.define('Invoice', { id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true }, invoiceNumber: { type: DataTypes.STRING, allowNull: false, unique: true }, // 其他字段... }, { hooks: { afterCreate: async (invoice) => { // 生成带格式的编号,基于自增ID const year = new Date().getFullYear(); const formattedNumber = `INV-${year}-${String(invoice.id).padStart(4, '0')}`; await invoice.update({ invoiceNumber: formattedNumber }); } } });
这种方式完全由数据库保证编号的唯一性和递增性,无需手动处理并发。
2. 串行创建而非并发(适合小批量场景)
放弃Promise.all的并发执行,改用串行循环逐个创建Invoice,确保每次创建前都能获取到最新的编号:
示例代码:
async function batchCreateInvoices(invoiceDataList) { const transaction = await sequelize.transaction(); try { for (const data of invoiceDataList) { // 获取当前最新发票编号 const lastInvoice = await Invoice.findOne({ order: [['invoiceNumber', 'DESC']], transaction }); let nextNumber = 1; if (lastInvoice) { // 解析编号中的数字部分并加1(假设编号格式为 INV-2024-0001) const numPart = parseInt(lastInvoice.invoiceNumber.split('-')[2], 10); nextNumber = numPart + 1; } const formattedNumber = `INV-${new Date().getFullYear()}-${String(nextNumber).padStart(4, '0')}`; // 创建发票 await Invoice.create({ ...data, invoiceNumber: formattedNumber }, { transaction }); // 同步更新Packages、发送邮件 await updatePackage(data.packageId, { status: 'invoiced' }, transaction); await sendInvoiceEmail(formattedNumber, data.email); } await transaction.commit(); } catch (err) { await transaction.rollback(); throw err; } }
这种方式虽然牺牲了一点并发性能,但彻底避免了编号重复问题,适合批量不大的场景。
3. 事务内加锁保证原子性
如果必须保留并发创建(或批量较大),可以在获取最新编号时加数据库锁,确保同一时间只有一个请求能读取并更新编号:
示例代码:
async function createInvoiceWithLock(data, transaction) { // 加锁获取最新发票,阻止其他事务读取 const lastInvoice = await Invoice.findOne({ order: [['invoiceNumber', 'DESC']], lock: true, // 行锁,不同数据库语法可能略有差异 transaction }); // 生成下一个编号逻辑同前 let nextNumber = 1; if (lastInvoice) { const numPart = parseInt(lastInvoice.invoiceNumber.split('-')[2], 10); nextNumber = numPart + 1; } const formattedNumber = `INV-${new Date().getFullYear()}-${String(nextNumber).padStart(4, '0')}`; return await Invoice.create({ ...data, invoiceNumber: formattedNumber }, { transaction }); } // 批量创建时仍用Promise.all,但每个创建操作都在事务内加锁 async function batchCreateInvoices(invoiceDataList) { const transaction = await sequelize.transaction(); try { const promises = invoiceDataList.map(data => createInvoiceWithLock(data, transaction).then(async (invoice) => { await updatePackage(data.packageId, { status: 'invoiced' }, transaction); await sendInvoiceEmail(invoice.invoiceNumber, data.email); }) ); await Promise.all(promises); await transaction.commit(); } catch (err) { await transaction.rollback(); throw err; } }
注意:数据库锁可能会影响性能,需根据业务场景评估。
内容的提问来源于stack exchange,提问作者NaguiHW
相关产品推荐
相关产品推荐

