Redis分布式锁
很多人在刚接触分布式系统的时候,都会遇到一个问题:
多个服务实例同时处理同一件事情,如何避免数据被重复处理?
例如:
- 用户抢优惠券
- 定时任务执行
- 库存扣减
- 订单状态更新
如果系统只有一个进程,其实很简单,用 本地锁(mutex) 就能解决。
但在微服务架构或者集群部署之后,问题就变了。
系统可能有:
- 10个服务实例
- 100个Worker
- 甚至多个数据中心
这时候,本地锁就完全失效了,因为不同进程之间根本不知道彼此的锁状态。
于是就出现了一个概念:
分布式锁(Distributed Lock)
分布式锁的目标很简单:
在分布式环境下,保证某一时刻只有一个节点能执行某段逻辑。
为什么 Redis 可以做分布式锁?
在实现分布式锁的时候,很多人第一反应是数据库。
例如:
select ... for update
但数据库锁的问题是:
- 性能差
- 锁粒度大
- 并发高时压力很大
于是大家开始寻找一个更适合做锁的系统。
Redis就非常合适。
原因很简单:
Redis天然具备三个优势:
1)单线程模型
Redis核心命令是单线程执行的。
这意味着:
同一时间只有一个命令在执行。
所以像下面这种操作:
SET key value NX
是 绝对原子操作。
不会出现竞争条件。
2)内存操作,速度极快
Redis所有数据都在内存中。
一次锁操作通常只需要:
- 一次 SET
- 一次 DEL
延迟通常在:
几十微秒级别
远远比数据库快。
3)支持自动过期
Redis key 可以设置 TTL。
例如:
SET lock:order 123 NX EX 10
含义是:
- 不存在才创建
- 10秒自动过期
这样即使服务崩溃,锁也不会永远卡住。
Redis锁释放
释放锁时不能直接 DEL。
因为可能发生这种情况:
线程A获取锁
线程A执行超时
锁过期
线程B获取锁
线程A执行完删除锁
这时候线程A会误删线程B的锁。
正确做法是:
只删除自己持有的锁。
通常通过 Lua 脚本实现:
if redis.call("GET",KEYS[1]) == ARGV[1] then
return redis.call("DEL",KEYS[1])
else
return 0
end
这样才能保证安全释放。
redis分布式锁的几个经典问题
虽然Redis锁很简单,但在真实系统中会遇到很多坑。
最常见的几个问题是:
锁过期问题
如果业务执行时间超过TTL:
锁就会提前释放。
其他节点就可能同时进入。
解决办法:
自动续期(WatchDog)。
例如:
- 锁10秒
- 根据过期时间最后30%的时间进行自动秒续期一次,并添加抖动时间,防止在一个时刻进行续期,对Redis进行集中请求
只要任务还在执行,锁就不会过期。
Redis单点问题
如果Redis挂掉:
所有锁都会失效。
常见解决方案是:
- Redis Sentinel
- Redis Cluster
- RedLock算法
Redis AP设计与 ETCD,Zookeeper CP 架构区别
Redis本身是一个偏 AP(可用性优先) 的系统。
也就是说:
在网络分区或者节点故障的时候,Redis会优先保证服务可用,而不是保证数据绝对一致。
这就可能带来一个风险:
在极端情况下,可能同时出现两个客户端都认为自己拿到了锁。
例如:
客户端A在主节点获取了锁
主节点还没来得及同步到从节点
主节点突然宕机
从节点被提升为新的主节点
这时候新的主节点并不知道A已经加过锁。
客户端B就可能再次获取锁。
这样就会出现:
两个客户端同时持有锁。
这就是Redis分布式锁在理论上的一致性风险。
相比之下,像 etcd 或 ZooKeeper 这样的系统是基于 CP模型(强一致) 设计的。
它们内部通过 Raft 或 ZAB 协议 保证多数节点确认之后才会提交数据。
因此:
一旦锁被获取成功,整个集群都会达成一致。
不会出现多个客户端同时拿到锁的情况。
所以在工程实践中通常有一个经验:
如果是 业务级锁(例如库存、任务控制)
Redis分布式锁通常已经足够使用。
但如果是 强一致要求很高的场景
例如:
金融交易
全局调度系统
分布式协调服务
很多系统会选择:
etcd / ZooKeeper 这种强一致的锁实现。
Redis分布式锁总结
Redis分布式锁的核心思想其实非常简单
利用Redis原子操作实现互斥。
它通常包含三个关键点:
1)原子加锁
2)安全释放
3)自动过期或续期
在实际工程中,一套成熟的Redis锁通常还会包含:
重试机制
- WatchDog自动续期
- Lua安全释放
- 锁唯一标识
这些设计组合在一起,才能真正成为一个稳定可靠的 分布式锁系统。