Redis高级(十)、彻底掌握Redis基本数据类型及底层实现【上篇】SDS

82 阅读11分钟

觉得对你有益的小伙伴记得点个赞+关注

后续完整内容持续更新中

希望一起交流的欢迎发邮件至javalyhn@163.com

看到标题,不知道有没有小伙伴还不知道这三种数据类型,如果不知道,那你将会从这篇文章彻底理解。如果你有所了解或者了解很深的话,就当复习一遍加深印象吧,毕竟这个内容面试是重灾区。

1. Redis源码在哪看

image.png

image.png

Redis源码在Redis文件的src目录下

2. Redis源码怎么看

楼主,看源码怎么看?你这问题太傻了吧,一个一个看啊,全部看完我就是精通Redis了

首先你要是能全部看完并且理解的话,那我是真的没话说,你真的可以。

但是,我们做任何事情都应该有条理性,我们不妨将这些源码分类,会不会看起来更舒心一点呢

在此呢,楼主为了小伙伴们一看就懂,特地画了张图来让小伙伴们参考。

image.png

3. Redis是字典数据库,KV键值对到底是什么?

3.1 先从宏观视角看一下Reids

image.png 参照

redissrc.readthedocs.io/en/latest/d…

redissrc.readthedocs.io/en/latest/i…

  • Redis定义了redisObjec结构体,来表示string、hash、list、set、zset等数据类型

  • Redis中每个对象都是一个redisObject结构

3.2 Redis中的字典是什么

dict是一种用于维护key和value映射关系的数据结构,与很多编程语言中的Map类似。
为什么dict/map 这么受欢迎呢?
因为dict/map实现了key和value的映射,通过key查询value是效率非常高的操作,时间复杂度是O(C),C是常数,在没有冲突/碰撞的情况下,可以达到O(1)。

下面给出Redis中dict的结构体定义

/*
 * 字典
 *
 * 每个字典使用两个哈希表,用于实现渐进式 rehash
 */
typedef struct dict {

    // 特定于类型的处理函数
    dictType *type;

    // 类型处理函数的私有数据
    void *privdata;

    // 哈希表(2 个)
    dictht ht[2];

    // 记录 rehash 进度的标志,值为 -1 表示 rehash 未进行
    int rehashidx;

    // 当前正在运作的安全迭代器数量
    int iterators;

} dict;

typedef struct dictType {
    unsigned int (*hashFunction)(const void *key);
    void *(*keyDup)(void *privdata, const void *key);
    void *(*valDup)(void *privdata, const void *obj);
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    void (*keyDestructor)(void *privdata, void *key);
    void (*valDestructor)(void *privdata, void *obj);
} dictType;

3.3 Redis中的KV键值对到底是什么

redis 是 key-value 存储系统,其中key类型一般为字符串,value 类型则为redis对象(redisObject)

image.png

image.png

特别说明一下,别以为bitmap、byperloglog、geo底层是由什么新的数据类型组成的,本质上分别是string、string、zset

image.png

image.png

每个键值对都会有一个dictEntry

3.4 redisObject +Redis数据类型+Redis 所有编码方式(底层实现)三者之间的关系

image.png

4. 从set hello world入手

image.png

5. redisObject结构的作用

image.png

为了便于操作,Redis采用redisObject结构来统一五种不同的数据类型,这样所有的数据类型就都可以以相同的形式在函数间传递而不用使用特定的类型结构。同时,为了识别不同的数据类型,redisObjec中定义了type和encoding字段对不同的数据类型加以区别。简单地说,redisObject就是string、hash、list、set、zset的父类,可以在函数间传递时隐藏具体的类型信息,所以作者抽象了redisObject结构来到达同样的目的。

安特雷兹不服不行

5.1 redisObject各字段含义

image.png image.png

  1. 4位的type表示具体的数据类型

  2. 4位的encoding表示该类型的物理编码方式见下表,同一种数据类型可能有不同的编码方式。 (比如String就提供了3种:int embstr raw)

image.png

  1. lru字段表示当内存超限时采用LRU算法清除内存中的对象。

  2. refcount表示对象的引用计数。

  3. ptr指针指向真正的底层数据结构的指针。

5.2 通过set age 20 查看redisObject结构

image.png

字段含义
type类型
encoding编码,此处是数字类型
lru最近被访问的时间
refcount等于1,表示当前对象被引用的次数
ptrvalue值是多少,当前就是17

5.3 数据类型以及数据结构之间的关系

image.png

6. string数据结构介绍

6.1 3大编码格式

  1. int 保存long型(长整型)的64位(8个字节)有符号整数,最大值为9223372036854775807 有19位

image.png

注意,只有整数采用int,如果是浮点数,Redis内部其实会先将浮点数转换为字符串类型,然后再保存

  1. embstr (embedded string,表示嵌入式的String) 代表 embstr 格式的 SDS(Simple Dynamic String 简单动态字符串),保存长度小于44字节的字符串

  2. raw

保存长度大于44字节的字符串

6.2 3大编码案例

image.png

6.3 c语言中字符串的展现

image.png

Redis没有直接复用c语言的字符串,而是新建了属于自己的结构-----SDS

在Redis数据库里,包含字符串值的键值对都是由SDS实现的(Redis中所有的键都是由字符串对象实现的即底层是由SDS实现,Redis中所有的值对象中包含的字符串对象底层也是由SDS实现)。

image.png

6.4 sds(简单动态字符串)

image.png

image.png

Redis中字符串的实现,SDS有多种结构(sds.h):

image.png

sdshdr5、(2^5=32byte)

sdshdr8、(2 ^ 8=256byte)

sdshdr16、(2 ^ 16=65536byte=64KB)

sdshdr32、 (2 ^ 32byte=4GB)

sdshdr64,2的64次方byte=17179869184G

用于存储不同的长度的字符串。

image.png

  1. len 表示 SDS 的长度,使我们在获取字符串长度的时候可以在 O(1)情况下拿到,而不是像 C 那样需要遍历一遍字符串

  2. alloc 可以用来计算 free 就是字符串已经分配的未使用的空间,有了这个值就可以引入预分配空间的算法了,而不用去考虑内存分配的问题。

  3. flags表示sds的类型,该类型决定了 len 和 alloc所占用的大小。对于sdshdr5, head部分只有一个字节,其中高5位存储的是len,低三位是sdshdr5的类型。

  4. buf 表示字符串数组,存放真存数据的。

6.5 Redis为什么重新设计一个 SDS 数据结构

首先我们看c语言字符串的设计

image.png

c语言没有java里面的String类型,只能是靠自己的char[]实现。

字符串在c中的存储方式,想要获取例如Reids的字符长度,需要从头开始遍历,直到遇到'\0' 为止。

所以,Redis 没有直接使用 C 语言传统的字符串标识,而是自己构建了一种名为简单动态字符串 SDS(simple dynamic string)的抽象类型,并将 SDS 作为 Redis 的默认字符串。

用表格总结如下

csds
字符串长度处理需要从头开始遍历,直到遇到 '\0' 为止,时间复杂度O(N)记录当前字符串的长度,直接读取即可,时间复杂度 O(1)
内存重新分配分配内存空间超过后,会导致数组下标越级或者内存分配溢出空间预分配 : SDS 修改后,len 长度小于 1M,那么将会额外分配与 len 相同长度的未使用空间。如果修改后长度大于 1M,那么将分配1M的使用空间。 惰性空间释放 : 有空间分配对应的就有空间释放。SDS 缩短时并不会回收多余的内存空间,而是使用 free 字段将多出来的空间记录下来。如果后续有变更操作,直接使用 free 中记录的空间,减少了内存的分配。
二进制安全二进制数据并不是规则的字符串格式,可能会包含一些特殊的字符,比如 '\0' 等。前面提到过,C中字符串遇到 '\0' 会结束,那 '\0' 之后的数据就读取不上了根据 len 长度来判断字符串结束的,二进制安全的问题就解决了

6.6 源码分析之INT编码格式

命令:set k1 20230125

当字符串键值的内容可以用一个64位有符号整形来表示时,Redis会将键值转化为long型来进行存储,此时即对应 OBJ_ENCODING_INT 编码类型。内部的内存结构表示如下:

image.png

int编码的思想和java中有类似的地方,详情如下

Redis 启动时会预先建立 10000 个分别存储 0~9999 的 redisObject 变量作为共享对象,这就意味着如果 set字符串的键值在 0~10000 之间的话, 则可以 直接指向共享对象 而不需要再建立新对象,此时键值不占空间!

set k1 123

set k2 123

image.png

大家看到这应该都想到了java中的Integer缓冲区

image.png

private static class IntegerCache {
    static final int low = -128;
    static final int high;
    static final Integer cache[];

    static {
        // high value may be configured by property
        int h = 127;
        String integerCacheHighPropValue =
            sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
        if (integerCacheHighPropValue != null) {
            try {
                int i = parseInt(integerCacheHighPropValue);
                i = Math.max(i, 127);
                // Maximum array size is Integer.MAX_VALUE
                h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
            } catch( NumberFormatException nfe) {
                // If the property cannot be parsed into an int, ignore it.
            }
        }
        high = h;

        cache = new Integer[(high - low) + 1];
        int j = low;
        for(int k = 0; k < cache.length; k++)
            cache[k] = new Integer(j++);

        // range [-128, 127] must be interned (JLS7 5.1.7)
        assert IntegerCache.high >= 127;
    }

    private IntegerCache() {}
}

image.png 具体内容就不详细说明了,很简单的。

那么int编码在Reids源码的哪个位置呢 object.c

image.png

image.png

6.7 源码分析之EMBSTR编码格式

命令:set k1 abc

redis源代码:object.c

image.png

对于长度小于 44的字符串,Redis 对键值采用OBJ_ENCODING_EMBSTR 方式,EMBSTR 顾名思义即:embedded string,表示嵌入式的String。从内存结构上来讲 即字符串 sds结构体与其对应的 redisObject 对象分配在同一块连续的内存空间,字符串sds嵌入在redisObject对象之中一样。

image.png

image.png

用图可以清晰地理解,如下

image.png

目的就是为了 减少内存碎片

6.8 源码分析之RAW 编码格式

命令:set k1 大于44长度的一个字符串,随便写

image.png

当字符串的键值为长度大于44的超长字符串时,Redis 则会将键值的内部编码方式改为OBJ_ENCODING_RAW格式,这与OBJ_ENCODING_EMBSTR编码方式的不同之处在于,此时动态字符串sds的内存与其依赖的redisObject的内存不再连续了

用图可以清晰地理解,如下

image.png

6.9 明明没有超过阈值,为什么变成 raw 了

image.png

由此可见,当我们给编码为embstr的key修改后,编码格式转为了raw

解释如下

对于embstr,由于其实现是只读的,因此在对embstr对象进行修改时,都会先转化为raw再进行修改。因此只要是修改embstr对象,修改后的对象一定是raw的,无论是否达到了44个字节

6.10 string底层编码格式逻辑转化图

image.png

6.11 string编码格式案例总结

只有整数才会使用 int,如果是浮点数, Redis 内部其实先将浮点数转化为字符串值,然后再保存。

embstr 与 raw 类型底层的数据结构其实都是 SDS (简单动态字符串,Redis 内部定义 sdshdr 一种结构)。

编码格式特点
intLong类型整数时,RedisObject中的ptr指针直接赋值为整数数据,不再额外的指针再指向整数了,节省了指针的空间开销。
embstr当保存的是字符串数据且字符串小于等于44字节时,embstr类型将会调用内存分配函数,只分配一块连续的内存空间,空间中依次包含 redisObject 与 sdshdr 两个数据结构,让元数据、指针和SDS是一块连续的内存区域,这样就可以避免内存碎片
raw当字符串大于44字节时,SDS的数据量变多变大了,SDS和RedisObject布局分家各自过,会给SDS分配多的空间并用指针指向SDS结构,raw 类型将会调用两次内存分配函数,分配两块内存空间,一块用于包含 redisObject结构,而另一块用于包含 sdshdr 结构

image.png

Redis内部会根据用户给的不同键值而使用不同的编码格式,自适应地选择较优化的内部编码格式,而这一切对用户完全透明!