highlight: atom-one-dark theme: fancy
前言
通过前面的学习我们了解了Redis6.0中典型的数据结构实现, 我们都知道Redis常用的数据类型有五种, 分别为String, Set, Zset, Hash, List。这五种类型的的底层都使用了那些编码结构来实现呢?在前期的【Redis源码系列】Redis6.0 DB结构以及渐进式rehash超详细源码解读中我们分析了redisDB
结构的dictRntry
中存储了redisObject
字段, 本期我们来详细分析redisObject
对象的编码结构。
前期回顾 【Redis源码系列】Redis6.0数据结构详解--ziplist篇 【Redis源码系列】Redis6.0数据结构详解--skiplist篇 【Redis源码系列】Redis6.0数据结构详解--sds篇
解析redisObject
redisObject
结构体定义在src/server.h
下, 具体结构图下:
-
type字段: 表示当前对象存储的数据类型, 即我们常见的String, Hash等, 具体的枚举值如下: Define | value | 含义 | | ---------- | - | ----------- | | OBJ_STRING | 0 | 字符串 | | OBJ_LIST | 1 | 列表 | | OBJ_SET | 2 | 集合 | | OBJ_ZSET | 3 | 有序集 | | OBJ_HASH | 4 | 哈希表 | | OBJ_MODULE | 5 | 模块(R4.0后可以通过外部模块对Redis进行功能性扩展) | | OBJ_STREAM | 6 | 流(R5.0新增用于消息队列)
-
encoding字段: 用于标识当前数据结构的编码方式, 具体枚举值如下: Defaine | value
| ----------------------- | -- | | OBJ_ENCODING_RAW | 0 | | OBJ_ENCODING_INT | 1 | | OBJ_ENCODING_HT | 2 | | OBJ_ENCODING_ZIPMAP | 3 | | OBJ_ENCODING_LINKEDLIST | 4 | | OBJ_ENCODING_ZIPLIST | 5 | | OBJ_ENCODING_INTSET | 6 | | OBJ_ENCODING_SKIPLIST | 7 | | OBJ_ENCODING_EMBSTR | 8 | | OBJ_ENCODING_QUICKLIST | 9 | | OBJ_ENCODING_STREAM | 10 | -
lru字段: 当淘汰策略是lru时记录相对于lru_clock全局时钟的操作时间, 当lfu时记录操作频率。
解析encoding编码
字符串(String)
字符串使用三种编码方式来存储OBJ_ENCODING_RAW
, OBJ_ENCODING_INT
,OBJ_ENCODING_EMBSTR
, 当我们执行set
命令时, 首先调用src/t_string.c
的setCommand
方法, 进行参数解析, 并且创建redis对象, 逻辑如下:
void setCommand(client *c) {
int j;
robj *expire = NULL;
int unit = UNIT_SECONDS;
int flags = OBJ_SET_NO_FLAGS;
for (j = 3; j < c->argc; j++) {
/*** 省略参数解析 ***/
}
// 创建redisObject对象
c->argv[2] = tryObjectEncoding(c->argv[2]);
setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}
在创建redisObject时会调用tryObjectEncoding
方法, 设置不同的对象编码格式, 逻辑如下:
robj *tryObjectEncoding(robj *o) {
long value;
sds s = o->ptr;
size_t len;
/* 断言确保数据类型时字符串 */
serverAssertWithInfo(NULL,o,o->type == OBJ_STRING);
if (!sdsEncodedObject(o)) return o;
/* 如果当前对象已被引用, 证明已经设置完成 */
if (o->refcount > 1) return o;
/* 确认当前字符串是否可以被设置为长整数编码, 当字符串长度超过20时, 不能被表示为int32 or int64 编码, string2l表示当前字符串可以转换为整形 */
len = sdslen(s);
if (len <= 20 && string2l(s,len,&value)) {
/* 当服务没有设置最大内存限制或者maxmemory-policy设置的淘汰算法中不计算LRU值时, 0 到 10000 之间OBJ_ENCODING_INT编码的字符串对象将进行共享*/
if ((server.maxmemory == 0 ||
!(server.maxmemory_policy & MAXMEMORY_FLAG_NO_SHARED_INTEGERS)) &&
value >= 0 &&
value < OBJ_SHARED_INTEGERS)
{
decrRefCount(o);
incrRefCount(shared.integers[value]);
return shared.integers[value];
} else {
// 如果是 RAW 或 EMBSTR 编码, 则转换为 INT编码
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);
}
}
}
/* 如果长度小于44则尝试将编码转换为 EMB */
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;
}
// 不能在进行编码, 尝试清除多余的空间
trimStringObjectIfNeeded(o);
return o;
}
总结如下:
- 如果值可以转换为整形, 则优先使用OBJ_ENCODING_INT进行编码
- 如果值的长度小于44, 则转换为OBJ_ENCODING_EMBSTR编码
- 默认使用OBJ_ENCODING_RAW进行编码
列表(List)
Redis6.0列表使用OBJ_ENCODING_QUICKLIST
编码, 不同于3.0版本使用OBJ_ENCODING_ZIPLIST
,OBJ_ENCODING_QUICKLIST
两种编码方式来进行存储,
这是需要大家注意的一点。 在6.0.0版本的lPush
指令中只会调用createQuicklistObject
创建QuickList编码的对象:
// Redis6.0.0版本
void pushGenericCommand(client *c, int where) {
/**** 省略 ****/
for (j = 2; j < c->argc; j++) {
if (!lobj) {
// 创建Quicklist对象
lobj = createQuicklistObject();
quicklistSetOptions(lobj->ptr, server.list_max_ziplist_size,
server.list_compress_depth);
dbAdd(c->db,c->argv[1],lobj);
}
listTypePush(lobj,c->argv[j],where);
pushed++;
}
/**** 省略 ****/
}
// Redis3.0.0版本
void pushGenericCommand(redisClient *c, int where) {
/**** 省略 ****/
for (j = 2; j < c->argc; j++) {
c->argv[j] = tryObjectEncoding(c->argv[j]);
if (!lobj) {
// 创建ziplist对象
lobj = createZiplistObject();
dbAdd(c->db,c->argv[1],lobj);
}
listTypePush(lobj,c->argv[j],where);
pushed++;
}
/**** 省略 ****/
}
同时经过仔细的分析, listTypeConvert
方法的enc
参数(编码方式), 也只有一种, 并且createZiplistObject
方法已经不再被调用。
集合(Set)
集合使用REDIS_ENCODING_INTSET
, REDIS_ENCODING_HT
两种编码方式来存储, 在执行SADD命令时, 会首先检查元素是否可以用整数标识, 如果可以, 则使用REDIS_ENCODING_INTSET
编码, 否则使用REDIS_ENCODING_HT
编码创建对象。
// sadd命令入口函数
void saddCommand(client *c) {
robj *set;
int j, added = 0;
set = lookupKeyWrite(c->db,c->argv[1]);
if (set == NULL) {
// 创建集合对象
set = setTypeCreate(c->argv[2]->ptr);
dbAdd(c->db,c->argv[1],set);
}
/*** 省略 ***/
}
// 创建集合对象方法:setTypeCreate
// 当对象具有整数可编码值时,将返回intset。否则,使用一个常规的哈希表。
robj *setTypeCreate(sds value) {
if (isSdsRepresentableAsLongLong(value,NULL) == C_OK)
return createIntsetObject();
return createSetObject();
}
// 创建一个普通hash编码的集合对象
robj *createSetObject(void) {
dict *d = dictCreate(&setDictType,NULL);
robj *o = createObject(OBJ_SET,d);
o->encoding = OBJ_ENCODING_HT;
return o;
}
// 创建一个整形编码的集合对象
robj *createIntsetObject(void) {
intset *is = intsetNew();
robj *o = createObject(OBJ_SET,is);
o->encoding = OBJ_ENCODING_INTSET;
return o;
}
有序集合(Zset)
有序集合使用OBJ_ENCODING_SKIPLIST
,OBJ_ENCODING_ZIPLIST
两种编码结构来保存对象, 通过两项服务配置来控制转换规则:
//仅当排序集的长度和元素低于以下限制时,才使用此编码, 后面数字为默认值, 可在redis.conf中配置
zset-max-ziplist-entries 128
zset-max-ziplist-value 64
当我们执行zadd添加一个有序几何元素时, 会根据上面配置的条件做判断创建何种类型的对象:
void zaddGenericCommand(client *c, int flags) {
/*** 省略 ***/
zobj = lookupKeyWrite(c->db,key);
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))
{
// 创建skiplist编码对象
zobj = createZsetObject();
} else {
// 创建ziplist编码对象
zobj = createZsetZiplistObject();
}
dbAdd(c->db,key,zobj);
}
/*** 省略 ***/
哈希(Hash)
哈希使用REDIS_ENCODING_ZIPLIST
, OBJ_ENCODING_HT
两种编码方式存储, 与有序集合类似, 通过redis.conf配置文件中的参数来控制转换规则, 具体配置如下:
hash-max-ziplist-entries 512
当执行hset命令时, 逻辑如下:
// 执行hset命令逻辑
void hsetCommand(client *c) {
int i, created = 0;
robj *o;
/*** 省略 ***/
// 创建对象, 使用 ziplist编码
if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
hashTypeTryConversion(o,c->argv,2,c->argc-1);
// 循环设置hash参数
for (i = 2; i < c->argc; i += 2)
created += !hashTypeSet(o,c->argv[i]->ptr,c->argv[i+1]->ptr,HASH_SET_COPY);
/*** 省略 ***/
}
// 设置元素类型
int hashTypeSet(robj *o, sds field, sds value, int flags) {
int update = 0;
if (o->encoding == OBJ_ENCODING_ZIPLIST) {
/*** 省略 ***/
// 如果当前编码类型是 OBJ_ENCODING_ZIPLIST, 并且当前对象元素数量 > hash_max_ziplist_entries 配置, 则转换为 OBJ_ENCODING_HT 编码
if (hashTypeLength(o) > server.hash_max_ziplist_entries)
hashTypeConvert(o, OBJ_ENCODING_HT);
} else if (o->encoding == OBJ_ENCODING_HT) {
/*** 省略 ***/
} else {
serverPanic("Unknown hash encoding");
}
/*** 省略 ***/
return update;
}
总结
本文我们分了Redis6.0中的各种数据类型对应的编码格式, 基于源码分析, 并未过多深入讨论这样设计的原因以及优缺点。其中需要注意的是List编码方式和老版本实现是不一样的, 目前能够查看到的很多资料都未描述到这一点。本文总结如下:
数据类型 编码方式 转换规则 REDIS_STRING REDIS_ENCODING_INT 如果长度小于20并且可以value可以转换为整形 REDIS_ENCODING_EMBSTR 如果长度小于44, 使用此编码 REDIS_ENCODING_RAW 默认编码 REDIS_LIST OBJ_ENCODING_QUICKLIST 默认编码, 6.0.0版本无其他编码 REDIS_HASH REDIS_ENCODING_ZIPLIST 初始化默认编码 REDIS_ENCODING_HT 当元素超过配置: hash-max-ziplist-entries时使用此编码, 默认值是512 REDIS_SET REDIS_ENCODING_INTSET 如果member可以转换为整形, 则使用REDIS_ENCODING_INTSET编码, 否则使用REDIS_ENCODING_HT编码 REDIS_ENCODING_HT REDIS_ZSET REDIS_ENCODING_ZIPLIST 如果 zset-max-ziplist-entries 配置为0并且, 元素长度小于 zset-max-ziplist-value 配置(默认64), 则使用REDIS_ENCODING_SKIPLIST编码, 否则使用REDIS_ENCODING_ZIPLIST编码 REDIS_ENCODING_SKIPLIST觉得对大家理解有帮助帮忙点个赞吧 :)