Redis从精通到入门——数据类型String实现源码详解

String的实现

String类型在日常工作中大家用到的是最多的,但是我们的使用只是最表层皮毛。Redis内部做了极多的优化调整,大幅度节省内存使用、增加查询效率。我认为作为开发人员是有必要了解并学习其设计理念与实现方案。

sds源码阅读

  • Redis 3.2之前,只有一个sds类型3.2之前的版本现在用的太少了,接下来所有内容均为3.2及以上版本
struct sdshdr {
    int len;//字符串已使用长度(实际长度)
    int free;//剩余可使用长度(len+free = 实际分配的内存空间)
    char buf[];//字符串数组
};
  • Redis 3.2及之后,sds扩展了5种类型分别存放不同长度的字符串。在创建字符串时,sdsReqType 方法会根据字符串的长度选择不同的类型
/* 根据当前字符串长度,获取当前字符串需要使用哪种sds类型存储 */
static inline char sdsReqType(size_t string_size) {
    if (string_size < 32) //2^5 -1 
        return SDS_TYPE_5;
    if (string_size < 0xff) //2^8 -1  
        return SDS_TYPE_8;
    if (string_size < 0xffff) // 2^16 -1  
        return SDS_TYPE_16;
    if (string_size < 0xffffffff)  // 2^32 -1 
        return SDS_TYPE_32;
    return SDS_TYPE_64;
}

/* sdshdr5 逻辑上用于存储小于32位的字符串
 * 但是在sdsnewlen方法(创建字符串的方法)中存在如下判断if (type == SDS_TYPE_5 && initlen == 0) type = SDS_TYPE_8;
 * 并且博主验证好像value值长度不为0且小于32也不会使用sdshdr5 而是使用sdshdr8(如有错误希望大佬们多多指正)
 * 但是redis的Key长度 如果小于32位是会使用sdshdr5 这个类型的
*/
struct __attribute__ ((__packed__)) sdshdr5 而是使用 {
    unsigned char flags; // 标志位,低三位表示类型(sdshdr5、sdshdr8、sdshdr16、......),其余五位未使用
    char buf[];// 用于存储字符串的char数组
};
struct __attribute__ ((__packed__)) sdshdr8 {
    uint8_t len; // 实际使用的长度(当前字符串长度)
    uint8_t alloc; // 分配的长度,不包括标头和空终止符
    unsigned char flags; // 标志位,低三位表示类型(sdshdr5、sdshdr8、sdshdr16、......),其余五位未使用
    char buf[];// 用于存储字符串的char数组
};
struct __attribute__ ((__packed__)) sdshdr16 {
    uint8_t len; // 实际使用的长度(当前字符串长度)
    uint8_t alloc; // 分配的长度,不包括标头和空终止符
    unsigned char flags; // 标志位,低三位表示类型(sdshdr5、sdshdr8、sdshdr16、......),其余五位未使用
    char buf[];// 用于存储字符串的char数组
};
struct __attribute__ ((__packed__)) sdshdr32 {
    uint8_t len; // 实际使用的长度(当前字符串长度)
    uint8_t alloc; // 分配的长度,不包括标头和空终止符
    unsigned char flags; // 标志位,低三位表示类型(sdshdr5、sdshdr8、sdshdr16、......),其余五位未使用
    char buf[];// 用于存储字符串的char数组
};
struct __attribute__ ((__packed__)) sdshdr64 {
    uint8_t len; // 实际使用的长度(当前字符串长度)
    uint8_t alloc; // 分配的长度,不包括标头和空终止符
    unsigned char flags; // 标志位,低三位表示类型(sdshdr5、sdshdr8、sdshdr16、......),其余五位未使用
    char buf[];// 用于存储字符串的char数组
};

sds设计优势

  • Redis是通过C语言实现,C语言中String是不保存长度的,而是通过 \0 来标记字符串结束。通过len保存长度而不直接使用C语言的实现让Redis对其它语言的兼容性得到提升但是为了兼容C语言的函数调用字符串数组依然使用了\0结尾
  • len 字段的存在,让字符串长度获取 (STRLEN)的时间复杂度降低至 O(1)不需要遍历数组
  • alloc 字段的存在,让字符串的扩容变得更加简单和高效Redis字符串的扩容为成倍扩容,但是当字符串在长度超过1024 * 1024 (1MB)之后每次扩容只会多分配 1MB的空间
  • 成倍扩容 通过“空间预分配” 和“惰性空间释放”,防止多次重分配内存。redis的String对象直接通过 set 等方法创建时,不会创建冗余空间,因为大多数情况下创建的String类型不需要进行追加截取等操作

redisObject对象

redisObject 是Redis类型系统的核心, 数据库中的每个键、值, 以及Redis本身处理的参数, 都表示为这种数据类型.

redisObject源码阅读

//  redisObject对象 :  string , list ,set ,hash ,zset ...
// 总空间(8 bit = 1 byte):  4 bit + 4 bit + 24 bit + 4 byte + 8 byte = 16 byte  
typedef struct redisObject {
    unsigned type:4;		//  对象的数据类型 4 bit
    unsigned encoding:4;	//  对象的编码类型 4 bit,分别为OBJ_STRING、OBJ_ENCODING_INT或OBJ_ENCODING_EMBSTR
    unsigned lru:LRU_BITS;	//	最近一次的访问时间	24 bit ,用于LRU算法
    int refcount;           //	引用计数			4 byte  
    void *ptr;              //	指向底层数据实现的指针	8 byte
} robj;

String的对象编码

int类型(REDIS_ENCODING_INT)

大家日常的Key Value 设值,表面上Redis给的都是String类型,特别是像我们在JAVA应用中使用 Jedis 的set、get命令对Value的 入参/出参 都是String类型,但是大家都知道内存中整数类型的存储空间使用是更小的,redis底层实现也有考虑到针对int类型的存储优化。

/* 尝试对字符串对象进行编码以节省空间 */
robj *tryObjectEncoding(robj *o) {
    //我会用...省略掉许多其他的代码
    ...
    //获取字符串长度
    len = sdslen(s);

    //字符串长度范围是否在整数值的表示范围内, 0 - 2^64,最多不超过20位 string2l方法是转换整数的方法
    if (len <= 20 && string2l(s,len,&value)) {
        //没有设置内存淘汰策略,且数字范围在缓存整数的范围内
        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 {
            // 如果前一步不能使用共享小对象来表示,那么将原来的robj编码成encoding = OBJ_ENCODING_INT,这时ptr字段直接存成这个long型的值。
            // 因此在64位机器上有64位宽度,正好能存储一个64位的整数值。 所以此刻ptr不需要存指针而是直接存整数值即可
            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);
            }
        }
    }

   ...
    return o;
}

int类型的存在直接使用 redisObjectptr 来存储值,可以 减少一次IO ,同时也 节省了内存空间

  • 通过object encoding命令 可以Redis查询指定Key的内部编码格式
    在这里插入图片描述

embstr类型(REDIS_ENCODING_EMBSTR)

也是String类型,Redis 在保存长度小于 44 字节的字符串时会采用 embstr 编码方式,主要是因为现代处理器在读写内存时,为了提高DDR内存的读写效率,通常会连续读写一串内存 。同时每次读写都要填满一个 Cache Line ,所以每次内存读写都需要读写 64字节 (现在的一个 Cache Line 大小,很早以前是32字节)

/* 尝试对字符串对象进行编码以节省空间 */
robj *tryObjectEncoding(robj *o) {
    //我会用...省略掉许多其他的代码
    ...
    //获取字符串长度
    len = sdslen(s);

     // 数据长度 小于 OBJ_ENCODING_EMBSTR_SIZE_LIMIT 44  的话, 用 embstr 进行编码
    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;
    }

   ...
    return o;
}

已知处理器每次读取内存为 64 byte ,redisObject占用 16 byte ,sdshdr 基础信息(len、alloc、flags)占用 3 byte ,为兼容C语言 buf[] 数组用 \0 结尾占用 1 byte,64 - 16 - 3 - 1 = 44。所以当字符串长度 <= 44 时,直接申请一块连续的内存空间,存放 redisObject 与 sdshdr 。创建时少分配一次空间,删除时少释放一次空间。

embstr

  • 通过object encoding命令 可以Redis查询指定Key的内部编码格式
    在这里插入图片描述

raw类型(REDIS_ENCODING_RAW)

用于存储长度超过44位的字符串

raw

  • 通过object encoding命令 可以Redis查询指定Key的内部编码格式
    在这里插入图片描述

Redis 命令参考手册Redis常用命令传送门

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

zhibo_lv

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值