Redis锁在Yii中应使用yii\redis\Connection的executeCommand方法配合SET NX EX实现原子加锁,解锁需用Lua脚本校验token,key须带业务上下文且长度≤128字符,token需唯一,超时时间按业务耗时2~3倍设置。

Redis锁在Yii里用 yii\redis\Connection 直接操作最稳
Yii自带的 yii\redis\Connection 提供了原生命令支持,比封装层更可控。很多项目图省事用 yii\redis\Cache 做锁,结果发现 set() 不带 NX 和 EX 参数,根本没法原子性加锁。
常见错误现象:set('lock:order:123', '1') 后没校验是否真拿到了锁,多个请求同时写入,锁失效;或者用 exists() + set() 两步操作,中间被其他请求插队。
- 必须用
executeCommand('SET', ['lock:key', 'token', 'NX', 'EX', 30]),NX保证只在 key 不存在时设置,EX防止死锁 - 返回值是
'OK'才算真正拿到锁,其他情况(如false或null)要立刻拒绝后续逻辑 - 不要依赖
yii\redis\Cache::set(),它不支持原子性条件写入
释放锁必须用 Lua 脚本校验 token
直接 del('lock:key') 是危险的——A 拿到锁,超时前没执行完,B 又抢到了新锁,这时 A 一删就把 B 的锁干掉了。Yii 没内置解锁脚本,得自己写。
使用场景:订单提交、库存扣减、支付回调幂等处理,任何需要“谁加的锁谁才能解”的地方。
- 解锁脚本必须先
GET再比对 token,再DEL,三步必须原子执行 - 推荐这段 Lua:
if redis.call("GET", KEYS[1]) == ARGV[1] then return redis.call("DEL", KEYS[1]) else return 0 end - 在 Yii 中调用:
$redis->executeCommand('EVAL', [$luaScript, 1, 'lock:key', $token]),$token必须是加锁时生成的唯一值(比如uniqid('', true))
Yii控制器里加锁别卡在 beforeAction,得包住具体业务逻辑
有人把锁逻辑塞进 beforeAction(),结果整个 action 生命周期都占着锁,包括渲染视图、日志记录这些无关操作,锁持有时间远超必要。
性能影响:锁时间越长,并发吞吐越低;兼容性上,Redis 连接池也可能因阻塞变紧张。
- 锁只应包裹真正需要互斥的代码块,比如“查库存 → 扣减 → 写订单”这一段
- 记得 try/finally,确保无论成功失败都尝试解锁(但解锁失败不能抛异常中断主流程)
- 别在事务里嵌套 Redis 锁,MySQL 事务和 Redis 锁无关联,两者失败策略不同,容易误判
锁 key 设计要带业务上下文,避免全局冲突
用固定 key 如 lock:submit 看似简单,实际会让所有提交请求串行,完全丧失并发能力。真正的重复提交,往往针对同一笔订单、同一个用户、同一次表单 ID。
容易踩的坑:key 里拼接变量但没过滤特殊字符,导致 key 含空格或冒号,Redis 拒绝写入;或者 key 过长,影响 Redis 内存和集群分片。
- 推荐格式:
lock:order_submit:{orderId}、lock:form:{formId},用大括号明确占位符 - orderId 等变量需做基础校验(如正则
/^\d+$/),防止注入非法字符 - key 总长度建议控制在 128 字符内,过长会影响 Redis Cluster 的 slot 计算
token 生成和锁超时时间这两个点,最容易被忽略:token 不唯一会导致误删,超时设太短会业务没跑完锁就没了,设太长又拖慢恢复速度。得按接口平均耗时 × 2~3 倍来定,再加一点缓冲。


















