1. 架构

1.1 部署方式

如果业务的读请求量很大,可以部署多个从库,实现读写分离,从而提升读性能。如果业务的写请求量很大,可以部署 Redis 集群,通过多个分片来分担写压力。

Redis 在做数据持久化时会 fork 子进程,虚拟机执行 fork 相较物理机会更慢,所以 Redis 应该尽量部署在物理机上。

按业务线将资源进行隔离,分别部署不同的 Redis,通过隔离避免发生故障时影响到其他业务。

1.2 持久化

对于丢失数据不敏感的业务,不用开启 AOF 持久化,因为 AOF 会频繁写磁盘,影线整体性能。

如果需要开启 AOF,建议配置为每秒同步一次,将数据持久化的刷盘操作放到后台线程去执行,降低写磁盘对性能的影响。

2. 键值设计

2.1 禁用部份命令

生产环境应该禁用部份全局的命令。

keys

在高并发、大数据量的场景下,keys * 一次性返回所有键,有可能会导致 CPU 使用率飙升、客户端内存溢出、长时间阻塞 Redis 处理线程,不建议使用,在生产环境应当禁用 keys *。。

建议使用 scan 命令替代 keys *,它通过游标分批次返回数据,分散了服务器的压力,避免长时间阻塞主线程。

scan 命令的优势在于:

  • Redis 集群中,keys * 只能针对单个节点操作,而 scan 支持跨节点遍历;
  • 在渐进式 rehash 时,keys * 可能会产生键重复或遗漏,而 scan 会同时扫描 ht[0] 与 ht[1];

scan 命令的劣势:

  • scan 不保证实时一致性,遍历期间若有数据新增删除,会导致键重复或遗漏;
  • 实际返回数量可能多于或少于参数 count 指定值;

monitor

monitor 命令会把监控到的内容持续写入输出缓冲区,如果操作命令很多,会导致缓冲区溢出,谨慎使用。

全量操作命令

对于一些结构体的全量遍历命令,如哈希的 hgetall、列表的 lrange、集合的 smembers、有序集合的 zrange,时间复杂度为 O(N),如果 N 特别大,则要避免进行全量扫描,应该每次扫描其中一部份,分词执行。

2.2 bigkey

bigkey 是指存储了较大量数据的键值对,Redis 中应当避免产生 bigkey,建议 string 类型控制在 10KB 以内,hash、list、set、zset 控制在 5000 个元素以内。

bigkey 的危害:

  • 操作 bigkey 会长时间阻塞 Redis 主线程,影响服务使用;
  • 集群内的 bigkey 会导致节点内存不平衡;
  • 读取 bigkey 会产生较大网络流量和阻塞;

发现 bigkey 的方式:

# 找出 bigkey
redis-cli --bigkeys

# 计算键值对占用字节数
memory usage <key>

bigkey 删除 bigkey,stirng 类型使用 del 命令删除,其他类型使用 hscan、sscan、zscan 的方式渐进式地删除,同时避免因为键过期导致的主动删除。

避免 bigkey 的方法:

  • 数据拆分,水平拆分如将 hash 按字段分片存到多个 key,垂直拆分如按业务纬度分离数据;
  • 选择合适的数据结构,用 bitmap 替代 set 存储状态信息,用 hyperloglog 替代 set 进行 UV 统计,用 stream 替代 list 实现消息队列;
  • 对临时数据设置过期时间;

从 Redis 4.0 开始,支持 lazy-free 机制,开启后,Redis 在删除一个 bigkey 释放内存的操作,将会放到后台线程去执行,避免对主线程影响。

2.3 过期时间与淘汰策略

我们把 Redis 当成缓存来使用,通过缓存数据库的热点数据,以提高系统并发性能和响应速度。但缓存不是数据库,不应该保存数据库的所有数据,因此我们对于写入 Redis 的数据,应该尽可能都设置合理的过期时间。

大量 key 集中在某个时间过期,可能造成 Redis 清理时阻塞主线程,也可能导致很多访问未命中索引而全部去访问数据库,对数据库造成压力。为了避免 key 的集中过期,在设置过期时间时,可以增加一个随机时间,将过期时间打散。

另外,Redis 的数据保存在内存中,相比硬盘容量有限,应该根据实际设置最大内存和内存淘汰策略,以最大化发挥缓存的作用。

2.4 压缩对象

存储较大的对象时,可以先将其压缩,然后再存入 Redis,以节省内存消耗。

3. 命令

3.1 批量命令代替单个命令

每次向 Redis 发送命令时,都包含通过网络发送命令、Redis 执行命令、通过网络接收返回这三部份。

对于同类型命令的多次操作,可以通过 pipeline 或者批量命令来处理,减少客户端和服务器来回的网络 I/O 次数。

string 类型将 get/set 替换为 mget/mset,hash 类型将 hget/hset 替换为 hmget/hmset,其他数据类型使用 pipeline 打包发送命令。

4. 问题排查

4.1 阻塞

Redis 的命令执行是单线程架构,所有读写操作都是在一个主线程中完成,在高并发场景下,当出现了阻塞,会对 Redis 的读写产生影响。

API 或数据结构使用不合理

对其中一个比较大的键进行复杂的操作,如对一个上万元素的 hash 结构进行 hgetall 操作,执行速度必然会比较慢。

在高并发场景中,应该尽量避免在大对象执行时间复杂度超过 O(n) 的命令。

获取最近的n条慢查询命令:

slowlog get <n>

通过分段 scan 操作发现大对象,并将最大对象统计出来:

redis-cli --bigkeys

优化调整:

  • 禁用 keys、sort 等命令;
  • 调整使用低时间复杂度的命令,如 hgetall 改为 hmget;
  • 将大对象拆分为多个小对象,防止一次操作过多数据;

key 集中过期淘汰

当很多 key 过期淘汰时,异步清理过期 key 的线程会持续地扫描清理,导致性能波动。

可以为同时过期的一批 key 的过期时间再增加一个随机数,打散过期时间。

CPU 使用率过高

当 Redis 将单核 CPU 使用率跑到接近 100%,将导致 Redis 无法处理更多的命令。

通过 top 命令可以看到 Redis 进程的 CPU 使用率。

Redis 提供了命令统计当前 Redis 的使用情况,并每秒输出一行信息:

redis-cli --stat

持久化导致的阻塞

Redis 主线程调用 fork 产生共享内存的子进程,来完成持久化文件的重写,当本身内存占用较大时,fork 操作本身耗时过长,会导致主线程的阻塞。通过 info status 命令获取 latest_fork_usec 指标,表示最近一次 fork 操作耗时。

开启 AOF 持久化时,后台线程定时对 AOF 文件做 fsync 操作,当硬盘压力过大时,fsync 操作需要等待直到写入完成,也可能会阻塞主线程。通过 info persistence 命令获取 aof_delayed_fsync 指标,表示 fsync 等待时间。

Redis 利用 Linux 写时复制技术降低内存开销,只有写操作时才复制要修改的内存页,也可能在大量写操作时拖慢执行时间。

CPU 竞争

在服务器上,其他进程消耗 CPU 资源较多时也会影响 Redis 进程,需要考虑将同样消耗 CPU 的程序部署到不同服务器上。

部署 Redis 绑定到 CPU 时,虽然可以降低 CPU 频繁切换上下文的开销,但如果创建子进程进行 RDB/AOF 重写,父进程与子进程共享一个 CPU,会产生 CPU 竞争。

内存交换

当操作系统把 Redis 使用的部分内存,通过内存交换(swap)换出硬盘,会导致 Redis 的性能急剧下降。

根据进程 id 查询内存交换信息:

cat /proc/12345/smaps | grep Swap

开启内存大页

Linux 默认内存页为 4KB,后期版本支持内存大页机制,支持 2MB 大小的内存页分配。

如果开启了内存大页,生成 RDB 文件时,即使修改的数据很少,也需要复制一个 2MB 的大页,写命令多时会导致大量复制内存页操作,导致性能变差。可以禁用 Linux 的内存大页功能。

网络问题

当网络异常时,会导致连接拒绝、网络延迟等问题。