面试----Redis篇

130 阅读15分钟

缓存穿透

image.png

实际上就是一直查询根本不存在的数据,导致每次查询都请求到数据库,使数据库压力过大

解决方法

1、缓存空数据,如果查询根本不存在的数据,就造一个空数据写入缓存供其查询,但这样会消耗内存,并且如果后期这个不存在的数据被插入到数据库中,则会出现redis和数据库数据不一致的情况

image.png

image.png

2、使用布隆过滤器,布隆过滤器就是在数据预热(缓存热点数据)的同时将这些数据库数据同时写入布隆过滤器(布隆过滤器就是用来拦截根本不存在的数据的),并且布隆过滤器是基于位图的,所以写入方法是:写入数据时先经过多次不同的hash运算,将位图中对应位置的值置为1,那么在查询数据时会针对要查询的数据同样做hash运算,判断位图中对应位置的值是否为1,如果为1,说明数据存在。

但这样存在误判的情况,即

image.png

image.png

image.png

既可以扩大位图(但会消耗内存),也可以手动设置误判率(0.05为可接受的误判率,不至于压垮数据库)

image.png

缓存击穿

image.png

缓存击穿就是某个key突然过期,而刚好有针对该key的大量请求(即可称为热点key),导致查询缓存时出现查询不到大部分数据的情况,请求都打到数据库上,使数据库压力过大

解决方法

image.png

互斥锁就是在大量线程查询缓存时只允许一个线程在查询不到缓存的情况下通过数据库数据重建缓存,而其他线程因为获取不到锁而等待一段时间后又重试,这里的重试指的是从查询缓存开始,直到缓存被获取到锁的线程重建成功后,其他线程就可以获取到缓存了。这种方式最终获取到的缓存数据具有强一致性(也就是一定是正确的),但由于其他线程需要等待,所以性能差

image.png

逻辑过期就是一旦发现缓存数据过期,同样会加锁重建缓存,但是通过新开一个线程进行重建的,父线程会直接返回过期的数据,不必等待,同时其他线程无法获取到锁重建缓存,说明此刻已经有其他线程在重建缓存了,因此这个线程只需要返回过期数据即可,也不必等待。这种方式最终是高可用,性能优的

缓存雪崩

image.png

缓存雪崩就是大量的key过期或者redis宕机导致大量请求打到数据库

解决方法

image.png 如图,针对于大量key过期,可以给TTL添加随机值,针对于redis宕机,可以使用哨兵、集群,还可以限流、添加多级缓存

双写一致性

image.png

image.png

对于写操作,如何保证双写一致性:使用延迟双删

先删除缓存还是先删除数据库

先给答案:两种方式都有问题

image.png

在线程1写数据时,首先删除缓存,突然有线程2来查缓存,发现缓存不存在,所以根据数据库重建缓存,并查询到数据v=10,之后线程1继续执行,写入新数据v=20,这样就出现线程2读取到老数据的情况,并且数据库和缓存也不一致

image.png

缓存过期,线程1查询不到缓存于是去查询数据库,此时突然有线程2来写数据库,由于此刻还未重建缓存,于是线程2删除缓存是没有用处的,之后线程1根据重建缓存,但重建的缓存是旧数据,于是又出现了数据库和缓存不一致的情况

为什么要双删

防止数据库和缓存数据不一致(即防止脏数据)

为什么要延时

因为实际场景使用数据库主从模式,需要延迟一定时间让主库的数据同步到从库,但延迟的时间无法精确确定,因此只能控制一部分的风险

实现双写一致

强一致性要求的业务:

image.png

如果使用分布式锁,即将删除缓存和删除数据库锁成一个原子性操作,则线程会互斥等待,性能低。所以可以从缓存的特点出发,缓存的数据一般都是读多写少的,写多的数据一般不会存在缓存中,所以可以使用共享锁和排他锁,共享锁是指读操作不互斥,但是写操作互斥,排他锁是指所有操作都互斥,因此与分布式锁相比,读写锁有一部分锁是不互斥的,也就是线程无需等待,从而提高了一部分性能,保证了数据强一致,但总体上性能还是低的,因此读写锁适用于强一致性要求的业务

image.png

读写锁的实现如上

允许延迟一致的业务:

image.png

使用mq(消息队列)进行异步通知,具体原理可参考redis学习记录中的收银员--厨师场景

image.png

使用阿里中间价Canal进行异步通知

持久化

image.png

RDB

image.png

RDB的执行原理

image.png

这里的原理和数据库篇中的原理一致,所以不再赘述,或者可以自行查看视频

需要补充的点是,主进程写数据的时候拷贝数据副本,之后的主进程的读操作也映射到数据副本,此时就出现了内存数据与磁盘数据不一致的情况,但不必担心,因为每次fork子进程写rdb文件后,子进程会被回收,一段时间后再次fork子进程写rdb文件就会将数据副本的数据写入rdb文件,这个过程是一个不断重复的过程,所以不必担心数据不一致的情况

image.png

AOF

image.png

image.png

image.png

image.png

对比

image.png

image.png

数据过期策略

image.png

惰性删除,cpu不需要经常性地检查key是否过期,提高了一部分性能,但是由于不检查过期,也就不会删除数据,会占用空间

image.png

image.png

定期删除,为了保证对cpu保持一定的友好性,限制了删除操作的频率和耗时,从而使cpu可以空出一部分时间去干别的事,同时由于定期删除不像惰性删除那样不检查过期就不删除数据,而是间隔一定时间删除数据,从而也保证了空间的释放,不会占用很大的空间。然而删除操作的频率和耗时是难以确定的

Redis的缓存失效会不会立即删除

image.png

数据淘汰策略

假如缓存过多,内存是有限的,内存被占满了怎么办:使用数据淘汰策略

注意区分数据淘汰策略和数据删除策略,数据删除策略指的是数据过期后如何处理,而数据淘汰策略是指内存空间不够后数据如何处理

image.png

个人认为比较重要的策略:

  • 不淘汰任何key,但不允许写新数据
  • 根据ttl的剩余值进行淘汰,越小越淘汰
  • 对所有key(包括没有设置过期时间的)随机淘汰
  • 对设置过期时间的key随机淘汰
  • 使用lru算法淘汰
  • 使用lfu算法淘汰
  • 其他的策略就是上面的策略进行组合得到的

使用建议

image.png

熟悉问法

image.png

分布式锁

使用场景

集群模式下的定时任务、抢单、幂等性等场景

示例

image.png

image.png

加互斥锁可以解决,但性能低,然而如果是在集群模式下,每个jvm各自维护自己的互斥锁,这样就导致仍然可能存在来自不同服务器的线程并发修改数据的情况,因此引进分布式锁(锁住所有的jvm线程)

image.png

实现原理

基于setnx实现分布式锁

image.png

为了避免由于服务器宕机等原因导致锁没人释放,所以需要设置一个超时时间,但设置的超时时间太短,则可能业务还没执行完所就被释放了,设置的超时时间太长,则业务执行完后锁还要保留一段时间才释放,其他线程依然处于阻塞状态

如何合理的设置超时时间: 1、 预估时间(还不是很合理) 2、 在业务执行的过程中开一个线程来监视业务的执行时间,如果锁到达超时时间还未执行完业务,就给锁的超时时间延长(较为合理,但手动实现还是很麻烦,因此引入redisson分布式锁)

基于redisson实现分布式锁

redisson分布式锁底层使用的是setnx和lua脚本保证原子性

image.png

redisson实现的分布式锁提供了一个看门狗机制(相当于另外一个线程)用于给锁续期,redisson分布式锁的默认超时过期时间为30s,那么看门狗会每隔10s重置超时时间(相当于续期),所以在这种情况下,只有业务执行完成后才能手动的释放锁,但是这里的手动释放锁与之前并不冲突,如果服务器宕机了,看门狗机制会失效,也就是不再延迟超时时间,当达到锁的超时时间,同样会被自动释放掉

重试机制:另外一个线程获取不到锁,会不断重试,但重试是有阈值的,达到一定重试次数后,线程会直接结束

image.png

如果在使用tryLock时传入了第二个参数,则redisson分布式锁默认你可以自己控制超时时间,此时看门狗机制就不再生效

redisson分布式锁的可重入特性

image.png

获取锁时会判断当前线程的id是否在缓存中存在记录,如果存在则重入次数加一,释放锁时则重入次数减一

redisson分布式锁的主从一致性

image.png

image.png

对于redis搭建的集群,如果主节点突然宕机了,从节点会选举产生新的从节点,但是之前获取锁的线程还未把信息同步到从节点,所以新的主节点依然可以获取到锁,从而出现锁不能实现互斥的情况,可能出现脏数据,因此引入了红锁

image.png

其实就是为了保证数据一致(信息同步),会在多个redis节点创建锁,避免数据的丢失,但红锁的实现复杂、性能差、运维繁,实际项目中很少使用

redis集群方案

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

主从复制、主从同步流程

针对的是高并发

主从复制

image.png

搭建redis集群,可以采取主从复制的方案,其中master节点主要用于写操作,slave节点主要用于读操作,实现读写分离,同时master节点执行写操作后需要将数据同步到从节点,从而提高redis并发能力

主从同步

全量同步

image.png

首先从节点执行replicaof建立连接,并携带replid和offset向主节点请求数据同步,其中主从节点的replid是一致的,如果主节点判断接收到的请求中replid是一致的,说明发送请求的节点是自己的从节点,和自己的存储的数据集是相同的,于是可以将自己的版本信息发送给从节点,从节点保存版本信息

接着主节点执行bgsave命令生成rdb文件发送给从节点,从节点清空本地数据并加载rdb文件,然而由于rdb文件是主节点的父进程fork一个子进程出来写rdb文件的,父进程仍然可以接收请求修改数据,因此为了保证数据一致,从节点加载完rdb文件后,还需要接收到主节点发送来的repl_baklog并执行,这个日志文件保存的就是从节点在加载rdb文件时主节点修改的数据命令,从而进一步保证数据一致性

之后每次同步都不必再加载rdb文件,只需要再执行repl_baklog文件命令即可,那么如何后面每次同步如何知道要执行repl_baklog中的哪部分命令呢,这就需要用到偏移量offset,当从节点再次发起同步请求时会携带offset,主节点对比一下repl_baklog中的offset,从而知道哪部分的数据命令是还未执行过的,将这部分命令发送给从节点执行即可

image.png

image.png 增量同步

image.png

增量同步就是从节点重启等情况下,发送同步请求,被主节点判断为不是第一次同步(因为重启前已经同步过了),所以直接根据offset发送未执行的命令给从节点即可

image.png

image.png

image.png

哨兵模式、集群脑裂

针对的是高可用

哨兵模式

image.png

哨兵就是用来监控、选出新的主节点、通知redis客户端(java代码中使用redistemplate等客户端,当主节点发送变化,无需修改java代码,客户端会自动使用新的主节点)。哨兵本身也是一个节点

image.png

哨兵集群监控redis集群,间隔地向节点发送ping命令,如果响应了pong,则说明节点正常,不响应则还不能认为节点失效了,只能说针对于该哨兵来说这个节点失效了,如果超过一半的哨兵都没收到响应,则说明这个节点真的失效了,需要选取新的主节点(注意术语:主观下线、客观下线)

sentinel集群选取leader:从sentinel集群中选出一个sentinel节点来进行选择redis集群主节点:

image.png

选取redis集群主节点方法

  • 根据断开时间的长度,断开时间越长则缺失的数据越多,越不应该成为主节点
  • 根据从节点优先级slave-priority,越小优先级越高,越可能成为主节点
  • 如果salve-priority都相同,则根据offset值,offset越大,说明从节点同步的数据越多,越可能成为主节点
  • 如果offset还是相同,则判断从节点的id大小,越小越可能成为主节点

集群脑裂

image.png

由于网络的原因,导致主节点和从节点位于不同的网络分区,且此时哨兵集群无法监测到主节点的状态,则会去从节点的分区中选出新的主节点,这样redis集群就有两个主节点了,看起来就像是分裂了一样,称之为集群脑裂,而此时redis客户端连接的还是老的主节点,写数据还是写在老的主节点中

image.png

当网络恢复后,老的主节点就会被强制降为从节点,并作为从节点去同步主节点中的数据,那么之前在老节点新写的数据就被丢失了

image.png

解决方案如下:增加redis配置,对于一个主节点必须至少存在一个从节点,也可以设置主从同步的间隔不能超过5秒,如果违反了以上规则,说明出现了集群脑裂

分片集群、数据读写规则

针对的是海量数据存储和高并发写

分片集群

image.png

多个master节点,各自拥有自己的从节点,并且分片集群不需要哨兵了,因为不同的master节点就相当于哨兵了,相互之间可以监测,同时也不需要哨兵的通知机制,即不需要哨兵通知java代码的redis客户端使用哪个master节点,因为分片集群具有自动路由功能,可以将客户端的请求路由到正确的master节点上

image.png

每个master节点具有各自数量的hash槽,在写缓存的时候会对key进行hash、取模之后放入对应的hash槽,如果要读则根据同样的hash、取模操作得知数据存放在哪个master节点,于是将redis客户端的请求路由到这个正确的master节点

image.png

image.png

redis主从和集群可以保证数据一致性吗

image.png

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

image.png

image.png

image.png

讲一下Redis底层的数据结构

image.png

ZSet用过吗

image.png

image.png

Zset 底层是怎么实现的

image.png

跳表是怎么实现的

image.png

image.png

image.png

跳表是怎么设置层高的

同上

Redis为什么使用跳表而不是用B+树

image.png

压缩列表是怎么实现的

image.png

image.png

介绍一下 Redis 中的 listpack

image.png

image.png

哈希表是怎么扩容的

image.png

image.png

String 是使用什么存储的?为什么不用 c 语言中的字符串

image.png

image.png

Redis哪些地方使用了多线程

TODO

Redis怎么实现的io多路复用

image.png

image.png

TODO

Redis的网络模型是怎样的

image.png TODO

如何实现redis 原子性

image.png

除了lua有没有什么也能保证redis的原子性

image.png

为什么使用redis

Redis 具备高性能

image.png

Redis 具备高并发

image.png

image.png

本地缓存和Redis缓存的区别

image.png

image.png

redis应用场景是什么

image.png

Redis除了缓存,还有哪些应用

image.png

Redis支持并发操作吗

image.png

Redis分布式锁的实现原理?什么场景下用到分布式锁

image.png

image.png

Redis的大Key问题是什么

image.png

大Key问题的缺点

image.png

Redis大key如何解决

image.png

什么是热key

image.png

如何解决热key问题

image.png

如何保证 redis 和 mysql 数据缓存一致性问题

延迟双删(建议先修改缓存、再修改数据库)、分布式锁、读写锁

如何设计秒杀场景处理高并发以及超卖现象

image.png

image.png