大家好,我是白晨,一个不是很能熬夜,但是也想日更的人。如果喜欢这篇文章,点个赞👍,关注一下👀白晨吧!你的支持就是我最大的动力!💪💪💪
文章目录
- String源码剖析
- redisObject
- String底层解析
- 概述
- SDS
- set是如何执行的?
- 总结
- Redis Object(redisObject)
- String的底层实现
- SDS的内部结构
- SDS编码类型
- String类型的最大长度
- set命令的执行过程
- 创建String对象的过程
String源码剖析
redisObject
在了解String的底层实现之前,我们首先要了解一下Redis中value
是使用什么数据结构进行存储的。
如果对于面向对象思想理解比较深刻的同学应该有些思路,为了便于统一管理,value
的结构应该都是继承于同一个基类,在基类上进行对于每个特定种类的value
进行实现。
这种思路是非常正确的,虽然Redis是使用C语言进行开发的(C语言是面向过程的语言,没有官方支持的继承语法),但是其实现也是使用了面向对象的思想,在不同种类的value
之上封装了一个redisObject
的类,这就是所有value
的基类。
查看redisObject
的源码定义:
#define LRU_BITS 24
struct redisObject {unsigned type:4; // 类型字段,占4位,用于标识对象的类型(如字符串、列表、集合等)unsigned encoding:4; // 编码字段,占4位,用于存储对象采用的内部编码方式unsigned lru:LRU_BITS; /* * LRU位字段,位数由LRU_BITS定义,用于两种用途:* 1. 作为LRU时间,是相对于全局lru_clock的相对时间,用于跟踪对象的最近访问时间,以便进行LRU淘汰。* 2. 作为LFU数据,低8位是频率,高16位是访问时间,用于跟踪对象的使用频率和最近访问时间,以便进行LFU淘汰。*/int refcount; // 引用计数,记录该对象被引用的次数void *ptr; // 指针,指向实际存储对象数据的内存位置
};
先来看第一个类项type
,它是用于标识对象的类型的,比如字符串、列表、集合等,也即我们日常操作Redis时使用的数据类型。
下面是常见数据结构type
的取值:
/* The actual Redis Object */
#define OBJ_STRING 0 /* String object. */
#define OBJ_LIST 1 /* List object. */
#define OBJ_SET 2 /* Set object. */
#define OBJ_ZSET 3 /* Sorted set object. */
#define OBJ_HASH 4 /* Hash object. */
我们可以使用type
命令来查看一个key
对应的value
是什么类型的。
127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> type k1
string
接下来,来看第二个类项encoding
,此字段用于存储对象采用的内部编码方式。上面type
字段是告诉用户使用的是Redis支持的什么数据类型(比如String、List等),而encoding
字段是告诉底层应该怎么实现这个数据类型,也即编码类型。
encoding
编码类型可能的取值及对应的编码类型的映射如下:
#define OBJ_ENCODING_RAW 0 /* Raw representation */
#define OBJ_ENCODING_INT 1 /* Encoded as integer */
#define OBJ_ENCODING_HT 2 /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3 /* No longer used: old hash encoding. */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* No longer used: old list/hash/zset encoding. */
#define OBJ_ENCODING_INTSET 6 /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7 /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8 /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of listpacks */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
#define OBJ_ENCODING_LISTPACK 11 /* Encoded as a listpack */char *strEncoding(int encoding) {switch(encoding) {case OBJ_ENCODING_RAW: return "raw";case OBJ_ENCODING_INT: return "int";case OBJ_ENCODING_HT: return "hashtable";case OBJ_ENCODING_QUICKLIST: return "quicklist";case OBJ_ENCODING_LISTPACK: return "listpack";case OBJ_ENCODING_INTSET: return "intset";case OBJ_ENCODING_SKIPLIST: return "skiplist";case OBJ_ENCODING_EMBSTR: return "embstr";case OBJ_ENCODING_STREAM: return "stream";default: return "unknown";}
}
最后,redisObject
中最后一个类项ptr
,指向实际存储对象数据的内存位置,也即指向encoding
编码类型的具体实例。
String底层解析
概述
Redis中,String
类型底层实现的数据结构为 int
和 SDS
(简单动态字符串)。
为什么不使用C原生的字符数组,而要使用SDS
呢?
- 获取字符串长度
C字符串获取长度会使用strlen
函数,时间复杂度为O(n)
,其获取字符串长度的逻辑如下图所示:
而SDS
直接用一个类项记录了字符串长度,获取长度的时间复杂度为O(1)
。
- 字符串溢出
C字符串对于越界检查不是很完善,会出现内存踩踏的问题:
而SDS
会记录可用空间,在需要扩容时自动扩容。
- 存储特殊类型数据
如果要使用C字符串存储上面的字符串,要取字符串长度等函数全部都会出问题,因为C字符串约定以\0
作为字符串的结尾标志。
但是使用SDS
可用很好的存储这些特殊类型的数据:
总结一下,C字符串和SDS
的区别如下:
可以发现,SDS
在获取字符串长度、安全、内存分配等方面比C原生字符数组更加优越,所以使用SDS
实现String类型。
SDS
有两种encoding
编码类型的实现,分别为embstr
和raw
。
所以,String
类型底层实现的encoding
编码类型有三种,分别为int
、embstr
以及raw
,其中embstr
和raw
的只是相同数据结构——SDS
的不同实现。
分别在什么情况下,使用上面不同的编码呢?
int
:当存储的字符串全是整数值,并且这个整数值可以用long
类型(8字节有符号整数)来表示时,此时使用int
方式来存储。
long
类型数字最长为19位。
127.0.0.1:6379> set long 123456789# 可以转换为long类型,此时是int编码
OK
127.0.0.1:6379> object encoding long
"int"
127.0.0.1:6379> set long 12345678901234567890 # 20位整数,已经超出long类型的范围
OK
127.0.0.1:6379> object encoding long
"embstr"
embstr
:当存储的字符串长度小于44个字符时,此时使用embstr
方式来存储。raw
:当存储的字符串长度大于44个字符时,此时使用raw
方式来存储。
127.0.0.1:6379> set k1 xxxxxxxxxxxxxxxxxxxxxxxxx
OK
127.0.0.1:6379> strlen k1
(integer) 25
127.0.0.1:6379> object encoding k1
"embstr"
127.0.0.1:6379> set k2 xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
OK
127.0.0.1:6379> strlen k2
(integer) 90
127.0.0.1:6379> object encoding k2
"raw"
SDS
embstr
编码方式会在一块连续的内存中创建RedisObject
结构和SDS
结构。这种方式在创建和删除时只需要分配和释放一次内存。此外,所有的数据都在一起,寻找起来更方便,并且更加遵循缓存局部性,增加缓存使用效率。但是,embstr
存储的数据是只读的,如果需要修改embstr
存储的值时,Redis底层会将String
的存储方式由embstr
转换为raw
,然后再去修改其值。
127.0.0.1:6379> set k1 xxxxxxxxxxxxxxxxxx
OK
127.0.0.1:6379> strlen k1
(integer) 18
127.0.0.1:6379> object encoding k1
"embstr"
127.0.0.1:6379> append k1 yyyyy
(integer) 23
127.0.0.1:6379> strlen k1
(integer) 23
127.0.0.1:6379> object encoding k1
"raw"
raw
编码方式会分别为RedisObject
结构和SDS
结构分配内存。这意味着在创建和删除时需要分配和释放两次内存,并且对于缓存的效率方面不如 embstr
。
注:embstr
和raw
的边界条件在Redis的不同版本中有所变化:
- 在Redis 3.0、3.2以及4.0版本中,
embstr
和raw
的边界条件是39个字节。如果字符串长度小于等于39个字节,那么会使用embstr
编码方式,否则会使用raw
编码方式。 - 在Redis 5.0和6.0版本中,
embstr
和raw
的边界条件变为44个字节。这是因为在这些版本中,Redis对内存的使用进行了优化,将原来的sdshdr
结构改成了sdshdr16
、sdshdr32
、sdshdr64
,其中的unsigned int
变成了uint8_t
、uint16_t
。这样的改变使得sdshdr
的内存占用从原来的8个字节缩减为3个字节,因此embstr
和raw
的边界条件从39个字节增加到了44个字节。
下面,我将结合Redis源码,详细解析String
类型的底层实现,坐好了,开始发车。
首先,要了解String
类型,肯定要了解SDS
,首先查看Redis7中SDS
结构:
typedef char *sds;/* Note: sdshdr5 is never used, we just access the flags byte directly.* However is here to document the layout of type 5 SDS strings. */
struct __attribute__ ((__packed__)) sdshdr5 {unsigned char flags; /* 3 lsb of type, and 5 msb of string length */char buf[];
};
struct __attribute__ ((__packed__)) sdshdr8 {uint8_t len; /* used */uint8_t alloc; /* excluding the header and null terminator */unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[];
};
struct __attribute__ ((__packed__)) sdshdr16 {uint16_t len; /* used */uint16_t alloc; /* excluding the header and null terminator */unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[];
};
struct __attribute__ ((__packed__)) sdshdr32 {uint32_t len; /* used */uint32_t alloc; /* excluding the header and null terminator */unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[];
};
struct __attribute__ ((__packed__)) sdshdr64 {uint64_t len; /* used */uint64_t alloc; /* excluding the header and null terminator */unsigned char flags; /* 3 lsb of type, 5 unused bits */char buf[];
};
sdshdr5
:这个结构体并未实际使用,只是用来说明类型为5的SDS字符串的布局。其中,flags
字段的3个最低有效位用于存储类型,5个最高有效位用于存储字符串长度。sdshdr8
:这个结构体用于存储长度小于等于255的字符串。其中,len
字段表示已使用的字符数,alloc
字段表示分配的字符数(不包括头部和空字符),flags
字段的3个最低有效位用于存储类型,5个最高有效位未使用。sdshdr16
:这个结构体用于存储长度小于等于65535的字符串。其中,len
和alloc
字段的类型变为uint16_t
,可以存储更大的数值。sdshdr32
:这个结构体用于存储长度小于等于4294967295的字符串。其中,len
和alloc
字段的类型变为uint32_t
,可以存储更大的数值。sdshdr64
:这个结构体用于存储长度小于等于18446744073709551615的字符串。其中,len
和alloc
字段的类型变为uint64_t
,可以存储更大的数值。
可以看到Redis针对不同大小的字符串分别规划了不同的数据类型,比如sdshdr8
中len
字段最大值为255,所以其存储的字符串的最大长度也不会超过255。所以,字符串长度不超过44字节的embstr
的编码方式就使用的sdshdr8
这个结构体。
那为什么限制字符串长度不超过44字节才能使用embstr
的编码方式呢?
首先redisObject
这个结构体的大小为16字节,sdshdr8
不计算最后一个buf
字段得到的结构体大小为 1 + 1 + 1 = 3 1+1+1=3 1+1+1=3 字节,由于embstr
内存分配是连续,也即redisObject
和SDS
在同一连续空间,所以将其大小加和,此时需要连续内存 16 + 3 = 19 16+3=19 16+3=19 字节,假设此时buf
中有效字符串为20字节,再加上1字节的\0
,就一共需要 19 + 20 + 1 = 40 19+20+1=40 19+20+1=40 字节,而Linux分配内存最好都是按照2的次方数分配,否则会出现许多内存碎片,所以最后得到分配的内存为64( 2 6 2^6 26)字节。
现在我们只计算必要的内存开销,也即redisObject + sdshdr8中len、alloc以及flags + sdshdr8中buf的结束符\0
,得到必要内存开销为 16 + 1 + 1 + 1 + 1 = 20 16+1+1+1+1=20 16+1+1+1+1=20 字节, 总共分配内存为64字节, 64 − 20 = 44 64-20=44 64−20=44 字节 ,所以就得到了44字节。
所以当buf
中字符数大于12时,小于44时,就会分配64字节,而一般字符串长度都是大于12字节的,所以限制字符串长度不超过44字节才能使用embstr
的编码方式是为了更好的使用使用内存,提高内存的使用率,提高CPU访问速度。
在Redis 3.0、3.2以及4.0版本中,embstr
和raw
的边界条件是39个字节,这是因为当时SDS
还没有按照字符串长度对其使用的结构体进行分类,具体结构如下面代码块中所示,当时的sdshdr
比sdshdr8
大了5个字节,所以边界就为 44 − 5 = 39 44 - 5 = 39 44−5=39字节。
struct sdshdr {unsigned int len;// 4个字节unsigned int free;// 4个字节char buf[];
};
一张图总结SDS
:
set是如何执行的?
现在有这样一条最简单的命令:set k1 v1
,你知道它是怎么执行的吗?
先来看一下Redis内部是如何创建一个redisObject
的:
typedef struct redisObject robj;
robj *createObject(int type, void *ptr) {robj *o = zmalloc(sizeof(*o));o->type = type;o->encoding = OBJ_ENCODING_RAW;o->ptr = ptr;o->refcount = 1;o->lru = 0;return o;
}
可以看到默认的编码格式就是raw
类型。
直接看set
命令对应的源码:
/* SET key value [NX] [XX] [KEEPTTL] [GET] [EX <seconds>] [PX <milliseconds>]* [EXAT <seconds-timestamp>][PXAT <milliseconds-timestamp>] */
void setCommand(client *c) {robj *expire = NULL;int unit = UNIT_SECONDS;int flags = OBJ_NO_FLAGS;if (parseExtendedStringArgumentsOrReply(c,&flags,&unit,&expire,COMMAND_SET) != C_OK) {return;}c->argv[2] = tryObjectEncoding(c->argv[2]); // 此时value为raw类型编码,尝试对键的值进行重编码,以节省存储空间。setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL); // 执行SET命令,设置键的值并处理过期时间等参数
}
再来查看一下tryObjectEncoding
这个函数:
robj *tryObjectEncoding(robj *o) {return tryObjectEncodingEx(o, 1);
}#define OBJ_SHARED_INTEGERS 10000
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
/* Try to encode a string object in order to save space */
robj *tryObjectEncodingEx(robj *o, int try_trim) {long value;sds s = o->ptr;size_t len;/* Make sure this is a string object, the only type we encode* in this function. Other types use encoded memory efficient* representations but are handled by the commands implementing* the type. */serverAssertWithInfo(NULL,o,o->type == OBJ_STRING);/* We try some specialized encoding only for objects that are* RAW or EMBSTR encoded, in other words objects that are still* in represented by an actually array of chars. */if (!sdsEncodedObject(o)) return o;/* It's not safe to encode shared objects: shared objects can be shared* everywhere in the "object space" of Redis and may end in places where* they are not handled. We handle them only as values in the keyspace. */if (o->refcount > 1) return o;/* Check if we can represent this string as a long integer.* Note that we are sure that a string larger than 20 chars is not* representable as a 32 nor 64 bit integer. */len = sdslen(s);if (len <= 20 && string2l(s,len,&value)) {/* 该对象可编码为long类型。尝试使用共享对象。* 注意,当使用maxmemory时,我们避免使用共享整数* 因为每个对象都需要有一个私有的LRU字段,这样LRU算法才能正常工作。*/if ((server.maxmemory == 0 ||!(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) &&value >= 0 &&value < OBJ_SHARED_INTEGERS){decrRefCount(o);return shared.integers[value];} else {if (o->encoding == OBJ_ENCODING_RAW) {sdsfree(o->ptr);o->encoding = OBJ_ENCODING_INT;o->ptr = (void*) value;return o;} else if (o->encoding == OBJ_ENCODING_EMBSTR) {decrRefCount(o);return createStringObjectFromLongLongForValue(value);}}}/* 如果字符串很小并且仍然是RAW编码,* 尝试更有效的EMBSTR编码。* 在这种表示中,对象和SDS字符串被分配* 在同一块内存中,以节省空间和缓存丢失。*/if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT) {robj *emb;if (o->encoding == OBJ_ENCODING_EMBSTR) return o;emb = createEmbeddedStringObject(s,sdslen(s));decrRefCount(o);return emb;}/* We can't encode the object...* Do the last try, and at least optimize the SDS string inside */if (try_trim)trimStringObjectIfNeeded(o, 0);/* Return the original object. */return o;
}
robj *tryObjectEncoding(robj *o)
:这个函数是一个包装函数,它调用tryObjectEncodingEx(o, 1)
函数并返回结果。robj *tryObjectEncodingEx(robj *o, int try_trim)
:这个函数尝试对字符串对象o
进行编码。参数try_trim
表示是否尝试优化SDS字符串。- 如果字符串长度小于等于20,并且可以转换为长整数,那么就会尝试将其编码为整数。
- 如果服务器没有设置最大内存限制,或者服务器的最大内存策略允许使用共享整数,并且值在0到
OBJ_SHARED_INTEGERS(10000)
之间,那么会尝试使用共享整数对象来表示这个值。这是因为Redis预先创建了一些常用的整数对象,可以直接使用,这样可以节省内存。 - 如果不能使用共享整数对象,那么会根据原来的编码方式来处理。如果原来的编码方式是
OBJ_ENCODING_RAW
,那么会释放原来的SDS字符串,将编码方式改为OBJ_ENCODING_INT
,并将值存储在ptr
字段中。如果原来的编码方式是OBJ_ENCODING_EMBSTR
,那么会创建一个新的整数对象来存储这个值。
- 如果服务器没有设置最大内存限制,或者服务器的最大内存策略允许使用共享整数,并且值在0到
- 如果
o
的长度小于等于OBJ_ENCODING_EMBSTR_SIZE_LIMIT(44)
,并且o
仍然是RAW编码的,函数会尝试使用EMBSTR编码,这种编码方式更加高效。 - 最后,如果函数无法对
o
进行编码,它会尝试优化SDS字符串,然后返回原始对象o
。
- 如果字符串长度小于等于20,并且可以转换为长整数,那么就会尝试将其编码为整数。
再来看看以embstr
和raw
编码的对象的创建过程:
#define OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44
// 创建字符串对象
robj *createStringObject(const char *ptr, size_t len) {if (len <= OBJ_ENCODING_EMBSTR_SIZE_LIMIT)return createEmbeddedStringObject(ptr,len);elsereturn createRawStringObject(ptr,len);
}// 创建以embstr编码的对象
robj *createEmbeddedStringObject(const char *ptr, size_t len) {robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr8)+len+1);struct sdshdr8 *sh = (void*)(o+1);o->type = OBJ_STRING;o->encoding = OBJ_ENCODING_EMBSTR;o->ptr = sh+1;o->refcount = 1;o->lru = 0;sh->len = len;sh->alloc = len;sh->flags = SDS_TYPE_8;if (ptr == SDS_NOINIT)sh->buf[len] = '\0';else if (ptr) {memcpy(sh->buf,ptr,len);sh->buf[len] = '\0';} else {memset(sh->buf,0,len+1);}return o;
}// 创建以raw编码的对象
robj *createRawStringObject(const char *ptr, size_t len) {return createObject(OBJ_STRING, sdsnewlen(ptr,len));
}robj *createObject(int type, void *ptr) {robj *o = zmalloc(sizeof(*o));o->type = type;o->encoding = OBJ_ENCODING_RAW;o->ptr = ptr;o->refcount = 1;o->lru = 0;return o;
}
最后,为什么String
类型最大长度为512MB
?
static int checkStringLength(client *c, long long size, long long append) {if (mustObeyClient(c))return C_OK;/* 'uint64_t' cast is there just to prevent undefined behavior on overflow */long long total = (uint64_t)size + append;/* Test configured max-bulk-len represending a limit of the biggest string object,* and also test for overflow. */if (total > server.proto_max_bulk_len || total < size || total < append) {addReplyError(c,"string exceeds maximum allowed size (proto-max-bulk-len)");return C_ERR;}return C_OK;
}
可以看到,String类型的最大长度由server.proto_max_bulk_len
这个变量决定,那么这个变量的值为多少呢?
查看redis.conf
文件可得:
# In the Redis protocol, bulk requests, that are, elements representing single
# strings, are normally limited to 512 mb. However you can change this limit
# here, but must be 1mb or greater
#
proto-max-bulk-len 512mb
可以得到,proto-max-bulk-len
为默认为512mb
,所以String
类型最大长度为512MB
。
当然,也可以在Redis客户端查询单个字符串的最大长度:
127.0.0.1:6379> config get proto-max-bulk-len
1) "proto-max-bulk-len"
2) "536870912"
也可以得到512MB
。
总结
Redis Object(redisObject)
- Redis使用
redisObject
作为所有value的基类,采用面向对象的设计思想。 redisObject
包含类型(type)、编码(encoding)、LRU位字段、引用计数(refcount)和指向实际数据的指针(ptr)。
String的底层实现
- Redis中String类型可以通过两种方式实现:整数(int)和简单动态字符串(SDS)。
- SDS相比传统C字符串提供了获取长度的常数时间复杂度、更好的内存安全性和存储特殊数据的能力。
SDS的内部结构
- SDS使用不同的头部结构(sdshdr5, sdshdr8, sdshdr16, sdshdr32, sdshdr64)来存储不同长度的字符串,以优化内存使用。
sdshdr5
不用于实际存储,仅作为文档说明。sdshdr8
、sdshdr16
、sdshdr32
和sdshdr64
根据字符串长度使用不同大小的整数来存储长度和分配大小。
SDS编码类型
embstr
:当SDS较短时使用,它将redisObject
和SDS结构分配在同一块内存中,节省内存并提高缓存效率。raw
:当SDS较长时使用,redisObject
和SDS结构分别分配内存,灵活性更高但内存分配效率较低。
String类型的最大长度
- Redis中单个String类型的最大长度为512MB,由配置参数
proto-max-bulk-len
决定。
set命令的执行过程
set
命令首先尝试对值进行编码优化,以节省空间。- 如果值可以表示为整数,且在共享整数范围内,Redis会使用共享整数对象。
- 如果字符串较短,Redis会使用
embstr
编码。 - 对于较长的字符串,Redis会使用
raw
编码。
创建String对象的过程
- Redis提供了创建String对象的函数,根据字符串长度和编码类型创建相应的
redisObject
。
如果讲解有不对之处还请指正,我会尽快修改,多谢大家的包容。
如果大家喜欢这个系列,还请大家多多支持啦😋!
如果这篇文章有帮到你,还请给我一个大拇指
👍和小星星
⭐️支持一下白晨吧!喜欢白晨【Redis】系列的话,不如关注
👀白晨,以便看到最新更新哟!!!
我是不太能熬夜的白晨,我们下篇文章见。