ZSet 为什么是有序的?
在 Redis 数据库中,ZSet(有序集合)是一种非常重要的数据结构,它不仅与普通的集合(Set)类似,但与普通集合不同的是,ZSet 中的元素是按照权重(通常是分数)来排序的。每个元素都会关联一个分数,Redis 会根据这些分数对 ZSet 中的元素进行排序。
那么,为什么 ZSet 会是有序的?
ZSet 的内部实现
Redis的ZSet底层实现方式在不同版本中有所变化。Redis 7.0之前,ZSet使用压缩列表(Ziplist)存储小量数据,而数据量较大时使用跳跃表(Skiplist)。在Redis 7.0及其之后的版本中,Ziplist被紧凑列表(Listpack)所取代,而跳跃表仍然是ZSet的主要存储方式。
通过这两种结构的结合,Redis 的 ZSet 可以实现以下特性:
- 有序性:元素按照分数从小到大排序。
- 高效性:查找、插入、删除等操作都能够在 O(logN) 或 O(1) 时间复杂度内完成。
1. 跳跃表(Skiplist)
跳跃表是一种多级索引的数据结构,它通过多层索引链表来加速对元素的查找。Redis 在 ZSet 中使用跳跃表来存储元素,保证了插入和查找操作的高效性,时间复杂度为 O(logN) 。
- 层级结构:跳跃表由多级链表组成,每一层索引都包含部分元素。查找操作可以从上层的索引跳跃到下层,从而加速查询过程。
- 插入与删除:跳跃表支持 O(logN) 的插入和删除操作,插入时会根据元素的分数决定插入位置,并在多级索引中更新相应的指针。
2. 压缩列表(Ziplist)与紧凑列表(Listpack)
在 Redis 7.0 之前,ZSet 会使用 压缩列表(Ziplist) 存储元素。压缩列表是一种非常节省内存的存储结构,适用于小规模的数据存储。它将元素和分数压缩在一起,以节省内存空间。当 ZSet 中的元素较少时,压缩列表能够提供更好的内存利用率。
但是,当 ZSet 中的元素数量增大时,压缩列表会变得效率低下,因为它的插入和删除操作的时间复杂度是 O(N),并且无法有效支持快速查找。因此,当数据量较大时,Redis 会切换到跳跃表来处理 ZSet。
在 Redis 7.0 及之后的版本,压缩列表被 紧凑列表(Listpack) 所取代。紧凑列表是 Redis 7.0 新引入的一种数据结构,它与压缩列表类似,但性能更高,特别是在处理大量数据时。与压缩列表相比,紧凑列表在存储和访问效率上都有较大的提升。
紧凑列表具有以下特点:
- 内存效率:紧凑列表比压缩列表使用更少的内存,同时保持较高的性能。
- 性能优化:对于大规模数据集,紧凑列表能够提供更好的性能,支持 O(1) 的插入和删除操作。
ZSet 的存储方式演变
-
Redis 7.0 之前:
- 小数据量时:使用 压缩列表(Ziplist) 存储 ZSet 的元素。
- 大数据量时:使用 跳跃表(Skiplist) 存储 ZSet 的元素。
-
Redis 7.0 及之后:
- 小数据量时:使用 紧凑列表(Listpack) 存储 ZSet 的元素。
- 大数据量时:仍然使用 跳跃表(Skiplist) 存储 ZSet 的元素。
ZSet 的主要操作
在 Redis 中,ZSet 提供了多种操作,它们的高效性得益于跳跃表和紧凑列表的结合。
- 添加元素(add) :向
ZSet中添加一个元素时,需要指定元素的值和分数。Redis 会将元素插入到跳跃表或紧凑列表中,并确保按分数排序。 - 删除元素(remove) :当从
ZSet中删除元素时,Redis 会在跳跃表和紧凑列表中删除对应的节点。 - 获取排名(rank) :获取某个元素的排名。在 Redis 中,ZSet 中的元素是根据分数升序排序的。通过跳跃表或紧凑列表,可以高效地获取某个元素的排名。
- 范围查询(range) :根据分数范围返回一组元素。Redis 可以在 O(logN) 或 O(1) 的时间复杂度内完成这个操作,具体时间复杂度取决于使用的数据结构(跳跃表或紧凑列表)。
我们可以看出 ZSet 主要通过跳表来保持元素的有序性。跳表使得我们能够高效地进行元素的插入、查找和删除操作。同时,紧凑列表则提供了 O(1) 时间复杂度来快速定位元素的分数。在 Redis 中,ZSet 的有序性和高效性正是依赖于这两种数据结构的结合。
Java 实现 ZSet 的算法过程
下面我们通过 Java 代码来模拟实现一个简单的 ZSet 数据结构。这个实现将使用跳表和哈希表来完成 ZSet 的基本操作。
1. 定义跳表的节点(SkipListNode)
public class SkipListNode {
String value; // ZSet 元素
double score; // 分数
SkipListNode[] forward; // 指向不同层的指针
public SkipListNode(String value, double score, int level) {
this.value = value;
this.score = score;
this.forward = new SkipListNode[level + 1];
}
}
2. 定义跳表(SkipList)
public class SkipList {
private static final int MAX_LEVEL = 16;
private SkipListNode header; // 跳表的头节点
private int level; // 当前跳表的最大层数
public SkipList() {
this.header = new SkipListNode(null, Double.NEGATIVE_INFINITY, MAX_LEVEL);
this.level = 0;
}
// 创建随机层数
private int randomLevel() {
int lvl = 0;
while (Math.random() < 0.5 && lvl < MAX_LEVEL) {
lvl++;
}
return lvl;
}
// 向跳表插入元素
public void insert(String value, double score) {
SkipListNode[] update = new SkipListNode[MAX_LEVEL + 1];
SkipListNode current = header;
// 从高层到低层查找插入位置
for (int i = level; i >= 0; i--) {
while (current.forward[i] != null && current.forward[i].score < score) {
current = current.forward[i];
}
update[i] = current;
}
current = current.forward[0];
// 如果元素已经存在,更新分数
if (current != null && current.score == score) {
current.value = value;
} else {
int newLevel = randomLevel();
if (newLevel > level) {
for (int i = level + 1; i <= newLevel; i++) {
update[i] = header;
}
level = newLevel;
}
SkipListNode newNode = new SkipListNode(value, score, newLevel);
for (int i = 0; i <= newLevel; i++) {
newNode.forward[i] = update[i].forward[i];
update[i].forward[i] = newNode;
}
}
}
// 查找元素
public SkipListNode find(double score) {
SkipListNode current = header;
for (int i = level; i >= 0; i--) {
while (current.forward[i] != null && current.forward[i].score < score) {
current = current.forward[i];
}
}
current = current.forward[0];
return (current != null && current.score == score) ? current : null;
}
}
3. 定义 ZSet(使用跳表和哈希表)
import java.util.HashMap;
import java.util.Map;
public class ZSet {
private SkipList skipList; // 跳表
private Map<String, Double> scoreMap; // 哈希表存储元素与分数
public ZSet() {
this.skipList = new SkipList();
this.scoreMap = new HashMap<>();
}
// 添加元素
public void add(String value, double score) {
if (!scoreMap.containsKey(value)) {
scoreMap.put(value, score);
skipList.insert(value, score);
} else {
// 如果已经存在,更新分数
scoreMap.put(value, score);
skipList.insert(value, score);
}
}
// 获取元素的分数
public Double getScore(String value) {
return scoreMap.get(value);
}
// 查找某个元素
public String find(double score) {
SkipListNode node = skipList.find(score);
return (node != null) ? node.value : null;
}
}
总结
Redis 的 ZSet 在不同版本中采取了不同的存储方式,以提高内存利用率和性能。Redis 7.0 之前,ZSet 使用压缩列表存储小量数据,而使用跳跃表存储大数据。而在 Redis 7.0 之后,压缩列表被更高效的紧凑列表取代,同时跳跃表仍然是 ZSet 的主要存储方式。这些变革使得 Redis 在处理有序集合时更加高效和灵活。