Redis技术交流群481804090

Redis:https://github.com/zwjlpeng/Redis_Deep_Read

Redis中支持的数据结构比Memcached要多的多啦,如基本的字符串、哈希表、列表、集合、可排序集,在这些基本数据结构上也提供了针对该数据结构的各种操作,这也是Redis之所以流行起来的一个重要原因,当然Redis能够流行起来的原因,远远不只这一个,如支持高并发的读写、数据的持久化、高效的内存管理及淘汰机制...

从Redis的git提交历史中,可以查到,2009/10/24在1.050版本,Redis开始支持可排序集,在该版本中,只提供了一条命令zadd,宏定义如下所示:

     {"zadd",zaddCommand,,REDIS_CMD_BULK|REDIS_CMD_DENYOOM},

那么什么是可排序集呢? 从Redis 1.0开始就给我们提供了集合(Set)这种数据结构,集合就跟数学上的集合概念是一个道理【无序性,确定性,互异性】,集合里的元素无法保证元素的顺序,而业务上的需求,可能不止是一个集合,而且还要求能够快速地对集合元素进行排序,于是乎,Redis中提供了可排序集这么一种数据结构,似乎也是合情合理,无非就是在集合的基础上增加了排序功能,也许有人会问,Redis中不是有Sort命令嘛,下面的操作不也是同样可以达到对无序集的排序功能嘛,是的,是可以,但是在这里我们一直强调的是快速这两个字,而Sort命令的时间复杂度为O(N+M*Log(M)),可排序集获取一定范围内元素的时间复杂度为O(log(N) + M)

root@bjpengpeng-VirtualBox:/home/bjpengpeng/redis-3.0./src# ./redis-cli
127.0.0.1:> sort set
) ""
) ""
) ""
) ""
127.0.0.1:> sort set desc
) ""
) ""
) ""
) ""
127.0.0.1:>

在了解可排序集是如何实现之前,需要了解一种数据结构跳表(Skip List),跳表与AVL、红黑树...等相比,数据结构简单,算法易懂,但查询的时间复杂度与平衡二叉树/红黑树相当,跳表的基本结构如下图所示

上图中整个跳表结构存放了4个元素5->10->20->30,图中的红色线表示查找元素30时,走的查找路线,从Head指针数组里最顶层的指针所指的20开始比较,与普通的链表查找相比,跳表的查询可以跳跃元素,上图中查询30,发现30比20大,则查找就是20开始,而普通链表的查询必须一个元素一个元素的比较,时间复杂度为O(n)

有了上图所示的跳表基本结构,再看看如何向跳表中插入元素,向跳表中插入元素,由于元素所在层级的随机性,平均起来也是O(logn),说白了,就是查找元素应该插入在什么位置,然后就是普通的移动指针问题,再想想往有序单链表的插入操作吧,时间复杂度是不是也是O(n),下图所示是往跳表中插入元素28的过程,图中红色线表示查找插入位置的过程,绿色线表示进行指针的移动,将该元素插入

有了跳表的查找及插入那么就看看在跳表中如何删除元素吧,跳表中删除元素的个程,查找要删除的元素,找到后,进行指针的移动,过程如下图所示,删除元素30

有了上面的跳表基本结构图及原理,自已设计及实现跳表吧,这样当看到Redis里面的跳表结构时我们会更加熟悉,更容易理解些,【下面是对Redis中的跳表数据结构及相关代码进行精减后形成的可运行代码】,首先定义跳表的基本数据结构如下所示

#include<stdio.h>
#include<stdlib.h> #define ZSKIPLIST_MAXLEVEL 32
#define ZSKIPLIST_P 0.25
#include <math.h> //跳表节点
typedef struct zskiplistNode {
int key;
int value;
struct zskiplistLevel {
struct zskiplistNode *forward;
} level[1];
} zskiplistNode; //跳表
typedef struct zskiplist {
struct zskiplistNode *header;
int level;
} zskiplist;

在代码中我们定义了跳表结构中保存的数据为Key->Value这种形式的键值对,注意的是skiplistNode里面内含了一个结构体,代表的是层级,并且定义了跳表的最大层级为32级,下面的代码是创建空跳表,以及层级的获取方式

//创建跳表的节点
zskiplistNode *zslCreateNode(int level, int key, int value) {
zskiplistNode *zn = (zskiplistNode *)malloc(sizeof(*zn)+level*sizeof(zn->level));
zn->key = key;
zn->value = value;
return zn;
} //初始化跳表
zskiplist *zslCreate(void) {
int j;
zskiplist *zsl;
zsl = (zskiplist *) malloc(sizeof(*zsl));
zsl->level = 1;//将层级设置为1
zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,NULL,NULL);
for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {
zsl->header->level[j].forward = NULL;
}
return zsl;
} //向跳表中插入元素时,随机一个层级,表示插入在哪一层
int zslRandomLevel(void) {
int level = 1;
while ((rand()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
level += 1;
return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

在这段代码中,使用了随机函数获取过元素所在的层级,下面就是重点,向跳表中插入元素,插入元素之前先查找插入的位置,代码如下所示,代码中注意update[i]

//向跳表中插入元素
zskiplistNode *zslInsert(zskiplist *zsl, int key, int value) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
int i, level;
x = zsl->header;
//在跳表中寻找合适的位置并插入元素
for (i = zsl->level-1; i >= 0; i--) {
while (x->level[i].forward &&
(x->level[i].forward->key < key ||
(x->level[i].forward->key == key &&
x->level[i].forward->value < value))) {
x = x->level[i].forward;
}
update[i] = x;
}
//获取元素所在的随机层数
level = zslRandomLevel();
if (level > zsl->level) {
for (i = zsl->level; i < level; i++) {
update[i] = zsl->header;
}
zsl->level = level;
}
//为新创建的元素创建数据节点
x = zslCreateNode(level,key,value);
for (i = 0; i < level; i++) {
x->level[i].forward = update[i]->level[i].forward;
update[i]->level[i].forward = x;
}
return x;
}

下面是代码中删除节点的操作,和插入节点类似

//跳表中删除节点的操作
void zslDeleteNode(zskiplist *zsl, zskiplistNode *x, zskiplistNode **update) {
int i;
for (i = 0; i < zsl->level; i++) {
if (update[i]->level[i].forward == x) {
update[i]->level[i].forward = x->level[i].forward;
}
}
//如果层数变了,相应的将层数进行减1操作
while(zsl->level > 1 && zsl->header->level[zsl->level-1].forward == NULL)
zsl->level--;
} //从跳表中删除元素
int zslDelete(zskiplist *zsl, int key, int value) {
zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;
int i;
x = zsl->header;
//寻找待删除元素
for (i = zsl->level-1; i >= 0; i--) {
while (x->level[i].forward &&
(x->level[i].forward->key < key ||
(x->level[i].forward->key == key &&
x->level[i].forward->value < value))) {
x = x->level[i].forward;
}
update[i] = x;
}
x = x->level[0].forward;
if (x && key == x->key && x->value == value) {
zslDeleteNode(zsl, x, update);
//别忘了释放节点所占用的存储空间
free(x);
return 1;
} else {
//未找到相应的元素
return 0;
}
return 0;
}

最后,附上一个不优雅的测试样例

//将链表中的元素打印出来
void printZslList(zskiplist *zsl) {
zskiplistNode *x;
x = zsl->header;
for (int i = zsl->level-1; i >= 0; i--) {
zskiplistNode *p = x->level[i].forward;
while (p) {
printf(" %d|%d ",p->key,p->value);
p = p->level[i].forward;
}
printf("\n");
}
} int main() {
zskiplist *list = zslCreate();
zslInsert(list,1,2);
zslInsert(list,4,5);
zslInsert(list,2,2);
zslInsert(list,7,2);
zslInsert(list,7,3);
zslInsert(list,7,3);
printZslList(list);
//zslDelete(list,7,2);
printZslList(list);
}

有了上面的跳表理论基础,理解Redis中跳表的实现就不是那么难了,先分析到这,下回续写,【代码以Redis 2.9为例】~

Redis有序集内部实现原理分析的更多相关文章

  1. Redis有序集内部实现原理分析(二)

    Redis技术交流群481804090 Redis:https://github.com/zwjlpeng/Redis_Deep_Read 本篇博文紧随上篇Redis有序集内部实现原理分析,在这篇博文 ...

  2. Redis 发布/订阅机制原理分析

    Redis 通过 PUBLISH. SUBSCRIBE 和 PSUBSCRIBE 等命令实现发布和订阅功能.   这些命令被广泛用于构建即时通信应用,比如网络聊天室(chatroom)和实时广播.实时 ...

  3. Redis cluster的核心原理分析

    一.节点间的内部通信机制 1.基础通信原理 (1)redis cluster节点间采取gossip协议进行通信 跟集中式不同,不是将集群元数据(节点信息,故障,等等)集中存储在某个节点上,而是互相之间 ...

  4. 【Redis】跳跃表原理分析与基本代码实现(java)

    最近开始看Redis设计原理,碰到一个从未遇见的数据结构:跳跃表(skiplist).于是花时间学习了跳表的原理,并用java对其实现. 主要参考以下两本书: <Redis设计与实现>跳表 ...

  5. redis 发布与订阅原理分析

    前言:用了redis也有一段时间了,但是发布与订阅的使用频率也不高,趁着这次空闲,深究下redis的发布与订阅模式. 一.订阅频道和信息发布 功能说明:Redis 的 SUBSCRIBE 命令可以让客 ...

  6. Redlock(redis分布式锁)原理分析

    Redlock:全名叫做 Redis Distributed Lock;即使用redis实现的分布式锁: 使用场景:多个服务间保证同一时刻同一时间段内同一用户只能有一个请求(防止关键业务出现并发攻击) ...

  7. 【Redis】内部数据结构自顶向下梳理

    本博客将顺着自顶向下的思路梳理一下Redis的数据结构体系,从数据库到对象体系,再到底层数据结构.我将基于我的一个项目的代码来进行介绍:daredis.该项目中,使用Java实现了Redis中所有的数 ...

  8. 利用多写Redis实现分布式锁原理与实现分析(转)

    利用多写Redis实现分布式锁原理与实现分析   一.关于分布式锁 关于分布式锁,可能绝大部分人都会或多或少涉及到. 我举二个例子:场景一:从前端界面发起一笔支付请求,如果前端没有做防重处理,那么可能 ...

  9. redis原理分析

    基本全是参考http://blog.csdn.net/a600423444/article/details/8944601     redis的使用大家都很熟悉,可能除了watch 锁,pipelin ...

随机推荐

  1. JavaWeb基础—Servlet

    一.Servlet是什么 是服务器上运行的Java小应用程序,并被称为JavaWeb三大组件之一 通常我们把实现了Servlet的类,称之为Servlet Servlet作用主要是 1.接收请求数据 ...

  2. 20155212 2016-2017-2 《Java程序设计》第9周学习总结

    20155212 2016-2017-2 <Java程序设计>第9周学习总结 教材学习内容总结 Chapter16 数据库本身是个独立运行的应用程序. 应用程序如何呼叫这组链接库? 不同的 ...

  3. VBA 语言基础

    VBA 语言基础 第一节 标识符 一.定义 标识符是一种标识变量.常量.过程.函数.类等语言构成单位的符号,利用它可以完成对变量.常量.过程.函数.类等的引用. 二.命名规则 1) 字母打头,由字母. ...

  4. P4438 [HNOI/AHOI2018]道路

    辣稽题目 毁我青春 耗我钱财. 设\(f[x][i][j]\)为从1号点走到x点经过i条公路j条铁路,子树的最小代价. \(f[leaf][i][j]=(A+i)(B+j)C\) \(f[x][i][ ...

  5. Redis架构之防雪崩设计:网站不宕机背后的兵法

    互联网系统中不可避免要大量用到缓存,在缓存的使用过程中,架构师需要注意哪些问题?本文以 Redis 为例,详细探讨了最关键的 3 个问题. 一.缓存穿透预防及优化 缓存穿透是指查询一个根本不存在的数据 ...

  6. 聊聊Http协议

    http协议是大家在互联网中最为熟悉的协议,只要上网大家都会遇到,但是,很多人被问道什么是http协议,http协议的内容是什么就懵了.这里,我们随便聊聊http协议. 首先,我们说说协议.我一直觉得 ...

  7. Mkdir方法

    新建目录或文件夹. 语法 MkDir 路径 所需的_路径_参数是一个字符串表达式,标识的目录或文件夹创建. _路径_可以包含驱动器. 如果未指定驱动器, MkDir当前的驱动器上创建新目录或文件夹. ...

  8. hdu1272小希的迷宫(并查集判断回路和是否连通)

    传送门 迷宫中不能有回路,还要连通 如果最后集合数是一个那就是连通,否则不联通 要合并的两个顶点在相同集合内,表示出现了回路 输入时注意一下 #include<bits/stdc++.h> ...

  9. Mybatis批量更新报错com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException

    批量更新数据,非常简单的一段代码,硬是报错,插入的数据也能显示出来 List<User> userlist = new ArrayList<User>(); userlist. ...

  10. Python中的异常(Exception)处理

    异常 当你的程序出现例外情况时就会发生异常(Exception).例如,当你想要读取一个文件时,而那个文件却不存在,怎么办?又或者你在程序执行时不小心把它删除了,怎么办?这些通过使用异常来进行处理. ...