Redis 中的有序集合(ZSet)是一种非常灵活的数据结构,它不仅可以存储唯一的成员,还能为每个成员分配一个分数(score),从而实现对成员的排序。在 Redis 内部,有序集合ZSet使用了两种数据结构来实现:跳跃表(SkipList)和哈希表(Dict)。跳跃表作为一种数据结构,具有简单易实现、性能优越的特点,非常适合 Redis 这样的高性能数据库。

一、跳跃表概述
跳跃表是一种随机化的数据结构,它通过在链表的基础上添加多级索引层来提高查找效率。这种数据结构可以看作是平衡树的一种替代方案,但它在实现上更为简单,同时在某些方面甚至可以提供更好的性能。
1. 跳跃表的主要优点
平均查找时间复杂度为 O(log N)。
插入和删除操作的平均时间复杂度也为 O(log N)。
实现简单,相比平衡树来说更容易理解和实现。
由于使用了指针,它在内存使用上比平衡树更灵活。

二、跳跃表的结构
每个节点至少有一个指向下一个节点的指针(称为“第 0 层”),更高的层则可能有更多的指针。节点的层数是随机的,但通常不超过某个预定义的最大层数,以防止过大的内存消耗。
1. 数据项
存储的数据,对于 Redis 的 ZSet 来说,数据项包括成员和分数。
2. 多个指针
指向其他节点的指针,这些指针构成了不同级别的索引层。

三、跳跃表的设计
跳跃表通过多层链表实现数据的“快速通道”,以空间换时间,将平均时间复杂度优化到 O(log N),同时避免平衡树(如红黑树)的复杂再平衡操作。
1. 随机层数
新加入的节点随机确定其层数,通常采用一个概率分布,例如以 1/4 的概率增加一层。
2. 层级索引
每一层都构成一个链表,高层索引指向底层索引中的节点,形成一个层次结构。
3. 高效查找
查找操作利用索引层快速定位目标节点。
4. 动态调整
随着节点的增加或删除,跳跃表会动态调整层数以保持性能。

四、跳跃表的实现
1. 节点结构
每个节点包含成员、分数以及指向下一节点的指针数组。
指针数组的长度对应节点的层数。
2. 查找算法
查找时从最高层开始,逐层向下移动,直到找到目标节点或确定目标不在表中。
查找过程中利用了跳跃表的索引结构,使得查找效率大大提高。
3. 插入和删除
插入时首先确定新节点的层数,然后在适当的层级插入新节点,并更新索引。
删除时需要遍历跳跃表以找到目标节点,并删除它以及相关的索引指针。
4. 层级调整
Redis 通过概率分布来控制节点的层数,以确保跳跃表的性能和内存消耗达到平衡。
通常,Redis 限制最高层数,以避免过高的内存使用。
5. Java 实现
下面是一个简化的跳表实现示例,使用 Java 编写。此示例展示了跳表的基本结构和核心操作,包括插入、查找和删除。
(1)节点类 (SkipListNode)
定义一个节点类,该类包含键值对、分数(score)以及指向其他节点的指针数组。
public class SkipListNode<K extends Comparable<K>, V> {private K key;private V value;private double score;private SkipListNode<K, V>[] next;public SkipListNode(K key, V value, double score, int level) {this.key = key;this.value = value;this.score = score;this.next = new SkipListNode[level];}public K getKey() {return key;}public void setKey(K key) {this.key = key;}public V getValue() {return value;}public void setValue(V value) {this.value = value;}public double getScore() {return score;}public void setScore(double score) {this.score = score;}public SkipListNode<K,V> getNext(int level) {if (level < next.length) {return next[level];}return null;}public void setNext(int level, SkipListNode<K,V> node) {if (level < next.length) {next[level] = node;}}@Overridepublic String toString() {return "SkipListNode{" +"key=" + key +",value=" + value +", score=" + score +'}';}}
接下来定义跳表类,它包含跳表的顶层节点、当前层数等属性,并实现了基本操作。
import java.util.Random;public class SkipList<K extends Comparable<K>, V> {private static final int MAX_LEVEL = 16;private static final double P = 0.25;private SkipListNode<K, V> head;private int levelCount;private Random random = new Random();public SkipList() {head = new SkipListNode<>(null, null, 0.0, MAX_LEVEL);levelCount = 1;}private int randomLevel() {int level = 1;while (random.nextDouble() < P && level < MAX_LEVEL) {level++;}return level;}public boolean insert(K key, V value, double score) {int newLevel = randomLevel();if (newLevel > levelCount) {for (int i = levelCount; i < newLevel; i++) {head.setNext(i, null);}levelCount = newLevel;}SkipListNode<K, V> newNode = new SkipListNode<>(key, value, score, newLevel);SkipListNode<K, V>[] update = new SkipListNode[levelCount];SkipListNode<K, V> x = head;for (int i = levelCount -1 ; i >= 0; i--){while (x.getNext(i) != null && (x.getNext(i).getScore() < score || (x.getNext(i).getScore() == score && x.getNext(i).getKey().compareTo(key) < 0))) {x = x.getNext(i);}update[i] = x;}x = x.getNext(0);if (x != null && x.getScore() == score && x.getKey().equals(key)) {// 如果已经存在相同的分数,则更新键x.setKey(key);} else {for (int i = 0; i < newLevel; i++) {newNode.setNext(i, update[i].getNext(i));update[i].setNext(i, newNode);}}return true;}public boolean delete(K key, double score) {SkipListNode<K, V>[] update = new SkipListNode[levelCount];SkipListNode<K, V> x = head;for (int i = levelCount - 1; i >= 0; i--) {while (x.getNext(i) != null && (x.getNext(i).getScore() < score || (x.getNext(i).getScore() == score && x.getNext(i).getKey().compareTo(key) < 0))) {x = x.getNext(i);}update[i] = x;}x = x.getNext(0);if (x != null && x.getScore() == score && x.getKey().equals(key)) {for (int i = 0; i < levelCount; i++) {if (update[i].getNext(i) != x) {break;}update[i].setNext(i, x.getNext(i));}// 清理多余的层级while (levelCount > 1 && head.getNext(levelCount - 1) == null) {levelCount--;}return true;}return false;}public V find(K key, double score) {SkipListNode<K, V> x = head;for (int i = levelCount - 1; i >= 0; i--) {while (x.getNext(i) != null && (x.getNext(i).getScore() < score || (x.getNext(i).getScore() == score && x.getNext(i).getKey().compareTo(key) < 0))) {x = x.getNext(i);}}x = x.getNext(0);if (x != null && x.getScore() == score && x.getKey().equals(key)) {return x.getValue();}else{return null;}}@Overridepublic String toString() {StringBuilder sb = new StringBuilder();SkipListNode<K, V> p = head;while (p != null) {sb.append(p).append(" -> ");p = p.getNext(0);}sb.append("null");return sb.toString();}}public class Main {public static void main(String[] args) {SkipList<String,Object> skipList = new SkipList<>();skipList.insert("Alice", "1",1.5);skipList.insert("Bob", "2",2.0);skipList.insert("Charlie", "3",3.5);skipList.insert("Diana", "4",2.5);System.out.println("SkipList: " + skipList);Object foundNode = skipList.find("Diana" ,2.5);if (foundNode != null) {System.out.println("Found Node: " + foundNode);} else {System.out.println("Node not found.");}boolean deleted = skipList.delete("Diana", 2.5);System.out.println("Deleted Diana: " + deleted);System.out.println("SkipList after deletion: " + skipList);}}}
6. 测试
SkipList: SkipListNode{key=null,value=null, score=0.0} -> SkipListNode{key=Alice,value=1, score=1.5} -> SkipListNode{key=Bob,value=2, score=2.0} -> SkipListNode{key=Diana,value=4, score=2.5} -> SkipListNode{key=Charlie,value=3, score=3.5} -> nullFound Node: 4Deleted Diana: trueSkipList after deletion: SkipListNode{key=null,value=null, score=0.0} -> SkipListNode{key=Alice,value=1, score=1.5} -> SkipListNode{key=Bob,value=2, score=2.0} -> SkipListNode{key=Charlie,value=3, score=3.5} -> null

五、跳跃表优化
1. 层级概率
通过调整节点层级的概率分布,可以控制跳跃表的平均高度,从而减少内存消耗。
2. 压缩存储
对于整数分数的 ZSet,Redis 会使用整数集合(IntSet)来代替跳跃表,以减少内存占用。
3. 缓存机制
Redis 可能会在跳跃表中使用一些缓存机制,以减少频繁查找的开销。
4. 性能优化
ZSet 同时使用哈希表(dict)存储成员到分值的映射,实现 O(1) 的单元素查询(如 ZSCORE)。节点的 level 数组采用柔性数组,节省内存。

实现简单:跳跃表的插入、删除和范围查询逻辑更易实现和维护。 范围查询高效:按层遍历比树的遍历更缓存友好。 并发友好:无复杂旋转操作,更易实现无锁并发(Redis 6.0 后的多线程模型未直接利用此点,但设计上留有空间)。

Redis 的跳跃表通过多层链表和随机层高,在保证高效操作(平均 O(log N))的同时,简化了实现复杂度。结合哈希表,ZSet 在范围查询和单点查询间取得了平衡,成为支持排行榜、延迟队列等场景的理想数据结构。了解 Redis 中跳跃表的具体实现细节,可以帮助开发者更好地利用 Redis 的有序集合功能,为自己的应用程序提供高效的数据管理和排序服务。




