Redis底层数据结构
动态字符串SDS
Redis中的key都是字符串,而value往往是字符串或者是字符串的集合(List、hash里面保存的还是字符串)。可见字符串是Redis中最常见的数据结构。
Redis虽然是用C语言来实现的,但是并没有直接使用C语言中的字符串,因为C语言字符串有很多问题:
- 获取字符串长度需要运算 (末尾有一个\0来标识字符串的结尾,不直接保存字符串的长度)
- 非二进制安全 (不能包含特殊字符,其中如果保存\0,读取这个字符串就会出错)
- 不可修改
所以Redis就构建了一个新的字符串结构,称为 简单动态字符串(Simple Dynamic String) ,简称是SDS。
Redis是C语言实现的,其中SDS是一个结构体,源码如下:
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* buf已保存的字符串字节数,不包含结束标示*/
uint8_t alloc; /* buf申请的总的字节数,不包含结束标示*/
unsigned char flags; /* 不同SDS的头类型,用来控制SDS的头大小 */
char buf[];
};
还分别由有sdshdr5,sdshdr16,sdshdr32,sdshdr64。代表的是len这个字段的大小,意味着这个字符串的长度大小。flags属性就是标识是哪种头类型。例如,一个包含字符串“name”的sds结构如下:

SDS数据结构本身保存了长度,所以不需要计算就知道长度,也不会有二进制安全问题,同时是可以动态扩容,可修改的。
如果要追加字符串,如果内存不够,那么首先会先申请内存空间,由于申请内存空间的操作是很耗时的,涉及到内核态和用户态的转换,所以我们会预先申请比较多的内存。 内存预分配 如下
- 如果新字符串小于1M,新空间为扩展后字符串长度的两倍+1,
- 如果新字符串大于1M,则新空间为扩展后字符串长度+1M+1。
IntSet
顾名思义,IntSet是一个整数集合,是Redis中set集合的一种实现方式,基于整数数组来实现,具有长度可变、有序等特征。
// 整数集合结构体
typedef struct intset {
uint32_t encoding; // 编码方式,决定存储的整数类型
uint32_t length; // 集合中元素的数量
int8_t contents[]; // 柔性数组,存储实际的整数数据
} intset;
// 编码方式定义(根据整数大小选择不同类型,节省空间)
#define INTSET_ENC_INT16 (sizeof(int16_t)) // 2字节,范围:-32768~32767
#define INTSET_ENC_INT32 (sizeof(int32_t)) // 4字节,范围:-2147483648~2147483647
#define INTSET_ENC_INT64 (sizeof(int64_t)) // 8字节,范围:-9223372036854775808~9223372036854775807
contents是指针,指向了IntSet用来保存数的数组中第一个元素的地址。而encoding决定了每个数的大小。
为了方便查找,Redis会兼顾IntSet中所有的整数按照升序依次保存在contents数组中(保存指针)。结构如图:

固定每个元素的encoding方式,方便我们可以通过数组角标寻址。
IntSet的编码方式会 自动升级 ,如果当前IntSet中的元素现在是int16_t的编码方式,如果加入了一个50000的元素,这个元素超出了int16_t的范围,那么IntSet就会自动将所有元素的编码方式都升级为int32_t,每个元素就变成了4字节。然后 倒序 地将数组中现有的元素拷贝到扩容后地正确位置,因为如果正序地话,因为元素占用字节扩充了,会往后延申,会覆盖掉后面的元素,倒序不会出现 数据覆盖 的问题。最后将待添加的元素加入数组末尾。
底层使用 二分查找 方式来查询。
Dict
Redis是键值型的数据库,我们可以根据键快速查找到对应的值,并且快速地进行增删改查。键和值地映射关系就是通过Dict这个数据结构来实现的。
Dict由三部分组成,分别是:哈希表(DictHashTable)、哈希节点(DictEntry)、字典(Dict)。
这里的字典其实就很类似Java中的HashMap,键值对保存在Entry里(哈希节点),键和值的类型一般都是 指针 ,指向 SDS对象 ,然后用数组(哈希表)来保存哈希节点,如果出现哈希冲突了,就使用拉链法也就是链表把冲突的哈希节点串起来保存。哈希表扩容的长度一定要是2^n次方长度,因为可以直接使用&运算来计算出Entry所在的桶位置。
Dict就是类似HashMap,其中有两个哈希表,主要就是使用其中一个,另外一个是rehash的时候使用的。
当哈希表中的元素越来越多的时候,哈希冲突也会很多,就会导致链表越来越长,查询效率就会很低。
Dict在每次新增键值对的时候都会检查 负载因子 (LoadFactor = used/size),满足以下两种情况时就会触发 哈希表扩容:
- 哈希表的LoadFactor >= 1,并且服务器没有指向BGSAVE 或者 BGReWRITEAOF等进程。
- 哈希表的 LoadFactor >5。
除了扩容之外,每次删除元素的时候,也会对负载因子进行检查,当LoadFactor<0.1的时候,就会做哈希表收缩。
rehash 不管是扩容还是收缩,一定会创建一个新的哈希表,导致哈希表的size和sizemask变化,而key的查询与sizemask有关。因此必须要对哈希表中的每一个key重新计算索引,插入到新的哈希表中,这个过程叫做 rehash 。还记得我们之前说过的每个dict都有两个哈希表吗,其中一个哈希表就是当前使用的,另外一个哈希表就是用来做扩容用的,将正在使用的哈希表中的所有entry都rehash到另外一个哈希表中,然后再把rehash后的哈希表复制回正在使用的哈希表,再把用来rehash的哈希表变为空。所以dict中两个哈希表的作用就是一个是用来 正常使用 的,另外一个是用来做 rehash 使用的。
上面对rehash的概括是粗略的,真实的reash为了保证Redis的性能(避免Redis的主线程阻塞)并不是一次性完成的,而是 渐进式 地,分多次完成的,也叫做 渐进式rehash:
- 计算新hash表的size,值取决于当前要做的是扩容还是收缩:。如果是扩容,则新size为第一个大于等于dict.ht[0].used + 1的2ⁿ 。如果是收缩,则新size为第一个大于等于dict.ht[0].used的2ⁿ(不得小于4)
- ②按照新的size申请内存空间,创建dictht,并赋值给dict.ht[1]
- 设置dict.rehashidx = 0,标示开始rehash
将dict.ht[0]中的每一个dictEntry都rehash到dict.ht[1]- 每次执行新增、查询、修改、删除操作时,都检查一下dict.rehashidx是否大于-1,如果是则将dict.ht[0].table[rehashidx]的entry链表rehash到dict.ht[1],并且将rehashidx++。直至dict.ht[0]的所有数据都rehash到dict.ht[1]
- 将dict.ht[1]赋值给dict.ht[0],给dict.ht[1]初始化为空哈希表,释放原来的dict.ht[0]的内存
- 将rehashidx赋值为-1,代表rehash结束
- 在rehash过程中,新增操作,则直接写入ht[1],查询、修改和删除则会在dict.ht[0]和dict.ht[1]依次查找并执行。这样可以确保ht[0]的数据只减不增,随着rehash最终为空 。
ZipList
ZipList是一种特殊的 “双端链表” ,说它是特殊的链表,因为它不是链表,链表的内存空间往往是不连续的,且每个节点都会保存指针,指向下一个或者上一个节点,很浪费内存。而ZipList由一系列特殊编码的 连续内存块 组成。可以在任意一端进行压入/弹出操作,并且该操作的时间复杂度为O(1)。


ZipList的Entry并不像普通链表那样记录前后节点的指针,记录两个指针的话会占用16个字节,那样会很占用内存,ZipList的Entry由自己独特的结构:

previous_entry_length: 前一节点的长度,占1个或5个字节。
◆ 如果前一节点的长度小于254字节,则采用1个字节来保存这个长度值
◆ 如果前一节点的长度大于254字节,则采用5个字节来保存这个长度值,第一个字节为0xfe,后四个字节才是真实长度数据
encoding: 编码属性,记录content的数据类型(字符串还是整数)以及长度,占用1个、2个或5个字节
contents: 负责保存节点的数据,可以是字符串或整数
这样,我们可以很轻松地知道当前这个Entry的长度,就可以顺序遍历。借助previous_entry_length,我们可以很轻松地知道前一个Entry的长度,然后去进行逆序遍历。
ZipList的Entry没有前后节点指针可以节省很多内存空间,连续存储的特性可以加快访问速度。但是有一个问题,就是连续存储的内存是很稀有的,内存往往是碎片化的,所以ZipList比较适合存储比较少的数据。
QuickList
那么既然内存中连续内存比较稀有,ZipList只适合存储少量的数据,那么当我们需要存储大量数据的时候应该怎么做呢?我们可以创建多个ZipList来分片存储数据,然后把这些多个ZipList连起来就行了。
QuickList就是这么做的,它是一个双端链表,只不过链表中的每个节点都是一个ZipList。QuickList本身的双端链表是有前后指针的,就类似普通的双端链表了。
为了避免 QuickList 中的每个 ZipList 中 entry 过多,Redis 提供了一个配置项: list-max-ziplist-size 来限制。
- 如果值为正,则代表 ZipList 的允许的 entry 个数的最大值。
- 如果值为负,则代表 ZipList 的最大内存大小,分 5 种情况:
① -1: 每个 ZipList 的内存占用不能超过 4kb
② -2: 每个 ZipList 的内存占用不能超过 8kb
③ -3: 每个 ZipList 的内存占用不能超过 16kb
④ -4: 每个 ZipList 的内存占用不能超过 32kb
⑤ -5: 每个 ZipList 的内存占用不能超过 64kb

QuickList 的特点:
- 是一个节点为 ZipList 的双端链表
- 节点采用 ZipList,解决了传统链表的内存占用问题
- 控制了 ZipList 大小,解决连续内存空间申请效率问题
- 中间节点可以压缩,进一步节省了内存
SkipList
ZipList和QuickList都可以说是“双端链表”,那么我们知道双端链表是没办法做随机读取的,我们想要读取某个数据,只能顺序遍历然后取出该数据。那么我们需要随机读取怎么办呢,SkipList就可以实现这个功能。
SkipList(跳表) 首先是链表,但与传统链表相比有些差异:
- 元素按照升序排列存储。
- 节点可能包含多个指针,指针跨度不同。


SkipList 的特点:
- 跳跃表是一个双向链表,每个节点都包含 score 和 ele 值
- 节点按照 score 值排序,score 值一样则按照 ele 字典排序
- 每个节点都可以包含多层指针,层数是 1 到 32 之间的随机数
- 不同层指针到下一个节点的跨度不同,层级越高,跨度越大
- 增删改查效率与红黑树基本一致,实现却更简单
RedisObject
上面介绍的SDS、IntSet、Dict、ZipList、QuickList、SkipList都是底层数据结构。Redis中的任意数据类型的键和值都会被封装成一个RedisObject,也叫做Redis对象。
typedef struct redisObject {
unsigned type:4; // 数据类型(4位):OBJ_STRING, OBJ_LIST, OBJ_HASH, 等
unsigned encoding:4; // 编码方式(4位):OBJ_ENCODING_RAW, OBJ_ENCODING_INT, 等
unsigned lru:LRU_BITS; // LRU 时间戳或 LFU 计数器(24位,取决于配置)
int refcount; // 引用计数,用于内存回收
void *ptr; // 指向实际数据的指针(如 SDS、字典、跳表等)
} robj;
// 数据类型定义
#define OBJ_STRING 0 // 字符串
#define OBJ_LIST 1 // 列表
#define OBJ_SET 2 // 集合
#define OBJ_ZSET 3 // 有序集合
#define OBJ_HASH 4 // 哈希
// 编码方式定义(部分)
#define OBJ_ENCODING_RAW 0 // 原始字符串(SDS)
#define OBJ_ENCODING_INT 1 // 整数(直接存储在 ptr 中,不使用 SDS)
#define OBJ_ENCODING_HT 2 // 哈希表(字典)
#define OBJ_ENCODING_ZIPMAP 3 // 压缩映射(已废弃)
#define OBJ_ENCODING_LINKEDLIST 4 // 双向链表(列表的旧编码)
#define OBJ_ENCODING_ZIPLIST 5 // 压缩列表
#define OBJ_ENCODING_INTSET 6 // 整数集合
#define OBJ_ENCODING_SKIPLIST 7 // 跳表(有序集合)
#define OBJ_ENCODING_EMBSTR 8 // 嵌入式字符串(短字符串优化)
#define OBJ_ENCODING_QUICKLIST 9 // 快速列表(列表的新编码)
#define OBJ_ENCODING_STREAM 10 // 流
| 数据类型 | 编码方式 |
|---|---|
| OBJ_STRING | int、embstr、raw |
| OBJ_LIST | LinkedList 和 ZipList (3.2 以前)、QuickList(3.2 以后) |
| OBJ_SET | intset、HT |
| OBJ_ZSET | ZipList、HT、SkipList |
| OBJ_HASH | ZipList、HT |

992

被折叠的 条评论
为什么被折叠?



