Redis常见面试问题汇总梳理解答

  1. 缓存穿透

缓存穿透:查询一个不存在的数据,mysql查询不到数据也不会直接写入缓存,就会导致每次请求都查询数据库。

造成原因:1、业务误操作导致

                2、有人恶意攻击,查询一些不存在的数据

解决方案一

缓存空数据,查询返回的数据为空,仍把这个空数据进行缓存,例如:{key:1,value:null}

优点:简单

缺点:消耗内存,可能会发生不一致的问题

解决方案二:

布隆过滤器

bitmap(位图):相当于是一个以(bit)位为单位的数组,数组中每个单元只能存储二进制数0或1

布隆过滤器作用:可以用于检索一个元素是否在一个集合中

数组越小,误判率越大,数组越大误判率越小,但是同时带来了更多的内存消耗。

布隆过滤器的实现方案:Redisson、Guava

缓存击穿

缓存击穿:给某一个key设置了过期时间,当key过期的时候,恰好这个时间点对这个key有大量的并发请求过来,这些并发的请求可能会瞬间把DB压垮。

解决方案一:互斥锁(强一致,性能差)

解决方案二:逻辑过期(高可用,性能优)

缓存雪崩

缓存雪崩是指在同一时间段有大量的的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

  • 给不同的Key的TTL添加随机值
  • 利用Redis集群提高服务的高可用性        (哨兵模式、集群模式)
  • 给缓存业务添加降级限流策略        (nginx或spring cloud gateway)
  • 给业务添加多级缓存        (Guava或Caffeine)

双写一致性

双写一致性:当修改了数据库的数据,也要同时更新缓存的数据,缓存和数据库的数据要保持一致。

读操作:缓存命中,直接返回,缓存未命中查询数据库,写入缓存,设定超时时间

写操作:延迟双删

先删除缓存还是先修改数据库???

先删除缓存,再修改数据库

1、当线程1删除缓存后,将数据库值改为20,线程2此时来查询数据,未命中缓存,查询数据库获得值为20,此时不会出现问题。

2、当线程1删除缓存后,还没来得及修改数据库值,线程2此时来查询数据,未命中缓存,查询数据库获得值为10(未修改的数据),线程2将值缓存到redis中,此时线程1将数据库中值改为20,此时会出现数据不一致的情况(脏数据)。

先操纵数据库,再删除缓存

1、线程1更新数据库中数据为20,并且删除缓存中数据,此时线程2来查询数,命中缓存中数据20,此时不会出问题。

2、线程1查询缓存,由于数据过期,未命中缓存,查询数据库数据,获得数据为10,此时线程切换,线程2修改数据库中值为20,删除缓存,此时线程1将数据10写入缓存,此时会造成数据不一致的问题。

为什么要删除两次缓存??

为了避免脏数据的产生

为什么还要延时双删?

因为我们一般数据库都是主从模式,所以要延时一会,让主数据库将数据同步到从数据库中,

延时这个时间不好控制,在延时的过程中,依然可能出现脏数据的产生,所以延时双删只能避免一部分脏数据产生的风险。做不到绝对的强一致。

如何解决双写一致问题?

1、添加分布式锁,性能比较差

添加redisson提供的共享锁(读锁)和排他锁(写锁)(强一致,性能差)

允许延时一致的业务,采用异步通知。

具体代码实现:

1、RabbitMQ,使用MQ中间件,更新数据之后,通知缓存删除(允许延时一致)

2、canal监听mysql数据库的binlog文件变化,不需要修改业务代码,伪装为mysql的一个从节点,canal通过读取binlog数据更新缓存(允许延时一致)

  • 二进制日志(binlog)记录了所有的DDL(数据定义语言)语句和DML(数据操纵语言)语句,但不包括数据查询(SELECT,SHOW)语句。

问:redis作为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)(强一致性)

答:我们的项目中,里面有xxx的功能,需要让数据库与redis高度保持一致,因为要求时效性比较高,我们当时采用的读写锁保证的强一致性。

        我们采用的时redisson实现的读写锁,在读的时候添加共享锁,可以保证读读不互斥,读写互斥。当我们更新数据的时候添加排他锁,它是读写,读读都互斥,这样就能保证在写数据的同时是不会让其他线程读数据的,避免了脏数据。这里需要注意的是读方法和写方法需要使用同一把锁才行。

问:那这个排他锁是如何保证读写、读读互斥的呢?

答:其实排他锁底层使用的也是setnx,保证了同时只能有一个线程操作锁住的方法。

问:你听说过延时双删吗?为什么不适用它?

答:延迟双删,如果是写操作,我们先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据,其中这个延时多久不太好确定,在延时的过程中,可能会出现脏数据,并不能保证强一致性,所以没有采用它。

问:redis作为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)(允许延时一时的业务)

答:我们的项目中,里面有xxx的功能,数据同步可以有一定的延时(符合大部分业务)

我们当时采用的是阿里的canal组件实现数据同步,不需要更改业务代码,部署一个canal服务。

canal服务把自己伪装成mysql的一个从节点,当mysql数据更新以后,canal会读取binlog数据,然后在通过canal的客户端获取到数据,更新缓存即可。

持久化

RDB,全称Redis DataBase Backup file(Redis数据备份文件),也被叫做redis数据快照,简单来说就是把内存中的所有数据都记录到磁盘中,当redis实例故障重启后,从磁盘读取快照文件,恢复数据。

RDB的执行原理:

bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据,完成fork后读取内存数据并写入RDB文件。

fork采用的是copy-on-write技术:

  • 当主进程执行读操作时,访问共享内存
  • 当主进程执行写操作时,则会拷贝一份数据,执行写操作

AOF,全称为Append only File(追加文件)。Redis处理每一个写命令都会记录在AOF文件,可以看作是命令日志文件。

因为是记录命令,AOF文件会比RDB文件大的多,而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。通过执行bgrewriteaof命令,可以让AOF文件执行重写功能,以最少的命令达到相同的效果。

Redis也会在触发阈值时自动去重写AOF文件。阈值也可以在redis.conf中配置

数据过期策略

redis对数据设置数据的有效时间,数据过期以后,就需要将数据从内存中删除掉,可以按照不同的规则进行删除,这种删除规则被称为数据的删除策略(数据过期策略)。

惰性删除:设置该key过期时间后,我们不去管它,当需要该key时,我们再检查其是否过期,如果过期,我们就删除它,反之,返回该key。

优点:对CPU比较友好,只会在使用该key时才会进行过期检查,对于很多用不到的key不用浪费时间进行检查。

缺点:对内存不友好,如果一个key已经过期,但是一直没有使用,那么该key就会一直存在内存中,内存永远不会释放。

定期删除:每隔一段时间,我们就会对一些key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数量的随机key进行检查,并删除其中的过期key)。

定期清理有两种模式:

  • SLOW模式是定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf的hz选项来调整这个次数。
  • FAST模式执行频率不固定,但每次间隔不低于2ms,每次耗时不超过1ms。

优点:可以通过限制删除操作执行的时长和频率来减少删除操作对CPU的影响。另外定期删除,也能有效释放过期键占用的内存。

缺点:难以确定删除操作执行的市场和频率。

Redis的过期删除策略:惰性删除 + 定期删除两种策略进行配合使用

数据淘汰策略

数据的淘汰策略:当Redis的内存不够用时,此时在向redis中添加新的key,那么redis就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。

redis支持8中不同策略来选择要删除的key:

  • noeviction:不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略
  • volatile-ttl:对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰。
  • allkeys-random:对全体key,随机进行淘汰。
  • volatile-random:对设置了TTL的key,随机进行淘汰。
  • allkeys-lru:对全体key,基于LRU算法进行淘汰。
  • volatile-lru:对设置了TTL的key,基于LRU算法进行淘汰。
  • allkeys-lfu:对全体key,基于LFU算法进行淘汰。
  • volatile-lfu:对设置了TTL的key,基于LFU算法进行淘汰。

LRU(Least Recently used):最近最少使用,用当前时间减去最后一次访问时间,则淘汰优先级越高。

LFU(Least Frequently used):最少频率使用,会统计每个key的访问频率,值越小淘汰优先级越高。

数据淘汰策略-使用建议

1、优先使用allkeys-lru策略。充分利用LRU算法的优势,把最近最常访问的数据留在缓存中,如果业务有明显的冷热数据区分,建议使用。

2、如果业务中数据的访问频率差别不大,没有明显冷热数据区分,建议使用allkeys-random,随机选择淘汰。

3、如果业务中有置顶的需求,可以使用volatile-lru策略,同时置顶数据不设置过期时间,这些数据就一直不被删除,会淘汰其他设置过期时间的数据。

4、如果业务中有短时高频访问的数据,可以使用allkeys-lfu或volatile-lfu策略。

问:数据库有1000万数据,redis只能缓存20w数据,如何保证redis中的数据都是热点数据?

答:使用allkeys-lru(挑选最近最少使用的数据淘汰)淘汰策略,留下来的都是经常访问的热点数据。

问:redis的内存用完了会发生什么?

答:主要看你数据淘汰策略是什么,如果是默认的配置(noeviction),会直接报错。

redis分布式锁

使用场景:集群情况下的定时任务、抢单、幂等性场景

如果是单体项目,加synchronized锁是没有问题的,如下图:

如果是集群模式下:synchronized锁是jvm的,每个服务都有各自的jvm,它只能解决同一个jvm下线程的互斥,解决不了多个jvm下线程的互斥。所以集群模式下就不能使用本地锁来解决,需要使用外部锁来解决。其实就是分布式锁

Redis实现分布式锁主要利用Redis的setnx命令,setnx是SET if not exit(如果不存在,则SET)的简写。

获取锁:

注意:此处如果不设置过期时间会导致死锁的问题。

#添加锁,NX是互斥,EX是设置超时时间

SET lock value NX EX 10

释放锁:

#释放锁,删除即可
DEL key

Redis实现分布式锁如何合理的控制锁的有效时长?

场景解释:如果业务执行时间比较长,超过了锁的有效时间

如果设置了”锁自动释放时间“这个参数,就不会使用watch-dog这个机制了。

加锁、设置过期时间等操作都是基于lua脚本完成。lua脚本可以保证命令执行的原子性

lua脚本能够调用redis命令,保证多条命令执行的原子性

redisson实现的分布式锁-可重入

redisson实现的分布式锁-主从一致性

redis集群的情况下,有线程1获取锁之后将数据缓存到redis-master中,此时master宕机了,还没来的及将数据同步到slave中,此时又进来一个线程2,由于数据没有同步,所有线程2可以获取到同一个分布式锁,将数据缓存到由哨兵模式选中的master中,此时会出现脏数据。

Redisson锁能解决主从数据一致的问题吗?

不能解决,但是可以使用redisson提供的红锁进行解决,但是这样的话,性能就太低了,如果业务中非要保证数据的强一致性,建议采用实现分布式锁。

问:redis分布式锁如何实现?

答:在redis中提供了一个命令setnx,由于redis是单线程的,用了命令之后,只能由一个客户端对某一个key设置值,在没有过期或删除key的时候,其他客户端是不能设置这个key的。

问:如何控制redis实现分布式锁的有效时长?

答:redis的setnx不好控制这个问题,我们当时采用redis的一个框架redisson实现的。

        在redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间,当锁住的一个业务还没有执行完成的时候,在redisson中加入了一个看门狗机制,就是说每隔一段时间就检查当前业务是否还持有锁,如果持有就增加加锁的持有时间,当业务执行完成之后需要使用释放锁就可以了。

还有一个好处就是在高并发下,一个业务有可能会执行很快,先客户1持有锁的时候,客户2来了以后并不会马上拒绝,它会自旋不断尝试获取锁,如果客户1释放锁之后,客户2就可以马上持有锁,性能也得到了提升。

问:redisson实现的分布式锁是可重入的吗?

答:是可以重入的,这样做是为了避免死锁的产生。这个重入其实在内部就是判断是否是当前线程持有的锁,如果是当前线程持有的锁就会计数,如果释放锁就会在计算上减一。在存储数据的时候采用的hash结构,大key可以按照自己的业务进行定制,其中小key是当前线程的唯一标识。value是当前线程重入的次数。

问:redisson实现的分布式锁能解决主从一致性的问题吗?

答:这个是不能的,比如,当线程1加锁成功后,master节点会异步复制到slave节点,此时当前持有redis锁的master节点宕机,slave节点被提升为新的master节点,假如现在来了一个新的线程2,再次尝试获取锁,就可以直接获得锁,并进行数据更新到新的master节点,从而导致主从不一致。

redis集群

在redis中提供的集群方案由三种:

  • 主从复制
  • 哨兵模式
  • 分片集群

1、redis主从数据同步的流程是什么?

Replication id:简称replid,是数据集的标记,id一致说明是同一数据集,每一个都有唯一的replid,

salve则会继承master节点的replid

offset:偏移量,随着记录在repl_baklog中的数据增多而逐渐增大,slave完成同步时也会记录当前同步的offset,如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。

redis的主从同步

单节点redis的并发能力是有上限的,要进一步提高redis的并发能力,就需要搭建主从集群,实现读写分离,一般都是一主多从,主节点负责写数据,从节点负责读数据

主从同步数据的流程是什么?

全量同步

  1. 从节点请求主节点同步数据(replication id, offset)
  2. 主节点判断是否时第一次请求,是第一次就与从节点同步版本信息(replication id 和offset)
  3. 主节点执行bgsave,生成rdb文件后,发送给从节点去执行
  4. 在rdb生成执行期间,主节点会以命令的方式记录到缓冲区(一个日志文件)
  5. 把生成之后的命令日志文件发送给从节点进行同步

增量同步

  1. 从节点请求主节点同步数据,主节点判断不是第一次请求,不是第一次就获取从节点的offset值
  2. 主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步

2、怎么保证redis的高并发高可用?

Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复,哨兵的机构和作用如下:

  • 监控:Sentinel会不断检查您的master和salve是否按预期工作
  • 自动故障恢复:如果master故障,Sentinel会将一个slave提升为master。当故障实例恢复后也以新的master为主。
  • 通知:Sentinel充当redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给redis的客户端

服务状态监控

Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令:

  • 主管下线:如果某sentinel节点发现某实例未在规定时间响应,则认为该实例主管下线
  • 客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线。quorum值最好超过Sentinel实例数量的一半。

哨兵选主规则

  • 首先判断主与从节点断开时间长短,如超过指定值就排该从节点
  • 然后判断从节点的salve-priority值,越小优先级越高
  • 如果slave-prority一样,则判断slave节点的offset值,越大优先级越高
  • 最后是判断salve节点的运行id大小,越小优先级越高

3、你们使用redis是单点还是集群,哪种集群?

主从(1主1从)+哨兵就可以了。单节点不超过10GB内存,如果redis内存不足则可以给不同服务分配独立的redis主从节点。

4、redis集群脑裂,该怎么解决?

集群脑裂是由于主节点、从节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到主节点,所以通过一个选举的方式提升了一个从节点为主,这样就存在两个master,就像大脑分裂了一样,这样会导致客户端还在老的主节点那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将老的主节点降为从节点,这时再从新master同步数据,就会导致数据丢失。

解决方案:我们可以修改redis的配置,可以设置最少的从节点数量以及缩短主从数据同步的延迟时间,达不到要求就拒绝请求,就可以避免大量的数据丢失。

5、redis分片集群中数据是怎么存储和读取的?

分片集群结构

主从和哨兵可以解决高可用、高并发读的问题,但是依然有两个问题没有解决

  • 海量数据存储问题
  • 高并发写的问题

使用分片集群可以解决上述问题,分片集群特征:

  • 集群中有个多个master,每个master保存不同的数据
  • 每个master都可以有多个slave节点
  • master之间通过ping监测彼此健康状态
  • 客户端请求可以访问集群任意节点,最终都会被转发到正确节点

分片集群结构-数据读写

Redis分片集群引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽

Redis是单线程的,但是为什么还那么快?

  • redis是纯内存操作,执行速度非常快
  • 采用单线程,避免不必要的上下文切换可竞争条件,多线程还要考虑线程安全问题
  • 使用I/O多路复用模型,非阻塞IO

解释一下IO多路复用模型?

redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度,I/O多路复用模型主要就是实现了高效的网络请求。

  • 用户空间和内核空间
  • 常见的IO模型
  1. 阻塞IO(Blocking IO)
  2. 非阻塞IO(Nonblocking IO)
  3. IO多路复用(IO Multiplexing)
  • Redis网络模型

用户空间和内核空间

linux系统中一个进程使用的内存情况划分为两部分:内核空间、用户空间

  • 用户空间只能执行受限的命令(Ring3),而且不能直接调用系统资源,必须通过内核提供的接口来访问
  • 内核空间可以执行特权命令(Ring0),调用一切系统资源

linux系统为了提高IO效率,会在用户空间和内核空间都加入缓冲区:

  • 写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写入设备
  • 读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区

阻塞IO

顾名思义,阻塞IO就是两个阶段都必须阻塞等待:

阶段一:

  1. 用户进程尝试读取数据(比如网卡数据)
  2. 此时数据尚未到达,内核需要等待数据
  3. 此时用户进程也处于阻塞状态

阶段二:

  1. 数据到达并拷贝到内核缓冲区,代表已就绪
  2. 将内核数据拷贝到用户缓冲区
  3. 拷贝过程中,用户进程依然阻塞等待
  4. 拷贝完成,用户进程解除阻塞,处理数据

可以看到,阻塞IO模型中,用户进程在两个阶段都是阻塞状态

非阻塞IO

顾名思义,非阻塞IO的recvfrom操作会立即返回结果而不是阻塞用户进程。

阶段一:

  1. 用户进程尝试读取数据(比如网卡数据)
  2. 此时数据尚未到达,内核需要等待数据
  3. 返回异常给用户进程
  4. 用户进程拿到error后,再次尝试读取
  5. 循环往复,知道数据就绪

阶段二:

  1. 将内核数据拷贝到用户缓冲区
  2. 拷贝过程中,用户进程依然阻塞等待
  3. 拷贝完成,用户进程接触阻塞,处理数据

可以看到,非阻塞IO模型中,用户进程在第一个阶段是非阻塞,第二个阶段是阻塞状态,虽然是非阻塞,但性能并没有得到提高。而且忙等机制会导致cpu空转,cpu使用率暴增。

IO多路复用

IO多路复用:是利用单个线程来监听多个socket,并在某个socket可读、可写时得到通知,从而避免无效的等待,充分利用cpu资源。

阶段一:

  1. 用户进程调用select,指定要监听的Socket集合
  2. 内核监听对应的多个socket
  3. 任意一个或多个socket数据就绪则返回readable
  4. 此过程中用户进程阻塞

阶段二:

  1. 用户进程找到就绪的socket
  2. 依次调用recvfrom读取数据
  3. 内核将数据拷贝到用户空间
  4. 用户进程处理数据

IO多路复用是指利用单个线程来同时监听多个socket,并在某个socket可读、可写时得到通知,从而避免无效的等待,充分利用cpu资源。目前的I/O多路复用都是采用epoll模式实现,它会在通知用户进程socket就绪的同时,把已就绪的socket写入用户空间,不需要挨个遍历socket来判断是否就绪,提升了性能。不过监听socket的方法、通知的方式又有多种实现,常见的有:

  • select
  • poll
  • epoll

差异:

  • select和poll只会通知用户进程有socket就绪,但不确定具体是哪个socket,需要用户进程逐个遍历socket来确认
  • epoll则会在通知用户进程socket就绪的同时,把已就绪的socket写入用户空间

Redis网络模型

redis通过IO多路复用来提高网络性能,并且支持各种不同的多路复用实现,并且将这些实现进行封装,提供了统一的高性能事件库。

就是IO多路复用结合事件的处理器来应对多个socket请求

  • 连接应答处理器
  • 命令回复处理器,在redis6.0以后,为了提升更好的性能,使用了多线程来处理回复事件
  • 命令请求处理器,在redis6.0以后,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程

  • 24
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值