为什么需要SDS
字符串数据类型作为redis最基础的数据类型之一在redis中使用的也是最频繁的数据类型.
因为redis只有一个线程在执行指令,如果某一个指令执行时间过长其他指令都会排队等候,又因为在c语言中获取字符串长度的时间复杂度为O(n),并且每次在字符串扩容或者缩容的时候,c语言中的字符串都需要重新分配内存,并且复制数组,这个两个操作是redis承受不起的,所以redis弃用了c语言中的字符串类型,自己定义了一种SDS(Simple Dynamic String)的数据结构用来存储字符串类型的数据.
SDS的结构
struct SDS<T> { T capacity; // 数组容量,包含了字符串的真实长度和预留出来执行append操作的空间 T len; // 数组长度,当前字符串的真实长度 byte flags; // 特殊标识位,标识当前的SDS为哪一种头部 byte[] content; // 数组内容,保存了真正的数据 }
len字段是为了在获取字符串长度的时候不对字符串进行遍历,所以SDS保存了字符串的长度len.这样在获取字符串长度的时候直接返回len即可.
capacity字段为该条字符串占用的长度字节数.在第一次分配内存的时候redis会为字符串分配len长度的数组,但是在执行一次append操作之后,redis会认为还有可能再次执行append操作,所以会预留出空间来防止每次进行append操作的时候都进行重新分配内容和拷贝字符串,redis默认的预留空间为原来的一倍,但是最大为1M的内存空间.如果字符串操过了1M,redis每次扩容最大也只会预留出1M的空间,防止内存浪费
flags字段是为了区分不同的SDS头部做的一个标识.redis为了节省内存空间,在保存不同长度的字符串的时候capacity和len使用的长度也是不同的,redis为此定义了很多不同的SDS头部.
// flags的低三位代表不同类型的sds头部: #define SDS_TYPE_5 0 //暂时不知道哪里使用 #define SDS_TYPE_8 1 //字符串长度使用一个字节标识 #define SDS_TYPE_16 2 //字符串长度使用两个字节标识 #define SDS_TYPE_32 3 //字符串长度使用三个字节标识 #define SDS_TYPE_64 4 //字符串长度使用四个字节标识 #define SDS_TYPE_MASK 7 //为了方便的获取字符串的类型,只需要flags字段和SDD_TYPE_MASK做位与操作就可以得到SDS头部的类型 #define SDS_TYPE_BITS 3 //因为SDS_TYPE_5类型的字符串的值是保存在flags中的,所以只需要flags字段向左移动SDS_TYPE_BITS位就可以得到字符串保存的值
embed or raw
在redis中为了减少内存分配的次数,redis把44(闭区间)以下长度的字符串使用embed格式存储,45长度以上的字符串使用raw格式存储,embed和raw格式的唯一区别就是,embed格式的时候字符串的SDS头部和字符串的值是保存在内存的一块连续的存储空间中的,redis只需要分配一次内存就可以,而raw格式中SDS的头部和字符串的内容是分开保存的,这样redis会分配两次空间.
为什么redis是以44个长度为分界点呢?这个需要从redisObject的结构说起.具体redisObject的结构可以查看redis数据结构(一).每个redisObject中都会占用16字节的长度,而SDS结构的大小为len(capacity)+len(len)+len(flags)+len(content),当字符串比较短时capacity和flags和len都只会占用一个字节的长度,所以一个sds占用的空间为16+3+capacity的长度.又因为redis分配内存的时候只会分配2,4,8,16,32,64等等,如果一个redisObject超过了64字节,redis就会认为这是一个大字符串,就会采用raw格式存储.这时使用embed存储的字符串的最大长度为64,减去固定的19个字节串为45个字节,在减去redis中字符串结尾固定的\0字符,所以embed总共可以存储的最大字节数为44