大家好,今天为大家带来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)首先是链表,但与传统的链表相比有几点差异:
- ·跳表结合了链表和二分查找的思想元素按照升序排列存储
- ·节点可能包含多个指针,指针跨度不同
- ·查找时从顶层向下,不断缩小搜索范围
- ·整个查询的复杂度为 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 主从同步?
难易程度:☆☆☆☆
出现频率:☆☆☆☆
主从第一次同步是全量同步
第一阶段,全量同步流程
- 从节点执行replicaof命令,发送自己的replid和offset给主节点
- 主节点判断从节点的replid与自己的是否一致,
- 如果不一致说明是第一次来,需要做全量同步,主节点返回自己的replid给从节点
- 主节点开始执行bgsave,生成rdb文件
- 主节点发送rdb文件给从节点,再发送的过程中
- 从节点接收rdb文件,清空本地数据,加载rdb文件中的数据
- 同步过程中,主节点接收到的新命令写入从节点的写缓冲区(repl_buffer)
- 从节点接收到缓冲区数据后写入本地,并记录最新数据对应的offset
- 后期采用增量同步
后期数据变化后,则执行增量同步
- 主节点会不断把自己接收到的命令记录在repl_baklog中,并修改offset
- 从节点向主节点发送psync命令,发送自己的offset和replid
- 主节点判断replid和offset与从节点是否一致
- 如果replid一致,说明是增量同步。然后判断offset是否一致
- 如果从节点offset小于主节点offset,并且在repl_baklog中能找到对应数据则将offset之间相差的数据发送给从节点
- 从节点接收到数据后写入本地,修改自己的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的客户端获取到数据,更新缓存即可。