有序集合 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. 【题解】「SP867」 CUBES - Perfect Cubes

    这道题明显是一道暴力. 暴力枚举每一个 \(a, b, c, d\) 所以我就写了一个暴力.每个 \(a, b, c, d\) 都从 \(1\) 枚举到 \(100\) #include<ios ...

  2. 题解-洛谷P5217 贫穷

    洛谷P5217 贫穷 给定长度为 \(n\) 的初始文本 \(s\),有 \(m\) 个如下操作: \(\texttt{I x c}\),在第 \(x\) 个字母后面插入一个 \(c\). \(\te ...

  3. 【Scrapy笔记】使用方法

    安装: 1.pip install wheel 安装wheel 2.安装Twisted a.访问 http://www.lfd.uci.edu/~gohlke/pythonlibs/#twisted ...

  4. Web服务器-并发服务器-Epoll(3.4.5)

    @ 目录 1.介绍 2.代码 关于作者 1.介绍 epoll是一种解决方案,nginx就是用的这个 中心思想:不要再使用多进程,多线程了,使用单进程,单线程去实现并发 在上面博客实现的代码中使用过的轮 ...

  5. Sharding-JDBC使用jasypt3.0及以上版本加密数据库连接密码

    本文中介绍的是基于Sharding-JDBC 4.0和jasypt 3.0及其以上版本对数据库连接密码进行加密操作 引入依赖 项目的pom.xml中引入maven依赖 <dependency&g ...

  6. 庐山真面目之九微服务架构 NetCore 基于 Docker 基础镜像和挂载文件部署

    庐山真面目之九微服务架构 NetCore 基于 Docker 基础镜像和挂载文件部署 一.简介      我们在上一篇文章<庐山真面目之八微服务架构 NetCore 基于 Dockerfile ...

  7. NET 5 原子操作、接口限流Semaphore以及性能测试JMeter

    一.Semaphore 限流也可使用令牌桶算法+redis 1.Semaphore定义 类似互斥锁,但它可以允许多个线程同时访问一个共享资源 通过使用一个计数器来控制对共享资源的访问,如果计数器大于0 ...

  8. 安装篇五:安装MySQL(5.6.38版本)

    #1.MySQL安装 #1.准备环境 # No1:关闭:(iptables)selinux # No2:下载好安装包(这里使用 mysql-5.6.38.tar.gz 包安装) # No3:安装依赖包 ...

  9. Web自动化测试:xpath & CSS Selector定位

    Xpath 和 CSS Selector简介 CSS Selector CSS Selector和Xpath都可以用来表示XML文档中的位置.CSS (Cascading Style Sheets)是 ...

  10. java中token的生成和验证

    package com.zjn.token; /** * token编码工具类 * @author ouyangjun */ public class TokenEncryptUtils { // 编 ...