Laravel并发场景下设计幂等性接口,核心是用唯一键+原子操作+明确状态边界三者协同:客户端生成UUIDv4作为Idempotency-Key并强制携带,服务端禁用请求体哈希fallback;Redis缓存需结构化存储状态与压缩响应,锁必须带随机token、租约续期及Lua校验释放;数据库层须建联合唯一索引兜底,杜绝TOCTOU漏洞。

直接说结论:Laravel并发场景下设计幂等性接口,核心不是“加锁”或“缓存”,而是**用唯一键 + 原子操作 + 明确状态边界**三者协同,否则锁会失效、缓存会错乱、Redis超时会导致重复执行。
Idempotency-Key 必须由客户端生成且不可预测
服务端自动生成 hash('sha256', ...) 是危险的——相同请求体在不同时间、不同服务器上可能因序列化顺序、空格、header差异导致哈希不一致,造成“假重复”或“假非重复”。
- 客户端必须在首次请求前生成一个全局唯一、一次性的
Idempotency-Key(推荐 UUID v4) - 该 key 需随请求携带在 header 中:
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000 - 服务端禁止 fallback 到请求体哈希;若 header 缺失,直接返回
400 Bad Request,不降级处理 - 幂等键有效期建议设为业务窗口期(如支付类 24h,订单类 72h),过期后 key 不再被识别,避免长期占用 Redis 内存
Redis 缓存响应必须带状态标记与压缩
单纯 Redis::setex($key, $ttl, $response->getContent()) 会丢失状态信息,导致“处理中”和“已成功”无法区分,高并发下极易出现双写。
- 缓存值必须是结构化 JSON,至少包含:
status(processing/success/failed)、code、headers、content(gz 压缩 + base64) - 写入前先用
Redis::set($key, json_encode(['status' => 'processing']), 'NX', 'EX', 5)占位,防止并发进入业务逻辑 - 成功后用 Lua 脚本原子更新:
if redis.call("GET", KEYS[1]) == "processing" then return redis.call("SETEX", KEYS[1], ARGV[1], ARGV[2]) else return 0 end - 读取时若命中
"processing"状态,应返回409 Conflict或轮询等待,而非直接放行
分布式锁必须防误删且带租约续期
用 Cache::add() 或简单 SET key val EX 60 NX 实现锁,在网络延迟或进程卡顿场景下极易产生死锁——锁未释放,但业务已超时返回,后续请求永远被阻塞。
Laravel Cloud(cloud.laravel.com)。此技能用于处理所有 Laravel Cloud 请求,包括搜索和读取数据。只要任务涉及 Laravel Cloud,即使用此技能。
- 锁值必须是随机 token(如
Str::random(32)),不能是固定字符串或时间戳 - 获取锁后,需在业务处理过程中启动后台续期任务(如 Laravel 的
Redis::expire($lockKey, 30)每 10 秒刷新一次) - 释放锁必须用 Lua 脚本校验 token 一致性,杜绝 A 进程释放 B 进程的锁:
if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end - 中间件里不要把锁释放写在
finally块里——PHP-FPM worker 被 kill 时finally不保证执行;锁必须有明确 TTL + 续期机制兜底
数据库层需配合唯一约束做最终防线
Redis 故障、网络分区、锁失效时,仅靠缓存和锁无法保证绝对幂等。数据库唯一索引才是最后的“保险丝”。
- 所有幂等关键操作,必须在 DB 表中建联合唯一索引,例如:
UNIQUE KEY `uk_idempotent_order` (`idempotency_key`, `business_type`) - 业务逻辑中插入前不查、不判断,直接
DB::insert(... ON DUPLICATE KEY UPDATE status = VALUES(status))或用upsert() - 捕获
Illuminate\Database\QueryException,当$e->getCode() === '23000'且$e->errorInfo[1] === 1062(MySQL)时,查询已有记录并返回,而非抛出 500 - 不要在事务内先
SELECT再INSERT——这是典型的 TOCTOU(time-of-check to time-of-use)漏洞,高并发下必然击穿
真正难的不是实现某一个环节,而是让 Redis 缓存、分布式锁、数据库约束三者在故障切换时不互相矛盾——比如 Redis 挂了走 DB 唯一约束,DB 唯一约束又依赖幂等键字段存在且非空,那这个字段就必须在所有入口强制校验、不可 NULL、不可默认值。漏掉任意一环,线上就可能多扣一笔钱或多发一张券。










