字符串是我们平时接触频率最高的一个基础类型,但就是这么一个平平无奇的基本类型,在Redis里面也是经历了各种各样的优化,来优化它对内存的占用,了解这部分内容,与其说是“学习Redis”,不如说是“向Redis学习”,学习Redis从各个可能的角度,来优化内存使用的方法和不放过任何一个可能的内存优化项的态度。
Redis的字符串叫“SDS”,也就是Simple Dynamic String。它的结构是一个带长度信息的字节数组。
struct SDS<T> {T capacity; // 数组容量T len; // 数组长度byte flags; // 特殊标志位,不用理睬它byte[] content; // 数组内容
}
上面的SDS结构使用了泛型T。为什么不直接用int呢?因为当字符串比较短时,len和capacity可以使用byte和short来表示,Redis为了对内存做极致的优化,不同长度的字符串使用不同的结构体来表示。
Redis的字符串有两种存储方式,在长度特别短时,使用embstr形式存储,而当长度超过44字节时,使用raw形式存储。
为了解释这种现象,我们首先来了解一下Redis对象头结构,所有的Redis对象都有下面的这个头结构。
struct RedisObject {int4 type; // 4 bitsint4 encoding; // 4 bitsint24 lru; // 24 bitsint32 refcount; // 4 bytesvoid *ptr; // 8 bytes, 64-bit system
}
不同的对象具有不同的类型type(4bit)。同一个类型的type会有不同的存储形式encoding(4bit)。为了记录对象的LRU信息,使用了24个bit来记录LRU信息。每个对象都有个引用计数,当引用计数为0时,对象就会被销毁,内存被回收。ptr指针将指向对象内容(body)的具体存储位置。这样一个RedisObject对象头结构需要占据16字节的存储空间。
接着我们再看SDS结构体的大小,在字符串比较小时,SDS对象头结构的大小是capacity+3
,至少是3字节。意味着分配一个字符串的最小空间占用为19(即16+3)字节。
struct SDS {int8 capacity; // 1 byteint8 len; // 1 byteint8 flags; // 1 bytebyte[] content; // 内联数组,长度为capacity
}
embstr将RedisObject对象头结构和SDS对象连续存在一起,使用malloc方法一次分配,而raw存储形式不一样,它需要两次malloc方法,两个对象头在内存地址上一般是不连续的。
内存分配器jemalloc、tcmalloc等分配内存大小的单位都是2/4/8/16/32/64字节等,为了能容纳一个完整的embstr对象,jemalloc最少会分配32字节的空间,如果字符串再稍微长一点,那就是64自己的空间。如果字符串总体超出了64字节,Redis认为它是一个大字符串,不再适合使用embstr存储,而该使用raw形式。
当内存分配了64字节空间时,那这个字符串长度最大可以是多少呢?这个长度就是44字节。
为什么是44字节呢?64字节中,除了RedisObject的16字节和SDS的3字节,留给content的长度最多只有45(即64 - 19)自己饿了。字符串又是以NULL结尾,所以embstr形式最大能容纳的字符串长度就是44字节。
扩容策略:
在字符串长度小于1MB之前,扩容空间采用加倍策略,也就是保留100%的冗余空间。当字符串长度超过1MB之后,为了避免加倍后的冗余空间过大而导致浪费,每次扩容只会多分配1MB大小的冗余空间。