Laravel重复条目异常:订单编号自增逻辑问题排查求助
解决订单号重复(Duplicate entry)的问题
首先,你遇到的这个问题核心是并发场景下的竞态条件——当多个请求同时创建订单时,它们会同时执行Order::orderBy('created_at', 'DESC')->first(),拿到同一个“最新订单”,然后生成完全相同的order_nr,最后插入数据库时触发唯一约束的重复条目错误。另外,用created_at排序也存在隐患:如果两个订单在同一毫秒(或数据库时间精度范围内)创建,排序结果可能不准确,导致你拿到的不是真正最新的订单。
下面给你几个可行的解决方案,按推荐程度排序:
方案一:基于自增ID生成订单号(最稳妥,推荐)
数据库的自增ID本身就是全局唯一且严格递增的,完全可以利用它来生成order_nr,从根源上避免重复问题。你可以通过Laravel模型的created事件来实现:
// 在Order模型中添加 protected static function booted() { static::created(function ($order) { // 利用已生成的自增ID补零生成订单号 $order->order_nr = '#' . str_pad($order->id, 8, "0", STR_PAD_LEFT); // 用saveQuietly避免触发事件循环 $order->saveQuietly(); }); }
之后创建订单的代码可以简化成:
$order = new Order; $order->user_id = Auth()->id(); $order->sub_total = $subtotal; // 其他字段赋值 $order->save();
这种方式完全依赖数据库的自增机制,并发下绝对不会出现重复,还解决了原代码中“首次创建订单时latestOrder为null导致报错”的问题。
方案二:数据库事务+行锁(适合必须手动控制订单号规则的场景)
如果你的订单号规则不能直接用ID,那可以用数据库事务加行锁,把“查询最新订单”和“生成订单号并保存”变成原子操作,防止并发冲突:
$order = new Order; $order->user_id = Auth()->id(); $order->sub_total = $subtotal; // 其他字段赋值 DB::transaction(function () use ($order) { // 用lockForUpdate()锁住最新订单行,其他事务必须等待当前事务完成才能读取 $latestOrder = Order::orderBy('id', 'DESC')->lockForUpdate()->first(); // 处理首次创建订单的情况 $nextOrderId = $latestOrder ? $latestOrder->id + 1 : 1; $order->order_nr = '#' . str_pad($nextOrderId, 8, "0", STR_PAD_LEFT); $order->save(); });
这里注意把created_at排序改成id排序,因为ID是严格自增的,比时间戳更可靠。
方案三:Redis原子自增(适合分布式多服务器部署场景)
如果你的系统是多服务器部署,数据库行锁可能会影响性能,这时可以用Redis的原子自增命令来生成唯一的订单号序列:
// 先确保你的Laravel已经配置好Redis连接 $nextSequence = Redis::incr('order_number_sequence'); $order = new Order; $order->user_id = Auth()->id(); $order->order_nr = '#' . str_pad($nextSequence, 8, "0", STR_PAD_LEFT); $order->sub_total = $subtotal; // 其他字段赋值 $order->save();
Redis的INCR命令是原子性的,不管多少并发请求,都会生成唯一递增的数字,完美避免重复问题。
最后再提醒你几个原代码的坑:
- 没有处理
latestOrder为null的情况(首次创建订单时),会直接抛出“Trying to get property 'id' of non-object”错误; - 用
created_at排序不可靠,数据库时间戳可能存在精度问题,导致拿到的不是真正最新的订单; - 并发场景下完全没有做冲突防护,这是导致Duplicate entry的直接原因。
内容的提问来源于stack exchange,提问作者user7301337
相关产品推荐
相关产品推荐

