Redis 源码:图解 Redis 五种数据类型

String

StringRedis 中最常见的数据存储类型

  • Raw 基于动态字符串 SDS 实现,存储上限位 512mb(大于 44 字节使用)
  • 如果存储的 SDS 小于等于 44 字节,则采用 EMBSTR 编码,此时 Object HeadSDS 是一段连续空间,申请内容时,只需要调用一次内存分配函数,效率更高
  • 如果存储的字符串是整数值,并且大小在 LONG_MAX 范围内,则采用 INT 编码:直接将数据保存在 RedisObjectptr 指针位置(刚好 8 字节),不在需要 SDS

List

RedisList 类型可以从首、尾操作列表中的元素

  • List 底层的数据结构是 QuickListQuickList 是由 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

SetRedis 中的单列集合,满足下列特点:

  • 不保证有序性
  • 保证元素唯一性(可以判断元素是否存在)
  • 求交集、并集、差集

SetRedis 中的集合,不一定确保元素有序,可以满足元素唯一、查询效率要求极高:

  • 为了查询效率和唯一性,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,其中每一个元素都需要指定一个 scoremember

  1. 可以根据 score 值进行排序
  2. member 必须唯一
  3. 可以根据 member 查询分数

ZSet 底层数据结构必须满足兼键值存储键必须唯一可排序

  • ZSet 采用 SkipList + Dict 实现,SkipList 用来排序,Dict 用来存储 memberscore 的映射关系
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;
}

内存图如下:

此方案有一个缺点:内存占用太高了

所以当元素不多时,HTSkipList 优势不明显,而且更耗内存。因此 zset 还会采用 ZipList 结构来存储,来节省内存,不过需要同时满足两个条件:

  1. 元素数量小于 zset_max_ziplist_entries,默认是 128
  2. 每个元素数量都小于 zset_max_ziplist_value 字节,默认是 64

ZipList 本身没有排序功能,而且没有键值对的概念,因此需要有 zset 通过编码实现:

  • ZipList 是连续内存,因此 scoreelement 是紧挨在一起的两个 entryelement 在前,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,值是 scoreHash 的键和值都是任意值
  • ZSet 要根据 score 排序;Hash 不需要排序

因此 Hash 底层采用的编码 ZSet 也基本一致,只需要把排序有关的 SkipList 去掉即可:

  • Hash 结构默认采用 ZipList 编码,用来节省内存。ZipList 中相邻的两个 entry 分别保存 fieldvalue
  • 当数据量较大时,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;
}

更多文章

  1. Redis 基本命令
  2. Redis 源码:图解 Redis 六种数据结构
相关推荐
zpjing~.~32 分钟前
Mongo 分页判断是否有下一页
数据库
2401_8576009533 分钟前
技术与教育的融合:构建现代成绩管理系统
数据库·oracle
秋恬意1 小时前
Mybatis能执行一对一、一对多的关联查询吗?都有哪些实现方式,以及它们之间的区别
java·数据库·mybatis
潇湘秦1 小时前
一文了解Oracle数据库如何连接(1)
数据库·oracle
雅冰石1 小时前
oracle怎样使用logmnr恢复误删除的数据
数据库·oracle
web前端神器1 小时前
mongodb给不同的库设置不同的密码进行连接
数据库·mongodb
从以前1 小时前
Berlandesk 注册系统算法实现与解析
数据库·oracle
Muko_0x7d21 小时前
Mongodb
数据库·mongodb
Ren_xixi1 小时前
redis和mysql的区别
数据库·redis·mysql
m0_748233882 小时前
SQL语句整理五-StarRocks
数据库·sql