面试不怂之redis与缓存大全

Posted xhmj12

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试不怂之redis与缓存大全相关的知识,希望对你有一定的参考价值。

相关阅读:一款神仙接私活低代码平台,吊到不行(附源码)

三万字长文,建议收藏,方便查阅

目录:

  • 基础篇

  • 高级内容

  • Redis设计篇

  • Redis原理篇

  • 实操篇


来吧,缓存面试


基础篇

问题:什么是redis?

答:Redis 是一个基于内存的高性能key-value数据库。

问题:redis可以用在哪些业务上?

答:redis可以做很多事情,比如:

  • 缓存

  • 分布式锁 (setnx)

  • 简易的消息队列(List/Streams)

  • 简易订阅通知(Pub/Sub)

  • 延时通知(键过期事件通知)

  • 附近的人(GEO)

以下是具体业务场景:

  • 记录帖子的点赞数、评论数和点击数 (hash)。

  • 记录用户的帖子 ID 列表 (排序),便于快速显示用户的帖子列表 (zset)。

  • 记录帖子的标题、摘要、作者和封面信息,用于列表页展示 (hash)。

  • 记录帖子的点赞用户 ID 列表,评论 ID 列表,用于显示和去重计数 (zset)。

  • 缓存近期热帖内容 (帖子内容空间占用比较大),减少数据库压力 (hash)。

  • 记录帖子的相关文章 ID,根据内容推荐相关帖子 (list)。

  • 如果帖子 ID 是整数自增的,可以使用 Redis 来分配帖子 ID(计数器)。

  • 收藏集和帖子之间的关系 (zset)。

  • 记录热榜帖子 ID 列表,总热榜和分类热榜 (zset)。

  • 缓存用户行为历史,进行恶意行为过滤 (zset,hash)。

问题:Redis有哪些数据结构?

答:Redis是一种Key-Value的模型,key是字符串类型,而常说的数据结构一般是指value的数据结构,一般包含以下类型。最普通常见的,字符串(String),字典(Hash),列表(List),集合(Set),有序集合(SortedSet)。
高级数据结构,HyperLogLogGeobitmap
更高级用户可能还知道Redis Module,像 BloomFilterRedisSearchRedis-ML

问题:使用Redis有哪些好处?

答:

  • 速度快,因为数据存在内存中,类似于HashMapHashMap的优势就是查找和操作的时间复杂度都是O(1)

  • 支持丰富数据类型,支持stringlistsetsorted sethash

  • 支持事务,操作都是原子性,所谓的原子性就是对数据的更改要么全部执行,要么全部不执行

  • 丰富的特性:可用于缓存,消息,按key设置过期时间,过期后将会自动删除。

问题:redis相比memcached有哪些优势?

答:优势如下

  • memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型

  • redis的速度比memcached快很多

  • redis可以持久化其数据

问题:使用redis有什么缺点?

答:主要有以下四点缺点:

  • 缓存和数据库双写一致性问题

  • 缓存雪崩问题

  • 缓存击穿问题

  • 缓存的并发竞争问题

问题:Redis提供了几种数据淘汰策略?该怎么选择?

答:

  • volatile-lru:从已经设置过期时间的数据集中,挑选最近最少使用的数据淘汰。

  • volatile-ttl:从已经设置过期时间的数据集中,挑选即将要过期的数据淘汰。

  • volatile-random:从已经设置过期时间的数据集中,随机挑选数据淘汰。

  • volatile-lfu:从已经设置过期时间的数据集中,会使用LFU算法选择设置了过期时间的键值对。

  • allkeys-lru:从所有的数据集中,挑选最近最少使用的数据淘汰。

  • allkeys-random:从所有的数据集中,随机挑选数据淘汰。

  • no-enviction:禁止淘汰数据,如果redis写满了将不提供写请求,直接返回错误。

注意这里的6种机制,volatileallkeys规定了是对已设置过期时间的数据集淘汰数据还是从全部数据集淘汰数据,后面的lru、ttl以及random是三种不同的淘汰策略,再加上一种no-enviction永不回收的策略。

 

使用策略规则:

  • 如果数据呈现幂律分布,也就是一部分数据访问频率高,一部分数据访问频率低,则使用allkeys-lru

  • 如果数据呈现平等分布,也就是所有的数据访问频率都相同,则使用allkeys-random

问题:为什么redis需要把所有数据放到内存中?

答:Redis为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘。所以redis具有快速和数据持久化的特征。如果不将数据放在内存中,磁盘I/O速度为严重影响redis的性能。在内存越来越便宜的今天,redis将会越来越受欢迎。

问题:Redis是单线程的吗?

答:Redis是单线程的,主要是指Redis的网络IO和键值对读写是由一个线程来完成的,这也是Redis对外提供键值存储服务的主要流程。但Redis的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。

问题: Redis6.0之前为什么一直不使用多线程?

答:官方曾做过类似问题的回复:使用Redis时,几乎不存在CPU成为瓶颈的情况, Redis主要受限于内存和网络。例如在一个普通的Linux系统上,Redis通过使用pipelining每秒可以处理100万个请求,所以如果应用程序主要使用O(N)或O(log(N))的命令,它几乎不会占用太多CPU。

使用了单线程后,可维护性高。多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。Redis通过AE事件模型以及IO多路复用等技术,处理性能非常高,因此没有必要使用多线程。单线程机制使得 Redis 内部实现的复杂度大大降低,Hash 的惰性 Rehash、Lpush 等等 “线程不安全” 的命令都可以无锁进行。

问题:Redis6.0为什么要引入多线程呢?

答:Redis将所有数据放在内存中,内存的响应时长大约为100纳秒,对于小数据包,Redis服务器可以处理80,000到100,000 QPS,这也是Redis处理的极限了,对于80%的公司来说,单线程的Redis已经足够使用了。

但随着越来越复杂的业务场景,有些公司动不动就上亿的交易量,因此需要更大的QPS。常见的解决方案是在分布式架构中对数据进行分区并采用多个服务器,但该方案有非常大的缺点,例如要管理的Redis服务器太多,维护代价大;某些适用于单个Redis服务器的命令不适用于数据分区;数据分区无法解决热点读/写问题;数据偏斜,重新分配和放大/缩小变得更加复杂等等。

从Redis自身角度来说,因为读写网络的read/write系统调用占用了Redis执行期间大部分CPU时间,瓶颈主要在于网络的 IO 消耗, 优化主要有两个方向:

  • 提高网络 IO 性能,典型的实现比如使用 DPDK 来替代内核网络栈的方式

  • 使用多线程充分利用多核,典型的实现比如 Memcached。

协议栈优化的这种方式跟 Redis 关系不大,支持多线程是一种最有效最便捷的操作方式。所以总结起来,redis支持多线程主要就是两个原因:

  • 可以充分利用服务器 CPU 资源,目前主线程只能利用一个核

  • 多线程任务可以分摊 Redis 同步 IO 读写负荷

问题:Redis6.0默认是否开启了多线程? 

答:Redis6.0的多线程默认是禁用的,只使用主线程。如需开启需要修改redis.conf配置文件:io-threads-do-reads yes  

问题:Redis6.0多线程开启时,线程数如何设置?

答:开启多线程后,还需要设置线程数,否则是不生效的。同样修改redis.conf配置文件
关于线程数的设置,官方有一个建议:4核的机器建议设置为2或3个线程,8核的建议设置为6个线程,线程数一定要小于机器核数。还需要注意的是,线程数并不是越大越好,官方认为超过了8个基本就没什么意义了。

问题:Redis6.0采用多线程后,性能的提升效果如何?

答:Redis 作者 antirez 在 RedisConf 2019分享时曾提到:Redis 6 引入的多线程 IO 特性对性能提升至少是一倍以上。国内也有大牛曾使用unstable版本在阿里云esc进行过测试,GET/SET 命令在4线程 IO时性能相比单线程是几乎是翻倍了。

测试环境:Redis Server: 阿里云 Ubuntu 18.04,8 CPU 2.5 GHZ, 8G 内存,主机型号 ecs.ic5.2xlarge Redis Benchmark Client: 阿里云 Ubuntu 18.04,8 2.5 GHZ CPU, 8G 内存,主机型号 ecs.ic5.2xlarge 测试结果:

 

详见:zhuanlan.zhihu.com/p/76788470

  • 说明1:这些性能验证的测试并没有针对严谨的延时控制和不同并发的场景进行压测。数据仅供验证参考而不能作为线上指标。

  • 说明2:如果开启多线程,至少要4核的机器,且Redis实例已经占用相当大的CPU耗时的时候才建议采用,否则使用多线程没有意义。所以估计80%的公司开发人员看看就好。

问题:Redis6.0多线程的实现机制?

答:如图

流程简述如下:

  1. 主线程负责接收建立连接请求,获取 socket 放入全局等待读处理队列

  2. 主线程处理完读事件之后,通过 RR(Round Robin) 将这些连接分配给这些 IO 线程

  3. 主线程阻塞等待 IO 线程读取 socket 完毕

  4. 主线程通过单线程的方式执行请求命令,请求数据读取并解析完成,但并不执行

  5. 主线程阻塞等待 IO 线程将数据回写 socket 完毕

  6. 解除绑定,清空等待队列

该设计有如下特点:

  1. IO 线程要么同时在读 socket,要么同时在写,不会同时读或写

  2. IO 线程只负责读写 socket 解析命令,不负责命令处理

问题:开启多线程后,是否会存在线程并发安全问题? 

答:从上面的实现机制可以看出,Redis的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程顺序执行。所以我们不需要去考虑控制 key、lua、事务,LPUSH/LPOP 等等的并发及线程安全问题。另外,搜索公众号互联网架构师后台回复“2T”,获取一份惊喜礼包。

问题:Redis线程中经常提到IO多路复用,如何理解?

这是IO模型的一种,即经典的Reactor设计模式,有时也称为异步阻塞IO。

多路指的是多个socket连接,复用指的是复用一个线程。多路复用主要有三种技术:select,poll,epoll。epoll是最新的也是目前最好的多路复用技术。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络IO的时间消耗),且Redis在内存中操作数据的速度非常快(内存内的操作不会成为这里的性能瓶颈),主要以上两点造就了Redis具有很高的吞吐量。

 

问题:redis如何做到高可用?

答:redis具备的高可用,其实包含两层含义:一是数据尽量少丢失,二是服务尽量少中断。对于前者redis使用AOFRDB两种持久化方式保证,对于后者Redis的做法就是增加副本冗余量,将一份数据同时保存在多个实例上。


高级内容

问题:redis的并发竞争问题如何解决?

答:Redis为单进程单线程模式,采用队列模式将并发访问变为串行访问。Redis本身没有锁的概念,Redis对于多个客户端连接并不存在竞争,但是在Jedis客户端对Redis进行并发访问时会发生连接超时、数据转换错误、阻塞、客户端关闭连接等问题,这些问题均是由于客户端连接混乱造成。

对此有两种解决方法:

  • 客户端角度,为保证每个客户端间正常有序与Redis进行通信,对连接进行池化,同时对客户端读写Redis操作采用内部锁synchronized

  • 服务器角度,利用setnx实现锁。

注:对于第一种,需要应用程序自己处理资源的同步,可以使用的方法比较通俗,可以使用synchronized也可以使用lock
第二种需要用到Redissetnx命令,但是需要注意一些问题。

问题:redis过期键的删除策略?

答:

  • 定时删除:在设置键的过期时间的同时,创建一个timer,让定时器在键的过期时间到达时,立即执行对键的删除操作。(主动删除)
    对内存友好,但是对cpu时间不友好,有较多过期键的而情况下,删除过期键会占用相当一部分cpu时间。

  • 惰性删除:放任过期键不管,但是每次从键空间中获取键时,都检查取到的键是否过去,如果过期就删除,如果没过期就返回该键。(被动删除)
    cpu时间友好,程序只会在取出键的时候才会对键进行过期检查,这不会在删除其他无关过期键上花费任何cpu时间,但是如果一个键已经过期,而这个键又保留在数据库中,那么只要这个过期键不被删除,他所占用的内存就不会释放,对内存不友好。

  • 定期删除:每隔一段时间就对数据库进行一次检查,删除里面的过期键。(主动删除)
    采用对内存和cpu时间折中的方法,每个一段时间执行一次删除过期键操作,并通过限制操作执行的时长和频率来减少对cpu时间的影响。难点在于,选择一个好的策略来设置删除操作的时长和执行频率。

问题:简述redis的哨兵模式?

答:哨兵是对redis进行实时的监控,主要有两个功能。

  • 监测主数据库和从数据库是否正常运行。

  • 当主数据库出现故障的时候,可以自动将一个从数据库转换为主数据库,实现自动切换。

问题:redis的哨兵的监控机制是怎样的?

答:哨兵监控也是有集群的,会有多个哨兵进行监控,当判断发生故障的哨兵达到一定数量的时候才进行修复。一个健壮的部署至少需要三个哨兵实例。

  • 每个Sentinel以每秒钟一次的频率向它所知的MasterSlave以及其他 Sentinel 实例发送一个 PING 命令

  • 如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被 Sentinel 标记为主观下线。

  • 如果一个Master被标记为主观下线,则正在监视这个Master的所有 Sentinel 要以每秒一次的频率确认Master的确进入了主观下线状态。

  • 当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认Master的确进入了主观下线状态, 则Master会被标记为客观下线

  • 在一般情况下, 每个 Sentinel 会以每 10 秒一次的频率向它已知的所有MasterSlave发送 INFO 命令

  • Master被 Sentinel 标记为客观下线时,Sentinel 向下线的 Master 的所有 Slave 发送 INFO 命令的频率会从 10 秒一次改为每秒一次

  • 若没有足够数量的 Sentinel 同意 Master 已经下线, Master 的客观下线状态就会被移除。若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。

问题:redis常见性能问题和解决方案?

答:

  • Master写内存快照,save命令调度rdbSave函数,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,所以Master最好不要写内存快照。

  • Master AOF持久化,如果不重写AOF文件,这个持久化方式对性能的影响是最小的,但是AOF文件会不断增大,AOF文件过大会影响Master重启的恢复速度。Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化,如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。

  • Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。

  • Redis主从复制的性能问题,为了主从复制的速度和连接的稳定性,SlaveMaster最好在同一个局域网内

问题:为什么Redis是单线程的?

答:官方给出答案是因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了。

问题:简述Redis的高并发和快速原因?

答:

  • Redis是纯内存数据库,一般都是简单的存取操作,线程占用的时间很多,时间的花费主要集中在IO上,所以读取速度快。

  • 再说一下IO,Redis使用的是非阻塞IO,IO多路复用,使用了单线程来轮询描述符,将数据库的开、关、读、写都转换成了事件,减少了线程切换时上下文的切换和竞争。

  • Redis采用了单线程的模型,保证了每个操作的原子性,也减少了线程的上下文切换和竞争。

  • 另外,数据结构也帮了不少忙,Redis全程使用hash结构,读取速度快,还有一些特殊的数据结构,对数据存储进行了优化,如压缩表,对短数据进行压缩存储,再如,跳表,使用有序的数据结构加快读取的速度。

  • 还有一点,Redis采用自己实现的事件分离器,效率比较高,内部采用非阻塞的执行方式,吞吐能力比较大。

问题:Redis持久化方式是什么?

答:两种持久化方式即AOF日志和RDB快照。

问题:简单介绍一下AOFRDB,如果同时使用AOFRDBredis重启会使用哪个构建数据?

答:AOFRDB都是redis持久化方案。
RDB持久化机制,对redis中的数据执行周期性的持久化。
AOF机制对每条写入命令作为日志,以append-only的模式写入一个日志文件中,在redis重启的时候,可以通过回放AOF日志中的写入指令来重新构建整个数据集。
如果我们想要redis仅仅作为纯内存的缓存来用,那么可以禁止RDBAOF所有的持久化机制。
如果同时使用RDBAOF两种持久化机制,那么在redis重启的时候,会使用AOF来重新构建数据,因为AOF中的数据更加完整。

问题:RDB持久化机制的优缺点?

答:优缺点如下。

问题:AOF持久化机制的优缺点?

答:优缺点如下:

  • 优点:

    • (1)AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据

    • (2)AOF日志文件以append-only模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复

    • (3)AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。因为在rewrite log的时候,会对其中的指导进行压缩,创建出一份需要恢复数据的最小日志出来。再创建新日志文件的时候,老的日志文件还是照常写入。当新的merge后的日志文件ready的时候,再交换新老日志文件即可。

    • (4)AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据

  • 缺点:

    • (1)对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大

    • (2)AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的

    • (3)以前AOF发生过bug,就是通过AOF记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。所以说,类似AOF这种较为复杂的基于命令日志/merge/回放的方式,比基于RDB每次持久化一份完整的数据快照文件的方式,更加脆弱一些,容易有bug。不过AOF就是为了避免rewrite过程导致的bug,因此每次rewrite并不是基于旧的指令日志进行merge的,而是基于当时内存中的数据进行指令的重新构建,这样健壮性会好很多。

问题:AOF日志是如何实现的?

答:AOF日志是一种写后日志,“写后”的意思是Redis是先执行命令,把数据写入内存,然后才记录日志,如下图所示: 

 AOF里记录的是Redis收到的每一条命令,这些命令是以文本形式保存的。我们以Redis收到“set testkey testvalue”命令后记录的日志为例。

 

 其中,"*3"表示当前命令有三个部分,每部分都是由"$+数字"开头,后面紧跟着具体的命令、键或值。这里,"数字"表示这部分中的命令、键或值一共有多少字节。例如,"$3 set"表示这部分有3个字节,也就是"set"命令。这种写后日志的好处就是:

  • 先执行命令,如果命令出错了就报错,而写入日志的命令肯定是正确的,避免出现记录错误命令的情况。

  • 写日志是在命令执行后才记录日志,所以不会阻塞当前命令的写操作。

AOF的日志何时写入磁盘呢?AOF日志写入磁盘是比较影响性能的,为了平衡性能与数据安全,开发了三种机制,也就是AOF配置项appendfsync的三个可选值:

  • Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
    同步写回可以做到基本不丢数据,但是它在每一个写命令后都有一个慢速的落盘操作,不可避免地会影响主线程性能。

  • Everysec,每秒写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
    每秒写回采用一秒写回一次的频率,避免了“同步写回”的性能开销,虽然减少了对系统性能的影响,但是如果发生宕机,上一秒内未落盘的命令操作仍然会丢失。所以,这只能算是,在避免影响主线程性能和避免数据丢失两者间取了个折中

  • No,操作系统控制的写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。
    操作系统控制的写回在写完缓冲区后,就可以继续执行后续的命令,但是落盘的时机已经不在Redis手中了,只要AOF记录没有写回磁盘,一旦宕机对应的数据就丢失了;

所以想要获得高性能,就选择No策略;如果想要得到高可靠性保证,就选择Always策略;如果允许数据有一点丢失,又希望性能别受太大影响的话,那么就选择Everysec策略。

问题:AOF写日志方式会有什么问题?

答:随着接收的写命令越来越多,AOF文件会越来越大,这事就会带来性能问题,性能问题主要是三个方面:

  • 文件系统本身对文件大小有限制,无法保存过大的文件。

  • 如果文件太大,之后再往里面追加命令记录的话,效率也会变低。

  • 如果发生宕机,AOF中记录的命令要一个个被重新执行,用于故障恢复,如果日志文件太大,整个恢复过程就会非常缓慢,这就会影响到Redis的正常使用。

所以就需要使用AOF重写机制重写日志。

问题:请你谈谈AOF重写机制?

答:AOF在重写时,Redis根据数据库现状创建一个新的AOF文件,假如数据库中有键值对:"test":"hello",那么重写机制会这样记录:set test hello
由上可知AOF重写机制可以把日志变少。因为在旧日志多条命令,在重写之后就变成一条命令了。以下画图解释:

 

 由上图可知重写前6条命令,重写后只有一条命令,因为重写机制是基于数据库当前数据的,之前这个数据经历怎样的变化我都不关心,只关心结果。

重写时还有个问题就是重写会不会阻塞主线程?

AOF日志由主线程写回不同,重写过程是由后台线程bgrewriteaof来完成的,这也是为了避免阻塞主线程。

整个重写过程是这样的:

  • 主线程fork出后台的bgrewriteaof子进程,fork会把主线程内存拷贝一份给bgrewriteaof子进程,这就是数据库此时最新数据,bgrewriteaof子进程会在不影响主线程的情况下逐一把拷贝的数据写成操作命令,记录在重写日志里。(拷贝)

  • 主线程未阻塞,可以处理新命令,处理之后将命令写入AOF日志缓冲区,保证AOF日志完整。(确保AOF日志完整)

  • 主线程处理的新命令也需要写入AOF重写日志,保证AOF重写日志不丢新数据。(确保AOF重写日志完整)

总的来说,整个过程就是:一处拷贝,两处日志完整。

 

问题:AOF何时重写日志呢?或者说什么时候会触发重写 AOF重写?

答:主要有两种触发方式:

  • 手动执行 bgrewriteaof 触发 AOF 重写

  • 在 redis.conf 文件中配置重写的条件,如:

    auto-aof-rewrite-min-size 64MB // 当文件小于64M时不进行重写
    auto-aof-rewrite-min-percenrage 100 // 当文件比上次重写后的文件大100%时进行重写
    复制代码

问题:AOF日志重写的时候,是由bgrewriteaof子进程来完成的,不用主线程参与,非阻塞是指子进程的执行不阻塞主线程。但是,你觉得,这个重写过程有没有其他潜在的阻塞风险呢?如果有的话,会在哪里阻塞?

问题:AOF重写也有一个重写日志,AOF本身也有一个日志,它为什么不把两个日志共享呢?

问题:请你谈谈redis持久化机制RDB是怎么样的?

答:首先,RDB出现的原因由于使用AOF方法进行故障恢复的时候,如果日志特别多,Redis就会恢复得很缓慢,影响到正常使用。所以出现了RDB
RDB叫做内存快照,就是Redis DataBase的缩写。它记录的是某一时刻的数据,保存在磁盘的dump.rdb文件中。所以,在做数据恢复时,我们可以直接把RDB文件读入内存,很快地完成恢复。
对于使用RDB来说有几个关键的地方:

  • 它给哪些数据做快照?
    为了提供所有数据可靠性,它执行的是全量快照。也就是所有数据做快照。redis提供了两个命令,savebgsave

    • save在主线程中执行,会导致阻塞;

    • bgsave:创建一个子进程,专门用于写入RDB文件,避免了主线程的阻塞,这也是Redis RDB文件生成的默认配置。

  • 快照时数据能修改吗?
    如果redis在快照时数据不能修改,那无疑是给业务造成损失。也许你会说可以使用bgsave来避免阻塞,但是注意,避免阻塞和正常处理写操作并不是一回事。如果数据不能修改,虽然没有阻塞,但是主线程只能接受读请求,而写请求不能执行。这肯定不能接受,所以,Redis就会借助操作系统提供的写时复制技术(Copy-On-Write, COW),在执行快照的同时,正常处理写操作。


     

  • 如图所示,如果主线程读取数据A,主线程和bgsave互不影响。如果修改数据C,这个数据就复制一份,生成数据副本。bgsave子进程把副本写入RDB文件,主线程修改数据C不影响。

  • 多久做一次快照?如果想要数据尽可能少丢失,那快照间隔的时间就要变得很短。虽然bgsave执行时不阻塞主线程,但是,如果频繁地执行全量快照,也会带来两方面的开销。

    所以,我们要做增量快照,就是指,做了一次全量快照后,后续的快照只对修改的数据进行快照记录,这样可以避免每次全量快照的开销。那如何记住后续的修改操作呢?


redis4.0提出混合使用AOF日志和内存快照的方法。就是内存快照以一定的频率执行,在两次快照之间,使用AOF日志记录这期间的所有命令操作。这样快照不需要频繁执行,AOF只需要记录两次快照间的操作。所以AOF文件也不会过大。

 

    • 一方面,频繁将全量数据写入磁盘,会给磁盘带来很大压力,多个快照竞争有限的磁盘带宽,前一个快照还没有做完,后一个又开始做了,容易造成恶性循环。

    • 另一方面,bgsave子进程需要通过fork操作从主线程创建出来。fork这个创建过程本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长。

问题:redis主从库如何实现数据一致?

答:如何实现数据一致,这里分以下几个问题说明。

  • 多个实例如何形成主从?
    当启动多个redis实例时,它们通过replicaof命令(Redis 5.0之前使用slaveof)形成主从关系。比如有两个实例:实例1(ip: 172.16.19.3)和实例2(ip: 172.16.19.5),在实例2上执行replicaof 172.16.19.3 6379命令,实例2变成实例1的重库,并从实例1上复制数据。

  • 主从之间第一次数据同步过程

     

  •  如上图,可以分为以下步骤:

    • runID,是每个Redis实例启动时都会自动生成的一个随机ID,用来唯一标记这个实例。当从库和主库第一次复制时,因为不知道主库的runID,所以将runID设为“?”。

    • offset,此时设为-1,表示第一次复制。

    • 第一阶段,段建立连接,协商同步。从库给主库发送psync命令表示数据同步,主库根据psync命令的参数来启动复制。参数主要包含主库的runID和复制进度offset。主库收到psync命令后,会用FULLRESYNC响应命令带上两个参数:主库runID和主库目前的复制进度offset,返回给从库。从库收到响应后,会记录下这两个参数。

      注意:FULLRESYNC响应表示第一次复制采用的全量复制,也就是说,主库会把当前所有的数据都复制给从库。

    • 第二阶段,主库将所有数据同步给从库。从库收到数据后,在本地完成数据加载。

      主库执行bgsave命令,生成RDB文件,接着将文件发给从库,从库接收RDB文件后清空数据库,然后加载RDB文件。主库数据同步过程中,仍然可以接收请求,这些请求没有记录在刚刚生成的RDB文件中,所以主库在内存中有一个replication buffer用于记录RDB文件生成后收到的所有写操作。

    • 第三阶段,主库会把replication buffer中的命令发送给从库,从库重新执行操作,这样主从就一致了。

  • 主-从-从模式下,数据如何同步?

如果存在多个从数据库都需要从主数据库中同步数据,那主redis就忙这fork子进程生成RDB文件了,fork操作会阻塞主线程的,而且传输RDB文件也会占用主库网络带宽。所以就有了另一种办法:主从级联模式分担全量复制时的主库压力,什么意思呢?就是再选择一个从库,将这个从库作为其他从库的主库。那么就由这个从库为其他从库同步数据。

  

主库同步数据给从库,从库又同步数据给其他从库,这个过程也称为基于长连接的命令传播,可以避免频繁建立连接的开销。

  • 如果主从库之间网路断了怎么办?

    • redis2.8之前,重新全量复制。

    • redis2.8之后采用增量复制,下面说说增量复制过程。
      主从断了之后,主库会把这段时间收到的写命令写入replication bufferrepl_backlog_buffer。增量复制所有操作都是基于repl_backlog_buffer的。它是一个环形缓冲区,主库会记录自己写到的位置(偏移量 master_repl_offset),从库则会记录自己已经读到的位置(偏移量 slave_repl_offset)。 刚开始,master_repl_offsetslave_repl_offset在一起,然后主库写,从库读,所以一般slave_repl_offset要小于master_repl_offset

       

    • 回顾下增量复制的流程

       

      这里有个注意的地方,repl_backlog_buffer是环形的,所以它会覆盖掉之前写的。如果从库的读取速度比较慢,就有可能导致从库还未读取的操作被主库新写的操作覆盖了,这会导致主从库间的数据不一致。这种情况可以调整repl_backlog_size参数大小。
      计算公式是:缓冲空间大小 = 主库写入命令速度 * 操作大小 - 主从库间网络传输命令速度 * 操作大小。在实际应用中,考虑到可能存在一些突发的请求压力,我们通常需要把这个缓冲空间扩大一倍,即repl_backlog_size = 缓冲空间大小 * 2,这也就是repl_backlog_size的最终值。

综上,整个redis数据同步问题就解释清楚了。

问题:主从库间的数据复制同步使用的是RDB文件,AOF记录的操作命令更全,相比于RDB丢失的数据更少。那么,为什么主从库间的复制不使用AOF呢?

问题:主从集群方式有什么缺点?

答:主从集群方式一般是一主多从的模式,主库可接收读/写请求,并把数据同步给从库,从库只能接收读请求。那么这里就有个问题,如果主库挂了呢?那么就无法接收写请求了。

 

问题:为啥需要哨兵?哨兵机制是什么样的?

答:因为主从集群模式,如果主库挂了,就无法提供正常的写功能了。这里涉及了三个问题:

  • 主库真的挂了吗?(是否误判)

  • 该选择哪个从库作为主库?(如何选主)

  • 怎么把新主库的相关信息通知给从库和客户端呢?(怎么通知)

所以,哨兵机制就是解决上面这三个问题而出现的,哨兵主要负责的就是三个任务:监控、选主(选择主库)和通知。就围绕这三个任务来看一下哨兵机制流程。

 

  • 监控
    监控是指哨兵进程会周期性的使用PING命令检测它自己和主、从库的网络连接情况,用来判断实例的状态。如果发现PING命令响应超时,哨兵就会把它标记为主观下线。为了防止哨兵误判

    哨兵误判就是主库并没有故障,误判一般会发生在集群网络压力较大、网络拥塞,或者是主库本身压力较大的情况下,而选主的代价比较高。

    一般都会会采用多实例组成的集群模式进行部署,这也就是哨兵集群。当多数哨兵标记主库为主观下线时,那么主库就会被标记为客观下线,也就是表示主库下线是客观事实的。判断的原则就是少数服从多数。下面再给张图帮助理解:

     

  • 选主 哨兵选主的过程可以概括为筛选+打分。

    • 第一轮:优先级最高的从库得分高
      可以通过slave-priority配置从库优先级,如果你有台从库实例,配置比较高,你就可以手动设置它优先级,那么在哨兵选主的时候,它就会胜出。

    • 第二轮:和旧主库同步程度最接近的从库得分高
      同步接近程度,也就是repl_backlog_buffer这里的数据同步程度,可以通过从库slave_repl_offset与旧主库的master_repl_offset值是否相近来判断接近程度。如下图,从库2就胜出称为新主库。

       

    • 第三轮:ID号小的从库得分高。
      每个实例都有一个ID,在优先级和复制进度都相同的情况下,ID号最小的从库得分最高,会被选为新主库

    • 筛选
      筛选时,哨兵除了要检查从库的当前在线状态,还要判断它之前的网络连接状态(因为如果它刚选上就网络不好挂了,那这次选主不就白费了么)。如果从库总是和主库断连而且超过了阈值,那这个从库就被排除了。

      具体可以通过 down-after-milliseconds * 10配置,down-after-milliseconds是我们认定主从库断连的最大连接超时时间。如果在down-after-milliseconds毫秒内,主从节点都没有通过网络联系上,我们就可以认为主从节点断连了。如果发生断连的次数超过了10次,就说明这个从库的网络状况不好,不适合作为新主库。

      总的来说,哨兵会按照在线状态、网络状态,筛选过滤掉一部分不符合要求的从库。

    • 打分
      打分按照三个规则依次进行三轮打分,主要某个库从在某一轮胜出,那么它就是主库,选举结束,规则分别是从库优先级、从库复制进度以及从库ID号。

  • 通知
    在执行通知任务时,哨兵会把新主库的连接信息发给其他从库,让它们执行replicaof命令,和新主库建立连接,并进行数据复制。同时,哨兵会把新主库的连接信息通知给客户端,让它们把请求操作发到新主库上。

问题:通过哨兵机制,可以实现主从库的自动切换,这是实现服务不间断的关键支撑,同时,主从库切换是需要一定时间的。所以,在这个切换过程中,客户端能否正常地进行请求操作呢?如果想要应用程序不感知服务的中断,还需要哨兵或需要客户端再做些什么吗?

问题:哨兵集群是如何自动发现的?

答:哨兵互相之间的发现,是通过redispub/sub系统实现的,每个哨兵都会往__sentinel__:hello这个channel里发送一个消息,这时候所有其他哨兵都可以消费到这个消息,并感知到其他的哨兵的存在,每隔两秒钟,每个哨兵都会往自己监控的某个master+slaves对应的__sentinel__:hello channel里发送一个消息,内容是自己的host、iprunid还有对这个master的监控配置,每个哨兵也会去监听自己监控的每个master+slaves对应的__sentinel__:hello channel,然后去感知到同样在监听这个master+slaves的其他哨兵的存在,每个哨兵还会跟其他哨兵交换对master的监控配置,互相进行监控配置的同步。

问题:使用redis要注意哪些影响性能的潜在因素?

答:主要要注意下面这些因素:

  • Redis内部的阻塞式操作;

  • CPU核和NUMA架构的影响;

  • Redis关键系统配置;

  • Redis内存碎片;

  • Redis缓冲区

问题:redis实例有哪些阻塞点?

答:可从redis实例的交互对象来分析。redis实例会和以下对象发生交互。

  • 客户端

    • 网络IO,不是阻塞点
      网络IO比较慢,但是redis使用了IO多路复用,避免了主线程一直等待网络连接或者请求到来的状态。所以网络IO它不是一个阻塞点。

    • 集合全量查询和聚合操作,阻塞点
      键值对的操作是redis和客户端的主要交互对象,那么复杂度高的操作一定会阻塞redis主线程,所以要留意时间复杂度为O(n)的操作,所以涉及集合的操作通常都是O(n),以及集合间的聚合操作。例如:HGETALLSMEMBERSLRANGE
      所以集合全量查询和聚合操作:可以使用SCAN命令,分批读取数据,再在客户端进行聚合计算;

    • 删除bigKey,阻塞点
      删除操作是要释放键值对占用的内存空间,把释放掉的内存块插入一个空闲内存块的链表,以便后续进行管理和再分配。这个过程很费时,所以在删除大量元素的集合时肯定会阻塞,也就是bigkey删除也是一个阻塞点。
      ** 可以异步处理来优化。**

    • 清空数据库,阻塞点
      FLUSHDB和FLUSHALL操作也是会阻塞的,因为它需要删除和释放所有的键值对。
      可以异步处理来优化。

  • 磁盘

    • 生成RDB快照文件采用fork子进程的方式不会阻塞主线程,不是阻塞点

    • AOF日志重写操作也是采用子进程方式不会阻塞,不是阻塞点

    • AOF日志同步写,阻塞点
      AOF日志同步写,会根据不同的写回策略对数据做落盘保存。一个同步写磁盘的操作的耗时大约是1~2ms,如果有大量的写操作需要记录在AOF日志中,并同步写回的话,就会阻塞主线程了。
      所以从库加载RDB文件:把主库的数据量大小控制在2~4GB左右,以保证RDB文件能以较快的速度加载。

  • 主从节点
    主库生成RDB快照文件,并把文件传输给从库,这是fork子进程做的不会阻塞主线程,但是从库收到主库快照文件时,需要清空本地数据,加载RDB文件这个过程对于从库来说是阻塞的。

  • 切片集群实例 当部署redis集群切片时,redis实例上分配的哈希槽信息需要在不同实例之间传递,以及负载均衡,实例增删时,数据会在不同实例间传递。不过哈希槽信息不大,而数据迁移是渐进式的,所以说不会阻塞。

综上,redis实例的阻塞点在集合全量查询和聚合操作、bigkey删除、清空数据库、AOF日志同步写、从库加载RDB文件。

问题:对于redis阻塞点中哪些可以异步操作的?怎么来异步操作呢?

答:redis实例的阻塞点在集合全量查询和聚合操作、bigkey删除、清空数据库、AOF日志同步写、从库加载RDB文件。
异步操作,也就是它不是主线程的关键操作。

  • 集合全量查询和聚合操作,是客户端的读请求,客户端发送了读操作之后,就会等待读取的数据返回,以便进行后续的数据处理。所以它不能异步操作。

  • bigkey删除,删除操作并不需要给客户端返回具体的数据结果,所以可以异步操作。

  • 清空数据库,同样删除操作可以异步。

  • AOF日志同步写,为了保证数据可靠性,Redis实例需要保证AOF日志中的操作记录已经落盘,这个操作虽然需要实例等待,但它并不会返回具体的数据结果给实例,可以异步。

  • 从库加载RDB文件,从库要想对客户端提供数据读取服务,就必须把RDB文件加载完成。所以,这个操作也属于关键路径上的操作,必须让从库的主线程来执行。

异步的子线程机制
redis主线程启动后,会通过操作系统pthread_create函数创建3个子线程,分别负责AOF日志写操作、键值对删除以及文件关闭的异步执行。
主线程通过一个链表形式的任务队列和子线程交互。主线程收到客户端请求时,比如删除命令,主线程会把删除操作封装成任务放入任务队列,然后回复客户端完成,但实际上,这个时候删除还没有执行,等到后台子线程从任务队列中读取任务后再删除。执行机制如下图 

问题:明明做了数据删除,数据量已经不大了,为什么使用top命令查看时,还会发现Redis占用了很多内存呢?

答:redis释放的内存空间由内存分配器管理,不会立刻返回给操作系统。

问题:什么是内存碎片化?为什么redis会出现内存碎片化?

答:redis内存碎片化和JVM内存碎片化是一样的,就是虽然操作系统剩余的内存总量足够,但是应用申请一块连续的空间,发现操作系统中无法申请一块连续的空间,因为没有这么大的连续空间,那么这些就是内存碎片化了。redis出现内存碎片化主要是两个方面。

  • 一个是内存分配器的分配策略造成的,内存分配器是按照固定大小分配的,而不是按照实际申请的大小分配的。比如申请20字节内存,但是分配器分配是按照8字节、16字节、32字节...分配,你申请的20字节,那么内存分配器会给你分配32字节的。那么多出部分就是碎片。

  • 另一个是redis键值对删除之后会释放部分空间带来的内存碎片。

可以通过INFO memory来判断是否有内存碎片

INFO memory
# Memory
used_memory:1073741736
used_memory_human:1024.00M
used_memory_rss:1997159792
used_memory_rss_human:1.86G
…
mem_fragmentation_ratio:1.86
复制代码

这里有一个mem_fragmentation_ratio的指标,它表示的就是Redis当前的内存碎片率。这个指标就是上面的命令中的两个指标used_memory_rss(操作系统实际分配内存)used_memory(redis申请的内存)相除的结果。对于这个指标:

  • mem_fragmentation_ratio 大于1但小于1.5。这种情况是合理的。这是因为,刚才我介绍的那些因素是难以避免的。毕竟,内因的内存分配器是一定要使用的,分配策略都是通用的,不会轻易修改;而外因由Redis负载决定,也无法限制。所以,存在内存碎片也是正常的。

  • mem_fragmentation_ratio 大于 1.5 。这表明内存碎片率已经超过了50%。一般情况下,这个时候,我们就需要采取一些措施来降低内存碎片率了。

清理内存碎片方法:

  • 重启Redis实例

  • 开启redis自动内存碎片清理功能

问题:缓存今典问题之如何解决缓存和数据库的数据不一致问题?

问题:缓存今典问题之如何解决缓存雪崩难题?

答:缓存雪崩是指的部分缓存节点不可以用,导致大量请求无法在redis中处理,进而大量请求打在数据库上,导致数据库层压力激增。缓存雪崩一般有两个原因:

  • 第一、缓存中大量数据同时过期,导致大量请求无法处理 针对这种情况导致缓存雪崩,一般有以下解决方案:

    • 1.对缓存Key的过期时间做微调,避免同时过期,假如需要同时过期的情况,可以在过期时间上加一点随机数(例如,增加个1~3分钟)这样避免大量缓存Key同时过期。

    • 2.服务降级,对于非核心业务,在发生缓存雪崩时,暂时停止从缓存中查询这些数据,而是直接返回预定义信息、空值或是错误信息;对于核心业务允许查询缓存以及数据库,这样快速失败(failfast)处理牺牲部分功能,保全整个系统。

    • 3.应用中

       

  • 第二、redis实例发生故障宕机了(比如缓存节点过载被打死了),一般也有两种解决方案: