背景
Redis 是一个非常通用的缓存中间件,使用的很广泛。但是在使用的过程中,出现了一些不规范的使用,造成。于是需要一些规范让 redis 使用者遵守,本文就是
目标
最佳实践优化主要分为下面几个方面:
- 高效使用 Redis 内存
- 保证 Redis 的高性能低延迟
- 保证 Redis 的可靠性
- Redis 运维规范
- Redis 安全
- 其它注意事项
规范
如何 高效使用 Redis 内存 ?
当你的业务应用在 Redis 中存储数据很少时,你可能并不太关心内存资源的使用情况。但随着业务的发展,你的业务存储在 Redis 中的数据就会越来越多。针对内存使用有下面几个建议:
- 限定 key 的长度
最简单直接的内存优化,就是控制 key 的长度。
在开发业务时,你需要提前预估整个 Redis 中写入 key 的量级,如果 key 数量达到了百万级别,那么,过长的 key 名也会占用过多的内存空间。
所以,你需要保证 key 在逻辑清晰的前提下,尽可能把 key 设计的短一些。
例如,原有的 key 为 order:orderid:123,则可以优化为 o:id:123。
这样,Redis 就可以节省大量的内存,这个方案很节省 redis 内存。
- 避免存储 bigkey
同样的,也需要关注 value 的大小,如果存在大量的 bigkey,也会导致 Redis 内存增长过快。同时,客户端在读写 bigkey 时,还有产生性能问题。
所以要避免在 Redis 中存储 bigkey,规则是:
- String:大小控制在 10KB 以下
- List/Hash/Set/ZSet:元素数量控制在 1 万以下
- 选择合适的数据类型
Redis 提供了丰富的数据类型,这些数据类型在实现上,也对内存使用做了优化。具体来说就是,一种数据类型对应多种数据结构来实现:
String、Set 在存储 int 数据时,会采用整数编码存储(整数占的空间小)。Hash、ZSet 在元素数量比较少时(可配置),底层存储结构采用 ziplist(ziplist占的空间小),在存储比较多的数据时,才会转换为哈希表和跳表。
这样的设计就是在数据量不多的前提下,进一步节约内存资源。所以我们应该利用这些特性来优化 Redis 的内存:
-
String、Set:尽可能存储 int 类型数据
-
Hash、ZSet:存储的元素数量控制在转换阈值之下,以压缩列表存储,节约内存
- 设置合理的过期时间
Redis 数据存储在内存中,这也意味着其资源是有限的。你在使用 Redis 时,要把它当做缓存来使用,而不是数据库。
所以,你的应用写入到 Redis 中的数据,尽可能地都设置「过期时间」。
业务应用在 Redis 中查不到数据时,再从后端数据库中加载到 Redis 中。
采用这种方案,可以让 Redis 中只保留经常访问的「热数据」,内存利用率也会比较高。
- 实例设置 maxmemory + 淘汰策略
Redis key 都设置了过期时间,但如果你的业务应用写入量很大,并且过期时间设置得比较久,那么短期间内 Redis 的内存依旧会快速增长。
如果不控制 Redis 的内存上限,也会导致使用过多的内存资源,最后造成内存用满导致 redis 不能正常的工作。
对于这种场景,需要预估业务数据量,然后设置 maxmemory 控制实例的内存上限,这样可以避免 Redis 的内存持续膨胀。
配置了 maxmemory,此时你还要设置数据淘汰策略,而淘汰策略如何选择,你需要结合你的业务特点来决定:
- volatile-lru / allkeys-lru:优先保留最近访问过的数据
- volatile-lfu / allkeys-lfu:优先保留访问次数最频繁的数据(4.0+版本支持)
- volatile-ttl :优先淘汰即将过期的数据
- volatile-random / allkeys-random:随机淘汰数据
- 数据压缩后写入 Redis
可以通过在业务应用中先将数据压缩,再写入到 Redis 中(例如采用 snappy、gzip 等压缩算法)。
当然,客户端在读取时还需要解压缩,在这期间会消耗更多 CPU 资源,同时会增加延迟时间,所以要根据实际情况进行权衡。
保障 Redis 的高性能
一个单机版 Redis 就可以达到 10W QPS,并发量很高,但是如果在使用过程中发生延迟情况,就会与我们的预期不符。
所以,在使用 Redis 时,如何持续发挥它的高性能,避免操作延迟的情况发生,也是我们的关注焦点。
这方面有11个建议
- 避免存储 bigkey
存储 bigkey 除了前面讲到的使用过多内存之外,对 Redis 性能也会有很大影响。
由于 Redis 处理请求是单线程的,当你的应用在写入一个 bigkey 时,更多时间将消耗在内存分配上,这时操作延迟就会增加。同样地,删除一个 bigkey 在释放内存时,也会发生耗时。
而且,当你在读取这个 bigkey 时,也会在网络数据传输上花费更多时间,此时后面待执行的请求就会发生排队,Redis 性能下降。
结论:
所以,你的业务应用尽量不要存储 bigkey,避免操作延迟发生。
如果你确实有存储 bigkey 的需求,你可以把 bigkey 拆分为多个小 key 存储。
- 开启 lazy-free 机制
如果无法避免存储 bigkey,建议开启 Redis 的 lazy-free 机制。(4.0+版本支持)
当开启这个机制后,Redis 在删除一个 bigkey 时,释放内存的耗时操作,将会放到后期去执行,这样可以在最大程度上,避免对线程的影响。当然有时候延期删除会严重影响业务,这样的场景不适用。
- 不使用复杂度过高的命令
Redis 是单线程模型处理请求,除了操作 bigkey 会导致后面请求发生排队之外,在执行复杂度过高的命令时,也会发生这种情况。
因为执行复杂度过高的命令,会消耗更多的 CPU 资源,主线程中的其它请求只能等待,这时也会发生排队延迟。
所以,你需要避免执行例如 SORT、SINTER、SINTERSTORE、ZUNIONSTORE、ZINTERSTORE 等聚合类命令。
对于这种聚合类操作,建议你把它放到业务端或其它的中间件来执行,不要让 Redis 承担太多的计算工作。
- 关注时间复杂度
当你在执行 Redis 命令时,同样需要注意时间复杂度的大小。
如果一次性查询过多的数据,也会在网络传输过程中耗时过长,操作延迟变大。
所以,对于容器类型(List/Hash/Set/ZSet),在元素数量未知的情况下,一定不要执行 LRANGE key 0 -1 / HGETALL / SMEMBERS / ZRANGE key 0 -1。
在查询数据时,需要遵循以下原则:
- 先查询数据元素的数量(LLEN/HLEN/SCARD/ZCARD)
- 元素数量较少,可一次性查询全量数据
- 元素数量非常多,分批查询数据(LRANGE/HASCAN/SSCAN/ZSCAN)
- 关注 DEL 时间复杂度
在删除一个 key 时,如果方法不对,也有可能影响到 Redis 性能。
当你删除的是一个 String 类型 key 时,时间复杂度确实是 O(1)。
但当你要删除的 key 是 List/Hash/Set/ZSet 类型,它的复杂度其实为 O(N),N 代表元素个数。
也就是说,删除一个 key,其元素数量越多,执行 DEL 也就越慢!
原因在于,删除大量元素时,需要依次回收每个元素的内存,元素越多,花费的时间也就越久!
而且,这个过程默认是在主线程中执行的,这势必会阻塞CPU,产生性能问题。
那删除这种元素比较多的 key,如何处理呢?
正确删除元素比较多的 key 的方法:
- List类型:执行多次 LPOP/RPOP,直到所有元素都删除完成
- Hash/Set/ZSet类型:先执行 HSCAN/SSCAN/SCAN 查询元素,再执行 HDEL/SREM/ZREM 依次删除每个元素
- 批量命令代替单个命令
当你需要一次性操作多个 key 时,你应该使用批量命令来处理。
批量操作相比于多次单个操作的优势在于,可以显著减少客户端、服务端的来回网络 IO 次数。
建议是:
- String / Hash 使用 MGET/MSET 替代 GET/SET,HMGET/HMSET 替代 HGET/HSET
- 其它数据类型使用 Pipeline,打包一次性发送多个命令到服务端执行
- 避免集中过期 key
Redis 清理过期 key 是采用定时 + 懒惰的方式来做的,而且这个过程都是在线程中执行。
如果你的业务存在大量 key 集中过期的情况,那么 Redis 在清理过期 key 时,也会有阻塞线程的风险。
想要避免这种情况发生,你可以在设置过期时间时,增加一个随机时间,把这些 key 的过期时间打散,从而降低集中过期对主线程的影响。
- 连接 Redis 的策略
- 业务应该使用长连接操作 Redis,避免短连接。
当使用短连接操作 Redis 时,每次都需要经过 TCP 三次握手、四次挥手,这个过程也会增加操作耗时。
- 客户端应该使用连接池的方式访问 Redis,并设置合理的参数,长时间不操作 Redis 时,需及时释放连接资源。
- 只使用 db0
尽管 Redis 提供了 16 个 db,但仅仅建议使用 db0。
主要有以下 3 点原因:
- 在一个连接上操作多个 db 数据时,每次都需要先执行 SELECT,这会给 Redis 带来额外的压力
- 使用多个 db 的目的是,按不同业务线存储数据,其实可以拆分成多个实例存储。拆分多个实例部署,多个业务线不会互相影响,还能提高 Redis 的访问性能
- Redis Cluster 只支持 db0,如果后期迁移到 Redis Cluster,迁移成本高
- 使用读写分离 + 分片集群
如果业务读请求量很大,那么可以采用部署多个从库的方式,实现读写分离,让 Redis 的从库分担读压力,进而提升性能。
如果业务写请求量很大,单个 Redis 实例已无法支撑这么大的写流量,那么此时你需要使用分片集群,分担写压力。
- 不开启 AOF 或 AOF 配置为每秒刷盘
对于丢失数据不敏感的业务,建议你不开启 AOF,避免 AOF 写磁盘拖慢 Redis 的性能。
如果确实需要开启 AOF,建议你配置为 appendfsync everysec,把数据持久化的刷盘操作,放到后台线程中去执行,尽量降低 Redis 写磁盘对性能的影响。
- 关闭操作系统内存大页机制
Linux 操作系统提供了内存大页机制。好处在于,每次应用程序向操作系统申请内存时,申请单位由之前的 4KB 变为了 2MB。
这样会导致一些问题。
当 Redis 在做数据持久化时,会先 fork 一个子进程,此时主进程和子进程共享相同的内存地址空间。
当主进程需要修改现有数据时,会采用写时复制(Copy On Write)的方式进行操作,在这个过程中,需要重新申请内存。
如果申请内存单位变为了 2MB,那么势必会增加内存申请的耗时,如果此时主进程有大量写操作,需要修改原有的数据,那么在此期间,操作延迟就会变大。
所以,为了避免出现这种问题,需要在操作系统上关闭内存大页机制。
如何保证 Redis 的可靠性?
要从从资源隔离,多副本,故障恢复这三大维度保障 Redis 可靠性。
- 按业务线部署实例
其实就是资源隔离。
需要按不同的业务线来部署 redis 实例,这样做可以避免当一个实例发生故障时,不会影响到其它业务。
- 部署主从集群
单机节点 Redis 会存在机器宕机服务不可用的风险。
所以,需要部署 Redis 主从集群,这样当主库宕机后,依旧有从库可以使用,避免了数据丢失的风险,也降低了服务不可用的时间。
主从库需要分布在不同机器上,避免交叉部署。这么做的原因在于,通常情况下,Redis 的主库会承担所有的读写流量,所以我们一定要优先保证主库的稳定性,即使从库机器异常,也不要对主库造成影响。
同时,我们需要对 Redis 做日常维护,例如数据定时备份等操作,这时你就可以只在从库上进行,这只会消耗从库机器的资源,也避免了对主库的影响。
- 合理配置主从复制参数
在部署主从集群时,如果参数配置不合理,也有可能导致主从复制发生问题:
- 主从复制中断
- 从库发起全量复制,主库性能受到影响
对于上述问题有以下2个建议:
- 设置合理的 repl-backlog 参数:过小的 repl-backlog 在写流量比较大的场景下,主从复制中断会引发全量复制数据的风险
- 设置合理的 slave client-output-buffer-limit:当从库复制发生问题时,过小的 buffer 会导致从库缓冲区溢出,从而导致复制中断
- 部署哨兵集群,实现故障自动切换
只部署了主从节点,但故障发生时是无法自动切换的,所以,你还需要部署哨兵集群,实现故障的自动切换。
而且,多个哨兵节点需要分布在不同机器上,实例为奇数个,防止哨兵选举失败,影响切换时间。
日常运维 Redis 需要注意什么?
- 禁止使用 KEYS/FLUSHALL/FLUSHDB 命令
执行这些命令,会长时间阻塞 Redis 主线程,危害极大,所以必须禁止使用。
如果确实想使用这些命令建议是:
- SCAN 替换 KEYS
- 4.0+版本可使用 FLUSHALL/FLUSHDB ASYNC,清空数据的操作放在后台线程执行
- 扫描线上实例时,设置休眠时间
不管你是使用 SCAN 扫描线上实例,还是对实例做 bigkey 统计分析,我建议你在扫描时一定记得设置休眠时间。
防止在扫描过程中,实例 OPS 过高对 Redis 产生性能抖动。
- 慎用 MONITOR 命令
有时在排查 Redis 问题时,你会使用 MONITOR 查看 Redis 正在执行的命令。
但如果你的 Redis OPS 比较高,那么在执行 MONITOR 会导致 Redis 输出缓冲区的内存持续增长,这会严重消耗 Redis 的内存资源,甚至会导致实例内存超过 maxmemory,引发数据淘汰,这种情况你需要格外注意。
所以在执行 MONITOR 命令时,一定要谨慎,尽量少用。
- 从库必须设置为 slave-read-only
你的从库必须设置为 slave-read-only 状态,避免从库写入数据,导致主从数据不一致。
还有,从库如果是非 read-only 状态,如果你使用的是 4.0 以下的 Redis,它存在这样的 Bug:
从库写入了有过期时间的数据,不会做定时清理和释放内存。
这会造成从库的内存泄露!这个问题直到 4.0 版本才修复,你在配置从库时需要格外注意。
- 合理配置 timeout 和 tcp-keepalive 参数
网络原因会导致你的大量客户端连接与 Redis 意外中断。造成这个问题原因在于,客户端与服务端每建立一个连接,Redis 都会给这个客户端分配了一个 client fd,直到服务端认为超过了 maxclients。
当客户端与服务端网络发生问题时,服务端并不会立即释放这个 client fd,如果 Redis 没有开启 tcp-keepalive 的话,服务端直到配置的 timeout 时间后,才会清理释放这个 client fd。
Redis 内部有一个定时任务,会定时检测所有 client 的空闲时间是否超过配置的 timeout 值。
建议:
- 不要配置过高的 timeout:让服务端尽快把无效的 client fd 清理掉
- Redis 开启 tcp-keepalive:这样服务端会定时给客户端发送 TCP 心跳包,检测连接连通性,当网络异常时,可以尽快清理僵尸 client fd
为了解决主从数据不一致,在调整 maxmemory 时,一定要注意主从库的修改顺序:
- 调大 maxmemory:先修改从库,再修改主库
- 调小 maxmemory:先修改主库,再修改从库
Redis 安全如何保证?
无论如何,在互联网时代,安全问题一定是我们需要随时警戒的。
相关建议建议是:
- 不要把 Redis 部署在公网可访问的服务器上
- 部署时不使用默认端口 6379
- 以普通用户启动 Redis 进程,禁止 root 用户启动
- 限制 Redis 配置文件的目录访问权限
- 推荐开启密码认证
- 禁用/重命名危险命令(KEYS/FLUSHALL/FLUSHDB/CONFIG/EVAL)
如何预防 Redis 出问题?
要想提前预防 Redis 问题,你需要做好以下两个方面:
- 合理的资源规划
- 完善的监控预警
在部署 Redis 时,需要提前做好资源规划,可以避免很多因为资源不足产生的问题。有以下 3 点建议:
-
保证机器有足够的 CPU、内存、带宽、磁盘资源
-
提前做好容量规划,主库机器预留一半内存资源,防止主从机器网络故障,引发大面积全量同步,导致主库机器内存不足的问题
-
单个实例内存建议控制在 10G 以下,大实例在主从全量同步、RDB 备份时有阻塞风险
同时,监控预警是提高稳定性的重要环节,完善的监控预警,可以把问题提前暴露出来,这样我们才可以快速反应,把问题最小化。
这方面的建议是:
- 做好机器 CPU、内存、带宽、磁盘监控,资源不足时及时报警,任意资源不足都会影响 Redis 性能
- 设置合理的 slowlog 阈值,并对其进行监控,slowlog 过多及时报警
- 监控组件采集 Redis INFO 信息时,采用长连接,避免频繁的短连接
- 做好实例运行时监控,重点关注 expired_keys、evicted_keys、latest_fork_usec 指标,这些指标短时突增可能会有阻塞风险