概述

上一篇主要介绍了kafka时间轮源码和原理,这篇主要介绍一下kafka时间轮简单实现和使用kafka时间轮。如果要实现一个时间轮,就要了解他的数据结构和运行原理,上一篇随笔介绍了不同种类的数据结构kafka时间轮的原理(一)。大体上也就是需要使用数组或者链表组成一个环形的结构,数组或者链表的节点保存任务,这个任务肯定需要使用抽象一些的线程类来实现它以便于后期的任务执行等。时间轮的启动和停止肯定也是一个单独的线程来保证时间轮中的任务正确执行和取消等。现在详细说一下实现过程,其实知道原理实现起来就很方便了。

时间轮的功能

本文说明的时间轮具有的功能:

1,可以添加指定时间的延时任务,每个任务都是task抽象的父类,每个任务都放在环形object类型数组中,在这个任务中可以实现自己的业务逻辑。

2,有一个触发任务,实际上是一个线程,主要作用是相当于按时遍历时间轮每个节点,查看是否到时间执行,就相当于表针运行状态触发执行任务,这里就是TriggerJob。

3,停止运行(包含强制停止和所有任务完成后停止)。

4,查看待执行任务数量。

时间轮的数据结构

本文时间轮是一个object数组,每个数组元素这里定义是个set集合,set集合可以有多个任务,时间轮的大小规则是2的指数,这样设计的目的是可以通过左移达到取模的目的,这里使用线程池ExecutorService,因为多个任务肯定从线程池快速申请,见代码:

 1     //时间轮默认大小2的五次方
2 private static final int STATIC_RING_SIZE=64;
3 //数组作为时间轮
4 private Object[] ringBuffer;
5 private int bufferSize;
6 //线程池
7 private ExecutorService executorService;
8
9 //时间轮中总任务个数
10 private volatile int size =0 ;
11
12 //主要确定是否继续执行触发轮询时间轮的任务,相当关闭轮询时间轮的任务
13 private volatile boolean stop=false;
14 //使用原子类,初始化只需要一个线程执行,确定只一次初始化启动。
15 private volatile AtomicBoolean start= new AtomicBoolean(false);
16 //触发任务中的表针,tick 顾名思义
17 private AtomicInteger tick = new AtomicInteger();
18
19 //条件锁,用于stop
20 private Lock lock = new ReentrantLock();
21 private Condition condition = lock.newCondition();
22
23 //每一个任务有一个任务id
24 private AtomicInteger taskId= new AtomicInteger();
25
26 //用于按照taskId查找任务取消
27 private Map<Integer,Task> taskMap= new HashMap<Integer,Task>();

时间轮的构造函数,有2个,一个默认大小。一个用户自定义大小:

 1     public RhettBufferWheel(ExecutorService  executorService){
2 this.executorService=executorService;
3 this.bufferSize=STATIC_RING_SIZE;
4 this.ringBuffer= new Object[bufferSize];
5 }
6
7 public RhettBufferWheel(ExecutorService executorService, int bufferSize) {
8 this(executorService);
9 //判断bufferSize是否是2的指数
10 if(!powerOf2(bufferSize)){
11 throw new RuntimeException("bufferSize=[" + bufferSize + "] must be a power of 2");
12 }
13 this.bufferSize = bufferSize;
14 this.ringBuffer = new Object[bufferSize];
15 }

时间轮的启动和停止

下面就是初始化,时间轮初始化只需要一个线程实现就行。

 1     public void start() {
2 if (!start.get()) {
3 if (start.compareAndSet(start.get(), true)) {
4 logger.info("delay task is starting");
5 Thread job = new Thread(new TriggerJob());
6 job.setName("consumer RingBuffer thread");
7 job.start();
8 start.set(true);
9 }
10
11 }
12 }

有启动就有停止,停止有2中情况,一是强制停止所有任务,二是使用条件队列锁挂起所有任务,关闭addTask,直到任务执行完毕后被唤醒。

 1     public void stop(boolean force) {
2 if (force) {
3 logger.info("delay task is forced stop");
4 stop = true;
5 executorService.shutdownNow();
6 } else {
7 logger.info("delay task is stopping");
8 if (taskSize() > 0) {
9 try {
10 lock.lock();
11 condition.await();
12 stop = true;
13 } catch (InterruptedException e) {
14 logger.error("InterruptedException", e);
15 } finally {
16 lock.unlock();
17 }
18 }
19 executorService.shutdown();
20 }
21
22 }

时间轮的任务模块实现

上一节说明了时间轮的整体思路和实现。现在讲解时间轮的任务管理,先说明抽象任务类,这个类只是抽象了任务最基本的属性,任务的在时间轮的具体位置,以及时间轮的延时时间:

 1 public abstract static class Task extends Thread{
2 //时间轮的索引位置
3 private int index;
4 //时间轮的圈数
5 private int cycleNum;
6 //时间轮延时时间,到期执行时间
7 private int key;
8
9 @Override
10 public void run(){
11
12 }
13 public int getIndex() {
14 return index;
15 }
16 public void setIndex(int index) {
17 this.index = index;
18 }
19 public int getCycleNum() {
20 return cycleNum;
21 }
22 public void setCycleNum(int cycleNum) {
23 this.cycleNum = cycleNum;
24 }
25 public int getKey() {
26 return key;
27 }
28 public void setKey(int key) {
29 this.key = key;
30 }
31
32
33 }

知道了任务实现,下面我再和你们说一下任务的增删改查,任务的增加,增加是一个原子操作,所以这里实现了锁ReentrantLock。

 1     public int addTask(Task task){
2 int key= task.getKey();
3 int id;
4 try {
5 lock.lock();
6 //通过key到期时间计算出index位置也就是数组位置
7 int index = mod(key, bufferSize);
8 logger.info("task's key = {},task's index ={}",key,index);
9 task.setIndex(index);
10 //查看这个数组集合之前是否有数据,因为每个数组对应一个set集合所以这里要区分
11 Set<Task> tasks = get(index);
12
13 if (tasks != null) {
14 int cycleNum = cycleNum(key, bufferSize);
15 task.setCycleNum(cycleNum);
16 tasks.add(task);
17 } else {
18 int cycleNum = cycleNum(key, bufferSize);
19 task.setIndex(index);
20 task.setCycleNum(cycleNum);
21 //如果需要重新建立set集合就要重新增加task外,还要set对应正确的数组位置。
22 Set<Task> sets = new HashSet<>();
23 sets.add(task);
24 put(key, sets);
25 }
26 //每个任务的唯一id,统一放到hashmap中,为了查找方便,指定取消任务
27 id = taskId.incrementAndGet();
28 taskMap.put(id, task);
29 size++;
30 } finally {
31 lock.unlock();
32 }
33 //启动时间轮
34 start();
35
36 return id;
37 }

增加有一个地方需要知道一下,就是按照与运算取模。

 1     private int mod(int target, int mod) {
2 // equals target % mod
3 target = target + tick.get();
4 return target & (mod - 1);
5 }
6
7 private int cycleNum(int target, int mod) {
8 //equals target/mod
9 return target >> Integer.bitCount(mod - 1);
10 }

首先是根据延时时间 (key) 计算出所在的位置,其实就和 HashMap 一样的取模运算,只不过这里使用了位运算替代了取模,同时效率会高上不少。这样也解释了为什么数组长度一定得是 2∧n。其中的cycleNum() 自然是用于计算该任务所处的圈数,也是考虑到效率问题,使用位运算替代了除法

任务的取消,任务取消就是用到了hashmap,按照key找到task,然后取消,取消相当于在集合中删除任务,也是需要加锁的,

 1     /**
2 * Cancel task by taskId
3 * @param id unique id through {@link #addTask(Task)}
4 * @return
5 */
6 public boolean cancel(int id) {
7
8 boolean flag = false;
9 Set<Task> tempTask = new HashSet<>();
10
11 try {
12 lock.lock();
13 Task task = taskMap.get(id);
14 if (task == null) {
15 return false;
16 }
17
18 Set<Task> tasks = get(task.getIndex());
19 for (Task tk : tasks) {
20 if (tk.getKey() == task.getKey() && tk.getCycleNum() == task.getCycleNum()) {
21 size--;
22 flag = true;
23 } else {
24 tempTask.add(tk);
25 }
26
27 }
28 //update origin data
29 ringBuffer[task.getIndex()] = tempTask;
30 } finally {
31 lock.unlock();
32 }
33
34 return flag;
35 }

时间轮的指针触发任务实现

触发任务是一个单独的线程,这个是时间轮的指针,是时间轮的核心。

 1     private  class TriggerJob implements Runnable{
2 @Override
3 public void run(){
4 int index=0;
5 while(!stop){
6 try{
7 //取出指定位置的集合,
8 Set<Task> tasks=remove(index);
9 for(Task task:tasks){
10 //这个就是真正执行定时任务了
11 executorService.submit(task);
12 }
13 //一个轮询
14 if(++index>bufferSize-1){
15 index=0;
16 }
17 //Total tick number of records
18 tick.incrementAndGet();
19 TimeUnit.SECONDS.sleep(1);
20 }catch(Exception e){
21 logger.error("Exception", e);
22 }
23 }
24 logger.info("delay task is stopped");
25 }
26 }

这里的remove方法需要注意,这个就是按照索引取出指定数组位置的set集合。

 1     private Set<Task> remove(int key) {
2 Set<Task> tempTask = new HashSet<>();
3 Set<Task> result = new HashSet<>();
4
5 Set<Task> tasks = (Set<Task>) ringBuffer[key];
6 if (tasks == null) {
7 return result;
8 }
9
10 for (Task task : tasks) {
11 if (task.getCycleNum() == 0) {
12 result.add(task);
13
14 size2Notify();
15 } else {
16 // decrement 1 cycle number and update origin data
17 task.setCycleNum(task.getCycleNum() - 1);
18 tempTask.add(task);
19 }
20 }
21
22 //update origin data
23 ringBuffer[key] = tempTask;
24
25 return result;
26 }

其中的 size2Notify() 倒是值得说一下,他是用于在停止任务时,主线程等待所有延时任务执行完毕的唤醒条件。这类用法几乎是所有线程间通信的常规套路,值得收入技能包。

 1     private void size2Notify() {
2 try {
3 lock.lock();
4 size--;
5 if (size == 0) {
6 condition.signal();
7 }
8 } finally {
9 lock.unlock();
10 }
11 }

简单说来就是,上文的stop时间轮中条件队列锁阻塞,这里就是唤醒所有的线程,真正的stop,因为没有任务了。

总结

看了kafka的时间轮,高大尚无比,这次也是按照晚上不错的时间轮自己实现,觉得对自己的代码和开发思路多有补益,感谢网络大神的帮助:

github源码:https://github.com/Rhett-wang-888/Java-Algorithm/blob/master/src/main/java/util/RhettBufferWheel.java

https://crossoverjie.top/2019/09/27/algorithm/time%20wheel/#more

kafka时间轮简易实现(二)的更多相关文章

  1. kafka时间轮的原理(一)

    概述 早就想写关于kafka时间轮的随笔了,奈何时间不够,技术感觉理解不到位,现在把我之前学习到的进行整理一下,以便于以后并不会忘却.kafka时间轮是一个时间延时调度的工具,学习它可以掌握更加灵活先 ...

  2. Kafka中时间轮分析与Java实现

    在Kafka中应用了大量的延迟操作但在Kafka中 并没用使用JDK自带的Timer或是DelayQueue用于延迟操作,而是使用自己开发的DelayedOperationPurgatory组件用于管 ...

  3. [从源码学设计]蚂蚁金服SOFARegistry之时间轮的使用

    [从源码学设计]蚂蚁金服SOFARegistry之时间轮的使用 目录 [从源码学设计]蚂蚁金服SOFARegistry之时间轮的使用 0x00 摘要 0x01 业务领域 1.1 应用场景 0x02 定 ...

  4. 时间轮算法在Netty和Kafka中的应用,为什么不用Timer、延时线程池?

    大家好,我是yes. 最近看 Kafka 看到了时间轮算法,记得以前看 Netty 也看到过这玩意,没太过关注.今天就来看看时间轮到底是什么东西. 为什么要用时间轮算法来实现延迟操作? 延时操作 Ja ...

  5. Kafka解惑之时间轮 (TimingWheel)

    Kafka中存在大量的延迟操作,比如延迟生产.延迟拉取以及延迟删除等.Kafka并没有使用JDK自带的Timer或者DelayQueue来实现延迟的功能,而是基于时间轮自定义了一个用于实现延迟功能的定 ...

  6. Redis之时间轮机制(五)

    一.什么是时间轮 时间轮这个技术其实出来很久了,在kafka.zookeeper等技术中都有时间轮使用的方式. 时间轮是一种高效利用线程资源进行批量化调度的一种调度模型.把大批量的调度任务全部绑定到同 ...

  7. iOS:实现图片的无限轮播(二)---之使用第三方库SDCycleScrollView

    iOS:实现图片的无限轮播(二)---之使用第三方库SDCycleScrollView 时间:2016-01-19 19:13:43      阅读:630      评论:0      收藏:0   ...

  8. 时间轮算法(TimingWheel)是如何实现的?

    前言 我在2. SOFAJRaft源码分析-JRaft的定时任务调度器是怎么做的?这篇文章里已经讲解过时间轮算法在JRaft中是怎么应用的,但是我感觉我并没有讲解清楚这个东西,导致看了这篇文章依然和没 ...

  9. kafka Poll轮询机制与消费者组的重平衡分区策略剖析

    注意本文采用最新版本进行Kafka的内核原理剖析,新版本每一个Consumer通过独立的线程,来管理多个Socket连接,即同时与多个broker通信实现消息的并行读取.这就是新版的技术革新.类似于L ...

随机推荐

  1. 查看MySQL正在执行的线程

    一.使用SQL语句查询正在执行的线程 SHOW PROCESSLIST; 二.使用kill 线程id就可以结束线程(引起数据变化的线程需特别小心) SHOW PROCESSLIST; +------+ ...

  2. 静态类中不可以使用$this

    //静态方法中不能使用$this,静态方法调用其他方法可以用static\self\类名来代替class ceshi{ static public function aa(){ static::bb( ...

  3. xmake v2.6.2 发布,新增 Linux 内核驱动模块构建支持

    Xmake 是一个基于 Lua 的轻量级跨平台构建工具. 它非常的轻量,没有任何依赖,因为它内置了 Lua 运行时. 它使用 xmake.lua 维护项目构建,相比 makefile/CMakeLis ...

  4. Windows通过计划任务定时执行bat文件

    第一步 第二步 第三步 第四步 第五步 第六步

  5. CF1006B Polycarp's Practice 题解

    Content 给定一个长度为 \(n\) 的数列,试将其分成 \(k\) 段,使得每一段中的最大值的和最大. 数据范围:\(1\leqslant k,n,a_i\leqslant 2000\). S ...

  6. CF1438A Specific Tastes of Andre 题解

    Content 如果一个序列的和能够被它的长度整除,我们称这个序列是不错的.如果一个序列的所有的非空子序列都是不错的,我们就称这个序列是完美的.现在有 \(t\) 组询问,每组询问给定一个整数 \(n ...

  7. 【LeetCode】632. Smallest Range 解题报告(Python)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 题目地址: https://leetcode.com/problems/smallest ...

  8. 【LeetCode】482. License Key Formatting 解题报告(Python)

    作者: 负雪明烛 id: fuxuemingzhu 个人博客: http://fuxuemingzhu.cn/ 目录 题目描述 题目大意 解题方法 日期 题目地址:https://leetcode.c ...

  9. 1434 区间LCM

    1434 区间LCM 基准时间限制:1 秒 空间限制:131072 KB 一个整数序列S的LCM(最小公倍数)是指最小的正整数X使得它是序列S中所有元素的倍数,那么LCM(S)=X. 例如,LCM(2 ...

  10. Sum of Consecutive Prime Numbers(poj2739)

    Sum of Consecutive Prime Numbers Time Limit: 1000MS Memory Limit: 65536K Total Submissions: 22019 Ac ...