Kafka 时间轮算法

简介: Kafka中存在大量的延时操作。1. 发送消息-超时+重试机制的延时。2. ACKS 确认机制的延时。

@[TOC]

前言

Kafka中存在大量的延时操作。

  1. 发送消息-超时+重试机制的延时。
  2. ACKS 确认机制的延时。

Kafka并没有使用JDK自带的Timer或者DelayQueue来实现延迟的功能,而是基于时间轮自定义了一个用于实现延迟功能的定时器(SystemTimer)

JDK的Timer和DelayQueue插入和删除操作的平均时间复杂度为O(log(n)),并不能满足Kafka的高性能要求,而基于时间轮可以将插入和删除操作的时间复杂度都降为O(1)。

时间轮的应用并非Kafka独有,其应用场景还有很多,在Netty、Akka、Quartz、Zookeeper等组件中都存在时间轮的踪影。

Java 任务调度

给假设有1000个任务,都是不同的时间执行的,时间精确到秒,你怎么实现对所有的任务的调度?

第一种思路是启动一个线程,每秒钟对所有的任务进行遍历,找出执行时间跟当前时间匹配的,执行它。如果任务数量太大,遍历和比较所有任务会比较浪费时间。

第二个思路,把这些任务进行排序,执行时间近(先触发)的放在前面。这里会涉及到大量的元素移动(新加入任务,任务执行--删除任务之类,都需要重新排序)

Timer

JDK包里面自带了一个Timer工具类(java.util包下),可以实现延时任务(例如30分钟以后触发),也可以实现周期性任务(例如每1小时触发一次)。

它的本质是一个优先队列(TaskQueue),和一个执行任务的线程(TimerThread)。

普通的队列是一种先进先出的数据结构,元素在队尾追加,而从队头删除。在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高级先出 (first in, largest out)的行为特征。通常采用堆数据结构来实现。

image.png

image.png

在这个优先队列中,最先需要执行的任务排在优先队列的第一个。然后 TimerThread 不断地拿第一个任务的执行时间和当前时间做对比。如果时间到了先看看这个任务是不是周期性执行的任务,如果是则修改当前任务时间为下次执行的时间,如果不是周期性任务则将任务从优先队列中移除。最后执行任务。

但是Timer是单线程的,在很多场景下不能满足业务需求。

在JDK1.5之后,引入了一个支持多线程的任务调度工具ScheduledThreadPoolExecutor用来替代TImer,它是几种常用的线程池之一,里面是一个延迟队列DelayedWorkQueue,也是一个优先队列。

image.png

DelayedWorkQueue的最小堆实现

优先队列的使用的是最小堆实现。

最小堆的含义: 一种完全二叉树,父结点的值小于或等于它的左子节点和右子节点

比如插入以下的数据 [1,2,3,7,17,19,25,36,100]

最小堆就长成这个样子。

image.png

优先队列的插入和删除的时间复杂度是O(logn),当数据量大的时候,频繁的入堆出堆性能不是很好。

比如要插入0,过程如下:

  1. 插入末尾元素
    image.png

  2. 0比19小,所以要向上移动且互换。
    image.png

  3. 0比2小,所以要向上移动且互换。
    image.png

4、0比2小,所以要向上移动且互换。

image.png

算法复杂度

N个数据的最小堆, 共有logN层, 最坏的情况下, 需要移动logN次

时间轮

时间轮先考虑对所有的任务进行分组,把相同执行时刻的任务放在一起。比如下图,数组里面的一个下标就代表1秒钟。它就会变成一个数组加链表的数据结构。分组以后遍历和比较的时间会减少一些。

image.png

但是还是有问题,如果任务数量非常大,而且时间都不一样,或者有执行时间非常遥远的任务,那这个数组长度是不是要非常地长?比如有个任务2个月之后执行,从现在开始计算,它的下标是5253120。

所以长度肯定不能是无限的,只能是固定长度的。比如固定长度是8,一个格子代表1秒(现在叫做一个bucket槽),一圈可以表示8秒。遍历的线程只要一个格子一个格子的获取任务,并且执行就OK了。

固定长度的数组怎么用来表示超出最大长度的时间呢?可以用循环数组。

比如一个循环数组长度8,可以表示8秒。8秒以后执行的任务怎么放进去?只要除以8,用得到的余数,放到对应的格子就OK了。比如10%8=2,它放在第2个格子。这里就有了轮次的概念,第10秒的任务是第二轮的时候才执行。

image.png

这时候,时间轮的概念已经出来了。

如果任务数量太多,相同时刻执行的任务很多,会导致链表变得非常长。这里我们可以进一步对这个时间轮做一个改造,做一个多层的时间轮。

比如:最内层8个格子,每个格子1秒;外层8个格子,每个格子8*8=64秒;最内层走一圈,外层走一格。这时候时间轮就跟时钟更像了。随着时间流动,任务会降级,外层的任务会慢慢地向内层移动。

image.png

时间轮任务插入和删除时间复杂度都为O(1),应用范围非常广泛,更适合任务数很大的延时场景。Dubbo、Netty、Kafka中都有实现。

Kafka中时间轮实现

Kafka里面TimingWheel的数据结构

image.png

kafka会启动一个线程,去推动时间轮的指针转动。其实现原理其实就是通过queue.poll()取出放在最前面的槽的TimerTaskList

image.png

image.png

添加新的延迟任务

image.png

往时间轮添加新的任务

image.png

时间轮指针的推进

image.png

第二层时间轮的创建代码如下

image.png

相关文章
|
消息中间件 负载均衡 算法
【Kafka从入门到成神系列 三】Kafka 生产者消息分区及压缩算法
【Kafka从入门到成神系列 三】Kafka 生产者消息分区及压缩算法
【Kafka从入门到成神系列 三】Kafka 生产者消息分区及压缩算法
|
消息中间件 存储 算法
Kafka的心跳处理机制竟然用到了时间轮算法?
Kafka的心跳处理机制竟然用到了时间轮算法?
Kafka的心跳处理机制竟然用到了时间轮算法?
|
消息中间件 NoSQL 算法
原创Kafka学习笔记,java空间换时间算法
原创Kafka学习笔记,java空间换时间算法
原创Kafka学习笔记,java空间换时间算法
|
消息中间件 算法 Java
面试官:知道时间轮算法吗?在Netty和Kafka中如何应用的?为什么不用Timer、延时线程池?(下)
面试官:知道时间轮算法吗?在Netty和Kafka中如何应用的?为什么不用Timer、延时线程池?(下)
面试官:知道时间轮算法吗?在Netty和Kafka中如何应用的?为什么不用Timer、延时线程池?(下)
|
消息中间件 算法 Java
面试官:知道时间轮算法吗?在Netty和Kafka中如何应用的?为什么不用Timer、延时线程池?(上)
面试官:知道时间轮算法吗?在Netty和Kafka中如何应用的?为什么不用Timer、延时线程池?(上)
面试官:知道时间轮算法吗?在Netty和Kafka中如何应用的?为什么不用Timer、延时线程池?(上)
|
消息中间件 缓存 算法
Kafka竟然也用二分搜索算法查找索引!(下)
Kafka竟然也用二分搜索算法查找索引!
175 0
|
2月前
|
消息中间件 存储 运维
为什么说Kafka还不是完美的实时数据通道
【10月更文挑战第19天】Kafka 虽然作为数据通道被广泛应用,但在实时性、数据一致性、性能及管理方面存在局限。数据延迟受消息堆积和分区再平衡影响;数据一致性难以达到恰好一次;性能瓶颈在于网络和磁盘I/O;管理复杂性涉及集群配置与版本升级。
|
2月前
|
消息中间件 Java Kafka
Flink-04 Flink Java 3分钟上手 FlinkKafkaConsumer消费Kafka数据 进行计算SingleOutputStreamOperatorDataStreamSource
Flink-04 Flink Java 3分钟上手 FlinkKafkaConsumer消费Kafka数据 进行计算SingleOutputStreamOperatorDataStreamSource
50 1
|
4月前
|
消息中间件 Java Kafka
Kafka不重复消费的终极秘籍!解锁幂等性、偏移量、去重神器,让你的数据流稳如老狗,告别数据混乱时代!
【8月更文挑战第24天】Apache Kafka作为一款领先的分布式流处理平台,凭借其卓越的高吞吐量与低延迟特性,在大数据处理领域中占据重要地位。然而,在利用Kafka进行数据处理时,如何有效避免重复消费成为众多开发者关注的焦点。本文深入探讨了Kafka中可能出现重复消费的原因,并提出了四种实用的解决方案:利用消息偏移量手动控制消费进度;启用幂等性生产者确保消息不被重复发送;在消费者端实施去重机制;以及借助Kafka的事务支持实现精确的一次性处理。通过这些方法,开发者可根据不同的应用场景灵活选择最适合的策略,从而保障数据处理的准确性和一致性。
303 9
|
4月前
|
消息中间件 负载均衡 Java
"Kafka核心机制揭秘:深入探索Producer的高效数据发布策略与Java实战应用"
【8月更文挑战第10天】Apache Kafka作为顶级分布式流处理平台,其Producer组件是数据高效发布的引擎。Producer遵循高吞吐、低延迟等设计原则,采用分批发送、异步处理及数据压缩等技术提升性能。它支持按消息键值分区,确保数据有序并实现负载均衡;提供多种确认机制保证可靠性;具备失败重试功能确保消息最终送达。Java示例展示了基本配置与消息发送流程,体现了Producer的强大与灵活性。
70 3