thinkphp模型的auto和default无法保证编号唯一,因其不处理并发、无锁且不重试;高并发下基于id或时间拼接易重复,须用事务+唯一索引+重试或redis原子计数器兜底。

ThinkPHP 模型字段不能靠内置机制自动生成唯一编号(如订单号),必须手动实现,且不能依赖数据库自增或默认值。
为什么 auto 和 default 在模型里对编号无效
ThinkPHP 的 auto(自动完成)和 default(默认值)只在数据写入前触发,但它们不感知并发、不加锁、不重试。一旦两个请求几乎同时创建记录,date('Ymd').str_pad($id, 4, '0', STR_PAD_LEFT) 这类基于主键 ID 的拼接方式会直接崩——因为 $id 此时还没生成,或者被其他请求抢先占了同一 ID。
常见错误现象:SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicate entry '202405200001' for key 'order_sn'。
实操建议:
- 放弃在
create()或save()前靠模型属性推导编号 - 不把编号生成逻辑放在
protected $auto = ['order_sn']里 - 避免用
time()或microtime(true)直接拼接——精度不够,高并发下极易重复
推荐做法:用数据库事务 + 唯一索引 + 重试生成
核心思路是“先生成、再校验、失败重来”,靠数据库唯一约束兜底,而不是靠 PHP 逻辑预判。
使用场景:订单号、流水号、业务单据编号等需全局唯一、有业务含义(如含日期)、不可重复的字符串。
实操建议:
- 字段类型设为
VARCHAR(32),加UNIQUE索引(如ALTER TABLE `order` ADD UNIQUE(`order_sn`)) - 生成函数用
date('ymd').uniqid('', true)或date('Ymd').sprintf('%06d', mt_rand(1, 999999))起手,保证初始随机性 - 封装成独立方法,带最多 3 次重试(避免死循环):
public function generateOrderSn() { for ($i = 0; $i where('order_sn', $sn)->find()) { return $sn; } } throw new Exception('Order sn generate failed after 3 attempts'); } - 调用时务必在事务中:
$this->startTrans(); try { $this->order_sn = $this->generateOrderSn(); $this->save(); $this->commit(); } catch (\Exception $e) { $this->rollback(); throw $e; }
更稳方案:用 Redis 原子计数器 + 日期前缀
当并发量超过每秒百级,纯数据库查重+重试开始吃力,这时该换方案。Redis 的 INCR 是原子的,不会冲突,适合做日粒度自增序列。
性能影响:每次生成多一次 Redis 请求,但比 DB 查重快一个数量级;兼容性无问题,ThinkPHP 支持原生 Redis 驱动。
实操建议:
- Key 设计为
order_sn:20240520,每天一个计数器 - 用
$redis->incr("order_sn:".date('Ymd"))获取当日第 N 单 - 拼接后补零:
$sn = date('Ymd').sprintf('%06d', $redis->incr("order_sn:".date('Ymd'))) - 注意:Redis 宕机时需降级到数据库查重方案,否则单点故障会导致整个下单流程中断
真正难的不是怎么拼字符串,而是怎么让“生成 → 写库 → 不重复”这三步在分布式、高并发下依然成立。很多人卡在测试环境没问题,上线后突然报唯一键冲突——那基本就是没压测重试逻辑,或 Redis 降级没写。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










