Redis 使用规范与常用优化方案
业务使用规范
| 原则 | 原则说明 | 级别 | 备注 |
|---|---|---|---|
| 就近部署业务,避免时延过大 | 如果部署位置过远(非同一个地区)或者时延较大(例如业务服务器与 Redis 实例通过公网连接),网络延迟将极大影响读写性能。 | 强制 | - |
| 冷热数据区分 | 建议将热数据加载到 Redis 中。低频数据可存储在 Mysql 或者 ElasticSearch 中。 | 建议 | Redis 将低频数据存入内存中,并不会加速访问,且占用 Redis 空间。 |
| 业务数据分离 | 避免多个业务共用一个 Redis。 | 强制 | 一方面避免业务相互影响,另一方面避免单实例膨胀,并能在故障时降低影响面,快速恢复。 |
| 禁止使用 select 功能在单 Redis 实例做多 db 区分。 | 强制 | Redis 单实例内多 DB 隔离性较差,Redis 开源社区已经不再发展多 DB 特性,后续不建议依赖该特性。 | |
| 设置合理的内存淘汰(逐出)策略 | 合理设置淘汰策略,可以在 Redis 内存意外写满的时候,仍然正常提供服务。 | 强制 | 默认值为volatile-lru。Redis 支持的数据逐出策略,参见:Redis 数据逐出策略 |
| 以缓存方式使用 Redis | Redis 事务功能较弱,不建议过多使用。 | 建议 | 事务执行完后,不可回滚。 |
| 数据异常的情况下,支持清空缓存进行数据恢复。 | 强制 | Redis 本身没有保障数据强一致的机制和协议,业务不能强依赖 Redis 数据的准确性。 | |
| 以缓存方式使用 Redis 时,所有的 key 需设置过期时间,不可把 Redis 作为数据库使用。 | 强制 | 失效时间并非越长越好,需要根据业务性质进行设置。 | |
| 防止缓存击穿 | 推荐搭配本地缓存使用 Redis,对于热点数据建立本地缓存。本地缓存数据使用异步方式进行刷新。 | 建议 | - |
| 防止缓存穿透 | 非关键路径透传数据库,建议对访问数据库进行限流。 | 建议 | - |
| 从 Redis 获取数据未命中时,访问只读数据库实例。可通过域名等方式对接多个只读实例。 | 建议 | 核心是未命中的缓存数据不会打到主库上。 用域名对接多个只读数据库实例,一旦出现问题,可以增加只读实例应急。 | |
| 不用作消息队列 | 发布订阅场景下,不建议作为消息队列使用。 | 强制 | 如没有非常特殊的需求,不建议将 Redis 当作消息队列使用。 Redis 当作消息队列使用,会有容量、网络、效率、功能方面的多种问题。 如需要消息队列,可使用高吞吐的 Kafka 或者高可靠的 RocketMQ。 |
| 合理选择规格 | 如果业务增长会带来 Redis 请求增长,请选择集群实例(Proxy 集群和 Cluster 集群) | 强制 | 单机和主备扩容只能实现内存、带宽的扩容,无法实现计算性能扩容。 |
| 生产实例需要选择主备或者集群实例,不能选用单机实例 | 强制 | - | |
| 主备实例,不建议使用过大的规格。 | 建议 | Redis 在执行 RewriteAOF 和 BGSAVE 的时候,会 fork 一个进程,过大的内存会导致卡顿 | |
| 具备降级或容灾措施 | 缓存访问失败时,具备降级措施,从 DB 获取数据;或者具备容灾措施,自动切换到另一个 Redis 使用。 | 建议 | - |
数据设计规范
| 分类 | 原则 | 原则说明 | 级别 | 备注 |
|---|---|---|---|---|
| Key 相关规范 | 使用统一的命名规范。 | 一般使用业务名(或数据库名)为前缀,用冒号分隔。Key 的名称保证语义清晰。 | 建议 | 例如,业务名:子业务名:id |
| 控制 Key 名称的长度。 | 在保证语义清晰的情况下,尽量减少 Key 的长度。 有些常用单词可使用缩写,例如,user 缩写为 u,messages 缩写为 msg。 | 建议 | 建议不要超过 128 字节(越短越好)。 | |
| 禁止包含特殊字符(大括号“{}”除外)。 | 禁止包含特殊字符,如空格、换行、单双引号以及其他转义字符。 | 强制 | 由于大括号“{}”为 Redis 的 hash tag 语义,如果使用的是集群实例,Key 名称需要正确地使用大括号避免分片不均的情况。 | |
| Value 相关规范 | 设计合理的 Value 大小。 | 设计合理的 Key 中 Value 的大小,string 类型推荐小于 10 KB。 | 建议 | 过大的 Value 会引发分片不均、热点 Key、实例流量或 CPU 使用率冲高等问题,还可能导致变更规格和迁移失败。应从设计源头上避免此类问题带来的影响。 |
| 设计合理的 Key 中元素的数量。 | 对于集合和列表类的数据结构(例如 Hash,Set,List 等),避免其中包含过多元素,建议单 Key 中的元素不要超过 5000 个。 | 建议 | 由于某些命令(例如 HGETALL)的时间复杂度直接与 Key 中的元素数量相关。如果频繁执行时间复杂度为 O(N)及以上的命令,且 Key 中的子 Key 数量过多容易引发慢请求、分片流量不均或热点 Key 问题。 | |
| 选择合适的数据类型。 | 合理地选择数据结构能够节省内存和带宽。 | 建议 | 例如存储用户的信息,可用使用多个 key,使用 set u:1:name "X"、set u:1:age 20 存储,也可以使用 hash 数据结构,存储成 1 个 key,设置用户属性时使用 hmset 一次设置多个,同时这样存储也能节省内存。 | |
| 设置合理的过期时间。 | 合理设置 Key 的过期时间,将过期时间打散,避免大量 Key 在同一时间点过期。 | 建议 | 设置过期时间时,可以在基础值上增减一个随机偏移值,避免在同一个时间点大量 Key 过期。大量 Key 过期会导致 CPU 使用率冲高。 |
命令使用规范
| 原则 | 原则说明 | 级别 | 备注 |
|---|---|---|---|
| 谨慎使用 O(N)复杂度的命令 | 时间复杂度为 O(N)的命令,需要特别注意 N 的值。避免 N 过大,造成 Redis 阻塞以及 CPU 使用率冲高。 | 强制 | 例如:hgetall、lrange、smembers、zrange、sinter 这些命令都是做全集操作,如果元素很多,会消耗大量 CPU 资源。可使用 hscan、sscan、zscan 这些分批扫描的命令替代。 |
| 禁用高危命令 | 禁止使用 flushall、keys、hgetall 等命令,或对命令进行重命名限制使用。 | 强制 | 修改启动配置文件 [SECURITY] 项中 增加 rename-command oldName newName |
| 慎重使用 select | Redis 多数据库支持较弱,多业务用多数据库实际还是单线程处理,会有干扰。最好是拆分使用多个 Redis。 | 建议 | - |
| 使用批量操作提高效率 | 如果有批量操作,可使用 mget、mset 或 pipeline,提高效率,但要注意控制一次批量操作的元素个数。 | 建议 | mget、mset 和 pipeline 的区别如下: 1. mget 和 mset 是原子操作,pipeline 是非原子操作。 2. pipeline 可以打包不同的命令,mget 和 mset 做不到。 3. 使用 pipeline,需要客户端和服务端同时支持。 |
| 避免在 lua 脚本中使用耗时代码 | lua 脚本的执行超时时间为 5 秒钟,建议不要在 lua 脚本中使用比较耗时的代码。 | 强制 | 比如长时间的 sleep、大的循环等语句。 |
| 避免在 lua 脚本中使用随机函数 | 调用 lua 脚本时,建议不要使用随机函数去指定 key,否则在主备节点上执行结果不一致,从而导致主备节点数据不一致。 | 强制 | - |
| 遵循集群实例使用 lua 的限制 | 遵循集群实例使用 lua 的限制。 | 强制 | 使用 EVAL 和 EVALSHA 命令时,所有 key,必须在 1 个 slot 上。 |
| 对 mget,hmget 等批量命令做并行和异步 IO 优化 | 某些客户端对于 MGET,HMGET 这些命令没有做特殊处理,串行执行再合并返回,效率较低,建议做并行优化。 | 建议 | 例如 Jedis 对于 MGET 命令在集群中执行的场景就没有特殊优化,串行执行,比起 lettuce 中并行 pipeline,异步 IO 的实现,性能差距可达到数十倍,该场景建议使用 Jedis 的客户端自行实现 slot 分组和 pipeline 的功能。 |
| 禁止使用 del 命令直接删除大 Key | 使用 del 命令直接删除大 Key(主要是集合类型)会导致节点阻塞,影响后续请求 | 强制 | Redis 4.0 后的版本可以通过 UNLINK 命令安全地删除大 Key,该命令是异步非阻塞的。 对于 Redis 4.0 之前的版本: 1. 如果是 Hash 类型的大 Key,推荐使用 hscan + hdel 2. 如果是 List 类型的大 Key,推荐使用 ltrim 3. 如果是 Set 类型的大 Key,推荐使用 sscan + srem 4. 如果是 SortedSet 类型的大 Key,推荐使用 zscan + zrem 同时要注意防止 bigkey 过期时间自动删除问题。 |
| 必要情况下使用 monitor 命令时,要注意不要长时间使用 | 必要情况下使用 monitor 命令时,要注意不要长时间使用 | 建议 | 1. monitor 命令在高并发条件下,会存在内存暴增和影响 Redis 性能的隐患,所以此种方法适合在短时间内使用。 2. 只能统计一个 Redis 节点的热点 key,对于 Redis 集群需要进行汇总统计。 |
SDK 使用规范
| 原则 | 原则说明 | 级别 | 备注 |
|---|---|---|---|
| 使用连接池和长连接 | 短连接性能差,推荐使用带有连接池的客户端。 | 建议 | 连接的频繁创建和销毁,会浪费大量的系统资源,极限情况会造成宿主机宕机。请确保使用了正确的 Redis 客户端连接池配置。 |
| 客户端需要对可能的故障和慢请求做容错处理 | 由于 Redis 服务可能因网络波动或基础设置故障的影响,引发主备倒换,命令超时或慢请求等现象,需要在客户端内设计合理的容错重试机制。 | 建议 | 参考:Redis 客户端重试指南 |
| 合理设置重试时间和次数 | 合理设置容错处理的重试时间,根据业务要求设置,避免过短或者过长。 | 强制 | 如果超时重试时间设置的非常短(例如 200 毫秒以下),可能引发重试风暴,极易引发业务层雪崩。 如果重试时间设置得较长或者重试次数设置得较大,则可能导致在主备倒换情况下业务恢复较慢。 |
| 避免使用 Lettuce 客户端 | Lettuce 客户端在默认配置下有一定性能优势,并且是 spring 的默认客户端,但是 Jedis 客户端在面对连接异常,网络抖动等场景下的异常处理和检测能力明显强于 Lettuce,可靠性更强,建议使用 Jedis。 | 建议 | Lettuce 存在几个方面的问题: 1. Lettuce 客户端在请求多次超时后,不再发起自动重连,当发生主备倒换后,可能出现连接超时导致无法重连。 2.Lettuce 默认未配置集群拓补刷新的配置,会导致 Cluster 集群在发生拓补信息变化(主备倒换,扩容缩容)时,无法识别新的节点信息,导致业务失败。可参考:使用 Lettuce 连接 Cluster 集群实例时的扩容异常处理。 3. Lettuce 没有连接池校验的功能,无法检测连接池中的连接是否仍然有效,获取失效连接之后会导致业务失败。 |
运维管理规范
| 原则 | 原则说明 | 级别 | 备注 |
|---|---|---|---|
| 生产开启密码保护 | 生产系统中需要开启 Redis 密码保护机制。 | 强制 | - |
| 现网操作安全 | 禁止开发人员私自连到线上 Redis 服务。 | 强制 | - |
| 验证业务的故障处理能力或容灾逻辑 | 在测试环境或者预生产环境中组织演练,验证在 Redis 主备倒换、宕机或者扩缩容场景下业务的可靠性。 | 建议 | - |
| 监控实践 | 关注 Redis 负载,在过载前提前扩容。 | 强制 | - |
| 日常巡检 | 例行检查各个节点的内存使用率,查看主节点内存使用率是否有不均衡的状态。 | 建议 | 内存使用率不均衡说明存在大 Key 问题,需要进行大 Key 拆分及优化。参见:如何发现和处理大 Key、热 Key |
| 开启热 Key 例行分析,并分析是否有 Key 频繁调用。 | 建议 | - | |
| 例行诊断 Redis 实例的命令,分析 O(N)类命令是否存在隐患。 | 建议 | 针对 O(N)命令,即使耗时很小,建议开发分析业务增长,N 是否会增长。 | |
| 例行巡检 Redis 慢日志命令。 | 建议 | 针对慢日志分析隐患,并尽快从业务上进行修复。 |
补充资料
Redis 数据逐出策略
Redis 实例支持通过修改配置参数(maxmemory-policy),修改数据逐出策略。
在达到内存上限(maxmemory)时,Redis 支持选择以下 8 种数据逐出策略:
- noeviction:在这种策略下,如果缓存达到了配置的上限,实例将不再处理客户端任何增加缓存数据的请求,比如写命令,实例直接返回错误给客户端。缓存达到上限后,实例只处理删除和少数几个例外请求。
- allkeys-lru:根据 LRU(Least recently used,最近最少使用)算法尝试回收最少使用的键,使得新添加的数据有空间存放。
- volatile-lru:根据 LRU(Least recently used,最近最少使用)算法尝试回收最少使用的键,但仅限于在过期集合的键,使得新添加的数据有空间存放。
- allkeys-random:回收随机的键使得新添加的数据有空间存放。
- volatile-random:回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
- volatile-ttl:回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
- allkeys-lfu (Redis4.0 新增):从所有键中驱逐最不常用的键。
- volatile-lfu (Redis4.0 新增):从具有“expire”字段集的所有键中驱逐最不常用的键。
Redis 客户端 重试指南
引发 Redis 操作失败的场景
| 场景 | 说明 |
|---|---|
| 故障触发了主备倒换 | 因 Redis 底层硬件或其他原因导致主节点故障后,会触发主备倒换。 |
| 慢查询引起了请求堵塞 | 执行时间复杂度为 O(N)的操作,引发慢查询和请求的堵塞,此时,客户端发起的其他请求可能出现暂时性失败。 |
| 复杂的网络环境 | 由于客户端与 Redis 服务器之间复杂网络环境引起,可能出现偶发的网络抖动、数据重传等问题,此时,客户端发起的请求可能会出现暂时性失败。 |
| 复杂的硬件问题 | 由于客户端所在的硬件偶发性故障引起,例如虚拟机 HA,磁盘时延抖动等场景,此时,客户端发起的请求可能会出现暂时性失败。 |
推荐的重试准则
| 重试准则 | 说明 |
|---|---|
| 仅重试幂等的操作 | 由于超时可能发生在下述任一阶段: 1. 该命令由客户端发送成功,但尚未到达 Redis。 2. 命令到达 Redis,但执行超时。 3. 命令在 Redis 中执行结束,但结果返回给客户端时发生超时。 执行重试可能导致某个操作在 Redis 中被重复执行,因此不是所有操作均适合设计重试机制。通常推荐仅重试幂等的操作,例如 SET 操作,即多次执行 SET a b 命令,那么 a 的值只可能是 b 或执行失败;如果执行 LPUSH mylist a 则不是幂等的操作,可能导致 mylist 中包含多个 a 元素。 |
| 适当的重试次数与间隔 | 根据业务需求和实际场景调整适当的重试次数与间隔,否则可能引发下述问题: 1. 如果重试次数不足或间隔太长,应用程序可能无法完成操作而导致失败。 2. 如果重试次数过大或间隔过短,应用程序可能会占用过多的系统资源,且可能因请求过多而堵塞在服务器上无法恢复。 常见的重试间隔方式包括立即重试、固定时间重试、指数增加时间重试、随机时间重试等。 |
| 避免重试嵌套 | 重试嵌套可能导致重试时间被指数级放大。 |
| 记录重试异常并打印失败报告 | 在重试过程中,建议在 WARN 级别上打印重试错误日志,同时,仅在重试失败时打印异常信息。 |
如何发现和处理大 Key、热 Key
大 Key 和热 Key 的定义
- 大 Key 和热 Key 场景较多,没有非常明确的边界,需要根据实际业务判断
| 名词 | 定义 |
|---|---|
| 大 Key | 大 Key 可以分为两种情况: 1. Key 的 Value 较大,例如一个 String 类型的 Key 大小达到 10MB,或者一个集合类型(Hash,List,Set 等)的元素总大小达到了 100MB。一般我们定义单个 String 类型的 Key 大小达到 10KB,或者集合类型的 Key 总大小达到 50MB,则定义其为大 Key。 2. Key 的元素较多,例如一个 Hash 类型的 Key,其元素数量达到了 10000。一般我们定义集合类型的 Key 中元素超过 5000 个,则认为其为大 Key。 |
| 热 Key | 通常以一个 Key 被操作的频率和占用的资源来判定其是否为热 Key,例如: 1. 某个集群实例一个分片每秒处理 10000 次请求,其中有 3000 次都是操作同一个 Key。 2. 某个集群实例一个分片的总带宽使用(入带宽+出带宽)为 100Mbits/s,其中 80Mbits 是由于对某个 Hash 类型的 Key 执行 HGETALL 所占用。 |
大 Key 和热 Key 的影响
| 类别 | 影响 |
|---|---|
| 大 Key | 造成数据迁移失败: 1. Redis 集群变更规格过程中会进行数据 rebalance(节点间迁移数据),单个 Key 过大的时候会触发 Redis 内核对于单 Key 的迁移限制,造成数据迁移超时失败,Key 越大失败的概率越高,大于 512MB 的 Key 可能会触发该问题。 2. 数据迁移过程中,如果一个大 Key 的元素过多,则会阻塞后续 Key 的迁移,后续 Key 的数据会放到迁移机的内存 Buffer 中,如果阻塞时间太久,则会导致迁移失败。 容易造成集群分片不均的情况: 1. 各分片内存使用不均。例如某个分片占用内存较高甚至首先使用满,导致该分片 Key 被逐出,同时也会造成其他分片的资源浪费。 2. 各分片的带宽使用不均。例如某个分片被频繁流控,其他分片则没有这种情况。 客户端执行命令的时延变大: 1. 对大 Key 进行的慢操作会导致后续的命令被阻塞,从而导致一系列慢查询。 导致主备倒换: 1. 对大 Key 执行危险的 DEL 操作可能会导致主节点长时间阻塞,从而导致主备倒换。 |
| 热 Key | 容易造成集群分片不均的情况: 1. 造成热 Key 所在的分片有大量业务访问而同时其他的分片压力较低。这样不仅会容易产生单分片性能瓶颈,还会浪费其他分片的计算资源。 使得 CPU 冲高: 1. 对热 Key 的大量操作可能会使得 CPU 冲高,如果表现在集群单分片中就可以明显地看到热 Key 所在的分片 CPU 使用率较高。这样会导致其他请求受到影响,产生慢查询,同时影响整体性能。业务量突增场景下甚至会导致主备切换。 易造成缓存击穿: 1. 热 Key 的请求压力过大,超出 Redis 的承受能力易造成缓存击穿,即大量请求将被直接指向后端的数据库,导致数据库访问量激增甚至宕机,从而影响其他业务。 |
如何发现大 Key 和热 Key
| 方法 | 说明 |
|---|---|
| 通过 redis-cli 的 bigkeys 和 hotkeys 参数查找大 Key 和热 Key | 1. Redis-cli 提供了 bigkeys 参数,能够使 redis-cli 以遍历的方式分析 Redis 实例中的所有 Key,并返回 Key 的整体统计信息与每个数据类型中 Top1 的大 Key,bigkeys 仅能分析并输入六种数据类型(STRING、LIST、HASH、SET、ZSET、STREAM),命令示例为:redis-cli -h <实例的连接地址> -p <端口> -a <密码> --bigkeys。 2. 自 Redis 4.0 版本起,redis-cli 提供了 hotkeys 参数,可以快速帮您找出业务中的热 Key,该命令需要在业务实际运行期间执行,以统计运行期间的热 Key。命令示例为:redis-cli -h <实例的连接地址> -p <端口> -a <密码> --hotkeys。热 Key 的详情可以在结果中的 summary 部分获取到。 |
| 通过 Redis 命令查找大 Key | 如果有已知的大 Key 模式,例如知道其前缀为 u:plus:detail,那么可以通过一个程序,SCAN 符合该前缀的 Key,然后通过查询成员数量和查询 Key 大小的相关命令,来判断具体的大 Key。 查询成员数量的相关命令:LLEN,HLEN,XLEN,ZCARD,SCARD 查询 Key 占用内存大小的命令:DEBUG OBJECT,MEMORY USAGE 注意:该方法会大量消耗计算资源,不要在业务压力较大的实例使用该方法,否则可能会对正常业务造成影响。 |
| 通过 redis-rdb-tools 工具找出大 Key | redis-rdb-tools是分析 Redis RDB 快照文件的开源工具。可以根据需求自定义分析 Redis 实例中所有 Key 的内存占用情况。 注意:使用此方法需要导出 rdb 文件。时效性相较于在线分析来说较差,优势在于完全不影响现有业务 |
如何优化大 Key 和热 Key
| 类别 | 方法 |
|---|---|
| 大 Key | 进行大 Key 拆分,分为以下几种场景: 1. 该对象为 String 类型的大 Key:可以尝试将对象分拆成几个 Key-Value, 使用 MGET 或者多个 GET 组成的 pipeline 获取值,分拆单次操作的压力,对于集群来说可以将操作压力平摊到多个分片上,降低对单个分片的影响。 2. 该对象为集合类型的大 Key,并且需要整存整取:在设计上严格禁止这种场景的出现,因为无法拆分。有效的方法是将该大 Key 从 Redis 去除,单独放到其余存储介质上。 3. 该对象为集合类型的大 Key,每次只需操作部分元素:将集合类型中的元素分拆。以 Hash 类型为例,可以在客户端定义一个分拆 Key 的数量 N,每次对 HGET 和 HSET 操作的 field 计算哈希值并取模 N,确定该 field 落在哪个 Key 上,实现上类似于 Redis Cluster 的计算 slot 的算法。 将大 Key 单独转移到其余存储介质: 1. 无法拆分的大 Key 建议使用此方法,将不适用 Redis 能力的数据存至其它存储介质,如 SFS 或者其余 NoSQL 数据库,并在 Redis 中删除该大 Key。 合理设置过期时间并对过期数据定期清理: 1. 合理设置过期时间,避免历史数据在 Redis 中大量堆积。可以配置数据删除策略为定期删除 |
| 热 Key | 使用读写分离: 如果热 Key 主要是读流量较大,则可以在客户端配置读写分离,降低对主节点的影响。 使用客户端缓存/本地缓存: 该方案需要提前了解业务的热点 Key 有哪些,设计客户端/本地和远端 Redis 的两级缓存架构,热点数据优先从本地缓存获取,写入时同时更新,这样能够分担热点数据的大部分读压力。缺点是需要修改客户端架构和代码,改造成本较高。 设计熔断/降级机制: 热 Key 极易造成缓存击穿,高峰期请求都直接透传到后端数据库上,从而导致业务雪崩。因此热 Key 的优化一定需要设计系统的熔断/降级机制,在发生击穿的场景下进行限流和服务降级,保护系统的可用性。 |