定时器原理与实现

定时器概述

对于服务端来说,驱动服务端逻辑的事件主要有两个,⼀个是⽹络事件,另⼀个是时间事件;

在不同框架中,这两种事件有不同的实现⽅式;

第⼀种,⽹络事件和时间事件在⼀个线程当中配合使⽤, 将定时器中最近的时间,设置为epoll_wait的timeout参数;例如nginx、redis;

第⼆种,⽹络事件和时间事件在不同线程当中处理;例如linux crontab, skynet;

// 第⼀种
while (!quit) {
    int now = get_now_time();// 单位:ms
    int timeout = get_nearest_timer() - now;
    if (timeout < 0) timeout = 0;
    int nevent = epoll_wait(epfd, ev, nev, timeout);
    for (int i=0; i<nevent; i++) {
    	//... ⽹络事件处理
    }
    update_timer(); // 时间事件处理
}
// 第⼆种 在其他线程添加定时任务
void* thread_timer(void * thread_param) {
    init_timer();
    while (!quit) {
    update_timer(); // 更新检测定时器,并把定时事件发送到消息队列中
    	sleep(t); // 这⾥的 t 要⼩于 时间精度
    }
    clear_timer();
    return NULL;
}
pthread_create(&pid, NULL, thread_timer, &thread_param);

定时器误差大怎么解决

对于第一种方式,如果网络事件的处理时间很长,影响了定时器事件的处理,造成定时器误差很大,怎么解决?

定时信号解决。定时发送信号,会打断epoll_wait

img

https://www.cnblogs.com/li-hao/archive/2013/04/23/3038542.html

定时器设计

定时器本质是越近要触发的定时任务,它的优先级越高。

定时器需要包含以下方法:

  • 初始化定时器

  • 添加定时任务

  • 删除定时任务

  • 检测定时任务

  • 销毁定时器

定时器数据结构选择

定时器的数据结构,必须满足以下两点:

  • 能够快速进行增加删除操作;
  • 能够快速找到最小的节点。

以下数据结构可以实现定时器:

  • 最小堆

对于增查删,时间复杂度为o(logn); 对于最小节点(即根节点)的查找复杂度是o(1)。

  • 红黑树

增查删时间复杂度都是o(logn); 对于最小节点的查找时间复杂度是o(h), h是红黑树高度。

  • 跳表

先不讨论skiplist,后面redis中会具体介绍skiplist。

  • 时间轮

时间轮分为单层级和多层级时间轮。

红黑树

关于红黑树的实现,可以参考rbtree实现

怎么解决相同时间的key?

红黑树并没有要求key不能相同。对于key相同的情况,新节点直接加入到原节点的右子树。nginx红黑树实现就是这样的。

void
ngx_rbtree_insert_value(ngx_rbtree_node_t *temp, ngx_rbtree_node_t *node,
    ngx_rbtree_node_t *sentinel)
{
    ngx_rbtree_node_t  **p;

    for ( ;; ) {

        p = (node->key < temp->key) ? &temp->left : &temp->right;

        if (*p == sentinel) {
            break;
        }

        temp = *p;
    }

    *p = node;
    node->parent = temp;
    node->left = sentinel;
    node->right = sentinel;
    ngx_rbt_red(node);
}


void
ngx_rbtree_insert_timer_value(ngx_rbtree_node_t *temp, ngx_rbtree_node_t *node,
    ngx_rbtree_node_t *sentinel)
{
    ngx_rbtree_node_t  **p;

    for ( ;; ) {

        /*
         * Timer values
         * 1) are spread in small range, usually several minutes,
         * 2) and overflow each 49 days, if milliseconds are stored in 32 bits.
         * The comparison takes into account that overflow.
         */

        /*  node->key < temp->key */
        p = ((ngx_rbtree_key_int_t) (node->key - temp->key) < 0)
            ? &temp->left : &temp->right;

        if (*p == sentinel) {
            break;
        }

        temp = *p;
    }

    *p = node;
    node->parent = temp;
    node->left = sentinel;
    node->right = sentinel;
    ngx_rbt_red(node);
}

stl的map则不允许有重复的key,如果key相同,则更新value值。实现类似下面这样。

void
ngx_rbtree_insert_value(ngx_rbtree_node_t *temp, ngx_rbtree_node_t *node,
    ngx_rbtree_node_t *sentinel)
{
    ngx_rbtree_node_t  **p;

    for ( ;; ) {

        if (node->key == temp->key) {
            temp->data = node->data;
            return;
        }

        p = (node->key < temp->key) ? &temp->left : &temp->right;

        if (*p == sentinel) {
            break;
        }

        temp = *p;
    }

    *p = node;
    node->parent = temp;
    node->left = sentinel;
    node->right = sentinel;
    ngx_rbt_red(node);
}

红黑树增删查复杂度是o(logn),查找最左侧节点复杂度是o(h)

int find_nearest_expire_timer() {
    ngx_rbtree_node_t  *node;
    if (timer.root == &sentinel) {
        return -1;
    }
    node = ngx_rbtree_min(timer.root, timer.sentinel);
    int diff = (int)node->key - (int)current_time();
    return diff > 0 ? diff : 0;
}

static ngx_rbtree_node_t *
ngx_rbtree_min(ngx_rbtree_node_t *node, ngx_rbtree_node_t *sentinel)
{
    while (node->left != sentinel) {
        node = node->left;
    }

    return node;
}

最小堆

定义

满二叉树:所有层的节点数都是该层所能容纳节点的最大数量(2^n; n >= 0);

完全二叉树:若二叉树的深度为h,除了h层外,其他层都是该层所能容纳节点的最大数量(2^n; n >= 0), 且h层的节点都集中在最左侧;

最小堆:

  1. 是一棵完全二叉树;

  2. 每一个节点的值都小于等于它的子节点;

  3. 每一棵子树都是最小堆。

img

这个是最小二叉堆。大部分定时器都使用最小堆。libevent使用最小二叉堆,go、libev使用最小四叉堆。在大量数据情况下,最小四叉堆比最小二叉堆性能大概提高5%。

img

最小堆通常使用数组来实现,对于下标为x的节点,它的左子树是2x+1,右子树是2x+2,父节点是floor((x-1)/2)

增加操作

为了满足完全二叉树的定义,往二叉树最深层沿着最左侧添加一个节点;然后考虑是否需要进行上升操作。

例如,添加值为4的节点,将4节点插入到5节点的左子树位置,4小于5,4节点和5节点需要交换位置。

删除操作

删除操作需要先查找是否包含这个节点,最小堆的查找复杂度是o(logn),最差情况是o(logn);找到之后,将该节点与最后一个节点交换,先考虑下降操作,如果失败则进行上升操作;最后删除最后一个节点。

例如,删除1号节点,则需要下沉;如果删除9号节点,则需要上升。

最小堆只关心父子的大小关系,不关心兄弟的大小关系,实现要比红黑树简单。

最小堆增删的复杂度都是o(logn), 查找最小节点的复杂度是o(1),最小堆是完全二叉树,相对于红黑树更加平衡,时间复杂度比红黑树稳定。更适合用于定时器。

时间轮

java netty网络库、kafka、skynet、linux crontab都是用时间轮。

红黑树、最小堆在单线程中使用;时间轮在多线程环境中使用。

单层级时间轮

单层级时间轮通常用来实现时间窗口

限流,是动态的,有一个滑动窗口,就是时间窗口;

熔断,是与限流对应的,窗口时固定的。前一个窗口的结束位置,是下一个窗口的开始位置。

怎么防止DDOS攻击?

  1. 网络底层 DPDK,复杂算法;这种方法是最优的。
  2. 应用层,nginx配置1秒内只能发10次包。限制流量。用的是熔断操作。

时间轮需要考虑两个因素:

  1. 时间轮的大小;
  2. 时间精度。

例如,5秒一个心跳包,10秒没有收到心跳包就断开连接。

时间轮的大小要选择2^n, 刚好大于10,所以选择16.

x % 16 = x & (16 - 1)

时间精度是1秒。

数据结构是数组 + 链表,链表可以优化为红黑树或者最小堆。

img

一般不会用单层级时间轮,实际使用为每个连接分配一个定时任务就可以了。

单层级时间轮如果使用不当,会有问题。

如果时间轮设置太大,就会出现踏空的现象(空推进);空推进是分布式定时器必须要解决的问题。分布式定时器大部分是使用的单层级时间轮 + 最小堆。最小堆能告诉时间轮下次检测的是哪个格子,解决了空推进的问题。kafka使用最小堆解决空推进问题。后面会讲分布式定时器。

如果时间精度设置太小,也会出现空推进。

多层级时间轮

多层级时间轮参考了表盘的原理,实现一个3层级的的时间轮。第1层级是秒针,第2层级是分针,第2层级是时针。即将要执行的任务都放在第一层。

img

对于这个时间轮,大小就是60 * 60 * 12秒,精度就是1秒。

多层级时间轮解决了空推进的问题。

添加任务的时候,怎么决定放在哪一层?

触发时间与当前时间间隔小于60,放在第1层级;间隔大于等于60,放在第2层级;间隔大于60*60,则放在第3层级。

在第1层级每1秒移动一格,格子为0-59;每移动一格,执行该格子中的所有定时任务;当第1层指针从59格开始移动,此时层级2移动一格;层级2移动一格的行为定义为,将该格子当中的定时任务重新映射到层级1当中;同理,当第2层指针从59格开始移动,层级3中的定时任务将重新映射到层级2中。

如何重新映射?

(定时任务的过期时间 - 当前时间)% (上一层的长度)

得到映射到上一层中的具体位置。

第一层0号索引有数据,后面层级的0号索引是没有数据的

为什么很多开源时间轮, 最后一层0号位置有数据?

如果定时任务超过时间轮的大小了,则会放到最后一层的0号位置。

时间轮增加查找复杂度都是o(1),锁的粒度小,用空间换时间。

所以多线程中,使用时间轮。

3层级。

在第1层级每1秒移动一格,格子为0-59;每移动一格,执行该格子中的所有定时任务;当第1层指针从59格开始移动,此时层级2移动一格;层级2移动一格的行为定义为,将该格子当中的定时任务重新映射到层级1当中;同理,当第2层指针从59格开始移动,层级3中的定时任务将重新映射到层级2中。

如何重新映射?

(定时任务的过期时间 - 当前时间)% (上一层的长度)

得到映射到上一层中的具体位置。

第一层0号索引有数据,后面层级的0号索引是没有数据的

为什么很多开源时间轮, 最后一层0号位置有数据?

如果定时任务超过时间轮的大小了,则会放到最后一层的0号位置。

时间轮增加查找复杂度都是o(1),锁的粒度小,用空间换时间。

所以多线程中,使用时间轮。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值