前言
在分布式的环境下,对共享资源的并发修改,普通的本地锁显得无能为力。这时我们需要分布式锁解决问题,基于 Redis 构建的分布式锁则是一种常见的实现。
基本要求
一个分布式锁至少要保证 3 个属性
安全:互斥,任何情况下,只有一个客户端能够持有锁。无死锁:即使持有锁的客户端奔溃或分区,其它活动的客户端仍然可以获得锁。容错:只要大部分的 Redis 节点存活,客户端可以获得锁。
WATCH
在事务执行前,由客户端 watch(监视)的 key,如果被其它的客户端修改,则在事务执行时失败。watch 并不能够阻止其它客户端修改数据,被称为乐观锁。
基于 Redis 构建的分布式锁
简易锁
锁的基本操作:获取锁 -> 执行操作 -> 释放锁。简易锁对锁的基本操作做了基本实现,在一些情况下并不能正常运作。但这是一个健壮的分布式锁的基本,后面会对可能面临到的问题,对锁操作实现做改进。
1 | |
失败自动重试
在简易锁中,一旦锁获取失败,则直接返回。如果想让获取锁失败后,不断尝试获取,直到成功。又或者在给出的超时时间范围内,重试获取锁。
1 | |
死锁
当一个进程获取锁后,在释放锁之前奔溃,导致锁无法释放,陷入死锁。通过对锁设置过期时间,可以让锁在指定时间后得到释放。
1 | |
误释放
进程 A 获取到锁后,执行操作… 嗯,可能需要点时间。
而就在这个时候,锁的过期时间到了。被释放后,由进程 B 获取到。没过多久,A 执行完操作,并执行释放锁操作,导致 B 获取的锁被释放。可以通过在加锁时,设置一个标识符并返回,在释放锁时通过匹配标识符进行释放。
1 | |
Redlock
上述的分布式锁运行在单实例的 Redis 中 ,表面上能够很好的运行,实际存在单点故障的问题。没问题,我们加个 slave 节点吧。但由于复制机制是异步进行的,会违反基本要求中的安全属性。
- 客户端 A 从 master 获得锁。
- master 在将 lock 的 key 传输给 slave 之前奔溃。
- slave 晋升为 master。
- 客户端 B 从新的 master 中获得锁,B 持有的锁与 A 持有的锁,锁定同一个资源。
对于基于 Redis 的分布式锁实现,作者给出了 Redlock[3] 算法。它使用多 Redis 实例解决单点故障问题,实例间完全独立避免了异步复制带来的问题。
算法描述
- 客户端获取当前时间(毫秒)。
- 依次从 N 个 Redis 相互独立的节点,使用相同的 key 和随机值,尝试获取锁。对于客户端获取锁的超时时间,应该小于锁的自动释放时间。比如,锁的自动释放时间为 10s,那么客户端获取这个锁的超时时间可以在 5~50ms 之间。这样做,可以防止客户端向一个不可用的 Redis 节点请求等待过多的时间,当服务节点不可用时,应尽快转向下一个节点。
- 客户端计算出向每个节点获取锁的时间,使用当前时间减去步骤1获取的时间戳。当客户端获取大多数的锁(N/2 + 1,分布式环境中讨论的 N 通常为奇数),且获取所有锁的总时间小于锁的自动释放时间,这样的锁才可以被使用。
- 如果锁被获取,那么它的有效时间应该为:初始的有效时间 - 步骤3获取锁的时间。
- 如果客户端获取锁失败,它会去释放所有实例上的锁(即使是那些它无法锁定的实例)。
Redisson 实现 Redlock
Redisson 是 Java 的一个 Redis 客户端,提供了 Redlock 实现。
1 | |
参考
[1] Josiah L. Carlson 著. 黄健宏 译. Redis实战. 6.2 分布式锁