本章面试内容:
一、使用场景
1、缓存
2、分布式锁
一、使用场景
1、缓存
解决方案:
1、缓存 null 值,在redis中缓存 null ,缺点是需要消耗内存。
2、在查询 redis 之前,利用布隆过滤器判断数据是否存在,可能存在误差。
面试官:解释一下 布隆过滤器。
答:布隆过滤器 主要是用来检查一个元素是否在一个集合中,在项目中使用的是redisson实现的布隆过滤器。
布隆过滤器底层,先初始化一个比较大的数组,一开始这个数组里全是 0 ,当一个key来了后经过三次hash计算,并模于数组的长度,得到数组中的三个坐标,将对应坐标上的数字 0 改为 1 ,这样数组中的三个坐标就可以表示一个key的存在,查询的过程也一样。
缺点:存在误差,会产生一定的误差,一般可以设置这个误差在 5% 以内,一般项目是可以接受的,这样当大量的并发过来的时候不至于压倒数据库DB。如果要减小误差,就得扩大底层数组的长度,这样又会更消耗内存。
面试官:什么是缓存击穿?怎么解决?
缓存击穿:缓存击穿的意思是,对于设置了过期时间的key,某个时间这个key刚好过期了,这个时候,恰好这个key有大量的并发请求过来,这些请求发现缓存过期,一般都会去数据库中加载数据,并回设缓存到redis,可能会瞬间压垮数据库。
两种解决方案:
一、使用互斥锁:
当缓存失效的时候,不立即去 查询数据库,加载缓存,先使用redis的setnx设置一个互斥锁,再去数据库加载数据,并缓存数据到redis,否则重试get缓存方法。
二、设置key逻辑过期:
1、在设置key的时候,设置一个过期字段,不给当前的key设置过期时间。
2、当查询的时候,从redis取出数据判断当前key是否过期。
3、如果当前的key过期了,开通另一个线程进行数据同步,当前线程正常返回数据,这个数据不是最新的。
两种方案的优缺点:
强一致性选择互斥锁的方案,性能上可能没那么高,因为锁需要等待,也可能会产生死锁。
高可用选择key逻辑过期,性能比较高,但是数据同步做不到强一致。
面试官:什么是缓存雪崩?怎么解决
缓存雪崩:设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到数据库DB,数据库瞬间压力过大,导致雪崩,与缓存击穿的区别,雪崩时同一时刻大量的key过期,击穿是一个key。
解决方案:
1、将缓存失效的时间分开,比如在原来过期时间的基础上添加一个随机时间。避免key集体失效。
2、如果是redis宕机,导致并发请求到数据库,可以给redis做成集群模式,哨兵模式。
3、给缓存业务添加降级限流策略,比如在nginx,gateway添加限流策略。
4、给业务添加多级缓存。让redis作为二级缓存。
面试官:redis作为缓存,mysql的数据和redis的数据如何同步?(双写一致性)。
正常情况下:
读数据:请求redis数据命中,返回数据,未命中,到数据库查数据并,回设缓存。
写操作(延迟双删):删除缓存 --->操作数据库--->删除缓存
操作数据的时候出现的问题:
1、先操作redis:
线程1 删除缓存 --- 线程1 修改数据库数据为10 --- 线程2 查询缓存 --- 未命中 ---线程2查询数据库,查到 数据10 --- 缓存重建,回设缓存数据为10。这是正常情况。
假设:线程1 删除缓存 --- 线程2 查询缓存 --- 未命中,没查到数据 --- 线程2 查询数据库数据为10 --- 线程2 回设缓存为10 --- 线程1 修改数据库数据为20。这时数据库数据20、缓存数据10,数据不一致,出现脏数据。
2、先操作数据库:
线程1 修改数据库数据为20 --- 线程1 删除缓存 。线程2 查询缓存 --- 未命中,线程2 查询数据库数据20 --- 线程2 回设缓存。这是正常情况。
假设:线程1 查询缓存 --- 未命中 --- 线程1 查询数据库,得到数据 10 --- 线程2 更新数据库数据为20 ---- 线程2 删除缓存 --- 线程1 回设缓存为10。这时候数据库数据 20,缓存数据 10 ,数据不一致出现脏数据。
三个问题:
1、先删除缓存还是先删除数据库。
答:都不行,因为可能会出现脏数据。上面有分析。
2、为什么要删除两次缓存。
答:避免产生脏数据。
3、为什么要延时删除。
答:因为数据库一般是主从读写分离的方式。主从同步的时候有一定延时,这个延时的过程中可能会出现脏数据。
三种解决方案:
1、添加互斥锁的方式:
线程1 加锁 --- 线程1 写数据 --- 线程1 删除缓存 ---线程1 释放锁。
线程2 加锁 --- 线程2 读缓存,未命中 --- 线程2 读数据库 --- 线程2 更新缓存 --- 线程2 解锁。
优化:读写锁(redisson),共享锁(读读不互斥,读写互斥),排他锁(读读互斥,读写互斥)
采用redisson的读写锁,在读数据的时候添加共享锁,在写数据的时候添加排他锁,这样就能保证在写数据的同时,不让其他线程读取数据。避免脏数据的出现,注意,读方法和写方法需要使用同一把锁。
2、MQ异步通知的方式
操作数据库的服务、操作redis缓存的服务、MQ
数据库更新时,向MQ发送通知,缓存的服务监听MQ,更新缓存。数据同步有一定的延迟,但是最终数据一致。还要MQ消息的可靠性。
3、cannal异步通知的方式
操作数据库的服务、cannal组件、数据库的binlog日志文件
cannal服务把自己伪装成mysql的一个从节点,当数据库更新时,cannal读取binlog数据,然后通过cannal客户端获取数据,更新缓存。
cannal组件实现数据同步,不需要更改业务代码。
面试官:你听说过延时双删吗?为什么不用它?
答:延迟双删,如果时写操作,先删除缓存,然后更新数据库,最后在延时删除缓存。其中这个延时多久时间不好确定,在延时的过程中,可能会出现脏数据,并不能保证强一致性,所以没有采用它。
面试官:redis作为缓存,数据的持久化是怎么做的?
redis提供两种持久化方式:
1、RDB 数据备份文件
2、AOF 追加文件
面试官:RDB,AOF两种持久化方式有什么区别
1、RDB 是数据备份文件,将redis内存中的数据写到磁盘上,当redis宕机的时候通过RDB文件来恢复数据。RDB文件是二进制文件,体积小。恢复数据速度快。
2、AOF 是追加文件,redis操作写命令的时候,都会存储在这个文件中,当redis宕机的时候,恢复数据的时候,会在执行一遍文件中的命令,恢复数据。
面试官:两种方式,哪种方式恢复的比较快。
RDB是二进制文件,在保存数据的时候,体积也是比较小的,他恢复的比较快,但是它可能会丢数据。
AOF是追加文件,恢复数据的时候可能会慢一些,但是丢数据的风险小。在AOF文件中可以设置刷盘策略,一般设置为 everysec 每秒批量写入一次命令。
always 同步刷盘
everysec 每秒刷盘
no 操作系统控制
面试官:redis的数据过期策略有哪些?数据过期会立马删除吗?(不会)
答:redis的数据过期策略有两种
一、惰性删除
用到key的时候,在检查这个key是否过期,过期了就删除,没有过期就返回数据。
优缺点:这种方式对cpu友好,对内存不友好。只有用到的时候检测是否过期,不用的时候就在内存中不做处理。会浪费内存空间。
二、定期删除
每个一段时间定期检查内存中的key是否过期,过期了就删除。(这种方式只对一部分的key做检查,并不是检查所有的key)
两种模式的定期删除策略:
show模式:
show模式是定时任务,执行频率10hz,每秒10次,每次执行时间不超过25ms,可以通过修改redis.conf的hz选项来调整次数。
fast模式:
执行频率不固定,每次事件循环都会尝试执行,两次间隔不超过2ms,每次耗时不超过1ms。
优点:
可以通过限制删除操作的执行时常和频率来减少对cpu的影响。另外定期删除,也能有效释放过期键占用的内存
缺点:
难以确定删除操作执行的时常和频率。
redis的过期删除策略:
惰性删除+定期删除两种策略结合使用
面试官:假设缓存过多,内存有限,内存满了怎么办。(删除内存中过期的key,释放内存)
面试官:数据淘汰策略
当redis中内存不够时,这时候在向redis中添加数据,redis就会按照某种规则删除内存中的数据,这种删除数据的规则就是数据淘汰策略。
数据淘汰策略有8种:
1、noeviction:不淘汰任何的key,但是内存满了以后,向redis中添加数据就会报错。这个是redis中默认的策略。
2、volatitle-ttl:对设置了ttl过期时间的key,比较其剩余的时间,ttl剩余的时间越少,优先淘汰。
3、allkeys-random:对全体的key进行随机的淘汰。
4、volatitle-random:对设置了过期时间的key进行随机淘汰
lru或lfu算法随机淘汰
5、allkeys-lru:对所有的key,采用 lru 算法淘汰。
6、volatitle-lru:对设置了有效时间的 key 进行lru淘汰。
7、allkeys-lfu:对全体key,基于lfu算法淘汰。
8、volatitle-lfu:对设置了有效时间的key基于lfu淘汰。
面试官:reids数据淘汰策略有哪些?
redis的淘汰策略有很多,默认淘汰策略为 noeviction 当内存不足的时候,向redis添加数据,会直接报错。
淘汰策略可以在redis.conf中进行配置,这里有两个重要的概念。
LRU:最近最少访问,用当前时间减去最近时间,这个值越大,淘汰的优先级越高。
LFU:最少频率访问,对全体的key做使用频率统计。值越小,淘汰的优先级越高。
在项目中一般使用的是的 allkeys-LRU,挑选最近最少使用的kye进行淘汰,将一些常用的key留在redis中。
面试官:数据库中有100W 条数据,但是redis中只能存储 20 W的数据,如何保证redis中缓存的数据都是热点数据。
可以在redis.conf 配置文件中设置数据淘汰策略为,allkeys-lru(挑选最近最少使用的数据淘汰)。这样就可以保证redis中缓存的数据都是热点数据。
面试官:redis的内存使用完了会发生什么?
答:这要看redis使用的数据淘汰策略是什么,默认情况下,redis的数据淘汰策略是noeviction,这时redis的内存使用完了,会直接报错。一般在项目中会配置redis的数据淘汰策略为 allkeys-lru,这种数据淘汰策略可以保证redis中缓存的数据是热点数据,当内存不够的时候,redis会按照lru算法,去内存中挑选最近最少使用的数据优先淘汰。
面试官:redis分布式锁怎么实现
redis中提供了一个命令,setnx(set if not existes)
由于redis是单线程的,使用了这个命令后,就只能有一个客服端对这个key设置值。如果这个key没有过期,或者这个key没有删除,其他客服端不能设置这个key的值。
面试官:如何控制redis分布式锁的有效时常
redis提供的setnx不好控制,这个时候可以使用redis的一个框架redisson控制锁的有效时常。
在redisson中需要手动加锁,来控制锁的有效时常,和等到时间。在redisson中还有一个看门狗机制,就是说每隔一段时间,就检查当前业务是否还持有锁,如果持有锁,就会刷新锁持有的时间。业务执行完毕后就释放锁。
另一方面,如果是在高并发的情况下。一个业务可能执行的很快,一开始客户1先持有锁进行业务操作,这时候客户2 来了,也想要持有锁,并不会被马上拒绝,而是继续自旋尝试获取锁。当客户1执行完业务释放锁的时候,客户2 就可以立即获得锁。性能也得到提高。
面试官redisson实现的分布式锁可以重入吗?
答:可以重入,这样做是为了避免产生死锁。其实是在内部判断是否是统一个线程,如果是同一个线程就可以重入,不是就拒绝重入。当前线程再次持有锁就计数 +1 ,释放锁就计数 -1 。存储数据采用hash结构,大key可以自定义,小key表示当前线程的id,value表示当前线程重入的次数。
面试官:redis实现的分布式锁能解决主从一致的问题吗?
答:不能,假设线程1 加锁成功,master节点的数据异步复制到slave节点,此时master节点宕机了,slave节点成为新的master节点,线程2 加锁,会在新的master节点加锁成功,这时候就会出现两个线程同时持有一把锁。
redisson中有红锁的机制,当master节点宕机的时候,某个slave会成为新的master节点,其他的slave节点会到新的master上同步数据。old master上就没有了slave。线程到master加锁的时候,就会判断当前的master上是否有slave,没有加锁失败。
但是,如果使用红锁,就要在多个节点上加锁,性能会变低,维护成本也高,官方也不推荐这样的方式。一般项目中也不会使用这样的方式。(可以使用zookeeper实现的分布式锁,来解决这个问题)。
面试官:如果业务要保持强一致性,这个问题要怎么解决。
答:redis本身支持高可用,如果有强一致性要求高的业务,可以使用zookeeper的分布式锁,它可以保证数据的强一致性。