Redis分布式锁不能只用SET key value NX PX,因其无法解决锁续期和崩溃残留问题:业务超时导致锁被抢占后原节点仍操作,引发脑裂;且SET不校验持有者身份,DEL会误删他人锁。

Redis 实现分布式锁为什么不能只用 SET key value NX PX
因为单命令看似原子,但无法解决锁续期和崩溃后残留问题。比如业务逻辑执行超时,锁过期被其他节点抢走,而原节点还继续操作——典型的“脑裂”。SET 本身不校验持有者身份,DEL key 会误删别人持有的锁。
正确做法是:用唯一 client_id(如 UUID)作为 value,加锁时 SET key client_id NX PX 30000,解锁时必须先 EVAL 脚本比对 value 再删:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
- 脚本必须用
EVAL(不是EVALSHA),避免预热缺失导致失败 - 锁超时时间要明显大于业务最大耗时,但也不能设成永不过期——否则节点宕机锁永远不释放
- 不要依赖系统时间做锁续期,Redis 服务器时间可能漂移;应由客户端主动调用
GETSET或PEXPIRE延长 TTL(需再次校验 value)
Etcd 的 CompareAndSwap(CAS)比 Redis 更适合强一致性场景
Etcd v3 的 Put 支持 Lease 和 CompareAndSwap 原语,天然支持租约自动过期 + 持有者校验。C++ 客户端(如 etcd-cpp-apiv3)中,加锁本质是:
auto txn = cli->txn();
txn->If({etcd::v3::Compare(etcd::v3::Version(key), "=", 0)});
txn->Then({etcd::v3::Put(key, client_id, etcd::v3::LeaseID(lease_id))});
auto resp = txn->Commit().get();
- 首次写入要求 version == 0(key 不存在),避免覆盖他人锁
- lease_id 必须由客户端先调用
LeaseGrant获取,且需后台线程定期LeaseKeepAlive续约 - 解锁只需
Deletekey,Etcd 会在 lease 过期时自动清理——不用手动删,也不怕误删 - 注意:Etcd 的
Compare不支持直接比 value,所以必须用 version 或 mod_revision 做首次准入控制
C++ 客户端选型直接影响锁的可靠性与性能
Redis 推荐用 redis-plus-plus(非 hiredis 原生封装),它支持连接池、异步 pipeline 和自动重连;Etcd 必须用支持 v3 gRPC 的客户端(如 etcd-cpp-apiv3),旧版 HTTP JSON 接口无事务、无 lease 管理能力。
立即学习“C++免费学习笔记(深入)”;
-
redis-plus-plus的ConnectionPool要设置合理 size(通常 8–32),避免高并发下连接争抢 - Etcd 客户端必须启用
KeepAlive参数(如grpc.keepalive_time_ms=30000),否则空闲连接会被中间设备断开,导致 lease 续约失败 - 不要在锁逻辑里混用同步/异步调用——比如用同步
Get判断锁状态,再用异步Put加锁,时序难控 - 所有网络调用必须设 timeout(如
redis-plus-plus的timeout参数),否则阻塞线程会拖垮整个服务
分布式锁不是万能的,别在高频路径上硬套
真正需要锁的地方其实很少:比如订单幂等扣减、库存预占、配置热更新生效点。95% 的“想加锁”场景,其实是数据结构设计或补偿机制没到位。
- 如果每秒要抢锁上千次,说明架构已偏离正轨——考虑改用分片(sharding)+ 本地锁,或用消息队列削峰
- Redis 集群模式下,key 必须落在同一 slot(用
{xxx}标记 hash tag),否则EVAL脚本跨节点执行会报错CROSSSLOT - Etcd 的线性一致性读(
WithSerializable(false))默认开启,但高负载下会影响吞吐;若允许短暂 stale read,可显式关闭以提升性能
最常被忽略的一点:锁的粒度。用 /order/{id} 锁单订单没问题,但用 /order 锁全部订单,就等于把分布式系统退化成单点。粒度错了,再高性能的实现也白搭。



















