Java常用组件之Redis经典面试题(一)

大家好,今天为大家带来Java项目中,几乎必不可少的组件之一-Redis的一些常见面试题,帮忙近期需要面试的朋友们来一个理论基础突击!

一、数据类型

1.Redis的常用数据类型有哪些 ?

难易程度:☆☆☆

出现频率:☆☆☆☆☆

Redis是典型的“键值型”数据库,不同数据类型其key结构一致,value有所差异。常见的类型有:string、hash、list、set、SortedSet等。

而基于以上5种基本数据类型,Redis又拓展了几种拓展类型,例如:BitMap、

HyperLogLog、Geo等。

String类型是Redis中最常见的数据类型,value与key一样都是Redis自定义的字符串结构,称为SDS。不过在保存数字、小字符串时因为采用INT和 EMBSTR编码,内存结构紧凑,只需要申请一次内存分配,效率更高,更节省内存。

而超过44字节的大字符串时则需要采用RAW编码,申请额外的SDS空间,需要两次内存分配,效率较低,内存占用也较高,但最大不超过512mb,因此建议单个value尽量不要超过44字节。

String类型常用来做计数器、简单数据存储等。复杂数据建议采用其他数据结构。

Hash结构,其value与Java中的HashMap类似,是一个key-value结构。如果有一个对象需要被Redis缓存,而且将来可能有部分修改。建议用hash结构来存储这个对象的每一个字段和字段值。而不是作为一个JSON字符串存储到

String类型中。因为Hash结构的每一个字段都可以单独做修改,而String的 JSON串必须整体覆盖。

 

与Java中的hashMap不同的是,Redis中的Hash底层采用了渐进式rehash的算法,在做rehash时会创建一个新的hashtable,每次操作元素时移动一部分数据,直到所有数据迁移完成,再用新的HashTable来代替旧的,避免了因为 rehash导致的阻塞,因此性能更高。

List结构的value类型可以看作是一个双端链表,提供了一些命令便于我们从首尾操作元素。为了节省内存空间,底层采用了ZipList(压缩列表)来做基础存储。当压缩列表数据达到阈值(512)则会创建新的压缩列表。每个压缩列表作为一个双端链表的一个节点,最终形成一个QuickList结构。而且QuickList结构与一般的双端链表不同,他可以对中间不常用的ZipList节点做压缩以节省内存。

List结构常用来模拟队列,实现任务排队这样的功能。

Set结构的value与Java的Set类似,元素不可重复。Redis提供了求交集、并集等命令,可以帮助我们实现例如:好友列表、共同好友等功能。

当存储元素是整数时,其底层默认采用IntSet结构,可以看作是一个有序的数组,结构紧凑,效率较高。而元素如果不是整数,或者元素量超过512这个阈值时则会转为hash表结构,内存占用会有大的增加。因此我们在使用Set结构时尽量采用数组存储,例如数值类型的id。而且元素数量尽量不要超过512,避免出现BigKey。

SortedSet,也叫ZSet。其value就是一个有序的Set集合,元素唯一,并且会按照一个指定的score值排序。因此常用来做排行榜功能。

SortedSet底层的利用Hash表保证元素的唯一性。利用跳表(SkipList)来保证元素的有序性,因此数据会有重复存储,内存占用较高,是一种典型的以空间换时间的设计。不建议在SortedSet中放入过多数据。

2.跳表你了解吗?

难易程度:☆☆☆☆

出现频率:☆☆

跳表(SkipList)首先是链表,但与传统的链表相比有几点差异:

  1. ·跳表结合了链表和二分查找的思想元素按照升序排列存储
  2. ·节点可能包含多个指针,指针跨度不同
  3. ·查找时从顶层向下,不断缩小搜索范围
  4. ·整个查询的复杂度为 O ( log n )
 

Redis数据类型Sorted Set使用了跳表作为其中一种数据结构

二、持久化

Redis的数据持久化策略有哪些 ?

难易程度:☆☆☆

出现频率:☆☆☆☆

在Redis中提供了两种数据持久化的方式:1、RDB 2、AOF

RDB

定期更新,定期将Redis中的数据生成的快照同步到磁盘等介质上,磁盘上保存的就是Redis的内存快照

优点:数据文件的大小相比于aop较小,使用rdb进行数据恢复速度较快缺点:比较耗时,存在丢失数据的风险

AOF

将Redis所执行过的所有指令都记录下来,在下次Redis重启时,只需要执行指令就可以了

 

优点:数据丢失的风险大大降低了

缺点:数据文件的大小相比于rdb较大,使用aop文件进行数据恢复的时候速度较慢

你们的项目中的持久化是如何配置选择的?

RDB+AOF

三、主从和集群

3.1 Redis集群有哪些方案, 知道吗 ?

难易程度:☆☆☆

出现频率:☆☆☆

在Redis中提供的集群方案总共有三种:

1、主从复制

保证高可用性

实现故障转移需要手动

实现无法实现海量数据存储

2、哨兵模式

 保证高可用性

 可以实现自动化的故障转移

 无法实现海量数据存储

3、Redis分片集群

保证高可用性

可以实现自动化的故障转移

可以实现海量数据存储

3.2什么是 Redis 主从同步?

难易程度:☆☆☆☆

出现频率:☆☆☆☆

主从第一次同步是全量同步

第一阶段,全量同步流程

  1. 从节点执行replicaof命令,发送自己的replid和offset给主节点
  2. 主节点判断从节点的replid与自己的是否一致,
  3. 如果不一致说明是第一次来,需要做全量同步,主节点返回自己的replid给从节点
  4. 主节点开始执行bgsave,生成rdb文件
  5. 主节点发送rdb文件给从节点,再发送的过程中
  6. 从节点接收rdb文件,清空本地数据,加载rdb文件中的数据
  7. 同步过程中,主节点接收到的新命令写入从节点的写缓冲区(repl_buffer)
  8. 从节点接收到缓冲区数据后写入本地,并记录最新数据对应的offset
  9. 后期采用增量同步

后期数据变化后,则执行增量同步

 
  1. 主节点会不断把自己接收到的命令记录在repl_baklog中,并修改offset
  2. 从节点向主节点发送psync命令,发送自己的offset和replid
  3. 主节点判断replid和offset与从节点是否一致
  4. 如果replid一致,说明是增量同步。然后判断offset是否一致
  5. 如果从节点offset小于主节点offset,并且在repl_baklog中能找到对应数据则将offset之间相差的数据发送给从节点
  6. 从节点接收到数据后写入本地,修改自己的offset与主节点一致

增量同步的风险

repl_baklog大小有上限,写满后会覆盖最早的数据。如果slave断开时间过 久,导致尚未备份的数据被覆盖,则无法基于log做增量同步,只能再次全量同步。

repl_baklog可以在配置文件中进行修改存储大小

3.3你们使用Redis是单点还是集群 ? 哪种集群 ?(说说你们生产环境redis部署情况?)

难易程度:☆☆☆

出现频率:☆☆☆

一般部分服务做缓存用的Redis直接做主从(1主1从)加哨兵就可以了。单节点不超过10G内存,如果Redis内存不足则可以给不同服务分配独立的Redis主从节点。尽量不做分片集群。

原因:

维护起来比较麻烦

集群之间的心跳检测和数据通信会消耗大量的网络带宽

集群插槽分配不均和key的分批容易导致数据倾斜

客户端的route会有性能损耗

集群模式下无法使用lua脚本、事务

 

3.4Redis分片集群中数据是怎么存储和读取的 ?

难易程度:☆☆☆

出现频率:☆☆☆

Redis 集群引入了哈希槽的概念,Redis 集群有 16384 个哈希槽,每个 key通过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。


上图是存值的流程,取值的流程类似

set {aaa}name zhangsan 计算hash是根据aaa计算的

3.5redis集群脑裂?

难易程度:☆☆☆☆

出现频率:☆☆☆

关于reids集群会由于网络等原因出现脑裂的情况,所谓的集群脑裂就是,由于 redis master节点和redis salve节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到master,所以通过选举的方式提升了一个salve为master,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在old

master那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将old master降为salve,这时再从新master同步数据,这会导致大量数据丢失。

正常情况:

脑裂情况:

 

当哨兵与主节点由于网络抖动原因断开了连接,哨兵监控到之后,则会从剩余的从节点中选出一个作为主节点

redis的客户端这个时候并没有是可以正常连接之前的maser(主节点),并且可以正常写入数据

假如现在网络恢复了,哨兵发现主从中有两个主节点,则会强制一个主节点变为从节点,看下图

由于原来的主节点变成了从节点,则需要执行主从同步流程,清理数据(之前的主节点),同步新主节点中的数据,在之前脑裂过程中,客户端写入的数据丢失

解决方案:

redis中有两个配置参数:

 min-replicas-to-write 1 表示最少的salve节点为1个

 min-replicas-max-lag 5 表示数据复制和同步的延迟不能超过5秒

配置了这两个参数:如果发生脑裂:原master会在客户端写入操作的时候拒绝请求。这样可以避免大量数据丢失。

3.6怎么保证redis的高并发高可用

难易程度:☆☆☆

出现频率:☆☆☆

主从+哨兵

集群

四、使用场景

4.1项目中哪块使用了缓存?

难易程度:☆☆☆

出现频率:☆☆☆☆☆

结合自己简历上写的项目模块说明这个问题,要陈述出当时的场景

 数据字典

 用户Token

 热点数据

4.2什么是缓存穿透 ? 怎么解决 ?

难易程度:☆☆☆☆

出现频率:☆☆☆☆☆

加入缓存以后的数据查询流程:

 

缓存穿透:

概述:指查询一个一定不存在的数据,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。

解决方案:

1、查询返回的数据为空,仍把这个空结果进行缓存,但过期时间会比较短

2、布隆过滤器:将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对DB的查询

4.3什么是缓存击穿 ? 怎么解决 ?

难易程度:☆☆☆☆

出现频率:☆☆☆☆☆

概述:对于设置了过期时间的key,缓存在某个时间点过期的时候,恰好这时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。

解决方案:

1、使用互斥锁:当缓存失效时,不立即去load db,先使用如 Redis 的 setnx 去设置一个互斥锁,当操作成功返回时再进行 load db的操作并回设缓存,否则重试get缓存的方法

2、可以设置当前key逻辑过期,大概思路如下:

①:在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前key

设置过期时间

②:当查询的时候,从redis取出数据后判断时间是否过期

③:如果过期则开通另外一个线程进行数据同步,当前线程正常返回数据,这个数据不是最新

 

两种方案对比:

解决方案

优点

缺点

互斥锁

没有额外的内存消耗保证一致性

实现简单

线程需要等待,性能受影响可能有死锁风险

逻辑过期

线程无需等待,性能较好

不保证一致性 有额外内存消耗实现复杂

4.4什么是缓存雪崩 ? 怎么解决 ?

难易程度:☆☆☆☆

出现频率:☆☆☆☆☆

概述:设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB 瞬时压力过重雪崩。与缓存击穿的区别:雪崩是很多key,击穿是某一个key缓存。

解决方案:

 

将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。

4.5什么是布隆过滤器?

难易程度:☆☆☆☆

出现频率:☆☆☆☆☆

概述:布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上由一个很长的二进制向量(二进制数组)和一系列随机映射函数(hash函数)。

作用:布隆过滤器可以用于检索一个元素是否在一个集合中。添加元素:将商品的id(id1)存储到布隆过滤器

假设当前的布隆过滤器中提供了三个hash函数,此时就使用三个hash函数对id1进行哈希运算,运算结果分别为:1、4、9那么就会数组中对应的位置数据更改为1

判断数据是否存在:使用相同的hash函数对数据进行哈希运算,得到哈希值。然后判断该哈希值所对应的数组位置是否都为1,如果不都是则说明该数据肯定不存在。如果是说明该数据可能存在,因为哈希运算可能就会存在重复的情况。如下图所示:

 

假设添加完id1和id2数据以后,布隆过滤器中数据的存储方式如上图所示,那么此时要判断id3对应的数据在布隆过滤器中是否存在,按照上述的判断规则应该是存在,但是id3这个数据在布隆过滤器中压根就不存在,这种情况就属于误 判。

误判率:数组越小误判率就越大,数组越大误判率就越小,但是同时带来了更多的内存消耗。

删除元素:布隆过滤器不支持数据的删除操作,因为如果支持删除那么此时就会影响判断不存在的结果。

使用布隆过滤器:在redis的框架redisson中提供了布隆过滤器的实现,使用方式如下所示:

pom.xml文件


<dependency>
   <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.6</version>
</dependency>

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

 

测试代码:

import org.redisson.Redisson;
import org.redisson.api.RBloomFilter; import org.redisson.api.RedissonClient; import org.redisson.config.Config;

public class Application {

public static void main(String[] args) {
//链接redis,
Config config = new Config();

config.useSingleServer().setAddress("redis://192.168.200.130:6379")
.setPassword("leadnews");
//创建redisson客户端
RedissonClient redissonClient = Redisson.create(config);
//创建布隆过滤器 RBloomFilter<String> bloomFilter =
redissonClient.getBloomFilter("bloom-filter"); int size = 10000;
//初始化数据
//	initData(bloomFilter, size);
//测试误判率
int count = getData(bloomFilter, size); System.out.println("总的误判条数为:" + count);

}


/**
*测试误判率
*@param bloomFilter
*@param size
*@return
*/
private static int getData(RBloomFilter<String> bloomFilter, int size) {
int count = 0 ;	// 记录误判的数据条数
for(int x = size; x < size * 2 ; x++) { if(bloomFilter.contains("add" + x)) {
count++ ;
}
}
return count;
}

/**
*初始化数据
*@param bloomFilter
*@param size
*/
private static void initData(RBloomFilter<String> bloomFilter, int size) {
//第一个参数:布隆过滤器存储的元素个数
//第一个参数:误判率
bloomFilter.tryInit(size,0.01);
//在布隆过滤器初始化数据
for(int x = 0; x < size; x++) { bloomFilter.add("add" + x) ;
}
System.out.println("初始化完成...");
}
}

Redis中使用布隆过滤器防止缓存穿透流程图如下所示:

4.6redis双写问题?

难易程度:☆☆☆

出现频率:☆☆☆☆☆

同步方案:

普通缓存,一般采用更新时删除缓存,查询时建立缓存的延迟更新方案。异步方案:

1、使用消息队列进行缓存同步:更改代码加入异步操作缓存的逻辑代码(数据库操作完毕以后,将要同步的数据发送到MQ中,MQ的消费者从MQ中获取数据,然后更新缓存)

2、使用阿里巴巴旗下的canal组件实现数据同步:不需要更改业务代码,部署一个canal服务。canal服务把自己伪装成mysql的一个从节点,当mysql数据更新以后,canal会读取binlog数据,然后再通过canal的客户端获取到数据,更新缓存即可。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值