Zookeeper实现分布式锁
1.分布式锁的由来:
在程序开发过程中不得不考虑的就是并发问题。在java中对于同一个jvm而言,jdk已经提供了lock和同步等。但是在分布式情况下,往往存在多个进程对一些资源产生竞争关系,而这些进程往往在不同的机器上,这个时候jdk中提供的已经不能满足。分布式锁顾明思议就是可以满足分布式情况下的并发锁。
确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:
1.互斥性。在任意时刻,只有一个客户端能持有锁。
2.不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
3.具有容错性。只要大部分的节点正常运行,客户端就可以加锁和解锁。
4.解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。
下面我们讲解怎么利用zk实现分布式锁。
实现分布式锁方式四种:
1:Zookeeper
2:Redis
3:memcached
4:Mysql
2.实现思路:
2.1 zk简单介绍:
ZooKeeper是Apache软件基金会的一个软件项目,他为大型分布式计算提供开源的分布式配置服务、同步服务和命名注册。在 ZooKeeper 中,节点类型可以分为持久节点(PERSISTENT )、临时节点(EPHEMERAL),以及时序节点(SEQUENTIAL ),具体在节点创建过程中,一般是组合使用,可以生成 4 种节点类型:持久节点(PERSISTENT),持久顺序节点(PERSISTENT_SEQUENTIAL),临时节点(EPHEMERAL),临时顺序节点(EPHEMERAL_SEQUENTIAL);具体节点含义,谷歌之。
2.2 利用zk实现:
当很多进程需要访问共享资源时,我们可以通过zk来实现分布式锁。主要步骤是:
1.建立一个节点,假如名为:lock 。节点类型为持久节点(PERSISTENT)
2.每当进程需要访问共享资源时,会调用分布式锁的lock()或tryLock()方法获得锁,这个时候会在第一步创建的lock节点下建立相应的顺序子节点,节点类型为临时顺序节点(EPHEMERAL_SEQUENTIAL),通过组成特定的名字name。
3.在建立子节点后,对lock下面的name子节点,判断刚刚建立的子节点是否存在,则获得该锁对资源进行访问。
4.假如存在,并给该节点是否存在注册监听事件。同时在这里阻塞。等待监听事件的发生,获得锁控制权。
5.当调用完共享资源后,调用unlock()方法,关闭zk,进而可以引发监听事件,释放该锁。
2.3:重要方法
CuratorFramework提供的方法:
方法名 | 描述 |
---|---|
create() | 开始创建操作, 可以调用额外的方法(比如方式mode 或者后台执行background) 并在最后调用forPath()指定要操作的ZNode |
delete() | 开始删除操作. 可以调用额外的方法(版本或者后台处理version or background)并在最后调用forPath()指定要操作的ZNode |
checkExists() | 开始检查ZNode是否存在的操作. 可以调用额外的方法(监控或者后台处理)并在最后调用forPath()指定要操作的ZNode |
getData() | 开始获得ZNode节点数据的操作. 可以调用额外的方法(监控、后台处理或者获取状态watch, background or get stat) 并在最后调用forPath()指定要操作的ZNode |
setData() | 开始设置ZNode节点数据的操作. 可以调用额外的方法(版本或者后台处理) 并在最后调用forPath()指定要操作的ZNode |
getChildren() | 开始获得ZNode的子节点列表。 以调用额外的方法(监控、后台处理或者获取状态watch, background or get stat) 并在最后调用forPath()指定要操作的ZNode |
inTransaction() | 开始是原子ZooKeeper事务. 可以复合create, setData, check, and/or delete 等操作然后调用commit()作为一个原子操作提交 |
后台操作的通知和监控可以通过ClientListener接口发布. 你可以在CuratorFramework实例上通过addListener()
注册listener, Listener实现了下面的方法:
CuratorZookeeperClient 参数:
zookeeperFactory
- 用于创建ZooKeeper
实例的工厂ensembleProvider
- 合奏提供者sessionTimeoutMs
- 会话超时connectionTimeoutMs
- 连接超时watcher
- 默认观察者或nullretryPolicy
- 要使用的重试策略canBeReadOnly
- 如果为true,则允许ZooKeeper客户端在网络分区的情况下进入只读模式。详情 ZooKeeper(String, int, Watcher, long, byte[], boolean)
请见connectionHandlingPolicy
- 连接处理策略 - 使用其中一个预定义的策略或编写自己的策略3.代码实现:
下面将讲解使用java实现分布式锁:
@Configuration @Order(1) public class ZookeeperConfig { //间隔时间 static final Integer INTERVALS = 5000; //重试次数 static final Integer RETRYNUM = 10; static final String IP = "10.2.3.13:2181"; //回话超时时间 static final Integer SESSIONTIMEOUTMS = 20000; //连接创建超时时间 static final Integer CONNECTIONTIMEOUTMS = 5000; /** * zk重连策略 * @return */ @Bean public RetryNTimes retryNTimes(){ return new RetryNTimes(INTERVALS, RETRYNUM); } /** * zk客户端 * @return */ @Bean public CuratorFramework curatorFrameworkFactory(){ CuratorFramework curatorFramework = CuratorFrameworkFactory.newClient(IP, SESSIONTIMEOUTMS, CONNECTIONTIMEOUTMS, retryNTimes()); curatorFramework.start(); return curatorFramework; } @Bean public DistributedLock createDistributedLock(){ DistributedLock distributedLock = new DistributedLock(curatorFrameworkFactory()); distributedLock.init(); return distributedLock; } }
@Component public class DistributedLock { final static Logger log = LoggerFactory.getLogger(DistributedLock.class); //zk客户端 private CuratorFramework client = null; //用于挂起当前请求,并且等待上一个分布式锁释放 private static CountDownLatch zkLockLatch = new CountDownLatch(1); //分布式锁的总结点名 private static final String ZK_LOCK_PROJECT = "all_locks"; //分布式锁子节点 private static final String DISTRIBUTED_LOCK = "distributed_lock"; public DistributedLock(CuratorFramework client) { this.client = client; } /** * 初始化锁 */ public void init() { client = client.usingNamespace(null); /** * 创建zk锁的总节点,相当于idea工作空间下的项目 * * all_locks * | * __distributed_lock */ try { if (client.checkExists().forPath("/" + ZK_LOCK_PROJECT) == null) { client.create() .creatingParentsIfNeeded() .withMode(CreateMode.PERSISTENT) .withACL(Ids.OPEN_ACL_UNSAFE) .forPath("/" + ZK_LOCK_PROJECT); } //针对zk的分布式锁节点,创建相应的watcher事件监听 addWatcherToLock("/" + ZK_LOCK_PROJECT); } catch (Exception e) { log.error("客户端连接zookeeper服务器错误。。。请重试。。。"); e.printStackTrace(); } } /** * 获取分布式锁 */ public void getLock() { //使用死循环,当且仅当上一个锁释放且当前请求获得锁成功后才会跳出 while (true) { try { client.create() .creatingParentsIfNeeded() .withMode(CreateMode.EPHEMERAL) .withACL(Ids.OPEN_ACL_UNSAFE) .forPath("/" + ZK_LOCK_PROJECT + "/" + DISTRIBUTED_LOCK); log.info("获得分布式锁成功"); return; //如果锁的节点被创建成功,则锁没有被占用 } catch (Exception e) { log.info("获得分布式锁失败"); try { //如果没有获取到锁,需要重新设置同步资源值 if (zkLockLatch.getCount() <= 0) { zkLockLatch = new CountDownLatch(1); } //阻塞线程 zkLockLatch.await(); } catch (InterruptedException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } } } } /** * 释放分布式锁 */ public boolean releaseLock() { try { if (client.checkExists().forPath("/" + ZK_LOCK_PROJECT + "/" + DISTRIBUTED_LOCK) != null) { client.delete().forPath("/" + ZK_LOCK_PROJECT + "/" + DISTRIBUTED_LOCK); } } catch (Exception e) { e.printStackTrace(); return false; } log.info("分布式锁释放完毕"); return true; } /** * 创建watcher监听 * * @throws Exception */ public void addWatcherToLock(String path) throws Exception { final PathChildrenCache cache = new PathChildrenCache(client, path, true); //发布最初的事件 cache.start(StartMode.POST_INITIALIZED_EVENT); cache.getListenable().addListener(new PathChildrenCacheListener() { @Override public void childEvent(CuratorFramework client, PathChildrenCacheEvent event) throws Exception { if (event.getType().equals(PathChildrenCacheEvent.Type.CHILD_REMOVED)) { String path = event.getData().getPath(); log.info("上一个会话已释放锁或已断开,节点路径为:" + path); if (path.contains(DISTRIBUTED_LOCK)) { log.info("释放计数器,让当前请求来获得分布式锁"); zkLockLatch.countDown(); } } } }); } }
获取分布式锁--->distributedLock.getLock();
释放分布式锁--->distributedLock.releaseLock();
4.linux:
完成!!!!!!!!!!!!!!!!!!!!!!!!
分布式锁之间的对比
数据库分布式锁实现
缺点:1.db操作性能较差,并且有锁表的风险
2.非阻塞操作失败后,需要轮询,占用cpu资源;
3.长时间不commit或者长时间轮询,可能会占用较多连接资源
Redis(缓存)分布式锁实现
缺点:1.锁删除失败 过期时间不好控制
2.非阻塞,操作失败后,需要轮询,占用cpu资源;
ZK分布式锁实现
缺点:性能不如redis实现,主要原因是写操作(获取锁释放锁)都需要在Leader上执行,然后同步到follower。
总之:ZooKeeper有较好的性能和可靠性。
从理解的难易程度角度(从低到高)数据库 > 缓存 > Zookeeper
从实现的复杂性角度(从低到高)Zookeeper >= 缓存 > 数据库
从性能角度(从高到低)缓存 > Zookeeper >= 数据库
从可靠性角度(从高到低)Zookeeper > 缓存 > 数据库