Zookeeper分布式锁和Redis分布式锁的对比
1 问题分析:
有哪些常见的实现方法?在采用 Redis 进行分布式锁的设计时应遵循哪些原则?在设计分布式锁时是否考虑过 ZK 机制?比较之下,在性能上有优势的是哪一种方法?
通常提问时,人们往往采用这种方式询问. 为了更好地了解你的ZK情况,我们可能会先询问一些基本信息,并进而转向探讨与ZK相关的问题. 由于在分布式系统开发中应用广泛而常见.
2 面试题回答:
redis 分布式锁
官方叫做 RedLock 算法,是 redis 官方支持的分布式锁算法。
这个分布式锁有 3 个重要的考量点:
- exclusive(仅限一个客户端能够获取锁)
- 避免出现死锁问题
- 错耐力机制(只要大多数 Redis 节点创建了这把锁就可以)
redis 最普通的分布式锁
最基本的一种常见实现方法就是在 redis 中使用 setnx 命令创建一个 key 从而达到加锁的效果
SET resource_name my_random_value NX PX 30000
执行这个命令就 ok。
NX:仅在key不存在时被设置成功。(若此时 redis 中已有该 key,则操作失败并返回空值)PX:30秒:表示该锁将在30秒后自动释放。其他用户在创建此锁时若发现已有,则无法再进行加锁操作。
释放锁相当于删除 key ,通常可以通过 Lua 脚本实现 lock 的释放;只有当 value 相同时才会进行删除操作:
-- 删除锁的时候,找到 key 对应的 value,跟自己传过去的 value 做比较,如果是一样的才删除。
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
为什么选择使用random_value呢?这是因为如果某个客户端取得了锁并长时间等待后才完成操作(超过30秒),可能存在该锁已经被自动解除了这种情况。为了避免出现竞争条件(Race condition)问题,并且确保数据一致性(Data consistency),我们采用随机值配合Lua脚本的方式来进行手动解锁操作。
这种方法显然不可行。如果使用的是普通Redis单实例架构,则属于单点故障;若采用的是Redis普通主从架构,则采用异步复制机制,在这种情况下(主节点发生故障导致Key丢失),如果尚未完成对该Key在FromNode上的同步操作,则会将FromNode切换为主Node以便进行操作。此时他人即可通过设置该Key来获取锁权。
RedLock 算法
在这种情况下,在一个包含五个 Redis master实例的 Redis集群下,并为了实现对Redis集群的一把锁控制而执行以下步骤。
- 获取当前时刻的时间戳,并以毫秒为单位进行记录。
- 与前文所述相同,在各个 master节点之间依次申请锁定。
- 多数情况下,在多个节点中设置共享的锁定机制。
- 客户端计算完成锁定操作所需的时间。当所花锁定时间不超过预设的超时阈值时,则认为此次锁定成功。
- 当某次锁定申请失败时,则依次删除之前尝试申请成功的相关锁定项。
- 只要其他系统或节点已经实现了分布式互斥机制(即所谓的分布式锁),就需要持续不断地进行探测以确认其是否存在。

Redis 官方机构向大家介绍了其中一种基于 Redis 实现分布式锁的技术。关于这一技术的详细信息,请访问以下链接:https://redis.io/topics/distlock
zk 分布式锁
实现一个简单的zk分布式lock系统。当某个节点试图创建一个临时的znode时,在成功后即可获得对应的互斥lock。此时其他客户端试图在此时获取该lock将无法成功并必须注册一个监听器用于监控该lock的状态变化。当释放lock时需要执行删除操作并删除对应的znode。一旦该znode被删除所有等待其lock状态的客户端都可以重新发起加lock请求
/** * ZooKeeperSession
* * @author bingo
* @since 2018/11/29
* */
public class ZooKeeperSession {
private static CountDownLatch connectedSemaphore = new CountDownLatch(1);
private ZooKeeper zookeeper;
private CountDownLatch latch;
public ZooKeeperSession() {
try {
this.zookeeper = new ZooKeeper("192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181", 50000, new ZooKeeperWatcher());
try {
connectedSemaphore.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ZooKeeper session established......");
} catch (Exception e) {
e.printStackTrace();
}
}
/** * 获取分布式锁
* * @param productId
*/
public Boolean acquireDistributedLock(Long productId) {
String path = "/product-lock-" + productId;
try {
zookeeper.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
return true;
} catch (Exception e) {
while (true) {
try {
// 相当于是给node注册一个监听器,去看看这个监听器是否存在
Stat stat = zk.exists(path, true);
if (stat != null) {
this.latch = new CountDownLatch(1);
this.latch.await(waitTime, TimeUnit.MILLISECONDS);
this.latch = null;
}
zookeeper.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL);
return true;
} catch (Exception ee) {
continue;
}
}
}
return true;
}
/** * 释放掉一个分布式锁
* * @param productId
*/
public void releaseDistributedLock(Long productId) {
String path = "/product-lock-" + productId;
try {
zookeeper.delete(path, -1);
System.out.println("release the lock for product[id=" + productId + "]......");
} catch (Exception e) {
e.printStackTrace();
}
}
/** * 建立zk session的watcher
* * @author bingo
* @since 2018/11/29
* */
private class ZooKeeperWatcher implements Watcher {
public void process(WatchedEvent event) {
System.out.println("Receive watched event: " + event.getState());
if (KeeperState.SyncConnected == event.getState()) {
connectedSemaphore.countDown();
}
if (this.latch != null) {
this.latch.countDown();
}
}
}
/** * 封装单例的静态内部类
* * @author bingo
* @since 2018/11/29
* */
private static class Singleton {
private static ZooKeeperSession instance;
static {
instance = new ZooKeeperSession();
}
public static ZooKeeperSession getInstance() {
return instance;
}
}
/** * 获取单例
* * @return
*/
public static ZooKeeperSession getInstance() {
return Singleton.getInstance();
}
/** * 初始化单例的便捷方法
*/
public static void init() {
getInstance();
}
}
也可以采用另一种方式,创建临时顺序节点:
如果有一把锁被多个用户争夺权,则这些用户依次排队等待使用该lock。第一个拿到lock的人都将执行相应的操作后立即释放该lock以供他人使用。其余用户则会持续监听其之前的位置(即排在其前的那个人)创建的节点,并等待观察到目标节点是否已处于可解码状态。当某人成功解码其所在节点时(即其所在节点已解码),其后方位置上的所有用户都会通过zookeeper机制被提醒并完成解码过程并开始执行代码
public class ZooKeeperDistributedLock implements Watcher {
private ZooKeeper zk;
private String locksRoot = "/locks";
private String productId;
private String waitNode;
private String lockNode;
private CountDownLatch latch;
private CountDownLatch connectedLatch = new CountDownLatch(1);
private int sessionTimeout = 30000;
public ZooKeeperDistributedLock(String productId) {
this.productId = productId;
try {
String address = "192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181";
zk = new ZooKeeper(address, sessionTimeout, this);
connectedLatch.await();
} catch (IOException e) {
throw new LockException(e);
} catch (KeeperException e) {
throw new LockException(e);
} catch (InterruptedException e) {
throw new LockException(e);
}
}
public void process(WatchedEvent event) {
if (event.getState() == KeeperState.SyncConnected) {
connectedLatch.countDown();
return;
}
if (this.latch != null) {
this.latch.countDown();
}
}
public void acquireDistributedLock() {
try {
if (this.tryLock()) {
return;
} else {
waitForLock(waitNode, sessionTimeout);
}
} catch (KeeperException e) {
throw new LockException(e);
} catch (InterruptedException e) {
throw new LockException(e);
}
}
public boolean tryLock() {
try {
// 传入进去的locksRoot + “/” + productId
// 假设productId代表了一个商品id,比如说1
// locksRoot = locks
// /locks/10000000000,/locks/10000000001,/locks/10000000002
lockNode = zk.create(locksRoot + "/" + productId, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL);
// 看看刚创建的节点是不是最小的节点
// locks:10000000000,10000000001,10000000002
List<String> locks = zk.getChildren(locksRoot, false);
Collections.sort(locks);
if(lockNode.equals(locksRoot+"/"+ locks.get(0))){
//如果是最小的节点,则表示取得锁
return true;
}
//如果不是最小的节点,找到比自己小1的节点
int previousLockIndex = -1;
for(int i = 0; i < locks.size(); i++) {
if(lockNode.equals(locksRoot + “/” + locks.get(i))) {
previousLockIndex = i - 1;
break;
}
}
this.waitNode = locks.get(previousLockIndex);
} catch (KeeperException e) {
throw new LockException(e);
} catch (InterruptedException e) {
throw new LockException(e);
}
return false;
}
private boolean waitForLock(String waitNode, long waitTime) throws InterruptedException, KeeperException {
Stat stat = zk.exists(locksRoot + "/" + waitNode, true);
if (stat != null) {
this.latch = new CountDownLatch(1);
this.latch.await(waitTime, TimeUnit.MILLISECONDS);
this.latch = null;
}
return true;
}
public void unlock() {
try {
// 删除/locks/10000000000节点
// 删除/locks/10000000001节点
System.out.println("unlock " + lockNode);
zk.delete(lockNode, -1);
lockNode = null;
zk.close();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
public class LockException extends RuntimeException {
private static final long serialVersionUID = 1L;
public LockException(String e) {
super(e);
}
public LockException(Exception e) {
super(e);
}
}
}
redis 分布式锁和 zk 分布式锁的对比
- Redis分布式锁必须实现一定的机制以确保能够获得锁,并且这种机制往往会导致较高的性能消耗。
- 在ZK分布式锁中,在无法获得锁的情况下只需配置一个监听器即可完成注册过程,并且这种状态下的操作通常不会带来显著的性能开销。
另外一点需要注意的是,在使用 Redis 获取锁的客户端时,在发生故障后必须等待超时时间后才能解锁;而对于 ZK 协议中的临时 znode 创建方式而言,在客户端发生故障的情况下 znode 会随之消失并自动释放相关锁机制。
Redis 分布式的互斥锁确实让人感到有些头疼。每次获取互斥 lock 时需遍历节点列表,并计算整体操作时间。 Zk 的分布式互斥 lock 设计简洁且逻辑清晰。
所以不深入分析太多细节的情况下仅陈述两点个人实践经验表明 zk 的分布式锁相较于 redis 的分布式锁更加稳定可靠 并且模型设计更为简单易懂
