有序集合 sorted set (下面我们叫zset 吧) 有两种编码方式:压缩列表 ziplist 和跳表 skiplist

编码一:ziplist

zsetziplist 中,成员(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 设计与实现》图片集 )

  1. 图中最左部分就是 zskiplist 结构,其代码实现如下(server.h):
typedef struct zskiplist {
// 头指针和尾指针,指向头尾节点
struct zskiplistNode *header, *tail;
// 跳表的节点数(不包含头结点,空跳表也会包含头结点)
unsigned long length;
// 所有节点中,最大的层数
int level;
} zskiplist;
  1. 图中右边的四个节点,就是跳表节点 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:五大数据类型之有序集合的更多相关文章

  1. Redis 源码简洁剖析 04 - Sorted Set 有序集合

    Sorted Set 是什么 Sorted Set 命令及实现方法 Sorted Set 数据结构 跳表(skiplist) 跳表节点的结构定义 跳表的定义 跳表节点查询 层数设置 跳表插入节点 zs ...

  2. redis学习(七)——五大数据类型总结:字符串、散列、列表、集合和有序集合

    目录 字符串类型(String) 散列类型(Hash) 列表类型(List) 集合类型(Set) 有序集合类型(SortedSet) 其它命令 一.字符串类型(String) 1.介绍: 字符串类型是 ...

  3. 探索Redis设计与实现10:Redis的事件驱动模型与命令执行过程

    本文转自互联网 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial ...

  4. Redis学习笔记(七)——数据结构之有序集合(sorted set)

    一.介绍 Redis有序集合和集合一样都是string类型元素的机会,且不允许重复的成员. 不同的是每个元素都会关联一个double类型的分数.Redis正是通过分数来为集合中的成员进行从小到放大的排 ...

  5. Redis入门到高可用(九)——有序集合zset

    一.数据结构 集合与有序集合,列表与有序集合的对比 二.主要API zadd 将一个或多个 member 元素及其 score 值加入到有序集 key 当中. zrem 移除有序集 key 中的一个或 ...

  6. Redis 设计与实现 6:五大数据类型之字符串

    前文 Redis 设计与实现 2:Redis 对象 说到,五大数据类型都会封装成 RedisObject. typedef struct redisObject { unsigned type:4; ...

  7. Redis 详解 (五) redis的五大数据类型实现原理

    目录 1.对象的类型与编码 ①.type属性 ②.encoding 属性和 *prt 指针 2.字符串对象 3.列表对象 4.哈希对象 5.集合对象 6.有序集合对象 7.五大数据类型的应用场景 8. ...

  8. Redis详解(五)------ redis的五大数据类型实现原理

    前面两篇博客,第一篇介绍了五大数据类型的基本用法,第二篇介绍了Redis底层的六种数据结构.在Redis中,并没有直接使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这些对 ...

  9. redis的五大数据类型实现原理

    1.对象的类型与编码 Redis使用前面说的五大数据类型来表示键和值,每次在Redis数据库中创建一个键值对时,至少会创建两个对象,一个是键对象,一个是值对象,而Redis中的每个对象都是由 redi ...

随机推荐

  1. Vulnhub实战靶场:ME AND MY GIRLFRIEND: 1

    一.环境搭建 1.官网下载连接:https://www.vulnhub.com/entry/me-and-my-girlfriend-1,409/ 2.下载之后,使用Oracle VM Virtual ...

  2. sql注入之文件写入into outfile

    sql注入中写入webshell的几种方式 sql注入中写入webshell的几种方式 secure_file_priv="c:/-"被注释掉或者是web路径 php.ini中的g ...

  3. Day3 条件判断和循环

    条件判断 if...else if...elif...else 格式: 注意缩进! if x:     active elif y:     active else :     active    注 ...

  4. 三、TestNG的基本注解(1)

    Before类别和After类别注解 举例说明 创建两个TestNGAnnotationTest.java和TestNGAnnotationTest2.java的类 TestNGAnnotationT ...

  5. Git 工作原理以及常用命令操作

    GIT工作原理 要了解GIT工作原理,先了解GIT的这几块区域: 工作区域划分 工作区:指的是本地工作空间,如果刚拉取下来的代码,没有修改的内容,这块区域是空白的 (modified-已修改状态) 暂 ...

  6. 本科入行可能吗?做到这3点,斩获BAT offer不是梦

    大家好,前两天有一个小伙伴加我微信咨询.他说他不想读研,想要直接本科毕业就参与工作.但是又担心自己由于没有学历优势,无法在校招当中获得机会,于是便来向我请教,能不能指点迷津提供一些具体的实操性措施.与 ...

  7. TP学习第二天—

    一.控制器和对应方法的创建 2.路由解析 传统的路由解析方法: 具体url地址模式设置(配置文件在 ThinkPHP/Conf/convertion.php) 停到了之前的 黑马传智的 TP课,换了个 ...

  8. ElasticSearch 史上最全文章

    老规矩,本篇文章 不做 ElasticSearch 的 编码讲解 ,只介绍 文章学习的一些优秀文章 重点在于不要循规蹈矩,教程 这样走,你不一定要按他这样走,按自己的方式来,学习效率会更高,网上的教程 ...

  9. Django项目连接多个数据库配置

    1.设置数据库连接 pip install PyMySQL 2.在项目同名目录myproject/myproject下的__init__.py添加以下代码 import pymysql pymysql ...

  10. EF Core扩展工具记录 批量操作 记录修改删除历史 动态linq

      Microsoft.EntityFrameworkCore.UnitOfWork  Microsoft.EntityFrameworkCore的插件,用于支持存储库,工作单元模式以及支持分布式事务 ...