当前位置:首页 > 问答 > 正文

Redis跳表到底怎么读数据,流程细节其实挺复杂的,想搞清楚得慢慢理

关于Redis跳表读取数据的流程,确实不像我们想象中“沿着一条线找下去”那么简单,它之所以高效,是因为它巧妙地结合了“分层”和“跳跃”的思想,用空间换时间,下面我们就像侦探破案一样,一步步理清它找数据的完整路径。

得知道跳表长什么样。

你可以把跳表想象成一个多层的火车线路图,最底层(L0层)是一趟“站站停”的慢车,它连接了所有站点(也就是所有的数据节点),每个站点都停靠,所以这层链表是完整的、有序的,在它上面会有一层快车线(L1层),这趟车只停靠部分大站,再往上,可能还有更快的特快线(L2层),停靠的站点更少,最高层的线路可能就只有起点和终点几个大站了。

在Redis跳表中,每个节点都包含一个值(比如分数score和成员member)和一个“层高”数组,这个层高是随机的,意味着有的节点矮,只在最底层出现;有的节点高,能“冒”到上面的快车线甚至特快线里去,高节点在每一层都有一条指针指向同层的下一个节点。

侦探要出发找目标了,假设我们要找分数为88的节点。

  1. 从最高层开始侦查: 侦探不会傻乎乎地从最底层的第一个站开始一个一个问,他会直接站在最高的特快线(比如L3层)的起点(头节点),他看特快线上的第一个站点的分数是多少,比如是50,因为50<88,说明目标88还在后面,所以他沿着特快线走到50这个站点。

  2. 在当前层继续前进,直到下一个站点“超标”: 到了50这个高节点后,侦探会继续在特快线上看下一个站点是多少,比如下一个是100,坏了,100>88,这意味着特快线已经开过头了,目标88肯定不在50和100之间的特快线上。

  3. 下降一层,缩小范围: 既然特快线找不到了,侦探就下降一层,来到快车线(L2层),注意,他是在当前这个50号站点进行下降的,在50号站点的L2层,他再看下一个站点是多少,假设是70,70<88,没问题,说明在快车线上还可以继续前进,于是他沿着快车线走到70这个站点。

  4. 重复“前进”和“下降”的步骤: 到了70这个节点,侦探重复上面的操作:

    • 在快车线(L2层)看下一个站点:90,90>88,又超标了,不能再前进了。
    • 于是他从70号站点再下降一层,来到“大站车”线(L1层),看下一个站点是85,85<88,可以前进,走到85。
    • 到了85号站点,在L1层看下一个站点:90,90>88,再次超标。
    • 他只好从85号站点下降到最底层的“站站停”慢车线(L0层),看下一个站点:88!Bingo!找到了。

这个过程有几个非常精妙的细节,是高效的关键:

  • 跳跃,而不是遍历: 整个过程就像我们查字典,不会从A开始一页页翻,而是根据首字母跳着翻,跳表通过高层指针实现了大范围的跳跃,快速跳过那些肯定不是目标的大段数据。
  • “下一个”和“下降”的决策: 算法的核心逻辑就是在每一层,不断地比较“当前节点的下一个节点的值”与“目标值”,只要下一个节点值还小于目标,就说明还能在本层前进;一旦下一个节点值等于或大于目标,就说明本层已经定位到一个可能的位置,需要下降一层进行更精细的查找,这个比较和决策是循环往复进行的。
  • 查找路径的记录(非常重要但常被忽略的隐含步骤): 在Redis的源码中(zslGetElementByRank这类函数逻辑相关),这个查找路径其实是被暗中记录下来的,在每一步“下降”之前,算法会记下“当前节点”,为什么?因为对于像获取排名(rank)或者范围查询这样的操作,光找到节点本身是不够的,比如你要知道88分排第几名,你就需要知道从起点到88节点之间,每一层到底跳过了多少个节点,记录下路径上的节点,就能方便地回溯和计算跨度(span),从而快速确定排名,这个“记录路径”的过程,可以想象成侦探在每下一层楼梯时,都用粉笔在墙上做个记号。

总结一下整个读取流程:

侦探(查找指针)从最高层的头节点出发,在一个循环里,他会先向右看(比较下一个节点的值),如果下一个节点存在且比目标小,他就大胆地向右走一步(跳跃),如果右边没路了(下一个节点是NULL)或者下一个节点比目标大了,他就向下走一层(降低搜索粒度),这个“向右→向下→再向右→再向下……”的过程一直持续,直到他下降到最底层(L0层),并在最底层找到那个大于或等于目标值的节点为止,如果这个节点的值正好等于目标值,那么查找成功;否则,说明数据不存在。

Redis跳表的读数据,是一个在多条平行轨道上不断进行“跳跃-定位-下降-再精确定位”的智慧过程,它避免了底层漫长的顺序遍历,其效率可以媲美平衡树,但实现起来却优雅和简单得多。

Redis跳表到底怎么读数据,流程细节其实挺复杂的,想搞清楚得慢慢理