内存优化
我们都知道 Redis 的数据都存储在内存中,而内存又是非常宝贵的资源,本文将讲解如何进行内存优化。
redisObject 对象
首先需要了解什么是 redisObject,在 Redis 中存储的所有值对象在内部都被定义为redisObject,结构如下。
type
:表示当前对象使用的数据类型,主要就是 string、hash、list、set、zset 五种。4 表示占 4 个 bit 位。:使用 type [key] 命令可以查看对象的所属类型,返回的是值对象的类型,键都为 string 类型。
encoding
:表示内部编码的类型,代表当前对象使用哪种数据结构实现。理解内部编码类型对于内存优化非常重要。lru
:记录对象最后一次被访问的时间,当配置了 maxmemory 和 maxmemory-policy=volatile-lru 或者 allkeys-lru 时,用于辅助 LRU 算法删除键数据。:使用 object idletime [key] 在不更新 lru 字段时间的情况下查看当前键的空闲时间。
使用 scan + object idletime 命令可以批量查询出那些键长时间未被访问并进行清理,降低内存占用。
refcount
:记录当前对象被引用的次数,当 refcount=0 的时候可以安全的进行对象空间回收。:使用 object recount [key] 获取当前对象引用。当对象值在[0-9999]的时候,redis 会使用共享对象池来节省内存。
* ptr
:如果存的是整数,则直接存储数据,否则存储指向数据的指针。当值对象为字符串并且<=44 字节时候,内部编码为 embstr 类型,当>44 字节时候,使用 raw 类型。
缩减键值对象
缩减 key 和 value 的长度可以有效减少 Redis 内存使用情况,比如将 key 进行缩写等。
value 比较复杂,如果是将业务对象进行序列化为二进制数组,可以去掉不必要的属性,其次在序列化工具上可以选择更高效的如:protostuff、kryo 等。除了二进制数组外,我们也会存入 json、xml 等字符串,在内存紧张情况下,我们可以使用压缩算法来压缩 json、xml 再存入 redis。
共享对象池
共享对象池指的是在 Redis 中维护了[0-9999]的整数对象池。因为创建大量的整数类型的 redisObject 存在内存开销,一个 redisObject 内部至少占用 16 字节,所以 Redis 维护了整数对象池来节约内存。另外,list、hash、set、zset 中也是可以使用共享对象池的。
如上图,可以看到当值为 100 时,refcount 是 2147483647,其实就是 INT_MAX, 这是一个共享对象。而 12000 的引用计数是 1,是一个新创建的对象。
此时的 redisObject 如下:
:需要注意的是,在设置了 maxmemory 和 LRU 相关淘汰策略入:volatile-lru,allkeys-lru 时,Redis 此时会禁用共享对象池。
LRU 算法需要获取对象最近一次访问的时间,但共享对象池可能存在多个引用同时指向同一个 redisObject,这时 lru 字段也会被共享,导致无法获取每个对象的最后一次访问时间。但如果没有设置 maxmemory 的话,直到内存用完之前都不会触发回收机制,所以共享对象池可以一直使用。
:另外需要注意的是,如果内部编码使用的是 ziplist 的值对象,即使所有数据为整数也无法使用共享对象池。因为 ziplist 使用压缩且内存连续的结构,对象判断成本过高。
字符串优化
在 Redis 中最常见的就是字符串,所有的键都是字符串类型,值对象数据类型除了整数就是字符串类型。所以如何进行字符串优化也是重点之一。
首先我们先来了解字符串结构。
字符串结构
redis 并没有使用 C 语言中的字符串,而是自己实现了字符串结构。
简单动态字符串(simple dynamic string)SDS
struct sdshdr{
//字节数组
char buf[];
//buf数组中已使用字节数量
int len;
//buf数组中未使用字节数量
int free;
}
SDS 的优点:
- O(1)的时间复杂度获取字符串长度,已使用长度,未使用长度。
- 可以保存字节数组,支持安全的二进制数据存储。
- 内部实现空间预分配机制,降低内存再分配次数。
- 惰性删除机制,在字符串缩减后空间不立即释放,作为预分配空间保留。
PS:有关字符串 SDS 的相关内容可以看之前的文章。
:需要注意的是,字符串使用预分配机制是为了防止频繁修改字符串内容导致频繁地进行重分配内存和字符串拷贝。所以需要尽可能减少修改,比如 append,setrange。可以改为直接使用 set 修改字符串,降低分配带来的内存浪费和内存碎片化。
字符串重构
如果保存的是 json 数据,可以使用 hash 结构来进行存储,使用 hmget,hmset 进行批量获取和修改。
如果存在长字符串的情况下进行测试性能:
需要注意的是如果值对象字符串长度大于65则Redis会使用hashtable编码方式,反而会消耗更多内存。
通过调整hash-max-ziplist-value=xx
设置一个合适的值,则会使用 ziplist 编码方式,会更节省内存。
编码优化
了解编码
Redis 提供了 string、list、hash、set、zet 等类型。但对每种类型存在不同编码的概念,其实就是具体底层使用了哪种数据结构。不同的编码会直接影响内存占用和读写效率。
使用 object encoding [key]命令来获取编码类型。
> object encoding jack
int
> hset hello student jack
1
> object encoding hello
ziplist
...
类型 | 编码方式 | 数据结构 |
---|---|---|
string | raw | 动态字符串 |
string | embstr | 优化内存分配的字符串 |
string | int | 整数 |
hash | hashtable | 散列表 |
hash | ziplist | 压缩列表 |
list | linkedlist | 双向链表 |
list | ziplist | 压缩列表 |
list | quicklist | 快速列表 |
set | hashtable | 散列表 |
set | intset | 整数集合 |
zset | skiplist | 跳跃表 |
zset | ziplist | 压缩列表 |
:编码类型在 Redis 写入数据时自动完成,只能从小内存编码转换为大内存编码,过程是不可逆的。
接下来我们看一下转换条件是什么样的。
类型 | 编码 | 转换条件 |
---|---|---|
string | embstr | value 字节长度 <= 44 |
string | raw | value 字节长度 > 44 |
string | int | 整数 |
hash | ziplist | value 字节长度<=hash-max-ziplist-value 并且 field 个数<=hash-max-ziplist-entries |
hash | hashtable | value 字节长度>hash-max-ziplist-value 或者 field 个数>hash-max-ziplist-entries |
list | ziplist | value 字节长度<=list-max-ziplist-value 并且链表长度<=list-max-ziplist-entries |
list | linkedlist | value 字节长度>list-max-ziplist-value 或者链表长度>list-max-ziplist-entries |
list | quicklist | 废弃上述 list-max-ziplist-value、list-max-ziplist-entries 配置。使用:list-max-ziplist-size 表示最大压缩空间或长度。最大空间使用[-5~1]范围配置,默认-2 表示 8KB。正整数表示最大压缩长度。list-compress-depth:表示最大压缩深度,默认 0 不压缩 |
set | intset | 元素均为整数并且集合长度<=hash-max-ziplist-entries |
set | hashtable | 元素非整数或者集合长度>hash-max-ziplist-entries |
zset | ziplist | value 字节长度<=zset-max-ziplist-value 并且集合长度<=zset-max-ziplist-entries |
zset | skiplist | value 字节长度>zset-max-ziplist-value 或者集合长度>zset-max-ziplist-entries |
ziplist 编码
本文着重介绍一下 ziplist,ziplist 中所有数据都是采用线性连续存储的内存结构,可以作为 list、hash、zset 底层数据结构实现。
zlbytes
:记录整个压缩列表所占用的字节长度。类型是 int-32,长度为 4 字节。zltail
:记录距离尾结点的偏移量,方便尾节点弹出操作。类型是 int-32,长度为 4 字节。zllen
:记录压缩链表节点数量。entry
:记录具体的节点。prev_entry_bytes_length
:记录前一个节点所占空间,用于快速定位上一个节点,也可以实现列表反向迭代。encoding
:标示当前节点编码和长度,前两位标示编码类型:字符串/整数,其余位表示数据长度。contents
:保存节点的值。zlend
:记录列表结尾,占用一个字符。
特点:
- 内部为数据紧凑排列的一块连续内存数组。
- 可以模拟双向链表结构,O(1)时间复杂度入队和出队。
- ziplist 在空间利用率上极高,每个 entry 最多只有 6 字节的浪费。
- ziplist 底层结构无链表,通过内存偏移量获取 next 或 last 节点位置
- ziplist 在插入和删除的时候有很大的概率出现连锁更新,因此在使用时尽量保证所存储的 value 位数相同,否则最坏会出现 O(n^2)的时间复杂度。
总结
- Redis 内存消耗主要在于:键值对象、缓冲区内存。
- 通过
maxmemory
来控制 Redis 最大可用内存,当超出设置的内存大小后,根据maxmemory-policy
控制内存回收策略。 - 使用共享对象池优化小整数对象。
- 优先使用整数,比字符串更节省空间。
- 优化字符串使用,避免预分配造成的内存浪费。
- 使用 ziplist 压缩编码优化 hash、list、zset 结构。
- 使用 intset 编码优化整数集合。