大家好,我是路人张,这是Redis面试八股文的最后一篇,四篇共大约一万五千字,下一个准备总结JVM,大家在公众号回复“面试手册”可以获取PDF版(Redis的还为整理进去)
推荐阅读:
文章目录
-
如何保证缓存与数据库双写时的数据一致性?
-
一个字符串类型的值能存储最大容量是多少?
-
Redis如何实现大量数据插入?
-
如何通过Redis实现异步队列?
-
如何通过Redis实现延时队列?
-
Redis回收使用什么算法?
-
Redis 里面有1亿个 key,其中有 10 个 key 是包含 java,如何将它们全部找出来?
-
生产环境中的Redis是如何部署的
如何保证缓存与数据库双写时的数据一致性?
这是面试的高频题,需要好好掌握,这个问题是没有最优解的,只能数据一致性和性能之间找到一个最适合业务的平衡点
首先先来了解下一致性,在分布式系统中,一致性是指多副本问题中的数据一致性。一致性可以分为强一致性、弱一致性和最终一致性
-
强一致性:当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的更新过的值。强一致性对用户比较友好,但对系统性能影响比较大。
-
弱一致性:系统并不保证后续进程或者线程的访问都会返回最新的更新过的值。
-
最终一致性:也是弱一致性的一种特殊形式,系统保证在没有后续更新的前提下,系统最终返回上一次更新操作的值。
大多数系统都是采用的最终一致性,最终一致性是指系统中所有的副本经过一段时间的异步同步之后,最终能够达到一个一致性的状态,也就是说在数据的一致性上存在一个短暂的延迟。
如果想保证缓存和数据库的数据一致性,最简单的想法就是同时更新数据库和缓存,但是这实现起来并不现实,常见的方案主要有以下几种:
-
先更新数据库,后更新缓存
-
先更新缓存,后更新数据库
-
先更新数据库,后删除缓存
-
先删除缓存,后更新数据库
乍一看,感觉第一种方案就可以实现缓存和数据库一致性,其实不然,更新缓存是个坑,一般不会有更新缓存的操作。因为很多时候缓存中存的值不是直接从数据库直接取出来放到缓存中的,而是经过一系列计算得到的缓存值,如果数据库写操作频繁,缓存也会频繁更改,所以更新缓存代价是比较大的,并且更改后的缓存也不一定会被访问就又要重新更改了,这样做无意义的性能消耗太大了。下面介绍删除缓存的方案
先更新数据库,后删除缓存
这种方案也存在一个问题,如果更新数据库成功了,删除缓存时没有成功,那么后面每次读取缓存时都是错误的数据。
解决这个问题的办法是删除重试机制,常见的方案有利用消息队列和数据库的日志
利用消息队列实现删除重试机制,如下图
步骤在图中写的已经比较清除了,这里简单说下为什么使用消息队列,消息队列可以保证写到队列中的消息在成功消费之前不会消失,并且在第4步中获取消息时只有消费成功才会删除消息,否则会继续投递消息给应用程序,符合消息重试的要求。
但这个方案也有一些缺点,比如系统复杂度高,对业务代码入侵严重,这时可以采用订阅数据库日志的方法删除缓存。如下图
先删除缓存,后更新数据库
这种方案也存在一些问题,比如在并发环境下,有两个请求A和B,A是更新操作,B是查询操作
-
假设A请求先执行,会先删除缓存中的数据,然后去更新数据库
-
B请求查询缓存发现为空,会去查询数据库,并把这个值放到缓存中
-
在B查询数据库时A还没有完全更新成功,所以B查询并放到缓存中的是旧的值,并且以后每次查询缓存中的值都是错误的旧值
这种情况的解决方法通常是采用延迟双删,就是为保证A操作已经完成,最后再删除一次缓存
逻辑很简单,删除缓存后,休眠一会儿再删除一次缓存,虽然逻辑看起来简单,但实现起来并不容易,问题就出在延迟时间设置多少合适,延迟时间一般大于B操作读取数据库+写入缓存的时间,这个只能是估算,一般可以考虑读业务逻辑数据的耗时 + 几百毫秒。
在实际应用中,还是先更新数据库后删除缓存这种方案用的多些。
需要注意的是,无论哪种方案,如果数据库采取读写分离+主从复制延迟的话,即使采用先更新数据库后删除缓存也会出现类似先删除缓存后更新数据库中出现的问题,举个例子
-
A操作更新主库后,删除了缓存
-
B操作查询缓存没有查到数据,查询从库拿到旧值
-
主库将新值同步到从库
-
B操作将拿到的旧值写入缓存
这就造成了缓存中的是旧值,数据库中的是新值,解决方法还是上面说的延迟双删,延迟时间要大于主从复制的时间
一个字符串类型的值能存储最大容量是多少?
一个字符串类型键允许存储的数据的最大容量是512MB
Redis如何实现大量数据插入?
这个问题在Redis的官方文档给出了答案,从Redis 2.6开始redis-cli
支持一种新的被称之为pipe mode的新模式用于执行大量数据插入工作。具体可以看官网的详细解释,这里就不再复制粘贴了:https://www.redis.com.cn/topics/mass-insert.html
如何通过Redis实现异步队列?
主要有两种方式
第一种是使用List作为队列,通过RPUSH生产消息, LPOP消费消息
存在的问题:如果队列是空的,客户端会不停的pop,陷入死循环
解决方法:
-
当lpop没有消息时,可以使用sleep机制先休眠一段时间,然后再检查有没有消息。
-
可以使用blpop命令,在队列没有数据的时候,会立即进入休眠状态,一旦数据到来,则立刻醒过来。这种做法的缺点是只能提供一个消费者消费
第二种方法是pub/sub主题订阅模式,发送者(pub)发送消息,订阅者(sub)接收消息
存在的问题:消息的发布是无状态的,无法保证到达,如果订阅者在发送者发布消息时掉线,之后上线也无法接收发布者发送的消息
解决方法:使用消息队列
如何通过Redis实现延时队列?
先说下延时队列的使用场景:
-
常见的微信红包场景,A给B发红包,B没有收,1天后钱会退回原账户
-
电商的订单支付场景,订单在半小时内未支付会自动取消
上述场景可以通过定时任务采用数据库/非关系型数据库轮询方案或延迟队列,现主要介绍下Redis实现的延迟队列
可以通过Redis的zset命令实现延迟队列,ZSET是Redis的有序集合,通过zadd score1 value1
命令向内存中生产消息,并利用设置好的时间戳作为score进行排序,然后通过zrangebysocre 查询符合条件的所有待处理的任务,循环执行,也可以zrangebyscore key min max withscores limit 0 1
查询最早的一条任务,来进行消费,如下图(画的第二种,好画点)
Redis回收使用什么算法?
LRU算法和引用计数法
LRU算法很常见,在学习操作系统时也经常看到,淘汰最长时间没有被使用的对象,LRU算法在手撕代码环节也经常出现,要提前背熟
引用计数法在学习JVM中也见过的,对于创建的每一个对象都有一个与之关联的计数器,这个计数器记录着该对象被使用的次数,当对象被一个新程序使用时,它的引用计数值会被增1,当对象不再被一个程序使用时,它的引用计数值会被减1,垃圾收集器在进行垃圾回收时,对扫描到的每一个对象判断一下计数器是否等于0,若等于0,就会释放该对象占用的内存空间,简单来说就是淘汰使用次数最少的对象(LFU算法)
Redis 里面有1亿个 key,其中有 10 个 key 是包含 java,如何将它们全部找出来?
可以使用Redis的KEYS命令,用于查找所有匹配给定模式 pattern 的 key ,虽然时间复杂度为O(n),但常量时间相当小。
注意: 生产环境使用 KEYS命令需要非常小心,在大的数据库上执行命令会影响性能,KEYS指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个命令适合用来调试和特殊操作,像改变键空间布局。
不要在你的代码中使用 KEYS 。如果你需要一个寻找键空间中的key子集,考虑使用 SCAN 或 sets。
生产环境中的Redis是如何部署的
这个按自己的情况说就行了,但得提前想好怎么说,避免忘了
最后,最近建了一个微信求职交流群,欢迎大家进群交流,需先添加微信好友(备注进群),然后拉你进群哈