Redis分布式锁需用SET原子加锁、Lua安全解锁、幂等令牌防重复、锁续期防超时、预扣库存+异步落库五层防护,缺一不可。

如果您在ThinkPHP6.x项目中遇到商品超卖或用户重复提交订单的问题,很可能是由于并发请求绕过了业务层互斥控制。Redis分布式锁是解决该问题的主流手段,但必须规避裸用Cache::lock()带来的误删、死锁与锁残留风险。以下是针对ThinkPHP6.x环境的安全锁实践方案:
一、使用SET命令原子加锁(推荐基础方案)
该方案通过单条Redis SET命令完成“键不存在才设置 + 自动过期”两个动作,彻底规避SETNX与EXPIRE分步执行引发的竞态。ThinkPHP6.x需绕过默认Cache::lock(),直接调用Redis驱动的set方法并传入原子参数。
1、在控制器中获取Redis实例:$redis = Cache::store('redis')->getRedis();
2、生成唯一客户端标识:$token = uniqid('', true);
立即学习“PHP免费学习笔记(深入)”;
3、执行原子加锁:$result = $redis->set('lock:stock:'.$goodsId, $token, ['nx', 'ex' => 15]);
4、判断加锁结果:若$result为true,则进入库存扣减逻辑;否则返回“系统繁忙,请稍后重试”提示。
二、Lua脚本安全解锁(强制必需步骤)
直接调用DEL删除锁会导致“误删他人锁”的致命问题。必须使用Lua脚本将GET校验与DEL操作封装为不可分割的原子操作,确保仅当当前key的value与本客户端token完全一致时才执行删除。
1、定义Lua解锁脚本:$script = "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('DEL', KEYS[1]) else return 0 end";
2、执行脚本解锁:$redis->eval($script, ['lock:stock:'.$goodsId], [$token]);
3、检查返回值:若返回1表示解锁成功;返回0说明锁已被其他客户端持有或已过期,此时不可重试DEL,应直接结束流程。
三、结合幂等令牌防御串行重复提交
分布式锁仅能拦截并发请求,无法阻止用户F5刷新、后退重提等串行重复行为。需在锁机制外叠加幂等层,以request_id为唯一凭证,在Redis中维持短时效存在性校验。
1、前端生成并传递唯一ID:$requestId = 'req_'.date('YmdHis').'_'.mt_rand(1000,9999);
2、业务入口处校验幂等性:$exists = $redis->exists('idempotent:'.$requestId);
3、若$exists为true,立即返回“操作已完成,无需重复提交”并终止后续流程;否则执行加锁与业务逻辑。
4、业务成功后写入幂等记录:$redis->setex('idempotent:'.$requestId, 30, 'success');
四、锁续期防止业务超时释放
当库存扣减链路包含远程调用、数据库事务或MQ投递等不确定耗时环节时,固定TTL易导致锁提前释放。需在业务执行中周期性延长锁有效期,避免其他客户端抢占。
1、启动独立协程或定时器(如Swoole tick),每5秒执行一次续期:
2、构造续期Lua脚本:$renewScript = "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('EXPIRE', KEYS[1], ARGV[2]) else return 0 end";
3、调用脚本更新过期时间:$redis->eval($renewScript, ['lock:stock:'.$goodsId], [$token, 15]);
4、业务完成后停止续期,并执行第三步中的安全解锁流程。
五、预扣库存+异步落库分层防护
单纯依赖锁会将高并发压力传导至Redis,且无法应对主从延迟导致的锁丢失。应在锁内仅执行Redis原子预扣,将订单持久化、通知等非核心步骤剥离至异步队列,形成缓冲层。
1、锁内执行Lua预扣脚本:local stock = tonumber(redis.call('GET', KEYS[1])); if stock and stock > 0 then redis.call('DECR', KEYS[1]); return 1; else return 0; end
2、预扣成功后立即写入消息队列(如RabbitMQ/Kafka),携带goods_id、order_sn、user_id等上下文。
3、消费者服务从队列拉取任务,执行数据库写入、日志记录、短信通知等耗时操作。
4、若预扣失败,直接返回“库存不足”,不进入队列流程。



















