IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 大数据 -> Redis面试八股文第三弹 -> 正文阅读

[大数据]Redis面试八股文第三弹

大家好,我是路人张,这是Redis系列第三篇,还有一篇就结束了,下个系列是JVM,这篇写的有些上头,所以有的地方字数比较多。在公众号路人zhang后台回复"面试手册",获取PDF版,Redis系列完结后会整理PDF中。

推荐阅读:

文章目录:

  • Redis的分区

  • Redis的分区作用是什么?

  • Redis分区有哪些实现方案?

  • Redis分区的缺点?

  • Redis的分布式问题

  • 什么是分布式锁?

  • 分布式锁具有哪些特性?

  • 分布式锁的实现方法?

  • Redis如何实现分布式锁?

  • Redis并发竞争key问题应该如何解决?

  • 什么是RedLock

  • Redis的缓存问题

  • 说下什么是缓存雪崩、缓存穿透、缓存击穿,及它们的解决方案

Redis的分区

Redis的分区作用是什么?

  • 扩展数据库容量,可以利用多台机器的内存构建更大的数据库

  • 扩展计算能力,分区可以在多核和多计算机之间弹性扩展计算能力,在多计算机和网络适配器之间弹性扩展网络带宽

Redis分区有哪些实现方案?

在介绍Redis集群的实现方案时已经介绍过了客户端分区代理分区,常见的Redis分区方案主要有以下三种:

  • 客户端分区:客户端决定数据被存到哪个Redis节点或者从哪个节点读取

  • 代理分区:客户端将请求发送到代理,而不是直接发送到Redis节点,代理根据分区策略将请求发送到Redis节点上

  • 查询路由:客户端随机请求任意一个Redis节点,这个Redis节点将请求转发到正确的Redis节点。Redis Cluster实现了一种混合形式的查询路由,并不是直接将请求从一个Redis节点转发到另一个Redis节点,而是在客户端的帮助下直接重定向到正确的redis节点

Redis分区的缺点?

  • 不支持多个键的操作,例如不能操作映射在两个Redis实例上的两个集合的交叉集。(其实可以做到这一点,但是需要间接的解决)

  • Redis不支持多个键的事务

  • Redis是以键来分区,因此不能使用单个大键对数据集进行分片,例如一个非常大的有序集

  • 数据的处理会变得复杂,比如你必须处理多个RDB和AOF文件,在多个实例和主机之间持久化你的数据

  • 添加和删除节点也会变得复杂,例如通过在运行时添加和删除节点,Redis集群通常支持透明地再均衡数据,但是其他系统像客户端分区或者代理分区的特性就不支持该特性。不过_Pre-sharding_(预分片)可以在这方面提供帮助。

Redis的分布式问题

什么是分布式锁?

相信大家对程序中的锁并不陌生,无论是在并发编程或者Java虚拟机都有学到过。

锁在程序中的作用主要是同步,就是保证共享资源在同一时刻只能被同一个线程访问。

分布式锁则是为了保证在分布式场景下,共享资源在同一时刻只能被同一个线程访问,或者说是用来控制分布式系统之间同步访问共享资源。举个简单例子,如下图

从上图可以看出,变量A在三个服务器中都有涉及到,如果不对其进行控制的话,三个服务器中的变量A很难做到同步,解决这个问题的方法就是分布式锁。

分布式锁具有哪些特性?

  • 互斥性:在任意时刻,同一条数据只能被一台机器上的一个线程执行

  • 高可用性:当部分节点宕机后,客户端仍可以正常地获取锁和释放锁

  • 独占性:加锁和解锁必须同一台服务器执行,不能在一个服务器上加锁,在另一个服务器上释放锁

  • 防锁超时:如果客户端没有主动释放锁,服务器会在一定时间后自动释放锁, 防止客户端宕机或者网络异常导致宕机

分布式锁的实现方法?

基本思路就是要在整个系统中提供一个全局、唯一的获取锁的“东西”,然后每个系统在需要加锁时,都去问这个“东西”拿到一把锁,这样不同的系统拿到的就可以认为是同一把锁。

常见的分布式锁实现方案有三种:

基于关系型数据库

优点:直接借助数据库容易理解

缺点:在使用关系型数据库实现分布式锁的过程中会出现各种问题,例如数据库单点问题和可重入问题,并且在解决过程中会使得整个方案越来越复杂

基于Redis

**优点:**性能好,实现起来较为方便

缺点

  • key的过期时间设置难以确定,如何设置的失效时间太短,方法没等执行完,锁就自动释放了,那么就会产生并发问题。如果设置的时间太长,其他获取锁的线程就可能要平白的多等一段时间。

  • Redis的集群部署虽然能解决单点问题,但是并不是强一致性的,锁的不够健壮

基于zookeeper

优点:有效地解决单点问题,不可重入问题,非阻塞问题以及锁无法释放的问题,实现起来较为简单。

**缺点:**性能上不如使用缓存实现分布式锁

三种方案的对比

方案复杂度性能可靠性学习成本
基于关系型数据库
基于Redis
基于zookeeper

Redis如何实现分布式锁?

前面讲过了分布式锁的特性,其实实现分布式锁就是围绕着这些展开的

Redis实现分布式锁的主要命令:SETNX,该命令的作用是当key不存在时设置key的值,当Key存在时,什么都不做。

先来看最简单的实现方式,如下图

从上图可以看到主要两个关键步骤,加锁和解锁

但是这个简陋的分布式锁存在很多问题,并不能满足上述介绍的分布式锁的特性,

比如,当线程1执行到上图中执行业务这步时,业务代码突然出现异常了,无法进行删除锁这一步,那就完犊子了,死锁了,其他线程也无法获取到锁了(因为SETNX的特性)。

改进方案1

那咋整呢?

一提到异常,有人想起了try-catch-finally了,把删除锁的操作放到finally代码块中,就算出现异常,也是能正常释放锁的,执行业务出现异常这个问题确实解决了。但这玩意并不靠谱,如果Redis在执行业务这步宕机了呢,finally代码块也不会执行了。

改进方案2

其实这个问题很好解决,只需给锁设置一个过期时间就可以了,对key设置过期时间在Redis中是常规操作了。就是这个命令SET key value [EX seconds][PX milliseconds] [NX|XX]

  • EX second: 设置键的过期时间为second秒;

  • PX millisecond:设置键的过期时间为millisecond毫秒;

  • NX:只在键不存在时,才对键进行设置操作;

  • XX:只在键已经存在时,才对键进行设置操作;

  • SET操作完成时,返回OK,否则返回nil。

那现在这个方案就完美了吗?显然没有

例如,线程1获取到了锁,并设置了有效时间10秒,但线程1在执行业务时超过了10秒,锁到期自动释放了,在锁释放后,线程2又获取了锁,在线程2执行业务时,线程1执行完了,随后执行了删除锁这一步,但是线程1的锁早就到期自动释放了,他删除的是线程2的锁!!!

上面这个例子说的有点乱,突然想到一个现实生活中的例子,把Redis比作一个厕所,张三去上厕所,关上了门(获取锁,并设置了10秒),上到一半(10秒到了,门自动开了),这时李四进去了,关上了门(获取锁,并设置了10秒),张三上完了厕所,把门打开了走了(执行了删除锁操作)

上面这个荒诞的例子说明了方案2有两处不合理的地方,第一是张三厕所没上完,李四怎么能进去呢?他们上厕所产生了冲突,第二是张三上完厕所怎么能打开李四的门呢(分布式锁的特性,加锁和解锁必须同一台服务器执行)?

改进方案3

其实看起来方案2的问题很容易解决,只需要把锁的过期时间设置的非常长,就可以避免这两个问题,但是这样并不可行,因为这样相当于回到最简陋的方案(会导致李四一直上不到厕所)。

那如何能让李四上到厕所,还不会让自己锁的门被张三打开门呢?

很简单,为锁加一个标识,例如生成一个UUID,作为锁的标识,每个线程获取锁时都会生成一个不同的UUID作为锁的标识,在进行删除锁时会进行判断,锁的标识和自己生成UUID相等时才进行删除操作,这样就避免线程1释放了线程2的锁。(相当于自己上自己的锁,不要计较为什么张三在李四上厕所时不需要李四的钥匙就能离开厕所这种事,上厕所和分布式锁逻辑并不完全相同,只是简单类比)

那怎么解决李四未等张三上完厕所就进厕所呢?(如何确定锁的过期时间)

可以在加锁时,先设置一个预估的过期时间,然后开启一个守护线程,定时去检测这个锁的失效时间,如果锁快要过期了,操作共享资源还未完成,那么就自动对锁进行续期,重新设置过期时间。

好了,张三和李四上厕所的问题解决了。

那方案3就没有其他问题了吗?其实还是有的,比如目前的分布式锁还不具备可重入性(同一线程可以重复获取锁,解决线程需要多次进入锁内执行任务的问题)

改进方案4

参考其他重入锁的实现,可以通过对锁进行重入计数,加锁时加 1,解锁时减 1,当计数归 0 时才能释放锁。

那现在方案就没有问题了吗,其实还有

比如,线程1获取了锁,线程2没有获取到锁,那么线程2怎么知道线程1啥时候释放了锁,进而再去获取锁呢?

改进方案5

方案4中问题的解决方案,一般以下两种解决方案:

  • 可以通过客户端轮询的方式,就是线程2过一会就来看看是不是能获取锁了。这种方式比较消耗服务器资源,当并发量比较大时,会影响服务器的效率。

  • 通过Redis的发布订阅功能,当获取锁失败时,订阅锁释放消息,获取锁成功后释放时,发送锁释放消息。

那现在这个方案完美了吗?也还没有

目前讨论的都是redis是单节点的情况,如果这个节点挂了,那么所有的客户端都获取不到锁了

改进方案6

为了实现多节点Redis的分布式锁,Redis的作者提出了RedLock算法。

这是RedLock算法官网的地址,https://redis.io/topics/distlock,英文好的建议直接看官方文档,毕竟我这四六级飘过的人也可能理解的不准确,下面的内容主要是参考官网内容。

首先介绍保证分布式锁的有效性和安全性的要求:
  • 互斥性:在任何给定时刻,只有一个客户端可以持有一个锁

  • 释放死锁:即使锁定资源的客户端崩溃或被分区,也可以释放锁

  • 容错性:只要大多数Redis节点都在运行,客户端就能够获取和释放锁。

为什么基于故障转移实现的Redis分布式锁还不够用?

官网中举了一个例子:

客户端A获得主服务器上的锁,然后主服务器向从服务器复制数据的过程中崩了,导致数据没有复制到从数据库中,这时会在从服务器中选出来一个升级为主服务器,但新的主服务器中并没有客户端A设置的锁。所以客户端B也可以获取到锁,违背了上面说的互斥性

这就解释为什么需要RedLock算法

RedLock算法

假设有5个完全独立的Redis服务器,多节点Redis实现的RedLock算法如下

  • 获取当前时间戳

  • 客户端尝试在5个实例中按顺序获取锁,在所有实例中使用相同的键名和随机值。当在每个实例中设置锁时,需要将锁的获取时间设置为比锁过期短很多。例如,如果锁自动释放时间为10秒,则锁的获取时间在5-50毫秒。这是为了不要过长时间等待已经关闭的Redis实例,如果一个Redis实例不可用,我们应该尽快尝试获取下一个Redis实例的锁。

  • 客户端通过从当前时间中减去步骤1中获得的时间戳,计算出获取锁所需的时间。当且仅当客户端能够在大多数实例(至少3个)中获得锁,并且花费在获取锁的总时间小于锁的有效性时间时,该锁被认为已经获得。

  • 如果获得了锁,锁真正的有效时间为锁初始设置的有效时间(过期时间)减去第三步的时间,例如,锁初始有限时间为5s,获取锁花了0.5s,则锁真正的有效时间为4.5s(忽略了时钟漂移,时间漂移指指两个电脑间时间流速基本相同的情况下,两个电脑(或两个进程间)时间的差值)

  • 如果客户端由于某些原因无法获得锁(要么无法锁定N/2+1个Redis实例,要么有锁的有效时间为负数),客户端将尝试解锁所有Redis实例(即使是它认为无法锁定的Redis实例)。

RedLock算法是异步的吗?

可以看成同步算法,虽然没有跨进程的同步时钟,但每个进程(多个电脑)的本地时间仍然大致以相同的速度流动,与锁的自动释放时间相比,误差较小,将其忽略的话,则可以看成同步算法。

RedLock失败重试

当客户端无法获取到锁时,应该在随机时间后重试,并且理想的客户端应该并发地将所有命令同时发给所有Redis实例。对于已经获取锁的客户端要在完成任务后及时释放锁,这样其他客户端就不需要等锁自动过期后再获取。如果在获取锁后,在主动释放锁前无法连接到Redis实例,就只能等锁自动失效了。

释放锁

释放锁很简单,只要释放所有实例中的锁,不需要考虑是否释放成功(释放时会判断这个锁的value值是不是自己设置的,避免释放其他客户端设置的锁)

RedLock的 Safety arguments
  • 假设客户端可以获取到大多数Redis实例,并且所有Redis实例具有相同的key和过期时间,但不同的Redis实例的key是不同的时间设置的(获取锁的时间不可能完全一致),所以过期时间也不同,假设获取第一个Redis实例的锁的时间为T1,最后一个为T2,则客户端获得锁的最小有效时间为key的有效时间-(T2-T1)-时钟漂移。

  • 为什么需要获取一半以上的Redis实例的锁才算获取到锁成功呢?因为如果获取不到一半也算成功的话会导致多个客户端同时获取到锁,违背了互斥性

  • 一个客户端锁定大多数Redis实例所需的时间大于或者接近锁的过期时间时,会认为锁无效,并解锁所有Redis实例

RedLock崩溃的相关解决方法

场景:客户端A在成功获取锁后,如果所有Redis重启,这时客户端B就可以再次获取到锁,违背了互斥性

解决方法:开启AOF持久化,可以解决这个问题,但是AOF同步到磁盘上的方式默认是每秒一次,如果1秒内断电,会导致1秒内的数据丢失,如果客户端是在这1秒内获得的锁,立即重启可能会导致锁的互斥性失效,解决方法是每次Redis无论因为什么原因停掉都要等key的过期时间到了再重启(延迟重启),这么做的缺点就是在等待重启这段时间内Redis处于关闭的状态。

最后,上面的方案6还有其他问题吗?其实还是有的,不过还有一种更适合Java的强大方案是Redisson,有兴趣的小伙伴可以去了解下

Redis并发竞争key问题应该如何解决?

Redis并发竞争key就是多个客户端操作一个key,可能会导致数据出现问题,主要有以下几种解决办法:

  • 乐观锁watch 命令可以方便地实现乐观锁。watch 命令会监视给定的每一个key,当 exec 时如果监视的任一个key自从调用watch后发生过变化,则整个事务会回滚,不执行任何动作。不能在分片集群中使用

  • 分布式锁,适合分布式场景

  • 时间戳,适合有序场景,比如A想把key设置为1,B想把key设置为2,C想把key设置为3,对每个操作加上时间戳,写入前先比较自己的时间戳是不是早于现有记录的时间戳,如果早于,就不写入了

  • 消息队列,串行化处理

什么是RedLock

见上文Redis实现分布式锁的方案6

Redis的缓存问题

说下什么是缓存雪崩、缓存穿透、缓存击穿,及它们的解决方案

这是一个非常高频的面试题,也非常容易掌握,比较麻烦的是总是分不清这三个哪个是哪个

下图是一个正常的系统架构图,其中缓存的作用是减轻数据库的压力,提升系统的性能,无论是缓存雪崩、缓存击穿还是缓存穿透都是缓存失效了导致数据库压力过大。

缓存雪崩

什么是缓存雪崩?

缓存雪崩是指在某一个时刻出现大规模的缓存失效的情况,大量的请求直接打在数据库上面,可能会导致数据库宕机,如果这时重启数据库并不能解决根本问题,会再次造成缓存雪崩。

为什么会造成缓存雪崩?

一般来说,造成缓存雪崩主要有两种可能

  • Redis宕机了

  • 很多key采取了相同的过期时间

如何解决缓存雪崩?
  • 为避免Redis宕机造成缓存雪崩,可以搭建Redis集群

  • 尽量不要设置相同的过期时间,例如可以在原有的过期时间加上随机数

  • 服务降级,当流量到达一定的阈值时,就直接返回“系统繁忙”之类的提示,防止过多的请求打在数据库上,这样虽然难用,但至少可以使用,避免直接把数据库搞挂

缓存击穿

什么是缓存击穿?

缓存雪崩是大规模的key失效,而缓存击穿是一个热点的Key,有大并发集中对其进行访问,突然间这个Key失效了,导致大并发全部打在数据库上,导致数据库压力剧增,这种现象就叫做缓存击穿。

比较经典的例子是商品秒杀时,大量的用户在抢某个商品时,商品的key突然过期失效了,所有请求都到数据库上了。

如何解决缓存击穿
  • 热点key不设置过期时间,避免key过期失效

  • 加锁,如果缓存失效的情况,只有拿到锁才可以查询数据库,降低了在同一时刻打在数据库上的请求,防止数据库宕机,不过这样会导致系统的性能变差。

缓存穿透

什么是缓存穿透

缓存穿透是指用户的请求没有经过缓存而直接请求到数据库上了,比如用户请求的key在Redis中不存在,或者用户恶意伪造大量不存在的key进行请求,都可以绕过缓存,导致数据库压力太大挂掉。

如何解决缓存穿透
  • 参数校验,例如可以对用户id进行校验,直接拦截不合法的用户的请求

  • 缓存空值,如果某个key在Redis中不存在,在数据库中也不存在,则将把这个Key值保存进Redis,设置value=“null”。

  • 布隆过滤器,布隆过滤器可以判断这个key在不在数据库中,特点是如果判断这个key不在数据库中,那么这个key一定不在数据库中,如果判断这个key在数据库中,也不能保证这个key一定在数据库中。就是会有少数的漏网之鱼,造成这种现象的原因是因为布隆过滤器中使用了hash算法,对key进行hash时,不同的key的hash值一定不同,但相同的hash的值不能说明这两个key相同。下面简单介绍下布隆过滤器,这个面试也常问。

布隆过滤器底层使用bit数组存储数据,该数组中的元素默认值是0。

布隆过滤器第一次初始化的时候,会把数据库中所有已存在的key,经过一系列的hash算法计算,算出每个key的位置,并将该位置的值置为1,为了减少哈希冲突的影响,可以对每个key进行多次hash计算,如下图

现在,用户所有的请求都要经过布隆过滤器过滤一遍,如果只有用户请求的key的hash值都是1才可以通过,否则直接拦截,如下图

那使用布隆过滤器就可以完美解决问题了吗?当然没有,使用布隆过滤器解决缓存穿透问题的同时也带来了一些其他问题:

  1. 布隆过滤器存在误判的情况

  2. 布隆过滤器不支持删除,因为布隆过滤器中存的1可能涉及多个key,直接删除可能会影响到其他key,比如上图第四个位置的1就涉及两个key

  3. 如果数据库中数据更新同步到布隆过滤器时失败,布隆过滤器则会将本来正常的请求拦截住,这是非常致命的

先来看第一个问题,前面已经解释过了布隆过滤器存在误判的原因,就是不同的key的hash值可能相同。因为每个key要经过多次hash计算,恰好每次hash计算都和其他key的hash值相同的概率是很低的,有少数的漏网之鱼通过了布隆过滤器也不要紧,所以第一个问题不必担心。如果想要减少hash冲突导致的误判,可以适当增加key的hash次数。

第二个问题可以在布隆过滤器中以计数的方式存储,如下图

上图中位置4被key1和key2的hash值覆盖,则为2,如果删除key2,则为下图

第三个问题出现概率不大,如果这种问题对业务影响很大,可以考虑其他解决缓存穿透的方法。

最后,最近建了一个微信求职交流群,欢迎大家进群交流,需先添加微信好友(备注进群),然后拉你进群哈

据说没几个人会看到最后,确定不点个赞再走吗

  大数据 最新文章
实现Kafka至少消费一次
亚马逊云科技:还在苦于ETL?Zero ETL的时代
初探MapReduce
【SpringBoot框架篇】32.基于注解+redis实现
Elasticsearch:如何减少 Elasticsearch 集
Go redis操作
Redis面试题
专题五 Redis高并发场景
基于GBase8s和Calcite的多数据源查询
Redis——底层数据结构原理
上一篇文章      下一篇文章      查看所有文章
加:2022-02-09 20:46:17  更:2022-02-09 20:46:25 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 12:42:28-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码