概述
本节主要分析Redis key-value及5大基本数据类型背后对应的具体数据结构,只有了解了底层数据结构才能真正做到灵活掌握基本数据类型的使用
1. 全局哈希表
全局哈希表用于存储所有的键值对,结构类似HashMap,由哈希桶数组 + entry链表组成
Redis全局哈希表.png
1.1 哈希桶数组
--结构定义
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
-
**table
: 哈希表数组,数组的每个项是dictEntry链表的头结点指针 -
**size
: 哈希表大小;在redis的实现中,size也是触发扩容的阈值 -
**sizemask
: 哈希表大小掩码,用于计算索引值;总是等于size-1 -
**used
: 哈希表中保存的节点的数量
1.2 dictEntry
--结构定义
typedef struct dictEntry {
void *key;
void *val;
struct dictEntry *next;
} dictEntry;
-
*key
: 指向基本对象结构RedisObject(key都是字符串对象)的指针,Redis键值对中的每一个键值都是用RedisObject保存 -
*val
: 指向基本对象结构RedisObject的指针,val可以是String/List/Set/ZSet/Hash -
*next
: 指向链表中下一个dictEntry指针
1.3 RedisObject
--结构定义
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:REDIS_LRU_BITS;
int refcount;
void *ptr;
} robj;
-
type
: 类型,即5种基本数据类型String/Set/List/Hash/Sorted Set -
encoding
: 编码方式,用来表示Redis中实现各个基本类型的底层数据结构,例如SDS、压缩列表、哈希表、跳表等 -
lru
: 记录了这个对象最后一次被访问的时间,用于淘汰过期的键值对 -
refcount
: 记录了对象的引用计数; -
*ptr
: 指向数据的指针
1.4 哈希冲突的处理
类比HashMap很容易想到全局哈希表这种结构的两个问题
1.4.1 哈希冲突怎么办?
通常处理哈希冲突的方法有拉链法
和链表法
,通过全局哈希表结构能看出Redis使用链表法
1.4.2 数组扩容怎么办?
哈希表保存的键值对逐渐地增多, dictEntry链表长度就会越来越长,导致查询变慢,就需要进行扩容;为了使rehash操作更高效,避免同时复制大量数据,Redis默认使用两个全局哈希表 + 渐进式rehash
进行操作
- 开始插入数据时,默认使用哈希表1,当需要扩容rehash时才使用哈希表2
- 给哈希表2分配更大的空间,例如是当前哈希表1大小的两倍
- redis每处理一个请求时,从哈希表1中的第一个索引位置开始,顺带着将这个索引位置上的所有dictEntry链表拷贝到哈希表2中;等处理下一个请求时,再顺带拷贝哈希表1中的下一个索引位置的dictEntry链表
- 释放哈希表1的内存空间
2. int/embstr/raw
- 当保存64位有符号整数时,String类型会把它保存为一个8字节的Long类型整数,即int编码方式;
- 当保存的数据中包含字符时,String类型就会用简单动态字符串SDS结构体来保存,对应embstr或raw编码
- 当字符串比较短(小于44字节)时,RedisObject中的元数据(type/encoding/lru/refcount)、指针(ptr)和SDS是一块连续的内存区域,这样就可以避免内存碎片,即embstr编码
- 当字符串比较大(大于44字节)时,SDS的数据量就开始变多了,Redis会给SDS分配独立的空间,并用指针指向SDS结构,即raw编码
--SDS结构定义
struct sdshdr {
int len;
int alloc;
char buf[];
};
buf
:字节数组,保存实际数据。为了表示字节数组的结束,Redis会自动在数组最后加一个“\0”,这就会额外占用1个字节的开销
len
:占4个字节,表示buf的已用长度
alloc
:也占个4字节,表示buf的实际分配长度,一般大于len
3. linkedlist/ziplist/quicklist
Redis3.2之前,列表对象其底层存储结构可以有两种,即:linkedlist和ziplist,而在Redis 3.2之后,列表对象底层存储结构优化成为了另一种:quicklist。而quicklist可以认为是linkedlist和ziplist的结合体
3.1 linkedlist
linkedlist是一个双向列表,每个节点都会存储指向上一个节点和指向下一个节点的指针。linkedlist因为每个节点的空间是不连续的,所以可能会造成过多的空间碎片
redis-linkedlist.png
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;
typedef struct listNode {
struct listNode *prev;//前一个节点
struct listNode *next;//后一个节点
void *value;//值(字符串对象)
} listNode;
3.2 ziplist
ziplist是为了节省内存而开发的一种压缩列表数据结构,由一系列连续内存块组成的顺序型数据结构;ziplist和linkedlist最大的区别是ziplist不存储指向上一个节点和下一个节点的指针,存储的是上一个节点的长度和当前节点的长度,牺牲了部分读写性能来换取更高的内存利用率,是一种时间换空间的思想
redis-ziplist.png
-
zlbytes
: 列表长度 -
zltail
: 列表尾的偏移量 -
zllen
: 列表中的 entry 个数 -
zlend
: 列表结束 -
entry prev_len
: 表示前一个 entry 的长度,prev_len有两种取值情况:1字节或5字节。取值1字节时,表示上一个entry的长度小于254字节。虽然1字节的值能表示的数值范围是0到255,但是压缩列表中zlend的取值默认是255,因此,就默认用255表示整个压缩列表的结束,其他表示长度的地方就不能再用255这个值了。所以,当上一个entry长度小于254字节时,prev_len取值为1字节,否则,就取值为5字节 -
entry len
: 表示自身长度,4 字节 -
encoding
: 表示编码方式,1 字节; -
data
: 保存实际数据
--entry使用时需要序列化成zlentry再使用
typedef struct zlentry {
unsigned int prevrawlensize; /* 内存中编码后的prevrawlen用了多少字节 */
unsigned int prevrawlen; /* 前一个entry占用的长度,主要是为了entry之间跳转 */
unsigned int lensize; /* 内存中编码后的len用了多少字节 */
unsigned int len; /* 当前entry的长度,如果是string则表示string的长度,如果是整数,则len依赖于具体数值大小。*/
unsigned int headersize; /* prevrawlensize + lensize. entry的head部分用了多少字节 */
unsigned char encoding; /* 当前entry的编码格式 */
unsigned char *p; /* 指向数据域的指针 */
} zlentry;
3.3 quicklist
3.2之后引入的,统一用quicklist来存储列表对象,可以理解成是一种混合结构,quicklist 是 ziplist 和 linkedlist 的混合体,它将 linkedlist 按段切分,每一段使用 ziplist 来紧凑存储,多个 ziplist 之间使用双向指针串接起来。这样既满足了快速的插入删除性能,又不会出现太大的空间冗余
redis-quicklist.png
typedef struct quicklist {
quicklistNode *head;//列表头节点
quicklistNode *tail;//列表尾节点
unsigned long count;//ziplist中一共存储了多少元素,即:每一个quicklistNode内的count相加
unsigned long len; //双向链表的长度,即quicklistNode的数量
int fill : 16;//填充因子
unsigned int compress : 16;//压缩深度 0-不压缩
} quicklist;
typedef struct quicklistNode {
struct quicklistNode *prev;//前一个节点
struct quicklistNode *next;//后一个节点
unsigned char *zl;//当前指向的ziplist或者quicklistLZF
unsigned int sz;//当前ziplist占用字节
unsigned int count : 16;//ziplist中存储的元素个数,16字节(最大65535个)
unsigned int encoding : 2; //是否采用了LZF压缩算法压缩节点 1:RAW 2:LZF
unsigned int container : 2; //存储结构,NONE=1, ZIPLIST=2
unsigned int recompress : 1; //当前ziplist是否需要再次压缩(如果前面被解压过则为true,表示需要再次被压缩)
unsigned int attempted_compress : 1;//测试用
unsigned int extra : 10; //后期留用
} quicklistNode;
4. intset
当一个集合只包含整数值元素, 并且这个集合的元素数量不多时, Redis 就会使用整数集合作为集合键的底层实现,来节约内存;但是由于是连续空间,修改效率不高
redis-intset.png
typedef struct intset {
uint32_t encoding; // 编码方式
uint32_t length; // 集合包含的元素数量
int8_t contents[]; // 保存元素的数组,从小到大排序,无重复
} intset;
encoding记录了当前集合的编码方式,主要有三种:
-
INTSET_ENC_INT16
: 此时contents[]内的每个元素都是一个int16_t类型的整数值,范围是:-32768 ~ 32767(-2^15 ~ 2^15-1) -
INTSET_ENC_INT32
: 此时contents[]内的每个元素都是一个int32_t类型的整数值,范围是:-2147483648 ~ 2147483647(-2^31 ~ 2^31-1) -
INTSET_ENC_INT64
: 此时contents[]内的每个元素都是一个int64_t类型的整数值,范围是:-9223372036854775808 ~ 9223372036854775807(-2^63 ~ 2^63-1)
5. hashtable
hashtable用来存储key-value格式数据,数据结构类似java种hashmap和全局哈希表
redis-hashtable.png
/* 字典 */
typedef struct dict {
dictType *type; // 类型特定函数
void *privdata; // 私有数据
dictht ht[2]; // 哈希表,注意这里是两个哈希表
int rehashidx; // rehash 索引,当 rehash 不在进行时,值为 -1
int iterators; // 目前正在运行的安全迭代器的数量
} dict;
/* 哈希表 */
typedef struct dictht {
dictEntry **table; // 哈希表数组,用链表方式解决冲突问题
unsigned long size; // 哈希表大小
unsigned long sizemask; // 哈希表大小掩码,用于计算索引值,总是等于 size - 1
unsigned long used; // 该哈希表已有节点的数量
} dictht;
/* 字典entry */
typedef struct dictEntry {
void *key; //指向基本对象结构RedisObject(key都是字符串对象)的指针
void *val; //指向基本对象结构RedisObject的指针,val可以是String/List/Set/ZSet/Hash
struct dictEntry *next; //指向链表中下一个dictEntry指针
} dictEntry;
- hashtable跟全局哈希表类似,也是采用两个哈希表dictht + 渐进式rehash的方案
6. skiplist
有序链表只能逐一查找元素,导致操作起来非常缓慢,于是就出现了跳表。跳表在链表的基础上,增加了多级索引,通过索引位置的几个跳转,实现数据的快速定位。大部分情况下,跳跃表的效率可以等同于平衡树,但是跳跃表的实现却远远比平衡树的实现简单,所以Redis选择了使用跳跃表来实现有序集合;
redis-skiplist.png
/* 有序集合 */
typedef struct zset {
dict *dict; // 字典,键为成员,值为分值, 用于支持 O(1) 复杂度的按成员取分值操作
zskiplist *zsl; // 跳跃表,按分值排序成员,用于支持平均复杂度为 O(log N) 的按分值定位成员操作, 以及范围操作
} zset;
/* 跳跃表 */
typedef struct zskiplist {
struct zskiplistNode *header, *tail; // 表头节点和表尾节点
unsigned long length; // 表中节点的数量
int level; // 表中层数最大的节点的层数
} zskiplist;
/* 跳跃表节点 */
typedef struct zskiplistNode {
robj *obj; // 成员对象
double score; // 分值
struct zskiplistNode *backward; // 后退指针
struct zskiplistLevel { // 层
struct zskiplistNode *forward; // 前进指针
unsigned int span; // 跨度
} level[];
} zskiplistNode;
-
level[](层)
:level即跳跃表中的层,是一个数组,也就是说一个节点的元素可以拥有多个层,即多个指向其他节点的指针,程序可以通过不同层级的指针来选择最快捷的路径提升访问速度。level是在每次创建新节点的时候根据幂次定律随机生成的一个介于1~32之间的数字。 -
forward(前进指针)
: 每个层都会有一个指向链表尾部方向元素的指针,遍历元素的时候需要使用到前进指针。 -
span(跨度)
: 跨度记录了两个节点之间的距离,需要注意的是,如果指向了NULL的话,则跨度为0 -
backward(后退指针)
: 和前进指针不一样的是后退指针只有一个,所以每次只能后退至前一个节点。 -
ele(元素)
: 跳跃表中元素是一个sds对象,元素必须唯一不能重复 -
score(分值)
: 节点的分值是一个double类型的浮点数,跳跃表中会将节点按照分值按照从小到大的顺序排列,不同节点的分值可以重复。
小结
本文主要分析了redis底层用到的各种数据结构。redis是用 C 编写,本文列举的typedef struct
可以类比java中实体对象,理解起来还是比较容易的
---------over---------
网友评论