直接使用 redisstore 在哨兵或集群模式下可能失效,因其依赖单节点原子命令;需按部署模式选用对应 store,显式处理锁续期与释放,并确保分片一致性。

为什么直接用 Symfony\Component\Lock\Store\RedisStore 可能不生效
RedisStore 默认使用 SET key value EX seconds NX 实现加锁,但若 Redis 配置了哨兵(Sentinel)或集群(Cluster),单节点命令可能被重定向或拆分,导致 NX 语义失效——锁被多个客户端同时获取。更隐蔽的问题是:PHP 进程崩溃、超时未释放、或锁续期(renew)失败时,RedisStore 不会自动兜底清理,容易形成死锁。
- 确认你的 Redis 是单节点、哨兵还是集群模式;集群模式必须改用
RedisClusterStore,且确保所有 key 的 slot 落在同一节点(靠 key 前缀哈希对齐) - 不要依赖默认的
ttl=30,业务耗时超过这个值必须显式调用$lock->refresh() - 在
try...finally中释放锁,而不是仅靠__destruct—— PHP-FPM 子进程回收不可控
如何安全地加锁并处理续期
分布式锁的核心不是“加一次”,而是“持有期间持续有效”。Symfony 的 Lock 接口支持 acquire() 和 refresh(),但刷新失败不会抛异常,需手动检查返回值。
-
$lock = $lockFactory->createLock('order:12345');—— 锁名建议带业务前缀和唯一标识,避免不同订单冲突 -
if (!$lock->acquire()) { throw new \RuntimeException('Failed to acquire lock'); }—— 必须判断返回值,acquire()返回bool,不是异常驱动 - 在循环或长任务中,每
ttl/2时间调用一次$lock->refresh(),它返回bool;失败时应主动中断逻辑,不能继续执行 - 不要在
catch块里释放锁——异常可能发生在加锁前;释放操作只应在finally或明确成功路径后执行
Redis Cluster 下的 key 分片陷阱
Redis Cluster 将 key 按 CRC16 哈希分片,而 RedisStore 的内部 key(如 lock:order:12345)和信号 key(如 lock:order:12345:signal)可能落在不同节点,导致 PUBLISH 失效、阻塞等待永远不唤醒。
- 改用
Symfony\Component\Lock\Store\RedisClusterStore,它会强制所有相关 key 使用相同哈希标签({...}) - 锁名必须包含显式标签,例如:
$lockFactory->createLock('{order:12345}:process'),大括号内内容决定分片 hash - 验证方式:在 Redis CLI 执行
CLUSTER KEYSLOT "{order:12345}:process"和CLUSTER KEYSLOT "{order:12345}:signal",结果必须一致 - 如果已有锁名无法加标签(如来自外部 ID),可在创建前做一次哈希映射:
$shardKey = '{' . substr(md5($id), 0, 8) . '}:' . $id
别忽略锁的可见性与调试成本
线上出问题时,你没法 var_dump($lock) 看状态。锁是否生效、谁持有、剩余时间,全靠 Redis 里的原始 key 决定。
- 用
redis-cli --raw keys "lock:*"查看活跃锁,注意部分 store 会加前缀(如symfony_lock:) - 读取锁内容:
get lock:order:12345—— 值是 JSON,含 owner、expires、token 字段;过期时间是毫秒时间戳,不是 TTL - 不要手动
DEL锁 key,除非确认 owner 已失效;否则可能释放别人正在使用的锁 - 开发环境可启用
Symfony\Component\Lock\Store\LoggingStore包一层,记录 acquire/refresh/release 的时间与结果
最常被跳过的一步:没验证锁是否真的互斥。哪怕配置全对,也得写个并发压测脚本,用两个进程争同一个锁,观察是否只有一个能进临界区——否则所有配置都是纸面正确。











