Redis使用技巧及最佳实践
Redis最佳实践
Redis键值设计
优雅的key结构
定义Key时, 最好遵循以下约定:
遵守基本格式:
[业务名称]:[数据名]:[数据id]如
login:user:10优点在于可读性强, 避免key冲突, 方便管理
长度不超过44字节
节省内存:
key是string类型, 底层编码包括int、embstr和raw三种. embstr在小于44字节使用, 采用连续内存空间, 内存占用更少

不使用特殊字符
拒绝BigKey
Redis中单个Key支持512M大小的数据存储, 但BigKey数据对系统性能有很大印象, 应尽量避免

危害:
网络阻塞
对BigKey执行读请求时,少量的QPS就可能导致带宽使用率被占满,导致Redis实例,乃至所在物理机变慢
数据倾斜
BigKey所在的Redis实例内存使用率远超其他实例,无法使数据分片的内存资源达到均衡
Redis操作阻塞
对元素较多的hash、list、zset等做运算会耗时较旧,使主线程被阻塞
CPU压力
对BigKey的数据序列化和反序列化会导致CPU的使用率飙升,影响Redis实例和本机其它应用
判断BigKey
BigKey通常以Key的大小和Key中成员的数量来综合判定,例如:
Key本身的数据量过大:一个String类型的Key,它的值为5 MB。
Key中的成员数过多:一个ZSET类型的Key,它的成员数量为10,000个。
Key中成员的数据量过大:一个Hash类型的Key,它的成员数量虽然只有1,000个但这些成员的Value(值)总大小为100 MB。
一般推荐, 单个key的value小于10KB. 对于集合类型的key,建议元素数量小于1000
发现BigKey
redis-cli --bigkeys利用redis-cli提供的
--bigkeys参数,可以遍历分析所有key,并返回Key的整体统计信息与每个数据的Top1的big key
scan扫描
自己编程,利用scan扫描Redis中的所有key,利用strlen、hlen等命令判断key的长度(此处不建议使用MEMORY USAGE)

处理BigKey
拆分
删除
BigKey内存占用较多,即便时删除这样的key也需要耗费很长时间,导致Redis主线程阻塞,引发一系列问题。
version 3.0及以前
如果是集合类型,则遍历BigKey的元素,先逐个删除子元素,最后删除BigKey


version 4.0之后
Redis在4.0后提供了异步删除的命令:unlink


恰当的数据类型
1.存储用户信息, 存在三种方式:
方式一:json字符串
key value user:1 {“name”: “Jack”, “age”: 21} 优点:实现简单粗暴
缺点:数据耦合,不够灵活
方式二:字段打散
key value user:1:name Jack user:1:age 21 优点:可以灵活访问对象任意字段
缺点:占用空间大、没办法做统一控制
方式三:hash (适合)
key field value user:1 name Jack user:1 age 21 优点:底层使用ziplist,空间占用小,可以灵活访问对象的任意字段
缺点:代码相对复杂
2.hash类型的key,其中有100万对field和value,field是自增id,这个key存在什么问题?如何优化?
| Key | field | value |
|---|---|---|
| id:0 | value0 | |
| somekey | ….. | ….. |
| id:999999 | value999999 |
存在的问题:
①hash的entry(field-value对)数量超过500时,会使用哈希表而不是ZipList,内存占用较多。
②可以通过
hash-max-ziplist-entries配置entry上限。但是如果entry过多就会导致BigKey问题处理方案:
拆分为小的hash,将
id / 100作为key, 将id % 100作为field,这样每100个元素为一个HashKey field value id:00 value0 key:0 ….. ….. id:99 value99 Key field value id:00 value0 key:1 ….. ….. id:99 value99 …
Key field value id:00 value0 key:9999 ….. ….. id:99 value99 优点: 减少无用结构数据, 避免BigKey产生
设置合理的超时时间
批处理优化
对于大量数据的处理, 存在多次少量和少次多量两个方案, 由于网络传输耗时数量级高于命令执行耗时, 所以优先采用一次性执行多条命令的方案
Redis命令
Redis提供了执行多个数据的命令, 批量查询/插入/修改, 如mset , hmset等, 可以保证原子性
但需要注意, 不要在一次批处理中传输太多命令,否则单次命令占用带宽过多,会导致网络阻塞
Pipeline
MSET虽然可以批处理,但是却只能操作部分数据类型,因此如果有对复杂数据类型的批处理需要,建议使用Pipeline功能. 但不保证多条命令的原子性
1 | $res = $redis->pipeline(function ($pipe) { |
集群下的批处理
如MSET或Pipeline这样的批处理需要在一次请求中携带多条命令,而此时如果Redis是一个集群(Cluster),那批处理命令的多个key必须落在一个插槽中,否则就会导致执行失败。

| 串行命令 | 串行slot | 并行slot | hash_tag | |
|---|---|---|---|---|
| 实现思路 | for循环遍历,依次执行每个命令 | 在客户端计算每个key的slot,将slot一致分为一组,每组都利用Pipeline批处理。 串行执行各组命令 | 在客户端计算每个key的slot,将slot一致分为一组,每组都利用Pipeline批处理。 并行执行各组命令 | 将所有key设置相同的hash_tag,则所有key的slot一定相同 |
| 耗时 | N次网络耗时 + N次命令耗时 | m次网络耗时 + N次命令耗时 m = key的slot个数 | 1次网络耗时 + N次命令耗时 | 1次网络耗时 + N次命令耗时 |
| 优点 | 实现简单 | 耗时较短 | 耗时非常短 | 耗时非常短、实现简单 |
| 缺点 | 耗时非常久 | 实现稍复杂 slot越多,耗时越久 | 实现复杂 | 容易出现数据倾斜 |
hash_tag使用:

对键执行 crc16 算法, 再对 16384 取余,即可得到一个小于 16384 的数,该结果就是 slot编号
服务端优化
持久化配置
Redis的持久化虽然可以保证数据安全,但也会带来很多额外的开销,因此持久化请遵循下列建议:
用来做缓存的Redis实例尽量不要开启持久化功能
建议关闭RDB持久化功能,使用AOF持久化
利用脚本定期在slave节点做RDB,实现数据备份
设置合理的rewrite阈值,避免频繁的bgrewrite
配置no-appendfsync-on-rewrite = yes,禁止在rewrite期间做aof,避免因AOF引起的阻塞, 但需要注意可能造成期间数据丢失问题
部署有关建议:
Redis实例的物理机要预留足够内存,应对fork和rewrite
单个Redis实例内存上限不要太大,例如4G或8G。可以加快fork的速度、减少主从同步、数据迁移压力
不要与CPU密集型应用部署在一起
不要与高硬盘负载应用一起部署。例如:数据库、消息队列
慢查询
慢查询:在Redis执行时耗时超过某个阈值的命令,称为慢查询。
1 | 慢查询的阈值可以通过配置指定: |

查看慢查询日志列表:
slowlog len:查询慢查询日志长度slowlog get [n]:读取n条慢查询日志slowlog reset:清空慢查询列表
命令及安全配置
Redis会绑定在0.0.0.0:6379,这样将会将Redis服务暴露到公网上,而Redis如果没有做身份认证,会出现严重的安全漏洞.
漏洞出现的核心的原因有以下几点:
Redis未设置密码
利用了Redis的
config set命令动态修改Redis配置使用了Root账号权限启动Redis
示例
1 | (echo -e " "; cat pass.txt; echo -e " ") > pass_e.txt |

建议
为了避免这样的漏洞,这里给出一些建议:
Redis一定要设置密码

禁止线上使用下面命令:
keys、flushall、flushdb、config set等命令。可以利用rename-command禁用。
bind:限制网卡,禁止外网网卡访问
开启防火墙
不要使用Root账户启动Redis
尽量不使用默认的端口
内存配置
当Redis内存不足时,可能导致Key频繁被删除、响应时间变长、QPS不稳定等问题。当内存使用率达到90%以上时就需要我们警惕,并快速定位到内存占用的原因。
内存占用来源
| 内存占用 | 说明 |
|---|---|
| 数据内存 | 是Redis最主要的部分,存储Redis的键值信息。主要问题是BigKey问题、内存碎片问题 |
| 进程内存 | Redis主进程本身运⾏肯定需要占⽤内存,如代码、常量池等等;这部分内存⼤约⼏兆,在⼤多数⽣产环境中与Redis数据占⽤的内存相⽐可以忽略 |
| 缓冲区内存 | 一般包括客户端缓冲区、AOF缓冲区、复制缓冲区等。客户端缓冲区又包括输入缓冲区和输出缓冲区两种。这部分内存占用波动较大,不当使用BigKey,可能导致内存溢出 |
内存碎片: Redis内部内存划分产生, 如10字节的数据在8字节与16字节期间, 故为其分配16字节数据, 多出来的6字节为内存碎片, 可以通过重新启动Redis服务处理
查看内存状态
Redis提供了一些命令,可以查看到Redis目前的内存分配状态:
**info memory **


memory [xxx]


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40# memory stats 结果解析
1) "peak.allocated" // Redis进程启动以来消耗内存的峰值
2) (integer) 945048
3) "total.allocated" // Redis使用分配器分配的总字节数, 即当前的总内存使用量
4) (integer) 885240
5) "startup.allocated" // Redis启动时消耗的初始内存量
6) (integer) 801232
7) "replication.backlog" // 复制积压缓冲区的大小
8) (integer) 0
9) "clients.slaves" // 主从复制中其他所有从节点的读写缓冲区大小
10) (integer) 0
11) "clients.normal" // 除从节点外, 所有其他普通客户端的读写缓冲区大小
12) (integer) 41040
13) "aof.buffer" // AOF持久化使用的缓存和AOF重写时产生的缓存
14) (integer) 0
15) "lua.caches" // lua脚本执行缓存
16) (integer) 0
17) "db.0" // 当前业务数据库
18) 1) "overhead.hashtable.main" //当前数据库的hash链表开销内存综合, 即元数据内存
2) (integer) 152
3) "overhead.hashtable.expires" // 用于存储可以的过期时间消耗的内存
4) (integer) 32
19) "overhead.total" // 总开销
20) (integer) 842456
21) "keys.count" // Redis实例中Key的数量
22) (integer) 3
23) "keys.bytes-per-key" // Redis实例中每个Key的平均大小
24) (integer) 28002
25) "dataset.bytes" // 纯业务数据占用的内存大小
26) (integer) 42784
27) "dataset.percentage" // 纯业务数据占用的内存比例
28) "50.928482055664062"
29) "peak.percentage" // 当前总内存与历史峰值的比例
30) "93.671432495117188"
...
49) "fragmentation" // 内存碎片率
50) "6.1035556793212891"
51) "fragmentation.bytes"
52) (integer) 4308544
内存缓冲区配置
内存缓冲区常见的有三种:
复制缓冲区:主从复制的
repl_backlog_buf,如果太小可能导致频繁的全量复制,影响性能。通过repl-backlog-size来设置,默认1mb
AOF缓冲区:AOF刷盘之前的缓存区域,AOF执行
bgrewriteaof的缓冲区。无法设置容量上限客户端缓冲区:分为输入缓冲区和输出缓冲区,输入缓冲区最大1G且不能设置。输出缓冲区可以设


普通客户端无限制缓冲区, 可以通过限制BigKey, 增加服务带宽, 修复对应业务逻辑
定位请求客户端
可以通过以下命令定位缓存溢出问题的客户端
info clients

client list

集群最佳实践
集群虽然具备高可用特性,能实现自动故障恢复,但是如果使用不当,也会存在一些问题:
集群完整性问题
在Redis的默认配置中,如果发现任意一个插槽不可用,则整个集群都会停止对外服务:

为了保证高可用特性,这里建议将
cluster-require-full-coverage配置为no集群带宽问题
集群节点之间会不断的互相Ping来确定集群中其它节点的状态。每次Ping携带的信息至少包括:插槽信息和集群状态信息
集群中节点越多,集群状态信息数据量也越大,10个节点的相关信息可能达到1kb,此时每次集群互通需要的带宽会非常高。
解决途径:
①避免大集群,集群节点数不要太多,最好少于1000,如果业务庞大,则建立多个集群。
②避免在单个物理机中运行太多Redis实例
配置合适的
cluster-node-timeout值数据倾斜问题
出现BigKey或批处理时使用
hash_tag, 可能会造成数据集中于某个节点中客户端性能问题
客户端在处理集群时, 执行命令前必须要处理插槽的选择等问题, 会造成一定的性能损耗
命令的集群兼容性问题
批处理命令不能直接向集群中执行, 因为数据可能需要落到不同的slot, 需要客户端自行处理
lua和事务问题
类型与批处理问题
集群还是主从
单体Redis(主从Redis)已经能达到万级别的QPS,并且也具备很强的高可用特性。如果主从能满足业务需求的情况下,尽量不搭建Redis集群。
