Redis中的key都是字符串,而value往往是字符串或者是字符串的集合(List、hash里面保存的还是字符串)。可见字符串是Redis中最常见的数据结构。
Redis虽然是用C语言来实现的,但是并没有直接使用C语言中的字符串,因为C语言字符串有很多问题:
所以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数据结构本身保存了长度,所以不需要计算就知道长度,也不会有二进制安全问题,同时是可以动态扩容,可修改的。
如果要追加字符串,如果内存不够,那么首先会先申请内存空间,由于申请内存空间的操作是很耗时的,涉及到内核态和用户态的转换,所以我们会预先申请比较多的内存。 内存预分配 如下
顾名思义,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字节。然后 倒序 地将数组中现有的元素拷贝到扩容后地正确位置,因为如果正序地话,因为元素占用字节扩充了,会往后延申,会覆盖掉后面的元素,倒序不会出现 数据覆盖 的问题。最后将待添加的元素加入数组末尾。
底层使用 二分查找 方式来查询。
Redis是键值型的数据库,我们可以根据键快速查找到对应的值,并且快速地进行增删改查。键和值地映射关系就是通过Dict这个数据结构来实现的。
Dict由三部分组成,分别是:哈希表(DictHashTable)、哈希节点(DictEntry)、字典(Dict)。
这里的字典其实就很类似Java中的HashMap,键值对保存在Entry里(哈希节点),键和值的类型一般都是 指针 ,指向 SDS对象 ,然后用数组(哈希表)来保存哈希节点,如果出现哈希冲突了,就使用拉链法也就是链表把冲突的哈希节点串起来保存。哈希表扩容的长度一定要是2^n次方长度,因为可以直接使用&运算来计算出Entry所在的桶位置。
Dict就是类似HashMap,其中有两个哈希表,主要就是使用其中一个,另外一个是rehash的时候使用的。
当哈希表中的元素越来越多的时候,哈希冲突也会很多,就会导致链表越来越长,查询效率就会很低。
Dict在每次新增键值对的时候都会检查 负载因子 (LoadFactor = used/size),满足以下两种情况时就会触发 哈希表扩容:
除了扩容之外,每次删除元素的时候,也会对负载因子进行检查,当LoadFactor<0.1的时候,就会做哈希表收缩。
rehash 不管是扩容还是收缩,一定会创建一个新的哈希表,导致哈希表的size和sizemask变化,而key的查询与sizemask有关。因此必须要对哈希表中的每一个key重新计算索引,插入到新的哈希表中,这个过程叫做 rehash 。还记得我们之前说过的每个dict都有两个哈希表吗,其中一个哈希表就是当前使用的,另外一个哈希表就是用来做扩容用的,将正在使用的哈希表中的所有entry都rehash到另外一个哈希表中,然后再把rehash后的哈希表复制回正在使用的哈希表,再把用来rehash的哈希表变为空。所以dict中两个哈希表的作用就是一个是用来 正常使用 的,另外一个是用来做 rehash 使用的。
上面对rehash的概括是粗略的,真实的reash为了保证Redis的性能(避免Redis的主线程阻塞)并不是一次性完成的,而是 渐进式 地,分多次完成的,也叫做 渐进式rehash:
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比较适合存储比较少的数据。
那么既然内存中连续内存比较稀有,ZipList只适合存储少量的数据,那么当我们需要存储大量数据的时候应该怎么做呢?我们可以创建多个ZipList来分片存储数据,然后把这些多个ZipList连起来就行了。
QuickList就是这么做的,它是一个双端链表,只不过链表中的每个节点都是一个ZipList。QuickList本身的双端链表是有前后指针的,就类似普通的双端链表了。
为了避免 QuickList 中的每个 ZipList 中 entry 过多,Redis 提供了一个配置项: list-max-ziplist-size 来限制。
QuickList 的特点:
ZipList和QuickList都可以说是“双端链表”,那么我们知道双端链表是没办法做随机读取的,我们想要读取某个数据,只能顺序遍历然后取出该数据。那么我们需要随机读取怎么办呢,SkipList就可以实现这个功能。
SkipList(跳表) 首先是链表,但与传统链表相比有些差异:
SkipList 的特点:
上面介绍的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 |
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。