Redis数据结构底层知识总结
Redis数据结构底层总结
本篇文章是基于作者黄建宏写的书Redis设计与实现而做的笔记
数据结构与对象
Redis中数据结构的底层实现包括以下对象:
对象 | 解释 |
---|---|
简单动态字符串 | 字符串的底层实现 |
链表 | 列表的底层实现 |
字典 | 运用在多个方面,包括Hash的实现等 |
跳跃表 | 有序集合的底层实现 |
整数集合 | 集合的底层实现之一 |
压缩字典 | 列表键和哈希键的底层实现之一 |
String
Redis中并没有直接使用C语言中的字符串,而是在其基础之上实现了字符串的数据结构,叫做简单动态字符串(SDS)。
其内部的定义为:
/* Redis简单动态字符串的数据结构 */
struct sdshdr {
//字符长度,记录buf数组中已使用的字节数量
unsigned int len;
//当前可用空间,记录buf数组中未使用的字节数量
unsigned int free;
//具体存放字符的buf
char buf[];
};
SDS和C字符串的区别
常数复杂度获取字符串长度
因为SDS纪录了自身字符串中已经使用的长度和未使用的长度,所以可以在O(1)的时间复杂度内获取到字符串长度,然而C字符串不得不通过遍历整个字符串才能获取到长度,其花费的则是O(N)。
杜绝缓冲区溢出
和C字符串不同的是,SDS会利用纪录下来的长度去检查自身是否还有足够的空间去容纳新的需求,如果不满足的话,会先进行扩容,然后才执行新的操作。
减少修改字符串时带来的内存重分配次数
C字符串中每次进行增加和缩短的操作时,都会涉及到内存的重新分配,SDS利用未使用空间来实现空间预分配和惰性空间释放这两种优化策略。
空间预分配
用于优化SDS的字符串增长操作:当要涉及到对SDS进行空间扩展的时候,程序不仅仅会为SDS分配修改所需要的空间,还会为SDS分配额外的未使用空间,好处在于在下次扩容的时候,如果未使用空间还足够使用的话,就使用未使用空间进行扩容。
惰性空间
用于优化SDS的字符串缩短操作:当要缩短SDS保存的字符串时,程序并不会立即使用内存重分配来回收缩短后多出来的字节,而是将其回收起来纪录到free空间中,以便将来继续使用。
二进制安全
C字符串中判断结束的条件是遇见空字符,不同的是,SDS则选择了通过自身的len属性的值来判断字符串是否结束,这样做的目的在于使得SDS不仅仅能够存储字符串,还能存储二进制。
兼容部分C字符串函数
通过遵循C字符串以空字符结尾的惯例,SDS可以在有需要的时候重用C语言中的string函数库,比如对比函数,追加函数等等,从而实现代码的重用。
链表
链表提供了高效的结点重排能力,以及顺序性的结点访问方式,并且可以通过增删结点来灵活的调整链表的长度。
链表结点
每个链表结点使用的是一个listNode结构表示:
typedef struct listNode{
// 前置结点
struct listNode *prev;
// 后置结点
struct listNode * next;
// 结点值
void * value;
}
链表
在此基础之上,Redis通过封装了listNode实现双端链表,如下:
typedef struct list{
//表头节点
listNode * head;
//表尾节点
listNode * tail;
//链表长度
unsigned long len;
//节点值复制函数
void *(*dup) (void *ptr);
//节点值释放函数
void (*free) (void *ptr);
//节点值对比函数
int (*match)(void *ptr, void *key);
}
list结构为链表提供了表头指针head、表尾指针tail,以及链表长度计数器len,而dup、free和match成员则是用于实现多态链表所需的类型特定函数:
- dup函数用于
复制
链表结点所保存的值; - free函数用于
释放
链表结点所保存的值; - match函数用于
对比
链表结点所保存的值和另一个输入值是否相等;
Redis的链表实现的特性总结如下:
- 双端:链表节点带有prev 和next 指针,获取某个节点的前置节点和后置节点的时间复杂度都是O(1)。
- 无环:表头节点的 prev 指针和表尾节点的next 都指向NULL,对链表的访问时以NULL来做判断是否截止。
- 带表头指针和表尾指针:因为链表带有head指针和tail 指针,程序获取链表头结点和尾节点的时间复杂度为O(1)。
- 带链表长度计数器:链表中存有记录链表长度的属性 len。
- 多态:链表节点使用 void* 指针来保存节点值,并且可以通过list 结构的dup 、 free、 match三个属性为节点值设置类型特定函数,所以链表可以用来保存各种不同类型的值。
字典
字典由哈希表组成,而哈希表又由哈希结点组成。
哈希表结点
和链表一样,Redis也自己实现了哈希表结点结构和哈希表结构,如下:
哈希表结点:
typeof struct dictEntry{
//键
void *key;
//值
union{
void *val;
uint64_tu64;
int64_ts64;
}
struct dictEntry *next;
}
每个dictEntry结构都保存着一个键值对,分别对应属性key和value,同时,next属性是指向另外一个哈希表结点的指针,作用就是将多个哈希值相同的哈希结点连接起来,以此来解决键冲突的问题。
哈希表
Redis在dictEntry的基础之上封装实现了哈希表,如下:
typedef struct dictht {
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
unsigned long sizemask;
//该哈希表已有节点的数量
unsigned long used;
}
其中需要提到的是sizemark,这个属性和哈希值一起决定一个键应该被放到table数组中的哪个索引上面。
字典
Redis在哈希表的基础上封装了dictht实现字典,如下:
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privedata;
// 哈希表
dictht ht[2];
// rehash 索引
int rehashidx;
}
type属性和privdata属性是针对不同类型的键值对,为创建多态字典而设置的。ht属性是一个包含两个项的数组,数组中的每个项都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用,而rehashidx则决定了rehash的进度,如果没有进行rehash,其值则为-1。
哈希算法
当要把一个新的键值对添加到字典里面时,程序先要根据键值对中的键值计算出哈希值,再计算出索引值,然后将包含新键值对的哈希表结点放到哈希表数组的指定索引上面。
Redis使用murmurhash算法来计算哈希值
解决键冲突
键冲突:存在两个或者两个以上的键被分配到了哈希表数组的同一个索引上面。
解决办法:Redis的哈希表使用开链法来解决冲突,每个哈希表结点都存在一个next指针,多个哈希表值可以用next指针来构成一个单向链表,被分配到同一个索引上的多个结点可以用这个单向链表连接起来,从而解决键冲突问题。
需要注意的是,因为考虑到下次方便再次读取,因此总是将冲突的新结点插入到链表的表头位置,也就是已有其他结点的前面。
rehash
当哈希表的负载因子(已有数量/表数量)达到一个阀值以后,再次保存新的键值对时,冲突的几率将逐渐增加,因此需要进行响应的扩展(收缩)。
以扩展为例,程序需要经过以下步骤(腾笼换鸟):
- 扩展空间:为字典的ht[1]哈希表分配空间,其空间将会被扩展到第一个大于等于ht[0].used*2^n的整数值;
- 数据迁移:将保存在ht[0]上的所有键值对迁移到ht[1]中;
- 交换:迁移完后,释放掉ht[0],将现在的ht[1]设置为ht[0],并且为ht[1]新创建一个空白哈希表,实现了相互交换的过程;
渐进式rehash
为了避免一次性交换所造成的性能影响,Redis采用的是渐进式rehash,也就是说,将会分多次、渐进式的完成数据的迁移。所以会同时存在两个哈希表数组,并不会急着一次性的将数ht[0]的数据迁移到ht[1]中,而是在每次操作的同时,将部分的ht[0]中的数据保存到ht[1]中,采用愚公移山的方式最终将ht[0]中的数据搬完,为了避免ht[0]中的数据不断增加,相关的增加的操作都会作用在ht[1]之上,最后,搬完后的操作和之前的操作是一致的。
它的优点在于:采用了分而治之的方式,将rehash键值对所需的操作均摊到字典的每个添加、删除、查找和更新操作之上,从而避免集中式rehash而带来的庞大计算量。
我认为它的缺点也是存在的,譬如在查询的时候,可能在ht[0]中查找不到,还得跑到ht[1]中查找,无形中增加了开销。
跳跃表
跳跃表是一种有序数据结构,它通过在每个结点中维持多个指向其它结点的指针,从而达到快速访问结点的目的。其查找的时间复杂度平均可以达到O(logn),最坏O(N),还可以通过顺序性操作来批量处理结点。
目前,Redis中只有两个地方用到了跳跃表,一个是有序集合键,另外一个是集群结点中用作内部数据结构。
跳跃表由多个跳跃结点组成:
跳跃表结点
typedef struct zskiplistNode{
//层
struct zskiplistLevel{
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
} level[];
//后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj *obj;
}
- 层:level 数组可以包含多个元素,每个元素都包含一个指向其他节点的指针。
- 前进指针:用于指向表尾方向的前进指针
- 跨度:用于记录两个节点之间的距离
- 后退指针:用于从表尾向表头方向访问节点
- 分值和成员:跳跃表中的所有节点都按分值从小到大排序。成员对象指向一个字符串,这个字符串对象保存着一个SDS值
跳跃表
typedef struct zskiplist {
//表头节点和表尾节点
structz skiplistNode *header,*tail;
//表中节点数量
unsigned long length;
//表中层数最大的节点的层数
int level;
}zskiplist;
其搜索的步骤为,先通过头结点定位到跳跃表结点,然后通过层去定位到下一个跳跃表结点的位置,直到找到给定分值的结点。
整数集合
前提:当一个集合只包含整数值元素,并且这个集合的元素数量不多时。
typedef struct intset{
//编码方式
uint32_t enconding;
// 集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
}
因为可能存在存入的整数不符合已存在集合中的编码格式,因此需要使用升级策略来解决。
- 扩展空间:根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间
- 转换编码:将底层数组现有的所有元素都转换成新的编码格式,重新分配空间
- 添加:将新元素加入到底层数组中
一旦对数组进行了升级,编码就会一直保存升级后的状态。
压缩列表
压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构。一个压缩列表可以包含任意多个结点,每个结点可以保存一个字节数或者一个整数值。
Redis数据结构底层知识总结的更多相关文章
- Redis专题(2):Redis数据结构底层探秘
前言 上篇文章Redis闲谈(1):构建知识图谱介绍了redis的基本概念.优缺点以及它的内存淘汰机制,相信大家对redis有了初步的认识.互联网的很多应用场景都有着Redis的身影,它能做的事情远远 ...
- 面试官:你看过Redis数据结构底层实现吗?
面试中,redis也是很受面试官亲睐的一部分.我向在这里讲的是redis的底层数据结构,而不是你理解的五大数据结构.你有没有想过redis底层是怎样的数据结构呢,他们和我们java中的HashMap. ...
- 面试官:你了解过Redis对象底层实现吗
上一章我们讲了Redis的底层数据结构,不了解的人可能会有疑问:这个和平时用的五大对象有啥关系呢?这一章我们就主要解释他们所建立的联系. 看这个文件之前,如果对ziplist.skiplist.int ...
- Redis(二)--- Redis的底层数据结构
1.Redis的数据结构 Redis 的底层数据结构包含简单的动态字符串(SDS).链表.字典.压缩列表.整数集合等等:五大数据类型(数据对象)都是由一种或几种数结构构成. 在命令行中可以使用 OBJ ...
- 聊聊Mysql索引和redis跳表 ---redis的有序集合zset数据结构底层采用了跳表原理 时间复杂度O(logn)(阿里)
redis使用跳表不用B+数的原因是:redis是内存数据库,而B+树纯粹是为了mysql这种IO数据库准备的.B+树的每个节点的数量都是一个mysql分区页的大小(阿里面试) 还有个几个姊妹篇:介绍 ...
- Redis 数据结构之字符串的那些骚操作
Redis 字符串底层用的是 sds 结构,该结构同 c 语言的字符串相比,其优点是可以节省内存分配的次数,还可以... 这样写是不是读起来很无聊?这些都是别人咀嚼过后,经过一轮两轮三轮的再次咀嚼,吐 ...
- 选择合适Redis数据结构,减少80%的内存占用
redis作为目前最流行的nosql缓存数据库,凭借其优异的性能.丰富的数据结构已成为大部分场景下首选的缓存工具. 由于redis是一个纯内存的数据库,在存放大量数据时,内存的占用将会非常可观.那么在 ...
- Redis 数据结构使用场景
转自http://get.ftqq.com/523.get 一.redis 数据结构使用场景 原来看过 redisbook 这本书,对 redis 的基本功能都已经熟悉了,从上周开始看 redis 的 ...
- Redis 数据结构与内存管理策略(上)
Redis 数据结构与内存管理策略(上) 标签: Redis Redis数据结构 Redis内存管理策略 Redis数据类型 Redis类型映射 Redis 数据类型特点与使用场景 String.Li ...
随机推荐
- 原生JS的HTTP请求
ar xhr = new XMLHttpRequest(); xhr.onreadystatechange = function(){ if( xhr.readyState == 4){ if( xh ...
- Unity3D中使用BMFont制作图片字体 (NGUI版)
[旧博客转移 - 发布于2015年9月10日 16:07] 有时美术会出这种图片格式的文字,NGUI提供了UIFont来支持BMFont导出的图片字体 BMFont原理其实很简单,首先会把文字小图拼成 ...
- jsp获取当前日期,包括星期几
<%@ page language="java" pageEncoding="GB2312" %> <html> <head> ...
- SPFA 算法详解
适用范围:给定的图存在负权边,这时类似Dijkstra等算法便没有了用武之地,而Bellman-Ford算法的复杂度又过高,SPFA算法便 派上用场了. 我们约定有向加权图G不存在负权回路,即最短路径 ...
- (cljs/run-at (JSVM. :all) "Metadata就这样哦")
前言 动态类型语言,少了静态类型语言必须声明变量类型的累赘,但也缺失了编译时类型检查和编译时优化的好处.cljs虽然作为动态类型语言,但其提供Metadata让我们在必要的时候可选择地补充类型提示, ...
- java封装性、继承性及关键字
方法的参数传递(重点.难点)1.形参:方法声明时,方法小括号内的参数 实参:调用方法时,实际传入的参数的值 2.规则:java中的参数传递机制:值传递机制 1)形参是基本数据类型的:将实参的值传递 ...
- [信息安全] 3.HTTPS工作流程
[信息安全]系列博客:http://www.cnblogs.com/linianhui/category/985957.html 0. 简单回顾 在前面两篇博客中介绍了密码相关的一些基本工具,包括(对 ...
- Unity strip engine code可能会使程序崩溃
最近正在做新大厅的红包推荐口令快速领金币入口拍卖行之类的功能,同事把我的捕鱼整合到他的项目中时出现了闪退的问题,经排查是因为strip engine code选项. Strip engine code ...
- tensorflow tanh应用
1.tanh()函数 tanh是双曲函数中的一个,tanh()为双曲正切. 双曲正切函数的导数公式: 2.tensorflow tanh()例子 import tensorflow as tf i ...
- Laravel 中使用子域名(一个框架多项目)
1.本地虚拟域名为:www.test.com,子域名为admin.test.com 2.apache环境中,配置apache的httpd-vhost.conf文件 <VirtualHost *: ...