redis的zset跳表

基础

读懂才会用 : 带你见识 Redis 的 zset

读懂才会用:Redis ZSet 的几种使用场景

1 跳表

1.1 在实践中,通常采用的策略是每一层的节点数是其下一层的一半(或者固定的比例),这样可以确保每一层都能有效地减少查找的节点数。为什么,这可以证明吗?

跳表中每一层节点数是其下一层的一半(或其他固定比例),是为了在保证查找效率的同时,尽可能地减少额外的空间消耗。当我们在跳表中查找一个元素时,我们首先在最高层的链表中进行查找,如果当前节点的下一个节点的值比要查找的值大,那么我们就到下一层继续查找,直到找到目标元素或者确定元素不存在。

由于每一层节点数是其下一层的一半,我们在每一层都可以跳过大量的节点,从而大大提高查找效率。具体来说,如果跳表的层数为 h,且每层节点数是其下一层的 1/2,那么在最坏情况下,我们需要查找的节点数为 O(h)。由于 h 通常取 log(n),其中 n 是跳表中元素的数量,所以跳表的查找效率为 O(log(n))。

这个查找效率是基于假设每一层节点数是其下一层的一半得出的。如果每一层的节点数不是其下一层的一半,比如说每一层的节点数都一样,那么跳表的查找效率就会变为 O(n),这是因为在最高层的链表中,我们无法跳过任何节点,只能一个一个地查找。

至于为什么选择每一层节点数是其下一层的一半,而不是其他比例,这是因为这个比例在保证查找效率的同时,可以尽可能地减少额外的空间消耗。如果比例过大,比如说每一层节点数是其下一层的 3/4,查找效率不一定会提高,但是额外的空间消耗会增大,;反之,如果比例过小,比如说每一层节点数是其下一层的 1/4,虽然可以减少额外的空间消耗,但查找效率也会降低。

因此,每一层节点数是其下一层的一半这个比例是一种折中的选择,它在查找效率和空间消耗之间找到了一个平衡。

1.2 跳表每一层的跳数

跳表是一个可以进行快速查找的数据结构,它是通过在普通有序链表上增加多级索引来实现的。每一层的索引节点数是其下一层的一半(或者固定的比例),因此查找时可以在上层索引快速跳过不必要的节点,然后再在下一层进行查找,以此类推,直到找到目标节点或者确定目标节点不存在。

每一层跳跃的节点数并没有固定的规定,一般会根据实际的数据量和查询效率的要求来动态调整。但在实践中,通常采用的策略是每一层的节点数是其下一层的一半(或者固定的比例),这样可以确保每一层都能有效地减少查找的节点数。

可以举一个例子,假设数组大小为10,则第0层为最底层10个节点,第一层有5个,第二层为3个,第四层为2个,第五层为1个。

那么第i层的跳数均值为O(10/(2^i)/2);

1.3 跳表插入时的操作

插入的时候,首先要进行查询,然后从最底层开始,插入被插入的元素。然后看看从下而上,是否需要逐层插入。可是到底要不要插入上一层呢?我们要想每层的跳跃都非常高效,那就越是平衡越好(第一层1级跳,第二层2级跳,第3层4级跳,第4层8级跳)。但是用算法实现起来,确实非常地复杂的,并且要严格地按照2地指数次幂,我们还要对原有地结构进行调整。所以跳表的思路是抛硬币,听天由命,产生一个随机数。Redis 中 25%概率再向上扩展。这样子,每一个元素能够有X层的概率为0.25^(X-1)次方。在 Redis 中level初始化时就定义好了,为 32 层。那么,第32层有多少个元素的概率大家可以算一下。

1.4 跳表的删除操作相对于插入和查找来说稍微复杂一些,但其基本步骤如下:

  1. 查找要删除的节点:从跳表的最高层开始,沿着每一层进行查找,直到找到要删除的节点。这个过程和跳表的查找操作类似。

  2. 删除节点:找到要删除的节点后,将该节点从每一层的链表中删除。这个过程从最低层开始,将要删除的节点的前后节点连接起来,使其跳过要删除的节点。然后逐层向上,重复此过程,直到最高层。

  3. 调整跳表的层数:如果删除的节点是跳表中最高层的唯一节点,那么需要将跳表的总层数减一。

通过以上步骤,我们可以从跳表中删除任意节点,并且保证跳表的查找效率。虽然这个过程看起来有点复杂,但实际上,由于跳表的结构,每一步操作的时间复杂度都是O(1),所以总的时间复杂度仍然是O(log n)。

2 ZSET

2.1 redis的zset由hash+zskiplist组成,讲讲hash表的作用,key和value分别存什么,那zskiplist的一个节点存什么呢?

Redis zset 组成和存储:在 Redis 中,zset(有序集合)是由哈希表和跳表共同组成的。哈希表用于存储元素和其对应的分数,即 key 是元素,value 是分数,这样可以保证了 zset 的元素唯一性,并且可以快速查找元素及其对应的分数。跳表则用于根据分数对元素进行排序,跳表的一个节点包含元素和其对应的分数。

3 应用场景

3.1 实现固定时间段内 “1小时最热门” 榜单:

这个实现方案的关键在于利用 zset 的排序功能和 Redis 的过期删除功能。我们以当前小时的时间戳作为 zset 的 key,贴子ID作为 member,点击数评论数等作为 score。这样,在每个小时内,所有发生的点击和评论都会被记录在对应的 zset 中,并按照 score 进行排序。**在新的一小时开始时,会创建一个新的 zset,**并设置其过期时间为一小时,旧的 zset 会在过期后被自动删除。

以上方案只能实现固定时间段(如每小时、每天等)的“最热门”榜单。这是因为我们是通过创建一个新的 zset 并设置过期时间的方式,来切换到新的时间段。这样做的结果就是,在两个时间段之间,我们无法获取到连续的数据。

3.1.1 如果我们想要实现连续时间段内的“最热门”榜单(比如过去一小时内的榜单),我们就需要采取不同的策略。其中一种可能的策略是:

对于每一个帖子,我们都维护一个 zset,key 为帖子的 ID,member 为时间戳,score 为点击数或评论数。每次用户对帖子进行点击或评论,我们就将当前时间戳和对应的 score 添加到这个 zset 中。

在获取过去一小时内的“最热门”榜单时,我们首先获取当前时间戳一小时前的时间戳。然后,我们遍历每一个帖子的 zset,使用 ZRANGEBYSCORE 命令,获取 score 在这一小时内的所有 member,并计算其总分。然后,我们按照总分进行排序,得到过去一小时内的“最热门”榜单。

3.1.2 缺点

这种策略能够实现连续时间段内的“最热门”榜单,但是需要注意,如果帖子的数量非常大,那么在获取榜单时,我们可能需要遍历大量的 zset,这可能会导致性能问题。所以,在选择具体的实现方案时,我们需要根据实际的业务需求和性能需求进行权衡。

3.2 zset实现限流

滑动窗口是限流常见的一种策略。如果我们把一个用户的 ID 作为 key 来定义一个 zset ,member 或者 score 都为访问时的时间戳。我们只需统计某个 key 下在指定时间戳区间内的个数,就能得到这个用户滑动窗口内访问频次,与最大通过次数比较,来决定是否允许通过。

思路是每一个请求到来时,将时间窗口外的记录全部清理掉,只保留窗口内的记录。zset 中只有 score 值非常重要,value 值没有特别的意义,只需要保证它是唯一的就可以了

3.2.1 优缺点

使用 Redis 的 zset 实现滑动窗口限流算法是一个简洁高效的方法。这种方法基于 Redis 的有序集合,使得你能在一个滑动时间窗口内对请求进行计数,并且可以精确地对请求在时间轴上进行排序和清除。

优点:

  1. 简洁有效:使用 zset 可以非常方便的实现滑动窗口限流。你只需要在每次请求时将时间戳添加到 zset,然后移除时间窗口之前的记录,再检查剩余的记录数是否超过限流值即可。

  2. 高性能:Redis 是内存数据库,读写速度非常快,可以支持高并发环境下的限流。

  3. 精确控制:由于 zset 中的每个元素都有一个与之关联的分数(这里是时间戳),因此你可以精确地对请求在时间轴上进行排序和清除。

缺点:

  1. 存储消耗:对于访问量巨大的系统,zset 中可能需要存储大量的记录,如果滑动窗口的时间段设置较长,可能会占用大量的内存。

  2. 无法跨节点共享:如果你的系统是分布式的,这种基于 Redis 的限流方式无法在多个节点之间共享限流状态。每个服务节点的 Redis 都会维护一个独立的限流计数,这可能会导致实际的请求次数超过你的限流值。

  3. Redis单点故障:如果Redis出现故障,可能会导致限流功能无法使用,对系统造成影响。

  4. 清理过期数据的操作可能消耗较大的CPU资源:如果清理过期数据的操作过于频繁或者需要清理的数据量过大,可能会占用较大的CPU资源。

3.3 延时队列

3.3.1 实现

zset 会按 score 进行排序,如果 score 代表想要执行时间的时间戳。在某个时间将它插入zset集合中,它变会按照时间戳大小进行排序,也就是对执行时间前后进行排序。

起一个死循环线程不断地进行取第一个key值,如果当前时间戳大于等于该key值的score就将它取出来进行消费删除,可以达到延时执行的目的。

3.3.2 没有 ack 机制,当消费失败的情况下队列如何处理?

Redis 本身并没有提供消息队列中常见的 ack 机制(确认机制),所以在使用 Redis 实现队列时需要自行解决这个问题。一种可能的解决方案是,当一个消费者从队列中取出一个消息后,先将它存入一个“处理中”队列,然后开始处理。如果处理成功,就从“处理中”队列中删除该消息;如果处理失败,就可以根据“处理中”队列重新进行处理或者进行其他的错误处理。

3.3.3 这是 topic 模式,广播模式如何搞

在 Redis 中,可以使用 Pub/Sub(发布/订阅)模式来实现广播。在这个模式中,生产者(发布者)可以向一个频道(channel)发送消息,所有订阅了该频道的消费者(订阅者)都可以接收到这个消息。这就是一种广播模式,因为消息被发送到了所有的订阅者。

3.3.4 示例代码是demo,简单应用,投入生产中还需要考虑各种细节问题

将 Redis 用于生产环境中的消息队列,确实需要考虑许多额外的问题。例如:

  • 持久化:Redis 的数据是保存在内存中的,如果 Redis 服务器宕机,未处理的消息可能会丢失。因此,你可能需要配置 Redis 的-

  • 持久化策略,或者使用其他的持久化消息队列系统,如 RabbitMQ 或 Kafka。

  • 并发处理:如果有大量的消息需要处理,可能需要在多个消费者之间进行负载均衡。

  • 错误处理:如果一个消息处理失败,需要有策略来处理这种情况,例如重试、记录错误信息等。

  • 消息顺序:Redis 的 List 和 Pub/Sub 都不能保证在高并发下消息的顺序,如果业务上需要严格的顺序,这需要额外的设计来保证。

以上只是一些常见的问题,具体的实现可能还需要考虑更多的细节和业务需求。

猜你喜欢

转载自blog.csdn.net/yxg520s/article/details/132112140