当前位置: 首页 > 科技观察

面试前你必须知道的Redis面试

时间:2023-03-22 16:30:27 科技观察

今天分享一些关于Redis的面试常见问题:缓存雪崩如何解决?如何解决缓存穿透?双写时如何保证缓存和数据库的一致性?1.1什么是缓存雪崩?回顾我们为什么使用缓存(Redis):为什么我们需要缓存现在有一个问题,如果我们的缓存宕机了,那么意味着我们所有的请求都去了数据库。如果缓存关闭,所有请求都将转到数据库。我们都知道Redis不可能缓存所有的数据(内存昂贵且有限),所以Redis需要给数据设置一个过期时间,使用惰性删除。+定期删除过期密钥删除的两种策略。Redis对过期键的策略+持久化如果缓存数据设置的过期时间相同,Redis只是将这部分数据全部删除。这会导致这些缓存在这段时间内同时失效,所有的请求都会发送到数据库。这就是缓存雪崩:Redis挂了,所有的请求都跑到数据库去了。为缓存数据设置相同的过期时间,导致缓存在一定时间内失效,所有请求都去数据库。如果发生缓存雪崩,很可能会破坏我们的数据库,导致整个服务瘫痪!1.2如何解决缓存雪崩?对于“为缓存的数据设置相同的过期时间,导致缓存在一定时间内失效,所有的请求都去数据库”这种情况,很容易解决:解决方法:给过期时间加一个随机值缓存时,同时会大大减少缓存过期。对于“Redis挂了,所有请求都跑到数据库”的情况,我们可以有以下思路:事发前:实现Redis的高可用(master-slave架构+Sentinel或者RedisCluster),尽量避免Redis挂掉的情况发生。事件中:万一Redis真的挂了,我们可以设置本地缓存(ehcache)+限流(hystrix)来尽量避免我们的数据库免于被kill掉(至少保证我们的服务还能正常工作)事发后:Redis是持久化的,重启后自动从磁盘加载数据,快速恢复缓存数据2.缓存穿透2.1什么是缓存穿透?例如,我们有一个数据库表,其ID从1(正数)开始:我刚找到一个数据库表,但可能有黑客想要破坏我的数据库。每个请求的ID都是一个负数。这样会导致我的缓存没有用,所有的请求都会去数据库,但是数据库没有这个值,所以每次都返回空。缓存穿透是指查询一定不存在的数据。因为缓存并不完善,而且为了容错,如果从数据库中查不到数据,就不会写入缓存,这样会导致每次都去数据库中查询不存在的数据被请求了,缓存的意义就没有了。缓存穿透这就是缓存穿透:大量请求的数据在缓存中不准确,导致请求转到数据库。如果发生缓存穿透,也有可能让我们的数据库崩溃,导致整个服务瘫痪!2.1如何解决缓存穿透?解决缓存穿透有两种方案:因为请求的参数是非法的(每次请求都是不存在的参数),所以我们可以使用布隆过滤器(BloomFilter)或者压缩过滤器提前拦截,如果是非法的,这个请求就会不会被传送到数据库层!当我们从数据库中找不到它的时候,我们也会将这个空对象设置到缓存中。下次请求时,可以从缓存中获取。这种情况下,我们一般会设置一个过期时间较短的空对象。参考:缓存系列文章——5.缓存穿透问题https://carlosfu.iteye.com/blog/22481853.缓存与数据库双写一致性3.1对于读操作,流程是这样的上面提到的缓存穿透当时也提到:如果从数据库中找不到数据,就不会写入缓存。一般我们对于读操作都有这样一个固定的套路:如果我们的数据在缓存中,那么我们就直接去缓存中取数据。如果缓存中没有我们想要的数据,我们会先查询数据库,然后将数据库中查到的数据写入缓存。***返回数据给请求3.2什么是缓存和数据库的双写一致性?如果只是查询的话,缓存数据和数据库数据都没有问题。但是当我们想要更新时呢?各种情况都可能导致数据库和缓存数据不一致。这里的不一致是指:数据库中的数据与缓存中的数据不一致。理论上只要我们设置好key的过期时间,就可以保证缓存中的数据和数据库最终是一致的。因为只要缓存数据过期,就会被删除。后面读取的时候,因为没有缓存,可以去数据库中查数据,然后把数据库中查到的数据写入缓存中。除了设置过期时间,我们还需要采取更多措施,尽可能避免数据库和缓存不一致的情况。3.3对于更新操作,一般来说,在执行更新操作时,我们有两种选择:先操作数据库,再操作缓存,两种操作要么同时成功,要么同时失败。所以,这就变成了一个分布式事务问题。因此,如果原子性被破坏,可能会出现以下情况:数据库操作成功,但缓存操作失败。缓存操作成功,但数据库操作失败。如果第一步失败了,我们就直接返回Exception,根本不会执行第二步。我们来详细分析一下。3.3.1操作缓存操作缓存也有两种选择:更新缓存和删除缓存一般我们采用删除缓存的策略和删除缓存的原因如下:更新缓存,更有可能导致数据库和缓存数据不一致。(删除缓存就直接简单多了)如果每次更新数据库都要更新缓存【这里指的是频繁更新的场景,会消耗一定的性能】,还不如直接删除.再次读取时,缓存中不存在,所以会去数据库中查找,在数据库中查找,然后写入缓存中(体现懒加载)。基于这两点,建议更新缓存时进行删除操作!3.3.2先更新数据库,再删除缓存。正常情况是这样的:先操作数据库,成功;删除缓存),会导致数据库中有新数据,缓存中有旧数据。如果第一步(操作数据库)失败,我们可以直接返回错误(Exception),不会出现数据不一致的情况。如果在高并发场景下,数据库和缓存数据不一致的概率极低,也不是不可能:缓存刚过期。线程A查询数据库并获取旧值。线程B将新值写入数据库。线程B删除缓存。线程A将如果找到的旧值写入缓存,实现上述情况,概率极低:因为这种情况需要在读取缓存和缓存失效时发生,并且有并发的写操作。事实上,数据库的写操作会比读操作慢很多,而且表必须加锁,而且读操作必须在写操作之前进入数据库操作,比写操作晚更新缓存。这些条件都满足的概率基本不大。对于这种策略,其实是一种设计模式:CacheAsidePattern先修改数据库,再删除缓存。删除缓存失败的解决方法:将需要删除的key发送到消息队列中自行消费消息,获取需要删除的key。不断reload尝试删除,直到成功3.3.3先删除缓存,再更新数据库正常情况是这样的:先删除缓存,成功;然后更新数据库,也成功了;如果原子性被破坏:第一步成功(删除缓存),第二步失败(更新数据库),数据库和缓存中的数据仍然一致。如果第一步(删除缓存)失败,我们可以直接返回错误(Exception),数据库和缓存中的数据仍然是一致的。看起来很好,但是如果我们在并发场景下分析,就会知道还有一个问题:线程A删除了缓存。线程B查询,发现缓存已经不存在了。线程B去数据库查询旧值。线程B保存旧值写缓存线程A将新值写入数据库,所以也会造成数据库和缓存不一致。解决并发下数据库与缓存不一致的思路:删除缓存、修改数据库、读取缓存等操作,积压在队列中,实现序列化。3.4比较两种策略,可以发现两种策略各有优缺点:先删除缓存,再更新数据库。高并发下性能不尽如人意,但是在破坏原子性的情况下性能非常优秀。先更新数据库,再更新数据库。删除缓存(CacheAsidePattern设计模式)在高并发下表现良好,但在破坏原子性时表现不佳。3.5其他解决方案和保证数据一致性的数据可以用databus或者阿里的运河监控binlog进行更新。参考资料:缓存更新例程https://coolshell.cn/articles/17416.html双写时如何保证缓存和数据库的数据一致性?https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-consistence.md分布式数据库和缓存双写一致性方案解析https://zhuanlan.zhihu.com/p/48334686CacheAsidePatternhttps://blog.csdn.net/z50l2o08e2u4aftor9a/article/details/81008933***这些是关于Redis的一些常见面试问题。我希望你会发现他们有帮助并获得报价!