FastThreadLocal 时间轮,jdktimer,sche线程池,sentinel滑动窗口【重要】

1 FastThreadLocal

1)FastThreadLocal l = new xxx(index.increAndGet())

避免线性探测

l.get时,去到数组里用l.getIndex()访问

public final class InternalThreadLocalMap extends UnpaddedInternalThreadLocalMap {
// 存储所有 FastThreadLocal 的值
private Object[] indexedVariables;

// 全局索引分配器
private static final AtomicInteger nextIndex = new AtomicInteger();
}

与哈希表扩容不同,扩容过程是 数组复制不会触发再哈希。

 

2)需要自己的线程对象访问自己那个数组map

FastThreadLocalThread

 

 

 

 

2 时间轮

在Netty中的一个典型应用场景是判断某个连接是否idle,如果idle(如客户端由于网络原因导致到服 务器的心跳无法送达),则服务器会主动断开连接,释放资源。 得益于Netty NIO的优异性能(这不是netty的,这是epoll的),基于Netty开发的服务器可以维持大量的长连接,单台8核16G的云主机 可以同时维持几十万长连接(epoll的优势),及时掐掉不活跃的连接就显得尤其重要。

(1) 单定时器方案 redis滑动窗口

描述: 把所有需要定时审核的资源放到redis中,例如sorted set中,需要审核通过的时间作为score值。 后台启动一个定时器,定时轮询sortedSet,当score值小于当前时间,则运行任务审核通过。 

问题 这个方案在小批量数据的情况下没有问题, 但是在大批量任务的情况下就会出现问题了,因为每次都要轮询全量的数据,逐个判断是否需要执行, 一旦轮询任务执行比较长,就会出现任务无法按照定时的时间执行的问题。

(2) 多定时器方案

描述:每个需要定时完成的任务都启动一个定时任务,然后等待完成之后销毁

问题:这个方案带来的问题很明显,定时任务比较多的情况下,会启动很多的线程,这样服务器会承受不了之 后崩溃。 基本上不会采取这个方案。

(3)redis的过期通知功能

描述:和方案一类似,针对每一个需要定时审核的任务,设定过期时间,过期时间也就是审核通过的时间,订阅redis的过期事件,当这个事件发生时,执行相应的审核通过任务。

问题:这个方案来说是借用了redis这种中间件来实现我们的功能,这中实际上属于redis的发布订阅功能中的 一部分,针对redis发布订阅功能是不推荐我们在生产环境中做业务操作的,通常redis内部(例如redis集群节点上下线,选举等等来使用),我们业务系统使用它的这个事件会产 生如下两个问题一个是redis发布订阅的不稳定问题,另一个是redid发布订阅的可靠性问题,具体可以参考redis的发布订阅缺陷

(4)Hash分层记时轮(分层时间轮)算法

这个东西就是专为大批量定时任务管理而生。比如要支持触发时间是一年的精度为秒级别的时间轮,如果单纯的用一个秒级的时间轮:365*24*60*60 这都三千多万个时间格了,造成大量资源开销。而分层的话,那么可分为四个层次:天级别的时间轮,小时级时间轮,分钟级时间轮,秒级时间轮,他们的时间格数分别为:365,24,60,60;总时间格数只有365+24+60+60 = 509个!

(5)MQ的延时消息

当然 MQ的延时消息也可以实现,但是你要知道比如你发送一个延时消息到MQ,但是当你想取消的时候,就没办法删除队列里的消息了,只能通过增加某个取消标志,当延时消息执行的时候,判断一下取消标志,再决定是否进行后续的操作。

 

本质是限流

支付宝接口有流量控制,一定的时间内只允许 N 次接口调用,针对一些业务我们需要频繁调用支付宝开放平台接口,如果不对请求做限制,很容易触发流控告警。

为了避免这个问题,我们按照一定延迟规则将任务加载进时间轮内,通过时间轮的调度来实现接口异步调用

Netty 中的时间轮是通过单线程实现的,如果在执行任务的过程中出现阻塞,会影响后面任务执行。除此之外,Netty 中的时间轮并不适合创建延迟时间跨度很大的任务,比如往时间轮内丢成百上千个任务并设置 10 天后执行,这样可能会导致链表过长 round 值很大,而且这些任务在执行之前会一直占用内存。

 

https://www.cnblogs.com/binlovetech/p/18629491

 

image

2.1 tick 每隔 tickDuration (100ms) 走一个刻度,也就是说 Netty 时间轮的时钟精度就是 100 ms

netty用sleep一个刻度

image

 

2.2 ring

在延时任务模型 HashedWheelTimeout 中有一个字段 —— remainingRounds,用于记录延时任务还剩多少时钟周期可以执行。

private static final class HashedWheelTimeout implements Timeout, Runnable {
    // 执行该延时任务需要经过多少时钟周期
    long remainingRounds;
}

本次时钟周期内可以执行的延时任务,它的 remainingRounds = 0 ,workerThread 在遇到 remainingRounds = 0 的 HashedWheelTimeout 就会执行。

下一个时钟周期才能执行的延时任务,它的 remainingRounds = 1 ,依次类推。当 workerThread 遇到 remainingRounds > 0 的 HashedWheelTimeout 就会直接跳过,并将 remainingRounds 减 1

时间轮的核心数据结构就是一个 HashedWheelBucket 类型的环形数组 wheel , 数组长度默认为 512(位运算 通过 & 运算来代替 % 运算去寻找延时任务对应的 HashedWheelBucket),同 sentinel 1.7.2 ,remainingRounds相当于sentinel的windowstart同样功能

 

3 jdk timer java.util.Timer

3.1 sleep or wait?

image

 用的jdk object timed wait

 

3.2 数组操作小根堆

3.2.1 add

image

数组索引 1 2 3 4 5 6 7
2 4 6 8 10 12 14
3 5 7 9 11 13 15

时间复杂度logn

image

设现在新增第11个元素,路径为--5--2--1,依次比较父节点大小

 

3.2.2 pop

image

根节点与后末尾节点互换,原根节点解除引用

image

设现在size为12,要pop 1,首先1与12互换,然后size变11,从1开始比大小

先找2和3,选近的,假设3近则j变为3,互换1和3,第二层级最近的任务到顶部

k变为3

继续,j变为6

 

 

4 sche 线程池 java.util.concurrent.DelayQueue & java.util.PriorityQueue

数据结构实现同timer

wait or sleep同timer

image

 

5 小结

  timer sche 时间轮   sentinel  
数据结构 数组实现最小堆,无界 数组实现,默认无界,支持有界 ringbuffer 有序链表 ringbuffer  
插入  logn logn o(1) o(n)    
全局数据结构锁  树锁 一把锁,同arrayblockingqueue 无,可仅对槽位加cas乐观锁或偏向锁    
取出/删除  logn logn o(1) o(1)    
   timed wait 没看 sleep      
适用

数量不多

间隔大

使用wait避免大量无意义sleep

可以覆盖delayqueue使之成为有界

支持自定义排序,timer只能用执行时间由近及远

海量短延迟

比如心跳/超时重传

空间换时间

     

posted on 2025-08-29 00:19  silyvin  阅读(12)  评论(0)    收藏  举报