开发者社区> 问答> 正文

一般实现分布式锁都有哪些方式?使用 Redis 如何设计分布式锁?使用 zk 来设计分布式锁可以吗?

面试题

一般实现分布式锁都有哪些方式?使用 Redis 如何设计分布式锁?使用 zk 来设计分布式锁可以吗?这两种分布式锁的实现方式哪种效率比较高?

面试官心理分析

其实一般问问题,都是这么问的,先问问你 zk,然后其实是要过渡到 zk 相关的一些问题里去,比如分布式锁。因为在分布式系统开发中,分布式锁的使用场景还是很常见的。

面试题剖析

Redis 分布式锁 官方叫做 RedLock 算法,是 Redis 官方支持的分布式锁算法。

这个分布式锁有 3 个重要的考量点:

  • 互斥(只能有一个客户端获取锁)
  • 不能死锁
  • 容错(只要大部分 Redis 节点创建了这把锁就可以)

Redis 最普通的分布式锁

第一个最普通的实现方式,就是在 Redis 里使用 SET key value [EX seconds] [PX milliseconds] NX 创建一个 key,这样就算加锁。其中:

  • NX:表示只有 key 不存在的时候才会设置成功,如果此时 redis 中存在这个 key,那么设置失败,返回 nil。
  • EX seconds:设置 key 的过期时间,精确到秒级。意思是 seconds 秒后锁自动释放,别人创建的时候如果发现已经有了就不能加锁了。
  • PX milliseconds:同样是设置 key 的过期时间,精确到毫秒级。 比如执行以下命令:

1.PNG

Redis 官方给出了以上两种基于 Redis 实现分布式锁的方法,详细说明可以查看:

zk 分布式锁 zk 分布式锁,其实可以做的比较简单,就是某个节点尝试创建临时 znode,此时创建成功了就获取了这个锁;这个时候别的客户端来创建锁会失败,只能注册个监听器监听这个锁。释放锁就是删除这个 znode,一旦释放掉就会通知客户端,然后有一个等待着的客户端就可以再次重新加锁。

/**

  • ZooKeeperSession */ 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 */ 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();
        }
    }
    

    }

    /** * 封装单例的静态内部类 */

    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(); }

}

也可以采用另一种方式,创建临时顺序节点:

如果有一把锁,被多个人给竞争,此时多个人会排队,第一个拿到锁的人会执行,然后释放锁;后面的每个人都会去监听排在自己前面的那个人创建的 node 上,一旦某个人释放了锁,排在自己后面的人就会被 ZooKeeper 给通知,一旦被通知了之后,就 ok 了,自己就获取到了锁,就可以执行代码了。

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 获取锁的那个客户端 出现 bug 挂了,那么只能等待超时时间之后才能释放锁;而 zk 的话,因为创建的是临时 znode,只要客户端挂了,znode 就没了,此时就自动释放锁。

Redis 分布式锁大家没发现好麻烦吗?遍历上锁,计算时间等等......zk 的分布式锁语义清晰实现简单。

所以先不分析太多的东西,就说这两点,我个人实践认为 zk 的分布式锁比 Redis 的分布式锁牢靠、而且模型简单易用。

往期回顾:

【Java问答学堂】1期 为什么使用消息队列?消息队列有什么优点和缺点?Kafka、ActiveMQ、RabbitMQ、RocketMQ 都有什么区别,以及适合哪些场景?

【Java问答学堂】2期 如何保证消息队列的高可用?

【Java问答学堂】3期 如何保证消息不被重复消费?或者说,如何保证消息消费的幂等性?

【Java问答学堂】4期 如何保证消息的可靠性传输?(如何处理消息丢失的问题?)

【Java问答学堂】5期 如何保证消息的顺序性?

【Java问答学堂】6期 如何解决消息队列的延时以及过期失效问题?

【Java问答学堂】7期 如果让你写一个消息队列,该如何进行架构设计?

【Java问答学堂】8期 es 的分布式架构原理能说一下么(es 是如何实现分布式的啊)?

【Java问答学堂】9期 es 写入数据的工作原理是什么啊?es 查询数据的工作原理是什么啊?

【Java问答学堂】10期 es 在数据量很大的情况下(数十亿级别)如何提高查询效率啊?

【Java问答学堂】11期 es 生产集群的部署架构是什么?每个索引的数据量大概有多少?

【Java问答学堂】12期 项目中缓存是如何使用的?为什么要用缓存?缓存使用不当会造成什么后果?

【Java问答学堂】13期 redis 和 memcached 有什么区别?

【Java问答学堂】14期 redis 都有哪些数据类型?分别在哪些场景下使用比较合适?

【Java问答学堂】15期redis 的过期策略都有哪些?内存淘汰机制都有哪些?

【Java问答学堂】16期如何保证 redis 的高并发和高可用?redis 的主从复制原理能介绍

为什么使用消息队列?【Java问答学堂】17期

消息队列有什么优点和缺点?【Java问答学堂】18期

Kafka、ActiveMQ、RabbitMQ、RocketMQ的区别?【Java问答学堂】19期

如何保证消息队列的高可用?【Java问答学堂】20期

如何保证消息不被重复消费?或者说,如何保证消息消费的幂等性?【Java问答学堂】21期

如何保证消息的可靠性传输?或者说,如何处理消息丢失的问题?【Java问答学堂】22期

如何保证消息的顺序性?【Java问答学堂】23期

如何解决消息队列的延时以及过期失效问题?【Java问答学堂】24期

如果让你写一个消息队列,该如何进行架构设计?【Java问答学堂】25期

ES 的分布式架构原理能说一下么(ES 是如何实现分布式的啊)?【Java问答学堂】26期

ES 写入数据的工作原理是什么啊?ES 查询数据的工作原理是什么啊?【Java问答学堂】27期

ES 在数据量很大的情况下(数十亿级别)如何提高查询效率啊?【Java问答学堂】28期

ES 生产集群的部署架构是什么?每个索引的数据量大概有多少?【Java问答学堂】29期

项目中缓存是如何使用的?为什么要用缓存?缓存使用不当会造成什么后果?【Java问答学堂】30期

Redis 和 Memcached 的区别?Redis 的线程模型是什么?【Java问答学堂】31期

Redis 都有哪些数据类型?分别在哪些场景下使用比较合适?【Java问答学堂】32期

Redis 的过期策略都有哪些?内存淘汰机制都有哪些?手写一下 LRU 代码实现?【Java问答】33期

如何保证 redis 的高并发和高可用?【Java问答】34期

Redis 的持久化有哪几种方式?【Java问答】35期

Redis 集群模式的工作原理能说一下么?【Java问答】36期

了解什么是 Redis 的雪崩、穿透和击穿?Redis 崩溃之后会怎么样?【Java问答】37期

如何保证缓存与数据库的双写一致性?【Java问答】38期

Redis 的并发竞争问题是什么?如何解决这个问题?【Java问答】39期

生产环境中的 Redis 是怎么部署的?【Java问答】40期

为什么要分库分表(设计高并发系统的时候,数据库层面该如何设计)?【Java问答】41期

如何设计才可以让系统从未分库分表动态切换到分库分表上?【Java问答】42期

分库分表之后,id 主键如何处理?【Java问答】43期

你们有没有做 MySQL 读写分离?如何实现 MySQL 的读写分离?【Java问答】44期

如何设计一个高并发系统?【Java问答学堂】45期

为什么要进行系统拆分?如何进行系统拆分?拆分后不用 dubbo 可以吗?【Java问答学堂】46期

dubbo 的工作原理?注册中心挂了的问题?说说一次 rpc 请求的流程?【Java问答】47期

dubbo 支持的通信协议?有哪些序列化协议?说下 Hessian 的数据结构?【Java问答】48期

dubbo 负载均衡策略和集群容错策略都有哪些?动态代理策略呢?【Java问答学堂】49期

dubbo 的 spi 思想是什么?【Java问答学堂】50期

如何基于 dubbo 进行服务治理、服务降级、失败重试以及超时重试?【Java问答学堂】51期

分布式服务接口的幂等性如何设计(比如不能重复扣款)?【Java问答学堂】52期

分布式服务接口请求的顺序性如何保证?【Java问答学堂】53期

如何自己设计一个类似 Dubbo 的 RPC 框架?【Java问答学堂】54期

分布式系统 CAP 定理 P 代表什么含义【Java问答学堂】55期

zookeeper 都有哪些使用场景?【Java问答学堂】56期

展开
收起
请回答1024 2020-07-14 09:42:35 4715 0
1 条回答
写回答
取消 提交回答
  • 收藏~

    2020-07-15 09:56:33
    赞同 展开评论 打赏
问答排行榜
最热
最新

相关电子书

更多
Redis集群演化的心路历程——从2.x到3.0时代 立即下载
微博的Redis定制之路 立即下载
云数据库Redis版的开源之路 立即下载