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 这样的命令,可以实现批量插入数据

  1. @Test
  2. void testMxx() {
  3. String[] arr = new String[2000];
  4. int j;
  5. for (int i = 1; i <= 100000; i++) {
  6. j = (i % 1000) << 1;
  7. arr[j] = "test:key_" + i;
  8. arr[j + 1] = "value_" + i;
  9. if (j == 0) {
  10. jedis.mset(arr);
  11. }
  12. }
  13. }

2.2 pipline

MSET 虽然可以批处理,但是却只能操作部分数据类型,因此如果有对复杂数据类型的批处理需要,建议使用 Pipeline 功能

注意 Pipeline 的多个命令之间不具备原子性

  1. @Test
  2. void testPipeline() {
  3. // 创建管道
  4. Pipeline pipeline = jedis.pipelined();
  5. for (int i = 1; i <= 100000; i++) {
  6. // 放入命令到管道
  7. pipeline.set("test:key_" + i, "value_" + i);
  8. if (i % 1000 == 0) {
  9. // 每放入1000条命令,批量执行
  10. pipeline.sync();
  11. }
  12. }
  13. }

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

  1. 127.0.0.1:6379> config get slowlog-log-slower-than
  2. 1) "slowlog-log-slower-than"
  3. 2) "10000"
  4. 127.0.0.1:6379> config set slowlog-log-slower-than 1000
  5. OK
  6. 127.0.0.1:6379> config get slowlog-log-slower-than
  7. 1) "slowlog-log-slower-than"
  8. 2) "1000"
  9. 127.0.0.1:6379> config get slowlog-max-len
  10. 1) "slowlog-max-len"
  11. 2) "128"
  12. 127.0.0.1:6379> config set slowlog-max-len 1000
  13. OK
  14. 127.0.0.1:6379> config get slowlog-max-len
  15. 1) "slowlog-max-len"
  16. 2) "1000"
  1. # 查询慢查询日志长度
  2. 127.0.0.1:6379> SLOWLOG len
  3. (integer) 1
  4. # 读取 n 条慢查询日志, 最新的日志会放在最前面
  5. 127.0.0.1:6379> slowlog get 2
  6. 1) 1) (integer) 34 # 日志编号
  7. 2) (integer) 1656128921 # 日志记录时间戳
  8. 3) (integer) 214680 # 耗时
  9. 4) 1) "hgetall" # 命令 hgetall chushibiao
  10. 2) "chushibiao"
  11. 5) "127.0.0.1:62935" # 客户端 ip 和端口
  12. 6) "" # 客户端名称
  13. 2) 1) (integer) 33
  14. 2) (integer) 1656128835
  15. 3) (integer) 26445
  16. 4) 1) "hkeys"
  17. 2) "chushibiao"
  18. 5) "127.0.0.1:62935"
  19. 6) ""
  20. # 清空慢查询列表
  21. 127.0.0.1:6379> slowlog reset
  22. OK
  23. 127.0.0.1:6379> slowlog len
  24. (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,可能导致内存溢出
  1. 127.0.0.1:6379> info memory
  2. # Memory
  3. used_memory:263465248
  4. used_memory_human:251.26M
  5. used_memory_rss:10502144
  6. used_memory_rss_human:10.02M
  7. used_memory_peak:490073472
  8. used_memory_peak_human:467.37M
  9. used_memory_peak_perc:53.76%
  10. used_memory_overhead:1099832
  11. used_memory_startup:1095648
  12. used_memory_dataset:262365416
  13. used_memory_dataset_perc:100.00%
  14. ...
  15. 127.0.0.1:6379> memory stats
  16. 1) "peak.allocated"
  17. 2) (integer) 490073472
  18. 3) "total.allocated"
  19. 4) (integer) 263465056
  20. 5) "startup.allocated"
  21. 6) (integer) 1095648
  22. 7) "replication.backlog"
  23. 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 和事务问题