Redis底层数据结构分析
截止目前(2020-11-19),Redis 一共有以下几种上层数据类型:
- Strings
- Lists
- Hashes
- Sets
- Sorted Sets
- Bitmaps
- HyperLogLogs
- Streams
- Geo
最上面五种可谓是最经常使用的数据结构,每种又可能由多种更底层的数据结构实现。
Redis Strings
一个字符串的值不能超过 proto-max-bulk-len
大小,默认是512M(512ll*1024*1024)
,在 t_string.c#checkStringLength
定义了检查方法。
redis字符串的编码格式由三种:
- OBJ_ENCODING_INT
- OBJ_ENCODING_RAW
- OBJ_ENCODING_EMBSTR
其中后面两种都是底层采用的是一个叫做 sdshdrx
定义的结构体(x
为字节数),这种结构体分别是:
- sdshdr5 这个很特殊
- sdshdr8
- sdshdr16
- sdshdr32
- sdshdr64
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[];
};
小于32位长的字符用sdshdr5,flags低三位存类型,高五位存长度。当创建空字符的时候,sdshdr5会转成sdshdr8。
采用的规则,可以查看一下 object.c 中的 tryObjectEncoding
方法
OBJ_ENCODING_INT
我们看一下这段代码:
if (len <= 20 && string2l(s,len,&value)) {
/* This object is encodable as a long. Try to use a shared object.
* Note that we avoid using shared integers when maxmemory is used
* because every object needs to have a private LRU field for the LRU
* algorithm to work well. */
if ((server.maxmemory == 0 ||
!(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) &&
value >= 0 &&
value < OBJ_SHARED_INTEGERS) // 10000
{
decrRefCount(o);
incrRefCount(shared.integers[value]);
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);
}
}
}
对于大于0并且小于10000的数字:
set str 9999
redis 在启动的时候,会预先创建10000个 redisObject
存储 0-99999
,所以如果你设置的值在这个范围内,redis不需要再额外创建内存,只需要把引用加1就好。
对于其它的整型,则会创建一个redisObj,编码为int的redis对象并返回。
OBJ_ENCODING_EMBSTR
当字符串长度小于等于44个字节的时候,redis 将使用embstr
编码字符串,这个时候,生成的redisObj和sdshdr是连续内存。
OBJ_ENCODING_RAW
其它情况使用 raw
格式编码,注意这里要申请两次内存,因为sdshdr和redisObj不在连续的内存中。
Redis Lists
列表的底层数据结构采用 OBJ_ENCODING_QUICKLIST 编码。
quicklist 是redis3.2引入的数据结构,它由 List(链表)
和 ziplist
结合而成。
A doubly linked list of ziplists
在quicklist.c 第一行注释解释了quicklist 到底是什么东西,顾名思义:quicklist 是一个双向链表,链表中的每个节点都是一个ziplist结构。
quicklist可以看成是用双向链表将若干小型的ziplist连接到一起组成的一种数据结构。当ziplist节点个数过多,quicklist退化为双向链表,一个极端的情况就是每个ziplist节点只包含一个entry,即只有一个元素。当ziplist元素个数过少时,quicklist可退化为ziplist,一种极端的情况就是quicklist中只有一个ziplist节点。
Redis Hashes
散列表底层数据结构采用 ziplist
和 ht(hashtable)
两种中的一种。
当需要存储的key-value都满足下面两个条件时,采用 ziplist 作为底层存储,否则需要转为 hashtable 存储,需要注意的是,ziplist 的存储顺序和写入顺序是一致的,而hashtable则不是。
- key-value 结构的所有键值对的字符串长度都小于 hash-max-ziplist-value,默认值是64,可以通过配置文件进行修改
- 散列表对象保存的键值对个数(一个键值对记为一个)小于 hash-max-ziplist-entries,默认值是512,可以通过配置文件修改
当数据结构从ziplist转为散列表时,如果后面满足ziplist的条件,也不会再转换回来。
Redis Sets
无序集合,集合成员唯一。
set 的底层基于 ht
和 inset
两种数据结构中的一种。
整数集合(inset)是一个有序的、存储整型数据的结构。
当集合数量超过 set-max-inset-entries(默认是512),即使往集合里面继续放整型元素,集合也会将编码转换位hashtable。
Redis Sorted Sets
Zset 底层数据结构采用 ziplist
或者 skiplist
两种中的一种。
redis 的配置文件中关于有序集合的两个配置:
- zset-max-ziplist-entries 128:zset 采用压缩列表时,元素个数最大值位128
- zset-max-ziplist-value 64:zset 采用压缩列表时,元素最大长度不能超过64
在 t_zset.c
的 zaddGenericCommand 函数中,如果是第一次添加元素,会判断下面两个条件:
- zset-max-ziplist-entries 是否等于0
- 插入元素大小是否大于 zset-max-ziplist-value
if (zobj == NULL) {
if (xx) goto reply_to_client; /* No key + XX option: nothing to do. */
if (server.zset_max_ziplist_entries == 0 ||
server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr))
{
zobj = createZsetObject();
} else {
zobj = createZsetZiplistObject();
}
dbAdd(c->db,key,zobj);
}
如果满足其中一个,则zset使用 OBJ_ENCODING_SKIPLIST
编码,否则采用 OBJ_ENCODING_ZIPLIST
作为底层数据结构。
一般情况下,不会将zset-max-ziplist-entries配置成0,元素的字符串长度也不会太长,所以在创建有序集合时,默认使用压缩列表的底层实现。
zset新插入元素时,会判断以下两种条件:
- zset中元素个数大于zset_max_ziplist_entries
- 插入元素的字符串长度大于zset_max_ziplist_value
当满足任一条件时,Redis便会将zset的底层实现由压缩列表转为跳跃表。代码如下:
if (zzlLength(zobj->ptr) > server.zset_max_ziplist_entries || sdslen(ele) > server.zset_max_ziplist_value)
zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);
zset在转为跳表之后,即使元素被逐渐删除,也不会重新转为压缩列表。