当前位置: 首页 > article >正文

Redis经典面试题

文章目录

  • 1、Redis适用场景
  • 2、redis为什么执行这么快?
  • 3、Redis的线程模型
    • Redis版本在6.0之前都是单线程
    • Redis6.x多线程的实现机制
    • Redis6.x默认是否开启了多线程
  • 4、Redis常见的数据类型
  • 5、缓存穿透、击穿、雪崩
    • 1、缓存穿透
    • 2、缓存雪崩
    • 3、缓存击穿
  • 6、Redis的数据持久化策略
  • 7、Redis集群有哪些方案
    • 主从复制集群
    • 哨兵集群
    • Cluster分片集群
  • 8、Redis分布式锁如何实现
    • 你的项目中哪里用到了分布式锁
    • 分布式锁具体场景分析
      • 1、死锁问题
      • 2、锁超时问题
      • 3、归一问题
      • 4、可重入问题
      • 5、阻塞与非阻塞问题
  • 9、Redis的过期策略与内存淘汰策略
  • 10、Redis有序集合以及底层结构解析
  • 11、Redis做消息队列
  • 12、Redis事务
  • 13、MySQL 里有 2000w 数据,redis 中只存 20w 的数据,如何保证 redis 中的数据都是热点数据
  • 14、Redis 里有 1 亿个 key,其中有 10w 个 key 是以某个固定的前缀开头的,如果将它们全部找出来?


1、Redis适用场景

  • 会话缓存(Session Cache)
    最常用的一种使用Redis的情景是会话缓存(session cache)。用Redis缓存会话比其他存储(如Memcached)的优势在于:Redis提供持久化。
  • 队列
    Reids在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得Redis能作为一个很好的消息队列平台来使用。Redis作为队列使用的操作。
  • 适用bitmap构建布隆过滤器。
  • 排行榜/计数器
    Redis在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis只是正好提供了这两种数据结构。
  • 发布/订阅,Redis的发布/订阅功能。发布/订阅的使用场景确实非常多。

2、redis为什么执行这么快?

  • 纯内存操作:Redis将所有数据存储在内存中,这意味着对数据的读写操作直接在内存中运行,而内存的访问速度远远高于磁盘。这种设计使得Redis能够已接近硬件极限的速度处理数据读写
  • 单线程模型:Redis使用单线程模型来处理客户端请求。这可能听起来效率不高,但是实际上,这种设计避免了多线程频繁切换和过度竞争带来的性能开销。Redis每个请求的执行时间都是很短的,因此单线程下,也能处理大量的并发请求
  • I/O多路复用:Redis使用了I/O多路复用技术,可以在单线程的环境下同时监听多个客户端连接,只有当有网络事件(如用户发送一个请求)发生的时候才会进行实际的I/O操作。这样有效的利用了CPU资源,减少了无谓的等待
  • 高效数据结构:Redis提供了多种高效的数据结构,如哈希表、有序集合等。这些数据结构的实现都经过了优化,使得Redis在处理这些数据结构的操作是非常高效的

3、Redis的线程模型

Redis版本在6.0之前都是单线程

所有的客户端的请求处理、命令执行以及数据读写操作都是在一个主线程中完成得。这种设计目的就是为了防止多线程环境下的锁竞争和上下文切换所带来的性能开销,这样保证在高并发场景下的性能

Redis6.0 引入多线程主要是为了提高网络 IO 读写性能(Redis 的瓶颈并不在 CPU,而在内存和网络。)。但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了,对于指令的执行过程仍然是在主线程来处理 。因此不需要担心线程安全问题。

Redis6.x多线程的实现机制

在这里插入图片描述

  • 主线程负责接收建立连接请求,获取 Socket 放入全局等待读处理队列。
  • 主线程处理完读事件之后,通过 RR(Round Robin)将这些连接分配给这些 IO 线程。
  • 主线程阻塞等待 IO 线程读取 Socket 完毕。
  • 主线程通过单线程的方式执行请求命令,请求数据读取并解析完成,但并不执行。
  • 主线程阻塞等待 IO 线程将数据回写 Socket 完毕。
  • 解除绑定,清空等待队列。
    在这里插入图片描述该设计有如下特点
  • IO 线程要么同时在读 socket,要么同时在写,不会同时读或写。
  • IO 线程只负责读写 socket 解析命令,不负责命令处理。

Redis6.x默认是否开启了多线程

Redis6.0 的多线程默认是禁用的,只使用主线程。
如需开启需要修改 redis 配置文件 redis.conf

io-threads-do-reads yes

开启多线程后,还需要设置线程数,否则是不生效的。同样需要修改 redis 配置文件 redis.conf

io-threads 4 #官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程

4、Redis常见的数据类型

Redis是一个高性能的键值存储系统,支持多种数据结构。每种数据结构都是为了解决特定场景的问题而设计的,想要用好Redis,就必须了解其底层结构,再结合具体的场景和需求进行选择和使用。

  • 5种基本类型:String(字符串)、Hash(哈希)、List(列表)、Set(集合)、Zset(有序集合),
  • 三种特殊类型,Geo(地理位置)、HyperLog(基数统计)、bitMaps(位图)。

参考大佬文章:redis五种数据结构的详细分析。

5、缓存穿透、击穿、雪崩

1、缓存穿透

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,造成数据库的压力倍增的情况。
例如:发起为id值为 -1 的数据或 id 为特别大不存在的数据。
在这里插入图片描述
解决方案:

  • 接口层增加校验,比如用户鉴权校验,参数做校验 比如:id 做基础校验,id <=0的直接拦截。
  • 对于像ID为负数的非法请求直接过滤掉,采用布隆过滤器(Bloom Filter)。
  • 针对在数据库中找不到记录的,我们仍然将该空数据存入缓存中,当然一般会设置一个较短的过期时间。

2、缓存雪崩

缓存服务器宕机或者大量缓存集中某个时间段失效,导致请求全部去到数据库,造成数据库压力倍增的情况,这个是针对多个key而言
在这里插入图片描述
解决方案:

  • 实现缓存高可用,通过redis cluster将热点数据均匀分布在不同的Redis库中也能避免全部失效的问题。
  • 批量往Redis存数据的时候,把每个Key的失效时间都加个随机值。

3、缓存击穿

redis过期后的一瞬间,有大量用户请求同一个缓存数据,导致这些请求都去请求数据库,造成数据库压力倍增的情,针对一个key而言

  • 缓存击穿与缓存雪崩的区别是这里针对的是某一热门key缓存,而雪崩针对的是大量缓存的集中失效
    在这里插入图片描述
    解决方案

  • 设置热点数据永远不过期。

  • 添加互斥锁,保证同一时刻只有一个线程可以访问数据库。

6、Redis的数据持久化策略

  • RDB 持久化(全量),是指在指定的时间间隔内将内存中的数据集快照写入磁盘。
  • AOF持久化(增量),以日志的形式记录服务器所处理的每一个写、删除操作
  • RDB和AOF一起使用, 在Redis4.0版本支持混合持久化方式 ( 设置 aof-use-rdb-preamble yes )

7、Redis集群有哪些方案

主从复制集群

读写分离, 一主多从 , 解决高并发读的问题。
分为一主一从或一主多从,主从之间同步分为全量或增量。量同步:master 节点通过 BGSAVE 生成对应的RDB文件,然后发送给slave节点,slave节点接收到写入命令后将master发送过来的文件加载并写入;增量同步:即在 master-slave 关系建立开始,master每执行一次数据变更的命令就会同步至slave节点。一般会将写请求转发到master,读请求转发到slave。提高了redis的性能。

哨兵集群

分别有哨兵集群与Redis的主从集群,哨兵作为操作系统中的一个监控进程,对应监控每一个Redis实例,如果master服务异常(ping pong其中节点没有回复且超过了一定时间),就会多个哨兵之间进行确认,如果超过一半确认服务异常,则对master服务进行下线处理,并且选举出当前一个slave节点来转换成master节点;如果slave节点服务异常,也是经过多个哨兵确认后,进行下线处理。提高了redis集群高可用的特性,及横向扩展能力的增强。

在这里插入图片描述

Cluster分片集群

集群部署:属于“去中心化”的一种方式,多个 master 节点保存整个集群中的全部数据,而数据根据 key 进行 crc-16 校验算法进行散列,将 key 散列成对应 16383 个 slot,而 Redis cluster 集群中每个 master 节点负责不同的slot范围。每个 master 节点下还可以配置多个 slave 节点,同时也可以在集群中再使用 sentinel 哨兵提升整个集群的高可用性。

在这里插入图片描述

8、Redis分布式锁如何实现

Redis分布式锁主要依靠一个SETNX指令实现的 , 这条命令的含义就是“SET if Not Exists”,即不存在的时候才会设置值。

  • 命令在设置成功时返回1。
  • 命令在设置失败时返回0。

假设此时有线程A和线程B同时访问临界区代码,假设线程A首先执行了SETNX命令,并返回结果1,继续向下执行。而此时线程B再次执行SETNX命令时,返回的结果为0,则线程B不能继续向下执行。只有当线程A执行DELETE命令将设置的锁状态删除时,线程B才会成功执行SETNX命令设置加锁状态后继续向下执行。

你的项目中哪里用到了分布式锁

在我最近做的一个项目中 , 我们在任务调度的时候使用了分布式锁
早期我们在进行定时任务的时候我们采用的是SpringTask实现的 , 在集群部署的情况下, 多个节点的定时任务会同时执行 , 造成重复调度 , 影响运算结果, 浪费系统资源
这里为了防止这种情况的发送, 我们使用Redis实现分布式锁对任务进行调度管理 , 防止重复任务执行
后期因为我们系统中的任务越来越多 , 执行规则也比较多 , 而且单节点执行效率有一定的限制 , 所以定时任务就切换成了XXL-JOB , 系统中就没有再使用分布式锁了

分布式锁具体场景分析

1、死锁问题

在使用分布式锁的时候, 如果因为一些原因导致系统宕机, 锁资源没有被释放, 就会产生死锁
解决的方案 : 上锁的时候设置锁的超时时间
Boolean isLocked = stringRedisTemplate.opsForValue().setIfAbsent(PRODUCT_ID, “binghe”, 30, TimeUnit.SECONDS);

2、锁超时问题

如果业务执行需要的时间, 超过的锁的超时时间 , 这个时候业务还没有执行完成, 锁就已经自动被删除了
其他请求就能获取锁, 操作这个资源 , 这个时候就会出现并发问题 , 解决的方案 :

  • 引入Redis的watch dog机制, 自动为锁续期
  • 开启子线程 , 每隔20S运行一次, 重新设置锁的超时时间

3、归一问题

如果一个线程获取了分布式锁, 但是这个线程业务没有执行完成之前 , 锁被其他的线程删掉了 , 又会出现线程并发问题 , 这个时候就需要考虑归一化问题
就是一个线程执行了加锁操作后,后续必须由这个线程执行解锁操作,加锁和解锁操作由同一个线程来完成。
为了解决只有加锁的线程才能进行相应的解锁操作的问题,那么,我们就需要将加锁和解锁操作绑定到同一个线程中,可以使用ThreadLocal来解决这个问题 , 加锁的时候生成唯一标识保存到ThreadLocal , 并且设置到锁的值中 , 释放锁的时候, 判断线程中的唯一标识和锁的唯一标识是否相同, 只有相同才会释放

public class RedisLockImpl implements RedisLock{
 @Autowired
 private StringRedisTemplate stringRedisTemplate;private ThreadLocal<String> threadLocal = new ThreadLocal<String>();@Override
 public boolean tryLock(String key, long timeout, TimeUnit unit){
     String uuid = UUID.randomUUID().toString();
     threadLocal.set(uuid);
     return stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
 }
 @Override
 public void releaseLock(String key){
     //当前线程中绑定的uuid与Redis中的uuid相同时,再执行删除锁的操作
     if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){
       stringRedisTemplate.delete(key);   
     }
 }
}

4、可重入问题

还有一种场景就是在一个业务中, 有个操作都需要获取到锁, 这个时候第二个操作就无法获取锁了 , 操作会失败
例如 : 下单业务中, 扣减商品库存会给商品加锁, 增加商品销量也需要给商品加锁 , 这个时候需要获取二次锁
第二次获取商品锁就会失败 , 这就需要我们的分布式锁能够实现可重入。
实现可重入锁最简单的方式就是使用计数器 , 加锁成功之后计数器 + 1 , 取消锁之后计数器 -1 , 计数器减为0 , 真正从Redis删除锁

public class RedisLockImpl implements RedisLock{
 @Autowired
 private StringRedisTemplate stringRedisTemplate;private ThreadLocal<String> threadLocal = new ThreadLocal<String>();private ThreadLocal<Integer> threadLocalInteger = new ThreadLocal<Integer>();@Override
 public boolean tryLock(String key, long timeout, TimeUnit unit){
     Boolean isLocked = false;
     if(threadLocal.get() == null){
         String uuid = UUID.randomUUID().toString();
      threadLocal.set(uuid);
         isLocked = stringRedisTemplate.opsForValue().setIfAbsent(key, uuid, timeout, unit);
     }else{
         isLocked = true;   
     }
     //加锁成功后将计数器加1
     if(isLocked){
         Integer count = threadLocalInteger.get() == null ? 0 : threadLocalInteger.get();
         threadLocalInteger.set(count++);
     }
     return isLocked;
 }@Override
 public void releaseLock(String key){
     //当前线程中绑定的uuid与Redis中的uuid相同时,再执行删除锁的操作
     if(threadLocal.get().equals(stringRedisTemplate.opsForValue().get(key))){
         Integer count = threadLocalInteger.get();
         //计数器减为0时释放锁
         if(count == null || --count <= 0){
           stringRedisTemplate.delete(key);      
         }
     }
 }
}

5、阻塞与非阻塞问题

在使用分布式锁的时候 , 如果当前需要操作的资源已经加了锁, 这个时候会获取锁失败, 直接向用户返回失败信息 , 用户的体验非常不好 , 所以我们在实现分布式锁的时候, 我们可以将后续的请求进行阻塞,直到当前请求释放锁后,再唤醒阻塞的请求获得分布式锁来执行方法。
具体的实现就是参考自旋锁的思想, 获取锁失败自选获取锁, 直到成功为止 , 当然为了防止多条线程自旋带来的系统资料消耗, 可以设置一个自旋的超时时间 , 超过时间之后, 自动终止线程 , 返回失败信息

9、Redis的过期策略与内存淘汰策略

参考大佬文章:Redis的过期策略与内存淘汰策略

10、Redis有序集合以及底层结构解析

参考大佬文章两篇:
redis的Zset底层数据结构解析1

redis的Zset底层数据结构解析2

11、Redis做消息队列

解决方案:一般使用 list 结构作为队列,rpush 生产消息,lpop 消费消息。当 lpop 没有消息的时候,要适当 sleep 一会再重试。如果对方追问可不可以不用 sleep 呢?list 还有个指令叫 blpop,在没有消息的时候,它会阻塞住直到消息到来。

延伸问题:如果对方追问能不能生产一次消费多次呢?使用 pub/sub 主题订阅者模式,可以实现1:N 的消息队列。

延伸问题: pub/sub 有什么缺点?
在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如 RabbitMQ等。

延伸问题: redis 如何实现延时队列?

使用 sortedSet,拿时间戳作为score,消息内容作为 key 调用 zadd 来生产消息,消费者用 zrangebyscore 指令获取 N 秒之前的数据轮询进行处理。

12、Redis事务

参考大佬文章:redis事务

13、MySQL 里有 2000w 数据,redis 中只存 20w 的数据,如何保证 redis 中的数据都是热点数据

Redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。我们可以参考
Redis的过期策略与内存淘汰策略,配置相应的内存淘汰策略。

14、Redis 里有 1 亿个 key,其中有 10w 个 key 是以某个固定的前缀开头的,如果将它们全部找出来?

解决方案:可以使用 keys 指令可以扫出指定模式的 key 列表。

延伸问题:如果redis 正在给线上的业务提供服务,那使用 keys 指令会有什么问题?

  • 这个时候你要回答 redis 关键的一个特性:redis 的单线程的。keys 指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。
  • 这个时候可以使用 scan 指令,scan 指令可以无阻塞的提取出指定模式的 key 列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用 keys 指令长。

http://www.kler.cn/a/441806.html

相关文章:

  • iOS 网络请求: Alamofire 结合 ObjectMapper 实现自动解析
  • 资料03:【TODOS案例】微信小程序开发bilibili
  • 【深度学习项目】语义分割-FCN网络(原理、网络架构、基于Pytorch实现FCN网络)
  • TypeScript - 利用GPT辅助学习
  • 【Linux】Linux入门(三)权限
  • 低代码系统-产品架构案例介绍(五)
  • [免费]SpringBoot+Vue校园社团管理系统(优质版)【论文+源码+SQL脚本】
  • centos7扩容ext4文件系统类型硬盘
  • element中input框添加@keyup.enter.native,按enter后刷新页面
  • MySQL数据库备份,恢复
  • EasyExcel 动态设置表格的背景颜色和排列
  • Ubuntu 上cutecom使用指南
  • 【Mysql优化】EXPLAIN 返回列详解:深入 SQL 查询优化的工具
  • 复习打卡MySQL篇03
  • 一篇文章解决HarmonyOS开发USB调试设备连接失败
  • 【精】Linux虚拟机 Docker 配置阿里云镜像加速
  • 深入理解 HTTP 协议:从基础到实践全解析
  • C++ 并发专题 - C++线程同步的几种方法
  • Mysql 笔记2 emp dept HRs
  • DCDC降压模块
  • MyBatis 核心知识与实践
  • AndroidStudio——安卓项目结构与文件介绍
  • ubuntu+ros新手笔记(二):古月·ROS2入门21讲学习笔记
  • 【PostgreSQL异常解决】PostgreSQL 异常错误: PG::Error 服务意外关闭连接
  • 【ArcGIS技巧】天地图下载瓦片并合并成图片
  • Hive-4.0.1数据库搭建(可选配置用户名密码远程连接,涵盖切换为tez引擎)