TP6.0需自建中间件+Redis实现Token幂等,核心是通过Lua脚本原子执行「校验并删除Token」,避免先get后del导致并发重复;Token键建议为"idempotent:".$token,过期设5–10分钟;必须覆盖所有入口(含回调、异步任务),仅靠数据库唯一索引无法防止业务逻辑重复执行。

TP6.0 里怎么用 Token + Redis 实现幂等拦截
直接结论:TP6.0 没内置幂等支持,必须自己写中间件 + Redis 配合防重 Token。核心是「生成、校验、删除」三步原子执行,否则并发下会失效。
常见错误现象:validateToken 方法里先 get 再 del,结果两个请求同时查到 token 存在,都放行——这就是典型非原子操作导致的重复提交。
- 必须用 Lua 脚本或 Redis 的
GETSET/DEL原子命令,TP6.0 的Cache::store('redis')->get()和delete()是分开调用,不安全 - Token 建议拼接为
"idempotent:" . $request->header('X-Idempotent-Token'),避免和其他业务 key 冲突 - 过期时间设 5–10 分钟足够,太短易因网络延迟误判,太长占 Redis 内存
- TP6.0 的中间件需注册到路由或全局,不能只加在控制器方法上(因为中间件执行早于控制器注解)
为什么 TP6.0 不能靠数据库唯一索引兜底
唯一索引只能拦住「插入重复数据」,拦不住「重复执行业务逻辑」。比如支付接口,即使订单号加了唯一索引,扣款、发消息、更新账户余额这些动作仍可能被执行两次。
典型场景:用户点两次提交,第一次插入成功,第二次唯一索引报错 SQLSTATE[23000]: Integrity constraint violation,但第一次的扣款已经完成,钱已扣,只是订单创建失败——这不算幂等。
- 唯一索引适合做最终一致性校验,不是幂等第一道防线
- TP6.0 的
Db::insertGetId()或$model->save()抛异常后,必须手动回滚事务,否则状态不一致 - 如果业务含多表操作(如订单+库存+日志),唯一索引无法覆盖全部副作用
TP6.0 中间件里怎么安全校验 Token
别在中间件里手写 if ($redis->get($key)) { $redis->del($key); ... } —— 这段代码在高并发下必然出问题。TP6.0 推荐用 Redis 的 EVAL 执行 Lua 脚本保证原子性。
示例 Lua 脚本(存为 app/common/redis/idempotent.lua):
if redis.call("GET", KEYS[1]) == "1" then
redis.call("DEL", KEYS[1])
return 1
else
return 0
end
中间件中调用:
$result = $this->redis->eval(file_get_contents(APP_PATH . 'common/redis/idempotent.lua'), [$tokenKey]);
-
$result === 1表示首次请求,放行;0表示重复,直接return json(['code' => 409, 'msg' => '请勿重复提交']); - TP6.0 的
think\cache\driver\Redis默认不支持eval,需确认配置中'read_timeout' => 0且 Redis 未禁用 Lua - Token 必须由前端在页面加载时通过
/api/token接口获取,不能由前端自生成(防伪造)
TP6.0 里哪些地方最容易漏掉幂等控制
最常被忽略的是「异步任务触发点」和「回调接口」。比如微信支付回调、MQ 消费入口、定时任务钩子——这些地方没加幂等,比主流程更危险。
- TP6.0 的命令行任务(
php think task:payCallback)必须手动加 Token 校验,框架不自动注入中间件 - 第三方回调(如支付宝 notify)通常无 session、无 header,Token 只能从 POST body 或 query 参数取,且需校验签名后再校验幂等
- 使用
think\queue\job时,Job 类的handle方法里要主动查 Redis,不能依赖控制器中间件 - TP6.0 的
Validate规则只校验参数,不校验业务唯一性,别指望它防重复
真正起作用的从来不是“加了注解”或“写了中间件”,而是所有可能被重复触发的入口,都经过同一套 Token 校验逻辑——漏一个,整个幂等设计就形同虚设。


















