Laravel 表单重复提交需用 Cache::lock() 实现后端互斥控制,锁 key 应包含业务上下文(如用户 ID、商品 ID),设 3–5 秒超时并显式判断 block() 返回值,避免依赖前端防重或数据库行锁。

表单重复提交时 Laravel 后端根本拦不住?
单纯靠前端防重(比如按钮置灰)不可靠,用户刷新页面、重发请求、调试工具重放都会绕过。Laravel 默认不自动处理并发提交,csrf_token 只防跨站,不防重复;验证通过后写数据库前,两个几乎同时的请求都可能通过校验,导致重复插入或状态错乱。
真正能起作用的是在关键业务逻辑入口加一层「互斥」——不是等前端配合,而是让后端自己拒绝第二个进来的人。
- 用
Cache::lock()最轻量,适合短时临界区(如创建订单、领取优惠券) - 避免用数据库行锁(如
forUpdate())处理高频表单,容易堆积死锁或拖慢响应 - 锁 key 必须带业务上下文,比如
"form_submit:order_{$userId}_{$itemId}",不能全站共用一个锁 - 务必设超时(默认 10 秒太长,建议
3–5秒),否则异常崩溃后锁不释放,后续请求全卡住
Laravel 8+ 的 Cache::lock() 怎么安全用
它底层依赖 Redis 或 Memcached,不是文件锁或 DB 锁,性能好、跨进程可靠。但默认行为有坑:不抛异常,只返回布尔值,容易被忽略失败分支。
- 必须显式调用
->get()并判断返回值,不能只写Cache::lock(...)->block(3)就完事 - 推荐写法:
$lock = Cache::lock('submit_'.$request->user()->id, 5); if (! $lock->block(3)) { return back()->withErrors(['message' => '操作太频繁,请稍后再试']); } try { // 执行核心逻辑:创建记录、扣库存、发消息... } finally { $lock->release(); } - 注意
block(3)是“最多等 3 秒”,不是“锁住 3 秒”;真正持有锁时长由你手动release()或作用域结束自动释放(PHP 8.1+ 支持自动释放) - 如果用了 Horizon 或队列,别在 job 里直接用
Cache::lock(),需确保锁驱动支持分布式,且超时设置比 job timeout 短至少 2 秒
为什么不用数据库 SELECT ... FOR UPDATE?
它确实能锁住某条记录,但只在事务内有效,且要求查询必须命中索引,否则会升级成表锁。表单提交场景下,往往还没生成主键(比如新建记录),或者要锁的不是已有数据(比如“用户今日首次提交”这种状态),这时根本没法选行。
- 常见翻车点:
DB::transaction里查不到对应记录就直接 insert,两个事务并行执行,都 insert 成功 → 重复数据 - 想靠
INSERT ... ON DUPLICATE KEY UPDATE补救?它只对唯一键冲突生效,而很多业务约束(如“每人限领一次”)没有对应唯一索引 - 高并发下
FOR UPDATE容易触发 MySQL 死锁检测,返回"Deadlock found when trying to get lock",需要额外重试逻辑 - 真要用,必须确保 WHERE 条件走索引,且事务尽可能短,不要在锁期间调外部 API 或渲染视图
锁 key 设计不当会导致什么问题
锁不是越粗越好,也不是越细越安全。key 漏掉关键维度,等于没锁;key 过于随机,又失去互斥意义。
- 错误示例:
"form_lock"→ 全站所有用户互相阻塞,体验灾难 - 错误示例:
"form_lock_".now()->format('Y-m-d')→ 时间粒度太大,同一秒内多个请求仍可能并发 - 正确思路:把「谁在什么条件下做哪件事」编码进 key,例如:
"claim_voucher:{$userId}:{$voucherId}"或"submit_order:{$userId}:{$cartHash}" - 如果 key 含敏感信息(如手机号),记得先哈希(
sha256($phone)),避免 Redis 日志/监控泄露 - Redis key 长度别超 1KB,否则影响性能;建议控制在 128 字符内


















