本文将从防止阻塞和内存节约两个方面介绍如和高效使用Reids。


使用Redis时,我们需要结合具体业务和Redis特性两方面来考虑如何设计使用方案。需要两个从两个方面考虑:

  1. 防止阻塞
  2. 节约内存

下面,我们将就上面两个点展开说明如何高效合理使用Redis。

防止阻塞

阻塞章节我们知道,引起Redis阻塞可能的原因有内因外因两方面。

内因规避

减少复杂命令的使用,或者有节制的使用。下面这些命令可以看做复杂命令(时间复杂度为O(N)或者更高):SETRANGE, GETRANGE, MSET, MGET, MSETNX, HMSET, HMGET, HKEYS,HVALS, HGETALL, HSCAN, LTRIM, LINDEX, SMEMBERS, SUNION, SUNIONSTORE, SDIFF, SDIFFSTORE, ZUNIONSTORE, ZINTERSTORE, SINTER, SINTERSTORE。这些命令当操作的key或者field过多时将会导致Redis进程阻塞。
举例来说,对一个包含上十万甚至百万个fieldhash执行hgetall操作,hgetall命令的时间复杂度为O(N),此时N页特别大(上十万甚至百万)必然耗时很长。
从这个例子,我们可以发现至少两个不合理的地方:

  1. 这种有大量元素的数据不应该存在,因为,我们并不能确定什么时候我们对它执行了复杂命令。
  2. 如果真的不可规避超多元素的情况,在获取多个元素或者全量元素时,务必使用scan之类命令,且确保每次获取元素数量在一定范围,比如50等。

避免频繁生成RDB和AOF重写,尤其是高峰期。正常情况下,Redis比较时候缓存类型数据,当然为了保证数据不丢失,可以进行导出RDB和重新AOF。但需要确保一下几点:

  1. 不要执行save等同步命令;
  2. 尽量不要在高峰期进行持久化操作;
  3. 尽量在从实例上做持久化操作;

如果必须频繁持久化,需要确保如下几点:

  1. 保证CPU、内存充足,建议CPU和内存留出一定的buffer
  2. 不要绑定CPU
  3. 避免和CPU密集型服务混布
  4. 如果多个Redis实例部署在同一台机器,注意规划好系统资源,可以考虑错峰持久化,避免同时持久化导致系统资源开销瞬间突增
  5. 系统尽量不要开启HugePage,防止复制内存页过大而拖慢执行时间,且会导致持久化期间内存消耗增长

避免单Redis实例负载过高。Redis是单线程服务,当负载过大必然影响整体性能,可以通过如下方案提高读写能力:

  1. 可以通过读写分离,从实例承接部分读请求,来降低主实例压力;
  2. 如果读写压力都很大的话,需要考虑集群方案。

外因规避

通常,引起服务的外因无外乎CPU、内存和网络,导致Redis阻塞的原因同样也需要从这几方面去考虑。
CPU竞争导致Redis阻塞的问题原因在阻塞章节已经详细介绍过,关于解决方案,可以通过以下手段来规避:

  1. 进程CPU资源竞争,建议不要和其他多线程CPU密集型服务混布,尤其是线上环境。另外,如果流量趋势有波动的服务,比如有早晚高峰,建议不要把流量波动一致的服务混布。
  2. 绑定CPU,绑定CPU(设置CPU亲和力affinity)是为了降低Redis进程在不同CPU来回切换导致缓存命中率下降等引起的性能问题,但是,进程的CPU亲和力会继承给子进程,Redis进程fork出的子进程也共享该CPU。因此,如果需要频繁持久化的Redis不建议绑定CPU。

节约内存

系统优化

减少内存碎片,正常的碎片率(mem_fragmentation_ratio)在1.03左右。但是当存储的数据长短差异较大时,以下场景容易出现高内存碎片问题:

  1. 频繁做更新操作,例如频繁对已存在的键执行appendsetrange等更新操作。
  2. 大量过期键删除,键对象过期删除后,释放的空间无法得到充分利用,导致碎片率上升。

出现高内存碎片问题时常见的解决方式如下:

  1. 数据对齐:在条件允许的情况下尽量做数据对齐,比如数据尽量采用数字类型或者固定长度字符串等,但是这要视具体的业务而定,有些场景无法做到。
  2. 安全重启:重启节点可以做到内存碎片重新整理,因此可以利用高可用架构,如Sentinel或Cluster,将碎片率过高的主节点转换为从节点,进行安全重启。

RDB生成和AOF重写会fork子进程,进而导致内存消耗。总结如下:

  1. 正常情况下Redis产生的子进程并不需要消耗1倍的父进程内存,实际消耗根据期间写入命令量决定,但是依然要预留出一些内存防止溢出。
  2. 需要设置sysctl vm.overcommit_memory=1允许内核可以分配所有的物理内存,防止Redis进程执行fork时因系统剩余内存不足而失败。
  3. 排查当前系统是否支持并开启THP,如果开启建议关闭,防止copy-onwrite期间内存过度消耗。

用户优化

减小键值字符串长度

  1. key可以通过字符串缩减来减少长度
  2. value可以通过序列化和压缩来减少存储,也可以可以通过业务侧优化减少不必要的字段

尽量使用set而非append

因为字符串(SDS)存在预分配机制,日常开发中要小心预分配带来的内存浪费。

表-2 set & append 对比测试

操作

数据量

key大小

value大小

used_memory_human

used_memory_rss_human

mem_fragmentation_ratio

说明

set

100w

20B

100B

176.66M

180.19M

1.02


set

100w

20B

200B

283.47M

287.66M

1.01

set && append

100w

20B

100B+100B

497.10M

503.19M

1.01

先set,value大小为100B,随后append大小100B的数据

从上面的实验可以看出,同样存储100w条key大小为20B,value大小为200B的数据,通过setappend操作实现的和直接使用set实现多了近75% 的存储消耗。

字符串重构

字符串重构:指不一定把每份数据作为字符串整体存储,像json这样的数据可以使用hash结构,这样做有如下收益:

  1. 使用二级结构存储也能帮我们节省内存。
  2. 同时可以使用hmget、hmset命令支持字段的部分读取修改,而不用每次整体存取。

注意,这样样做的一个前提是json key-value对中value相对较小,下面是一个测试例子。

{
"id" : "12345678",
"title" : "redis-memory-optimization",
"chinese_url" : "http://www.redis.cn/topics/memory-optimization.html",
"english_url" : "https://redis.io/topics/memory-optimization"
}

代码-2 一个json实例

表-3 hash优化测试

数据量

数据结构

编码

key

value

配置

used_memory_human

used_memory_rss_human

mem_fragmentation_ratio

说明

100w

string

raw

20B

json字符串

默认

252.95M

258.04M

1.02

100w

hash

hashtbale

20B

key-value

hash-max-ziplist-value 50

474.21M

484.27M

1.02

100w

hash

ziplist

20B

key-value

hash-max-ziplist-value 64

252.95M

258.09M

1.02

根据测试结构,hash-max-ziplist-value 50配置下使用hash类型,内存消耗不但没有降低反而比字符串存储多出2倍,而调整hash-max-ziplist-value 64之后内存降低为252.95M。因为json的chinese_url属性长度是51,调整配置后hash类型内部编码方式变为ziplist,相比字符串在内存使用上至少持平且支持属性的部分操作。
intset编码:intset编码是集合(set)类型编码的一种,内部表现为存储有序、不重复的整数集。当集合只包含整数且长度不超过set-max-intset-entries配置时被启用。

typedef struct intset {
uint32_t encoding;
uint32_t length;
int8_t contents[];
} intset;

代码-3 intset结构

encoding:整数表示类型,根据集合内最长整数值确定类型,整数类型划分为三种:int-16、int-32、int-64。intset保存的整数类型根据长度划分,当保存的整数超出当前类型时,将会触发自动升级操作且升级后不再做回退。升级操作将会导致重新申请内存空间,把原有数据按转换类型后拷贝到新数组。
使用intset编码的集合时,尽量保持整数范围一致,如都在int-16范围内。防止个别大整数触发集合升级操作,产生内存浪费。

控制键的数量

通过在客户端预估键规模,把大量键分组映射到多个hash结构中降低键的数量。简单的说就是复用key前缀

总结

内存是相对宝贵的资源,通过合理的优化可以有效地降低内存的使用量,内存优化的思路包括:

  1. 精简键值对大小,键值字面量精简,使用高效二进制序列化工具。
  2. 使用对象共享池优化小整数对象。
  3. 数据优先使用整数,比字符串类型更节省空间。
  4. 优化字符串使用,避免预分配造成的内存浪费。
  5. 使用ziplist压缩编码优化hash、list等结构,注重效率和空间的平衡。
  6. 使用intset编码优化整数集合。
  7. 使用ziplist编码的hash结构降低小对象链规模。

reference

Redis官网

Redis开发与运维

How Twitter Uses Redis To Scale - 105TB RAM, 39MM QPS, 10,000+ Instances

Latency Numbers Every Programmer Should Know