Redis SETNX+EXPIRE不能直接当分布式锁用,因为两命令非原子执行,进程崩溃会导致锁无过期时间而永久残留;正确做法是使用SET key value EX seconds NX原子命令。

Redis SETNX + 过期时间为什么不能直接当分布式锁用
因为 SETNX 和 EXPIRE 是两个独立命令,中间若进程崩溃或网络中断,会导致锁被设置但没设过期时间——锁永久残留。这不是理论风险,线上真实发生过。
正确做法是用原子命令:SET key value EX seconds NX(Redis 2.6.12+)。其中 NX 保证只在 key 不存在时设置,EX 绑定过期时间,整个操作不可拆分。
常见错误写法:
SETNX lock_key 1 EXPIRE lock_key 30 // ❌ 可能只执行了第一行
正确写法示例(PHP Redis 扩展):
$redis->set('order_lock:123', 'client_abc', ['NX', 'EX' => 30])
注意:value 必须唯一(比如 client ID + 时间戳),否则释放锁时无法区分归属,造成误删。
释放锁时为什么要用 Lua 脚本校验 value
直接 DEL lock_key 极其危险——A 拿到锁,超时前没处理完,锁自动过期;B 成功获取同一把锁;A 结束后执行 DEL,删掉的是 B 的锁,后续并发就失控了。
必须先检查 value 是否匹配,再删除,且这两步必须原子执行。Redis 不支持条件删除原生命令,所以用 Lua:
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
调用方式(Python redis-py):
lua_script = redis.register_script(lua_code) lua_script(keys=['order_lock:123'], args=['client_abc'])
要点:
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
- value 在加锁和解锁时必须完全一致(建议用 UUID 或
os.getpid() + time.time()组合) - 不要用
GET + DEL两步走,这是典型竞态漏洞 - Lua 脚本在 Redis 单线程中执行,天然原子
MySQL 事务里怎么安全配合 Redis 锁
Redis 锁只管“谁先拿到”,不管“事务是否成功提交”。常见陷阱:A 拿锁 → 查询库存 → 扣减 → 写入 DB → 事务中途失败回滚 → 但锁已释放 → B 拿锁重复扣减。
关键原则:锁的生命周期必须覆盖**整个业务逻辑闭环**,包括 DB 事务提交或回滚完成之后。
推荐结构(以扣库存为例):
- 用
SET ... NX EX获取锁,失败则快速返回或重试(带退避) - 开启 MySQL 事务(
BEGIN) - 执行业务查询与更新(如
SELECT ... FOR UPDATE) - 判断业务规则(如库存是否充足),不满足则
ROLLBACK并立即释放锁 - 满足则
COMMIT,再释放锁
特别注意:SELECT ... FOR UPDATE 必须走索引,否则会升级为表锁,拖垮性能;且该语句本身也受事务隔离级别影响(推荐 READ COMMITTED)。
为什么不能只靠 MySQL 行锁解决分布式竞争
MySQL 行锁只在单实例内生效。一旦数据库做了主从、分库分表或读写分离,SELECT ... FOR UPDATE 在从库无效,主库之间又没共享锁状态——跨节点请求仍会并发修改同一行。
更隐蔽的问题:长事务导致锁持有时间过长,阻塞其他请求;或者应用层重试机制触发多次相同请求,而 MySQL 无法识别这是“同一个业务动作”。
所以生产环境必须分层防御:
- Redis 分布式锁:控制入口并发(快、跨服务、可定制超时)
- MySQL 行锁 + 唯一约束:兜底防数据异常(如重复下单用
UNIQUE(order_no)) - 业务幂等设计:接口带业务流水号,DB 记录处理状态,重复请求直接返回
最易忽略的一点:Redis 锁的过期时间,必须大于「最长可能的业务执行时间 + 网络延迟 + 时钟漂移」,否则锁提前释放,防护就形同虚设。

















