laravel 8.0(实为thinkphp 8.0)中需用redis原子操作实现分布式锁防超卖,推荐手写lockservice或使用think-redis-lock扩展,并必须启用自动续期机制避免锁提前释放。

在 Laravel 8.0(TP8.0 是指 ThinkPHP 8.0 的常见误写,实际应为 ThinkPHP 8.0)中实现并发控制时,必须防止多个请求同时修改同一库存或订单状态导致超卖、重复提交等问题。直接用数据库行锁或 PHP 原生 sleep+select 不可靠,需借助 Redis 的原子性操作构建可重入、防误删、带自动续期能力的分布式锁。
手动实现:基于 SET 命令 + Lua 脚本的原子加锁与解锁
这是最轻量、不依赖第三方扩展的方案,适用于已接入 Redis 且对锁粒度有明确控制需求的场景。
第一步:在 config/cache.php 中确认 Redis 连接已启用,并确保 【Redis 服务运行正常且 PHP redis 扩展已加载】,否则后续所有操作会静默失败。
第二步:编写 LockService 类,存放在 app/service/LockService.php:
使用 Redis::set() 的原子参数组合:key、value(客户端唯一标识)、选项数组 ['nx' => true, 'ex' => 30] → 表示“仅当 key 不存在时设置,且过期时间为 30 秒”。这一步替代了旧式 setnx + expire 两步操作,彻底规避死锁风险。
第三步:解锁必须用 Lua 脚本校验 value 再删除,避免 A 线程锁过期后被 B 获取,A 仍执行 del 导致误删 B 的锁。脚本内容为:if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end。调用时传入 lockKey 和当初 set 的唯一 client_id。
第四步:在控制器中使用:
$lock = new \app\service\LockService('order:pay:'.$orderId);
if (!$lock->acquire(5)) { // 尝试获取锁,最多等 5 秒
throw new \think\Exception('系统繁忙,请稍后再试');
}
try {
// 执行扣库存、生成订单等核心逻辑
} finally {
$lock->release(); // 必须放在 finally 中,确保释放
}
使用 think-redis-lock 扩展包快速集成
如果你希望跳过手写 Lua 和异常兜底逻辑,推荐直接引入社区验证过的扩展包,它已内置可重入、自动续期、阻塞等待等能力。
方法一:安装扩展包
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
执行命令:composer require topthink/think-redis-lock,安装完成后无需额外配置,自动读取当前应用的 redis 配置。
方法二:在业务代码中调用
$lock = \think\facade\Cache::lock('goods:stock:'.$goodsId, 30);
if ($lock->get()) {
try {
// 扣减库存逻辑
} finally {
$lock->release();
}
} else {
throw new \think\Exception('操作冲突,请重试');
}
注意:该扩展默认使用 UUID 作为 client_id,且 unlock 时自动校验,不支持跨进程共享锁上下文,【务必不要在协程或 Swoole 长连接中复用同一个 Lock 实例】,每次请求需新建。
高并发下必须开启锁自动续期
当业务逻辑执行时间可能超过锁过期时间(例如支付回调处理耗时 45 秒),必须启用续期机制,否则锁提前释放会导致并发穿透。
第一步:启用心跳续期功能。在 acquire() 后立即启动后台定时器,每 10 秒调用一次 Redis::expire($key, 30),前提是当前 client_id 仍持有该 key —— 这需要先 get 对比再 expire,但 get+expire 非原子,所以必须改用 Lua:
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], tonumber(ARGV[2])) else return 0 end
第二步:续期任务需绑定到当前请求生命周期。ThinkPHP 8.0 中可在 middleware 或 controller 构造函数里注册 shutdown 函数清理定时器,避免子进程残留。
第三步:设置合理续期间隔。续期间隔必须小于锁过期时间的一半(如锁设为 30 秒,则续期间隔 ≤12 秒),否则存在窗口期被其他客户端抢占。










