为了保证并发访问的正确性,Redis 提供了两种方法,分别是加锁和原子操作。
原子操作
- 单命令操作(INCR/DECR);
- 把多个操作写到一个 Lua 脚本中,以原子性方式执行单个 Lua 脚本
分布式锁
分布式锁的加锁和释放锁的过程,涉及多个操作。需要保证这些锁操作的原子性;
共享存储系统保存了锁变量,需要考虑保证共享存储系统的可靠性,进而保证锁的可靠性。
单个 Redis 节点的分布式锁
加锁
SET key value [EX seconds | PX milliseconds] [NX]
key 不存在, key 会被创建。
Value 要具有唯一性。这个是为了在解锁的时候,需要验证 Value 是和加锁的一致才删除 Key。
过期时间是为了避免操作共享数据时发生了异常,结果一直没有执行最后的 DEL 命令释放锁。
解锁
执行完业务逻辑后,使用 DEL 命令删除锁变量,从而释放锁(释放锁涉及到两条指令,这两条指令不是原子性的,通过执行一段lua脚本)。
问题
- 锁过期的问题
- 获取不到锁直接不断尝试获取锁,比较消耗性能
多个 Redis 节点的高可靠分布式锁
分布式锁算法 Redlock
客户端获取当前时间。
客户端按顺序依次向 N 个 Redis 实例执行加锁操作。
一旦客户端完成了和所有 Redis 实例的加锁操作,客户端就要计算整个加锁过程的总耗时
客户端从超过半数(大于等于 N/2+1)的 Redis 实例上成功获取到了锁并且客户端获取锁的总耗时没有超过锁的有效时间,加锁成功
别人建立了一把分布式锁,你就得不断轮询去尝试获取锁。
缺点
无法保证加锁的过程一定正确
开源框架:Redission
企业级的开源 Redis Client,也提供了分布式锁的支持。
- 所有指令都通过 Lua 脚本执行,Redis 支持 Lua 脚本原子性执行。
- 设置一个 Key 的默认过期时间为 30s, Watchdog 会在你获取锁之后,每隔 10s 帮你把 Key 的超时时间设为 30s。
优点
一直持有锁也不会出现 Key 过期了,其他线程获取到锁的问题;
Zookeeper 实现
- 使用 ZK 的临时节点和有序节点,每个线程获取锁就是在 ZK 创建一个临时有序的节点,比如在 /lock/ 目录下。
- 创建节点成功后,获取 /lock 目录下的所有临时节点,再判断当前线程创建的节点是否是所有的节点的序号最小的节点。
- 如果当前线程创建的节点是所有节点序号最小的节点,则认为获取锁成功。
- 如果当前线程创建的节点不是所有节点序号最小的节点,则对节点序号的前一个节点添加一个事件监听。
对比
Redis 的分布式锁
缺点
- 它获取锁的方式简单粗暴,获取不到锁直接不断尝试获取锁,比较消耗性能。
- 即便使用 Redlock 算法来实现,在某些复杂场景下,也无法保证其实现 100% 没有问题。
- Redis 的设计定位决定了它的数据并不是强一致性的,在某些极端情况下,可能会出现问题。锁的模型不够健壮。比如,锁过期问题。
优点
Redis 的性能很高,可以支撑高并发的获取、释放锁操作。
ZK 分布式锁
优点
- ZK 天生设计定位就是分布式协调,强一致性。锁的模型健壮、简单易用、适合做分布式锁。
- 如果获取不到锁,只需要添加一个监听器就可以了,不用一直轮询,性能消耗较小。
缺点
如果有较多的客户端频繁的申请加锁、释放锁,对于 ZK 集群的压力会比较大。