redis分布式锁在thinkphp中易失效的根本原因是加锁未保证原子性、过期时间设置不合理、释放锁时缺乏token校验;需用lua脚本实现set nx ex与del前校验token,避免误删或死锁。

Redis 分布式锁为什么在 ThinkPHP 里容易失效
根本原因不是 ThinkPHP 不支持,而是默认的 Cache::lock() 或手写 setnx 逻辑没处理好原子性、过期时间、锁释放校验这三件事。常见现象是:并发请求下锁被误删(A 拿到锁超时了,B 续上了,但 A 还是执行了 del)、锁永远不释放(进程崩溃没走释放逻辑)、或多个服务实例用同一 key 冲突。
用 Redis::set() + Lua 脚本保证加锁原子性
ThinkPHP 6+ 的 think-redis 扩展支持原生命令,但 set 命令必须带 nx 和 ex 参数,且不能拆成 exists+set 两步。推荐直接调 Lua 脚本防竞争:
redis->eval("return redis.call('set', KEYS[1], ARGV[1], 'NX', 'EX', ARGV[2])", 1, $lockKey, $randomToken, $expire)
其中 $randomToken 是每个请求唯一字符串(比如 uniqid('', true)),后续释放锁时必须校验它,避免误删。
- 别用
Cache::lock()->once()做临界区控制——它底层没做 token 校验,B 可能删掉 A 的锁 -
$expire建议设为业务最大耗时的 2–3 倍,太短易自动过期,太长会阻塞后续请求 - ThinkPHP 5.1 的
think-cache默认用 file 驱动,切 Redis 必须显式配置type=redis并确认host/port正确
释放锁必须用 Lua 脚本校验 token
不能简单写 $redis->del($lockKey),否则 A 请求超时后仍执行 del,就把 B 正在用的锁干掉了。正确方式是:
redis->eval("if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end", 1, $lockKey, $randomToken)
这个脚本只在 key 存在且值匹配时才删,否则返回 0。ThinkPHP 中可封装成一个独立方法,每次加锁时把 $randomToken 存到本地变量或 Request 对象里,确保释放时能拿到同一个值。
- 别把 token 存 session 或 cookie——分布式环境下不可靠
- 如果用 Swoole 长连接,注意 Redis 实例复用时 token 作用域别污染
- 某些云 Redis(如阿里云 Tair)禁用 eval,得改用
SET key value NX PX timeout+ 单独 GET 校验,但会损失原子性,需加重试
高并发下锁等待与降级策略
单纯死等 while(!acquire()) sleep(10) 会导致大量请求堆积。ThinkPHP 应用里更实用的做法是:
- 用
redis->set($key, $val, ['nx', 'ex' => 10])尝试一次,失败就走降级逻辑(比如返回缓存旧数据、记录告警、或抛LockTimeoutException) - 需要重试时,用指数退避:
usleep(rand(10000, 50000) * pow(2, $retry)) - 关键路径(如库存扣减)建议加 Redis 计数器限流,提前拦截大部分无效请求,减少锁竞争概率
真正难的不是写对一行 eval,而是想清楚哪个环节允许失败、哪个必须强一致、以及锁粒度是否合理——比如用「用户ID」做锁 key 比用「订单ID」更易导致串行,但比全局锁更可控。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











