# 六、Redis 性能优化

参考:Redis 性能优化的 13 条优化 (opens new window)

# 1. 客户端优化

  • Pipeline 批量操作

    Pipeline 是客户端提供的一种批处理技术,用于一次处理多个 Redis 命令,从而提高整个交互的性能。

    public class PipelineExample {
        public static void main(String[] args) {
            Jedis jedis = new Jedis("127.0.0.1", 6379);
            // 记录执行开始时间
            long beginTime = System.currentTimeMillis();
            // 获取 Pipeline 对象
            Pipeline pipe = jedis.pipelined();
            // 设置多个 Redis 命令
            for (int i = 0; i < 100; i++) {
                pipe.set("key" + i, "val" + i);
                pipe.del("key"+i);
            }
            // 执行命令
            pipe.sync();
            // 记录执行结束时间
            long endTime = System.currentTimeMillis();
            System.out.println("执行耗时:" + (endTime - beginTime) + "毫秒");
        }
    }
    
  • 连接池的应用

    使用 Redis 连接池,而不是频繁创建销毁 Redis 连接,这样可减少网络传输次数和减少非必要调用指令。

# 2. 设置合理的内存大小

64 位操作系统中 Redis 的内存大小是没有限制的,即配置项maxmemory <bytes> 是被注释掉的,会导致在物理内存不足时,使用 swap 空间既交换空间,当操作系统将 Redis 所用的内存分页移至 swap 空间时,将会阻塞 Redis 进程,导致 Redis 出现延迟,从而影响 Redis 的整体性能。因此需限制 Redis 的内存大小为一个固定的值,当 Redis 的运行到达此值时会触发内存淘汰策略。

# 3. 选择合适的淘汰策略

内存淘汰策略在 Redis 4.0 之后有 8 种:

  • noeviction:不淘汰任何数据,当内存不足时,新增操作会报错,Redis 默认内存淘汰策略;
  • allkeys-lru:淘汰整个键值中最久未使用的键值;
  • allkeys-random:随机淘汰任意键值;
  • volatile-lru:淘汰所有设置了过期时间的键值中最久未使用的键值;
  • volatile-random:随机淘汰设置了过期时间的任意键值;
  • volatile-ttl:优先淘汰更早过期的键值。

Redis 4.0 版本中新增 2 种淘汰策略:

  • volatile-lfu:淘汰所有设置了过期时间的键值中,最少使用的键值;
  • allkeys-lfu:淘汰整个键值中最少使用的键值。

其中 allkeys-xxx 表示从所有的键值中淘汰数据,而 volatile-xxx 表示从设置了过期键的键值中淘汰数据。

根据实际的业务情况进行设置,默认的淘汰策略不淘汰任何数据,在新增时会报错。

# 4. 设置合理的过期时间

对键值设置合理的过期时间, Redis 会自动清除过期的键值对,以节约对内存的占用,以避免键值过多的堆积,频繁触发内存淘汰策略。

# 5. key 与 value 的优化

# 3.1 key 设计

  • 可读性和可管理性,以业务名(或数据库名)为前缀(防止 key 冲突),用冒号分割,比如 业务名:表名:id
  • 简洁性,保证语义的前提下,控制 key 的长度,当 key 较多时,内存占用也不容忽视
  • 不要包含特殊字符,比如:包含空格、换行、单双引号以及其他转义字符

# 3.2 value 设计

  • 拒绝 Big Key(防止网卡流量、慢查询),string 类型的 value 控制在 10KB 以内,hash、list、set、zset 元素个数不要超过 5000。
  • 选择合适的数据类型。
  • 控制 key 的生命周期,Redis 不是垃圾桶,一定要设置过期时间。
  • 在保证完整语义同时,尽量缩短键值对的存储长度,必要时对数据进行序列化和压缩再存储,以 Java 为例,序列化可使用 protostuff 或 kryo,压缩可使用 snappy。

# 3.3 发现 Big Key

Big Key

一般情况下,当 key 的值大于 10KB 时就可以算是 Big Key 了。如下场景都有可能遇到 Big Key:

  • 粉丝列表
  • 统计数据,比如 PV 或者 UV 统计
  • 使用不当的数据缓存,比如通过 string 保存序列化后的用户数据等

常规操作:通过 redis-cli --bigkeys 统计出比较大的 key

redis-cli --bigkeys

输出:

# Scanning the entire keyspace to find biggest keys as well as
# average sizes per key type.  You can use -i 0.1 to sleep 0.1 sec
# per 100 SCAN commands (not usually needed).

[00.00%] Biggest string found so far '"username"' with 5 bytes

-------- summary -------

Sampled 1 keys in the keyspace!
Total key length in bytes is 8 (avg len 8.00)

Biggest string found '"username"' has 5 bytes

1 strings with 5 bytes (100.00% of keys, avg size 5.00)
0 lists with 0 items (00.00% of keys, avg size 0.00)
0 hashs with 0 fields (00.00% of keys, avg size 0.00)
0 streams with 0 entries (00.00% of keys, avg size 0.00)
0 sets with 0 members (00.00% of keys, avg size 0.00)
0 zsets with 0 members (00.00% of keys, avg size 0.00)

精确操作:通过 MEMORY USAGE key 查询

127.0.0.1:6379> MEMORY USAGE username
(integer) 63

命令 MEMORY USAGE 给出一个 key 和它值在 RAM 中所占的字节数,返回的结果是 key 的值以及为管理该 key 分配的内存总字节数。

所以查询 Big Key 的手段就可以使用脚本进行查询,大概思路就是使用 scan 游标查询 key,然后使用 MEMORY USAGE key 获取这个 key 与 value 的字节数,这样就能很方便的得出结论进行优化。

# 3.4 删除 Big Key

Redis 4.0 新特性 —— Lazy Free

  • 当删除 key 的时候,Redis 提供异步延时释放 key 内存的功能,把 key 的是否操作放在 BIO(Background IO)单独的子线程处理中,减少删除 Big Key 对 Redis 主线程的阻塞。有效地避免删除 Big Key 带来的性能和可用性的问题。因此删除 Big Key 时使用 unlink 操作。

    127.0.0.1:6379> unlink key1
    (integer) 1
    

Redis 4.0 之前

  • 使用 hscan、ltrim、sscan、zscan 进行操作来实现删除。

一般来说,对于 string类型的数据进行 del 不会产生阻塞。

# 6. 禁止使用耗时操作指令

  • 禁止使用 keys 命令,因为这个命令会去遍历所有 key。可以使用 scan 命令进行分批的、游标式的遍历。
  • 删除(del)一个大数据的时候,可能会需要很长时间,所以建议用异步删除的方式 unlink,它会启动一个新的线程来删除目标数据,而不阻塞 Redis 的主线程。
  • 避免一次查询所有的成员,要使用 scan 命令进行分批的,游标式的遍历;
  • 通过机制严格控制 Hash、Set、Sorted Set 等结构的数据大小;
  • 将排序、并集、交集等操作放在客户端执行,以减少 Redis 服务器运行压力;

# 7. 使用 slowlog 优化耗时命令

我们可以使用 slowlog 功能找出最耗时的 Redis 命令进行相关的优化,以提升 Redis 的运行速度。

慢查询有两个重要的配置项:

  • slowlog-log-slower-than 10000:用于设置慢查询的评定时间,也就是说超过此配置项的命令将会被当场满操作记录在慢日志当中,它执行单位是微妙(1 秒等于 1,000,000 微妙)。
  • slowlog-max-len:用来配置慢查询日志的最大记录数。

使用 slowlog get n 来获取相关的慢查询日志,再找到这些慢查询对应的业务来进行相关的优化:

redis 127.0.0.1:6379> slowlog get 2
1) 1) (integer) 14
   2) (integer) 1309448221
   3) (integer) 15
   4) 1) "ping"
2) 1) (integer) 13
   2) (integer) 1309448128
   3) (integer) 30
   4) 1) "slowlog"
      2) "get"
      3) "100"

每一个条目由 4 个字段组成:

  1. 每个慢查询条目的唯一递增标识符
  2. 处理记录命令的 unix 时间戳
  3. 命令执行所需的总时间,以微妙为单位
  4. 组成该命令的参数数组

# 8. lazy free 特性

在删除时提供异步延时释放键值的功能,把键值释放操作放在 BIO(Background I/O) 单独的子线程处理中,以减少删除对 Redis 主线程的阻塞,可有效地避免删除 big key 带来的性能和可用性问题

lazy free 对应了4 种场景,默认都是关闭(lazy free 特性是 Redis 4.0 新增功能。)

lazyfree-lazy-eviction no lazyfree-lazy-expire no lazyfree-lazy-server-del no slave-lazy-flush no

场景含义说明:

  • lazyfree-lazy-eviction:当 Redis 运行内存超过 maxmeory 时,是否开启 lazy free 机制删除;
  • lazyfree-lazy-expire:设置过期时间的键值,当过期之后是否开启 lazy free 机制删除;
  • lazyfree-lazy-server-del:有些指令在处理已存在的键时,会带有隐式的 del 键的操作,比如 rename 命令,当目标键已存在,Redis 会先删除目标键,如果这些目标键是一个 big key,就会造成阻塞删除的问题,此配置表示是否开启 lazy free 机制删除;
  • slave-lazy-flush:针对 slave(从节点) 进行全量数据同步,slave 在加载 master 的 RDB 文件前,会运行 flushall 来清理自己的数据,表示此时是否开启 lazy free 机制删除。

建议开启 lazyfree-lazy-eviction、lazyfree-lazy-expire、lazyfree-lazy-server-del 等配置,可有效的提高主线程的执行效率

# 9. 使用物理机而非虚拟机

在虚拟机中运行 Redis 服务器,和物理机共享一个物理网口,并且一台物理机可能有多个虚拟机在运行,因此在内存占用上和网络延迟方面会有很糟糕的表现,可通过以下查看延迟时间,如果对 Redis 的性能有较高要求的话,应尽可能在物理机上直接部署 Redis 服务器。

./redis-cli --intrinsic-latency 100

# 10. 使用混合持久化

Redis 的持久化策略是将内存数据复制到硬盘上,才可以进行容灾恢复或者数据迁移,但维护此持久化的功能,需要很大的性能开销。

在 Redis 4.0 之后版本,Redis 有 3 种持久化的方式:

  • AOF(Append Only File,文件追加方式),记录所有的操作命令,并以文本的形式追加到文件中;
  • RDB(Redis DataBase,快照方式)将某一个时刻的内存数据,以二进制的方式写入磁盘;
  • 混合持久化方式,Redis 4.0 之后新增的方式,混合持久化是结合了 RDB 和 AOF 的优点,在写入的时候,先把当前的数据以 RDB 的形式写入文件的开头,再将后续的操作命令以 AOF 的格式存入文件,这样既能保证 Redis 重启时的速度,又能减低数据丢失的风险。

AOF 和 RDB 持久化各有利弊,RDB 可能会导致一定时间内的数据丢失, AOF 由于文件较大则会影响 Redis 的启动速度,为了能同时拥有 RDB 和 AOF 优点,Redis 4.0 之后新增混合持久化的方式,因此在必要进行持久化操作时,应选择混合持久化的方式。

查询是否开启混合持久化可以使用命令:

config get aof-use-rdb-preamble

开启混合持久化有两种方式:

  1. 命令行:config set aof-use-rdb-preamble yes
  2. 配置文件:aof-use-rdb-preamble yes

特殊的:在非必须进行持久化的业务中,可以关闭持久化,这样可以有效的提升 Redis 的运行速度,不会出现间歇性卡顿的困扰。

# 11. 避免大量数据同时失效

Redis 过期键值删除使用的是贪心策略,每秒会进行 10 次过期扫描,此配置可在 redis.conf 进行配置,默认值是 hz 10,Redis 会随机抽取 20 个值,删除这 20 个键中过期的键,如过期 key 的比例超过 25% ,重复执行此流程,如下图:

img

大型系统中有大量缓存在同一时间同时过期,那么会导致 Redis 循环多次持续扫描删除过期字典,直到过期字典中过期键值被删除的比较稀疏为止,而在整个执行过程会导致 Redis 的读写出现明显的卡顿,卡顿的另一种原因是内存管理器需要频繁回收内存页,也会消耗一定的 CPU。

为了避免这种卡顿现象产生,需预防大量的缓存在同一时刻一起过期,简单解决方案:在过期时间的基础上添加一个指定范围随机数

# 12. 禁用 THP 特性

Linux kernel 在 2.6.38 内核增加 Transparent Huge Pages (THP) 特性 ,支持大内存页 2MB 分配,默认开启。

当开启了 THP 时,fork 的速度会变慢,fork 之后每个内存页从原来 4KB 变为 2MB,会大幅增加重写期间父进程内存消耗。同时每次写命令引起的复制内存页单位放大了 512 倍,会拖慢写操作的执行时间,导致大量写操作慢查询。例如简单的 incr 命令也会出现在慢查询中,因此 Redis 建议将此特性进行禁用,禁用方法如下:

echo never > /sys/kernel/mm/transparent_hugepage/enabled

为了使机器重启后 THP 配置依然生效,可以在 /etc/rc.local 中追加 echo never > sys/kernel/mm/transparent_hugepage/enabled

# 13. 使用分布式架构增加读写速度

Redis 分布式架构方法:

  • 主从同步
  • 哨兵模式
  • Redis Cluster 集群

主从同步可把写入放到主库上执行,把读功能转移到从服务上,可在单位时间内处理更多的请求,从而提升的 Redis 整体的运行速度。

哨兵模式是对于主从功能的升级,但当主节点奔溃之后,无需人工干预就能自动恢复 Redis 的正常使用。

Redis Cluster 是 Redis 3.0 正式推出的,Redis 集群是通过将数据库分散存储到多个节点上来平衡各个节点的负载压力。Redis Cluster 采用虚拟哈希槽分区,所有的键根据哈希函数映射到 0 ~ 16383 整数槽内,计算公式:slot = CRC16(key) & 16383,每一个节点负责维护一部分槽以及槽所映射的键值数据。这样 Redis 就可把读写压力从一台服务器,分散给多台服务器了,因此性能会有很大的提升。

在三个方法中,只需使用一个即可, Redis Cluster 是首选的实现方案,它可把读写压力自动的分担给更多的服务器,并且拥有自动容灾的能力。

上次更新: 9/17/2022, 12:37:15 AM