Redis高频面试题


1.说一说Redis的持久化机制

Redis主要是RDB持久化、AOF持久化这两种持久化方式。

RDB

RDB(Redis Database)是Redis默认采用的持久化方式,它以快照的形式将内存数据持久化到硬盘中。RDB会创建一个经过压缩的二进制文件,文件以“.rdb”结尾。RDB持久化的触发方式有两种:

  • 手动触发:通过SAVE或BGSAVE命令触发RDB持久化操作,创建“.rdb”文件;
  • 自动触发:通过修改redis.conf配置文件,让服务器在满足指定条件时自动执行BGSAVE命令。

其中,SAVE命令执行期间,Redis服务器将阻塞,直到“.rdb”文件创建完毕为止,线上环境不建议使用。

而BGSAVE命令是异步版本的SAVE命令,它会fork出子进程来创建创建“.rdb”文件,在创建rdb文件期间父进程可以继续响应客户端发来的命令。

工作原理:

流程:客户端发出 BGSAVE 指令,若父进程发现有正在执行的子进程就直接返回;没有的话使用 fork 函数创建一个子进程,fork操作时父进程进入阻塞状态;fork操作完成后父进程继续响应其他命令;子进程会创建rdb文件,将父进程内存中的数据写入rdb文件中;创建完成后,父进程得到通知,将新文件替换旧的rdb文件。

子进程在读取数据的时候,父进程还可以修改数据,怎么解决这种并发问题呢?

主要用到的是写时复制(Copy On Write)技术,在Linux中,fork函数创建的子进程和父进程共享内存空间,内存中的数据都是一页一页的,当父进程需要修改内存中的数据时会将对应的page进行复制,然后对副本进行修改。

BGSAVE 命令是针对 SAVE 阻塞问题做的优化,Redis 内部所有涉及到 RDB 操作都采用 BGSAVE 的方式,SAVE 命令放弃使用

RDB持久化的优缺点如下:

  • 优点:RDB生成紧凑压缩的二进制文件,体积小,使用该文件恢复数据的速度非常快;

  • 缺点:BGSAVE每次运行都要执行fork操作创建子进程,属于重量级操作,不宜频繁执行,

    所以RDB持久化没办法做到实时的持久化。

AOF

AOF(Append Only File),解决了数据持久化的实时性,以日志的形式,每执行一条写命令,就把该命令追加写入到一个文件里;重启时再重新执行AOF文件中的命令来恢复数据。AOF的工作流程包括:命令写入(append)、文件同步(sync)、文件重写(rewrite)、重启加载(load)。

AOF默认不开启,需要修改配置项来启用它:

appendonly yes         # 启用AOF appendfilename "appendonly.aof"  # 设置文件名

AOF以文本协议格式写入命令,如:

*3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n

AOF持久化的文件同步机制:

为了提高程序的写入性能,现代操作系统会把针对硬盘的多次写操作优化为一次写操作。

  1. 当程序调用write对文件写入时,系统不会直接把数据写入硬盘,而是先将数据写入内存的缓冲区中;
  2. 当达到特定的时间周期或缓冲区写满时,系统才会执行flush操作,将缓冲区中的数据刷到磁盘中;

这种优化机制虽然提高了性能,但也给程序的写入操作带来了不确定性。

  1. 对于AOF这样的持久化功能来说,冲洗机制将直接影响AOF持久化的安全性;
  2. 为了消除上述机制的不确定性,Redis向用户提供了appendfsync选项,来控制系统冲洗AOF的频率;
  3. Linux的glibc提供了fsync函数,可以将指定文件强制从缓冲区刷到硬盘,上述选项正是基于此函数。

appendfsync选项的取值和含义如下:

取值 说明
always 每执行一个写入命令,,就缓冲区的数据刷入到磁盘中,这种情况下服务器突然宕机最多丢失一个命令,但是这样redis服务器性能很差,不如直接使用mysql这种基于磁盘的数据库。
everysec 每隔1秒就将缓冲区中的数据刷入磁盘中,这种情况下服务器最多丢失1秒之内的命令
no Redis不 控制写回磁盘的时机,每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。

AOF持久化的优缺点如下:

  • 优点:与RDB持久化可能丢失大量的数据相比,AOF持久化的安全性要高很多。通过使用everysec选项,用户可以将数据丢失的时间窗口限制在1秒之内。
  • 缺点:AOF文件存储的是协议文本,它的体积要比二进制格式的”.rdb”文件大很多。AOF需要通过执行AOF文件中的命令来恢复数据库,其恢复速度比RDB慢很多。AOF在进行重写时也需要创建子进程,在数据库体积较大时将占用大量资源,会导致服务器的短暂阻塞。

2.说一说Redis的缓存淘汰策略

当写入数据将导致超出maxmemory限制时,Redis有以下8种淘汰策略:

策略 说明
noevication 直接返回错误;
volatile-ttl 从设置了过期时间的键中,选择过期时间最小的键,进行淘汰;
volatile-random 从设置了过期时间的键中,随机选择键,进行淘汰;
volatile-lru 从设置了过期时间的键中,使用LRU算法选择键,进行淘汰;
volatile-lfu 从设置了过期时间的键中,使用LFU算法选择键,进行淘汰;
allleys-random 从所有的键中,随机选择键,进行淘汰;
allkeys-lru 从所有的键中,使用LRU算法选择键,进行淘汰;
allkeys-lfu 从所有的键中,使用LFU算法选择键,进行淘汰;

记两类就可以了,其中,volatile前缀代表从设置了过期时间的键中淘汰数据,allkeys前缀代表从所有的键中淘汰数据。

LRU(Least Recently Used)是按照最近最少使用原则来筛选数据,即最不常用的数据会被筛选出来!

  • 标准LRU:把所有的数据组成一个链表,表头和表尾分别表示MRU和LRU端,即最常使用端和最少使用端。刚被访问的数据会被移动到MRU端,而新增的数据也是刚被访问的数据,也会被移动到MRU端。当链表的空间被占满时,它会删除LRU端的数据。
  • 近似LRU:Redis会记录每个数据的最近一次访问的时间戳(LRU)。Redis执行写入操作时,若发现内存超出maxmemory,就会执行一次近似LRU淘汰算法。近似LRU会随机采样N个key,然后淘汰掉最旧的key,若淘汰后内存依然超出限制,则继续采样淘汰。可以通过maxmemory_samples配置项,设置近似LRU每次采样的数据个数,该配置项的默认值为5。

LRU算法的不足之处在于,若一个key很少被访问,只是刚刚偶尔被访问了一次,则它就被认为是热点数据,短时间内不会被淘汰。

LFU算法正式用于解决上述问题,LFU(Least Frequently Used)是Redis4新增的淘汰策略,它根据key的最近访问频率进行淘汰。LFU在LRU的基础上,为每个数据增加了一个计数器,来统计这个数据的访问次数。当使用LFU策略淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出内存。如果两个数据的访问次数相同,LFU再比较这两个数据的访问时间,把访问时间更早的数据淘汰出内存。

3.请介绍一下Redis的过期策略

redis支持如下两种过期策略:

  • 惰性删除:客户端访问一个key的时候,Redis会先检查它的过期时间,如果发现过期就立刻删除这个key。
  • 定期删除:Redis会将设置了过期时间的key放到一个独立的字典中,并对该字典进行每秒10次的过期扫描

过期扫描不会遍历字典中所有的key,而是采用了一种简单的贪心策略。该策略的删除逻辑如下:

  1. 从过期字典中随机选择20个key;
  2. 删除这20个key中已过期的key;
  3. 如果已过期key的比例超过25%,则重复步骤1。

4.Redis是单线程还是多线程的?

Redis 单线程指的是对读写命令是由一个线程(主线程)来完成的,这也是我们常说 Redis 是单线程的原因。

但是,Redis 程序并不是单线程的,Redis 在启动的时候,是会启动后台线程。例如,有执行AOF刷盘的后台线程,在 Redis 4.0 版本之后引入了多线程来执行一些大键值对的异步删除操作,Redis 在 6.0 版本之后,采用了多个 I/O 线程来处理网络请求,这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。但是对于命令的执行,Redis 仍然使用单线程来处理。

5.Redis的单线程模型了解吗?

Redis 基于 Reactor 模式开发了一套高效的事件处理模型,这套事件处理模型对应的是 Redis 中的文件事件处理器(file event handler)。由于文件事件处理器(file event handler)是单线程方式运行的,所以我们一般都说 Redis 是单线程模型。

  • 文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个socket,并根据socket目前执行的任务来为socket关联不同的事件处理器。
  • 当被监听的socket准备执行连接应答(accept)、读取(read)、写入(write)、关 闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。

既然是单线程,那怎么监听大量的客户端连接呢?

Redis 通过 IO 多路复用程序 来监听来自客户端的大量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。

这样的好处非常明显:I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗(和 NIO 中的 Selector 组件很像)。

文件事件处理器(file event handler)主要是包含 4 个部分:

  • 多个 socket(客户端连接)
  • IO 多路复用程序(支持多个客户端连接的关键)
  • 文件事件分派器(将 socket 关联到相应的事件处理器)
  • 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)

6.Redis大key会有什么问题?怎么解决?

大 key 会带来以下四种影响:

  • 客户端超时阻塞。由于 Redis 执行命令是单线程处理,然后在操作大 key 时会比较耗时,那么就会阻塞 Redis,从客户端这一视角看,就是很久很久都没有响应。
  • 引发网络阻塞。每次获取大 key 产生的网络流量较大,如果一个 key 的大小是 1 MB,每秒访问量为 1000,那么每秒会产生 1000MB 的流量,这对于普通千兆网卡的服务器来说是灾难性的。
  • 阻塞工作线程。如果使用 del 删除大 key 时,会阻塞工作线程,这样就没办法处理后续的命令。
  • 内存分布不均。集群模型在 slot 分片均匀情况下,会出现数据和查询倾斜情况,部分有大 key 的 Redis 节点占用内存多,QPS 也会比较小。

解决方式:

  • 拆分成多个小key。这是最容易想到的办法,降低单key的大小,读取可以用mget批量读取。
  • 设置合理的过期时间。为每个key设置过期时间,并设置合理的过期时间,以便在数据失效后自动清理,避免长时间累积的大Key问题。
  • 启用内存淘汰策略。启用Redis的内存淘汰策略,例如LRU(Least Recently Used,最近最少使用),以便在内存不足时自动淘汰最近最少使用的数据,防止大Key长时间占用内存。
  • 数据分片。例如使用Redis Cluster将数据分散到多个Redis实例,以减轻单个实例的负担,降低大Key问题的风险。
  • 删除大key。使用UNLINK命令删除大key,UNLINK命令是DEL命令的异步版本,它可以在后台删除Key,避免阻塞Redis实例。

7.Redis热key会有什么问题?怎么解决?

热key会带来的问题:

处理 hotkey 会占用大量的 CPU 和带宽,可能会影响 Redis 实例对其他请求的正常处理。此外,如果突然访问 hotkey 的请求超出了 Redis 的处理能力,Redis 就会直接宕机。这种情况下,大量请求将落到后面的数据库上,可能会导致数据库崩溃。

因此,hotkey 很可能成为系统性能的瓶颈点,需要单独对其进行优化,以确保系统的高可用性和稳定性。

如何解决热key?

  • 读写分离。主节点处理写请求,从节点处理读请求。
  • 使用Redis Cluster。将热点数据分散存储在多个Redis节点上
  • 二级缓存。热key采用二级缓存的方式进行处理,将热key存放一份到JVM本地内存中。

8.讲讲Redis的事务机制

Redis提供简单的事务,它不支持回滚,且通常不支持ACID中的D。

  • Redis 事务没有隔离级别的概念,队列中的命令在事务没有提交之前都不会实际被执行
  • Redis 单条命令式保存原子性的,但是事务不保证原子性,事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚

事务执行流程有这么几个命令

  1. multi:使客户端进入事务模式,此时客户端的所有命令都会按顺序放入一个事务队列中
  2. exec:执行当前事务,即将事务队列中的命令批量发给客户端
  3. discard:清空事务队列中已有的命令,并让客户端退出事务模式
  4. watch:加乐观锁,要求服务器对多个key进行监视,若在执行事务前这些key发生变化,则解决执行本次事务

watch监视机制的实现原理

每个 Redis 数据库都保存着一个 watched_keys 字典,键是某个被 WATCH 监视的数据库键,值则是一个链表,记录了所有监视相应数据库键的客户端:

typedef struct redisDb {
	// 正在被 WATCH 命令监视的键
    dict *watched_keys;
}

所有对数据库进行修改的命令,在执行后都会调用 multi.c/touchWatchKey 函数对 watched_keys 字典进行检查,是否有客户端正在监视刚被命令修改过的数据库键,如果有的话函数会将监视被修改键的客户端的 REDIS_DIRTY_CAS 标识打开,表示该客户端的事务安全性已经被破坏

服务器接收到个客户端 EXEC 命令时,会根据这个客户端是否打开了 REDIS_DIRTY_CAS 标识,如果打开了说明客户端提交事务不安全,服务器会拒绝执行。


文章作者: Fuchanglai
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Fuchanglai !
赏
  目录