多系统并发curl POST批量录入订单时出现重复发票编号问题咨询
问题根源
你的所有现有实现都存在竞态条件问题:多个并发请求会同时查询到相同的最新订单编号,各自加1后生成了重复的OrderNumber,这是重复插入的核心原因。
你之前的方案失效原因如下:
- PHP端处理逻辑:先提交事务再判断订单号是否存在,此时其他并发请求可能已经插入了相同编号,即使后续重新生成更新,仍然存在并发冲突概率,且效率极低。
- 触发器逻辑:BEFORE INSERT中查询最新编号的语句没有加锁,多个并发插入的触发器会同时读取到同一个LastOrderNo,自然生成重复编号。你添加唯一索引后插入直接报错,本质就是这个并发冲突被唯一索引拦截了。
可行解决方案
方案1:MySQL独立序列表(最稳定,推荐)
利用MySQL自增主键的原子性完全规避竞态,实现成本低、稳定性高:
- 新建专门用于生成订单序号的序列表:
CREATE TABLE order_sequence ( id INT NOT NULL AUTO_INCREMENT PRIMARY KEY ) ENGINE=InnoDB;
- 修改插入触发器逻辑,用序列表的自增ID作为订单号的递增序列:
CREATE TRIGGER `Insert_OrderNumber` BEFORE INSERT ON `tOrder` FOR EACH ROW BEGIN -- 插入序列表拿到全局唯一自增ID INSERT INTO order_sequence VALUES (NULL); SET @seq = LPAD(2245444, 6, '0'); -- 前缀可根据你的业务规则调整,比如示例中的ULEN、UCMC前缀、年份拼接等逻辑 SET NEW.OrderNumber = CONCAT('UABC', DATE_FORMAT(NOW(), '%y'), @seq); END
- 给
tOrder表的OrderNumber字段添加唯一索引作为兜底校验,避免极端场景出错。
方案2:PHP端加分布式锁
如果有Redis可用,可以通过分布式锁保证同一时间只有一个请求生成订单号:
foreach($Orders as $order) { $lockKey = 'order_number_generate_lock'; // 自旋抢锁,锁过期时间设为10秒足够处理插入逻辑 while (!$redis->set($lockKey, 1, ['nx', 'ex' => 10])) { usleep(10000); // 间隔10ms重试 } try { $insArr =[ 'OrderNumber' => $this->GenerateOrderNo(), 'CreatedBy' => 1, ]; $this->db->insert('tOrder',$insArr); } finally { // 执行完释放锁 $redis->del($lockKey); } }
方案3:MySQL行级锁实现原子生成
如果不想新增表也不想依赖Redis,可以在查询最新编号时加排他锁,放在事务内执行:
foreach($Orders as $order) { $this->db->trans_begin(); try { // 加FOR UPDATE排他锁,其他事务必须等当前事务提交后才能查询最新编号 $lastNo = $this->db->query("SELECT Right(OrderNumber,6) as no FROM tOrder ORDER BY OrderUID DESC LIMIT 1 FOR UPDATE")->row()->no; $newNo = str_pad($lastNo + 1, 6, '0', STR_PAD_LEFT); // 拼接前缀逻辑按业务需求调整 $orderNo = 'UABC' . date('y') . $newNo; $insArr =[ 'OrderNumber' => $orderNo, 'CreatedBy' => 1, ]; $this->db->insert('tOrder',$insArr); $this->db->trans_commit(); } catch (Exception $e) { $this->db->trans_rollback(); // 可按需添加重试逻辑 } }
注意该方案需要数据库事务隔离级别至少为READ COMMITTED,否则行锁可能不生效。
内容的提问来源于stack exchange,提问作者DSP
相关产品推荐
相关产品推荐

