redis 如何做内存优化?
Posted 莫等、闲
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了redis 如何做内存优化?相关的知识,希望对你有一定的参考价值。
edis所有的数据都在内存中,而内存又是非常宝贵的资源。对于如何优化内存使用一直是Redis用户非常关注的问题。本文让我们深入到Redis细节中,学习内存优化的技巧。分为如下几个部分:
一.redisObject对象
二.缩减键值对象
三.共享对象池
四.字符串优化
五.编码优化
六.控制key的数量
一. redisObject对象
Redis存储的所有值对象在内部定义为redisObject结构体,内部结构如下图所示。
Redis存储的数据都使用redisObject来封装,包括string,hash,list,set,zset在内的所有数据类型。理解redisObject对内存优化非常有帮助,下面针对每个字段做详细说明:
1.type字段:
表示当前对象使用的数据类型,Redis主要支持5种数据类型:string,hash,list,set,zset。可以使用type {key}命令查看对象所属类型,type命令返回的是值对象类型,键都是string类型。
2.encoding字段:
表示Redis内部编码类型,encoding在Redis内部使用,代表当前对象内部采用哪种数据结构实现。理解Redis内部编码方式对于优化内存非常重要 ,同一个对象采用不同的编码实现内存占用存在明显差异,具体细节见之后编码优化部分。
3.lru字段:
记录对象最后一次被访问的时间,当配置了 maxmemory和maxmemory-policy=volatile-lru | allkeys-lru 时, 用于辅助LRU算法删除键数据。可以使用object idletime {key}命令在不更新lru字段情况下查看当前键的空闲时间。
开发提示:可以使用scan + object idletime 命令批量查询哪些键长时间未被访问,找出长时间不访问的键进行清理降低内存占用。
4.refcount字段:
记录当前对象被引用的次数,用于通过引用次数回收内存,当refcount=0时,可以安全回收当前对象空间。使用object refcount {key}获取当前对象引用。当对象为整数且范围在[0-9999]时,Redis可以使用共享对象的方式来节省内存。具体细节见之后共享对象池部分。
5. *ptr字段:
与对象的数据内容相关,如果是整数直接存储数据,否则表示指向数据的指针。Redis在3.0之后对值对象是字符串且长度<=39字节的数据,内部编码为embstr类型,字符串sds和redisObject一起分配,从而只要一次内存操作。
开发提示:高并发写入场景中,在条件允许的情况下建议字符串长度控制在39字节以内,减少创建redisObject内存分配次数从而提高性能。
二. 缩减键值对象
降低Redis内存使用最直接的方式就是缩减键(key)和值(value)的长度。
-
key长度:如在设计键时,在完整描述业务情况下,键值越短越好。
-
value长度:值对象缩减比较复杂,常见需求是把业务对象序列化成二进制数组放入Redis。首先应该在业务上精简业务对象,去掉不必要的属性避免存储无效数据。其次在序列化工具选择上,应该选择更高效的序列化工具来降低字节数组大小。以JAVA为例,内置的序列化方式无论从速度还是压缩比都不尽如人意,这时可以选择更高效的序列化工具,如: protostuff,kryo等,下图是JAVA常见序列化工具空间压缩对比。
其中java-built-in-serializer表示JAVA内置序列化方式,更多数据见jvm-serializers项目: https://github.com/eishay/jvm-serializers/wiki,其它语言也有各自对应的高效序列化工具。
值对象除了存储二进制数据之外,通常还会使用通用格式存储数据比如:json,xml等作为字符串存储在Redis中。这种方式优点是方便调试和跨语言,但是同样的数据相比字节数组所需的空间更大,在内存紧张的情况下,可以使用通用压缩算法压缩json,xml后再存入Redis,从而降低内存占用,例如使用GZIP压缩后的json可降低约60%的空间。
开发提示:当频繁压缩解压json等文本数据时,开发人员需要考虑压缩速度和计算开销成本,这里推荐使用google的Snappy压缩工具,在特定的压缩率情况下效率远远高于GZIP等传统压缩工具,且支持所有主流语言环境。
三. 共享对象池
对象共享池指Redis内部维护[0-9999]的整数对象池。创建大量的整数类型redisObject存在内存开销,每个redisObject内部结构至少占16字节,甚至超过了整数自身空间消耗。所以Redis内存维护一个[0-9999]的整数对象池,用于节约内存。 除了整数值对象,其他类型如list,hash,set,zset内部元素也可以使用整数对象池。因此开发中在满足需求的前提下,尽量使用整数对象以节省内存。
整数对象池在Redis中通过变量REDIS_SHARED_INTEGERS定义,不能通过配置修改。可以通过object refcount 命令查看对象引用数验证是否启用整数对象池技术,如下:
redis> set foo 100
OK
redis> object refcount foo
(integer) 2
redis> set bar 100
OK
redis> object refcount bar
(integer) 3
设置键foo等于100时,直接使用共享池内整数对象,因此引用数是2,再设置键bar等于100时,引用数又变为3,如下图所示。
使用整数对象池究竟能降低多少内存?让我们通过测试来对比对象池的内存优化效果,如下表所示。
操作说明 | 是否对象共享 | key大小 | value大小 | used_mem | used_memory_rss |
---|---|---|---|---|---|
插入200万 | 否 | 20字节 | [0-9999]整数 | 199.91MB | 205.28MB |
插入200万 | 是 | 20字节 | [0-9999]整数 | 138.87MB | 143.28MB |
注意本文所有测试环境都保持一致,信息如下:
服务器信息: cpu=Intel-Xeon E5606@2.13GHz memory=32GB
Redis版本:Redis server v=3.0.7 sha=00000000:0 malloc=jemalloc-3.6.0 bits=64
使用共享对象池后,相同的数据内存使用降低30%以上。可见当数据大量使用[0-9999]的整数时,共享对象池可以节约大量内存。需要注意的是对象池并不是只要存储[0-9999]的整数就可以工作。当设置maxmemory并启用LRU相关淘汰策略如:volatile-lru,allkeys-lru时,Redis禁止使用共享对象池,测试命令如下:
redis> set key:1 99
OK //设置key:1=99
redis> object refcount key:1
(integer) 2 //使用了对象共享,引用数为2
redis> config set maxmemory-policy volatile-lru
OK //开启LRU淘汰策略
redis> set key:2 99
OK //设置key:2=99
redis> object refcount key:2
(integer) 3 //使用了对象共享,引用数变为3
redis> config set maxmemory 1GB
OK //设置最大可用内存
redis> set key:3 99
OK //设置key:3=99
redis> object refcount key:3
(integer) 1 //未使用对象共享,引用数为1
redis> config set maxmemory-policy volatile-ttl
OK //设置非LRU淘汰策略
redis> set key:4 99
OK //设置key:4=99
redis> object refcount key:4
(integer) 4 //又可以使用对象共享,引用数变为4
为什么开启maxmemory和LRU淘汰策略后对象池无效?
LRU算法需要获取对象最后被访问时间,以便淘汰最长未访问数据,每个对象最后访问时间存储在redisObject对象的lru字段。对象共享意味着多个引用共享同一个redisObject,这时lru字段也会被共享,导致无法获取每个对象的最后访问时间。如果没有设置maxmemory,直到内存被用尽Redis也不会触发内存回收,所以共享对象池可以正常工作。
综上所述,共享对象池与maxmemory+LRU策略冲突,使用时需要注意。 对于ziplist编码的值对象,即使内部数据为整数也无法使用共享对象池,因为ziplist使用压缩且内存连续的结构,对象共享判断成本过高,ziplist编码细节后面内容详细说明。
为什么只有整数对象池?
首先整数对象池复用的几率最大,其次对象共享的一个关键操作就是判断相等性,Redis之所以只有整数对象池,是因为整数比较算法时间复杂度为O(1),只保留一万个整数为了防止对象池浪费。如果是字符串判断相等性,时间复杂度变为O(n),特别是长字符串更消耗性能(浮点数在Redis内部使用字符串存储)。对于更复杂的数据结构如hash,list等,相等性判断需要O(n 2 )。对于单线程的Redis来说,这样的开销显然不合理,因此Redis只保留整数共享对象池。
四. 字符串优化
字符串对象是Redis内部最常用的数据类型。所有的键都是字符串类型, 值对象数据除了整数之外都使用字符串存储。比如执行命令:lpush cache:type “redis” “memcache” “tair” “levelDB” ,Redis首先创建”cache:type”键字符串,然后创建链表对象,链表对象内再包含四个字符串对象,排除Redis内部用到的字符串对象之外至少创建5个字符串对象。可见字符串对象在Redis内部使用非常广泛,因此深刻理解Redis字符串对于内存优化非常有帮助:
1.字符串结构
Redis没有采用原生C语言的字符串类型而是自己实现了字符串结构,内部简单动态字符串(simple dynamic string),简称SDS。结构下图所示。
Redis自身实现的字符串结构有如下特点:
- O(1)时间复杂度获取:字符串长度,已用长度,未用长度。
- 可用于保存字节数组,支持安全的二进制数据存储。
- 内部实现空间预分配机制,降低内存再分配次数。
- 惰性删除机制,字符串缩减后的空间不释放,作为预分配空间保留。
2.预分配机制
因为字符串(SDS)存在预分配机制,日常开发中要小心预分配带来的内存浪费,例如下表的测试用例。
表:字符串内存预分配测试
阶段 | 数据量 | 操作说明 | 命令 | key大小 | value大小 | used_mem | used_memory_rss | mem_fragmentation_ratio |
---|---|---|---|---|---|---|---|---|
阶段1 | 200w | 新插入200w数据 | set | 20字节 | 60字节 | 321.98MB | 331.44MB | 1.02 |
阶段2 | 200w | 在阶段1上每个对象追加60字节数据 | append | 20字节 | 60字节 | 657.67MB | 752.80MB | 1.14 |
阶段3 | 200w | 重新插入200w数据 | set | 20字节 | 120字节 | 474.56MB | 482.45MB | 1.02 |
从测试数据可以看出,同样的数据追加后内存消耗非常严重,下面我们结合图来分析这一现象。阶段1每个字符串对象空间占用如下图所示。
阶段1插入新的字符串后,free字段保留空间为0,总占用空间=实际占用空间+1字节,最后1字节保存‘