目录
前言
Redis为什么要使用2个对象?两个对象的好处
redisObject对象解析
String 类型
1、int 整数值实现
2、embstr
3、raw
List 类型
1、压缩链表:ziplist
2、双向链表:linkedlist
3、快速列表:quicklist
Hash 类型
Hashtable
哈希表的扩展和收缩
rehash
渐进式hash
Set 类型
intset 整数集合
Zset 类型
zkiplist 跳表
redis是通过对象来表示存储的数据的,redis 也是键值对存储的方式,那么每存储一条数据,redis至少会生成2个对象,一个是redisObject,用来描述具体数据的类型的,比如用的是那种数据类型,底层用了哪种数据结构,还有一个对象就是具体存储的数据。 这个存储对象数据就是通过redisObject这个对象的指针来指引的。
redisObject结构
typedef struct redisObject{
//类型
unsigned type:4;
//编码
unsigned encoding:4;
//对象最后一次被访问的时间
unsigned lru:REDIS_LRU_BITS
//引用计数
int refcount
//指向底层实现数据结构的指针
void *ptr;
…..
}
ptr指针指向的就是每种数据类型的具体的数据结构
type 表示的是使用的那种数据结构,常用的有5种,string,hash,list,set,zset。 type就是来存这个的
编码常量(encoding ) |
编码对应的底层数据结构 |
REDIS_ENCODING_INT |
long类型的整数 |
REDIS_ENCODING_EMBSTR |
embstr编码的简单动态字符串 |
REDIS_ENCODING_RAW |
简单动态字符串 |
REDIS_ENCODING_HT |
字典 |
REDIS_ENCODING_LINKEDLIST |
双向链表 |
REDIS_ENCODING_ZIPLIST |
压缩列表 |
REDIS_ENCODING_INTSET |
整数集合 |
REDIS_ENCODING_SKIPLIST |
跳跃表和字典 |
encoding 标注了这个对象具体的数据结构类型
每一种redisObject对象对应底层都会有至少2种数据结构
类型 |
编码 |
对象 |
String |
int |
整数值实现 |
String |
embstr |
sds实现 <=39 字节 |
String |
raw |
sds实现 > 39字节 |
List |
ziplist |
压缩列表实现 |
List |
linkedlist |
双端链表实现 |
Set |
intset |
整数集合使用 |
Set |
hashtable |
字典实现 |
Hash |
ziplist |
压缩列表实现 |
Hash |
hashtable |
字典使用 |
Sorted set |
ziplist |
压缩列表实现 |
Sorted set |
skiplist |
跳跃表和字典 |
介绍完了redisObject了,下面介绍每一种具体的数据类型及其对应的底层数据结构
在c语音中,定义的字符串是不可被修改的,因此redis设计了可变的字符串长度对象,接SDS(simple dynamic string),实现原理:
struct sdshdr{
//记录buf数组中已存的字节数量,也就是sds保存的字符串长度
int len;
// buf数组中剩余可用的字节数量
int free;
//字节数组,用来存储字符串的
char buf[];
}
参数解析:
这样设计的优点:
字符串由3种编码实现。
存储的数据是整数时,redis会将键值设置为int类型来进行存储,对应的编码类型是REDIS_ENCODING_INT
由sds实现 ,字节数 <= 39
存储的数据是字符串时,且字节数小于等于39 ,用的是embstr
优点:1、创建字符串对象由两次变成了一次
2、连续的内存,更好的利用缓存优势
缺点:1、由于是连续的空间,所以适合只读,如果修改的话,就会变成raw
2、由于是连续的空间,所以值适合小字符串
由sds实现 字节数 > 39
list是由压缩链表和双向链表实现的
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 存储的数据
void *value;
} listNode;
typedef struct list {
// 链表头节点
listNode *head;
// 链表尾节点
listNode *tail;
// 节点值复制函数
void *(*dup)(void *ptr);
// 节点值释放函数
void (*free)(void *ptr);
// 节点值对比函数
int (*match)(void *ptr, void *key);
// 链表所有节点数量
unsigned long len;
} list;
适用于长度较小的值,因为他是由连续的空间实现的。
存取的效率高,内存占用小,但由于内存是连续的,在修改的时候要重新分配内存
在数据量比较小的时候使用的是ziplist
当list对象同时满足以下两个条件是,使用的ziplist编码
1.list对象保存的所有字符串元素长度都小于64字节
2.list对象保存的元素数量小于512个
缺点:
在使用redis的list数据结构时,存储数据较大时,list对象已经不满足上面描述的ziplist条件,则会使用linkedlist
优点:修改效率高
缺点:保存前后指针,会占内存。
结合了压缩列表和双向链表的优点,也解决了他们的缺点
// 快速列表节点
typedef struct quicklistNode {
struct quicklistNode *prev; //上一个node节点
struct quicklistNode *next; //下一个node
unsigned char *zl; //保存的数据 压缩前ziplist 压缩后压缩的数据
unsigned int sz; /* ziplist size in bytes */
unsigned int count : 16; /* count of items in ziplist */
unsigned int encoding : 2; /* RAW==1 or LZF==2 */
unsigned int container : 2; /* NONE==1 or ZIPLIST==2 */
unsigned int recompress : 1; /* was this node previous compressed? */
unsigned int attempted_compress : 1; /* node can't compress; too small */
unsigned int extra : 10; /* more bits to steal for future usage */
} quicklistNode;
// 快速列表
typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count; /* total count of all entries in all ziplists */
unsigned long len; /* number of quicklistNodes */
int fill : 16; /* fill factor for individual nodes ziplist大小设置 */
unsigned int compress : 16; /* depth of end nodes not to compress;0=off 节点压缩深度设置*/
} quicklist;
// 被压缩的列表
typedef struct quicklistLZF {
unsigned int sz; /* LZF size in bytes*/
char compressed[];
} quicklistLZF;
底层是由ziplist 或hashtable 实现的
ziplist在上面介绍过了,下面来聊聊hashtable
一个哈希表里面可以有多个哈希节点,而每个哈希节点就保存了字典中的一个键值对
// 字典 数据结构
typedef struct dict {//管理两个dictht,主要用于动态扩容。
//类型特定函数
dictType *type;
//私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash索引,当rehash不再进行是,值为 -1
long rehashidx; /* 扩容标志rehashing not in progress if rehashidx == -1 */
unsigned long iterators; /* number of iterators currently running */
} dict;
/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
//定义一个hash桶,用来管理hashtable
typedef struct dictht {
// hash表数组,所谓的桶
dictEntry **table;
// hash表大小,元素个数
unsigned long size;
// hash表大小掩码,用于计算索引值,值总是size -1
unsigned long sizemask;
// 该hash表已有的节点数量
unsigned long used;
} dictht;
//hash节点
typedef struct dictEntry {
//键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
// 指向下一个节点,解决碰撞冲突
struct dictEntry *next;
} dictEntry;
//定义hash需要使用的函数
typedef struct dictType {
// 计算哈希值的函数
uint64_t (*hashFunction)(const void *key);
//复制键的函数
void *(*keyDup)(void *privdata, const void *key);
// 复制值的函数
void *(*valDup)(void *privdata, const void *obj);
// 对比键的函数
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
//销毁键的函数
void (*keyDestructor)(void *privdata, void *key);
// 销毁值的函数
void (*valDestructor)(void *privdata, void *obj);
} dictType;
参数解析:
dict中
type标识dictType类型,privatedata是指向特定数据类型的数组;
ht包含了两个哈希表的数组,其中一个ht[0]保存哈希表,ht[1]只会在对ht[0]进行rehash时使用
dicht中
table是一个数组,其中一个数组元素都是一个指向哈希表节点的指针,每一个哈希表节点都保存了一对键值对。
size记录了table的大小,也即是哈希表的大小
used记录了当前已使用节点的数量
sizemask=size-1 ,决定了一个键应该放tabl数组的哪个索引上
dicEntry中
key保存键值对中的键,
v保存这键值对中的值
next为指向另一个哈希表几点的指针,当有hash冲突的时候
当以下条件中的任意一个被满足时,程序会自动开始对哈希表执行扩展操作
1:服务器目前没有在执行bgsave命令或者bgrewriteaof命令、并且哈希表的负载因子大于等于1
2:服务器目前正在执行bgsave命令或者bgrewriteaof命令并且哈希表的负载因子大于等于5
rehash 跟Java中的hashmap扩容机制差不多
随着操作的不断执行,哈希表保存的键值对会逐渐地增多或减少,为了让哈希表的负载因子维持在一个合理的
范围内当哈希表保存的键值对数量太多或者太少时程序需要对哈希表的大小进行相应的扩展或者收缩
redis对字典的哈希表执行rehash的步骤如下;
1、为字典ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作以及ht[0]当前包含的键值对数量(ht[0].used)
2、如果执行的是扩展操作,那么ht[1]的大小为第一个大于等于ht[0].used*2的n次幂
3、如果执行的是收缩操作,那么ht[1]的大小为第一个大于等于ht[0].used的2的n次幂
4、将保存在ht[0]中的所有键值对rehash到ht[1]上面rehash指的是重新计算键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上
5、当ht[0]包含的所有键值对都迁移到了ht[1]之后释放ht[0]将ht[1]设置为ht[0],并在ht[1]新创键一个空白哈希表为下一次rehash做准备
为了避免rehash对服务器性能造成影响,服务器不是一次性将ht[0]里面的所有键值对全部rehash到ht[1]而是分多次将ht[0]里面的键值对慢慢地rehash到ht[1]
在进行渐进式rehash的过程中,字典会同时使用ht[0] ht[1]两个哈希表所以在渐进式rehash进行期间字典的删除 查找 更新等操作会在两个哈希表上进行 新添加到字典的键值对一律会保存到ht[1]里面则ht[0]不再进行任何添加操作
如果哈希表里保存的键值对数量是四百万、四千万甚至四亿个键值对,那么要一次性将这些键值对全部rehash到ht[1]的话,庞大的计算量可能会导致服务器在一段时间内停止服务。因此,为了避免rehash对服务器性能造成影响,服务器不是一次性将 ht[0]里面的所有键值对全部rehash到ht[1],而是分多次、渐进式地将ht[0]里面的键值对慢慢地rehash到ht[1]。
以下是哈希表渐进式 rehash 的详细步骤:
1、为ht[1]分配空间,让字典同时持有ht[0]和ht[1]两个哈希表。
2、在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始。
3、在rehash进行期间,**每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,**还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对
4、rehash到ht[1],当rehash工作完成之后,程序将rehashidx+1(表示下次将rehash下一个桶)。
5、随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成。
渐进式rehash的好处在于它采取分而治之的方式,将rehash键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式rehash 而带来的庞大计算量。而memcached通过也需要rehash,但是它是另外单独开一个线程,专门执行rehash操作。这就是区别与Redis的做法,个人任何Redis的作者方法更胜一筹。因为这种分治算法,将全部负载,均摊到了每次的操作过程中,在单个线程中实现。这个就是迁移线程,memcached中的迁移线程。
底层使用的是intset 或hashtable 实现的
hashtable 上面介绍过了,下面介绍intset
有序不重复的连续空间,特性如下:
1、节约内存,但由于是连续空间,修改效率不高
2、集合中的数都是整数时,且数据量不超过512个时,使用intset
3、整数集合是集合键的底层实现之一
4、 整数集合的底层实现是数组,这个数组以有序、无重复的方式保存集合元素,在有需要的时候,程序会根据新添加元素的类型,改变这个数组的类型。
5、升级操作为整数集合带来了操作上的灵活性,并且尽可能的节约了内存。
6、整数集合只支持升级操作,不支持降级操作
typedef struct intset {
// 编码类型 int16_t、int32_t、int64_t
uint32_t encoding;
// 长度 最大长度:2^32
uint32_t length;
// 用来存储数据的柔性数组
int8_t contents[];
} intset;
底层是由ziplist 或 skiplist 实现的,ziplist 上面介绍过了,下面介绍skiplist
跳表(Skiplist)是一个特殊的链表,相比一般的链表,有更高的查找效率,可比拟二叉查找树,平均期望的查找、插入、删除时间复杂度都是O(logn)。具有层次结构的链表,可支持范围查询
跳表的第一层是一个双向链表,其他层级的都是单向链表
它由很多层结构组成,每一层都是一个有序的链表,默认是升序,也可以根据创建映射时所提供的Comparator进行排序,具体取决于使用的构造方法
最底层(Level 1)的链表包含所有元素
如果一个元素出现在Level i 的链表中,则它在Level i 之下的链表也都会出现
每个节点包含两个指针,一个指向同一链表中的下一个元素,一个指向下面一层的元素
// 跳表节点
typedef struct zskiplistNode {
// 存放至该节点的数据
robj *obj;
// 数据的分数值,zset根据这个分数值进行升序排序
double score;
// 指向链表的上一个节点
struct zskiplistNode *backward;
// 跳表的层级,每一条数据代表着一层
struct zskiplistLevel {
// 指向下一个节点的指针
struct zskiplistNode *forward;
// 表示这个指针跳跃了多少个节点
unsigned int span;
} level[];
} zskiplistNode;
// 跳表
typedef struct zskiplist {
// 头节点指针,尾节点指针
struct zskiplistNode *header, *tail;
// 跳表的长度,不包括头指针
unsigned long length;
// 跳表的层数
int level;
} zskiplist;
到这里我们常用的redis的几种数据结构以及底层的实现原理也大致了解完了。
个人觉得配合图理解起来更为直观一些。
我本人在工作中也经常通过图来描述一些复杂的场景。有些场景文字描述比较难以理解,但是图片可以更形象,更直观的表达出来。