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 这样的命令,可以实现批量插入数据
@Test
void 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 的多个命令之间不具备原子性
@Test
void 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-than
1) "slowlog-log-slower-than"
2) "10000"
127.0.0.1:6379> config set slowlog-log-slower-than 1000
OK
127.0.0.1:6379> config get slowlog-log-slower-than
1) "slowlog-log-slower-than"
2) "1000"
127.0.0.1:6379> config get slowlog-max-len
1) "slowlog-max-len"
2) "128"
127.0.0.1:6379> config set slowlog-max-len 1000
OK
127.0.0.1:6379> config get slowlog-max-len
1) "slowlog-max-len"
2) "1000"
# 查询慢查询日志长度
127.0.0.1:6379> SLOWLOG len
(integer) 1
# 读取 n 条慢查询日志, 最新的日志会放在最前面
127.0.0.1:6379> slowlog get 2
1) 1) (integer) 34 # 日志编号
2) (integer) 1656128921 # 日志记录时间戳
3) (integer) 214680 # 耗时
4) 1) "hgetall" # 命令 hgetall chushibiao
2) "chushibiao"
5) "127.0.0.1:62935" # 客户端 ip 和端口
6) "" # 客户端名称
2) 1) (integer) 33
2) (integer) 1656128835
3) (integer) 26445
4) 1) "hkeys"
2) "chushibiao"
5) "127.0.0.1:62935"
6) ""
# 清空慢查询列表
127.0.0.1:6379> slowlog reset
OK
127.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
# Memory
used_memory:263465248
used_memory_human:251.26M
used_memory_rss:10502144
used_memory_rss_human:10.02M
used_memory_peak:490073472
used_memory_peak_human:467.37M
used_memory_peak_perc:53.76%
used_memory_overhead:1099832
used_memory_startup:1095648
used_memory_dataset:262365416
used_memory_dataset_perc:100.00%
...
127.0.0.1:6379> memory stats
1) "peak.allocated"
2) (integer) 490073472
3) "total.allocated"
4) (integer) 263465056
5) "startup.allocated"
6) (integer) 1095648
7) "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 和事务问题