Hyperf 3.0协程环境下Redis分布式锁续期必须摒弃线程模型看门狗,改用与业务协程强绑定、可取消、带client_id校验的轻量续期协程,并推荐分段锁+幂等设计替代长时续期。

Hyperf 3.0 基于 Swoole 协程,Redis 分布式锁在协程环境下不能直接照搬传统线程模型的续期方案——因为“守护线程”在协程中并不存在,且协程生命周期短、调度不可控,若仍用阻塞式 sleep 或后台线程做看门狗,极易失效或引发内存泄漏、goroutine 泄露(类比)等风险。
协程下锁过期的核心风险点
Hyperf 中一个请求对应一个协程,锁由该协程持有。但:
- 协程可能被挂起(如 await DB 查询、HTTP 调用),而此时协程不执行任何代码,无法主动续期
- 没有真正的“后台线程”,
go func(){...}()或pcntl_fork不适用,Swoole 不支持 fork,协程也无法跨请求长期存活 - 使用
Co::sleep启动续期协程,若主业务协程已结束(如提前 return 或 panic),续期协程可能还在运行,造成误续他人锁或资源浪费 - Redisson 的 Watchdog 依赖 JVM 线程模型,在 Hyperf(PHP+Swoole)中不可用,必须自建轻量续期机制
Hyperf 3.0 推荐的续期实践方案
核心原则:**续期动作必须与业务协程强绑定、可取消、带身份校验、非阻塞**。
- 加锁时生成唯一 client_id(建议用
swoole_get_last_cid().'_'.uniqid()),写入 value,同时设置合理初始 TTL(如 20s) - 启动一个「可取消的续期协程」:
Co::create(function() use ($lockKey, $clientId, $ttl) { ... }),内部用Co::sleep($ttl / 3)循环检测 - 每次续期前,先用 Lua 脚本原子判断 key 存在且 value 匹配:
if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("expire", KEYS[1], ARGV[2]) else return 0 end - 在业务逻辑 try-finally 或 defer 中显式 cancel 续期协程(Hyperf 无原生 cancel,可用 context 或全局 flag + 协程退出前检查)
- 避免用
while(true)无限续期;建议设置最大续期次数(如 5 次),超限后主动释放锁并抛异常,防止无限等待
更稳妥的替代思路:分段锁 + 业务幂等
对超长任务(如导出百万数据、批量审核),与其硬扛续期,不如重构为「可中断+幂等+状态机」:
- 将大任务拆成小单元(如每 1000 条为一批),每批加一次锁、执行、更新进度状态
- 锁只保护单个批次操作,TTL 控制在 5s 内,基本规避过期问题
- 全局任务 ID + 步骤序号作为幂等 key(如
task:export_123:step_5),即使锁失效重复执行也不影响结果 - 配合 Redis Stream 或数据库 status 字段记录执行状态,失败可重试、跳过已完成步骤
Hyperf 生态可用工具建议
不推荐手写全套 Lua + 协程调度逻辑。优先考虑:
- hyperf/redis-lock(社区维护):支持 client_id 校验、自动续期协程管理、context 取消感知
- 基于 hyperf/async-queue + Redis 锁封装:把耗时任务扔进异步队列,加锁粒度下沉到 job 层,天然解耦协程生命周期
- 若用 Redis Cluster,避免 Redlock(性能差、实现复杂),改用单节点高可用 Redis(如哨兵)+ 客户端续期,更符合 Hyperf 实际部署场景


















