首页 > 编程语言 >TP8.0 使用Redis实现分布式锁并发控制示例

TP8.0 使用Redis实现分布式锁并发控制示例

来源:互联网 2026-07-15 19:33:00

在ThinkPHP8.0中,通过Redis原子操作实现分布式锁,可防误删、自动续期且支持可重入。手动方案基于SET命令与Lua脚本保证加解锁原子性;扩展包think-redis-lock提供快速集成。高并发下需开启自动续期,续期间隔应小于锁过期时间的一半。

在 ThinkPHP 8.0 项目中实现并发控制,特别是库存扣减、订单防重复等场景,直接依赖数据库行锁或 PHP 的 sleep+select 轮询方案,往往容易埋下隐患。更可靠的做法是借助 Redis 原子操作构建分布式锁,既要防止误删,又要支持自动续期,最好还能实现可重入。下面两种方式——手写实现和使用扩展包——均能满足生产环境要求。

TP8.0 使用Redis实现分布式锁并发控制示例

长期稳定更新的攒劲资源: >>>点此立即查看<<<

手动实现:基于 SET 命令 + Lua 脚本的原子加锁与解锁

这是最轻量、不依赖第三方扩展的方案,适用于已接入 Redis 且对锁粒度有明确控制需求的场景。

首先确认环境:config/cache.php 中 Redis 连接必须正确配置,PHP redis 扩展必须加载,Redis 服务正常运行——否则后续操作将静默失败,调试时容易踩坑。

接下来编写一个 LockService 类,放在 app/service/LockService.php。核心逻辑是利用 Redis 的 SET 命令结合 NXEX 参数,一步完成加锁与过期时间设置:Redis::set('lock:key', $clientId, ['nx' => true, 'ex' => 30])。这样无需像旧代码那样先 setnxexpire,彻底消除死锁风险。

解锁时必须使用 Lua 脚本保证原子性:先检查 KEYS[1] 的值是否等于当前客户端传入的 ARGV[1],是则删除,否则直接返回。这样可以避免 A 线程的锁过期后,B 线程拿到锁,A 却执行 del 误删 B 锁的尴尬情况。脚本内容如下:

if redis.call('get', KEYS[1]) == ARGV[1] then
    return redis.call('del', KEYS[1])
else
    return 0
end

调用时传入锁的 key 和之前 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 中,确保释放
}

注意 acquire 的等待时间参数需结合业务耗时设定,太短容易失败,太长可能阻塞用户。另外,release 必须放在 finally 中,这是铁律。

使用 think-redis-lock 扩展包快速集成

如果不想手写 Lua 脚本,也不愿自行处理异常兜底和续期逻辑,那么直接使用社区验证过的扩展包更省心。它已内置可重入、自动续期、阻塞等待等能力。

安装非常简便:

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,解锁时自动校验,因此无需担心误删。但需要注意:它不支持跨进程共享锁上下文,千万不要在协程或 Swoole 长连接中复用同一个 Lock 实例,每次请求都应新建。

高并发下必须开启锁自动续期

业务逻辑执行时间有时会超过锁的过期时间——例如支付回调处理耗时 45 秒,而锁只有 30 秒。一旦锁提前释放,其他请求便能穿透进来,超卖问题依然会出现。因此务必开启续期机制。

续期如何实现?在 acquire() 成功后,启动一个后台定时器,每 10 秒调用一次 Redis::expire($key, 30)。但前提是当前客户端仍持有该锁——单纯的 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 中,可以在中间件或控制器构造函数中注册一个 shutdown 函数来清理定时器,避免子进程残留。

续期间隔的设置也有讲究:必须小于锁过期时间的一半。例如锁设为 30 秒,续期间隔 ≤ 12 秒才安全,否则在两个续期之间会有一个窗口期,其他客户端可能趁机抢走锁。

侠游戏发布此文仅为了传递信息,不代表侠游戏网站认同其观点或证实其描述

热游推荐

更多
湘ICP备14008430号-1 湘公网安备 43070302000280号
All Rights Reserved
本站为非盈利网站,不接受任何广告。本站所有软件,都由网友
上传,如有侵犯你的版权,请发邮件给xiayx666@163.com
抵制不良色情、反动、暴力游戏。注意自我保护,谨防受骗上当。
适度游戏益脑,沉迷游戏伤身。合理安排时间,享受健康生活。