String
String
是 Redis
中最常见的数据存储类型
Raw
基于动态字符串SDS
实现,存储上限位512mb
(大于44
字节使用)- 如果存储的
SDS
小于等于44
字节,则采用EMBSTR
编码,此时Object Head
与SDS
是一段连续空间,申请内容时,只需要调用一次内存分配函数,效率更高 - 如果存储的字符串是整数值,并且大小在
LONG_MAX
范围内,则采用INT
编码:直接将数据保存在RedisObject
的ptr
指针位置(刚好8
字节),不在需要SDS
了
List
Redis
的 List
类型可以从首、尾操作列表中的元素
List
底层的数据结构是QuickList
,QuickList
是由LinkedList + ZipList
组成的,可以双端访问,内存占用较低,存储上限高
c
// Generic 是通过的,也就是说从队首和队尾插入都是可以,是通过 where 参数来控制的
void pushGenericCommand(client *c, int where, int xx) {
int j;
// argv 用数组的形式存储命令,比如 lpush list one,argv 保存时 [lpush, list, one]
// j = 2 表示从第三个元素开始,因为第一个是 lpush,第二个是 list
// j < c->argc 表示遍历到最后一个元素
for (j = 2; j < c->argc; j++) {
// 判断元素大小,不能超过 LIST_MAX_ITEM_SIZE
if (sdslen(c->argv[j]->ptr) > LIST_MAX_ITEM_SIZE) {
addReplyError(c, "Element too large");
return;
}
}
// 尝试找到 key 对应的 list
// c->db,表示客户端要访问哪个数据库,
// c->argv[1],表示客户端要访问哪个 key
// 返回的是 robj,robj 是 RedisObject 的缩写
robj *lobj = lookupKeyWrite(c->db, c->argv[1]);
// 检查是不是 list 类型
if (checkType(c,lobj,OBJ_LIST)) return;
// 检查是否为空
if (!lobj) {
if (xx) {
addReply(c, shared.czero);
return;
}
// 为空,则创建新的 QuickList
lobj = createQuicklistObject();
// server.list_max_ziplist_size 用来限制每个 ziplist 的大小
// server.list_compress_depth 压缩深度
quicklistSetOptions(lobj->ptr, server.list_max_ziplist_size,
server.list_compress_depth);
dbAdd(c->db,c->argv[1],lobj);
}
for (j = 2; j < c->argc; j++) {
listTypePush(lobj,c->argv[j],where);
server.dirty++;
}
addReplyLongLong(c, listTypeLength(lobj));
char *event = (where == LIST_HEAD) ? "lpush" : "rpush";
signalModifiedKey(c,c->db,c->argv[1]);
notifyKeyspaceEvent(NOTIFY_LIST,event,c->argv[1],c->db->id);
}
c
robj *createQuicklistObject(void) {
// 申请内存并初始化 QuickList
quicklist *l = quicklistCreate();
// 创建 RedisObject,type 为 OBJ_LIST、ptr 指向 QuickList
robj *o = createObject(OBJ_LIST,l);
// 设置编码为 QuickList
o->encoding = OBJ_ENCODING_QUICKLIST;
return o;
}
内存图示意图:
Set
Set
是 Redis
中的单列集合,满足下列特点:
- 不保证有序性
- 保证元素唯一性(可以判断元素是否存在)
- 求交集、并集、差集
Set
是 Redis
中的集合,不一定确保元素有序,可以满足元素唯一、查询效率要求极高:
- 为了查询效率和唯一性,
Set
采用Dict
编码(HT
),Dict
中的key
用来存储元素,value
统一为null
- 当存储的所有数据都是整数,并且元素数量不超过
set-max-intset-entries
时,Set
采用IntSet
编码,以节省内存
c
// 创建 set 集合
robj *setTypeCreate(sds value) {
// 判断 value 是否是数值型 long long
if (isSdsRepresentableAsLongLong(value,NULL) == C_OK)
// 如果是数值类型,则采用 IntSet 编码
return createIntsetObject();
// 否则采用默认编码,也就是 HT
return createSetObject();
}
c
robj *createIntsetObject(void) {
// 初始化 IntSet 并申请内存空间
intset *is = intsetNew();
// 创建 RedisObject,type 为 OBJ_SET、ptr 指向 IntSet
robj *o = createObject(OBJ_SET,is);
// 设置编码为 IntSet
o->encoding = OBJ_ENCODING_INTSET;
return o;
}
c
robj *createSetObject(void) {
// 初始化 Dict 类型,并申请内存
dict *d = dictCreate(&setDictType,NULL);
// 创建 RedisObject,type 为 OBJ_SET、ptr 指向 Dict
robj *o = createObject(OBJ_SET,d);
// 设置编码为 HT
o->encoding = OBJ_ENCODING_HT;
return o;
}
c
// 接收两个值,一个是 set 集合,一个是要插入的值
int setTypeAdd(robj *subject, sds value) {
long long llval;
if (subject->encoding == OBJ_ENCODING_HT) { // 判断是不是 HT 编码,是的话直接插入
dict *ht = subject->ptr;
dictEntry *de = dictAddRaw(ht,value,NULL);
if (de) {
dictSetKey(ht,de,sdsdup(value));
dictSetVal(ht,de,NULL);
return 1;
}
} else if (subject->encoding == OBJ_ENCODING_INTSET) { // 目前是 INSET 编码
// 判断 value 是不是整数,是整数的话直接插入
if (isSdsRepresentableAsLongLong(value,&llval) == C_OK) {
uint8_t success = 0;
// 是整数,直接添加元素到 set
subject->ptr = intsetAdd(subject->ptr,llval,&success);
if (success) {
// 插入成功后,判断当前 entry 数量是否超过 set-max-intset-entries,超过了就转换为 HT 编码
// set-max-intset-entries 默认值为 512
size_t max_entries = server.set_max_intset_entries;
if (max_entries >= 1<<30) max_entries = 1<<30;
if (intsetLen(subject->ptr) > max_entries)
setTypeConvert(subject,OBJ_ENCODING_HT);
return 1;
}
} else { // 不是整数,转换为 HT 编码
setTypeConvert(subject,OBJ_ENCODING_HT);
// 类型转换后,将值插入
serverAssert(dictAdd(subject->ptr,sdsdup(value),NULL) == DICT_OK);
return 1;
}
} else {
serverPanic("Unknown set encoding");
}
return 0;
}
内存示意图如下:
ZSet
ZSet
也就是 SortedSet
,其中每一个元素都需要指定一个 score
和 member
- 可以根据
score
值进行排序 member
必须唯一- 可以根据
member
查询分数
ZSet
底层数据结构必须满足兼键值存储 、键必须唯一 、可排序
ZSet
采用SkipList
+Dict
实现,SkipList
用来排序,Dict
用来存储member
和score
的映射关系
c
// zet 结构
typedef struct zset {
// Dict 指针
dict *dict;
// SkipList 指针
zskiplist *zsl;
} zset;
c
robj *createZsetObject(void) {
// 申请内存空间
zset *zs = zmalloc(sizeof(*zs));
robj *o;
// 创建 dict
zs->dict = dictCreate(&zsetDictType,NULL);
// 创建 SkipList
zs->zsl = zslCreate();
// 创建 RedisObjet 对象
o = createObject(OBJ_ZSET,zs);
// 将编码设置为 SkipList
o->encoding = OBJ_ENCODING_SKIPLIST;
return o;
}
内存图如下:
此方案有一个缺点:内存占用太高了
所以当元素不多时,HT
和 SkipList
优势不明显,而且更耗内存。因此 zset
还会采用 ZipList
结构来存储,来节省内存,不过需要同时满足两个条件:
- 元素数量小于
zset_max_ziplist_entries
,默认是128
- 每个元素数量都小于
zset_max_ziplist_value
字节,默认是64
ZipList
本身没有排序功能,而且没有键值对的概念,因此需要有 zset
通过编码实现:
ZipList
是连续内存,因此score
和element
是紧挨在一起的两个entry
、element
在前,score
在后score
越小越接近队首,score
越大越接近队尾,按照score
值升序排列
c
// zset 添加元素时,先根据 key 找到 zset,不存在则创建新的 zset
zobj = lookupKeyWrite(c->db,key);
// 判断是否存在
if (checkType(c,zobj,OBJ_ZSET)) goto cleanup;
if (zobj == NULL) {
if (xx) goto reply_to_client;
// zset_max_ziplist_entries 设置为 0,表示禁用 ZipList,采用 SkipList
// 或者 value 大小超过 zset_max_ziplist_value,也禁用 ZipList,采用 SkipList
if (server.zset_max_ziplist_entries == 0 ||
server.zset_max_ziplist_value < sdslen(c->argv[scoreidx+1]->ptr))
{
zobj = createZsetObject();
} else { // 否则采用 ZpiList
zobj = createZsetZiplistObject();
}
dbAdd(c->db,key,zobj);
}
// 添加元素
zsetAdd(xxx)
c
robj *createZsetZiplistObject(void) {
// 创建 zset ziplist
unsigned char *zl = ziplistNew();
robj *o = createObject(OBJ_ZSET,zl);
o->encoding = OBJ_ENCODING_ZIPLIST;
return o;
}
c
int zsetAdd(robj *zobj, double score, sds ele, int in_flags, int *out_flags, double *newscore) {
// 判断编码方式
if (zobj->encoding == OBJ_ENCODING_ZIPLIST) { // 是 ZipList 编码
unsigned char *eptr;
// 判断当前元素是否已经存在,已经存在则更新 score 即可
if ((eptr = zzlFind(zobj->ptr,ele,&curscore)) != NULL) {
// ...
return 1;
} else if (!xx) {
// 元素不存在,需要新增,则判断 ziplist 长度有没有超、元素大小有没有超,总大小有没有超
if (zzlLength(zobj->ptr)+1 > server.zset_max_ziplist_entries ||
sdslen(ele) > server.zset_max_ziplist_value ||
!ziplistSafeToAdd(zobj->ptr, sdslen(ele)))
{
// 如果超出限制,则转换为 SkipList 编码
zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);
} else {
zobj->ptr = zzlInsert(zobj->ptr,ele,score);
if (newscore) *newscore = score;
*out_flags |= ZADD_OUT_ADDED;
return 1;
}
} else {
*out_flags |= ZADD_OUT_NOP;
return 1;
}
}
// 本身是 SkipList 编码,无需转换
if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
// ...
} else {
serverPanic("Unknown sorted set encoding");
}
return 0;
}
内存示意图如下:
Hash
Hash
结构与 ZSet
非常类似
- 都是键值存储
- 键唯一
- 根据键获取值
区别是:
ZSet
的键是member
,值是score
;Hash
的键和值都是任意值ZSet
要根据score
排序;Hash
不需要排序
因此 Hash
底层采用的编码 ZSet
也基本一致,只需要把排序有关的 SkipList
去掉即可:
Hash
结构默认采用ZipList
编码,用来节省内存。ZipList
中相邻的两个entry
分别保存field
和value
- 当数据量较大时,
Hash
结构会转为HT
编码,也就是Dict
,触发条件有两个:ZipList
中的元素数量超过了hash-max-ziplist-entries
,默认是512
个ZipList
的任意entry
大小超过了hash-max-ziplist-value
,默认是64
字节
内存图:
c
// 处理 hset 命令
void hsetCommand(client *c) {
int i, created = 0;
robj *o;
// 判断 hash 的 key 是否存在,不存在则创建一个新的,默认采用 ZipList 编码
if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
// 判断是否需要把 ZipList 转为 Dict
hashTypeTryConversion(o,c->argv,2,c->argc-1);
// 循环遍历每一对 field 和 value,并执行 hset 命令
for (i = 2; i < c->argc; i += 2)
created += !hashTypeSet(o,c->argv[i]->ptr,c->argv[i+1]->ptr,HASH_SET_COPY);
}
c
robj *hashTypeLookupWriteOrCreate(client *c, robj *key) {
// 查找 key
robj *o = lookupKeyWrite(c->db,key);
if (checkType(c,o,OBJ_HASH)) return NULL;
// 不存在则创建新的
if (o == NULL) {
o = createHashObject();
dbAdd(c->db,key,o);
}
return o;
}
c
robj *createHashObject(void) {
// 默认采用 ZipList 编码,申请 ZipList 内存空间
unsigned char *zl = ziplistNew();
robj *o = createObject(OBJ_HASH, zl);
// 设置编码
o->encoding = OBJ_ENCODING_ZIPLIST;
return o;
}
c
void hashTypeTryConversion(robj *o, robj **argv, int start, int end) {
int i;
size_t sum = 0;
// 判断编码,如果不是 ZipList 则直接返回
if (o->encoding != OBJ_ENCODING_ZIPLIST) return;
// 依次遍历命令中的 field、value
for (i = start; i <= end; i++) {
if (!sdsEncodedObject(argv[i]))
continue;
size_t len = sdslen(argv[i]->ptr);
// 如果 field 或 value 的长度超过了 hash-max-ziplist-value,则转换为 HT 编码
if (len > server.hash_max_ziplist_value) {
hashTypeConvert(o, OBJ_ENCODING_HT);
return;
}
sum += len;
}
// 如果 ZipList 大小超过 1G,也转为 HT 编码
if (!ziplistSafeToAdd(o->ptr, sum))
hashTypeConvert(o, OBJ_ENCODING_HT);
}
c
int hashTypeSet(robj *o, sds field, sds value, int flags) {
int update = 0;
// 判断是否为 ZipList 编码
if (o->encoding == OBJ_ENCODING_ZIPLIST) {
unsigned char *zl, *fptr, *vptr;
zl = o->ptr;
// 查询 head 指针
fptr = ziplistIndex(zl, ZIPLIST_HEAD);
// head 不为空,说明 ZipList 不为空,开始查找 key
if (fptr != NULL) {
fptr = ziplistFind(zl, fptr, (unsigned char*)field, sdslen(field), 1);
// 判断是否存在,如果已经存在则更新
if (fptr != NULL) {
vptr = ziplistNext(zl, fptr);
serverAssert(vptr != NULL);
update = 1;
zl = ziplistReplace(zl, vptr, (unsigned char*)value,
sdslen(value));
}
}
// 不存在则直接 push
if (!update) {
// 依次 push 新的 field 和 value 到 ZipList 的尾部
zl = ziplistPush(zl, (unsigned char*)field, sdslen(field),
ZIPLIST_TAIL);
zl = ziplistPush(zl, (unsigned char*)value, sdslen(value),
ZIPLIST_TAIL);
}
o->ptr = zl;
// 插入新元素,检查 list 长度是否超出,超出则转换为 HT 编码
if (hashTypeLength(o) > server.hash_max_ziplist_entries)
hashTypeConvert(o, OBJ_ENCODING_HT);
} else if (o->encoding == OBJ_ENCODING_HT) {
// HT 编码,直接插入或覆盖
} else {
serverPanic("Unknown hash encoding");
}
return update;
}