摘要
redis的拥有众多优点,但是的技术有利有弊,所以只有在redis最擅长的场景中才能让redis的作用发挥到最大的作用。同样的redis一样存在很多优化和改进的点。
一、Redis的性能测试
- 技术选型,比如测试 Memcached 和 Redis;
- 对比单机 Redis 和集群 Redis 的吞吐量;
- 评估不同类型的存储性能,例如集合和有序集合;
- 对比开启持久化和关闭持久化的吞吐量;
- 对比调优和未调优的吞吐量;
- 对比不同 Redis 版本的吞吐量,作为是否升级的一个参考标准。
1.1 性能测试的几种方式
- 编写代码模拟并发进行性能测试;
- 使用 redis-benchmark 进行测试。
因为自己编写代码进行性能测试的方式不够灵活,且很难短时间内模拟大量的并发数,所有作者并不建议使用这种方式。幸运的是 Redis 本身给我们提供了性能测试工具 redis-benchmark(Redis 基准测试),因此我重点来介绍redis-benchmark 的使用。
1.2 基准测试实战
redis-benchmark 位于 Redis 的 src 目录下,我们可以使用 ./redis-benchmark -h
来查看基准测试的使用,执行结果如下:
Usage: redis-benchmark [-h <host>] [-p <port>] [-c <clients>] [-n <requests>] [-k <boolean>]
-h <hostname> Server hostname (default 127.0.0.1)
-p <port> Server port (default 6379)
-s <socket> Server socket (overrides host and port)
-a <password> Password for Redis Auth
-c <clients> Number of parallel connections (default 50)
-n <requests> Total number of requests (default 100000)
-d <size> Data size of SET/GET value in bytes (default 3)
--dbnum <db> SELECT the specified db number (default 0)
-k <boolean> 1=keep alive 0=reconnect (default 1)
-r <keyspacelen> Use random keys for SET/GET/INCR, random values for SADD
Using this option the benchmark will expand the string __rand_int__
inside an argument with a 12 digits number in the specified range
from 0 to keyspacelen-1. The substitution changes every time a command
is executed. Default tests use this to hit random keys in the
specified range.
-P <numreq> Pipeline <numreq> requests. Default 1 (no pipeline).
-e If server replies with errors, show them on stdout.
(no more than 1 error per second is displayed)
-q Quiet. Just show query/sec values
--csv Output in CSV format
-l Loop. Run the tests forever
-t <tests> Only run the comma separated list of tests. The test
names are the same as the ones produced as output.
-I Idle mode. Just open N idle connections and wait.
----------------------------------------------------------------------------------------
可以看出 redis-benchmark 支持以下选项:
-h <hostname>:服务器的主机名(默认值为 127.0.0.1)。
-p <port>:服务器的端口号(默认值为 6379)。
-s <socket>:服务器的套接字(会覆盖主机名和端口号)。
-a <password>:登录 Redis 时进行身份验证的密码。
-c <clients>:并发的连接数量(默认值为 50)。
-n <requests>:发出的请求总数(默认值为 100000)。
-d <size>:SET/GET 命令所操作的值的数据大小,以字节为单位(默认值为 2)。
–dbnum <db>:选择用于性能测试的数据库的编号(默认值为 0)。
-k <boolean>:1 = 保持连接;0 = 重新连接(默认值为 1)。
-r <keyspacelen>:SET/GET/INCR 命令使用随机键,SADD 命令使用随机值。通过这个选项,基准测试会将参数中的 __rand_int__ 字符串替换为一个 12 位的整数,这个整数的取值范围从 0 到 keyspacelen-1。每次执行一条命令的时候,用于替换的整数值都会改变。通过这个参数,默认的测试方案会在指定范围之内尝试命中随机键。
-P <numreq>:使用管道机制处理 <numreq> 条 Redis 请求。默认值为 1(不使用管道机制)。
-q:静默测试,只显示 QPS 的值。
–csv:将测试结果输出为 CSV 格式的文件。
-l:循环测试。基准测试会永远运行下去。
-t <tests>:基准测试只会运行列表中用逗号分隔的命令。测试命令的名称和结果输出产生的名称相同。
-I:空闲模式,只会打开 N 个空闲的连接,然后等待。
可以看出 redis-benchmark 带的功能还是比较全的。
# 使用 ./redis-benchmark -t set,get,incr -n 1000000 -q 命令,来对 Redis 服务器进行精简测试,测试结果如下:
[@xjl:src]$ ./redis-benchmark -t set,get,incr -n 1000000 -q
SET: 81726.05 requests per second
GET: 81466.40 requests per second
INCR: 82481.03 requests per second
其中 -t 表示指定测试指令,-n 设置每个指令测试 100w 次。
# 我们测试一下 Pipeline 的吞吐量能到达多少,执行命令如下:
[@xjl:src]$ ./redis-benchmark -t set,get,incr -n 1000000 -q -P 10
SET: 628535.50 requests per second
GET: 654450.25 requests per second
INCR: 647249.19 requests per second
我们发现 Pipeline 的测试很快就执行完了,同样是每个指令执行 100w 次,可以看出 Pipeline 的性能几乎是普通命令的 8 倍, -P 10 表示每次执行 10 个 Redis 命令。
1.3 基准测试的影响元素
为什么每次执行 10 个 Redis 命令,Pipeline 的效率为什么达不到普通命令的 10 倍呢?
这是因为基准测试会受到很大外部因素的影响,例如以下几个:
- 网络带宽和网络延迟可能是 Redis 操作最大的性能瓶颈,比如有 10w q/s,平均每个请求负责传输 8 KB 的字符,那我们需要的理论带宽是 7.6 Gbits/s,如果服务器配置的是 1 Gbits/s,那么一定会有很多信息在排队等候传输,因此运行效率可想而知,这也是很多 Redis 生产坏境之所以效率不高的原因;
- CPU 可能是 Redis 运行的另一个重要的影响因素,如果 CPU 的计算能力跟不上 Redis 要求的话,也会影响 Redis 的运行效率;
- 如果 Redis 运行在虚拟设备上,性能也会受影响,因为普通操作在虚拟设备上会有额外的消耗;
- 普通操作和批量操作(Pipeline)对 Redis 的吞吐量也有很大的影响。
二、Redis的慢优化与性能优化
Redis 慢查询作用和 MySQL 慢查询作用类似,都是为我们查询出不合理的执行命令,然后让开发人员和运维人员一起来规避这些耗时的命令,从而让服务器更加高效和健康的运行。对于单线程的 Redis 来说,不合理的使用更是致命的,因此掌握 Redis 慢查询技能对我们来说非常的关键。
2.1 慢查询命令
在开始之前,我们先要了解一下 Redis 中和慢查询相关的配置项,Redis 慢查询重要的配置项有以下两个:
- slowlog-log-slower-than:用于设置慢查询的评定时间,也就是说超过此配置项的命令,将会被当成慢操作记录在慢查询日志中,它执行单位是微秒(1 秒等于 1000000 微秒);
- slowlog-max-len:用来配置慢查询日志的最大记录数。
#可以看出慢查询的临界值是 10000 微秒,默认保存 128 条慢查询记录。
#慢查询判断时间
127.0.0.1:6379> config get slowlog-log-slower-than
1) "slowlog-log-slower-than"
2) "10000"
#慢查询最大记录条数
127.0.0.1:6379> config get slowlog-max-len
1) "slowlog-max-len"
2) "128"
2.2 慢查询实战
1、我们先来设置慢查询的判断时间为 0 微秒,这样所有的执行命令都会被记录,设置命令如下:
127.0.0.1:6379> config set slowlog-log-slower-than 0
OK
接下来我们执行两条插入命令:
127.0.0.1:6379> set msg xiaoming
OK
127.0.0.1:6379> set lang java
OK
# 我们使用 slowlog show 来查询慢日志,结果如下:
127.0.0.1:6379> slowlog get #慢日志查询
1) 1) (integer) 2 #慢日志下标
2) (integer) 1581994139 #执行时间
3) (integer) 5 #花费时间 (单位微秒)
4) 1) "set" #执行的具体命令
2) "lang"
3) "java"
5) "127.0.0.1:47068"
6) ""
2) 1) (integer) 1
2) (integer) 1581994131
3) (integer) 6
4) 1) "set"
2) "msg"
3) "xiaoming"
5) "127.0.0.1:47068"
6) ""
3) 1) (integer) 0
2) (integer) 1581994093
3) (integer) 5
4) 1) "config"
2) "set"
3) "slowlog-log-slower-than"
4) "0"
5) "127.0.0.1:47068"
6) ""
# 加上本身的设置命令一共有三条“慢操作”记录,按照插入的顺序倒序存入慢查询日志中。小贴士:当慢查询日志超过设定的最大存储条数之后,会把最早的执行命令依次舍弃。
# 查询指定条数慢日志,语法:slowlog get n。
127.0.0.1:6379> slowlog get 2 #查询两条
1) 1) (integer) 20
2) (integer) 1581997567
3) (integer) 14
4) 1) "slowlog"
2) "get"
3) "4"
5) "127.0.0.1:47068"
6) ""
2) 1) (integer) 19
2) (integer) 1581997544
3) (integer) 11
4) 1) "slowlog"
2) "get"
3) "3"
5) "127.0.0.1:47068"
6) ""
# 获取慢查询队列长度,语法:slowlog len。
127.0.0.1:6379> slowlog len
(integer) 16
# 清空慢查询日志,使用 slowlog reset 来清空所有的慢查询日志,执行命令如下:
127.0.0.1:6379> slowlog reset
import redis.clients.jedis.Jedis;
import redis.clients.jedis.util.Slowlog;
import utils.JedisUtils;
import java.util.List;
/**
* 慢查询
*/
public class SlowExample {
public static void main(String[] args) {
Jedis jedis = JedisUtils.getJedis();
// 插入慢查询(因为 slowlog-log-slower-than 设置为 0,所有命令都符合慢操作)
jedis.set("db", "java");
jedis.set("lang", "java");
// 慢查询记录的条数
long logLen = jedis.slowlogLen();
// 所有慢查询
List<Slowlog> list = jedis.slowlogGet();
// 循环打印
for (Slowlog item : list) {
System.out.println("慢查询命令:"+ item.getArgs()+
" 执行了:"+item.getExecutionTime()+" 微秒");
}
// 清空慢查询日志
jedis.slowlogReset();
}
}
-------------------------------------------------------------------------
以上代码执行结果如下:
慢查询命令:[SLOWLOG, len] 执行了:1 微秒
慢查询命令:[SET, lang, java] 执行了:2 微秒
慢查询命令:[SET, db, java] 执行了:4 微秒
慢查询命令:[SLOWLOG, reset] 执行了:155 微秒
2.3 Redis 性能优化方案
Redis 是基于单线程模型实现的,也就是 Redis 是使用一个线程来处理所有的客户端请求的,尽管 Redis 使用了非阻塞式 IO,并且对各种命令都做了优化(大部分命令操作时间复杂度都是 O(1)),但由于 Redis 是单线程执行的特点,因此它对性能的要求更加苛刻,本文我们将通过一些优化手段,让 Redis 更加高效的运行。
- 缩短键值对的存储长度;
- 使用 lazy free(延迟删除)特性;
- 设置键值的过期时间;
- 禁用耗时长的查询命令;
- 使用 slowlog 优化耗时命令;
- 使用 Pipeline 批量操作数据;
- 避免大量数据同时失效;
- 客户端使用优化;
- 限制 Redis 内存大小;
- 使用物理机而非虚拟机安装 Redis 服务;
- 检查数据持久化策略;
- 使用分布式架构来增加读写速度。
键值对的长度是和性能成反比的,比如我们来做一组写入数据的性能测试,执行结果如下:
数据量 | key 大小 | value 大小 | string:set 平均耗时 | hash:hset 平均耗时 |
---|---|---|---|---|
100w | 20byte | 512byte | 1.13 微秒 | 10.28 微秒 |
100w | 20byte | 200byte | 0.74 微秒 | 8.08 微秒 |
100w | 20byte | 100byte | 0.65 微秒 | 7.92 微秒 |
100w | 20byte | 50byte | 0.59 微秒 | 6.74 微秒 |
100w | 20byte | 20byte | 0.55 微秒 | 6.60 微秒 |
100w | 20byte | 5byte | 0.53 微秒 | 6.53 微秒 |
从以上数据可以看出,在 key 不变的情况下,value 值越大操作效率越慢,因为 Redis 对于同一种数据类型会使用不同的内部编码进行存储,比如字符串的内部编码就有三种:int(整数编码)、raw(优化内存分配的字符串编码)、embstr(动态字符串编码),这是因为 Redis 的作者是想通过不同编码实现效率和空间的平衡,然而数据量越大使用的内部编码就越复杂,而越是复杂的内部编码存储的性能就越低。
这还只是写入时的速度,当键值对内容较大时,还会带来另外几个问题:
- 内容越大需要的持久化时间就越长,需要挂起的时间越长,Redis 的性能就会越低;
- 内容越大在网络上传输的内容就越多,需要的时间就越长,整体的运行速度就越低;
- 内容越大占用的内存就越多,就会更频繁地触发内存淘汰机制,从而给 Redis 带来了更多的运行负担。
因此在保证完整语义的同时,我们要尽量地缩短键值对的存储长度,必要时要对数据进行序列化和压缩再存储,以 Java 为例,序列化我们可以使用 protostuff 或 kryo,压缩我们可以使用 snappy。
使用 lazy free 特性
lazy free 特性是 Redis 4.0 新增的一个非常实用的功能,它可以理解为惰性删除或延迟删除。意思是在删除的时候提供异步延时释放键值的功能,把键值释放操作放在 BIO(Background I/O)单独的子线程处理中,以减少删除对 Redis 主线程的阻塞,可以有效地避免删除 big key 时带来的性能和可用性问题。
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 等配置,这样就可以有效的提高主线程的执行效率
设置键值的过期时间
我们应该根据实际的业务情况,对键值设置合理的过期时间,这样 Redis 会帮你自动清除过期的键值对,以节约对内存的占用,以避免键值过多的堆积,频繁的触发内存淘汰策略。
禁用耗时长的查询命令
Redis 绝大多数读写命令的时间复杂度都在 O(1) 到 O(N) 之间, O(1) 表示可以安全使用的,而 O(N) 就应该当心了,N 表示不确定,数据越大查询的速度可能会越慢。因为 Redis 只用一个线程来做数据查询,如果这些指令耗时很长,就会阻塞 Redis,造成大量延时。
要避免 O(N) 命令对 Redis 造成的影响,可以从以下几个方面入手改造:
- 决定禁止使用 keys 命令;
- 避免一次查询所有的成员,要使用 scan 命令进行分批的,游标式的遍历;
- 通过机制严格控制 Hash、Set、Sorted Set 等结构的数据大小;
- 将排序、并集、交集等操作放在客户端执行,以减少 Redis 服务器运行压力;
- 删除(del)一个大数据的时候,可能会需要很长时间,所以建议用异步删除的方式 unlink,它会启动一个新的线程来删除目标数据,而不阻塞 Redis 的主线程。
使用 slowlog 优化耗时命令
我们可以使用 slowlog 功能找出最耗时的 Redis 命令进行相关的优化,以提升 Redis 的运行速度,慢查询有两个重要的配置项:
- slowlog-log-slower-than:用于设置慢查询的评定时间,也就是说超过此配置项的命令,将会被当成慢操作记录在慢查询日志中,它执行单位是微秒(1 秒等于 1000000 微秒);
- slowlog-max-len:用来配置慢查询日志的最大记录数。
我们可以根据实际的业务情况进行相应的配置,其中慢日志是按照插入的顺序倒序存入慢查询日志中,我们可以使用 slowlog get n
来获取相关的慢查询日志,再找到这些慢查询对应的业务进行相关的优化。
使用 Pipeline 批量操作数据
Pipeline(管道技术)是客户端提供的一种批处理技术,用于一次处理多个 Redis 命令,从而提高整个交互的性能。
避免大量数据同时失效
Redis 过期键值删除使用的是贪心策略,它每秒会进行 10 次过期扫描,此配置可在 redis.conf 进行配置,默认值是 hz 10
,Redis 会随机抽取 20 个值,删除这 20 个键中过期的键,如果过期 key 的比例超过 25%,重复执行此流程,如下图所示:
如果在大型系统中有大量缓存在同一时间同时过期,那么会导致 Redis 循环多次持续扫描删除过期字典,直到过期字典中过期键值被删除的比较稀疏为止,而在整个执行过程会导致 Redis 的读写出现明显的卡顿,卡顿的另一种原因是内存管理器需要频繁回收内存页,因此也会消耗一定的 CPU。为了避免这种卡顿现象的产生,我们需要预防大量的缓存在同一时刻一起过期,最简单的解决方案就是在过期时间的基础上添加一个指定范围的随机数。
客户端使用优化
在客户端的使用上我们除了要尽量使用 Pipeline 的技术外,需注意要尽量使用 Redis 连接池,而不是频繁创建销毁 Redis 连接,这样就可以减少网络传输次数和减少了非必要调用指令。
限制 Redis 内存大小
在 64 位操作系统中 Redis 的内存大小是没有限制的,也就是配置项 maxmemory <bytes>
是被注释掉的,这样就会导致在物理内存不足时,使用 swap 空间既交换空间,而当操心系统将 Redis 所用的内存分页移至 swap 空间时,将会阻塞 Redis 进程,导致 Redis 出现延迟,从而影响 Redis 的整体性能。因此我们需要限制 Redis 的内存大小为一个固定的值,当 Redis 的运行到达此值时会触发内存淘汰策略,内存淘汰策略在 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 表示从设置了过期键的键值中淘汰数据。我们可以根据实际的业务情况进行设置,默认的淘汰策略不淘汰任何数据,在新增时会报错。
使用物理机而非虚拟机
在虚拟机中运行 Redis 服务器,因为和物理机共享一个物理网口,并且一台物理机可能有多个虚拟机在运行,因此在内存占用上和网络延迟方面都会有很糟糕的表现,我们可以通过 ./redis-cli --intrinsic-latency 100
命令查看延迟时间,如果对 Redis 的性能有较高要求的话,应尽可能在物理机上直接部署 Redis 服务器。
检查数据持久化策略
Redis 的持久化策略是将内存数据复制到硬盘上,这样才可以进行容灾恢复或者数据迁移,但维护此持久化的功能,需要很大的性能开销。
在 Redis 4.0 之后,Redis 有 3 种持久化的方式:
- RDB(Redis DataBase,快照方式)将某一个时刻的内存数据,以二进制的方式写入磁盘;
- AOF(Append Only File,文件追加方式),记录所有的操作命令,并以文本的形式追加到文件中;
- 混合持久化方式,Redis 4.0 之后新增的方式,混合持久化是结合了 RDB 和 AOF 的优点,在写入的时候,先把当前的数据以 RDB 的形式写入文件的开头,再将后续的操作命令以 AOF 的格式存入文件,这样既能保证 Redis 重启时的速度,又能减低数据丢失的风险。
RDB 和 AOF 持久化各有利弊,RDB 可能会导致一定时间内的数据丢失,而 AOF 由于文件较大则会影响 Redis 的启动速度,为了能同时拥有 RDB 和 AOF 的优点,Redis 4.0 之后新增了混合持久化的方式,因此我们在必须要进行持久化操作时,应该选择混合持久化的方式。
使用分布式架构来增加读写速度
Redis 分布式架构有三个重要的手段:
- 主从同步
- 哨兵模式
- Redis Cluster 集群
使用主从同步功能我们可以把写入放到主库上执行,把读功能转移到从服务上,因此就可以在单位时间内处理更多的请求,从而提升的 Redis 整体的运行速度。而哨兵模式是对于主从功能的升级,但当主节点奔溃之后,无需人工干预就能自动恢复 Redis 的正常使用。Redis Cluster 是 Redis 3.0 正式推出的,Redis 集群是通过将数据分散存储到多个节点上,来平衡各个节点的负载压力。
2.4 网络通信导致的延迟
客户端使用 TCP/IP 连接或 Unix 域连接连接到 Redis。1 Gbit/s 网络的典型延迟约为 200 us。Redis 客户端执行一条命令分 4 个过程:发送命令-〉 命令排队 -〉 命令执行-〉 返回结果这个过程称为 Round trip time(简称 RTT, 往返时间),mget mset 有效节约了 RTT,但大部分命令(如 hgetall,并没有 mhgetall)不支持批量操作,需要消耗 N 次 RTT ,这个时候需要 pipeline 来解决这个问题。Redis pipeline 将多个命令连接在一起来减少网络响应往返次数。
2.5 查找 bigkey
解决方案:对大 key 拆分
如将一个含有数万成员的 HASH Key 拆分为多个 HASH Key,并确保每个 Key 的成员数量在合理范围,在 Redis Cluster 结构中,大 Key 的拆分对 node 间的内存平衡能够起到显著作用。
异步清理大 key
Redis 自 4.0 起提供了 UNLINK 命令,该命令能够以非阻塞的方式缓慢逐步的清理传入的 Key,通过 UNLINK,你可以安全的删除大 Key 甚至特大 Key。
2.6 慢指令导致的延迟
根据上文的慢指令监控查询文档,查询到慢查询指令。可以通过以下两种方式解决:
- 比如在 Cluster 集群中,将聚合运算等 O(N) 操作运行在 slave 上,或者在客户端完成。
- 使用高效的命令代替。使用增量迭代的方式,避免一次查询大量数据。
除此之外,生产中禁用KEYS因为它会遍历所有的键值对,所以操作延时高。
- 获取当前 Redis 的基线性能;
- 开启慢指令监控,定位慢指令导致的问题;
- 找到慢指令,使用 scan 的方式;
- 将实例的数据大小控制在 2-4GB,避免主从复制加载过大 RDB 文件而阻塞;
- 禁用内存大页,采用了内存大页,生成 RDB 期间,即使客户端修改的数据只有 50B 的数据,Redis 需要复制 2MB 的大页。当写的指令比较多的时候就会导致大量的拷贝,导致性能变慢。
- Redis 使用的内存是否过大导致 swap;
- AOF 配置是否合理,可以将配置项 no-appendfsync-on-rewrite 设置为 yes,避免 AOF 重写和 fsync 竞争磁盘 IO 资源,导致 Redis 延迟增加。
- bigkey 会带来一些列问题,我们需要进行拆分防止出现 bigkey,并通过 UNLINK 异步删除。
2.7 expires 淘汰过期数据
Redis 有两种方式淘汰过期数据:
- 惰性删除:当接收请求的时候发现 key 已经过期,才执行删除;
- 定时删除:每 100 毫秒删除一些过期的 key。
定时删除的算法如下:
- 随机采样 A
CTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP
个数的 key,删除所有过期的 key; - 如果发现还有超过 25% 的 key 已过期,则执行步骤一。
ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP
默认设置为 20,每秒执行 10 次,删除 200 个 key 问题不大主。如果触发了第二条,就会导致 Redis 一致在删除过期数据取释放内存。而删除是阻塞的。也就是大量的 key 设置了相同的时间参数。同一秒内,大量 key 过期,需要重复删除多次才能降低到 25% 以下。
简而言之:大量同时到期的 key 可能会导致性能波动。解决方案:如果一批 key 的确是同时过期,可以在 EXPIREAT
和 EXPIRE
的过期时间参数上,加上一个一定大小范围内的随机数,这样,既保证了 key 在一个邻近时间范围内被删除,又避免了同时过期造成的压力。
2.8 AOF 和磁盘 I/O 导致的延迟
为了保证数据可靠性,Redis采用的AOF和RDB两种方式的实现的数据的持久化。可以使用appendfsync 配置将 AOF 配置为以三种不同的方式在磁盘上执行 write 或者 fsync (可以在运行时使用 CONFIG SET命令修改此设置,比如:redis-cli CONFIG SET appendfsync no
)。
- no:Redis 不执行 fsync,唯一的延迟来自于 write 调用,write 只需要把日志记录写到内核缓冲区就可以返回。
- everysec:Redis 每秒执行一次 fsync。使用后台子线程异步完成 fsync 操作。最多丢失 1s 的数据。
- always:每次写入操作都会执行 fsync,然后用 OK 代码回复客户端(实际上 Redis 会尝试将同时执行的许多命令聚集到单个 fsync 中),没有数据丢失。在这种模式下,性能通常非常低,强烈建议使用快速磁盘和可以在短时间内执行 fsync 的文件系统实现。
我们通常将 Redis 用于缓存,数据丢失完全恶意从数据获取,并不需要很高的数据可靠性,建议设置成 no 或者 everysec。除此之外,避免 AOF 文件过大, Redis 会进行 AOF 重写,生成缩小的 AOF 文件。可以把配置项 no-appendfsync-on-rewrite
设置为 yes,表示在 AOF 重写时,不进行 fsync 操作。也就是说,Redis 实例把写命令写到内存后,不调用后台线程进行 fsync 操作,就直接返回了。
2.9 swap:操作系统分页
当物理内存(内存条)不够用的时候,将部分内存上的数据交换到 swap 空间上,以便让系统不会因内存不够用而导致 oom 或者更致命的情况出现。
当某进程向 OS 请求内存发现不足时,OS 会把内存中暂时不用的数据交换出去,放在 SWAP 分区中,这个过程称为 SWAP OUT。
当某进程又需要这些数据且 OS 发现还有空闲物理内存时,又会把 SWAP 分区中的数据交换回物理内存中,这个过程称为 SWAP IN。
内存 swap 是操作系统里将内存数据在内存和磁盘间来回换入和换出的机制,涉及到磁盘的读写。
触发 swap 的情况有哪些呢?对于 Redis 而言,有两种常见的情况:
- Redis 使用了比可用内存更多的内存;
- 与Redis 在同一机器运行的其他进程在执行大量的文件读写 I/O 操作(包括生成大文件的 RDB 文件和 AOF 后台线程),文件读写占用内存,导致 Redis 获得的内存减少,触发了 swap。
如何排查是否因为 swap 导致的性能变慢呢?
#获取 Redis 实例 pid
$ redis-cli info | grep process_id
process_id:13160
# 进入此进程的 /proc 文件系统目录:
cd /proc/13160
在这里有一个 smaps 的文件,该文件描述了 Redis 进程的内存布局,运行以下指令,用 grep 查找所有文件中的 Swap 字段。
$ cat smaps | egrep '^(Swap|Size)'
Size: 316 kB
Swap: 0 kB
Size: 4 kB
Swap: 0 kB
Size: 8 kB
Swap: 0 kB
Size: 40 kB
Swap: 0 kB
Size: 132 kB
Swap: 0 kB
Size: 720896 kB
Swap: 12 kB
每行 Size 表示 Redis 实例所用的一块内存大小,和 Size 下方的 Swap 对应这块 Size 大小的内存区域有多少数据已经被换出到磁盘上了。如果 Size == Swap 则说明数据被完全换出了。
可以看到有一个 720896 kB 的内存大小有 12 kb 被换出到了磁盘上(仅交换了 12 kB),这就没什么问题。
Redis 本身会使用很多大小不一的内存块,所以,你可以看到有很多 Size 行,有的很小,就是 4KB,而有的很大,例如 720896KB。不同内存块被换出到磁盘上的大小也不一样。
如果 Swap 一切都是 0 kb,或者零星的 4k ,那么一切正常。当出现百 MB,甚至 GB 级别的 swap 大小时,就表明,此时,Redis 实例的内存压力很大,很有可能会变慢。
解决方案
- 增加机器内存;
- 将 Redis 放在单独的机器上运行,避免在同一机器上运行需要大量内存的进程,从而满足 Redis 的内存需求;
- 增加 Cluster 集群的数量分担数据量,减少每个实例所需的内存。
三、Redis技术选型与应用场景
3.1 数据结构方面
Memcached:主要支持简单的 key-value 数据结构,类似于 Redis 里的 String。
Redis:总共有9种,常见的5种,高级的4种:
- String:字符串,最基础的数据类型。
- List:列表。
- Hash:哈希对象。
- Set:集合。
- Sorted Set:有序集合,Set 的基础上加了个分值。
- HyperLogLog:通常用于基数统计。使用少量固定大小的内存,来统计集合中唯一元素的数量。统计结果不是精确值,而是一个带有0.81%标准差(standard error)的近似值。所以,HyperLogLog适用于一些对于统计结果精确度要求不是特别高的场景,例如网站的UV统计。
- Geo:redis 3.2 版本的新特性。可以将用户给定的地理位置信息储存起来, 并对这些信息进行操作:获取2个位置的距离、根据给定地理位置坐标获取指定范围内的地理位置集合。
- Bitmap:位图。
- Stream:主要用于消息队列,类似于 kafka,可以认为是 pub/sub 的改进版。提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。
3.2 数据存储方面
Memcached:数据全部存在内存中,重启实例会导致数据全部丢失
Redis:通常全部存在内存中,同时支持持久化到磁盘上。
3.3 持久化
Memcached:不支持
Redis:AOF、RDB、混合持久化
3.4 灾难恢复
Memcached:实例挂掉后,数据不可恢复
Redis:实例挂掉后可以通过RDB、AOF恢复 ,但是还是会有数据丢失问题
3.5 事件处理(事件库)
Memcached:使用 Libevent 库
Redis:自己封装了简易事件库 AeEvent
3.6 过期键删除策略
常见的有以下三种:
定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。对内存最友好,对 CPU 时间最不友好。
惰性删除:放任键过期不管,但是每次获取键时,都检査键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。对 CPU 时间最优化,对内存最不友好。
定期删除:每隔一段时间,默认100ms,程序就对数据库进行一次检査,删除里面的过期键。至 于要删除多少过期键,以及要检査多少个数据库,则由算法决定。前两种策略的折中,对 CPU 时间和内存的友好程度较平衡。
Memcached:惰性删除
Redis:惰性删除+定期删除
3.7 内存驱逐(淘汰)策略
当内存空间已经用满时,服务实例将将根据配置的驱逐策略,进行相应的动作。
memcached:主要为 LRU 算法
redis:当前总共有以下8种:
- noeviction:默认策略,不淘汰任何 key,直接返回错误
- allkeys-lru:在所有的 key 中,使用 LRU 算法淘汰部分 key
- allkeys-lfu:在所有的 key 中,使用 LFU 算法淘汰部分 key
- allkeys-random:在所有的 key 中,随机淘汰部分 key
- volatile-lru:在设置了过期时间的 key 中,使用 LRU 算法淘汰部分 key
- volatile-lfu:在设置了过期时间的 key 中,使用 LFU 算法淘汰部分 key
- volatile-random:在设置了过期时间的 key 中,随机淘汰部分 key
- volatile-ttl:在设置了过期时间的 key 中,挑选 TTL(time to live,剩余时间)短的 key 淘汰
3.8 性能
按“实例”维度进行比较时,个人认为由于 Memcached 多线程的特性,在 Redis 6.0 之前,通常情况下 Memcached 性能是要高于 Redis 的,同时实例的 CPU 核数越多,Memcached 的性能优势越大。而在 Redis 6.0 支持 I/O 多线程后,当 Redis 关闭持久化后,两者在性能上可能会比较接近。