用 cache::add() 实现原子性请求指纹去重:拼接 ip、url 和标准化请求内容生成唯一指纹,设 30–60 秒缓存有效期,避免使用 session 或非原子操作,确保 redis 驱动及 key 确定性。

如何用 Illuminate\Support\Facades\Cache 实现请求指纹去重
重复提交本质是同一用户在极短时间内发了多个相同意图的请求,比如连点两次“支付”按钮。Laravel 没有开箱即用的防重中间件,但可以用缓存打指纹快速拦截——关键不是锁整个表或 DB 行,而是拦在入口。
常见错误现象:TokenMismatchException 误判、CSRF token expired 频发、前端禁用按钮失效后仍收到双请求。
- 使用场景:表单提交(POST/PUT/PATCH)、支付回调幂等校验、敏感操作(删账号、改密码)
- 指纹生成建议拼接:
request()->ip()+request()->fullUrl()+md5(request()->getContent())(注意 GET 请求无 content,需 fallback 到request()->query()) - 缓存有效期设为 30–60 秒足够,太长占内存,太短挡不住真实重试
- 别用
session()存指纹——session 可能延迟写入或跨请求不一致;必须用Cache::put()+ 原子性校验
App\Http\Middleware\PreventDuplicateRequests 中间件怎么写才不漏判
直接在中间件里用 Cache::has() + Cache::put() 是错的——两个并发请求可能同时通过 has() 判断,再一起写入,导致漏判。必须用原子操作。
正确做法是调用 Cache::add():它只在 key 不存在时写入并返回 true,否则返回 false。这才是真正意义上的“抢锁”。
- 示例逻辑:
if (! Cache::add($fingerprint, true, 60)) { abort(422, 'Duplicate request detected'); } - 注意
$fingerprint必须对同一语义请求完全一致——比如带分页参数的列表搜索,page=1和page=2要算不同指纹 - 不要在中间件里做耗时操作(如查 DB、调外部 API),指纹校验必须在 5ms 内完成,否则拖慢所有请求
- 如果用了 Redis 集群,确保
add()操作落在同一个 slot,否则原子性失效
为什么不能只靠前端 disabled 按钮或节流
前端控制纯属安慰剂。用户可以 curl、Postman、Fiddler 重放,甚至禁用 JS 后反复提交。后端才是唯一可信防线。
- 常见错误:在 Vue/React 里用
loading状态禁用按钮,但没配axios的cancelToken或AbortController,导致请求其实已发出 - 节流(throttle)和防抖(debounce)只对连续触发有效,对网络延迟导致的“用户以为没响应而手动重点”完全无效
- CSRF token 本身不防重——它只防跨站,同域下刷新页面重提照样过 token 校验
- 真正的兜底是后端指纹 + 原子写入,且该逻辑必须独立于业务代码,放在中间件层最干净
遇到 Cache::add() always returns false 怎么排查
这个错误通常不是代码问题,而是缓存驱动或配置层面的坑。
- 检查缓存驱动是否为
redis或memcached——file和database驱动不支持原子add() - 确认 Redis 连接正常:
Cache::store('redis')->get('test')能读写 - 留意 Laravel 版本差异:8.x 之前
Cache::add()返回 bool;9.x+ 默认返回bool|int(写入字节数),但判断逻辑不变 - 如果用了 Horizon 或队列,别把指纹 key 设成全局固定名(如
duplicate_guard),必须含动态因子,否则所有请求互斥
最常被忽略的一点:指纹里混入了非确定性数据,比如 microtime()、随机数、未 normalize 的 JSON 字段顺序——这会导致同一语义请求生成不同 key,防重彻底失效。











