今天我们讲解字符串的底层原理,属于进阶内容,能回答出来可以秒杀80%的面试者。
大家都知道Redis有5种基本数据类型,但是你知道每种数据类型对应的底层编码或者数据结构是什么样的吗?
这在面试中是一个有区分度的问题,如果你不会,那么非常有必要继续阅读
这里只列举出不同数据类型的主要编码实现,并非全部。主要的底层编码有这几种:
•简单动态字符串
•双向链表
•整数
•哈希表
•压缩列表
•跳表
•整数集合
先说明 ,我们这里是以redis 5.0.8版本的代码为准,不同版本之间实现会有区别。
Redis 对象结构
首先补充一下基础知识,Redis对象由redisObject结构体表示。(这里的源码基于5.0.5版本)
typedef struct redisObject {
// 类型
unsigned type:4;
// 所使用的底层编码
unsigned encoding:4;
// 对象最后一次被访问的时间 ,和redis 的lru算法实现有关
unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
// 引用计数
int refcount;
// 重点:指向对象的底层实现数据结构
void *ptr;
} robj;
Redis中的每个键值对的键和值都是一个redisObject。共有五种类型的对象:字符串(String)、列表(List)、哈希(Hash)、集合(Set)、有序集合(SortedSet),源码server.h如下定义:
#define OBJ_STRING 0 /* String object. */字符串
#define OBJ_LIST 1 /* List object. */list
#define OBJ_SET 2 /* Set object. */集合
#define OBJ_ZSET 3 /* Sorted set object. */有序集合
#define OBJ_HASH 4 /* Hash object. */哈希
每种类型的对象至少都有两种或以上的编码方式;可以在不同的使用场景上优化对象的使用场景。用TYPE命令可查看某个键值对的类型。
#define OBJ_ENCODING_RAW 0 /* SDS简单动态字符串 Raw representation */
#define OBJ_ENCODING_INT 1 /* 整数 Encoded as integer */
#define OBJ_ENCODING_HT 2 /* 字典Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3 /* map Encoded as zipmap */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* 压缩列表 Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6 /*整数集合 Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7 /* 跳跃表Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8 /* 嵌入式SDS Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
本质上,Redis就是基于这些数据结构而构造出一个对象存储系统。redisObject结构体有个ptr指针,指向对象的底层实现数据结构,encoding属性记录对象所使用的编码,即该对象使用什么数据结构作为底层实现。
(关于我对Redis源码的阅读,已经在源码中做了中文注释和笔记,后期会在GitHub开放)
本期我们主要讲解Redis中String的底层编码方式。
String
Redis的String类型在5.0.8版本中主要有三种编码实现,分别是整数、简单动态字符、嵌入式字符串。
SDS
Redis 设计了简单动态字符串(Simple Dynamic String,SDS)的结构,用来表示字符串。相比于 C 语言中的字符串实现,SDS 这种字符串的实现方式,会提升字符串的操作效率,并且可以用来保存二进制数据。
我们可能以为redis在内部存储string都是用sds的数据结构实现的,其实在整个redis的数据存储过程中为了提高性能,内部做了很多优化。整体选择顺序应该是:
•整数,存储字符串长度小于21且能够转化为整数的字符串。
•EmbeddedString,存储字符串长度小于44的字符串。
•SDS,剩余情况使用sds进行存储。
为什么不用 C 自带 Char*
1.“\0”的影响,不能存储任意二进制数据
2.操作函数复杂度,很多复杂度达到On,不能保证有足够的空间,不符合 Redis 对字符串高效操作的需求。
所以在实现字符串时,需要尽量满足以下三个要求:
1.能支持丰富且高效的字符串操作,比如字符串追加、拷贝、比较、获取长度等
2.能保存任意的二进制数据,比如图片等
3.能尽可能地节省内存开销。
事实上,SDS 一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64。这 5 种类型的主要区别就在于,它们数据结构中的字符数组现有长度 len 和分配空间长度 alloc
// EG:sdshdr8
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* 字符数组现有长度*/
uint8_t alloc; /* 字符数组的已分配空间,不包括结构体和\0结束字符*/
unsigned char flags; /* SDS类型*/
char buf[]; /*字符数组*/
};
SDS结构设计
首先,SDS 结构里包含了一个字符数组 buf[],用来保存实际数据。同时,SDS 结构里还包含了三个元数据,分别是字符数组现有长度 len、分配给字符数组的空间长度 alloc,以及 SDS 类型 flags。其中,Redis 给 len 和 alloc 这两个元数据定义了多种数据类型,进而可以用来表示不同类型的 SDS。
另外,如果你在 Redis 源码中查找过 SDS 的定义,那你可能会看到,Redis 使用 typedef 给 char* 类型定义了一个别名,这个别名就是 sds,如下所示:
typedef char *sds;
其实,这是因为 SDS 本质还是字符数组,只是在字符数组基础上增加了额外的元数据。在 Redis 中需要用到字符数组时,就直接使用 sds 这个别名。
嵌入式字符串
尝试将 RAW 编码的字符串编码为 EMBSTR 编码,使用EMBSTR 编码嵌入式字符串,这个对象没办法进行编码,尝试从 SDS 中移除所有空余空间,使用SDS编码
SDS 在保存比较小的字符串时,会使用嵌入式字符串的设计方法,将字符串直接保存在 redisObject 结构体中。然后在 redisObject 结构体中,存在一个指向值的指针 ptr,而一般来说,这个 ptr 指针会指向值的数据结构。这里我们就以创建一个 String 类型的值为例,Redis 会调用 createStringObject 函数,来创建相应的 redisObject,而这个 redisObject 中的 ptr 指针,就会指向 SDS 数据结构,创建RedisObject时,同时也会分配好嵌入式字符串所需的内存空间,如下图所示。
字符串小于等于44字节时,使用嵌入式字符串,否则创建普通sds字符串
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
robj *createStringObject(const char *ptr, size_t len) {
//创建嵌入式字符串,字符串长度小于等于44字节
if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)
return createEmbeddedStringObject(ptr,len);
//创建普通字符串,字符串长度大于44字节
else
return createRawStringObject(ptr,len);
}
首先,createEmbeddedStringObject 函数会分配一块连续的内存空间,这块内存空间的大小等于 redisObject 结构体的大小、SDS 结构头 sdshdr8 的大小和字符串大小的总和,并且再加上 1 字节。注意,这里最后的 1 字节是 SDS 中加在字符串最后的结束字符“\0”。
在 Redis 源码中,createStringObject 函数会根据要创建的字符串的长度,决定具体调用哪个函数来完成创建。那么针对这个 createStringObject 函数来说,它的参数是字符串 ptr 和字符串长度 len。当 len 的长度大于 OBJ_ENCODING_EMBSTR_SIZE_LIMIT 这个宏定义时,createStringObject 函数会调用 createRawStringObject 函数,否则就调用 createEmbeddedStringObject 函数。而在我们分析的 Redis 5.0.8 源码版本中,这个 OBJ_ENCODING_EMBSTR_SIZE_LIMIT 默认定义为 44 字节。
embstr和sds的区别在于内存的申请和回收
•embstr的创建只需分配一次内存,而raw为两次(一次为sds分配对象,另一次为redisObject分配对象,embstr省去了第一次)。相对地,释放内存的次数也由两次变为一次。
•embstr的redisObject和sds放在一起,更好地利用缓存带来的优势
•缺点:redis并未提供任何修改embstr的方式,即embstr是只读的形式。对embstr的修改实际上是先转换为raw再进行修改。
整数
只对长度小于或等于 20 字节,并且可以被解释为整数的字符串进行编码,使用整数存储 ,使用整数是最节省空间的做法。
// robj *tryObjectEncoding(robj *o):尝试对字符串对象进行编码,以节约内存
// OBJ_ENCODING_INT 1
...
// 只对长度小于或等于 20 字节,并且可以被解释为整数的字符串进行编码
if (len <= 20 && string2l(s,len,&value)) {
...
o->encoding = OBJ_ENCODING_INT;
o->ptr = (void*) value;
return o;
...
加入讨论群是升职加薪第一步!
回复:加群
点赞是一种美德,如对您有帮助,欢迎评论和分享,感谢阅读!
一文读懂MySQL的BinLog写入机制|原创
2023-04-04
TCC真没这么简单,一文讲透|分布式事务系列(三)
2023-03-29
从二叉查找树到B*树,一文搞懂搜索树的演进!|金三银四系列
2023-03-25