1. Redis 键值设计
1.1 优雅的 key 结构
Redis 的 Key 虽然可以自定义,但最好遵循下面的几个最佳实践约定:
- 遵循基本格式:[业务名称]:[数据名]:[id]
- 长度不超过44字节
- key 是 string 类型,底层编码包含 int、embstr 和 raw 三种。embstr 在小于 44 字节使用,采用连续内存空间,内存占用更小
-
1.2 BigKey
1.2.1 什么是 BigKey
BigKey 通常以 Key 的大小和 Key 中成员的数量来综合判定,例如:
Key 本身的数据量过大:一个 String 类型的 Key,它的值为 5 MB
- Key 中的成员数过多:一个 ZSET 类型的 Key,它的成员数量为 10,000 个
- Key 中成员的数据量过大:一个 Hash 类型的 Key,它的成员数量虽然只有 1,000 个。但这些成员的Value(值)总大小为 100MB
推荐值:
- 单个 key 的 value 小于 10KB
- 对于集合类型的 key,建议元素数量小于 1000
1.2.2 BigKey 的危害
网络阻塞:对 BigKey 执行读请求时,少量的 QPS 就可能导致带宽使用率被占满,导致 Redis 实例乃至所在物理机变慢
数据倾斜:BigKey 所在的 Redis 实例内存使用率远超其他实例,无法使数据分片的内存资源达到均衡
Redis 阻塞:对元素较多的 hash、list、zset 等做运算会耗时较旧,使主线程被阻塞
CPU 压力:对 BigKey 的数据序列化和反序列化会导致 CPU 的使用率飙升,影响 Redis 实例和本机其它应用
1.2.3 如何发现 BigKey
redis-cli --bigkeys:利用 redis-cli 提供的 —bigkeys 参数,可以遍历分析所有 key,并返回 Key 的整体统计信息与每个数据的 Top1 的 big key
scan 扫描:自己编程,利用 scan 扫描 Redis 中的所有 key,利用 strlen、hlen 等命令判断 key 的长度(此处不建议使用 MEMORY USAGE)
1.2.4 如何删除 BigKey
BigKey 内存占用较多,即便是删除这样的 key 也需要耗费很长时间,导致 Redis 主线程阻塞,引发一系列问题
redis 3.0 及以下版本如果是集合类型,则遍历 BigKey 的元素,先逐个删除子元素,最后删除 BigKey
Redis 4.0 以后 Redis 在 4.0 后提供了异步删除的命令 unlink
1.3 合适的数据类型
1.3.1 hash 存储
比如存储一个 User 对象,常用的有如下两种存储方式:
方式一:json 字符串
- 优点:实现简单粗暴
- 缺点:数据耦合,不够灵活 | user:1 | {“name”: “Jack”, “age”: 21} | | —- | —- |
方式二:hash
- 优点:底层使用ziplist,空间占用小,可以灵活访问对象的任意字段
- 缺点:代码相对复杂 | user:1 | name | Jack | | —- | —- | —- | | | age | 21 |
1.3.2 分片存储
假如有 hash 类型的 key,其中有 100 万对 field 和 value,field 是自增id,那么该如何存储
| somekey | id:0 | value0 |
|---|---|---|
| …… | …… | |
| id:999999 | vaule999999 |
- hash 的 entry 数量超过 500(默认大小) 时,会使用哈希表而不是 ZipList,内存占用较多
- 可以通过 hash-max-ziplist-entries 配置 entry 上限。但是如果 entry 过多就会导致 BigKey 问题
拆分为小的 hash,将 id / 100 作为 key, 将 id % 100 作为 field,这样每 100 个元素为一个 Hash
| somekey:0 | id:0 | value0 |
|---|---|---|
| …… | …… | |
| id:99 | vaule99 |
| somekey:9999 | id:999899 | value999899 |
|---|---|---|
| …… | …… | |
| id:999999 | vaule999999 |
2. 批处理优化
单条命令执行:RT = 1 次往返的网络传输耗时 + 1 次 Redis 执行命令耗时
N 条命令依次执行:RT = N 次往返的网络传输耗时 + N 次 Redis 执行命令耗时
N次命令批量执行 = 1 次往返的网络传输耗时 + N 次 Redis 执行命令耗时
2.1 MSET
Redis 提供了很多 Mxxx 这样的命令,可以实现批量插入数据
@Testvoid testMxx() {String[] arr = new String[2000];int j;for (int i = 1; i <= 100000; i++) {j = (i % 1000) << 1;arr[j] = "test:key_" + i;arr[j + 1] = "value_" + i;if (j == 0) {jedis.mset(arr);}}}
2.2 pipline
MSET 虽然可以批处理,但是却只能操作部分数据类型,因此如果有对复杂数据类型的批处理需要,建议使用 Pipeline 功能
注意 Pipeline 的多个命令之间不具备原子性
@Testvoid testPipeline() {// 创建管道Pipeline pipeline = jedis.pipelined();for (int i = 1; i <= 100000; i++) {// 放入命令到管道pipeline.set("test:key_" + i, "value_" + i);if (i % 1000 == 0) {// 每放入1000条命令,批量执行pipeline.sync();}}}
2.3 集群下的批处理
如 MSET 或 Pipeline 这样的批处理需要在一次请求中携带多条命令,而此时如果 Redis 是一个集群,那批处理命令的多个 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 越多,耗时越久 |
实现复杂 | 容易出现数据倾斜 |
3. 服务端优化
3.1 持久化配置
Redis 的持久化虽然可以保证数据安全,但也会带来很多额外的开销,因此持久化请遵循下列建议:
- 用来做缓存的 Redis 实例尽量不要开启持久化功能
- 建议关闭 RDB 持久化功能,使用 AOF 持久化
- 利用脚本定期在 slave 节点做 RDB,实现数据备份
- 设置合理的 rewrite 阈值,避免频繁的 bgrewrite 配置
no-appendfsync-on-rewrite = yes,禁止在 rewrite 期间做 aof,避免因 AOF 引起的阻塞
部署有关建议:
- Redis 实例的物理机要预留足够内存,应对 fork 和 rewrite
- 单个 Redis 实例内存上限不要太大,例如 4G 或 8G。可以加快 fork 的速度、减少主从同步、数据迁移压力
- 不要与 CPU 密集型应用部署在一起
- 不要与高硬盘负载应用一起部署。例如:数据库、消息队列
3.2 慢查询
慢查询的阈值可以通过配置指定,slowlog-log-slower-than慢查询阈值,单位是微秒。默认是 10000,建议 1000
慢查询会被放入慢查询日志中,日志的长度有上限,可以通过配置指定,slowlog-max-len,慢查询日志(本质是一个队列)的长度。默认是 128,建议1000
127.0.0.1:6379> config get slowlog-log-slower-than1) "slowlog-log-slower-than"2) "10000"127.0.0.1:6379> config set slowlog-log-slower-than 1000OK127.0.0.1:6379> config get slowlog-log-slower-than1) "slowlog-log-slower-than"2) "1000"127.0.0.1:6379> config get slowlog-max-len1) "slowlog-max-len"2) "128"127.0.0.1:6379> config set slowlog-max-len 1000OK127.0.0.1:6379> config get slowlog-max-len1) "slowlog-max-len"2) "1000"
# 查询慢查询日志长度127.0.0.1:6379> SLOWLOG len(integer) 1# 读取 n 条慢查询日志, 最新的日志会放在最前面127.0.0.1:6379> slowlog get 21) 1) (integer) 34 # 日志编号2) (integer) 1656128921 # 日志记录时间戳3) (integer) 214680 # 耗时4) 1) "hgetall" # 命令 hgetall chushibiao2) "chushibiao"5) "127.0.0.1:62935" # 客户端 ip 和端口6) "" # 客户端名称2) 1) (integer) 332) (integer) 16561288353) (integer) 264454) 1) "hkeys"2) "chushibiao"5) "127.0.0.1:62935"6) ""# 清空慢查询列表127.0.0.1:6379> slowlog resetOK127.0.0.1:6379> slowlog len(integer) 0
3.3 命令及安全配置
为了避免生产漏洞,这里给出一些建议:
- Redis 一定要设置密码
- 禁止线上使用下面命令:keys、flushall、flushdb、config set 等命令。可以利用
rename-command禁用 - bind:限制网卡,禁止外网网卡访问
- 开启防火墙
- 不要使用 root 账户启动 Redis
- 尽量不是有默认的端口
3.4 内存配置
当 Redis 内存不足时,可能导致 Key 频繁被删除、响应时间变长、QPS 不稳定等问题。当内存使用率达到 90% 以上时就需要我们警惕,并快速定位到内存占用的原因
| 内存占用 | 说明 |
|---|---|
| 数据内存 | 是 Redis 最主要的部分,存储 Redis 的键值信息。主要问题是 BigKey 问题、内存碎片问题 |
| 进程内存 | Redis 主进程本身运⾏肯定需要占⽤内存,如代码、常量池等等;这部分内存⼤约⼏兆,在⼤多数⽣产环境中与 Redis 数据占⽤的内存相⽐可以忽略 |
| 缓冲区内存 | 一般包括客户端缓冲区、AOF 缓冲区、复制缓冲区等。客户端缓冲区又包括输入缓冲区和输出缓冲区两种。这部分内存占用波动较大,不当使用 BigKey,可能导致内存溢出 |
127.0.0.1:6379> info memory# Memoryused_memory:263465248used_memory_human:251.26Mused_memory_rss:10502144used_memory_rss_human:10.02Mused_memory_peak:490073472used_memory_peak_human:467.37Mused_memory_peak_perc:53.76%used_memory_overhead:1099832used_memory_startup:1095648used_memory_dataset:262365416used_memory_dataset_perc:100.00%...127.0.0.1:6379> memory stats1) "peak.allocated"2) (integer) 4900734723) "total.allocated"4) (integer) 2634650565) "startup.allocated"6) (integer) 10956487) "replication.backlog"8) (integer) 0
内存缓冲区常见的有三种:
- 复制缓冲区:主从复制的 repl_backlog_buf,如果太小可能导致频繁的全量复制,影响性能。通过 repl-backlog-size 来设置,默认 1mb
- AOF缓冲区:AOF 刷盘之前的缓存区域,AOF 执行 rewrite 的缓冲区。无法设置容量上限
客户端缓冲区:分为输入缓冲区和输出缓冲区,输入缓冲区最大 1G 且不能设置。输出缓冲区可以设置
4. 集群最佳实践
集群虽然具备高可用特性,能实现自动故障恢复,但是如果使用不当,也会存在一些问题:
集群完整性问题
- 在 Redis 的默认配置中,如果发现任意一个插槽不可用,则整个集群都会停止对外服务
- 为了保证高可用特性,这里建议将
cluster-require-full-coverage配置为 false
- 集群带宽问题
- 集群节点之间会不断的互相 Ping 来确定集群中其它节点的状态。每次 Ping 携带的信息至少包括插槽信息和集群状态信息
- 集群中节点越多,集群状态信息数据量也越大,10 个节点的相关信息可能达到 1kb,此时每次集群互通需要的带宽会非常高
- 解决途径就是避免大集群,集群节点数不要太多,最好少于1000,如果业务庞大,则建立多个集群
- 数据倾斜问题
- 客户端性能问题
- 命令的集群兼容性问题
- lua 和事务问题
