【项目优化】 用跳表实现timer

介绍

对C++选手烂大街的项目Webserver进行局部优化,将定时器模块(定期处理非活跃连接)由小根堆实现更改为用跳表实现,克服了用小根堆实现定时器的一些弊端。
定时器模块的功能主要包括:

  1. 每一个客户端与服务器建立好连接后,为该连接创建定时器,经过一段时间后,若客户端没有与服务器进行通信,服务器则认为其为非活跃连接,将这个连接删除,以便其他客户端接入。
  2. 定期检测(心跳)非活跃的连接,将这些非活跃的连接清除。
  3. 若用户在被清除前与服务端进行交互(读写),延长该节点的超时时间(因为是活跃用户)。
  4. 若因其他原因导致连接中断,立即清除该连接的定时器。

需求

wabserver项目利用定时器模块(以下称为timer),来定期清除不活跃的连接,对外需要提供接口实现清除和添加连接的功能,内部需要使用一定的存储结构来保存每个连接对应的定时器。

  • timer模块需要向主线程提供四个api:
    1. 添加定时器,有客户端与服务器进行连接时,为该连接创建定时器,并添加到timer模块的存储结构中(该存储结构默认是小根堆,本文目的就是将其更改为跳表);
    2. 给指定的定时器延时,延长活跃用户的超时时间;
    3. 心跳,检查并清除所有超时的用户;
    4. 删除指定连接的定时器。

小根堆实现 timer

利用小根堆实现timer,可以利用哈希表建立定时器节点索引与事件文件描述符的映射关系,所以查找事件所对应的定时器所需要的时间复杂度为O(1)。
简要的逻辑结构和物理结构的对应图如下:
在这里插入图片描述
节点上数据为该定时器剩余超时时间,单位为毫秒(ms)。

复杂度分析

  • 添加定时器:O(1),只要将节点插入小根堆末尾即可,因为新插入节点剩余的超时时间必然最大。
  • 节点延时:
    • 找到节点:O(1),利用文件描述符和节点下标的映射关系(哈希表),可以在O(1)时间复杂度下找到该节点;
    • 给节点设置延时后向下修复小根堆性质:O(logn)
    • 总的时间复杂度:O(logn)
  • 心跳:
    • 从前向后遍历节点,超时则删除连接,复杂度O(n)
    • 每次删除都要将头节点和尾节点互换,删除尾节点,再修复小根堆性质,复杂度O(logn)
    • 总的时间复杂度:O(nlogn);
  • 删除指定节点的定时器:
    • 找到节点,复杂度O(1),利用哈希表,同节点延时;
    • 删除节点并修复小根堆性质,复杂度O(logn);
    • 总的时间复杂度:O(logn)

存在的问题

1、节点漏删

小根堆逻辑结构是一个完全二叉树,而物理结构是一个数组(数组中每个元素代表一个定时器节点),如果一次只删除一个节点(就像实现优先队列一样),那么不存在节点漏删的情况。可是在实现timer的过程中,小根堆需要提供节点延时批量删除的功能,当一个节点延时之后,会破坏物理结构当中的递增顺序,如下图:

在这里插入图片描述

  • 按照节点插入的先后顺序插入小根堆,所以初始状态下小根堆中节点是按从小到大顺序存放的。
  • 假设根节点对应的连接此时和服务端进行交互,那么系统对根节点设置延时并向下修复小根堆性质,此时物理结构中节点已经不按顺序存放了。那么当11ms过后,下标为1和3的节点都已经过期,但下标为2的节点还剩很长的超时时间,这会导致下一次心跳操作的时候,遍历到节点2时,就结束心跳,后面的节点3也超时了,但是没被删除。

2、心跳复杂度太高

  • 心跳操作需要将已超时的所有节点都删除,根据小根堆性质,需要从前往后遍历,才能找到所有超时节点(剩余超时时间最小);
  • 而根据小根堆性质,每删除一个节点都需要将删除节点和小根堆最后一个元素交换,删除堆尾元素,最后再向下修复小根堆性质,需要的时间复杂度为O(logn)
  • 那么总的时间复杂度就为O(nlogn),实际上这是可以利用其它结构进行优化的,比如跳表。

跳表实现timer

用跳表实现timer,相比小根堆的一个明显优势就是存储结构本身是有序的。所以心跳操作过程中,从前向后遍历的时候不存在漏删的情况,而且大部分接口的时间复杂度要低于小根堆,具体分析如下。

复杂度分析

  • 添加定时器:O(1),只要将节点插入跳表末尾即可,因为新插入节点剩余的超时时间必然最大,需要设置哨兵指向定时器尾部,相比于小根堆还需要额外更新索引指针
  • 节点延时:
    • 找到节点:O(1),利用文件描述符和节点下标的映射关系(哈希表),可以在O(1)时间复杂度下找到该节点;
    • 删除该节点,需要从前往后找到该节点的所有前驱节点(因为跳表的索引有多个层次),时间复杂度为O(logn)
    • 最后在跳表尾部重新添加延时后的节点O(1)
    • 总的时间复杂度:O(logn)
  • 心跳:
    • 从前向后遍历节点,超时则删除连接,复杂度O(n)
    • 每次删除只需要释放节点并修改索引指向,时间复杂度O(1)
    • 总的时间复杂度:O(n);
  • 删除指定节点的定时器:
    • 找到节点,复杂度O(1),利用哈希表,同节点延时;
    • 删除节点并修复索引指向,复杂度O(logn)(需要找到前驱节点);
    • 总的时间复杂度:O(logn)

所有功能的时间复杂度相较于小根堆都有很大提升,理论上效率应当比小根堆高很多,但实际测试下来QPS相比用小根堆实现的服务器只高了1000左右…

思考原因
1、每个接口修复索引都可以在O(1)时间复杂度内完成,但实际上从上至下修复每一层索引的指针还是花了不少时间;
2、索引的层级(我设置8或16)还是比较高的,而且并不能缩小,不然每一层索引的密度会非常高;
导致每次删除或更新节点时修复索引的开销很大;
3、定时器模块对webserver的QPS影响可能本身就不大,还有其他几个模块也都会造成影响,思维确实不应该局限在这了。

存在的问题

  • 利用小根堆实现timer,可以使用哈希表建立文件描述符fd和节点的映射关系(这应该是效率最高的搜索方式,可以将查找某一结点的时间复杂度优化到O(1)),那么实际上就用不到跳表本身复杂度为O(logn)的查询优势了;
  • 实现过程中,需要设置哨兵指向跳表尾部(这样在添加新节点时时间复杂度为O(1)),但在高并发情况下,每次修改哨兵指向有可能被其他线程干扰,可以考虑使用原子变量进行优化。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 是一种高效的数据结构,它结合了链和二分查找的优点,可以在log(n)的时间复杂度内完成查询、插入和删除操作。 使用 Python 实现可以通过自定义一个类,实现的相关操作。这个类需要有插入、删除、查询等方法,还需要维护中各个节点的关系。 示例代码: ``` class Node: def __init__(self, key, value, level): self.key = key self.value = value self.forward = [None] * (level + 1) class SkipList: def __init__(self, max_level, p): self.MAX_LEVEL = max_level self.p = p self.header = self.createNode(self.MAX_LEVEL, -1, -1) self.level = 0 def createNode(self, level, key, value): n = Node(key, value, level) return n def randomLevel(self): level = 0 while random.random() < self.p and level < self.MAX_LEVEL: level += 1 return level def insert(self, key, value): update = [None] * (self.MAX_LEVEL + 1) x = self.header for i in range(self.level, -1, -1): while x.forward[i] and x.forward[i].key < key: x = x.forward[i] update[i] = x x = x.forward[0] if x is None or x.key != key: rlevel = self.randomLevel() if rlevel > self.level: for i in range(self.level + 1, rlevel + 1): update[i] = self.header self.level = rlevel x = self.createNode(rlevel, key, value) for i in range(rlevel + 1): x.forward[i] = update[i].forward[i] update[i].forward[i] = x def delete(self, key): update = [None] * (self.MAX_LEVEL + 1) x = self.header for i in range(self.level, -1, -1): while x.forward[i] and x.forward[i].key < key: x = x. ### 回答2: (Skip List)是一种用于有序链的数据结构,它允许快速的搜索、插入和删除操作。在中,每个节点都有多个指针,指向其他节点,从而形成多层次的链结构。每一层都是前一层的子集,跃指针允许我们快速地定位到需要搜索的节点。 使用Python实现可以按照以下步骤进行: 1. 定义SkipNode类:这个类中的每个节点,每个节点包含一个值和多个指针。可以使用Python的类和对象来示节点。 2. 定义SkipList类:这个类示整个。它包含一个指向头部的指针和其他辅助方法,如插入、搜索和删除等操作。 3. 实现插入操作:插入操作需要保持的有序性,可以从最高层开始,根据节点的值和搜索路径将节点插入到相应的位置。 4. 实现搜索操作:搜索操作需要根据节点的值和搜索路径找到目标节点,可以从最高层开始,通过比较节点的值和目标值来确定前进方向。 5. 实现删除操作:删除操作需要将目标节点从中删除,可以根据节点的值和搜索路径找到目标节点,并更新指针来过目标节点。 通过上述步骤,可以使用Python实现数据结构。 ### 回答3: (Skip List)是一种有序数据结构,它使用了多层链来加速查询操作。在中,每个节点除了保存自身的值外,还保存了若干个指向其他节点的引用,这些引用就像是过了中间的节点,直接连接到了更远的节点,从而实现了快速的查找。 要使用Python实现,可以定义一个节点类,节点类包含节点的值以及一个指向下一个节点的指针。然后可以定义一个类,类包含了的头节点以及其他相关方法和属性。 下面是一个简单的Python代码实现的示例: ```python import random import math class SkipListNode: def __init__(self, value): self.value = value self.next = None self.down = None class SkipList: def __init__(self): self.head = None self.max_levels = 16 self.current_levels = 0 def flip_coin(self): return random.randint(0, 1) == 1 def insert(self, value): if self.head is None: self.head = SkipListNode(value) self.current_levels = 1 return level = 1 while self.flip_coin() and level < self.max_levels: level += 1 if level > self.current_levels: for _ in range(level - self.current_levels): new_head = SkipListNode(None) new_head.down = self.head self.head = new_head self.current_levels = level current = self.head prev = None while current: if current.value is None or current.value > value: if prev is None: prev = SkipListNode(value) prev.next = current self.head = prev else: new_node = SkipListNode(value) new_node.next = current prev.next = new_node prev.down = new_node prev = new_node prev = current current = current.next def search(self, value): current = self.head while current: if current.value == value: return True elif current.next and current.next.value <= value: current = current.next else: current = current.down return False def display(self): current = self.head while current: print(current.value, end=" -> ") current = current.next print("None") skip_list = SkipList() for i in range(1, 20): skip_list.insert(i) skip_list.display() print(skip_list.search(10)) print(skip_list.search(20)) ``` 上述代码是一个基本实现的示例,通过`insert`方法可以将值插入中,通过`search`方法可以查找值是否存在于中。`display`方法用于打印的内容。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

事多做话少说

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值