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

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

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?

用的jdk object timed wait
3.2 数组操作小根堆
3.2.1 add

| 数组索引 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 
| 左 | 2 | 4 | 6 | 8 | 10 | 12 | 14 | 
| 右 | 3 | 5 | 7 | 9 | 11 | 13 | 15 | 
时间复杂度logn

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

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

设现在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

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只能用执行时间由近及远  | 
 海量短延迟 比如心跳/超时重传 空间换时间  | 
                    
                
                
            
        
浙公网安备 33010602011771号