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安全释放
  • 锁唯一标识

这些设计组合在一起,才能真正成为一个稳定可靠的 分布式锁系统