Redis底层数据结构之string

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis底层数据结构之string相关的知识,希望对你有一定的参考价值。

参考技术A

我们都知道, Redis 是由 C 语言编写的。在 C 语言中,字符串标准形式是以空字符 \\0 作为结束符的,但是 Redis 里面的字符串却没有直接沿用 C 语言的字符串。主要是因为 C 语言中获取字符串长度可以调用 strlen 这个标准函数,这个函数的时间复杂度是 O(N) ,由于 Redis 是单线程的,承受不了这个时间复杂度。

在上一篇文章中,我们介绍了 Redis 的 RedisObject 的数据结构,如下所示:

对于不同的对象, Redis 会使用不同的类型来存储。对于同一种类型 type 会有不同的存储形式 encoding 。对于 string 类型的字符串,其底层编码方式共有三种,分别为 int 、 embstr 和 raw 。

使用 object encoding key 可以查看 key 对应的 encoding 类型,如下所示:

对于 embstr 和 raw 这两种 encoding 类型,其存储方式还不太一样。对于 embstr 类型,它将 RedisObject 对象头和 SDS 对象在内存中地址是连在一起的,但对于 raw 类型,二者在内存地址不是连续的。

在介绍 string 类型的存储类型时,我们说到,对于 embstr 和 raw 两种类型其存储方式不一样,但 ptr 指针最后都指向一个 SDS 的结构。那什么是 SDS 呢? Redis 中的字符串称之为 Simple Dynamic String ,简称为 SDS 。与普通 C 语言的原始字符串结构相比, sds 多了一个 sdshdr 的头部信息, sdshdr 基本数据结构如下所示:

可以看出, SDS 的结构有点类似于 Java 中的 ArrayList 。 buf[] 表示真正存储的字符串内容, alloc 表示所分配的数组的长度, len 表示字符串的实际长度,并且由于 len 这个属性的存在, Redis 可以在 O(1) 的时间复杂度内获取数组长度。

为了追求对于内存的极致优化,对于不同长度的字符串, Redis 底层会采用不同的结构体来表示。在 Redis 中的 sds.h 源码中存在着五种 sdshdr ,分别如下:

上面说了, Redis 底层会根据字符串的长度来决定具体使用哪种类型的 sdshdr 。可以看出, sdshdr5 明显区别于其他四种结构,它一般只用于存储长度不会变化,且长度小于32个字符的字符串。但现在一般都不再使用该结构, 因为其结构没有 len 和 alloc 这两个属性,不具备动态扩容操作 ,一旦预分配的内存空间使用完,就需要重新分配内存并完成数据的复制和迁移,类似于 ArrayList 的扩容操作,这种操作对性能的影响很大。

上面介绍 sdshdr 属性的时候说过, flag 这个属性用于标识使用哪种 sdshdr 类型, flag 的低三位标识当前 sds 的类型,分别如下所示:

同时,注意到在每个 sdshdr 的头定义上都有一个 attribute((packed)) ,这个是为了告诉 gcc 取消优化对齐 ,这样,每个字段分配的内存地址就是 紧紧排列在一起的 , Redis 中字符串参数的传递直接使用 char* 指针,其实现原理在于,由于 sdshdr 内存分配禁止了优化对齐,所以 sds[-1] 指向的就是 flags 属性的内存地址,而通过 flags 属性又可以确定 sdshdr 的属性,进而可以读取头部字段确定 sds 的相关属性。

sds的逻辑图如下所示:

相比较于 C 语言原始的字符串, sdshdr 的具备一些优势。

由于 sdshdr 中存在 len 这个属性,所以可以在 O(1) 的时间复杂度下获得长度;而传统的 C 语言得使用 strlen 这个标准函数获取,时间复杂度为 O(N) 。

原始的 C 语言一直使用与长度匹配的内存,这样在追加字符串导致字符串长度发生变化时,就必须进行内存的重新分配。内存重新分配涉及到复杂算法和系统调用,耗费性能和时间。对于 Redis 来说,它是单线程的,如果使用原始的字符串结构,势必会引发频繁的内存重分配,这个显然是不合理的。

因而, sds 每次进行内存分配时,都会通过内存的预分配来减少因为修改字符串而引发的内存重分配次数。这个原理可以参数 Java 中的 ArrayList ,一般在使用 ArrayList 时都会建议使用带有容量的构造方式,这样可以避免频繁 resize 。

对于 SDS 来说,当其使用 append 进行字符串追加时,程序会用 alloc-len 比较下剩下的空余内存是否足够分配追加的内容 ,如果不够自然触发内存重分配,而如果剩余未使用内存空间足够放下,那么将直接进行分配,无需内存重分配。其扩容策略为, 当字符串占用大小小于1M时,每次分配为 len * 2,也就是保留100%的冗余;大于1M后,为了避免浪费,只多分配1M的空间。

通过这种预分配策略, SDS 将连续增长 N 次字符串所需的内存重分配次数 从必定 N 次降低为最多 N 次。

缓冲区溢出是指当某个数据超过了处理程序限制的范围时,程序出现的异常操作。 原始的 C 语言中,是由编码者自己来分配字符串的内存,当出现内存分配不足时就会发生 缓存区溢出 。而 sds 的修改函数在修改前会判断内存,动态的分配内存,杜绝了 缓冲区溢出 的可能性。

对于原始的 C 语言字符串来说,它会通过判断当前字符串中是否存在空字符 \\0 来确定是否已经是字符串的结尾。因而在某些情况下,如使用空格进行分割一段字符串时,或者是图片或者视频等二进制文件中存在 \\0 等,就会出问题。而 sds 不是通过空字符串来判断字符串是否已经到结尾,而是通过 len 这个字段的值。所以说, sds 还具备 二进制安全 这个特性,即可以安全的存储具有特殊格式的二进制数据。

https://www.cnblogs.com/reecelin/p/13358432.html

Redis数据结构之string类型和list类型

参考技术A         String是redis最基础和最常用的数据结构,其值最大能存储 512MB,可以是简单字符串、复杂的xml/json的字符串、二进制图像或者音频的字符串、以及可以是数字的字符串。String底层使用的是SDS,是Redis的一种基本数据结构,主要是用于存储字符串和整数。

    2.1  set命令  set key value

        用于设置给定key的值,如果key存储了其他值,覆盖写入,无视类型。

    2.2 get命令 get key

        获取指定key的值,如果key不存在返回nil

    2.3 getset命令 get key [value]

        该命令用于获取指定的key的旧值,然后按照新值对key进行赋值。当key中没有旧值的时候返回nil。

    2.4 mget命令 get key1 [key2 keyN]

        返回多个key的值,某个key不存在时返回nil

    2.5 decr命令 decr key

        对key对应的数字做减1操作。如果key不存在,那么在操作之前,这个key对应的值会被置为0。如果key有一个错误类型的value或者是一个不能表示成数字的字符串,就返回错误。

    2.6 incr命令 incr key

        对key对应的数字做减1操作。如果key不存在,那么在操作之前,这个key对应的值会被置为0。如果key有一个错误类型的value或者是一个不能表示成数字的字符串,就返回错误。

    2.7 append命令 append key value

    如果 key 已经存在,并且值为字符串,那么这个命令会把 value 追加到原来值(value)的结尾。 如果 key 不存在,那么它将首先创建一个空字符串的key,再执行追加操作,这种情况 APPEND 将类似于 SET 操作。返回append后字符串值(value)的长度。

    3.1 SDS动态字符串

        struct sdshdr

            unsigned int len;

            unsigned int free;

            char buf[];

        

        其中,buf表示数据空间,用于存储字符串;len表示buf中已占用的字节数;free表示空闲的字节数。

    3.2 新的SDS结构

        增加了一个flags来标识类型,用一个字节(8位)来存储,前3位表示字符串的类型;剩余5位,存储长度小于32的段字符串。

        创建 SDS 的大致流程是这样的:首先根据字符串长度计算得到 type,根据 type 计算头部所需长度,然后动态分配内存空间。

        注意:① 创建空字符串时,SDS_TYPE_5 被强制转换为 SDS_TYPE_8(原因是创建空字符串后,内容可能会频繁更新而引发扩容操作,故直接创建为 sdshdr8)

                    ②长度计算有 +1 操作,因为结束符 \0 会占用一个长度的空间。

                    ③返回的是指向 buf 的指针 s。

    4.1 session共享

    4.2 计数器(商品浏览记录)

     4.3 访问限速

    list类型用来存储多个有序的字符串,列表当中的每一个字符看做一个元素,一个列表当中可以存储有一个或者多个元素,redis的list支持存储2^32次方-1个元素。

    Redis可以从两端push和pop元素,支持读取指定范围或者制定下表的元素。list是一种灵活的链式结构,可以充当队列或者栈的角色。

    list的元素是有序的,且列表内的元素是可以重复的。

    注意:Redis3.2以前,列表底层的编码是ziplist(压缩列表)和linkedlist(双向列表)实现的,因为双线列表占用的内存比压缩列表多,所以当创建新的列表键时,列表会优先考虑用压缩列表,只有在需要的时候才会转换到双向列表实现。3.2以后重新引入了一个quicklist,列表底层都是有quicklist实现,quicklist是一个由ziplist组成的双向列表,每个节点使用ziplist来存储数据。

       2.1 Lpush命令 lpush key value

        将一个或多个值插入到列表头部。 如果 key 不存在,则创建list,然后再插入数据操作。 当 key 存在但不是列表类型时,返回一个错误。

    2.2 Rpush命令 rpush key value

        将一个或多个值从list的尾部插入

    2.3 Blpop命令 blpop key seconds

        Blpop是取出列表的第一个元素,如果list中没有元素则会一直等到到超时,或者发现有数据为止,seconds是指定多少秒返回。如没有数据,则返回nil。

        同理,Bropo为移除list列表的最后一个元素

    2.4 Linsert命令 linsert key before/after val1 val2

        在list列表的某一个元素前或者后插入另外一个元素。当指的的元素不存在时,不执行任何动作。如果列表不存在时,视为空列表,不执行任何动作。

    2.5 Lindex命令 lindex key index

        通过链表的下标获取列表中的元素,可以是-1表示链表最后一个元素,-2代表倒数第二个元素,没有返回nil

    2.6 Llen命令 llen key

        返回list的长度,如果list不存在,返回0

    2.7 Lrange命令

        返回指定list区间内的元素,区间以偏移量start和end决定。其中 0 表示列表的第一个元素, 1 表示列表的第二个元素,以此类推。 也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。

    5.1 队列秒杀抢购

        list类型的lpop和rpush(或者反过来,lpush和rpop)能实现队列的功能,故而可以用Redis的list类型实现简单的点对点的消息队列。不过不推荐在实战中这么使用,因为现在已经有Kafka、NSQ、RabbitMQ等成熟的消息队列了,它们的功能已经很完善了,除非是为了更深入地理解消息队列,不然没必要去重复造轮子。

    5.2 排行榜

        list类型的lrange命令可以分页查看队列中的数据。可将每隔一段时间计算一次的排行榜存储在list类型中。只有定时计算的排行榜才适合使用list类型存储,与定时计算的排行榜相对应的是实时计算的排行榜,list类型不能支持实时计算的排行榜。

以上是关于Redis底层数据结构之string的主要内容,如果未能解决你的问题,请参考以下文章

Redis数据结构之string类型和list类型

Redis之底层数据结构

深入探索Redis之底层数据结构

简介redis之集合类型数据

Redis 那么快之底层 ziplist 的奥秘!

《闲扯Redis四》List数据类型底层编码转换