常见性能问题和解决方案?
- Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化。
- 如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。
- 为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内。
- 尽量避免在压力较大的主库上增加从库
- Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。
- 为了Master的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关系为:Master<–Slave1<–Slave2<–Slave3…,这样的结构也方便解决单点故障问题,实现Slave对Master的替换,也即,如果Master挂了,可以立马启用Slave1做Master,其他不变。
使用批量操作减少网络传输
一个 Redis 命令的执行可以简化为以下 4 步:
- 发送命令
- 命令排队
- 命令执行
- 返回结果
其中,第 1 步和第 4 步耗费时间之和称为 Round Trip Time (RTT,往返时间) ,也就是数据在网络上传输的时间。
使用批量操作可以减少网络传输次数,进而有效减小网络开销,大幅减少 RTT。
另外,除了能减少 RTT 之外,发送一次命令的 socket I/O 成本也比较高(涉及上下文切换,存在read()
和write()
系统调用),批量操作还可以减少 socket I/O 成本。这个在官方对 pipeline 的介绍中有提到:https://redis.io/docs/manual/pipelining/ 。
Redis 中有一些原生支持批量操作的命令,比如:
MGET
(获取一个或多个指定 key 的值)、MSET
(设置一个或多个指定 key 的值)、HMGET
(获取指定哈希表中一个或者多个指定字段的值)、HMSET
(同时将一个或多个 field-value 对设置到指定哈希表中)、SADD
(向指定集合添加一个或多个元素)- ……
不过,在 Redis 官方提供的分片集群解决方案 Redis Cluster 下,使用这些原生批量操作命令可能会存在一些小问题需要解决。就比如说 MGET
无法保证所有的 key 都在同一个 hash slot(哈希槽)上,MGET
可能还是需要多次网络传输,原子操作也无法保证了。不过,相较于非批量操作,还是可以节省不少网络传输次数。
整个步骤的简化版如下(通常由 Redis 客户端实现,无需我们自己再手动实现):
- 找到 key 对应的所有 hash slot;
- 分别向对应的 Redis 节点发起
MGET
请求获取数据; - 等待所有请求执行结束,重新组装结果数据,保持跟入参 key 的顺序一致,然后返回结果。
如果想要解决这个多次网络传输的问题,比较常用的办法是自己维护 key 与 slot 的关系。不过这样不太灵活,虽然带来了性能提升,但同样让系统复杂性提升。
pipeline的作用?与原生批命令(mset和mget)的区别?
redis客户端执行一条命令分4个过程: 发送命令、命令排队、命令执行、返回结果。使用pipeline
可以批量请求,批量返回结果,执行速度比逐条执行要快。
使用pipeline
组装的命令个数不能太多,不然数据量过大,增加客户端的等待时间,还可能造成网络阻塞,可以将大量命令的拆分多个小的pipeline
命令完成。
原生批命令(mset和mget)与pipeline
对比:
- 原生批命令是原子性,
pipeline
是非原子性。pipeline命令中途异常退出,之前执行成功的命令不会回滚。 - 原生批命令只有一个命令,但
pipeline
支持多命令。
pipeline 和 Redis 事务的对比?
- 事务是原子操作,pipeline 是非原子操作。两个不同的事务不会同时运行,而 pipeline 可以同时以交错方式执行。
- Redis 事务中每个命令都需要发送到服务端,而 Pipeline 只需要发送一次,请求次数更少。
事务可以看作是一个原子操作,但其实并不满足原子性。当我们提到 Redis 中的原子操作时,主要指的是这个操作(比如事务、Lua 脚本)不会被其他操作(比如其他事务、Lua 脚本)打扰,并不能完全保证这个操作中的所有写命令要么都执行要么都不执行。这主要也是因为 Redis 是不支持回滚操作。
说说为什么Redis过期了为什么内存没释放?
第一种情况,可能是覆盖之前的key,导致key过期时间发生了改变。
当一个key在Redis中已经存在了,但是由于一些误操作使得key过期时间发生了改变,从而导致这个key在应该过期的时间内并没有过期,从而造成内存的占用。
第二种情况是,Redis过期key的处理策略导致内存没释放。
一般Redis对过期key的处理策略有两种:惰性删除和定时删除。
先说惰性删除的情况
当一个key已经确定设置了xx秒过期同时中间也没有修改它,xx秒之后它确实已经过期了,但是惰性删除的策略它并不会马上删除这个key,而是当再次读写这个key时它才会去检查是否过期,如果过期了就会删除这个key。也就是说,惰性删除策略下,就算key过期了,也不会立刻释放内容,要等到下一次读写这个key才会删除key。
而定时删除会在一定时间内主动淘汰一部分已经过期的数据,默认的时间是每100ms过期一次。因为定时删除策略每次只会淘汰一部分过期key,而不是所有的过期key,如果redis中数据比较多的话要是一次性全量删除对服务器的压力比较大,每一次只挑一批进行删除,所以很可能出现部分已经过期的key并没有及时的被清理掉,从而导致内存没有即时被释放。
Redis突然变慢,有哪些原因?
-
存在bigkey。如果Redis实例中存储了 bigkey,那么在淘汰删除 bigkey 释放内存时,也会耗时比较久。应该避免存储 bigkey,降低释放内存的耗时。
-
如果Redis 实例设置了内存上限 maxmemory,有可能导致 Redis 变慢。当 Redis 内存达到 maxmemory 后,每次写入新的数据之前,Redis 必须先从实例中踢出一部分数据,让整个实例的内存维持在 maxmemory 之下,然后才能把新数据写进来。
-
开启了内存大页。当 Redis 在执行后台 RDB 和 AOF rewrite 时,采用 fork 子进程的方式来处理。但主进程 fork 子进程后,此时的主进程依旧是可以接收写请求的,而进来的写请求,会采用 Copy On Write(写时复制)的方式操作内存数据。
什么是写时复制?
这样做的好处是,父进程有任何写操作,并不会影响子进程的数据持久化。
不过,主进程在拷贝内存数据时,会涉及到新内存的申请,如果此时操作系统开启了内存大页,那么在此期间,客户端即便只修改 10B 的数据,Redis 在申请内存时也会以 2MB 为单位向操作系统申请,申请内存的耗时变长,进而导致每个写请求的延迟增加,影响到 Redis 性能。
解决方案就是关闭内存大页机制。 -
使用了Swap。操作系统为了缓解内存不足对应用程序的影响,允许把一部分内存中的数据换到磁盘上,以达到应用程序对内存使用的缓冲,这些内存数据被换到磁盘上的区域,就是 Swap。当内存中的数据被换到磁盘上后,Redis 再访问这些数据时,就需要从磁盘上读取,访问磁盘的速度要比访问内存慢几百倍。尤其是针对 Redis 这种对性能要求极高、性能极其敏感的数据库来说,这个操作延时是无法接受的。解决方案就是增加机器的内存,让 Redis 有足够的内存可以使用。或者整理内存空间,释放出足够的内存供 Redis 使用
-
网络带宽过载。网络带宽过载的情况下,服务器在 TCP 层和网络层就会出现数据包发送延迟、丢包等情况。Redis 的高性能,除了操作内存之外,就在于网络 IO 了,如果网络 IO 存在瓶颈,那么也会严重影响 Redis 的性能。解决方案:1、及时确认占满网络带宽 Redis 实例,如果属于正常的业务访问,那就需要及时扩容或迁移实例了,避免因为这个实例流量过大,影响这个机器的其他实例。2、运维层面,需要对 Redis 机器的各项指标增加监控,包括网络流量,在网络流量达到一定阈值时提前报警,及时确认和扩容。
-
频繁短连接。频繁的短连接会导致 Redis 大量时间耗费在连接的建立和释放上,TCP 的三次握手和四次挥手同样也会增加访问延迟。应用应该使用长连接操作 Redis,避免频繁的短连接。
什么是大key?
通常我们会将含有较大数据或含有大量成员、列表数的Key称之为大Key。
以下是对各个数据类型大key的描述:
- value是STRING类型,它的值超过5MB
- value是ZSET、Hash、List、Set等集合类型时,它的成员数量超过1w个
上述的定义并不绝对,主要是根据value的成员数量和大小来确定,根据业务场景确定标准。
大Key造成的问题?
- 客户端超时阻塞:由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
- 网络阻塞:每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
- 工作线程阻塞:如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。
- 持久化阻塞(磁盘IO):对AOF 日志的影响
- 使用Always 策略的时候,
主线程
在执行完命令后,会把数据写入到 AOF 日志文件,然后会调用 fsync() 函数,将内核缓冲区的数据直接写入到硬盘,等到硬盘写操作完成后,该函数才会返回。因此当使用 Always 策略的时候,如果写入是一个大 Key,主线程
在执行 fsync() 函数的时候,阻塞的时间会比较久,因为当写入的数据量很大的时候,数据同步到硬盘这个过程是很耗时的。 - 另外两种策略都不影响主线程
- 使用Always 策略的时候,
大 key 造成的阻塞问题还会进一步影响到主从同步和集群扩容。
大key怎么处理?
- 压缩数据:当vaule是string时,可以使用序列化、压缩算法将key的大小控制在合理范围内,但是序列化和反序列化都会带来更多时间上的消耗。或者将key进行拆分,一个大key分为不同的部分,记录每个部分的key,使用multiget等操作实现事务读取。
- 分拆大Key :当value是list/set等集合类型时,根据预估的数据规模来进行分片,不同的元素计算后分到不同的片。
什么是 hotkey?
如果一个 key 的访问次数比较多且明显多于其他 key 的话,那这个 key 就可以看作是 hotkey(热 Key)。例如在 Redis 实例的每秒处理请求达到 5000 次,而其中某个 key 的每秒访问量就高达 2000 次,那这个 key 就可以看作是 hotkey。
hotkey 出现的原因主要是某个热点数据访问量暴增,如重大的热搜事件、参与秒杀的商品。
hotkey 有什么危害?
处理 hotkey 会占用大量的 CPU 和带宽,可能会影响 Redis 实例对其他请求的正常处理。此外,如果突然访问 hotkey 的请求超出了 Redis 的处理能力,Redis 就会直接宕机。这种情况下,大量请求将落到后面的数据库上,可能会导致数据库崩溃。
因此,hotkey 很可能成为系统性能的瓶颈点,需要单独对其进行优化,以确保系统的高可用性和稳定性。
如何解决 hotkey?
hotkey 的常见处理以及优化办法如下(这些方法可以配合起来使用):
- 读写分离:主节点处理写请求,从节点处理读请求。
- 使用 Redis Cluster:将热点数据分散存储在多个 Redis 节点上。
- 热点key拆分:将热点数据分散到多个 Key 中,例如通过引入随机前缀,使不同用户请求分散到多个 Key,多个 key 分布在多实例中,避免集中访问单一 Key。
- 多级缓存:在redis前增加其他缓存层(本地缓存、CDN)来缓解Redis的压力,从而减少对hotkey的直接访问。
- 限流和降级:在热点Key 访问过高时,应用限流策略,减少对 Redis 的请求,或者在必要时返回降级的数据或空值。
慢查询命令
经常使用O(n)以上复杂度的命令,由于Redis是单线程执行命令,因此这种情况Redis处理数据时就会很耗时。例如
- sort:对列表(list)、集合(set)、有序集合(sorted set)中的元素进行排序。在最简单的情况下(没有权重、没有模式、没有
LIMIT
),SORT
命令的时间复杂度近似于O(n*log(n))
- sunion:用于计算两个或多个集合的并集。时间复杂度可以描述为
O(N)
,其中N
是所有参与运算集合的元素总数。如果有多个集合,每个集合有不同数量的元素参与运算,那么复杂度会是所有这些集合元素数量的总和。 - zunionstore:用于计算一个或多个有序集合的并集,并将结果存储到一个新的有序集合中。在最简单的情况下,
ZUNIONSTORE
命令的时间复杂度是O(N*log(N))
,其中N
是所有参与计算的集合中元素的总数。 - keys * :获取所有的 key 操作;复杂度
O(n)
,数据量越大执行速度越慢;可以使用scan
命令替代 - Hgetall:返回哈希表中所有的字段和;
- smembers:返回集合中的所有成员;
解决方案就是,不使用这些复杂度较高的命令,并且一次不要获取太多的数据,每次尽量操作少量的数据,让Redis可以及时处理返回
keys命令存在的问题?如何高效安全的遍历所有key?
redis的单线程的。keys指令会导致线程阻塞一段时间,直到执行完毕,服务才能恢复。scan采用渐进式遍历的方式来解决keys命令可能带来的阻塞问题,每次scan命令的时间复杂度是O(1)
,但是要真正实现keys的功能,需要执行多次scan。
scan的缺点:在scan的过程中如果有键的变化(增加、删除、修改),遍历过程可能会有以下问题:新增的键可能没有遍历到,遍历出了重复的键等情况,也就是说scan并不能保证完整的遍历出来所有的键。
什么是内存碎片?为什么会有 Redis 内存碎片?
可以将内存碎片简单地理解为那些不可用的空闲内存。
当数据删除后,Redis 释放的内存空间会由内存分配器管理,并不会立即返回给操作系统。所以,操作系统仍然会记录着给 Redis 分配了大量内存。而Redis 释放的内存空间可能并不是连续的,那么,这些不连续的内存空间很有可能处于一种闲置的状态。也就产生了内存碎片
举个例子:Redis 默认使用 jemalloc 作为内存分配器,它是按照固定大小来分配内存的,比如实际需要 8kb 的内存,分配器给了 12kb。那么多余的 4kb 其实就无法被利用上了,它就叫内存碎片。
Redis 内存碎片虽然不会影响 Redis 性能,但是会增加内存消耗。
如何清理 Redis 内存碎片?
-
最简单的方法就是重启节点了,如果你采用的是高可用架构的 Redis 集群的话,你可以将碎片率过高的主节点转换为从节点,以便进行安全重启。
-
Redis4.0-RC3 版本以后自带了内存整理,可以避免内存碎片率过大的问题。
直接通过 config set 命令将 activedefrag 配置项设置为 yes 即可。
config set activedefrag yes
具体什么时候清理需要通过下面两个参数控制:
# 内存碎片占用空间达到 500mb 的时候开始清理
config set active-defrag-ignore-bytes 500mb
# 内存碎片率大于 1.5 的时候开始清理
config set active-defrag-threshold-lower 50
通过 Redis 自动内存碎片清理机制可能会对 Redis 的性能产生影响,我们可以通过下面两个参数来减少对 Redis 性能的影响:
# 内存碎片清理所占用 CPU 时间的比例不低于 20%
config set active-defrag-cycle-min 20
# 内存碎片清理所占用 CPU 时间的比例不高于 50%
config set active-defrag-cycle-max 50
Redis的Key和Value的设计原则有哪些?
Key 设计原则
- 短小精炼: 避免过长:Key 应该尽量短小,以节省内存和提高操作速度,通常不超过 256 字节。
- 使用命名空间: 分隔符:使用冒号(:)作为分隔符来组织命名空间,有助于实现 Key 的层级结构管理。 例如 user:1001:profile,可以很好地反映数据的逻辑分层关系。
- 避免热 Key: 负载均衡:确保 Key 的分布均匀,避免某单一 Key 承担过多的访问压力,可能需对数据进行分片处理。
- 选择唯一和通用的标识方式: 全局唯一性:确保 Key 的唯一性,避免不同数据使用相同的 Key。 结合业务逻辑,如使用用户ID、产品ID等。
Value 设计原则
- 选择合适的数据结构:对应使用:根据不同的需求选择适当的数据类型,如 String、List、Set、Hash、Sorted Set 等。 避免存储过大对象:如需存储大对象,建议先进行拆分或压缩。
- 限制单个 Value 的大小: 分片存储:对于需要存储大量数据的 Value,可以考虑拆分成多部分存储,以降低单个操作的复杂度。
- 合理设置Blob:如果需要存储Blob数据,考虑放在外部存储引擎中,只将引用或索引保存在 Redis。
- 利用压缩: 节省空间:对数据进行压缩,以减少内存占用和网络传输时间。
- TTL设置: 数据过期:合理使用 TTL 来控制数据的生命周期,避免无用数据长期占用内存。
Redis 性能瓶颈时如何处理?
如果 Redis 无法承受当前的负载的话,可以考虑从以下几个解决方法去解决:
- 首先想到的是扩容,比如增加 Redis 的配置,容纳更多的内存等。
- 如果超过单机配置了,那么可以上 redis 主从,通过从服务分担读取数据的压力,利用哨兵自动进行故障转移
- 还可以利用 redis 集群进行数据分片,比如 Redis Cluster。
- 也可以增加本地内存,通过多级缓存分担 redis 的压力。