# Redis面试题汇总
2019-04-02阅读2.9K0
## 1.介绍[Redis](https://cloud.tencent.com/product/crs?from=10680)中数据类型
redis中的五种常用类型分别是string,Hash,List,Set,ZSet。

## 2.redis中的持久化方案
**RDB**:快照形式,定期把内存中当前时刻的数据保存到磁盘。Redis默认支持的持久化方案。速度快但是服务器断电的时候会丢失部分数据
**AOF**:append only file。把所有对redis数据库操作的命令,增删改操作的命令。保存到文件中。数据库恢复时把所有的命令执行一遍即可。两种持久化方案同时开启使用AOF文件来恢复数据库.能保证数据的完整性,但是速度慢。
两者如何选择?
1. 如果redis仅仅是用来做为缓存服务器的话,我们可以不使用任何的持久化。
2. 一般情况下我们会将两种持久化的方式都开启。redis优先加载AOF文件来恢复数据。RDB的好处是快速。
3. 在主从节点中,RDB作为我们的备份数据,只在salve(从节点)上启动,同步时间可以设置的长一点,只留(save 900 1)这条规则就可以了。
4. 开启AOF的情况下,主从同步是时候必然会带来IO的性能影响,此时我们可以调大auto-aof-rewrite-min-size的值,比如5GB。来减少IO的频率
5. 不开启AOF的情况下,可以节省IO的性能影响,这是主从间通过RDB持久化同步,但如果主从都挂掉,影响较大~
## 3.redis的优点
1. 读写速度快. 数据存放在内存中,数据结构类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1)
2. 支持丰富的数据类型,string,hash,list,set,sorted
3. 支持事务,watch
4. 丰富的特性:可以用于缓存,[消息队列](https://cloud.tencent.com/product/cmq?from=10680),按key设置过期时间,到期后自动删除
5. 支持数据持久化(将内存数据持久化到磁盘),支持AOF和RDB两种持久化方式,从而进行数据恢复操作,可以有效地防止数据丢失
6. 支持主从(master-slave)复制来实现数据备份,主机会自动将数据同步到从机
## 4.主从模式
主从结构一是可以进行冗余备份,二是可以实现读写分离. **主从复制**: 当我们在redis中开启了持久化(RDB或者AOF)但是这样也不能保证数据一定不会丢失,当Redis服务因为硬盘损坏那么数据也就丢失了,这时我们通过主从模式,一个master设置多个slave,那么在slave上面就会有多个备份数据,提高了我们数据的抗灾能力。
1. 一个Master可以有多个Slave,不仅主服务器可以有从服务器,从服务器也可以有自己的从服务器
2. 复制在Master端是非阻塞模式的,这意味着即便是多个Slave执行首次同步时,Master依然可以提供查询服务;
3. 复制在Slave端也是非阻塞模式的:如果你在redis.conf做了设置,Slave在执行首次同步的时候仍可以使用旧数据集提供查询;你也可以配置为当Master与Slave失去联系时,让Slave返回客户端一个错误提示;
4. 当Slave要删掉旧的数据集,并重新加载新版数据时,Slave会阻塞连接请求
**读写分离**: 主从架构中,可以考虑**关闭主服务器**的数据**持久化**功能,只让从服务器进行持久化,这样可以提高主服务器的处理性能。**从服务器**通常被设置为**只读模式**,这样可以避免从服务器的数据被误修改
## 5.主从模式下宕机怎么办
### slave宕机
相对简单,slave启动后会自动同步数据,增量同步。
### master宕机
**手动恢复**
1. 在从数据库中执行SLAVEOFNO ONE命令,断开主从关系并且将从库提升为主库继续服务;
2. 将主库重新启动后,执行SLAVEOF命令,将其设置为其他库的从库,这时数据就能更新回来
**哨兵功能自动恢复** 通过sentinel模式启动redis后,自动监控master/slave的运行状态, 已经被集成在redis2.4+的版本中如果Master异常,则会进行Master-Slave切换,将其中一个Slave作为Master,将之前的Master作为Slave **基本原理是**:心跳机制+投票裁决
## 6.缓存问题
### 缓存穿透
缓存穿透是指查询一个**一定不存在**的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库去查询,造成缓存穿透。
解决办法
1. 对所有可能查询的参数以hash形式存储,在**控制层先进行校验**,不符合则丢弃。还有最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
2. 也可以采用一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
### 缓存雪崩
如果缓存集中在一段时间内失效,发生大量的缓存穿透,所有的查询都落在数据库上,造成了缓存雪崩。 **解决办法**
1. 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。
2. 可以通过缓存reload机制,预先去更新缓存,再即将发生大并发访问前手动触发加载缓存
3. 不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀. 比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件
4. 做二级缓存,或者双缓存策略。A1为原始缓存,A2为拷贝缓存,A1失效时,可以访问A2,A1缓存失效时间设置为短期,A2设置为长期。
### 缓存击穿
缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。
### 缓存预热
缓存预热就是系统上线后,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
**缓存预热解决方案**:
1. 直接写个缓存刷新页面,上线时手工操作下;
2. 数据量不大,可以在项目启动的时候自动进行加载;
3. 定时刷新缓存;
### 缓存更新
我们知道通过expire来设置key 的过期时间,那么对过期的数据怎么处理呢?除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
1. 定时去清理过期的缓存;
2. 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。
### 缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。 **降级的最终目的**是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。 在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:
1. 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
2. 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
3. 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
4. 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
### 缓存热点key
使用缓存 + 过期时间的策略既可以加速数据读写,又保证数据的定期更新,这种模式基本能够满足绝大部分需求。但是有两个问题如果同时出现,可能就会对应用造成致命的危害:
1. 当前 key 是一个热点 key( 可能对应应用的热卖商品、热点新闻、热点评论等),并发量非常大。
2. 重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的 SQL、多次 IO、多个依赖等。
在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。 热点 key 失效后大量线程重建缓存 要解决这个问题也不是很复杂,但是不能为了解决这个问题给系统带来更多的麻烦,所以需要制定如下目标: 减少重建缓存的次数 数据尽可能一致 较少的潜在危险
1)互斥锁 (mutex key)
此方法只允许一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可,
下面代码使用 Redis 的 setnx 命令实现上述功能,伪代码:
~~~javascript
String get(String key) {
//从redis中获取key
String value = redis.get(key);
//如果value为空则开始重构缓存
if (value == null) {
//只允许一个线程重构缓存,使用nx,并设置过期时间ex
String mutexKey = "mutex:key" + key;
if (redis.set(mutexKey, "1", "ex 180", "nx")) {
//从数据源获取数据
value = db.get(key);
//回写redis并设置过期时间
redis.set(key, value, timeout);
//删除mutexKey
redis.del(mutexKey);
} else {
//其他线程睡眠50秒再重试
Thread.sleep(50); get(key);
}
}
return value;
}
~~~
从 Redis 获取数据,如果值不为空,则直接返回值。 如果 set(nx 和 ex) 结果为 true,说明此时没有其他线程重建缓存,那么当前线程执行缓存构建逻辑。 如果 setnx(nx 和 ex) 结果为 false,说明此时已经有其他线程正在执行构建缓存的工作,那么当前线程将休息指定时间 (例如这里是 50 毫秒,取决于构建缓存的速度 ) 后,重新执行函数,直到获取到数据。
2)永远不过期
永远不过期”包含两层意思:
从缓存层面来看,确实没有设置过期时间,所以不会出现热点 key 过期后产生的问题,也就是“物理”不过期。
从功能层面来看,为每个 value 设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。
从实战看,此方法有效杜绝了热点 key 产生的问题,但唯一不足的就是重构缓存期间,会出现数据不一致的情况,这取决于应用方是否容忍这种不一致。下面代码使用 Redis 进行模拟:
~~~javascript
String get(final String key) {
V v = redis.get(key);
String value = v.getValue();
//逻辑过期时间
final Long logicTimeout = v.getLogicTimeout();
//如果逻辑时间小于当前时间,开始重建缓存
if (logicTimeout <= System.currentTimeMillis()) {
final String mutexKey = "mutex:key" + key;
if (redis.set(mutexKey, "1", "ex 180", "nx")) {
//重建缓存
threadPool.execute(new Runnable() {
@Override
public void run() {
String dbValue = db.get(key);
redis.set(key, (dbValue, newLogicTimeout));
redis.del(mutexKey);
}
});
}
}
return value;
}
~~~
作为一个并发量较大的应用,在使用缓存时有三个目标: 第一,加快用户访问速度,提高用户体验。 第二,降低后端负载,减少潜在的风险,保证系统平稳。 第三,保证数据“尽可能”及时更新。 下面将按照这三个维度对上述两种解决方案进行分析。
互斥锁 (mutex key):这种方案思路比较简单,但是存在一定的隐患,如果构建缓存过程出现问题或者时间较长,可能会存在死锁和线程池阻塞的风险,但是这种方法能够较好的降低后端存储负载并在一致性上做的比较好。
” 永远不过期 “:这种方案由于没有设置真正的过期时间,实际上已经不存在热点 key 产生的一系列危害,但是会存在数据不一致的情况,同时代码复杂度会增大。
## 7.Redis实现消息队列
普通队列:一般使用list结构作为队列,rpush生产消息,lpop消费消息,blpop阻塞消费。 消费多次:生产一次消费多次的情况使用发布/订阅模式 延时队列:使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理
## 8.如果有大量的key需要设置同一时间过期,一般需要注意什么?
如果大量的key过期时间设置的过于集中,到过期的那个时间点,redis可能会出现短暂的卡顿现象。一般需要在时间上加一个随机值,使得过期时间分散一些。
## 9.Redis的同步机制了解么?
主从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将rdb文件全量同步到复制节点,复制节点接受完成后将rdb镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。
## 10.是否使用过Redis集群,集群的原理是什么?
Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。 Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。
- 空白目录
- 算法
- 排序
- 冒泡排序
- 选择排序
- 插入排序
- 归并排序
- 快速排序
- 计数排序
- 桶排序
- 基数排序
- 希尔排序
- 堆排序
- 二分查找
- 最小堆
- 最小索引堆
- 平衡二叉树(AVL tree)
- bitmap位图
- 布隆过滤器
- hashmap
- topK
- 跳表
- LRU Cache
- kmp
- 最小堆和堆排序
- 最短路径
- C++
- 运行时类型判断RTTI
- C++反射
- 手动实现智能指针
- 序列化实现
- rpc实现
- std::forward
- 函数指针的妙用
- C/C++
- std::function
- 同步队列
- 线程池实现
- std::promise
- 深入理解虚函数
- extern "C" 关键字讲解
- 大端小端的区别
- 简历
- 简历1
- redis
- 数据结构和对象
- sds
- list
- zskiplist
- 腾讯云redis面试题总结
- redis集群部署
- LeetCode
- 目标
- go基础
- 算法快速入门
- 数据结构篇
- 二叉树
- 链表
- 栈和队列
- 二进制
- 基础算法篇
- 二分搜索
- 排序算法
- 动态规划
- 算法思维
- 递归思维
- 滑动窗口思想
- 二叉搜索树
- 回溯法
- 其他
- 剑指offer
- 笔记
- git代理加速
- Linux
- vim大法
- vscode远程不能跳转
- cmake
- 设计模式
- 单例模式
- 简单工厂模式
- 外观模式
- 适配器模式
- 工厂方法模式
- 抽象工厂模式
- 生成器模式
- 原型模式
- 中介者模式
- 观察者模式
- 访问者模式
- 命令模式
- 网络编程
- epoll reactor模式
- linux timerfd系列函数总结
- IO
- mapreduce
- 反射器
- leo通信库
- Mutex
- Condition
- thread
- raft
- 协程
- hook
- 定时器
- 别人的面试经验
- 面试题
- vector崩溃问题
- JAVA
- Linux java环境配置
- ucore
- lab1
- FreeNOS
- leveldb
- 刷题笔记
- 回文串
- 前缀树
- 字符串查找
- 查找两个字符串a,b中的最长公共子串
- 动态规划
- golang
- 顺序循环打印实现
- 数据结构
- rpc运用
- python
- 单例
- 深拷贝浅拷贝
- 链表
- python基础题
- mysql
- 事务
- Linux
- 共享内存
- 刷题记录
- 贪心算法
- 动态规划
- 面试
- 腾讯C++面试
- 微众面试JD
- 迅雷网络面试
- 学习网址
- rabbitMq
