Promise.all并发插入MySQL时BEFORE INSERT触发器仅触发一次问题
问题描述
BEFORE INSERT触发器按预期应当在每一条新增数据行插入前触发,同步执行插入操作时该逻辑运行正常。由于待处理数据量较大,改用异步方式执行插入后出现异常:
- 复现场景:插入2条交易头数据,预期触发器触发2次,为每条记录生成独立的交易单号,最终两条记录仅生成了同一个单号。
- 问题截图:

现有实现代码
表触发器定义
CREATE DEFINER=`root`@`%` TRIGGER `generate_transaction_number` BEFORE INSERT ON `transaction` FOR EACH ROW BEGIN DECLARE v_max_transaction_num VARCHAR(17); DECLARE v_separator VARCHAR(1) DEFAULT '/'; SELECT MAX(transaction_number) INTO v_max_transaction_num from transaction t WHERE DATE(t.transaction_date) = CURDATE(); IF(v_max_transaction_num IS NULL OR v_max_transaction_num = '') THEN SET v_max_transaction_num = '0'; END IF; SET new.transaction_number = concat('INV', v_separator, DATE_FORMAT(CURDATE(),'%Y%m%d'), v_separator, LPAD(SUBSTRING_INDEX(v_max_transaction_num,v_separator,-1)+1,4,'0')); END
异步插入JS代码
let promises = []; for (const transactionHeader of transactionHeaders){ promises.push(mTransactions.addTransaction(transactionHeader)); } await Promise.allSettled(promises);
实现目标
在异步插入数据的场景下,保证触发器对每一条插入行正常触发执行,生成不重复的交易单号。
问题根因
触发器逻辑本身没有语法问题,单号重复的核心原因是并发插入时的读一致性问题:
- 同步插入是串行执行,前一条插入完成提交后,后一条插入的触发器才会执行
SELECT MAX查询,能读到最新的最大单号,所以生成的单号不会重复。 - 异步
Promise.allSettled会并发发起多个插入请求,多个插入操作的触发器几乎在同一时间执行SELECT MAX查询,此时其他并发插入的记录还未提交,所有查询拿到的都是同一个当前最大单号,最终计算出的新单号完全一致。 - 触发器内的查询没有加锁,无法阻塞其他并发事务读取相同的最大值,是问题出现的直接诱因。
解决方案
方案1:调整触发器逻辑加排他锁(改动最小)
修改触发器,在查询最大单号时加行级排他锁,保证同一时间只有一个事务能读取并计算单号,其他事务会等待锁释放后读到最新提交的最大值。修改后的触发器代码如下:
CREATE DEFINER=`root`@`%` TRIGGER `generate_transaction_number` BEFORE INSERT ON `transaction` FOR EACH ROW BEGIN DECLARE v_max_transaction_num VARCHAR(17); DECLARE v_separator VARCHAR(1) DEFAULT '/'; -- 加FOR UPDATE加排他锁,配合事务隔离级别避免并发重复读 SELECT MAX(transaction_number) INTO v_max_transaction_num from transaction t WHERE DATE(t.transaction_date) = CURDATE() FOR UPDATE; IF(v_max_transaction_num IS NULL OR v_max_transaction_num = '') THEN SET v_max_transaction_num = '0'; END IF; SET new.transaction_number = concat('INV', v_separator, DATE_FORMAT(CURDATE(),'%Y%m%d'), v_separator, LPAD(SUBSTRING_INDEX(v_max_transaction_num,v_separator,-1)+1,4,'0')); END
注意:该方案需要保证数据库的事务隔离级别为READ COMMITTED及以上,同时插入操作必须在事务中执行,锁会在事务提交后自动释放。
方案2:使用独立序列表生成单号(高并发场景推荐)
单独建一张日单号序列表,记录当天当前的最大流水号,生成单号时直接对序列表做原子更新+读操作,利用InnoDB的行锁保证序列递增,不会出现重复,同时避免全表扫描transaction表查MAX的性能损耗:
- 先创建序列表:
CREATE TABLE `transaction_seq` ( `seq_date` date NOT NULL, `current_num` int NOT NULL DEFAULT 0, PRIMARY KEY (`seq_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
- 修改触发器逻辑,从序列表取号:
CREATE DEFINER=`root`@`%` TRIGGER `generate_transaction_number` BEFORE INSERT ON `transaction` FOR EACH ROW BEGIN DECLARE v_current_num INT; DECLARE v_separator VARCHAR(1) DEFAULT '/'; -- 当天无序列记录则插入初始值,有则原子更新流水号+1,行锁保证并发安全 INSERT INTO transaction_seq(seq_date, current_num) VALUES (CURDATE(), 1) ON DUPLICATE KEY UPDATE current_num = current_num + 1; -- 读取更新后的最新流水号 SELECT current_num INTO v_current_num FROM transaction_seq WHERE seq_date = CURDATE(); SET new.transaction_number = concat('INV', v_separator, DATE_FORMAT(CURDATE(),'%Y%m%d'), v_separator, LPAD(v_current_num,4,'0')); END
方案3:调整JS端为串行插入(临时兜底方案)
把异步并发插入改成串行插入,等上一条插入完成返回后再发起下一条插入请求,本质和同步插入逻辑一致,能避免单号重复,但插入性能会大幅下降,不符合异步批量处理的初衷:
// 串行执行插入 for (const transactionHeader of transactionHeaders){ await mTransactions.addTransaction(transactionHeader); }
内容的提问来源于stack exchange,提问作者Shira Ishikawa
相关产品推荐
相关产品推荐

