《2021最新Java面试题全集-2021年第二版》不断更新完善!

    

第十七章 Redis

1:Redis 是什么?

Redis是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API

因为是纯内存操作, Redis 的性能非常出色,每秒可以处理超过 10 万次读写操作,是已知性能最快的Key-Value DB

Redis 的出色之处不仅仅是性能, Redis 最大的魅力是支持保存多种数据结构,此外单个 value 的最大限制是 1GB,不像 memcached 只能保存 1MB 的数据,因此 Redis 可以用来实现很多有用的功能。

比方说用他的 List 来做 FIFO 双向链表,实现一个轻量级的高性 能消息队列服务,用他的 Set 可以做高性能的 tag 系统等等。

另外 Redis 也可以对存入的 Key-Value 设置 expire 时间,因此也可以被当作一 个功能加强版的memcached 来用。

Redis 的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此 Redis 适合的场景主要局限在较小数据量的高性能操作和运算上。

2:常见的使用场景?

Redis 使用场景:

    数据高并发的读写

    海量数据的读写

    对扩展性要求高的数据

比如:

1)会话缓存( Session Cache

最常用的一种使用 Redis 的情景是会话缓存( sessioncache),用 Redis 缓存会话比其他存储的优势在于: Redis 提供持久化。当维护一个不是严格要求一致性的缓存时,如果用户的购物车信息全部丢失,大部分人都会不高兴的,现在,他们还会这样吗?

2)全页缓存( FPC

除基本的会话 token 之外, Redis 还提供很简便的 FPC 平台。回到一致性问题,即使重启了 Redis 实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进。

3)队列

Reids 在内存存储引擎领域的一大优点是提供 list set 操作,这使得 Redis 能作为一个很好的消息队列平台来使用。 Redis 作为队列使用的操作,就类似于本地程序语言(如 Python)对 list push/pop操作。

4)排行榜/计数器

Redis 在内存中对数字进行递增或递减的操作实现的非常好。集合( Set)和有序集合( SortedSet)也使得我们在执行这些操作的时候变的非常简单, Redis 只是正好提供了这两种数据结构。

所以,我们要从排序集合中获取到排名最靠前的 10 个用户_我们称之为“user_scores”,我们只需要像下面一样执行即可:当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数,这样执行:

ZRANGE user_scores 0 10 WITHSCORES

5)发布/订阅

最后(但肯定不是最不重要的)是 Redis 的发布/订阅功能。发布/订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用 Redis 的发布/订阅功能来建立聊天系统!

 

3:Redis 有哪些功能?

数据缓存功能

分布式锁的功能

支持数据持久化

支持事务

支持消息队列

 

4:Redis memecache 有什么区别?

1Redis的速度比memcached快很多

2Memecache 把数据全部存在内存之中,断电后会挂掉,数据不能 超过内存大小。 Redis 有部份存在硬盘上,这样能保证数据的持久性。

3)数据支持类型 Memcache 对数据类型支持相对简单,所有的值均是简单的字符串 Redis 有复杂的数据类型。

 

5:Redis 为什么是单线程的?

因为 cpu 不是 Redis 的瓶颈,Redis 的瓶颈最有可能是机器内存或者网络带宽。既然单线程容易实现,而且 cpu 又不会成为瓶颈,那就顺理成章地采用单线程的方案了。

关于 Redis 的性能,官方网站也有,普通笔记本轻松处理每秒几十万的请求。而且单线程并不代表就慢 nginx nodejs 也都是高性能单线程的代表

 

6:说说Redis的线程模型

NIO、单线程、异步的线程模型

 

1:文件事件处理器

Redis基于reactor模式开发了网络事件处理器,这个处理器叫做文件事件处理器,file event handler。这个文件事件处理器,是单线程的,Redis才叫做单线程的模型,采用IO多路复用机制同时监听多个socket,根据socket上的事件来选择对应的事件处理器来处理这个事件。

如果被监听的socket准备好执行acceptreadwriteclose等操作的时候,跟操作对应的文件事件就会产生,这个时候文件事件处理器就会调用之前关联好的事件处理器来处理这个事件。

文件事件处理器是单线程模式运行的,但是通过IO多路复用机制监听多个socket,可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了Redis内部的线程模型的简单性。

文件事件处理器的结构包含4个部分:多个socketIO多路复用程序,文件事件分派器,事件处理器(命令请求处理器、命令回复处理器、连接应答处理器,等等)。

多个socket可能并发的产生不同的操作,每个操作对应不同的文件事件,但是IO多路复用程序会监听多个socket,但是会将socket放入一个队列中排队,每次从队列中取出一个socket给事件分派器,事件分派器把socket给对应的事件处理器。

然后一个socket的事件处理完之后,IO多路复用程序才会将队列中的下一个socket给事件分派器。文件事件分派器会根据每个socket当前产生的事件,来选择对应的事件处理器来处理。

2:文件事件

socket变得可读时(比如客户端对Redis执行write操作,或者close操作),或者有新的可以应答的sccket出现时(客户端对Redis执行connect操作),socket就会产生一个AE_READABLE事件。

socket变得可写的时候(客户端对Redis执行read操作),socket会产生一个AE_WRITABLE事件。

IO多路复用程序可以同时监听AE_REABLEAE_WRITABLE两种事件,要是一个socket同时产生了AE_READABLEAE_WRITABLE两种事件,那么文件事件分派器优先处理AE_REABLE事件,然后才是AE_WRITABLE事件。

3:文件事件处理器

如果是客户端要连接Redis,那么会为socket关联连接应答处理器

如果是客户端要写数据到Redis,那么会为socket关联命令请求处理器

如果是客户端要从Redis读数据,那么会为socket关联命令回复处理器

4:客户端与Redis通信的一次流程

Redis启动初始化的时候,Redis会将连接应答处理器跟AE_READABLE事件关联起来,接着如果一个客户端跟Redis发起连接,此时会产生一个AE_READABLE事件,然后由连接应答处理器来处理跟客户端建立连接,创建客户端对应的socket,同时将这个socketAE_READABLE事件跟命令请求处理器关联起来。

当客户端向Redis发起请求的时候(不管是读请求还是写请求,都一样),首先就会在socket产生一个AE_READABLE事件,然后由对应的命令请求处理器来处理。这个命令请求处理器就会从socket中读取请求相关数据,然后进行执行和处理。

接着Redis这边准备好了给客户端的响应数据之后,就会将socketAE_WRITABLE事件跟命令回复处理器关联起来,当客户端这边准备好读取响应数据时,就会在socket上产生一个AE_WRITABLE事件,会由对应的命令回复处理器来处理,就是将准备好的响应数据写入socket,供客户端来读取。

命令回复处理器写完之后,就会删除这个socketAE_WRITABLE事件和命令回复处理器的关联关系。

 

7:为啥Redis单线程模型也能效率这么高?

1)纯内存操作

2)核心是基于非阻塞的IO多路复用机制

3)单线程反而避免了多线程的频繁上下文切换问题

 

8:什么是缓存穿透?怎么解决?

1:缓存穿透:

指查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。

这里需要注意和缓存击穿的区别,缓存击穿,是指一个key热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开一个洞。

2:解决方案:

1)最简单粗暴的方法如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们就把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟

2)布隆过滤器

布隆过滤器是一种数据结构,用它对所有可能查询的参数以hash形式存储起来,当用户想要查询的时候,使用布隆过滤器发现不在集合中,就直接丢弃,不再对持久层查询。

 

 

9:什么是缓存雪崩?怎么解决?

缓存雪崩是指,缓存层出现了错误,不能正常工作了。于是所有的请求都会达到数据库,数据库的调用量会暴增,造成数据库也会挂掉,最终导致整个应用崩溃的情况。

解决方案

1redis高可用

这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。

2)限流降级

这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。

3)数据预热

数据加热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中。在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

 

10:Redis 支持的数据类型有哪些?

Redis支持的数据类型有:StringListHashSetZSet

1String

这是最基本的类型了,就是普通的setget,做简单的kv缓存。一个String类型最大容量是512M

2Hash

是类似map的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在Redis里,然后每次读写缓存的时候,可以就操作hash里的某个字段。

key=150

value={

  “id”: 150,

  “name”: “zhangsan”,

  “age”: 20

}

Hash类的数据结构,主要是用来存放一些对象,把一些简单的对象给缓存起来,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值

value={

  “id”: 150,

  “name”: “zhangsan”,

  “age”: 21

}

3List

有序列表,这个是可以玩儿出很多花样的

微博,某个大v的粉丝,就可以以list的格式放在Redis里去缓存

key=某大v

value=[zhangsan, lisi, wangwu]

比如可以通过list存储一些列表型的数据结构,类似粉丝列表了、文章的评论列表了之类的东西

比如可以通过lrange命令,就是从某个元素开始读取多少个元素,可以基于list实现分页查询,这个很棒的一个功能,基于Redis实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走

比如可以搞个简单的消息队列,从list头怼进去,从list尾巴那里弄出来

4Set

无序集合,自动去重。

直接基于set将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于jvm内存里的HashSet进行去重,但是如果你的某个系统部署在多台机器上呢?

得基于Redis进行全局的set去重

可以基于set玩儿交集、并集、差集的操作,比如交集吧,可以把两个人的粉丝列表整一个交集,看看俩人的共同好友是谁?对吧

把两个大v的粉丝都放在两个set中,对两个set做交集

5ZSet

排序的set,去重但是可以排序,写进去的时候给一个分数,自动根据分数排序,这个可以玩儿很多的花样,最大的特点是有个分数可以自定义排序规则

比如说你要是想根据时间对数据排序,那么可以写入进去的时候用某个时间作为分数,人家自动给你按照时间排序了

排行榜:将每个用户以及其对应的什么分数写入进去,zadd board score username,接着zrevrange board 0 99,就可以获取排名前100的用户;zrank board username,可以看到用户在排行榜里的排名

 

11:Redis 支持的 Java 客户端都有哪些?

RedissonJedisLettuce等,目前官方推荐使用Lettuce

 

12:Jedis Redisson 有哪些区别?

JedisRedisJava实现的客户端,其API提供了比较全面的Redis命令的支持。

Redisson实现了分布式和可扩展的Java数据结构,和Jedis相比,功能较为简单,不支持字符串操作,不支持排序、事务、管道、分区等Redis特性。Redisson的宗旨是促进使用者对Redis的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。

 

13:怎么保证缓存和数据库数据的一致性?

合理设置缓存的过期时间。

新增、更改、删除数据库操作时同步更新 Redis,可以使用事务机制来保证数据的一致性

 

14:Redis 持久化有几种方式?

Redis 的持久化有两种方式,或者说有两种策略:

RDBRedis Database):指定的时间间隔能对你的数据进行快照存储。
AOF
Append Only File):每一个收到的写命令都通过write函数追加到文件中。

RDB持久化机制,对Redis中的数据执行周期性的持久化

AOF机制对每条写入命令作为日志,以append-only的模式写入一个日志文件中,在Redis重启的时候,可以通过回放AOF日志中的写入指令来重新构建整个数据集

如果我们想要Redis仅仅作为纯内存的缓存来用,那么可以禁止RDBAOF所有的持久化机制

通过RDBAOF,都可以将Redis内存中的数据给持久化到磁盘上面来,然后可以将这些数据备份到别的地方去。

如果Redis挂了,服务器上的内存和磁盘上的数据都丢了,可以从云服务上拷贝回来之前的数据,放到指定的目录中,然后重新启动RedisRedis就会自动根据持久化数据文件中的数据,去恢复内存中的数据,继续对外提供服务

如果同时使用RDBAOF两种持久化机制,那么在Redis重启的时候,会使用AOF来重新构建数据,因为AOF中的数据更加完整。

 

15:说说RDB的优缺点?

1RDB持久化机制的优点

1RDB会生成多个数据文件,每个数据文件都代表了某一个时刻中Redis的数据,这种多个数据文件的方式,非常适合做冷备,可以将这种完整的数据文件发送到一些远程的安全存储上去

2RDBRedis对外提供的读写服务,影响非常小,可以让Redis保持高性能,因为Redis主进程只需要fork一个子进程,让子进程执行磁盘IO操作来进行RDB持久化即可

3)相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复Redis进程,更加快速

2RDB持久化机制的缺点

1)如果想要在Redis故障时,尽可能少的丢失数据,那么RDB没有AOF好。一般来说,RDB数据快照文件,都是每隔5分钟,或者更长时间生成一次,这个时候就得接受一旦Redis进程宕机,那么会丢失最近5分钟的数据

2RDB每次在fork子进程来执行RDB快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒

16:说说AOF的优缺点?

1AOF持久化机制的优点

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

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

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

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

2AOF持久化机制的缺点

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

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

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

 

17:RDBAOF到底该如何选择

1)不要仅仅使用RDB,因为那样会导致你丢失很多数据

2)也不要仅仅使用AOF,因为那样有两个问题,第一,你通过AOF做冷备,没有RDB做冷备,来的恢复速度更快; 第二,RDB每次简单粗暴生成数据快照,更加健壮,可以避免AOF这种复杂的备份和恢复机制的bug

3)综合使用AOFRDB两种持久化机制,用AOF来保证数据不丢失,作为数据恢复的第一选择; RDB来做不同程度的冷备,在AOF文件都丢失或损坏不可用的时候,还可以使用RDB来进行快速的数据恢复

 

18:Redis 怎么实现分布式锁?

Redis 分布式锁其实就是在系统里面占一个,其他程序也要占的时候,占用成功了就可以继续执行,失败了就只能放弃或稍后重试。

占坑一般使用 setnx(set if not exists)指令,只允许被一个程序占有,使用完调用 del 释放锁

    别忘记同时设置上expire,加一个过期时间,防止忘记释放锁。

19:Redis 分布式锁有什么缺陷?

Redis 分布式锁不能解决超时的问题,分布式锁有一个超时时间,程序的执行如果超出了锁的超时时间就会出现问题

 

20:Redis 如何做内存优化?

尽可能使用散列表,散列表(是说散列表里面存储的数少)使用的内存非常小,所以应该尽可能的将你的数据模型抽象到一个散列表里面。

 

21:Redis 淘汰策略有哪些?内存淘汰机制有哪些?

1:设置过期时间,定时删除

set key的时候,都可以给一个expire time,就是过期时间,指定这个key比如说只能存活1个小时?10分钟?这个很有用,我们自己可以指定缓存到期就失效。

如果假设你设置一个一批key只能存活1个小时,那么接下来1小时后,Redis是怎么对这批key进行删除的?

2:答案是:定期删除+惰性删除

所谓定期删除,指的是Redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。假设Redis里放了10万个key,都设置了过期时间,你每隔几百毫秒,就检查10万个key,那Redis基本上就死了,cpu负载会很高的,消耗在你的检查过期key上了。

注意,这里可不是每隔100ms就遍历所有的设置过期时间的key,那样就是一场性能上的灾难。实际上Redis是每隔100ms随机抽取一些key来检查和删除的。

但是问题是,定期删除可能会导致很多过期key到了时间并没有被删除掉,那咋整呢?所以就是惰性删除了。这就是说,在你获取某个key的时候,Redis会检查一下 ,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西。

所以并不是key到时间就被删除掉,而是你查询这个key的时候,Redis再懒惰的检查一下

通过上述两种手段结合起来,保证过期的key一定会被干掉。

很简单,就是说,你的过期key,靠定期删除没有被删除掉,还停留在内存里,占用着你的内存呢,除非你的系统去查一下那个key,才会被Redis给删除掉。

但是实际上这还是有问题的,如果定期删除漏掉了很多过期key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期key堆积在内存里,导致Redis内存块耗尽了,咋整?

答案是:走内存淘汰机制。

3:内存淘汰

如果Redis的内存占用过多的时候,此时会进行内存淘汰,有如下一些策略:

Redis 10key,现在已经满了,Redis需要删除掉5key

1key,最近1分钟被查询了100

1key,最近10分钟被查询了50

1key,最近1个小时倍查询了1

1noeviction:当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了

2allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)

3allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的key给干掉啊

4volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的key(这个一般不太合适)

5volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key

6volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除

 

22:Redis 常见的性能问题有哪些?该如何解决?

主服务器写内存快照,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务,所以主服务器最好不要写内存快照。

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

 

23:Redis回收进程如何工作的?

一个客户端运行了新的命令,添加了新的数据。

Redis 检查内存使用情况,如果大于 maxmemory 的限制, 则根据设定好的策略进行回收。

然后一个新的命令被执行。

你会发现,我们可能在不断地穿越内存限制的边界,通过不断达到边界,然后不断地回收回到边界以下。

如果一个命令的结果导致大量内存被使用(例如很大的集合的交集保存到一个新的键),不用多久内存限制就会被这个内存使用量超越。

 

24:为什么 Redis 需要把所有数据放到内存中?

Redis 为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘。

所以 Redis 具有快速和数据持久化的特征,如果不将数据放在内存中,磁盘 I/O 速度为严重影响 Redis 的性能。

在内存越来越便宜的今天, Redis 将会越来越受欢迎, 如果设置了最大使用的内存,则数据已有记录数达到内存限值后不能继续插入新值。

 

25:说说Redis的主从复制

1Redis采用异步方式复制数据到slave节点,不过Redis 2.8开始,slave node会周期性地确认自己每次复制的数据量

2)一个master node可以配置多个slave node

3slave node也可以连接其他的slave node

4slave node做复制的时候,是不会block master node的正常工作的

5slave node在做复制的时候,也不会block对自己的查询操作,它会用旧的数据集来提供服务; 但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了

6slave node主要用来进行横向扩容,做读写分离,扩容的slave node可以提高读的吞吐量

 

26:Redis主从复制原理

当启动一个slave node的时候,它会发送一个PSYNC命令给master node

如果这是slave node重新连接master node,那么master node仅仅会复制给slave部分缺少的数据; 否则如果是slave node第一次连接master node,那么会触发一次全量复制full resynchronization

开始全量复制的时候,master会启动一个后台线程,开始生成一份RDB快照文件,同时还会将从客户端收到的所有写命令缓存在内存中。

RDB文件生成完毕之后,master会将这个RDB发送给slaveslave会先写入本地磁盘,然后再从本地磁盘加载到内存中。然后master会将内存中缓存的写命令发送给slaveslave也会同步这些数据。

slave node如果跟master node有网络故障,断开了连接,会自动重连。master如果发现有多个slave node都来重新连接,仅仅会启动一个rdb save操作,用一份数据服务所有slave node

 

27:说说Redis主从数据同步的机制

指的就是第一次slave连接msater的时候,执行的全量复制,这个过程里面的一些细节机制

1masterslave都会维护一个offset

master会在自身不断累加offsetslave也会在自身不断累加offset

slave每秒都会上报自己的offsetmaster,同时master也会保存每个slaveoffset

这个倒不是说特定就用在全量复制的,主要是masterslave都要知道各自的数据的offset,才能知道互相之间的数据不一致的情况

2backlog

master node有一个backlog,默认是1MB大小

master nodeslave node复制数据时,也会将数据在backlog中同步写一份

backlog主要是用来做全量复制中断候的增量复制的

3master run id

info server,可以看到master run id

如果根据host+ip定位master node,是不靠谱的,如果master node重启或者数据出现了变化,那么slave node应该根据不同的run id区分,run id不同就做全量复制

如果需要不更改run id重启Redis,可以使用Redis-cli debug reload命令

4psync

从节点使用psyncmaster node进行复制,psync runid offset

master node会根据自身的情况返回响应信息,可能是(master run id发生变化)FULLRESYNC runid offset触发全量复制,可能是CONTINUEmaster run id没发生变化)触发增量复制

 

28:说说全量复制的机制

1master执行bgsave,在本地生成一份rdb快照文件

2master noderdb快照文件发送给salve node,如果rdb复制时间超过60秒(repl-timeout),那么slave node就会认为复制失败,可以适当调节大这个参数

3)对于千兆网卡的机器,一般每秒传输100MB6G文件,很可能超过60s

4master node在生成rdb时,会将所有新的写命令缓存在内存中,在salve node保存了rdb之后,再将新的写命令复制给salve node,保持数据一致

5client-output-buffer-limit slave 256MB 64MB 60,如果在复制期间,内存缓冲区持续消耗超过64MB,或者一次性超过256MB,那么停止复制,复制失败

6slave node接收到rdb之后,清空自己的旧数据,然后重新加载rdb到自己的内存中,同时基于旧的数据版本对外提供服务

7)如果slave node开启了AOF,那么会立即执行BGREWRITEAOF,重写AOF

rdb生成、rdb通过网络拷贝、slave旧数据的清理、slave aof rewrite,很耗费时间

如果复制的数据量在4G~6G之间,那么很可能全量复制时间消耗到1分半到2分钟

 

29:说说增量复制的机制

1)如果全量复制过程中,master-slave网络连接断掉,那么salve重新连接master时,会触发增量复制

2master直接从自己的backlog中获取部分丢失的数据,发送给slave node,默认backlog就是1MB

3msater就是根据slave发送的psync中的offset来从backlog中获取数据的

 

30:说说主从复制的端点续传

Redis支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份

master node会在内存中常见一个backlogmasterslave都会保存一个replica offset还有一个master idoffset就是保存在backlog中的。

如果masterslave网络连接断掉了,slave会让master从上次的replica offset开始继续复制

但是如果没有找到对应的offset,那么就会执行一次resynchronization

 

31:什么是无盘复制

Redis支持无盘复制,就是Master在内存中直接创建rdb,然后发送给slave,不会在自己本地落地磁盘了。

配置涉及到的两个参数

repl-diskless-sync

repl-diskless-sync-delay,等待一定时长再开始复制,因为要等更多slave重新连接过来

32:什么是异步复制

就是Master每次接收到写命令之后,现在内部写入数据,然后异步发送给slave node

 

33:什么是Redis的哨兵?

Sentinal,中文名是哨兵,是Redis集群架构中非常重要的一个组件,主要功能如下

1)集群监控,负责监控Redis masterslave进程是否正常工作

2)消息通知,如果某个Redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员

3)故障转移,如果master node挂掉了,会自动转移到slave node

4)配置中心,如果故障转移发生了,通知client客户端新的master地址

哨兵本身也是分布式的,作为一个哨兵集群去运行,互相协同工作

1)故障转移时,判断一个master node是宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题

2)即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了

 

34:为什么Redis哨兵集群只有2个节点无法正常工作?

哨兵集群必须部署2个以上节点

如果哨兵集群仅仅部署了个2个哨兵实例,quorum=1(配置参数)

+----+         +----+

| M1 |---------| R1 |

| S1 |         | S2 |

+----+         +----+

Configuration: quorum = 1 (理解为一个哨兵认为宕机了,就要重新选举了)

master宕机,s1s2中只要有1个哨兵认为master宕机就可以还行切换,同时s1s2中会选举出一个哨兵来执行故障转移

同时这个时候,需要majority,也就是大多数哨兵都是运行的,2个哨兵的majority就是2【同意数量】(2majority=23majority=25majority=34majority=2),2个哨兵都运行着,就可以允许执行故障转移

但是如果整个M1S1运行的机器宕机了,那么哨兵只有1个了,此时就没有majority来允许执行故障转移,虽然另外一台机器还有一个R1,但是故障转移不会执行

经典的3节点哨兵集群

       +----+

       | M1 |

       | S1 |

       +----+

          |

+----+    |    +----+

| R2 |----+----| R3 |

| S2 |         | S3 |

+----+         +----+

Configuration: quorum = 2majority

如果M1所在机器宕机了,那么三个哨兵还剩下2个,S2S3可以一致认为master宕机,然后选举出一个来执行故障转移

同时3个哨兵的majority2,所以还剩下的2个哨兵运行着,就可以允许执行故障转移

 

35:Redis会出现数据丢失的情况吗?如何处理?

1Redis会出现数据丢失的情况,比如主备切换的过程,可能会导致数据丢失,又有两种情况:

1)异步复制导致的数据丢失

因为master -> slave的复制是异步的,所以可能有部分数据还没复制到slavemaster就宕机了,此时这些部分数据就丢失了

2)脑裂导致的数据丢失

脑裂,也就是说,某个master所在机器突然脱离了正常的网络,跟其他slave机器不能连接,但是实际上master还运行着。

此时哨兵可能就会认为master宕机了,然后开启选举,将其他slave切换成了master

这个时候,集群里就会有两个master,也就是所谓的脑裂

此时虽然某个slave被切换成了master,但是可能client还没来得及切换到新的master,还继续写向旧master的数据可能也丢失了

因此旧master再次恢复的时候,会被作为一个slave挂到新的master上去,自己的数据会清空,重新从新的master复制数据

2:解决异步复制和脑裂导致的数据丢失

min-slaves-to-write 1

min-slaves-max-lag 10

要求至少有1slave,数据复制和同步的延迟不能超过10

如果说一旦所有的slave,数据复制和同步的延迟都超过了10秒钟,那么这个时候,master就不会再接收任何请求了

上面两个配置可以减少异步复制和脑裂导致的数据丢失

1)减少异步复制的数据丢失

有了min-slaves-max-lag这个配置,就可以确保说,一旦slave复制数据和ack延时太长,就认为可能master宕机后损失的数据太多了,那么就拒绝写请求,这样可以把master宕机时由于部分数据未同步到slave导致的数据丢失降低的可控范围内

2)减少脑裂的数据丢失

如果一个master出现了脑裂,跟其他slave丢了连接,那么上面两个配置可以确保说,如果不能继续给指定数量的slave发送数据,而且slave超过10秒没有给自己ack消息,那么就直接拒绝客户端的写请求

这样脑裂后的旧master就不会接受client的新数据,也就避免了数据丢失

上面的配置就确保了,如果跟任何一个slave丢了连接,在10秒后发现没有slave给自己ack,那么就拒绝新的写请求,因此在脑裂场景下,最多就丢失10秒的数据

 

36:哨兵集群的自动发现机制

哨兵互相之间的发现,是通过Redispub/sub系统实现的,每个哨兵都会往__sentinel__:hello这个channel里发送一个消息,这时候所有其他哨兵都可以消费到这个消息,并感知到其他的哨兵的存在

每隔两秒钟,每个哨兵都会往自己监控的某个master+slaves对应的__sentinel__:hello channel里发送一个消息,内容是自己的hostiprunid还有对这个master的监控配置

每个哨兵也会去监听自己监控的每个master+slaves对应的__sentinel__:hello channel,然后去感知到同样在监听这个master+slaves的其他哨兵的存在

每个哨兵还会跟其他哨兵交换对master的监控配置,互相进行监控配置的同步。

 

37:说说主从架构中的选主算法

如果一个master被认为odown了,而且majority哨兵都允许了主备切换,那么某个哨兵就会执行主备切换操作,此时首先要选举一个slave来升级成为Master,此时会考虑slave的一些信息:

1)跟master断开连接的时长

2slave优先级

3)复制offset

4run id

如果一个slavemaster断开连接已经超过了down-after-milliseconds10倍,外加master宕机的时长,那么slave就被认为不适合选举为master

(down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state

接下来会对slave进行排

1)按照slave优先级进行排序,slave priority越低,优先级就越高

2)如果slave priority相同,那么看replica offset,哪个slave复制了越多的数据,offset越靠后,优先级就越高

3)如果上面两个条件都相同,那么选择一个run id比较小的那个slave

 

38:说说quorummajority 主备切换的关系

每次一个哨兵要做主备切换,首先需要quorum数量的哨兵认为odown,然后选举出一个哨兵来做切换,这个哨兵还得得到majority哨兵的授权,才能正式执行切换。

如果quorum < majority,比如5个哨兵,majority就是3quorum设置为2,那么就3个哨兵授权就可以执行切换

但是如果quorum >= majority,那么必须quorum数量的哨兵都授权,比如5个哨兵,quorum5,那么必须5个哨兵都同意授权,才能执行切换。

 

39:说说Redis主从架构中的配置传播

哨兵完成切换之后,会在自己本地更新生成最新的master配置,然后同步给其他的哨兵,就是通过之前说的pub/sub消息机制

这里的version号就很重要了,因为各种消息都是通过一个channel去发布和监听的,所以一个哨兵完成一次新的切换之后,新的master配置是跟着新的version号的

其他的哨兵都是根据版本号的大小来更新自己的master配置的

 

40:谈谈Redis Cluster集群架构

Redis cluster,主要是针对海量数据+高并发+高可用的场景,特别是海量数据,也就是你的数据量很大,那么建议就用Redis cluster

Redis cluster可以支撑NRedis master node,每个master node都可以挂载多个slave node

读写分离的架构,对于每个master来说,写就写到master,然后读就从mater对应的slave去读。

高可用,因为每个master都有salve节点,那么如果mater挂掉,Redis cluster这套机制,就会自动将某个slave切换成master

只要基于Redis cluster去搭建Redis集群即可,不需要手工去搭建replication复制+主从架构+读写分离+哨兵集群+高可用

如果你的数据量很少,主要是承载高并发高性能的场景,比如你的缓存一般就几个G,单机足够了,那就采用主从架构,一个mater,多个slave,要几个slave跟你的要求的读吞吐量有关系,然后自己搭建一个哨兵集群,去保证Redis主从架构的高可用性,就可以了。

 

41:Redis Cluster数据分布的算法

Cluster在数据分布的时候:

1)自动将数据进行分片,每个master上放一部分数据

2)提供内置的高可用支持,部分master不可用时,还是可以继续工作的

 

1Redis clusterhash slot算法

Redis cluster有固定的16384hash slot,对每个key计算CRC16值,然后对16384取模,可以获取key对应的hash slot

Redis cluster中每个master都会持有部分slot,比如有3master,那么可能每个master持有5000多个hash slot

hash slotnode的增加和移除很简单,增加一个master,就将其他masterhash slot移动部分过去,减少一个master,就将它的hash slot移动到其他master上去,移动hash slot的成本是非常低的

客户端的api,可以对指定的数据,让他们走同一个hash slot,通过hash tag来实现。

 

42:Redis Cluster节点之间的通信

1redis cluster节点间采取gossip协议进行通信

跟集中式不同,不是将集群元数据(节点信息,故障,等等)集中存储在某个节点上,而是互相之间不断通信,保持整个集群所有节点的数据是完整的

维护集群的元数据用得,集中式,一种叫做gossip的协议。

集中式:好处在于,元数据的更新和读取,时效性非常好,一旦元数据出现了变更,立即就更新到集中式的存储中,其他节点读取的时候立即就可以感知到; 不好在于,所有的元数据的跟新压力全部集中在一个地方,可能会导致元数据的存储有压力

gossip:好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力; 缺点,元数据更新有延时,可能导致集群的一些操作会有一些滞后

210000端口

Redis cluster架构下,每个Redis要放开两个端口号,比如一个是6379,另外一个就是加10000的端口号,比如1637916379端口号是用来进行节点间通信的,也就是cluster bus的东西,集群总线。cluster bus的通信,用来进行故障检测,配置更新,故障转移授权。

每隔节点每隔一段时间都会往另外几个节点发送ping消息,同时其他几点接收到ping之后返回pong

3)交换的信息

故障信息,节点的增加和移除,hash slot信息,等等

 

43:Redis高并发

主从架构,一主多从,一般来说,很多项目其实就足够了,单主用来写入数据,单机几万QPS,多从用来查询数据,多个从实例可以提供每秒10万的QPS

Redis高并发的同时,还需要容纳大量的数据:一主多从,每个实例都容纳了完整的数据,比如Redis主就10G的内存量,其实你就最对只能容纳10g的数据量。

如果你的缓存要容纳的数据量很大,达到了几十g,甚至几百g,或者是几t,那你就需要Redis集群,而且用Redis集群之后,可以提供可能每秒几十万的读写并发。

44:Redis高可用

如果你做主从架构部署,其实就是加上哨兵就可以了,就可以实现,任何一个实例宕机,自动会进行主备切换。但要注意:

1)哨兵至少需要3个实例,来保证自己的健壮性

2)哨兵 + Redis主从的部署架构,是不会保证数据零丢失的,只能保证Redis集群的高可用性

3)对于哨兵 + Redis主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练

另外一个保障Reids高可用的方案就是:做Redis的集群。

45:在项目中缓存是如何使用的?

这个,你结合你自己项目的业务来,你如果用了那恭喜你,你如果没用那不好意思,你硬加也得加一个场景吧。

用缓存,主要是俩用途,高性能和高并发

1)高性能

假设这么个场景,你有个操作,一个请求过来,吭哧吭哧你各种乱七八糟操作mysql,半天查出来一个结果,耗时600ms。但是这个结果可能接下来几个小时都不会变了,或者变了也可以不用立即反馈给用户。那么此时咋办?

缓存啊,折腾600ms查出来的结果,扔缓存里,一个key对应一个value,下次再有人查,别走mysql折腾600ms了。直接从缓存里,通过一个key查出来一个value2ms搞定。性能提升300倍。

这就是所谓的高性能。

就是把你一些复杂操作耗时查出来的结果,如果确定后面不咋变了,然后但是马上还有很多读请求,那么直接结果放缓存,后面直接读缓存就好了。

2)高并发

    Mysql这么重的数据库,压根儿设计不是让你玩儿高并发的,虽然也可以玩儿,但是天然支持不好。mysql单机支撑到2000qps也开始容易报警了。

所以要是你有个系统,高峰期一秒钟过来的请求有1万,那一个mysql单机绝对会死掉。你这个时候就只能上缓存,把很多数据放缓存,别放mysql。缓存功能简单,说白了就是key-value式操作,单机支撑的并发量轻松一秒几万十几万,支撑高并发so easy。单机承载并发量是mysql单机的几十倍。

3)所以你要结合这俩场景考虑一下,你为啥要用缓存?

一般很多人的项目里没啥高并发场景,那就别折腾了,直接用高性能那个场景吧,就思考有没有可以缓存结果的复杂查询场景,后续可以大幅度提升性能,优化用户体验,有,就说这个理由,没有??那你也得编一个出来吧,不然你不是在搞笑么

4)用了缓存之后会有啥不良的后果?

你要是没考虑过这个问题,那你就尴尬了,面试官会觉得你头脑简单,四肢也不发达。你别光是傻用一个东西,多考虑考虑背后的一些事儿。

常见的缓存问题比如:

1)缓存与数据库双写不一致

2)缓存雪崩

3)缓存穿透

4)缓存并发竞争

这几个问题是常见面试题,你至少自己能说出来,并且给出对应的解决方案

 

46:谈谈缓存与数据库双写时的数据一致性

1:最经典的缓存+数据库读写的模式

1)读的时候,先读缓存,缓存没有的话,那么就读数据库,然后取出数据后放入缓存,同时返回响应

2)更新的时候,先删除缓存,然后再更新数据库

为什么是删除缓存,而不是更新缓存呢?

原因很简单,很多时候,复杂点的缓存的场景,因为缓存有的时候,不简单是数据库中直接取出来的值。

2:简单的缓存不一致问题

    问题:先修改数据库,再删除缓存,如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据出现不一致

解决思路:

    先删除缓存,再修改数据库,如果删除缓存成功了,如果修改数据库失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致

因为读的时候缓存没有,则读数据库中旧数据,然后更新到缓存中

3:较复杂的数据不一致问题

    数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改,一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中

数据变更的程序完成了数据库的修改

完了,数据库和缓存中的数据不一样了。。。。

这种情况,只有同一个数据在并发的进行读写的时候,才可能会出现这种问题,如果说你的并发量很低的话,特别是读并发很低,每天访问量就1万次,那么很少的情况下,不会出现刚才描述的那种不一致的场景

但是问题是,如果每天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现上述的数据库+缓存不一致的情况。高并发了以后,问题是很多的。

4:解决方案:数据库与缓存更新与读取操作进行异步串行化

更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个jvm内部的队列中。

读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个jvm内部的队列中。

一个队列对应一个工作线程。

每个工作线程串行拿到对应的操作,然后一条一条的执行。

这样的话,一个数据变更的操作,先执行,删除缓存,然后再去更新数据库,但是还没完成更新。

此时如果一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。

这里有一个优化点,一个队列中,其实多个更新缓存请求串在一起是没意义的,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可。

待那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存。

如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回; 如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。

5:高并发的场景下,该解决方案要注意的问题

1)读请求长时阻塞

由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须在超时时间范围内返回。

该解决方案,最大的风险点在于,可能数据更新很频繁,导致队列中积压了大量更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库。

务必通过一些模拟真实的测试,看看更新数据的频率是怎样的

另外一点,因为一个队列中,可能会积压针对多个数据项的更新操作,因此需要根据自己的业务情况进行测试,可能需要部署多个服务,每个服务分摊一些数据的更新操作。

如果一个内存队列里居然会挤压100个商品的库存修改操作,每隔库存修改操作要耗费10ms区完成,那么最后一个商品的读请求,可能等待10 * 100 = 1000ms = 1s后,才能得到数据。这个时候就导致读请求的长时阻塞。

一定要做根据实际业务系统的运行情况,去进行一些压力测试,和模拟线上环境,去看看最繁忙的时候,内存队列可能会挤压多少更新操作,可能会导致最后一个更新操作对应的读请求,会hang多少时间,如果读请求在200ms返回,如果你计算过后,哪怕是最繁忙的时候,积压10个更新操作,最多等待200ms,那还可以的

如果一个内存队列可能积压的更新操作特别多,那么你就要加机器,让每个机器上部署的服务实例处理更少的数据,那么每个内存队列中积压的更新操作就会越少。

其实根据之前的项目经验,一般来说数据的写频率是很低的,因此实际上正常来说,在队列中积压的更新操作应该是很少的。

针对读高并发,读缓存架构的项目,一般写请求相对读来说,是非常非常少的,每秒的QPS能到几百就不错了。

一秒,500的写操作,5份,每200ms,就100个写操作

单机器,20个内存队列,每个内存队列,可能就积压5个写操作,每个写操作性能测试后,一般在20ms左右就完成

那么针对每个内存队列中的数据的读请求,也就最多hang一会儿,200ms以内肯定能返回了

QPS扩大10倍,但是经过刚才的测算,就知道,单机支撑写QPS几百没问题,那么就扩容机器,扩容10倍的机器,10台机器,每个机器20个队列,200个队列。

大部分的情况下,应该是这样的,大量的读请求过来,都是直接走缓存取到数据的。

少量情况下,可能遇到读跟数据更新冲突的情况,如上所述,那么此时更新操作如果先入队列,之后可能会瞬间来了对这个数据大量的读请求,但是因为做了去重的优化,所以也就一个更新缓存的操作跟在它后面。

等数据更新完了,读请求触发的缓存更新操作也完成,然后临时等待的读请求全部可以读到缓存中的数据。

2)读请求并发量过高

这里还必须做好压力测试,确保恰巧碰上上述情况的时候,还有一个风险,就是突然间大量读请求会在几十毫秒的延时hang在服务上,看服务能不能抗的住,需要多少机器才能抗住最大的极限情况的峰值。

但是因为并不是所有的数据都在同一时间更新,缓存也不会同一时间失效,所以每次可能也就是少数数据的缓存失效了,然后那些数据对应的读请求过来,并发量应该也不会特别大。

1:99的比例计算读和写的请求,每秒5万的读QPS,可能只有500次更新操作,如果一秒有500的写QPS,那么要测算好,可能写操作影响的数据有500条,这500条数据在缓存中失效后,可能导致多少读请求,发送读请求到库存服务来,要求更新缓存。

一般来说,1:11:21:3,每秒钟有1000个读请求,会hang在库存服务上,每个读请求最多hang多少时间,200ms就会返回。

在同一时间最多hang住的可能也就是单机200个读请求,同时hang住,单机hang200个读请求,还是ok的。

1:20,每秒更新500条数据,这500秒数据对应的读请求,会有20 * 500 = 1万,1万个读请求全部hang在库存服务上,就死定了。

3)多服务实例部署的请求路由

可能这个服务部署了多个实例,那么必须保证说,执行数据更新操作,以及执行缓存更新操作的请求,都通过nginx服务器路由到相同的服务实例上

4)热点商品的路由问题,导致请求的倾斜

万一某个商品的读写请求特别高,全部打到相同的机器的相同的队列里面去了,可能造成某台机器的压力过大。

就是说,因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以更新频率不是太高的话,这个问题的影响并不是特别大。

但是的确可能某些机器的负载会高一些。

 

47:如果Redis要支撑超过10+的并发,那应该怎么做?

单机的Redis几乎不太可能QPS超过10+,除非一些特殊情况,比如你的机器性能特别好,配置特别高,物理机,维护做的特别好,而且你的整体的操作不是太复杂

单机能支撑几万的并发。要支撑10+并发,常见方案有:

1:主从架构 + 读写分离

读写分离,一般来说,对缓存,一般都是用来支撑读高并发的,写的请求是比较少的,可能写请求也就一秒钟几千,一两千

大量的请求都是读,一秒钟二十万次读

读写分离

主从架构 -> 读写分离 -> 支撑10+QPS的架构

2:对业务数据进行垂直拆分,放到多套Redis里面去

    比如:一个Redis单独放商品信息,支持6万并发、一个Redis单独放用户信息,支持2万并发、一个Redis单独放订单信息支持5万并发,合起来是不是也支持到10+了。

 

48:假如 Redis 里面有 1 亿个 key,其中有 10w key 是以 某个固定的已知的前缀开头的,如果将它们全部找出来?

1:方案一:使用 keys 指令可以扫出指定模式的 key 列表。

这个方案有一个问题,因为redis是单线程的。keys 指令会导致线 程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。

2:方案二:使用 scan 指令

scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,而且整体所花费的时间会比直接用 keys 指令长。