uniqid() 不够用因仅基于微秒时间戳且无跨进程校验,必须配合 redis 的 setnx 原子操作实现幂等,前端传入的 idempotency-key 需原样校验并设合理 ttl。

为什么 uniqid() 直接拼时间戳不够用
因为 uniqid() 默认只基于微秒时间戳,同一毫秒内并发请求可能生成相同值;PHP 进程间无共享状态,单靠它无法跨请求校验。真实场景下,用户狂点提交按钮、网络重试、前端防抖失效,都会让后端收到重复的 token 或 id,但校验逻辑没跟上,照样写库两遍。
- 别直接用
uniqid()作为幂等键——它不唯一,只是“大概率不重复” - 必须配合服务端存储(如 Redis)做原子性校验:
SETNX+ 过期时间 - 前端传来的
idempotency-key请求头或form字段,要原样参与校验,不能二次加工(比如 strtolower())导致大小写不一致漏判 - Redis key 建议带业务前缀和过期时间,例如
idempotent:order:create:abc123,TTL 设为 5–10 分钟,太短扛不住重试,太长占内存
用 Redis + SETNX 实现原子校验
核心不是“生成 token”,而是“首次见到这个 key 才放行”。SETNX(set if not exists)天然支持原子写入+设置过期,比先 GET 再 SET 少一次网络往返,也避开了竞态。
- PHP 中用
$redis->setex($key, $ttl, $value)不行——它不保证原子存在性检查;必须用$redis->set($key, $value, ['nx', 'ex' => $ttl]) - 返回
false表示 key 已存在,此时应直接返回409 Conflict或业务约定的重复提交错误码,**绝不能继续执行业务逻辑** - 注意 PHP Redis 扩展版本:低于 5.3.0 的
set()不支持数组参数,得降级用setnx()+expire()两步,但这两步非原子,需加锁或接受极小概率失败 - 如果用的是 Predis,语法是
$redis->set($key, $value, 'nx', 'ex', $ttl)
什么时候该把幂等逻辑下沉到网关层
当同一个接口被多个 PHP 应用(如订单服务、支付服务)共用,或 PHP 实例横向扩到 10+ 台时,靠各实例连同一套 Redis 做校验没问题;但如果接口本身很轻(比如日志上报)、QPS 超 5000,Redis 成为瓶颈,就得前置。
- Nginx + Lua(OpenResty)可拦截
Idempotency-Key头,用resty.redis直连 Redis 校验,命中即返回 409,不打到 PHP - Spring Cloud Gateway 也能用
RequestRateLimiter过滤器复用 Redis,但需自定义KeyResolver提取幂等键 - 别在 Nginx 里做业务逻辑判断(比如“只有 status=paying 才幂等”),这种耦合会让网关变重,且 PHP 层校验仍要保留——网关只是第一道防线
$_POST 和 JSON Body 都要统一提取幂等键
前端可能走表单提交、也可能走 fetch({method: 'POST', body: JSON.stringify(...)}),Content-Type 不同,PHP 解析方式就不同。不统一处理,同一个请求会因解析路径不同拿到两个 key,导致校验失效。
立即学习“PHP免费学习笔记(深入)”;
- 表单提交:
$_POST['idempotency_key']或$_REQUEST['idempotency_key'] - JSON 提交:
json_decode(file_get_contents('php://input'), true)后取字段,不能依赖$_POST(它为空) - 更稳妥的做法:封装一个
get_idempotency_key()函数,优先读$_SERVER['HTTP_IDEMPOTENCY_KEY'],其次 fallback 到 POST/JSON body,避免前端传参方式变化导致漏校验 - 别忘了验证 key 长度和字符集:
preg_match('/^[a-zA-Z0-9_\-]{12,64}$/', $key),防止恶意超长 key 打满 Redis 内存











