redis 相关优化
redis 占用的内存空间划分
- redis 进程自身占用的内存
- 存储用户数据的对象内存
- 各种的buffer 缓冲内存
- 内存碎片
- redis 默认无限使用服务器内存,如果不设置
maxmemory的话,会一直膨胀,直到内存耗尽。 - 如果设置了
maxmemory的话,在到达使用上限时,会触发溢出控制策略maxmemory-policy,默认是noeviction拒绝写入,并返回客户端OOM,但是不影响正常的读取。 - 当然还可以设置其他的
maxmemory-policy,按照规则删除一些键,腾出足够的空间。 - 当redis可用空间有限,而且业务逻辑允许键被删除的情况下,通过设置
非noeviction的溢出控制策略,肯定是最简单的做法。 - 下面讨论的都是在
noeviction情况下的一些优化策略 在这其中对象内存肯定是占用最多的,也是最应该优化的。其中进程自身占用的内存是优化空间最小的,不做讨论。
对象内存优化
- 控制键值对象的长度。关于key 在保证描述性的情况下,尽量往简单里写,能写成
uid的就不要写userId。关于value 如果我们存储的是json字符串,可以把字符串压缩一下再存储;如果是需要把对象序列化成二进制数组存储的,可以选择使用效率更高的序列化方式。 当然这种是对代码有侵入性的,而且在压缩、解压json 的过程中,也会对应一定的cpu消耗。 - 共享对象池。redis是支持共享对象池的,但是仅限于[0,9999] 之间的数字,所以value能用数字的尽量使用数字。为什么仅限[0,9999] 之间的是共享内存池的呢,首先是整数类型更容易比较,如果是字符串类型的话,每次比较消耗的时间复杂度是 O(n),如果引入字符串的共享对象池,意味着更复杂的处理逻辑和性能消耗,对于单线程模型的redis来说,可以但没必要。
- 字符串优化。redis 的字符串是使用SDS实现的,len、buf、free 三个字段,其中free是预分配的长度,可以在append操作时使用。当一次设置字符串的时候,free 默认是0,既没有预分配空间。如果频繁的 append、setrange 的话,会导致不断的扩容、收缩,可能会导致内存浪费和碎片化。
- 编码格式优化。其中ziplist是特殊编码的连续内存块,在 list,zset,hash 的使用时都可以使用ziplist 的编码方式,这一点通过修改redis 配置可以轻松实现。这里要注意的是,ziplist相关属性的设置不能无限膨胀,ziplist带来的内存占用小的优点,是通过增加cpu消耗,服务器响应耗时变长为代价的。
- 选择合适的数据结构,减少键的数量。redis提供了多种数据结构 list、set、zset、hash、bitmap,合理利用各种数据结构。拿hash来说,hash可以使用 ziplist优化,占用空间肯定比使用多个string更小,set还是可以使用 intset。还是可以考虑使用bitmap,bitmap也是通过SDS实现的。
- 设置键的过期时间。
buffer 缓存内存
- 客户端缓冲区。这里是指客户端通过tcp连接到redis服务器的输入、输出缓冲。
- 复制积压缓冲区。这里是用于部分复制的实现。
- aof缓存区。这里是用于实现aof重写的实现。
内存碎片
内存碎片无法避免,但是可以通过一些操作尽量降低碎片率。
- 尽量减少频率更新字符串SDS。
- 大量的键过期删除,释放的内存空间无法得到合理的利用也会导致高碎片率。
- 如果条件允许的话,可以重启一下节点,节点的重启可以做到内存碎片的重新整理。
子进程(bgSave)消耗
当redis 执行bgSave的时候,是通过fork一个子进程实现的,其中牵扯到 copy-on-write
。父子进程共享同一块内存,只在有数据有变动的时候才会对要修改的页复制出一个新的副本。 考虑如果在执行bgSave 的过程中,发生了大量的写请求,这时候就会有大量的复制操作。这个可以通过修改 overcommit_memory 和 THP 的配置来优化。
其他
当我们明确redis 的空间有限制时,就可以减少对redis 的使用,结合场景使用 cdn,Guava Cache等。