Redis 设计与实现 10:五大数据类型之有序集合
有序集合 sorted set (下面我们叫zset 吧) 有两种编码方式:压缩列表 ziplist 和跳表 skiplist。
编码一:ziplist
zset 在 ziplist 中,成员(member)和分数(score)是挨在一起的,元素按照分数从小到大存储。
举个例子,我们用以下命令创建一个zset:
redis> ZADD key 26.1 z 1 a 2 b
(integer) 3
那么这个zset的结构大致如下:

下面我们来分析一下 zscore 命令的源码,进一步了解 zset 是如何利用 ziplist 存储的
int zsetScore(robj *zobj, sds member, double *score) {
// ...
if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
if (zzlFind(zobj->ptr, member, score) == NULL) return C_ERR;
}
// ...
return C_OK;
}
unsigned char *zzlFind(unsigned char *zl, sds ele, double *score) {
// eptr 是 member 的指针,sptr 是 score 的指针
unsigned char *eptr = ziplistIndex(zl,0), *sptr;
// 遍历 ziplist
while (eptr != NULL) {
// 因为 member 和 score 是挨着存储的,所以获取 member 的下一个节点就是 score 啦
sptr = ziplistNext(zl,eptr);
serverAssert(sptr != NULL);
// 对比当前的 member 和要查询的 member 是否相等
if (ziplistCompare(eptr,(unsigned char*)ele,sdslen(ele))) {
// 如果相等,则获取分数
if (score != NULL) *score = zzlGetScore(sptr);
return eptr;
}
// 不相等则继续往下遍历
eptr = ziplistNext(zl,sptr);
}
return NULL;
}
// 获取分数
double zzlGetScore(unsigned char *sptr) {
unsigned char *vstr;
unsigned int vlen;
long long vlong;
char buf[128];
double score;
serverAssert(sptr != NULL);
// ziplistGet 通过 sptr 指针获取值。根据节点的编码(前文有说到ziplist节点的编码) 对参数赋值
// 如果是字符串,则赋值到 vstr; 如果是整数,则赋值到 vlong。
serverAssert(ziplistGet(sptr,&vstr,&vlen,&vlong));
if (vstr) {
// 如果是字符串,那么存的就是浮点数
memcpy(buf,vstr,vlen);
buf[vlen] = '\0';
// 字符串转换成浮点数
score = strtod(buf,NULL);
} else {
// 整数类型就直接赋值
score = vlong;
}
return score;
}
编码二:skiplist
skiplist 的中文名叫 "跳表"或者"跳跃表"。
下面是跳表的结构图 (图片来自 《Redis 设计与实现》图片集 )

- 图中最左部分就是
zskiplist结构,其代码实现如下(server.h):
typedef struct zskiplist {
// 头指针和尾指针,指向头尾节点
struct zskiplistNode *header, *tail;
// 跳表的节点数(不包含头结点,空跳表也会包含头结点)
unsigned long length;
// 所有节点中,最大的层数
int level;
} zskiplist;
- 图中右边的四个节点,就是跳表节点
zskiplistNode,其代码实现如下(server.h):
typedef struct zskiplistNode {
// 成员
sds ele;
// 分数
double score;
// 后退指针,指向前一个节点
struct zskiplistNode *backward;
// 层,每个节点可能有很多层,每个层可能指向不同的节点
struct zskiplistLevel {
// 前进指针,指向下一个节点
struct zskiplistNode *forward;
// 跟下一个节点之间的跨度
unsigned long span;
} level[];
} zskiplistNode;
跳表最重要的一个地方就是层 level,为什么这么说呢?
假设zset 用链表有序存储,如果我们要查找数据,只能从头到尾遍历,时间复杂度是 \(O(n)\),效率很低。

有什么办法提高效率呢?我们可以在上面添加一层索引。

可以看出,我们遍历的性能变高了。例如我们想找到 6,先遍历第一层,5 到 7 之间,再往下探,就能找到 6 了!
有读者就发现了,如果数据量很大,那找起来也很慢。
是的,那么怎么解决呢?再往上加索引呗!

这不,链表就变成了跳表了!而上面说的层,就是这些索引啦!最终跳表的查找时间复杂度是 \(O(logn)\)
我们来看看 zrange 命令的核心实现,来感受一下跳表的遍历吧
zskiplistNode* zslGetElementByRank(zskiplist *zsl, unsigned long rank) {
zskiplistNode *x;
unsigned long traversed = 0;
int i;
// 层头结点开始
x = zsl->header;
// 层从高到低
for (i = zsl->level-1; i >= 0; i--) {
// 只要遍历的数没有达到 rank,就一直遍历
while (x->level[i].forward && (traversed + x->level[i].span) <= rank)
{
// 每次加上层的跨度
traversed += x->level[i].span;
// 往前走
x = x->level[i].forward;
}
// 如果这一层走完还没到 rank,那就往下层走,如果还是找不到就继续走,直到走到最底层
if (traversed == rank) {
return x;
}
}
return NULL;
}
Redis 设计与实现 10:五大数据类型之有序集合的更多相关文章
- Redis 源码简洁剖析 04 - Sorted Set 有序集合
Sorted Set 是什么 Sorted Set 命令及实现方法 Sorted Set 数据结构 跳表(skiplist) 跳表节点的结构定义 跳表的定义 跳表节点查询 层数设置 跳表插入节点 zs ...
- redis学习(七)——五大数据类型总结:字符串、散列、列表、集合和有序集合
目录 字符串类型(String) 散列类型(Hash) 列表类型(List) 集合类型(Set) 有序集合类型(SortedSet) 其它命令 一.字符串类型(String) 1.介绍: 字符串类型是 ...
- 探索Redis设计与实现10:Redis的事件驱动模型与命令执行过程
本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...
- Redis学习笔记(七)——数据结构之有序集合(sorted set)
一.介绍 Redis有序集合和集合一样都是string类型元素的机会,且不允许重复的成员. 不同的是每个元素都会关联一个double类型的分数.Redis正是通过分数来为集合中的成员进行从小到放大的排 ...
- Redis入门到高可用(九)——有序集合zset
一.数据结构 集合与有序集合,列表与有序集合的对比 二.主要API zadd 将一个或多个 member 元素及其 score 值加入到有序集 key 当中. zrem 移除有序集 key 中的一个或 ...
- Redis 设计与实现 6:五大数据类型之字符串
前文 Redis 设计与实现 2:Redis 对象 说到,五大数据类型都会封装成 RedisObject. typedef struct redisObject { unsigned type:4; ...
- Redis 详解 (五) redis的五大数据类型实现原理
目录 1.对象的类型与编码 ①.type属性 ②.encoding 属性和 *prt 指针 2.字符串对象 3.列表对象 4.哈希对象 5.集合对象 6.有序集合对象 7.五大数据类型的应用场景 8. ...
- Redis详解(五)------ redis的五大数据类型实现原理
前面两篇博客,第一篇介绍了五大数据类型的基本用法,第二篇介绍了Redis底层的六种数据结构.在Redis中,并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这些对 ...
- redis的五大数据类型实现原理
1.对象的类型与编码 Redis使用前面说的五大数据类型来表示键和值,每次在Redis数据库中创建一个键值对时,至少会创建两个对象,一个是键对象,一个是值对象,而Redis中的每个对象都是由 redi ...
随机推荐
- Spark3.0中Dates和Timestamps
Spark3.0使用的是预公历,而之前都是儒略历和公历的混合(即1582年之前的日期使用儒略历,1582年之后使用公历,java.sql.Date这个API用的就是这种,而Java8里使用java.t ...
- Oh my God, Swagger API文档竟然可以这样写?
最好的总会在不经意间出现. 作为后端程序员,免不了与前端同事对接API, 一个书写良好的API设计文档可有效提高与前端对接的效率. 为避免联调时来回撕逼,今天我们聊一聊正确使用Swaager的姿势. ...
- 图的遍历DFS
图的遍历DFS 与树的深度优先遍历之间的联系 树的深度优先遍历分为:先根,后根 //树的先根遍历 void PreOrder(TreeNode *R){ if(R!=NULL){ visit(R); ...
- 主从复制直接转换MGR_5.7验证试验
环境信息 IP port role info 192.168.188.51 4000 node1 master 192.168.188.52 4000 node2 slave1 192.168.188 ...
- <UnityTheGreat><001>获取指定目录下指定类型的所有文件的名称
#region Environment Windows 10 Unity 2019.4.16f1c1 LTS VSCode 1.52 https://github.com/MirzkisD1Ex0/U ...
- vue 分支结构
分支循环结构 分支循环结构指令 v-if v-else v-else-if v-show v-if 指令 可以直接在元素中添加指令,添加判断的值 最后运行可以得到结果是: v-show v-show ...
- 谷歌学术: but your computer or network may be sending automated queries. To protect our users, we can't process your request right now. See Google Help for more information.
原因是屏蔽了日本和新加坡的服务器,切换服务器为其他地方即可
- 近期一些使用MATLAB常用的代码
背景 最近处理行为学数据和fMRI数据,主要使用MATLAB.有一些常用的代码被自己重复使用,所以在此记录,以便于随时调取使用. 1.PVT原生mat文件→txt文件 遇到在处理一些被试的PVT数据时 ...
- <未解决的问题>crontab 定时弹框任务
问题:crontab写别的定时脚本就可以执行(比如说每隔一分钟就创建一个txt文件),但是写shell就不知道为什么,反弹不了 但是开启Linux终端窗口单独执行bash shell时候,(不通过re ...
- centos8中的MySQL卸载和安装
centos8中的MySQL卸载和安装 前言 前几天在自己的服务器上安装了一个NDB集群[而且还没有成功] 放弃治疗后用一台没有mysql的服务器实现了单机版本的集群. 本来以为这事到这就结束了,结果 ...