validatetoken不能先get再del,因tp6.0中cache::store('redis')->get()与delete()是两次独立redis操作,存在并发窗口导致两个请求同时读到token并放行;必须用lua脚本或set(...['nx','ex'=>t])等原子操作确保校验与删除一步完成。

validateToken 为什么不能先 get 再 del
因为 Cache::store('redis')->get() 和 delete() 是两次独立 Redis 操作,中间存在时间窗口。两个并发请求同时执行 get() 都拿到非空值,接着都进入业务逻辑——重复执行就发生了。
必须用原子操作堵住这个缺口:
- 推荐用 Lua 脚本:传入 key 和预期 token 值,Redis 内部判断并删除,返回 1 或 0,PHP 里严格用
=== 1判断 - 或用
$redis->set($key, $value, ['nx', 'ex' => 300])(TP6.0 的 Redis 实例支持),但这是用于「生成」阶段;校验阶段仍需原子读删 - 别用
GETSET,它不校验值是否匹配,只覆盖,会误放行不同 token
幂等 key 怎么拼才安全
key 不是越长越好,关键是可追溯、无冲突、易清理。
建议格式:"idempotent:" . $request->header('X-Idempotency-Key'),而不是拼接用户 ID 或时间戳:
- 前端传的
X-Idempotency-Key必须原样使用,禁止strtolower()或 trim() 后再哈希,否则大小写不一致导致漏判 - 避免用
uniqid()直接当 key——它只靠微秒,高并发下可能重复;应配合bin2hex(random_bytes(16))生成 - 不要把 key 设成
"idempotent:{$userId}:{$uri}",URI 可能带动态参数(如时间戳),导致同一语义请求指纹不同
中间件里怎么写才算真正生效
TP6.0 的中间件注册位置决定它能否拦住所有入口。只加在控制器方法上或用注解,根本没用——中间件还没跑,业务逻辑已经执行了。
必须做到三点:
- 注册到全局中间件(
app/middleware.php的middleware数组),或绑定到具体路由组(Route::group(...)->middleware(...)) - 在中间件里第一时间取头:
$key = $request->header('X-Idempotency-Key', ''),空值或空白字符串要直接拒绝,否则所有无头请求全撞进同一个 key - 命中缓存时,不能简单
return json(['code'=>0]),得复用原始响应体(如果业务层已缓存过结果)或至少返回 HTTP 200 + 明确标识"idempotent": true,让前端区分“真成功”和“幂等返回”
为什么数据库唯一索引不是幂等方案
唯一索引只能拦住「插入失败」,拦不住「业务已执行」。
比如支付接口:第一次请求刚走到扣款逻辑、还没插订单表,第二次请求就进来了——两个请求都完成扣款、发消息、更新余额,最后只有其中一个能插进订单表,报 SQLSTATE[23000]: Integrity constraint violation。钱扣了两次,这才是真实风险。
所以:
- 唯一索引适合做最终一致性兜底,不是第一道防线
- 幂等必须卡在业务逻辑最前:token 校验通过才允许执行任何 DB/HTTP/消息动作
- 回调、异步任务、Webhook 这些非用户直触入口,同样要走同一套中间件——它们更容易被重试,反而更需要幂等
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











