# Redis相关面试题 **Repository Path**: mianshige/Redis-related-interview-questions ## Basic Information - **Project Name**: Redis相关面试题 - **Description**: Redis相关面试题 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-02-21 - **Last Updated**: 2025-02-21 ## Categories & Tags **Categories**: Uncategorized **Tags**: Redis, 中间件 ## README # Redis相关面试题 ### [PDF离线版本下载](https://pan.quark.cn/s/bd0d521ab472) ## 1.数据类型 ### 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 ) ![image-20220825104355442](Redis相关面试题.assets/image-20220825104355442.png) **Redis数据类型Sorted Set使用了跳表作为其中一种数据结构** ## 2.持久化 ### Redis的数据持久化策略有哪些 ? >难易程度:☆☆☆ > >出现频率:☆☆☆☆ 在Redis中提供了两种数据持久化的方式:1、RDB 2、AOF **RDB:** 定期更新,定期将Redis中的数据生成的快照同步到磁盘等介质上,磁盘上保存的就是Redis的内存快照 优点:数据文件的大小相比于aop较小,使用rdb进行数据恢复速度较快 缺点:比较耗时,存在丢失数据的风险 **AOF:** 将Redis所执行过的所有指令都记录下来,在下次Redis重启时,只需要执行指令就可以了 优点:数据丢失的风险大大降低了 缺点:数据文件的大小相比于rdb较大,使用aop文件进行数据恢复的时候速度较慢 >你们的项目中的持久化是如何配置选择的? > >RDB+AOF ## 3.主从和集群 ### 3.1 Redis集群有哪些方案, 知道嘛 ? >难易程度:☆☆☆ > >出现频率:☆☆☆☆ 在Redis中提供的集群方案总共有三种: 1、主从复制 * 保证高可用性 * 实现故障转移需要手动实现 * 无法实现海量数据存储 ![image-20220825235753402](Redis相关面试题.assets/image-20220825235753402.png) 2、哨兵模式 * 保证高可用性 * 可以实现自动化的故障转移 * 无法实现海量数据存储 ![image-20220825235832242](Redis相关面试题.assets/image-20220825235832242.png) 3、Redis分片集群 * 保证高可用性 * 可以实现自动化的故障转移 * 可以实现海量数据存储 ![image-20220825235913828](Redis相关面试题.assets/image-20220825235913828.png) ### 3.2 什么是 Redis 主从同步? >难易程度:☆☆☆☆ > >出现频率:☆☆☆☆ 主从第一次同步是**全量同步** ![image-20220826000009625](Redis相关面试题.assets/image-20220826000009625.png) >第一阶段,全量同步流程 >1.从节点执行replicaof命令,发送自己的replid和offset给主节点 >2.主节点判断从节点的replid与自己的是否一致, >3.如果不一致说明是第一次来,需要做全量同步,主节点返回自己的replid给从节点 >4.主节点开始执行bgsave,生成rdb文件 >5.主节点发送rdb文件给从节点,再发送的过程中 >6.从节点接收rdb文件,清空本地数据,加载rdb文件中的数据 >7.同步过程中,主节点接收到的新命令写入从节点的写缓冲区(repl_buffer) >8.从节点接收到缓冲区数据后写入本地,并记录最新数据对应的offset >9.后期采用增量同步 后期数据变化后,则执行**增量同步** ![image-20220826000106137](Redis相关面试题.assets/image-20220826000106137.png) >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脚本、事务 >其他扩展: > >- 一般的企业中redis存储超过100GB就是极少见的,一般只存热点数据 >- 极端情况下,可以设置较大的内存。以阿里云为主,购买内存型服务器,目前最大的为:2048GB > >https://www.aliyun.com/product/ebm?spm=5176.19720258.J_2686872250.8.328576f4mDkIu9&scm=20140722.M_4603843.P_156.MO_871-ID_4603843-MID_4603843-CID_545-ST_5018-V_1 > > ### 3.4 Redis分片集群中数据是怎么存储和读取的 ? >难易程度:☆☆☆ > >出现频率:☆☆☆ Redis 集群引入了哈希槽的概念,Redis 集群有 16384 个哈希槽,每个 key通过 CRC16 校验后对 16384 取模来决定放置哪个槽,集群的每个节点负责一部分 hash 槽。 ![](Redis相关面试题.assets/Redis集群Hash槽.jpg) 上图是存值的流程,取值的流程类似 >set {aaa}name zhangsan 计算hash是根据aaa计算的 ### 3.5 redis集群脑裂? >难易程度:☆☆☆☆ > >出现频率:☆☆☆ 关于reids集群会由于网络等原因出现脑裂的情况,所谓的集群脑裂就是,由于redis master节点和redis salve节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到master,所以通过选举的方式提升了一个salve为master,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在old master那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将old master降为salve,这时再从新master同步数据,这会导致大量数据丢失。 正常情况: ![image-20220826005227824](Redis相关面试题.assets/image-20220826005227824.png) 脑裂情况: ![image-20220826005755464](Redis相关面试题.assets/image-20220826005755464.png) >当哨兵与主节点由于网络抖动原因断开了链接,哨兵监控到之后,则会从剩余的从节点中选出一个作为主节点 > >redis的客户端这个时候并没有是可以正常链接之前的maser(主节点),并且可以正常写入数据 假如现在网络恢复了,哨兵发现主从中有两个主节点,则会强制一个主节点变为从节点,看下图 ![image-20220826010517578](Redis相关面试题.assets/image-20220826010517578.png) >由于原来的主节点变成了从节点,则需要执行主从同步流程,清理数据(之前的主节点),同步新主节点中的数据,**在之前脑裂过程中,客户端写入的数据丢失** **解决方案:** redis中有两个配置参数: - min-replicas-to-write 1 表示最少的salve节点为1个 - min-replicas-max-lag 5 表示数据复制和同步的延迟不能超过5秒 配置了这两个参数:如果发生脑裂:原master会在客户端写入操作的时候拒绝请求。这样可以避免大量数据丢失。 ### 3.6 怎么保证redis的高并发高可用 >难易程度:☆☆☆ > >出现频率:☆☆☆ - 主从+哨兵 - 集群 ## 4.使用场景 ### 4.1 项目中哪块使用了缓存? >难易程度:☆☆☆ > >出现频率:☆☆☆☆☆ 结合自己简历上写的项目模块说明这个问题,要陈述出当时的场景 黑马头条: - 用户行为数据 - 热点文章 其他: - 热点数据 ### 4.2 什么是缓存穿透 ? 怎么解决 ? >难易程度:☆☆☆☆ > >出现频率:☆☆☆☆☆ 加入缓存以后的数据查询流程: ![image-20220505084631909](Redis相关面试题.assets/image-20220505084631909.png) **缓存穿透**: 概述:指查询一个一定**不存在**的数据,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。 >get请求:api/v1/news/13 解决方案: 1、查询返回的数据为空,仍把这个空结果进行缓存,但过期时间会比较短 2、布隆过滤器:将所有可能存在的数据哈希到一个足够大的 bitmap 中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对DB的查询 ### 4.3 什么是缓存击穿 ? 怎么解决 ? >难易程度:☆☆☆☆ > >出现频率:☆☆☆☆☆ 概述:对于设置了过期时间的key,缓存在某个时间点过期的时候,恰好这时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。 解决方案: 1、使用互斥锁:当缓存失效时,不立即去load db,先使用如 Redis 的 setnx 去设置一个互斥锁,当操作成功返回时再进行 load db的操作并回设缓存,否则重试get缓存的方法 2、可以设置当前key逻辑过期,大概是思路如下: ①:在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前key设置过期时间 ②:当查询的时候,从redis取出数据后判断时间是否过期 ③:如果过期则开通另外一个线程进行数据同步,当前线程正常返回数据,这个数据不是最新 ![image-20220911001815407](Redis相关面试题.assets/image-20220911001815407.png) 两种方案对比: | **解决方案** | **优点** | **缺点** | | ------------ | ------------------------------------------------ | ---------------------------------------------- | | **互斥锁** | 没有额外的内存消耗
保证一致性
实现简单 | 线程需要等待,性能受影响
可能有死锁风险 | | **逻辑过期** | 线程无需等待,性能较好 | 不保证一致性
有额外内存消耗
实现复杂 | ### 4.4 什么是缓存雪崩 ? 怎么解决 ? >难易程度:☆☆☆☆ > >出现频率:☆☆☆☆☆ 概述:设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB 瞬时压力过重雪崩。与缓存击穿的区别:雪崩是很多key,击穿是某一个key缓存。 解决方案: 将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。 ### 4.5 什么是布隆过滤器? >难易程度:☆☆☆☆ > >出现频率:☆☆☆☆☆ **概述**:布隆过滤器(Bloom Filter)是1970年由布隆提出的。它实际上由一个很长的二进制向量(二进制数组)和一系列随机映射函数(hash函数)。 ![image-20220505124612288](Redis相关面试题.assets/image-20220505124612288.png) **作用**:布隆过滤器可以用于检索一个元素是否在一个集合中。 **添加元素**:将商品的id(id1)存储到布隆过滤器 ![image-20220505125247251](Redis相关面试题.assets/image-20220505125247251.png) 假设当前的布隆过滤器中提供了三个hash函数,此时就使用三个hash函数对id1进行哈希运算,运算结果分别为:1、4、9那么就会数组中对应的位置数据**更改为1**。 **判断数据是否存在**:使用相同的hash函数对数据进行哈希运算,得到哈希值。然后判断该哈希值所对应的数组位置是否都为1,如果不都是则说明该数据**肯定不存在**。如果是说明该数据**可能**存在,因为哈希运算可能就会存在重复的情况。如下图所示: ![image-20220505130720532](Redis相关面试题.assets/image-20220505130720532.png) 假设添加完id1和id2数据以后,布隆过滤器中数据的存储方式如上图所示,那么此时要判断id3对应的数据在布隆过滤器中是否存在,按照上述的判断规则应该是存在,但是id3这个数据在布隆过滤器中压根就不存在,这种情况就属于误判。 **误判率**:数组越小误判率就越大,数组越大误判率就越小,但是同时带来了更多的内存消耗。 **删除元素**:布隆布隆器不支持数据的删除操作,因为如果支持删除那么此时就会影响判断不存在的结果。 使用布隆过滤器:在redis的框架redisson中提供了布隆过滤器的实现,使用方式如下所示: pom.xml文件 ```xml org.redisson redisson 3.13.6 org.springframework.boot spring-boot-starter-data-redis ``` 测试代码: ```java 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 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 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 bloomFilter, int size) { //第一个参数:布隆过滤器存储的元素个数 //第一个参数:误判率 bloomFilter.tryInit(size,0.01); //在布隆过滤器初始化数据 for(int x = 0; x < size; x++) { bloomFilter.add("add" + x) ; } System.out.println("初始化完成..."); } } ``` Redis中使用布隆过滤器防止缓存穿透流程图如下所示: ![image-20220505134107303](Redis相关面试题.assets/image-20220505134107303.png) ### 4.6 redis双写问题? >难易程度:☆☆☆ > >出现频率:☆☆☆☆☆ 同步方案: 普通缓存,一般采用更新时删除缓存,查询时建立缓存的延迟更新方案。 异步方案: 1、使用消息队列进行缓存同步:更改代码加入异步操作缓存的逻辑代码(数据库操作完毕以后,将要同步的数据发送到MQ中,MQ的消费者从MQ中获取数据,然后更新缓存) 2、使用阿里巴巴旗下的canal组件实现数据同步:不需要更改业务代码,部署一个canal服务。canal服务把自己伪装成mysql的一个从节点,当mysql数据更新以后,canal会读取binlog数据,然后在通过canal的客户端获取到数据,更新缓存即可。 ## 5 Redis分布式锁 ### 5.1 Redis分布式锁如何实现 ? >难易程度:☆☆☆ > >出现频率:☆☆☆☆☆ Redis实现分布式锁主要利用Redis的**setnx**命令。setnx是SET if not exists(如果不存在,则 SET)的简写。 ![image-20220825225528939](Redis相关面试题.assets/image-20220825225528939.png) 上面这几个命令就是最基本的用来完成分布式锁的命令。 **加锁**:使用`setnx key value`命令,如果key不存在,设置value(加锁成功)。如果已经存在lock(也就是有客户端持有锁了),则设置失败(加锁失败)。 **解锁**:使用`del`命令,通过删除键值释放锁。释放锁之后,其他客户端可以通过`setnx`命令进行加锁。 ```java public boolean tryLock(long timeoutSec) { // 获取线程标示 String threadId = ID_PREFIX + Thread.currentThread().getId(); // 获取锁 Boolean success = stringRedisTemplate.opsForValue() .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS); return Boolean.TRUE.equals(success); } ``` ### 5.2 Redis实现分布式锁如何合理的控制锁的有效时长? >难易程度:☆☆☆☆ > >出现频率:☆☆☆☆☆ 有效时间设置多长,假如我的业务操作比有效时间长?我的业务代码还没执行完就自动给我解锁了,不就完蛋了吗。 解决方案: 1、第一种:程序员自己去把握,预估一下业务代码需要执行的时间,然后设置有效期时间比执行时间长一些,保证不会因为自动解锁影响到客户端业务代码的执行。 2、第二种:给锁续期。 锁续期实现思路:当加锁成功后,同时开启守护线程,默认有效期是用户所设置的,然后每隔10秒就会给锁续期到用户所设置的有效期,只要持有锁的客户端没有宕机,就能保证一直持有锁,直到业务代码执行完毕由客户端自己解锁,如果宕机了自然就在有效期失效后自动解锁。 上述的第二种解决方案可以使用redis官方所提供的Redisson进行实现。 使用步骤如下: 1、加入依赖 ```xml org.redisson redisson 3.13.6 ``` 2、定义配置类 ```java @Configuration public class RedisConfig { @Bean public RedissonClient redissonClient(){ Config config = new Config(); config.useSingleServer().setAddress("redis://192.168.200.130:6379").setPassword("leadnews"); return Redisson.create(config); } } ``` 3、业务代码加入分布式锁 ```java public void redisLock() throws InterruptedException { RLock lock = redissonClient.getLock("anyLock"); try { //第一个参数30s:表示尝试获取分布式锁,并且最大的等待获取锁的时间为30s //第二个参数10s:表示上锁之后,10s内操作完毕将自动释放锁 boolean isLock = lock.tryLock(30, 30, TimeUnit.SECONDS); String num = redisTemplate.opsForValue().get("num"); Integer intNum = Integer.parseInt(num); if (intNum == null || intNum <= 0) { throw new RuntimeException("商品已抢完"); } if(isLock){ intNum = intNum - 1; redisTemplate.opsForValue().set("num", intNum.toString()); System.out.println(redisTemplate.opsForValue().get("num")); } } finally { //释放锁 lock.unlock(); } } ``` 4、Jmeter测试 ![image-20220825230303894](Redis相关面试题.assets/image-20220825230303894.png) 5.Redisson分布式锁原理(重要) ![image-20220825230509879](Redis相关面试题.assets/image-20220825230509879.png) >注意:Redisson的watchDog不是每10秒做一次续期,而是每隔(releaseTime / 3)的时间做一次续期。也就是锁自动释放时间的1/3,默认的锁释放时间是30秒,因此默认每隔10秒续期。 redisson实现的分布式锁是可重入的 ```java public void add1(){ boolean isLock = lock.tryLock(30, 10, TimeUnit.SECONDS); //执行业务 add2(); //释放锁 lock.unlock(); } public void add2(){ boolean isLock = lock.tryLock(30, 10, TimeUnit.SECONDS); //执行业务 //释放锁 lock.unlock(); } ``` ### 5.3 你的项目中哪里用到了分布式锁 ? >难易程度:☆☆☆ > >出现频率:☆☆☆☆☆ 黑马头条: - 集群情况下定时任务(分布式锁) 立可得项目: - 下单(分布式锁) 其他情况: - 缓存 - 秒杀 - 幂等性场景 ## 6.其他 ### 6.1 Redis的数据过期策略有哪些 ? >难易程度:☆☆☆ > >出现频率:☆☆☆☆ 数据删除策略:Redis中可以对数据设置数据的有效时间,数据的有效时间到了以后,就需要将数据从内存中删除掉。而删除的时候就需要按照指定的规则进行删除,这种删除规则就被称之为数据的删除策略。 Redis中数据的删除策略: ① 惰性删除 概述:设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。 * 优点:对CPU友好,我们只会在使用该键时才会进行过期检查,对于很多用不到的key不用浪费时间进行过期检查。 * 缺点:对内存不友好,如果一个键已经过期,但是一直没有使用,那么该键就会一直存在内存中,如果数据库中有很多这种使用不到的过期键,这些键便永远不会被删除,内存永远不会释放。 >set name zhangsan 10 > >get name 发现name过期了,直接删除key ② 定期删除 概述:每隔一段时间,我们就对一些key进行检查,删除里面过期的key(从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键)。 * 优点:可以通过限制删除操作执行的时长和频率来减少删除操作对 CPU 的影响。另外定期删除,也能有效释放过期键占用的内存。 * 缺点:难以确定删除操作执行的时长和频率。 如果执行的太频繁,定期删除策略变得和定时删除策略一样,对CPU不友好。如果执行的太少,那又和惰性删除一样了,过期键占用的内存不会及时得到释放。 另外最重要的是,在获取某个键时,如果某个键的过期时间已经到了,但是还没执行定期删除,那么就会返回这个键的值,这是业务不能忍受的错误。 定期清理的两种模式: - SLOW模式是定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf 的 **hz** 选项来调整这个次数 - FAST模式执行频率不固定,每次事件循环会尝试执行,但两次间隔不低于2ms,每次耗时不超过1ms Redis的过期删除策略:**惰性删除 + 定期删除**两种策略进行配合使用定期删除函数的运行频率 ### 6.2 Redis的数据淘汰策略有哪些 ? >难易程度:☆☆☆ > >出现频率:☆☆☆☆ 数据的淘汰策略:当Redis中的内存不够用时,此时在向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据删除掉,这种数据的删除规则被称之为内存的淘汰策略。 常见的数据淘汰策略: ```shell noeviction # 不删除任何数据,内存不足直接报错(默认策略) volatile-lru # 挑选最近最久使用的数据淘汰(举例:key1是在3s之前访问的, key2是在9s之前访问的,删除的就是key2) volatile-lfu # 挑选最近最少使用数据淘汰 (举例:key1最近5s访问了4次, key2最近5s访问了9次, 删除的就是key1) volatile-ttl # 挑选将要过期的数据淘汰 volatile-random # 任意选择数据淘汰 allkeys-lru # 挑选最近最少使用的数据淘汰 allkeys-lfu # 挑选最近使用次数最少的数据淘汰 allkeys-random # 任意选择数据淘汰,相当于随机 ``` >- **LRU**(**L**east **R**ecently **U**sed)最少最近使用。用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。 >- **LFU**(**L**east **F**requently **U**sed)最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。 >- **volatile**:表示设置了带过期时间的key >- **allkeys**:表示所有的key 缓存淘汰策略常见配置项 ```shell maxmemory-policy noeviction # 配置淘汰策略 maxmemory ?mb # 最大可使用内存,即占用物理内存的比例,默认值为0,表示不限制。生产环境中根据需求设定,通常设置在50%以上。 maxmemory-samples count # 设置redis需要检查key的个数 ``` #### 使用建议: 1. 优先使用 allkeys-lru 策略。充分利用 LRU 算法的优势,把最近最常访问的数据留在缓存中。如果业务有明显的冷热数据区分,建议使用。 2. 如果业务中数据访问频率差别不大,没有明显冷热数据区分,建议使用 allkeys-random,随机选择淘汰。 3. 如果业务中有置顶的需求,可以使用 volatile-lru 策略,同时置顶数据不设置过期时间,这些数据就一直不被删除,会淘汰其他设置过期时间的数据。 4. 如果业务中有短时高频访问的数据,可以使用 allkeys-lfu 或 volatile-lfu 策略。 ### 6.3 数据库有1000万数据 ,Redis只能缓存20w数据, 如何保证Redis中的数据都是热点数据 ? >难易程度:☆☆☆ > >出现频率:☆☆☆ 使用 allkeys-lru (挑选最近最少使用的数据淘汰)淘汰策略,那留下来的都是经常访问的热点数据 ### 6.4 Redis的内存用完了会发生什么? >难易程度:☆☆☆ > >出现频率:☆☆☆ 主要看数据淘汰策略是什么?如果是默认的配置,则直接报错 ### 6.5 你们用过Redis的事务吗 ? 事务的命令有哪些 ? >难易程度:☆☆☆☆ > >出现频率:☆☆ Redis的事务与传统事务不同。传统事务一般ACID事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。 Redis中的事务:Redis事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。 总结说:Redis事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。Reids中,单条命令式原子性执行的,但事务不保证原子性,且没有回滚。 事务相关的命令: 1、MULTI:用来组装一个事务 2、EXEC:执行一个事物 3、DISCARD:取消一个事务 4、WATCH:用来监视一些key,一旦这些key在事务执行之前被改变,则取消事务的执行 5、UNWATCH:取消 WATCH 命令对所有key的监视 如下所示: ![image-20220225132328333](Redis相关面试题.assets/image-20220225132328333.png) ### 6.6 Redis是单线程的,但是为什么还那么快? >难易程度:☆☆☆ > >出现频率:☆☆☆☆ Redis总体快的原因: 1、完全基于内存的,C语言编写 2、采用单线程,避免不必要的上下文切换可竞争条件 3、数据简单,数据操作也相对简单 4、使用多路I/O复用模型,非阻塞IO ​ bgsave 在**后台**执行rdb的保存,不影响主线程的正常使用,不会产生阻塞 ​ bgrewriteaof 在**后台**执行aof文件的保存,不影响主线程的正常使用,不会产生阻塞 ## 7 面试现场 ### 7.1 数据类型 >**面试官**:Redis的常用数据类型有哪些 ? > >**候选人**: > >嗯,这个有很多,不过我们开发比较常见的有5种,分别是: > >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中放入过多数据。 > >**面试官**:跳表你了解吗? > >**候选人**: > >嗯,这个知道一些~~ > >Redis数据类型Sorted Set使用了跳表作为其中一种数据结构,跳表结合了链表和二分查找的思想 > >它的底层是一个有序的双向列表,但不同于其他双向列表的是,它有多个指针,指针的跨度也不同,查找时从顶层向下,不断缩小范围。效率相对比较高,它的时间复杂度为 O ( log ⁡ n ) 。 ### 7.2 持久化 >**面试官**:Redis的数据持久化策略有哪些 ? > >**候选人**: > >嗯~~,在Redis中提供了两种数据持久化的方式:1、RDB 2、AOF > >**RDB:** > >定期更新,定期将Redis中的数据生成的快照同步到磁盘等介质上,磁盘上保存的就是Redis的内存快照 > >优点:数据文件的大小相比于aop较小,使用rdb进行数据恢复速度较快 > >缺点:比较耗时,存在丢失数据的风险 > >**AOF:** > >将Redis所执行过的所有指令都记录下来,在下次Redis重启时,只需要执行指令就可以了 > >优点:数据丢失的风险大大降低了 > >缺点:数据文件的大小相比于rdb较大,使用aop文件进行数据恢复的时候速度较慢 > >**面试官**:好的,那在你们的项目中采用了哪种持久化策略? > >**候选人**: > >嗯,是这样,我们项目中为了能够预防更少的数据丢失,两种方式都用了,RDB+AOF ### 7.3 主从和集群 >**面试官**:Redis集群有哪些方案, 知道嘛 ? > >**候选人**: > >嗯~~,在Redis中提供的集群方案总共有三种:主从复制、哨兵模式、Redis分片集群 > >1、主从复制 > >* 保证高可用性 >* 实现故障转移需要手动实现 >* 无法实现海量数据存储 > >2、哨兵模式 > >* 保证高可用性 >* 可以实现自动化的故障转移 >* 无法实现海量数据存储 > >3、Redis分片集群 > >* 保证高可用性 >* 可以实现自动化的故障转移 >* 可以实现海量数据存储 > >**面试官**:什么是 Redis 主从同步? > >**候选人**: > >这个主要指的是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的大小是有上限的,我们也可以通过配置修改大小 > > > >**面试官**:你们使用Redis是单点还是集群 ? 哪种集群 ?(说说你们生产环境redis部署情况?) > >**候选人**: > >嗯~~,是这样的,因为我们项目使用缓存的业务量不是超级大,所以采用了哨兵模式,机器一共有5台,主从2台(1主1从),哨兵3台。 > >**面试官**:你感觉集群模式有什么问题吗? > >**候选人**: > >嗯~~~,集群模式是可以解决大数据量的存储,但也存在一些问题,比如 > >- 维护起来比较麻烦 >- 集群之间的心跳检测和数据通信会消耗大量的网络带宽 >- 集群插槽分配不均和key的分批容易导致数据倾斜 >- 客户端的route会有性能损耗 >- 集群模式下无法使用lua脚本、事务 > >**面试官**:Redis分片集群中数据是怎么存储和读取的 ? > >**候选人**: > >嗯~,在redis集群中是这样的 > >Redis 集群引入了哈希槽的概念,有 16384 个哈希槽,集群中每个主节点绑定了一定范围的哈希槽范围, key通过 CRC16 校验后对 16384 取模来决定放置哪个槽,通过槽找到对应的节点进行存储。 > >取值的逻辑是一样的 > >**面试官**:redis集群脑裂? > >**候选人**: > >嗯! 这个在项目很少见,原因是这样的,我们现在用的是redis的哨兵模式集群的 > >有的时候由于网络等原因可能会出现脑裂的情况,就是说,由于redis master节点和redis salve节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到master,所以通过选举的方式提升了一个salve为master,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在old master那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将old master降为salve,这时再从新master同步数据,这会导致old master中的大量数据丢失。 > >关于解决的话,我记得在redis的配置中可以设置 > >第一可以设置最少的salve节点个数, > >第二可以设置数据复制和同步的延迟时间 > >**面试官**:怎么保证redis的高并发高可用 > >**候选人**: > >参考回答一: > >嗯,由于我们的存入redis的业务量是有限的,为了能够承受突发的高并发请求,我们公司采用的主从架构和哨兵模式 > >参考回答二: > >嗯,由于我们的存入redis的业务量很大,为了能够承受住高并发请求,我们公司采用的集群模式 ### 7.4使用场景 >**面试官**:项目中哪块使用了缓存? > >**本文作者**:结合自己简历上写的项目模块说明这个问题,要陈述出当时的场景 > >**候选人**: > >嗯,有很多的~~ > >我负责的黑马头条项目(你不能写),在app端的用户的行为数据我们是存储到了缓存中的,就是redis。 > >还有就是在自媒体端,自媒体人发布文章的时候可以设置一个未来时间定时发布文章,我们采用的redis的zset作为延迟队列用的。 > >还有一块是,在app端的热点文章,因为这块内容相对请求数量比较多,我们会把一些热点数据缓存到redis。减轻一大部分访问的压力。 > >我们的很多的定时任务,都是晚上自动跑,但是我们的服务都是集群,为了防止每个集群都执行这个定时任务,我们当时使用了redis实现的分布式来限制的(setnx) > >立可得项目使用场景: > >- 工单排行榜 >- 下单(分布式锁) > >**面试官**:什么是缓存穿透 ? 怎么解决 ? > >**候选人**: > >嗯~~,我想一下 > >缓存穿透是指查询一个一定**不存在**的数据,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。这种情况大概率是遭到了攻击。 > >解决方案的话,我们通常都会用布隆过滤器来解决它 > >**面试官**:好的,你能介绍一下布隆过滤器吗? > >**候选人**: > >嗯,是这样~ > >布隆过滤器主要是用于检索一个元素是否在一个集合中。我们当时使用的是redisson实现的布隆过滤器。 > >它的底层主要是先去初始化一个比较大数组,里面存放的二进制0或1。在一开始都是0,当一个key来了之后经过3次hash计算,模于数组长度找到数据的下标然后把数组中原来的0改为1,这样的话,三个数组的位置就能标明一个key的存在。查找的过程也是一样的。 > >当然是有缺点的,布隆过滤器有可能会产生一定的误判,我们一般可以设置这个误判率,大概不会超过5%,其实这个误判是必然存在的,要不就得增加数组的长度,其实已经算是很划分了,5%以内的误判率一般的项目也能接受,不至于高并发下压倒数据库。 > >**面试官**:什么是缓存击穿 ? 怎么解决 ? > >**候选人**: > >嗯!! > >缓存击穿的意思是对于设置了过期时间的key,缓存在某个时间点过期的时候,恰好这时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。 > >解决方案有两种方式: > >第一可以使用互斥锁:当缓存失效时,不立即去load db,先使用如 Redis 的 setnx 去设置一个互斥锁,当操作成功返回时再进行 load db的操作并回设缓存,否则重试get缓存的方法 > >第二种方案可以设置当前key逻辑过期,大概是思路如下: > >①:在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前key设置过期时间 > >②:当查询的时候,从redis取出数据后判断时间是否过期 > >③:如果过期则开通另外一个线程进行数据同步,当前线程正常返回数据,这个数据不是最新 > >当然两种方案各有利弊: > >如果选择数据的强一致性,建议使用分布式锁的方案,性能上可能没那么高,锁需要等,也有可能产生死锁的问题 > >如果选择key的逻辑删除,则优先考虑的高可用性,性能比较高,但是数据同步这块做不到强一致。 > >**面试官**:什么是缓存雪崩 ? 怎么解决 ? > >**候选人**: > >嗯!! > >缓存雪崩意思是设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB 瞬时压力过重雪崩。与缓存击穿的区别:雪崩是很多key,击穿是某一个key缓存。 > >解决方案主要是可以将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。 > >**面试官**:redis双写问题? > >**候选人**: > >嗯!我们当时采用的阿里巴巴旗下的canal组件实现数据同步:不需要更改业务代码,部署一个canal服务。canal服务把自己伪装成mysql的一个从节点,当mysql数据更新以后,canal会读取binlog数据,然后在通过canal的客户端获取到数据,更新缓存即可。 ### 7.5 Redis分布式锁 >**面试官**:Redis分布式锁如何实现 ? > >**候选人**: > >嗯,在redis中提供了一个命令setnx(SET if not exists) > >由于redis的单线程的,用了命令之后,只能有一个客户端对某一个key设置值,在没有过期或删除key的时候是其他客户端是不能设置这个key的 > >**面试官**:好的,那你如何控制Redis实现分布式锁有效时长呢? > >**候选人**: > >嗯,的确,redis的setnx指令不好控制这个问题,我们当时采用的redis的一个框架redisson实现的。 > >在redisson中需要手动加锁,并且可以控制锁的失效时间和等待时间,当锁住的一个业务还没有执行完成的时候,在redisson中引入了一个看门狗机制,就是说每隔一段时间就检查当前业务是否还持有锁,如果持有就增加加锁的持有时间,当业务执行完成之后需要使用释放锁就可以了 > >还有一个好处就是,在高并发下,一个业务有可能会执行很快,先客户1持有锁的时候,客户2来了以后并不会马上拒绝,它会自选不断尝试获取锁,如果客户1释放之后,客户2就可以马上持有锁,性能也得到了提升。 > >**面试官**:好的,redisson实现的分布式锁是可重入的吗? > >**候选人**: > >嗯,是可以重入的。这样做是为了避免死锁的产生。这个重入其实在内部就是判断是否是当前线程持有的锁,如果是当前线程持有的锁就会计数,如果释放锁就会在计算上减一。 > >**面试官**:你的项目中哪里用到了分布式锁 ? > >**候选人**: > >**本文作者**:这个也要根据自己简历上的项目进行描述 > >常见的业务:集群情况下定时任务(分布式锁)、秒杀、幂等性场景 ### 7.6 其他 >**面试官**:Redis的数据过期策略有哪些 ? > >**候选人**: > >嗯~,在redis中提供了两种数据过期删除策略 > > >第一种是惰性删除,在设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。 > >第二种是 定期删除,就是说每隔一段时间,我们就对一些key进行检查,删除里面过期的key > >定期清理的两种模式: > >- SLOW模式是定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf 的 **hz** 选项来调整这个次数 >- FAST模式执行频率不固定,每次事件循环会尝试执行,但两次间隔不低于2ms,每次耗时不超过1ms > >Redis的过期删除策略:**惰性删除 + 定期删除**两种策略进行配合使用定期删除函数的运行频率。 > >**面试官**:Redis的数据淘汰策略有哪些 ? > >**候选人**: > >嗯,这个在redis中提供了很多种,默认是noeviction,不上任何数据,内部不足直接报错 > >当然在线上的redis都会进行设置数据淘汰策略:比较常见是 > >对有所key的操作有三个: > >- allkeys-lru 挑选最近最少使用的数据淘汰 > >- allkeys-lfu 挑选最近使用次数最少的数据淘汰 > >- allkeys-random 随机选择数据淘汰 > >对设置了过期key的操作有四个: > >- volatile-lru 挑选最近最久使用的数据淘汰 > >- volatile-lfu 挑选最近最少使用数据淘汰 > >- volatile-ttl 挑选将要过期的数据淘汰 > >- volatile-random 任意选择数据淘汰 > >我们在项目通常设置的allkeys-lru,挑选最近最少使用的数据淘汰,把一些经常访问的key留在redis中 > >**面试官**:数据库有1000万数据 ,Redis只能缓存20w数据, 如何保证Redis中的数据都是热点数据 ? > >**候选人**: > >嗯,我想一下~~ > >可以使用 allkeys-lru (挑选最近最少使用的数据淘汰)淘汰策略,那留下来的都是经常访问的热点数据 > >**面试官**:Redis的内存用完了会发生什么? > >**候选人**: > >嗯~,这个要看redis的数据淘汰策略是什么,如果是默认的配置,redis内存用完以后则直接报错。我们当时设置的 allkeys-lru 策略。把最近最常访问的数据留在缓存中。 > >**面试官**:你们用过Redis的事务吗 ? 事务的命令有哪些 ? > >**候选人**: > >嗯,这个没在项目中用过,不过我知道一些。 > >Redis的事务与传统事务不同。传统事务一般ACID事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行 > >Redis事务的本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。单条命令式原子性执行的,但事务不保证原子性,且没有回滚。 > >关于事务的命令有: > >1、MULTI:用来组装一个事务 > >2、EXEC:执行一个事物 > >3、DISCARD:取消一个事务 > >4、WATCH:用来监视一些key,一旦这些key在事务执行之前被改变,则取消事务的执行 > >5、UNWATCH:取消 WATCH 命令对所有key的监视 > >**面试官**:Redis是单线程的,但是为什么还那么快? > >**候选人**: > >嗯,这个有几个原因吧~~~ > >1、完全基于内存的,C语言编写 > >2、采用单线程,避免不必要的上下文切换可竞争条件 > >3、使用多路I/O复用模型,非阻塞IO > >例如:bgsave 和 bgrewriteaof 都是在**后台**执行操作,不影响主线程的正常使用,不会产生阻塞