【本文章旨在帮助你快速了解Java面试中会遇到的高频面试题,大部分内容来自黑马程序员的Java面试课程->视频链接,以及两个很好用的面试题网站,小林coding->网站链接 和JavaGuide->网站链接,我将这三者进行了总结和归纳,希望能帮助到你,也希望你在准备面试题的时候不只是“背会了”,而是“通过准备面试题拓展自己的知识面,掌握技术背后的细节和逻辑”。准备面试是一个煎熬,焦虑,迷茫的过程,多看天,多想想,多走走。】
最高优先级(必问,出现频率极高)
Redis缓存相关问题(穿透/击穿/雪崩)- 80%的面试必问
Redis和MySQL数据同步问题- 实际项目经验重点
Redis分布式锁实现- 并发场景必考
Redis持久化机制(AOF/RDB)- 基础原理必问
高优先级(经常被问到)
Redis数据结构及应用场景- 基础中的基础
Redis数据淘汰策略- 内存管理必问
Redis集群方案(主从复制、哨兵模式)- 高可用方案
Redis为什么这么快(单线程优势)- 原理性问题
中优先级(偶尔问到)
Redis分片集群(数据分片、哈希槽)- 大数据量场景
Redis数据过期策略- 基础原理
Redis集群脑裂问题- 高级问题
布隆过滤器原理- 缓存穿透解决方案细节
低优先级(选择性准备)
Redisson实现细节(可重入锁、看门狗)- 框架细节
RedLock红锁问题- 分布式锁进阶
AOF刷盘策略比较- 持久化细节
RDB写时复制技术- 原理细节
可以简化的
Redis集群脑裂:知道概念即可,不必深入细节
RedLock红锁:知道存在问题,不用记具体实现
AOF/RDB详细机制:知道是什么、区别、优缺点即可
最重要的5个问题(时间紧张就看这些)
缓存穿透、击穿、雪崩的区别和解决方案?
Redis和MySQL如何保证数据一致性?
Redis分布式锁怎么实现?有什么问题?
Redis持久化有哪两种方式?有什么区别?
Redis的数据结构有哪些?分别用在什么场景?
Redis
Redis数据结构以及应用场景: 高频
首先,我们要知道此处的数据类型指的是value的类型,Redis的key都是字符串类型
常见的:
String(字符串):
可以用来存储任何类型的数据比如字符串、整数、浮点数、图片(图片的 base64 编码或者解码或者图片的路径)、序列化后的对象。
应用场景:任何常规数据,分布式锁(setnx key value)
主要命令:SET,GET,MSET(批量),MGET
Hash(散列):
值是 field-value 结构,field为属性,value为字段的值。
应用场景:非常适合存储对象。
主要命令:HSET ,HGET,HMSET,HMGET
String和Hash谁更适合存储对象?
String存储对象是先把对象转为 Json 格式,需要使用再反序列化回来,适合对象整体结构简单且读 写整个对象是主要操作时;Hash是对对象的每个字段单独存储,可以获取部分字段的信息,也可以修改或者添加部分字段,如果对象中的字段信息经常变动,或者我们经常要获取对象某个字段,Hash 就非常适合。
问:购物车信息用 String 还是 Hash 存储更好呢?
由于购物车中的商品频繁修改和变动,购物车信息建议使用 Hash 存储:
- 用户 id 为 key
- 商品 id 为 field,商品数量为 value
List(列表):
与Java中的LinkedList类似,可以看做是一个双向链表结构。
有序,元素可以重复,插入删除快,查询一般
应用场景:适合存储一个有序数据,例如:朋友圈点赞列表,评论列表等。
主要命令:LPUSH,LPOP–>在左边插入删除元素, RPUSH,RPOP–>在右边插入删除元素
BLPOP,BRPOP–>阻塞一段时间再移除元素
模拟栈:入口和出口在同一边
模拟队列:入口和出口不在同一边
模拟阻塞队列:入口和出口不在同一边,且出队列采用 BLPOP 或 BRPOP
Set(集合):
与 Java 中的 HashSet类似,无序,元素不重复,查找快,支持交集、并集、差集
应用场景:存放数据不能重复的场景:网站 UV (访问人数)统计(数据量小),用户的共同好友。
ZSet(可排序集合):
每一个元素都带有一个score属性,可以基于score属性排序
注意:score值越大,排名越靠后,排名从0开始
应用场景:适合用来做排行榜。
不常见的:
BitMap(位图):
二进制数组,存放0和1,适合统计活跃用户(用户一段时间内的登录次数)
HyperLogLog(基数统计):
适合统计网站UV(网站人数多)
Geo (地理位置):
适合存储地理信息
Stream(流):
适合做消息队列
Redis缓存相关问题:
1.缓存穿透: 必问
缓存空值 布隆过滤器(误判)
缓存穿透是指查询一个一定不存在的数据,由于存储层查不到数据因此不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。这种情况大概率是遭到了攻击。解决方案的话,我们通常都会用布隆过滤器来解决它。
布隆过滤器:
布隆过滤器主要是用于检索一个元素是否在一个集合中。我们当时使用的是Redisson实现的布隆过滤器。它的底层原理是,先初始化一个比较大的数组,里面存放的是二进制0或1。一开始都是0,当一个key来了之后,经过3次hash计算,模数组长度找到数据的下标,然后把数组中原来的0改为1。这样,三个数组的位置就能标明一个key的存在。查找的过程也是一样的。当然,布隆过滤器有可能会产生一定的误判,我们一般可以设置这个误判率,大概不会超过5%。其实这个误判是必然存在的,要不就得增加数组的长度。5%以内的误判率一般的项目也能接受,不至于高并发下压倒数据库。
2.缓存击穿:必问
setnx互斥锁 逻辑过期时间
缓存击穿的意思是,对于设置了过期时间的key,缓存在某个时间点过期的时候,恰好这个时间点对这个Key有大量的并发请求过来。这些请求发现缓存过期,一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。解决方案有两种方式:第一,可以使用互斥锁:当缓存失效时,不立即去load db,先使用如 Redis 的SETNX去设置一个互斥锁。当操作成功返回时,再进行 load db的操作并回设缓存,否则重试get缓存的方法。第二种方案是设置当前key逻辑过期,大概思路如下:1) 在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前key设置过期时间;2) 当查询的时候,从redis取出数据后判断时间是否过期;3) 如果过期,则开通另外一个线程进行数据同步,当前线程正常返回数据,这个数据可能不是最新的。当然,两种方案各有利弊:如果选择数据的强一致性,建议使用分布式锁的方案,但性能上可能没那么高,且有可能产生死锁的问题。如果选择key的逻辑删除,则优先考虑高可用性,性能比较高,但数据同步这块做不到强一致。
3.缓存雪崩:必问
缓存过期时间设随机值
缓存雪崩意思是,设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重而雪崩。与缓存击穿的区别是:雪崩是很多key,而击穿是某一个key缓存。解决方案主要是,可以将缓存失效时间分散开。比如,可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机。这样,每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
4.Redis和Mysql数据同步问题:必问
Q:redis作为缓存,mysql的数据如何与redis进行同步呢?
在我“优觅生活”的项目中,我采用的方案是,使用采用Cache-Aside 模式作为缓存基础架构。
什么是Cache-Aside 模式呢?
如果是读操作,
- 先尝试从缓存读取数据。
- 如果缓存命中,直接返回数据。
- 如果缓存未命中,从数据库查询数据,将查到的数据放入缓存并返回数据。
如果是写操作,
- 先更新数据库。
- 再直接删除缓存中对应的数据。
同时呢,我针对不同业务场景,组合运用Redission读写锁+阿里巴巴Canal组件解决数据一致性问题。
A:我通过两个地方向您表述:1、嗯!就说我做的项目吧,里面有缓存店铺分类信息(分类名称,使用的图片地址,创建时间修改时间等)。数据同步可以有一定的延时(这符合大部分业务需求)。我们当时采用的阿里的Canal组件实现数据同步:不需要更改业务代码,只需部署一个Canal服务。Canal服务把自己伪装成mysql的一个从节点。当mysql数据更新以后,Canal会读取binlog日志中的数据,然后再通过Canal的客户端获取到数据,并更新缓存即可。(有了canal,还需要cache-Aside模式吗?需要,因为当第一个用户到来,他去缓存中拿数据,但是此时mysql数据库中没有数据变更,canal无法发挥作用,还得是基础的 mysql读数据—>写到缓存中)
2、还有一个地方,在我的平台中,商家可以编辑自己的店铺信息(店铺名称,营业时间,公告等),这是一个对数据一致性要求比较高的场景(假如商家临时更改了营业时间,用户读到旧数据会白跑一趟),我们使用的是Redisson实现的读写锁。在读的时候添加共享锁,可以保证读读不互斥、读写互斥。当我们更新数据的时候,添加排他锁。它是读写、读读都互斥,这样就能保证在写数据的同时,是不会让其他线程读数据的,避免了脏数据。这里面需要注意的是,读方法和写方法上需要使用同一把锁才行。
Q:那这个排他锁是如何保证读写、读读互斥的呢?A:其实排他锁底层使用的也是SETNX,它保证了同时只能有一个线程操作锁住的方法。
Q:你听说过延时双删吗?为什么不用它呢?A:延迟双删,如果是写操作,我们先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据。延时是因为我们的数据库一般是主从模式,读写分离的。我们需要延时一会把主节点的数据同步到从节点。其中,这个延时多久不太好确定。在延时的过程中,可能会出现脏数据,所以没有采用它。
5.Redis数据持久化: 必问
Q: redis作为缓存,数据的持久化是怎么做的?
A: 在Redis中提供了两种数据持久化的方式:1.AOF日志 2.RDB快照(默认)
AOF的实现原理:Redis 在执行命令后,会将对应的命令追加写入到 AOF 文件的缓冲区,然后根据配置的策略定期将缓冲区内容刷写到磁盘上的 AOF 文件中。 当Redis 重启时,会读取AOF文件的命令,通过逐一执行命令的方式来进行数据恢复。
AOF刷盘策略:Redis 提供了 3 种写回硬盘的策略:Always,这个词的意思是「总是」,所以它的意思是每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘;Everysec,这个词的意思是「每秒」,所以它的意思是每次写操作命令执行完后,先将命令写入到 AOF 文件的缓冲区中,然后每隔一秒将缓冲区里的内容写回硬盘;No,意味着将刷盘策略转交给操作系统控制,也就是每次写操作命令执行完后,先将命令写入到 AOF 文件的缓冲区中,再由操作系统决定于何时将缓冲区内容写回硬盘。
Redis 为了避免 AOF 文件越写越大,提供了 AOF 重写机制,当 AOF 文件的大小超过所设定的阈值后,Redis 就会启用 AOF 重写机制,来压缩 AOF 文件。 set name A set name B —> set name B(A被覆盖)
RDB的实现原理:RDB是一个快照文件。它是把redis内存存储的数据写到磁盘上。当redis恢复数据的时候,可以从RDB的快照文件中恢复数据。
RDB 做快照时会阻塞线程吗?
Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave(默认),它们的区别就在于是否在「主线程序」里执行:执行了 save 命令,就会在主线程序生成 RDB 文件,如果写入 RDB 文件的时间太长,会阻塞主线程序;执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这可以避免主线程序的阻塞。
RDB 在执行快照的时候,数据能修改吗?
可以的,执行 bgsave 过程时,数据是能被修改的,关键的技术就在于写时复制技术(Copy-On-Write, COW)。执行 bgsave 命令的时候,会通过 fork() 创建子进程,子进程会复制父进程的页表(记录虚拟地址和物理地址的映射关系),什么是页表呢?我们的Redis一般都是部署在linux操作系统中的,而在linux里,应用程序无法直接操作物理内存地址,但是可以通过操作虚拟地址来间接操作物理地址,而页表就记录着虚拟地址和物理地址的映射关系,子进程复制完父进程的页表后,此时子进程和父进程是共享同一片内存数据的。如果主线程序执行写操作,则被修改的数据会复制一份副本,然后子进程会把该副本数据写入 RDB 文件,在这个过程,主线程仍然可以直接修改原来的数据。
AOF和RDB的优缺点:
RDB因为比较轻量,它在数据恢复的时候是很快的,但是如果redis在两次快照之间宕机了,RDB就会丢失这段时间内的数据。并且 bgsave 命令会消耗很多 cpu 资源;AOF在恢复数据的时候比较慢,但是它在配置 everysec 命令时,如果redis宕机了,最多会丢失一秒的数据,它的数据安全性比较高。
我们在项目中是采用 RDB 和 AOF 混合持久化的。
6.Redis数据过期策略: 高频
在redis中提供了两种数据过期删除策略。第一种是惰性删除。在设置该key过期时间后,我们不去管它。当需要该key时,我们检查其是否过期。如果过期,我们就删掉它;反之,返回该key。第二种是定期删除。就是说,每隔一段时间,我们就对一些key进行检查,并删除里面过期的key。Redis的过期删除策略是:惰性删除 + 定期删除两种策略配合使用。
7.Redis的数据淘汰策略: 高频
Redis 数据淘汰策略共有八种,这八种策略大体分为「不进行数据淘汰」和「进行数据淘汰」两类策略。
1、,不进行数据淘汰的策略:• noeviction(Redis3.0之后,默认的内存淘汰策略):如果说我们 redis 的占用内存超过最大设置内存时,不淘汰任何数据,这时如果有新的数据写入,会报错,不淘汰任何数据。2、进行数据淘汰的策略:针对「进行数据淘汰」这一类策略,又可以细分为「在设置了过期时间的数据中进行淘汰」和「在所有数据范围内进行淘汰」这两类策略。• 在设置了过期时间的数据中进行淘汰:• volatile-random:随机淘汰设置了过期时间的任意键值;• volatile-ttl:优先淘汰较早过期的键值。• volatile-lru(Redis3.0 之前,默认的内存淘汰策略):淘汰所有设置了过期时间的键值中,最久未使用的键值;• volatile-lfu(Redis 4.0 之后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最少使用的键值;• 在所有数据范围内进行淘汰:• allkeys-random:随机淘汰任意键值;• allkeys-lru:淘汰整个键值中最久未使用的键值;• allkeys-lfu(Redis 4.0 之后新增的内存淘汰策略):淘汰整个键值中最少使用的键值。
数据库有1000万数据,Redis只能缓存20w数据。如何保证Redis中的数据都是热点数据?**
候选人:嗯,我想一下()。可以使用allkeys-lru(挑选最近最少使用的数据淘汰)淘汰策略。那留下来的都是经常访问的热点数据。
Redis的内存用完了会发生什么?
候选人:嗯~,这个要看redis的数据淘汰策略是什么。如果是默认的配置,redis内存用完以后则直接报错。我们当时设置的是allkeys-lru策略,把最近最常访问的数据留在缓存中。
8.Redis分布式锁怎么实现? 高频
在redis中提供了一个命令SETNX(SET if not exists)。由于redis是单线程的,用了这个命令之后,只能有一个客户端请求能对某一个key设置值。在没有过期或删除key的时候,其他客户端是不能设置这个key的。
那你如何控制Redis实现分布式锁的有效时长呢?
候选人:嗯,的确。redis的SETNX指令不好控制这个问题。我们当时采用的是redis的一个框架Redisson实现的。在Redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间。当锁住的一个业务还没有执行完成的时候,Redisson会引入一个看门狗机制。就是说,每隔一段时间就检查当前业务是否还持有锁。如果持有,就增加加锁的持有时间。当业务执行完成之后,需要使用释放锁就可以了。还有一个好处就是,在高并发下,一个业务有可能会执行很快。客户1持有锁的时候,客户2来了以后并不会马上被拒绝。它会自旋不断尝试获取锁。如果客户1释放之后,客户2就可以马上持有锁,性能也得到了提升。
Redisson实现的分布式锁是可重入的吗?
候选人:嗯,是可以重入的。这样做是为了避免死锁的产生。这个重入其实在内部就是判断是否是当前线程持有的锁,如果是当前线程持有的锁就会计数,如果释放锁就会在计数上减一。在存储数据的时候采用的hash结构,大key可以按照自己的业务进行定制,其中小key是当前线程的唯一标识,value是当前线程重入的次数(计数器)。
Redisson实现的分布式锁能解决锁的主从一致性的问题吗?
候选人:这个是不能的。我们Redis一般都是部署的主从+哨兵模式,主节点用来写数据,从节点用来读数据。如果线程1通过setnx获取到锁之后,主节点的数据还没有同步到从节点就宕机了,哨兵会将一个从节点提升为新的主节点,而老的主节点恢复之后会被降为从节点并做主节点数据的全量同步。此时来了一个线程2,通过新的主节点也可以获取到锁,这样就导致两个线程产生“共享同一把锁”的情况(线程1仍然认为自己持有锁),违背了锁的互斥性。
我们可以利用Redisson提供的红锁来解决这个问题,它的主要作用是,不能只在一个Redis实例上创建锁,而是要求在大多数Redis节点上都成功创建锁,红锁中要求是成功创建锁的Redis节点数量要过半。这样就能避免线程1加锁成功后主节点宕机导致线程2成功加锁到新的主节点上的问题了。
但是,如果使用了红锁,性能会变差。我们使用Redis本身就是追求高可用的,我们可以想办法做到最终一致性,Redis也不会经常性的宕机,如果有强一致性要求高的业务,建议使用ZooKeeper实现的分布式锁,它是可以保证强一致性的。
9.Redis集群有哪些知道吗? 中频
在Redis中提供的集群方案总共有三种:主从复制、哨兵模式、Redis分片集群。
那你介绍一下主从同步:
单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,可以搭建主从集群,实现读写分离。一般都是一主多从,主节点负责写数据,从节点负责读数据,主节点写入数据之后,需要把数据同步到从节点中。
主从同步分为了两个阶段,一个是全量同步,一个是增量同步。
全量同步是指从节点第一次与主节点建立连接的时候使用全量同步,流程是这样的:
第一:从节点请求主节点同步数据,其中从节点会携带自己的replication id和offset偏移量。
第二:主节点判断是否是第一次同步,主要判断的依据就是,主节点与从节点是否是同一个replication id,如果不是,就说明是第一次同步,那主节点就会把自己的replication id和offset发送给从节点,让从节点与主节点的信息保持一致。
第三:在同时主节点会执行BGSAVE,生成RDB文件后,发送给从节点去执行,从节点先把自己的数据清空,然后执行主节点发送过来的RDB文件,这样就保持了一致。
当然,如果在RDB生成期间,依然有请求到了主节点,而主节点会以命令的方式记录到缓冲区日志文件中,最后把这个日志文件发送给从节点,这样就能保证主节点与从节点完全一致了,后期再同步数据的时候,都是依赖于这个日志文件,这个就是全量同步。
增量同步指的是,当从节点服务重启之后,数据就不一致了,所以这个时候,从节点会请求主节点同步数据,主节点还是判断不是第一次请求,不是第一次就获取从节点的offset值,然后主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步。
如何保证Redis的高并发高可用?
首先就是可以搭建主从集群,再加上使用Redis中的哨兵模式。
我来介绍一下哨兵模式:
什么是哨兵模式,有什么作用?在 Redis 中,由于主从模式是读写分离的,如果主节点挂了,那么将没有主节点来服务客户端的写操作请求,也没有主节点给从节点同步数据。Redis 提供了哨兵机制,哨兵节点主要负责三件事情:监控、选主、通知。监测主节点是否存活,如果发现主节点挂了,它就会选择一个从节点升级为主节点,并把新主节点的相关信息通知给所有从节点和客户端。
如何判断主节点真的故障了?哨兵会每隔1秒给所有节点发送PING命令,当主从节点收到PING命令后,会发送一个响应命令PONG给哨兵。如果主节点或者从节点没有在规定的时间内响应哨兵的PING命令,哨兵就会将它们标记为【主观下线】。为了降低误判率,当一个哨兵判断主节点为「主观下线」后,就会向其他哨兵发起命令,其他哨兵收到这个命令后,会做出赞成投票或者否决投票的响应。如果票数大于默认配置的 N/2 + 1(N为哨兵节点的数量),这时主节点就会被标记为【客观下线】。
选主节点的规则:我们首先要把已经下线的从节点过滤掉,再把网络状态不好的从节点给过滤掉,接下来要对所有从节点进行三轮考察:优先级、复制进度、ID号。第一轮考察:哨兵会根据从节点的优先级来进行排序,优先级越小排名越前。(优先级在配置中设置)第二轮考察:如果优先级相同,则查看复制进度,哪个从节点复制的数据多,哪个就靠前。(比较从节点和主节点的offset)第三轮考察:如果优先级和复制进度都相同,就选择从节点ID较小的那个。
Redis集群脑裂,该怎么解决呢?
脑裂的问题是这样的,我们现在用的是Redis的哨兵模式。如果主节点的网络突然发生了问题,它与所有的从节点失联了,使得哨兵不能心跳感知到主节点,所以通过选举的方式提升了一个从节点为新的主节点,这样就存在了两个主节点,就像大脑分裂了一样。但是此时老的主节点和客户端的网络是正常的,这样会导致客户端还在出问题的老主节点那里写入数据,新节点无法同步数据,当网络恢复后,哨兵会将老主节点降为从节点,这时再从新主节点同步数据,因为第一次同步是全量同步,此时的从节点会清空掉自己本地的数据,然后再做全量同步。这会导致老主节点在出问题时期中的数据丢失。
关于解决的话,在Redis的配置中可以设置:第一可以设置最少的从节点个数,比如设置至少要有一个从节点才能接收数据,第二个可以设置主从数据同步的延迟时间不能超过5秒,达不到要求,原主节点就会被限制从客户端写入数据。
Redis分片集群有什么用?
分片集群主要解决的是,海量数据的存储。集群中有多个master,每个master存储不同的数据,还可以给每个master设置多个slave。大大提高了并发能力和存储数据能力。另一方面,多个master之间通过ping检测彼此的健康状态。类似哨兵模式。当客户请求访问时,可以访问集群中的任意节点,请求最终都会转发到正确的节点上。
redis分片集群中数据怎么存储和读取?
redis集群引入hash槽概念,有16384个hash槽,每个master节点绑定一定范围的hash槽。key通过CRC16算法计算得到一个值,再模于16384得到值,决定放置在哪个槽。通过槽找到对应的节点进行存储。取值的逻辑也是一样的。
10.Redis是单线程的,为什么那么快? 中频
1.Redis 的大部分操作都在内存中完成,因此 Redis 瓶颈可能是机器的内存或者网络带宽,而并非 CPU,既然 CPU 不是瓶颈,那么自然就采用单线程的解决方案;
2.Redis 采用单线程模型可以避免多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销。
3.Redis 采用了 I/O 多路复用技术来处理大量客户端 的 Socket 请求,一个线程处理多个I/O流。这个机制让内核可以同时监听多个 Socket,内核会持续监控这些 Socket 上是否有新的连接请求。一旦有请求发生,内核会通知 Redis,之后由 Redis 线程对这些请求进行处理,从而极大提升了性能和效率。