在我们的系统开发过程 中不可避免的会使用到定时任务的功能,而当我们在生产环境部署的服务超过1台时,就需要考虑任务调度的问题,防止两台或多台服务器上执行同一个任务,这个问题今天咱们就用zookeeper来解决。

zookeeper的存储模型

Zookeeper的数据存储采用的是结构化存储,结构化存储是没有文件和目录的概念,里边的目录和文件被抽象成了节点(node),zookeeper里可以称为znode。Znode的层次结构如下图:

每个子目录项如 NameService 都被称作为 znode(目录节点),和文件系统一样,我们能够自由的增加、删除znode,在一个znode下增加、删除子znode,唯一的不同在于znode是可以存储数据的。

znode类型

  • PERSISTENT-持久化目录节点

    客户端与zookeeper断开连接后,该节点依旧存在

  • PERSISTENT_SEQUENTIAL-持久化顺序编号目录节点

    客户端与zookeeper断开连接后,该节点依旧存在,只是Zookeeper给该节点名称进行顺序编号

  • EPHEMERAL-临时目录节点

    客户端与zookeeper断开连接后,该节点被删除

  • EPHEMERAL_SEQUENTIAL-临时顺序编号目录节点

    客户端与zookeeper断开连接后,该节点被删除,只是Zookeeper给该节点名称进行顺序编号

监听通知机制

客户端注册监听它关心的目录节点,当目录节点发生变化(数据改变、被删除、子目录节点增加删除)时,zookeeper会通知客户端。基于这种监听,可以实现注册中心、分布式同步等功能。

zk分布式任务管理机制

使用zookeeper的临时顺序节点,来实现分布式任务的调度功能,每一台服务启动的时候都向zookeepe指定的目录下注册一下临时顺序节点,并把该节点记录的系统里,每一次任务执行的时候,获取所有的有序节点,跟当前系统创爱你的节点对比,如果当前服务创建的节点是所有节点中最小的,则执行任务,否则不执行任务,如下如所示:

代码实现

1、pom引用

  1. <zookeeper.version>3.4.8</zookeeper.version>
  2. <curator.version>2.11.1</curator.version>
  3.  
  4. <dependency>
  5. <groupId>org.apache.zookeeper</groupId>
  6. <artifactId>zookeeper</artifactId>
  7. <version>${zookeeper.version}</version>
  8. <exclusions>
  9. <exclusion>
  10. <groupId>org.slf4j</groupId>
  11. <artifactId>slf4j-log4j12</artifactId>
  12. </exclusion>
  13. <exclusion>
  14. <groupId>org.slf4j</groupId>
  15. <artifactId>slf4j-api</artifactId>
  16. </exclusion>
  17. </exclusions>
  18. </dependency>
  19. <dependency>
  20. <groupId>org.apache.curator</groupId>
  21. <artifactId>curator-recipes</artifactId>
  22. <version>${curator.version}</version>
  23. </dependency>

2、ZkClient类

该类封装了zookeeper的操作类,服务启动的时候回向zk上注册有序临时节点,目录为:/demo1/task/n,例如:/demo1/task/n00000001,/demo1/task/n00000002,创建的节点路径保存到变量:curTaskNodeId

  1. package com.blogs.client;
  2.  
  3. import java.util.ArrayList;
  4. import java.util.Collection;
  5. import java.util.List;
  6. import java.util.Map;
  7. import java.util.Random;
  8. import java.util.concurrent.ExecutorService;
  9. import java.util.concurrent.Executors;
  10. import java.util.concurrent.TimeUnit;
  11.  
  12. import org.apache.curator.RetryPolicy;
  13. import org.apache.curator.framework.CuratorFramework;
  14. import org.apache.curator.framework.CuratorFrameworkFactory;
  15. import org.apache.curator.framework.CuratorFrameworkFactory.Builder;
  16. import org.apache.curator.framework.api.ACLProvider;
  17. import org.apache.curator.framework.recipes.cache.ChildData;
  18. import org.apache.curator.framework.recipes.cache.TreeCache;
  19. import org.apache.curator.framework.recipes.cache.TreeCacheListener;
  20. import org.apache.curator.framework.recipes.locks.InterProcessMutex;
  21. import org.apache.curator.framework.recipes.locks.InterProcessReadWriteLock;
  22. import org.apache.curator.framework.state.ConnectionState;
  23. import org.apache.curator.framework.state.ConnectionStateListener;
  24. import org.apache.curator.retry.ExponentialBackoffRetry;
  25. import org.apache.zookeeper.CreateMode;
  26. import org.apache.zookeeper.ZooDefs;
  27. import org.apache.zookeeper.data.ACL;
  28. import org.springframework.stereotype.Component;
  29.  
  30. import lombok.Data;
  31. import lombok.extern.slf4j.Slf4j;
  32.  
  33. @Component
  34. @Slf4j
  35. @Data
  36. public class ZkClient {
  37. private CuratorFramework client;
  38. public TreeCache cache;
  39. //记录当前服务在zk上创建的nodeId
  40. public String curTaskNodeId="";
  41. //private ZookeeperProperties zookeeperProperties;
  42.  
  43. public ZkClient(){
  44. init();
  45. }
  46.  
  47. /**
  48. * 初始化zookeeper
  49. */
  50. public void init(){
  51. try {
  52. //初始sleep时间 ,毫秒,
  53. int baseSleepTimeMs=1000;
  54. //最大重试次数
  55. int maxRetries=3;
  56. RetryPolicy retryPolicy = new ExponentialBackoffRetry(baseSleepTimeMs,maxRetries);
  57. Builder builder = CuratorFrameworkFactory.builder()
  58. .connectString("127.0.0.1:2181").retryPolicy(retryPolicy)
  59. .sessionTimeoutMs( 1000) //会话超时时间,单位为毫秒,默认60000ms,连接断开后,其它客户端还能请到临时节点的时间
  60. .connectionTimeoutMs( 6000)//连接创建超时时间,单位为毫秒
  61. .namespace( "demo1");//zk的根节点
  62. //以下注释的为创建节点的用户名密码
  63. //builder.authorization("digest", "rt:rt".getBytes("UTF-8"));
  64. /*
  65. builder.aclProvider(new ACLProvider() {
  66. @Override
  67. public List<ACL> getDefaultAcl() {
  68. return ZooDefs.Ids.CREATOR_ALL_ACL;
  69. }
  70.  
  71. @Override
  72. public List<ACL> getAclForPath(final String path) {
  73. return ZooDefs.Ids.CREATOR_ALL_ACL;
  74. }
  75. });*/
  76. client = builder.build();
  77. client.start();
  78.  
  79. client.getConnectionStateListenable().addListener(new ConnectionStateListener() {
  80. public void stateChanged(CuratorFramework client, ConnectionState state) {
  81. if (state == ConnectionState.LOST) {
  82. //连接丢失
  83. log.info("lost session with zookeeper");
  84. } else if (state == ConnectionState.CONNECTED) {
  85. //连接新建
  86. log.info("connected with zookeeper");
  87. } else if (state == ConnectionState.RECONNECTED) {
  88. log.info("reconnected with zookeeper");
  89. }
  90. }
  91. });
  92. System.out.println("zk初始化完成");
  93. //获取当前服务启动时创建的节点,临时有序节点,用作定时任务的执行
  94. curTaskNodeId=createNode(CreateMode.EPHEMERAL_SEQUENTIAL,"/task/n","");
  95.  
  96. } catch (Exception e) {
  97. // TODO: handle exception
  98. e.printStackTrace();
  99. }
  100. }
  101.  
  102. public void stop() {
  103. client.close();
  104. }
  105.  
  106. public CuratorFramework getClient() {
  107. return client;
  108. }
  109. /**
  110. * 创建节点
  111. * @param mode 节点类型
  112. * 1、PERSISTENT 持久化目录节点,存储的数据不会丢失。
  113. * 2、PERSISTENT_SEQUENTIAL顺序自动编号的持久化目录节点,存储的数据不会丢失
  114. * 3、EPHEMERAL临时目录节点,一旦创建这个节点的客户端与服务器端口也就是session 超时,这种节点会被自动删除
  115. *4、EPHEMERAL_SEQUENTIAL临时自动编号节点,一旦创建这个节点的客户端与服务器端口也就是session 超时,这种节点会被自动删除,并且根据当前已近存在的节点数自动加 1,然后返回给客户端已经成功创建的目录节点名。
  116. * @param path 节点名称
  117. * @param nodeData 节点数据
  118. */
  119. public String createNode(CreateMode mode, String path , String nodeData) {
  120. String nodepath="";
  121. try {
  122. //使用creatingParentContainersIfNeeded()之后Curator能够自动递归创建所有所需的父节点
  123. nodepath = client.create().creatingParentsIfNeeded().withMode(mode).forPath(path,nodeData.getBytes("UTF-8"));
  124. System.out.println(nodepath);
  125. } catch (Exception e) {
  126. log.error("注册出错", e);
  127. }
  128. return nodepath;
  129. }
  130.  
  131. /**
  132. * 创建节点
  133. * @param mode 节点类型
  134. * 1、PERSISTENT 持久化目录节点,存储的数据不会丢失。
  135. * 2、PERSISTENT_SEQUENTIAL顺序自动编号的持久化目录节点,存储的数据不会丢失
  136. * 3、EPHEMERAL临时目录节点,一旦创建这个节点的客户端与服务器端口也就是session 超时,这种节点会被自动删除
  137. * 4、EPHEMERAL_SEQUENTIAL临时自动编号节点,一旦创建这个节点的客户端与服务器端口也就是session 超时,这种节点会被自动删除,并且根据当前已近存在的节点数自动加 1,然后返回给客户端已经成功创建的目录节点名。
  138. * @param path 节点名称
  139. */
  140. public void createNode(CreateMode mode,String path ) {
  141. try {
  142. //使用creatingParentContainersIfNeeded()之后Curator能够自动递归创建所有所需的父节点
  143. client.create().creatingParentsIfNeeded().withMode(mode).forPath(path);
  144. } catch (Exception e) {
  145. log.error("注册出错", e);
  146. }
  147. }
  148.  
  149. /**
  150. * 删除节点数据
  151. *
  152. * @param path
  153. */
  154. public void deleteNode(final String path) {
  155. try {
  156. deleteNode(path,true);
  157. } catch (Exception ex) {
  158. log.error("{}",ex);
  159. }
  160. }
  161.  
  162. /**
  163. * 删除节点数据
  164. * @param path
  165. * @param deleteChildre 是否删除子节点
  166. */
  167. public void deleteNode(final String path,Boolean deleteChildre){
  168. try {
  169. if(deleteChildre){
  170. //guaranteed()删除一个节点,强制保证删除,
  171. // 只要客户端会话有效,那么Curator会在后台持续进行删除操作,直到删除节点成功
  172. client.delete().guaranteed().deletingChildrenIfNeeded().forPath(path);
  173. }else{
  174. client.delete().guaranteed().forPath(path);
  175. }
  176. } catch (Exception e) {
  177. e.printStackTrace();
  178. }
  179. }
  180.  
  181. /**
  182. * 设置指定节点的数据
  183. * @param path
  184. * @param datas
  185. */
  186. public void setNodeData(String path, byte[] datas){
  187. try {
  188. client.setData().forPath(path, datas);
  189. }catch (Exception ex) {
  190. log.error("{}",ex);
  191. }
  192. }
  193.  
  194. /**
  195. * 获取指定节点的数据
  196. * @param path
  197. * @return
  198. */
  199. public byte[] getNodeData(String path){
  200. Byte[] bytes = null;
  201. try {
  202. if(cache != null){
  203. ChildData data = cache.getCurrentData(path);
  204. if(data != null){
  205. return data.getData();
  206. }
  207. }
  208. client.getData().forPath(path);
  209. return client.getData().forPath(path);
  210. }catch (Exception ex) {
  211. log.error("{}",ex);
  212. }
  213. return null;
  214. }
  215.  
  216. /**
  217. * 获取数据时先同步
  218. * @param path
  219. * @return
  220. */
  221. public byte[] synNodeData(String path){
  222. client.sync();
  223. return getNodeData( path);
  224. }
  225.  
  226. /**
  227. * 判断路径是否存在
  228. *
  229. * @param path
  230. * @return
  231. */
  232. public boolean isExistNode(final String path) {
  233. client.sync();
  234. try {
  235. return null != client.checkExists().forPath(path);
  236. } catch (Exception ex) {
  237. return false;
  238. }
  239. }
  240.  
  241. /**
  242. * 获取节点的子节点
  243. * @param path
  244. * @return
  245. */
  246. public List<String> getChildren(String path) {
  247. List<String> childrenList = new ArrayList<>();
  248. try {
  249. childrenList = client.getChildren().forPath(path);
  250. } catch (Exception e) {
  251. log.error("获取子节点出错", e);
  252. }
  253. return childrenList;
  254. }
  255.  
  256. /**
  257. * 随机读取一个path子路径, "/"为根节点对应该namespace
  258. * 先从cache中读取,如果没有,再从zookeeper中查询
  259. * @param path
  260. * @return
  261. * @throws Exception
  262. */
  263. public String getRandomData(String path) {
  264. try{
  265. Map<String,ChildData> cacheMap = cache.getCurrentChildren(path);
  266. if(cacheMap != null && cacheMap.size() > 0) {
  267. log.debug("get random value from cache,path="+path);
  268. Collection<ChildData> values = cacheMap.values();
  269. List<ChildData> list = new ArrayList<>(values);
  270. Random rand = new Random();
  271. byte[] b = list.get(rand.nextInt(list.size())).getData();
  272. return new String(b,"utf-8");
  273. }
  274. if(isExistNode(path)) {
  275. log.debug("path [{}] is not exists,return null",path);
  276. return null;
  277. } else {
  278. log.debug("read random from zookeeper,path="+path);
  279. List<String> list = client.getChildren().forPath(path);
  280. if(list == null || list.size() == 0) {
  281. log.debug("path [{}] has no children return null",path);
  282. return null;
  283. }
  284. Random rand = new Random();
  285. String child = list.get(rand.nextInt(list.size()));
  286. path = path + "/" + child;
  287. byte[] b = client.getData().forPath(path);
  288. String value = new String(b,"utf-8");
  289. return value;
  290. }
  291. }catch(Exception e){
  292. log.error("{}",e);
  293. }
  294. return null;
  295.  
  296. }
  297.  
  298. /**
  299. * 获取读写锁
  300. * @param path
  301. * @return
  302. */
  303. public InterProcessReadWriteLock getReadWriteLock(String path){
  304. InterProcessReadWriteLock readWriteLock = new InterProcessReadWriteLock(client, path);
  305. return readWriteLock;
  306. }
  307.  
  308. /**
  309. * 在注册监听器的时候,如果传入此参数,当事件触发时,逻辑由线程池处理
  310. */
  311. ExecutorService pool = Executors.newFixedThreadPool(2);
  312.  
  313. /**
  314. * 监听数据节点的变化情况
  315. * @param watchPath
  316. * @param listener
  317. */
  318. public void watchPath(String watchPath,TreeCacheListener listener){
  319. // NodeCache nodeCache = new NodeCache(client, watchPath, false);
  320. TreeCache cache = new TreeCache(client, watchPath);
  321. cache.getListenable().addListener(listener,pool);
  322. try {
  323. cache.start();
  324. } catch (Exception e) {
  325. e.printStackTrace();
  326. }
  327. }
  328.  
  329. }

3、定时任务调用

  1. package com.blogs.client;
  2.  
  3. import java.time.LocalDateTime;
  4. import java.util.List;
  5.  
  6. import org.springframework.beans.factory.annotation.Autowired;
  7. import org.springframework.scheduling.annotation.EnableScheduling;
  8. import org.springframework.scheduling.annotation.Scheduled;
  9. import org.springframework.stereotype.Component;
  10.  
  11. @Component
  12. @EnableScheduling
  13. public class ScheduleTask {
  14.  
  15. @Autowired
  16. private ZkClient zkClient;
  17.  
  18. //添加定时任务
  19. @Scheduled(cron = "0/5 * * * * ?")
  20. private void configureTasks() {
  21. System.out.println("开始执行任务");
  22. //获取所有节点
  23. List<String> taskNodes=zkClient.getChildren("/task");
  24. //查找最小节点
  25. int minNodeNum=Integer.MAX_VALUE;
  26. for (int i = 0; i < taskNodes.size(); i++) {
  27. //节点前面有一个n,把n替换掉,剩下的转换为数字
  28. int nodeNum=Integer.valueOf(taskNodes.get(i).replace("n", ""));
  29. if(nodeNum < minNodeNum){
  30. minNodeNum = nodeNum;
  31. }
  32. System.out.println("节点:"+taskNodes.get(i));
  33. }
  34. System.out.println("当前节点:"+zkClient.getCurTaskNodeId());
  35. //如果最小节点 等于该服务创建的节点,则执行任务
  36. int curNodeNum=Integer.valueOf(zkClient.getCurTaskNodeId().substring(zkClient.getCurTaskNodeId().lastIndexOf('/') + 2));
  37. if(minNodeNum - curNodeNum == 0){
  38. System.out.println("执行任务");
  39. }else {
  40. System.out.println("不执行任务");
  41. }
  42.  
  43. System.err.println("执行静态定时任务时间: " + LocalDateTime.now());
  44. }
  45. }

当前服务创建的服务为节点最小的,则执行服务,否则不执行服务

执行结果

把服务的端口分别修改为:8080,8081,模拟启动两个服务,查看定时任务的执行情况

当把两个服务的任何一个服务关闭,定时任务还可以正常执行。

zkCli查看查创建的目录结构

作者:Eric.Chen
出处:https://www.cnblogs.com/lc-chenlong

如果喜欢作者的文章,请关注“写代码的猿”订阅号以便第一时间获得最新内容。本文版权归作者所有,欢迎转载

zk分布式任务管理的更多相关文章

  1. 本地锁、redis分布式锁、zk分布式锁

    本地锁.redis分布式锁.zk分布式锁 https://www.cnblogs.com/yjq-code/p/dotnetlock.html 为什么要用锁? 大型站点在高并发的情况下,为了保持数据最 ...

  2. 2020-05-24:ZK分布式锁有几种实现方式?各自的优缺点是什么?

    福哥答案2020-05-24: Zk分布式锁有两种实现方式一种比较简单,应对并发量不是很大的情况.获得锁:创建一个临时节点,比如/lock,如果成功获得锁,如果失败没获得锁,返回false释放锁:删除 ...

  3. 【分布式锁的演化】终章!手撸ZK分布式锁!

    前言 这应该是分布式锁演化的最后一个章节了,相信很多小伙伴们看完这个章节之后在应对高并发的情况下,如何保证线程安全心里肯定也会有谱了.在实际的项目中也可以参考一下老猫的github上的例子,当然代码没 ...

  4. 【zookeeper】Apache curator的使用及zk分布式锁实现

    上篇,本篇主要讲Apache开源的curator的使用,有了curator,利用Java对zookeeper的操作变得极度便捷. 其实在学之前我也有个疑虑,我为啥要学curator,撇开涨薪这些外在的 ...

  5. .net下 本地锁、redis分布式锁、zk分布式锁的实现

    为什么要用锁? 大型站点在高并发的情况下,为了保持数据最终一致性就需要用到技术方案来支持.比如:分布式锁.分布式事务.有时候我们在为了保证某一个方法每次只能被一个调用者使用的时候,这时候我们也可以锁来 ...

  6. dubbo zk 分布式服务项目搭建与配置

    1. 项目 jar  -----提供接口 2. 项目 jar  -----接口实现   provider启动zk main方法启动 start applicationContext.xml <b ...

  7. Quart.Net分布式任务管理平台

           无关主题:一段时间没有更新文章了,与自己心里的坚持还是背驰,虽然这期间在公司做了统计分析,由于资源分配问题,自己或多或少的原因,确实拖得有点久了,自己这段时间也有点松懈,借口就不说那么多 ...

  8. Quart.Net分布式任务管理平台(续)

           感谢@Taking园友得建议,我这边确实多做了一步上传,导致后面还需处理同步上传到其他服务器来支持分布式得操作.所有才有了上篇文章得完善. 首先看一下新的项目结构图: 这个图和上篇文章中 ...

  9. Quartz.Net分布式任务管理平台(第二版)

    前言:在Quartz.Net项目发布第一版后,有挺多园友去下载使用,我们通过QQ去探讨,其中项目中还是存在一定的不完善.所以有了现在这个版本.这个版本的编写完成其实有段时间了一直没有放上去.现在已经同 ...

随机推荐

  1. css3新增动画

    1.transiition过渡:样式改变就会执行transition (1)格式:transiition:1s width linear,2s 1s height; (2)参数: transition ...

  2. 网页的cdn引用地址,js,react,bootstrap

    react+----这三个够用了 <script src="https://cdn.bootcss.com/react/15.4.2/react.min.js">< ...

  3. MyEclipse 10导入JDK1.7或1.8

    1.在MyEclipse,选择“windows”>"preferences"选择,打开“perference”窗口(如下图) 2.展开“perference”窗口左侧“jav ...

  4. SQLI LABS Challenges Part(54-65) WriteUp

    终于到了最后一部分,这些关跟之前不同的是这里是限制次数的. less-54: 这题比较好玩,10次之内爆出数据.先试试是什么类型: ?id=1' and '1 ==>>正常 ?id=1' ...

  5. MySQL · 引擎特性 · InnoDB崩溃恢复

    前言 数据库系统与文件系统最大的区别在于数据库能保证操作的原子性,一个操作要么不做要么都做,即使在数据库宕机的情况下,也不会出现操作一半的情况,这个就需要数据库的日志和一套完善的崩溃恢复机制来保证.本 ...

  6. 入门系列之使用Sysdig监视您的Ubuntu 16.04系统

    欢迎大家前往腾讯云+社区,获取更多腾讯海量技术实践干货哦~ 本文由乌鸦 发表于云+社区专栏 介绍 Sysdig是一个全面的开源系统活动监控,捕获和分析应用程序.它具有强大的过滤语言和可自定义的输出,以 ...

  7. Python爬虫入门教程 37-100 云沃客项目外包网数据爬虫 scrapy

    爬前叨叨 2019年开始了,今年计划写一整年的博客呢~,第一篇博客写一下 一个外包网站的爬虫,万一你从这个外包网站弄点外快呢,呵呵哒 数据分析 官方网址为 https://www.clouderwor ...

  8. 关于elementUi tab组件路由跳转卡死问题

    好久没来了,周五项目终于要上线了(*^▽^*),上线之前测出一个很恶心的bug真真是... 项目:Vue + elementUi   后台管理项目 问题描述:登录后首次通过侧边栏路由跳转到主页面有ta ...

  9. 【工具】-RAP接口管理工具

    前言 RAP 是一个可视化接口管理工具, 通过分析接口结构,动态生成模拟数据,校验真实接口正确性, 围绕接口定义,通过一系列自动化工具提升我们的协作效率. 在 RAP 中,您可定义接口的 URL.请求 ...

  10. 使用d3.v5实现条形图

    效果图: 条形图: 目录结构: <!DOCTYPE html> <html lang="en"> <head> <meta charset ...