【Redis源码系列】Redis6.0数据结构详解--skiplist篇

234 阅读3分钟

前言

上篇文章我们研究了Redis 压缩列表ziplist数据结构的实现原理, 并深入分析了其针对性的优化手段。本篇我们研究一下另一个数据结构跳跃表, 即skiplist。关于跳跃表原理实现相对比较简单, 想必大家都有所了解。本篇我们简要分析Redis中的调表实现, 重点自己实现一版跳跃表数据结构, 以此加深对于跳跃表的认知。

前期回顾: 【Redis源码系列】Redis6.0数据结构详解--ziplist篇

skiplist简介

跳跃表是一种随机的数据结构, 相比于普通单链表通过增加层级的指针指向, 能够更加快速的定位元素。核心思想类似于二分查找。查找和插入操作都可以在对数阶复杂度下完成。

image.png

Redis跳跃表实现

核心定义

数据定义在src/server.h中, 定义如下:

image.png 节点定义如下:

image.png 可以看到节点定义中增加了后退节点的指针, 相比于典型的跳跃表定义, Redis实现做了有如下改动:

  • 允许score重复, 即多个不同的member 的 score 值可以相同。进行比对操作时, 需要检查scoremember域。
  • 每个节点增加一个backward指针, 层高为1, 当执行ZREVRANGEZREVRANGEBYSCORE需要逆序处理的指令是会使用到相关的字段。

核心api

api定义实现在src/t_zset.c下。相关api实现以及时间复杂度如下:

函数作用复杂度
zslCreateNode创建并返回一个新的跳跃表节点最坏 O(1)
zslFreeNode释放给定的跳跃表节点最坏 O(1)
zslCreate创建并初始化一个新的跳跃表最坏 O(1)
zslFree释放给定的跳跃表最坏 O(N)
zslInsert将一个包含给定 score 和 member 的新节点添加到跳跃表中最坏 O(N)平均 O(logN)
zslDeleteNode删除给定的跳跃表节点最坏 O(N)
zslDelete删除匹配给定 member 和 score 的元素最坏 O(N) 平均 O(logN)
zslFirstInRange找到跳跃表中第一个符合给定范围的元素最坏 O(N) 平均 O(logN)
zslLastInRange找到跳跃表中最后一个符合给定范围的元素最坏 O(N) 平均 O(logN)
zslDeleteRangeByScore删除 score 值在给定范围内的所有节点最坏 O(N^2)
zslDeleteRangeByRank删除给定排序范围内的所有节点最坏 O(N^2)
zslGetRank返回目标元素在有序集中的排位最坏 O(N)平均 O(logN)
zslGetElementByRank根据给定排位,返回该排位上的元素节点最坏 O(N)平均 O(logN)

golang版本实现

结构定义

const (
   maxLevel         = 8  // 最大层高
   levelProbability = 0.5 // 新增层高概率定义
)

// 元素
type Ele struct {
   Score       float64
   Value       interface{}
   NextEleList []*Ele
}

type SkipList struct {
   header *Ele 
   len    int64 // 元素长度
   level  int64 // 跳表的层高
}

查找元素

// 查找
func (spl *SkipList) SearchByScore(score float64) *Ele {
   // 查找当前score前一个节点元素
   preEle := spl.header 
   for i := spl.level - 1; i >= 0; i-- {
      for preEle.NextEleList[i] != nil && preEle.NextEleList[i].Score < score {
         preEle = preEle.NextEleList[i]
      }
   }
   // 当前节点 = 前一个节点的第一层指向
   curEle := preEle.NextEleList[0]
   if curEle != nil && curEle.Score == score {
      return curEle
   }
   return nil
}

新增元素

// 插入
func (spl *SkipList) Insert(score float64, value interface{}) *Ele {
   // 查找当前score的前一个节点
   TraversedEle := make([]*Ele, maxLevel)
   preEle := spl.header
   for i := spl.level - 1; i >= 0; i-- {
      for preEle.NextEleList[i] != nil && preEle.NextEleList[i].Score < score {
         preEle = preEle.NextEleList[i]
      }
      TraversedEle[i] = preEle // 记录遍历过的节点
   }
   preEle = preEle.NextEleList[0] // 最近前一个节点的下一个节点

   // 如果当前节点已经相同, 则更新值
   if preEle != nil && preEle.Score == score {
      preEle.Value = value
      return preEle
   }

   // 维护层高
   level := randLevel()
   if level > spl.level {
      level = spl.level + 1 // 超出层高需要
      TraversedEle[spl.level] = spl.header
      spl.level = level
   }
   e := NewEle(score, value, level)
   for i := 0; int64(i) < level; i++ {
      // 新元素的 i 层指向上一个节点的 i 层
      e.NextEleList[i] = TraversedEle[i].NextEleList[i]
      // 上一个节点的 i 层 指向当前元素
      TraversedEle[i].NextEleList[i] = e
   }
   spl.len++
   return e
}

删除元素

// 删除
func (spl *SkipList) Remove(ele *Ele) bool {
   // 查找当前节点最近一个前节点
   travedEle := make([]*Ele, maxLevel)
   preEle := spl.header
   for i := spl.level - 1; i >= 0; i-- {
      for preEle.NextEleList[i] != nil && preEle.NextEleList[i].Score < ele.Score {
         preEle = preEle.NextEleList[i]
      }
      travedEle[i] = preEle
   }
   // 当前节点 = 最近一个节点的0层指向
   curEle := preEle.NextEleList[0]

   if curEle != nil && curEle.Score == ele.Score {
      for i := 0; int64(i) < spl.level; i++ {
         if travedEle[i].NextEleList[i] != curEle {
            return false
         }
         // 前一个节点指向当前节点的下一个元素
         travedEle[i].NextEleList[i] = curEle.NextEleList[i]
      }
      spl.len--
   }
   return true
}

总结

本节我们复习了跳跃表的实现, 跳跃表在数据库的实现中应用比较广泛, 如Redis, HBase都有使用,理解上相对也比较简单, 希望看到的小伙伴们可以点个赞 :)