本文将从防止阻塞和内存节约两个方面介绍如和高效使用Reids。
欢迎关注公众号:闲余说
使用Redis时,我们需要结合具体业务和Redis特性两方面来考虑如何设计使用方案。需要两个从两个方面考虑:
下面,我们将就上面两个点展开说明如何高效合理使用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进程阻塞。
举例来说,对一个包含上十万甚至百万个field
的hash
执行hgetall
操作,hgetall
命令的时间复杂度为O(N),此时N页特别大(上十万甚至百万)必然耗时很长。
从这个例子,我们可以发现至少两个不合理的地方:
scan
之类命令,且确保每次获取元素数量在一定范围,比如50等。避免频繁生成RDB和AOF重写,尤其是高峰期。正常情况下,Redis比较时候缓存类型数据,当然为了保证数据不丢失,可以进行导出RDB和重新AOF。但需要确保一下几点:
save
等同步命令;如果必须频繁持久化,需要确保如下几点:
HugePage
,防止复制内存页过大而拖慢执行时间,且会导致持久化期间内存消耗增长避免单Redis实例负载过高。Redis是单线程服务,当负载过大必然影响整体性能,可以通过如下方案提高读写能力:
通常,引起服务的外因无外乎CPU、内存和网络,导致Redis阻塞的原因同样也需要从这几方面去考虑。
CPU竞争导致Redis阻塞的问题原因在阻塞章节已经详细介绍过,关于解决方案,可以通过以下手段来规避:
fork
出的子进程也共享该CPU。因此,如果需要频繁持久化的Redis不建议绑定CPU。减少内存碎片,正常的碎片率(mem_fragmentation_ratio)在1.03左右。但是当存储的数据长短差异较大时,以下场景容易出现高内存碎片问题:
append
、setrange
等更新操作。出现高内存碎片问题时常见的解决方式如下:
RDB生成和AOF重写会fork
子进程,进而导致内存消耗。总结如下:
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的数据,通过set
和append
操作实现的和直接使用set
实现多了近75% 的存储消耗。
字符串重构:指不一定把每份数据作为字符串整体存储,像json这样的数据可以使用hash结构,这样做有如下收益:
注意,这样样做的一个前提是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前缀。
内存是相对宝贵的资源,通过合理的优化可以有效地降低内存的使用量,内存优化的思路包括:
How Twitter Uses Redis To Scale - 105TB RAM, 39MM QPS, 10,000+ Instances