tp6.0.16无内置幂等支持,须用自定义中间件+redis+lua脚本原子执行「校验并删除token」,键为"idempotent:".$token,过期5–10分钟,需覆盖所有入口,仅靠数据库唯一索引无法防止业务逻辑重复执行。

ThinkPHP6.0.16 本身不提供开箱即用的幂等性支持,必须通过自定义中间件 + Redis + 原子操作来构建可靠防线。核心不是“加个注解就完事”,而是围绕「唯一业务键」建立校验-执行-清理的闭环,且必须覆盖所有调用入口(含回调、异步任务、消息消费)。
一、Token机制:防重复提交的主力方案
适用于表单提交、支付发起、订单创建等前端可主动配合的场景。
- 客户端在请求前生成唯一 Token(如 UUID),通过请求头(如 X-Idempotent-Token)或参数传递
- 服务端中间件中拼接 Redis Key:"idempotent:" . $request->header('X-Idempotent-Token'),避免 key 冲突
- 必须用 Lua 脚本原子执行「判断存在并删除」,禁用先 get 后 del 的非原子写法
- Token 过期时间设为 5–10 分钟:太短易因网络延迟误判;太长浪费 Redis 内存
二、中间件实现要点:安全拦截的关键位置
中间件需注册到路由层或全局,不能仅绑定在控制器方法上——因为中间件执行时机早于控制器和注解解析,否则可能被绕过。
- 在中间件 handle() 方法中获取 Token,调用 Redis EVAL 执行预置 Lua 脚本
- Lua 脚本示例(建议存为 app/common/redis/idempotent.lua):
redis.call("DEL", KEYS[1])
return 1
else
return 0
end
- 脚本返回 1 表示 Token 有效且已清除,放行;返回 0 表示已使用或不存在,直接中断并返回 409 或自定义错误码
- 注意:TP6.0 的 Cache::store('redis')->get() 和 delete() 是两个独立命令,高并发下必然出现竞态
三、为什么不能只靠数据库唯一索引?
唯一索引只能防止重复插入,拦不住重复执行的业务副作用。
- 例如支付接口:第一次请求插入成功并完成扣款;第二次请求因 order_no 唯一索引报错回滚,但扣款动作已不可逆
- 涉及多表操作(订单+库存+日志+通知)时,唯一索引无法覆盖全部状态变更
- Db::insertGetId() 或 $model->save() 抛异常后,必须手动触发事务回滚,否则数据状态不一致
- 唯一索引适合做最终一致性兜底,而非第一道防线
四、业务幂等键设计:成败在此一举
选错幂等键,整个方案形同虚设。
- 优先使用天然业务唯一键:支付用 order_no + transaction_id,扣库存用 order_no + sku_id
- 避免用「用户ID + 时间戳」这类动态组合——每次请求都不同,等于没做幂等
- 客户端生成的 requestId 必须是页面级唯一(进入下单页时生成一次),而非每次点击都重发新 Token
- 对 MQ 消费或支付回调等被动入口,幂等键应基于业务字段(如微信回调中的 out_trade_no),而非消息 ID
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











