Advertisement

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集群的一把锁控制而执行以下步骤。

  1. 获取当前时刻的时间戳,并以毫秒为单位进行记录。
  2. 与前文所述相同,在各个 master节点之间依次申请锁定。
  3. 多数情况下,在多个节点中设置共享的锁定机制。
  4. 客户端计算完成锁定操作所需的时间。当所花锁定时间不超过预设的超时阈值时,则认为此次锁定成功。
  5. 当某次锁定申请失败时,则依次删除之前尝试申请成功的相关锁定项。
  6. 只要其他系统或节点已经实现了分布式互斥机制(即所谓的分布式锁),就需要持续不断地进行探测以确认其是否存在。
redis-redlock

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 的分布式锁更加稳定可靠 并且模型设计更为简单易懂

全部评论 (0)

还没有任何评论哟~