You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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的性能损耗:

  1. 先创建序列表:
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;
  1. 修改触发器逻辑,从序列表取号:
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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 15:51:27