应使用时间粒度对齐的Redis key(如order_sn_2405201530)配合INCR生成唯一订单号,设置过期时间,且需while重试+异常捕获兜底,避免空序号。

Redis INCR 生成递增段,但不能直接拼时间戳
直接用 microtime(true) 拼接 INCR 值看似唯一,实际在高并发下会撞——PHP 处理微秒级时间戳有精度损失,同一微秒内多个请求拿到相同时间部分;Redis 的 INCR 虽原子,但若不同时间戳段共用一个 key,递增序号会错乱。必须让「时间粒度」和「递增域」严格对齐。
实操建议:
- 用
date('ymdHis')(秒级)或date('ymdHi')(分钟级)作为 Redis key 后缀,确保同一时间窗口内所有请求共享同一个递增计数器 - key 命名如
order_sn_2405201530(2024年5月20日15:30),避免用毫秒/微秒导致 key 爆炸式增长 - 务必设置过期时间:
SETEX order_sn_2405201530 86400 0,防止冷 key 持久占用内存
ThinkPHP 中调用 incr() 要包裹事务性重试
Redis 网络抖动或连接中断时,incr() 可能返回 false 或抛异常,但订单号生成失败不能回退整个业务事务(比如已扣库存),必须本地兜底重试。
常见错误现象:未检查返回值,直接拼接字符串,结果生成 2405201530_ 这种缺序号的脏数据。
立即学习“PHP免费学习笔记(深入)”;
实操建议:
- 用 while 循环 + 最大重试次数(建议 ≤3),每次失败前
usleep(1000)避免打满 Redis - 捕获
think\exception\DbException和Predis\Connection\ConnectionException(取决于你用的 Redis 扩展) - 示例片段:
$key = 'order_sn_' . date('ymdHi');<br>$retry = 0;<br>do {<br> $sn = $this->redis->incr($key);<br>} while ($sn === false && ++$retry < 3 && usleep(1000));<br>if ($sn === false) {<br> throw new \RuntimeException('Order SN gen failed after retry');<br>}
最终订单号格式要预留位数,别硬拼字符串
单纯拼 date('ymdHis') . '_' . $sn 在高峰期容易出现长度不一致(如 240520153001 vs 2405201530127),影响数据库索引效率、日志切分、下游系统解析。
使用场景:MySQL 中 order_sn 字段常设为 VARCHAR(32) 或 CHAR(20),长度波动大会降低查询稳定性。
实操建议:
- 用
sprintf('%s%06d', $timestamp, $sn % 1000000)固定后 6 位,溢出时自动归零(配合 key 过期策略,实际不会撞) - 时间戳部分统一用
ymdHi(10 位),总长可控在 16 位内:2405201530000001 - 禁止用
uniqid()或mt_rand()补位——它们不保证全局唯一,且无序,破坏时间可追溯性
测试时别只压单机,要模拟多 PHP-FPM 进程并发
本地用 ab -n 1000 -c 100 测试,如果只跑一个 PHP 进程,microtime() + rand() 都像能用;但上线后 Nginx 启 8 个 fpm 子进程,每个进程里的时间获取、Redis 连接池复用、socket 缓冲区排队都会放大竞争。
性能影响:key 过期时间设太短(如 3600 秒),会导致分钟级高峰后大量 key miss,瞬间击穿到重试逻辑;设太长(如 1 年),冷 key 积压浪费内存。
实操建议:
- 压测前先用
redis-cli --scan --pattern "order_sn_*" | wc -l看 key 数量是否随时间线性增长 - 在 ThinkPHP 日志里加一行:
Log::info("sn_gen_key: {$key}, sn: {$sn}, pid: " . getmypid());,确认多进程是否写入同一 key - 真正难测的是「跨天/跨分钟边界」,比如 23:59:59.999 和 00:00:00.001 的两个请求,必须验证 key 切换是否原子
边界处理比主流程更耗精力,尤其是时间格式化函数在不同时区、SAPI 模式下的行为差异,别信文档,得看 var_dump(date('ymdHi')) 实际输出。


















