Java面试之Redis篇(offer 拿来吧你)

现在关于Java面试的资料是层出不穷,对于选择困难症的同学来说,无疑是陷入了一次次的抉择与不安中,担心错过了关键内容,现在小曾哥秉持着"融百家之所长,汇精辟之文档"的思想,整理一下目前主流的一些八股文,以达到1+1 > 2 的效果!

文章目录

    • 1、谈下你对 Redis 的了解?
    • 2、Redis和传统的关系型数据库有什么不同?
    • 3、Redis 为什么这么快?
    • 4、缓存穿透、缓存击穿、缓存雪崩有什么区别,该如何解决?
      • 4.1、缓存穿透
      • 4.2、缓存击穿
      • 4.3、缓存雪崩
    • 5、如何保证缓存与数据库的双写一致性?
      • 5.1、四种同步策略
      • 5.2、考虑问题
        • 5.2.1、更新缓存还是删除缓存
        • 5.2.2、操作数据库还是缓存
        • 最终结论
    • 6、说一说Redis的持久化策略
      • 6.1、持久化
      • 6.2、什么是 RDB 持久化?
      • 6.3、什么是 AOF 持久化?
      • 6.4、什么是 RDB-AOF混合持久化?
    • 7、Redis在持久化时fork出一个子进程,这时已经有两个进程了,怎么能说是单线程呢?
    • 8、Redis的过期策略
    • 9、Redis的缓存淘汰策略
    • 10、如何实现Redis的高可用?
      • 10.1、哨兵
        • 10.1.1 概念
        • 10.1.2 特点
        • 10.1.3 执行流程
        • 10.1.4 哨兵的作用
      • 10.2、集群
        • 10.2.1、概念
        • 10.2.3、Redis集群的通信方案
    • 11、Redis内存碎片?
      • 11.1、什么是内存碎片?
      • 11.2、产生内存碎片的原因?
      • 11.3、如何解决?
    • 12、分布式锁
      • 12.1、什么是分布式锁?
      • 12.2、常见的分布式锁有哪些解决方案?
        • 12.2.1、基于关系型数据库
        • 12.2.2、基于Redis实现
        • 12.2.3、基于zookeeper

1、谈下你对 Redis 的了解?

Redis 就是一个使用 C 语言开发的数据库,不过与传统数据库不同的是 Redis 的数据是存在内存中的 ,也就是它是内存数据库,所以读写速度非常快,因此 Redis 被广泛应用于缓存方向。

数据类型

1、Redis支持5种核心的数据类型,分别是字符串、哈希、列表、集合、有序集合;

  • string 字符串:字符串类型是Redis最基础的数据结构;字符串类型实际上可以是字符串:简单的字符串、XML、JSON;数字:整数、浮点数; 二进制:图片、音频、视频。
  • Hash(哈希):在Redis中哈希类型是指键本身是一-种键 值对结构,如value={{field1,le…{fieldN,valueN}}
  • List(列表):在Redis中,可以队列表两端插入和弹出,还可以获取指定范围的元素列表、获取指定索引下的元素等,列表是一种比较灵活的数据结构,它可以充当栈和队列的角色。
  • Set(集合):集合类型也是用来保存多个字符串的元素,但和列表不同的是集合中不允许有重复的元素,并且集合中的元素是无序的,不能通过索引下标获取元素,Redis 除了支持集合内的增删改查,同时还支持多个集合取交集、并集、差集。
  • zset(sorted set:有序集合):有序集合和集合有着必然的联系,它保留了集合不能有重复成员的特性,但不同得是,有序集合中的元素是可以排序的,但是它和列表的使用索引下标作为排序依据不同的是: 它给每个元素设置一个分数,作为排序的依据。

2、Redis还支持3 种特殊数据结构 :HyperLogLogs(基数统计)、Bitmap (位存储)、Geospatial (地理位置)。

  • Bitmap (位存储):Bitmap 存储的是连续的二进制数字(0 和 1),通过 Bitmap, 只需要一个 bit 位来表示某个元素对应的值或者状态,key 就是对应元素本身。

可以将 Bitmap 看作是一个存储二进制数字(0 和 1)的数组,数组中每个元素的下标叫做offset(偏移量)。
Java面试之Redis篇(offer 拿来吧你)_第1张图片

  • HyperLogLogs(基数统计):HyperLogLog 是一种有名的基数计数概率算法,Redis 提供的 HyperLogLog 占用空间非常非常小,只需要 12k 的空间就能存储接近2^64个不同元素;【稀疏矩阵 :计数较少的时候,占用空间很小;稠密矩阵 :计数达到某个阈值的时候,占用 12k 的空间】
  • Geospatial index(地理空间索引,简称 GEO) :主要用于存储地理位置信息,基于 Sorted Set 实现。通过 GEO 我们可以轻松实现两个位置距离的计算、获取指定位置附近的元素等功能。

应用场景:缓存、排行榜、计数器/限速器(统计在线人数/浏览量/播放量)、下好友关系(点赞/共同好友)、简单的消息队列(订阅发布)、Session服务器

1.缓存:减轻MySQL的查询压力,提升系统性能;
2.排行榜:利用Redis的SortSet (有序集合)实现;
3.计算器限速器:利用Redis中原子性的自增操作,我们可以统计类似用户点赞数、用户访问数等。这类操作如果用MySQL,频繁的读写会带来相当大的压力;限速器比较典型的使用场景是限制某个用户访问某个API的频率,常用的有抢购时,防止用户疯狂点击带来不必要的压力;
4.好友关系:利用集合的一-些命令,比如求交集、并集、差集等。可以方便解决一些共同好友、共同爱好之类的功能;
5.消息队列:除了Redis自身的发布/订阅模式,我们也可以利用List来实现一个队列机制,比如:到货通知、邮件发送之类的需求,不需要高可靠,但是会带来非常大的DB压力,完全可以用List来完成异步解耦;
6.Session 共享: Session是保存在服务器的文件中,如果是集群服务,同一个用户过来可能落在不同机器上,这就会导致用户频繁登陆;采用Redis保存Session后,无论用户落在那台机器上都能够获取到对应的Session信息。

2、Redis和传统的关系型数据库有什么不同?

  • Redis是一种基于键值对的NoSQL数据库,而键值对的值是由多种数据结构和算法组成的。关系型数据库是基于二维数据表来存储数据的,它的数据格式更为严谨,并支持关系查询。
  • Redis的数据都存储于内存中,因此它的速度惊人,读写性能可达10万/秒,远超关系型数据库。关系型数据库的数据存储于磁盘上,可以存放海量的数据,但性能远不如Redis。

3、Redis 为什么这么快?

  1. 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速;
  2. 数据结构简单,对数据操作也简单;
  3. 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
  4. 使用多路I/O复用模型,非阻塞I0。
    Java面试之Redis篇(offer 拿来吧你)_第2张图片
    Redis的单线程架构实现

4、缓存穿透、缓存击穿、缓存雪崩有什么区别,该如何解决?

4.1、缓存穿透

问题描述:客户端查询根本不存在的数据使得请求直达存储层,导致其负载过大,甚至宕机。出现这种情况的原因,可能是业务层误将缓存和库中的数据删除了,也可能是有人恶意攻击,专门访问库中不存在的数据。
解决方案:

  • 缓存空对象:存储层未命中后,仍然将空值存入缓存层,客户端再次访问数据时,缓存层会直接返回空值。
  • 布隆过滤器:将数据存入布隆过滤器,访问缓存之前以过滤器拦截,若请求的数据不存在则直接返回空值。

4.2、缓存击穿

问题描述:一份热点数据,它的访问量非常大。在其缓存失效的瞬间,大量请求直达存储层,导致服务崩溃。

解决方案

  • 永不过期:热点数据不设置过期时间,所以不会出现上述问题,这是“物理”上的永不过期。或者为每个数据设置逻辑过期时间,当发现该数据逻辑过期时,使用单独的线程重建缓存。
  • 加互斥锁:对数据的访问加互斥锁,当一个线程访问该数据时,其他线程只能等待。这个线程访问过后,缓存中的数据将被重建,届时其他线程就可以直接从缓存中取值。

4.3、缓存雪崩

问题描述
在某一时刻,缓存层无法继续提供服务,导致所有的请求直达存储层,造成数据库宕机。可能是缓存中有大量数据同时过期,也可能是Redis节点发生故障,导致大量请求无法得到处理。

举个例子 :秒杀开始 12 个小时之前,我们统一存放了一批商品到 Redis 中,设置的缓存过期时间也是 12 个小时,那么秒杀开始的时候,这些秒杀的商品的访问直接就失效了。导致的情况就是,相应的请求直接就落到了数据库上,就像雪崩一样可怕。

解决方案

  • 避免数据同时过期:设置过期时间时,附加一个随机数,避免大量的key同时过期。
  • 启用降级和熔断措施:在发生雪崩时,若应用访问的不是核心数据,则直接返回预定义信息/空值/错误信息。或者在发生雪崩时,对于访问缓存接口的请求,客户端并不会把请求发给Redis,而是直接返回。
  • 构建高可用的Redis服务:采用哨兵或集群模式,部署多个Redis实例,个别节点宕机,依然可以保持服务的整体可用。

5、如何保证缓存与数据库的双写一致性?

5.1、四种同步策略

想要保证缓存与数据库的双写一致,一共有4种方式,即4种同步策略:

  • 先更新缓存,再更新数据库;
  • 先更新数据库,再更新缓存;
  • 先删除缓存,再更新数据库;
  • 先更新数据库,再删除缓存。

5.2、考虑问题

从这4种同步策略中,我们需要作出比较的是:

  • 更新缓存与删除缓存哪种方式更合适?
  • 应该先操作数据库还是先操作缓存?

5.2.1、更新缓存还是删除缓存

  • 更新缓存
    优点:每次数据变化都及时更新缓存,所以查询时不容易出现未命中的情况。
    缺点:更新缓存的消耗比较大。如果数据需要经过复杂的计算再写入缓存,那么频繁的更新缓存,就会影响服务器的性能。如果是写入数据频繁的业务场景,那么可能频繁的更新缓存时,却没有业务读取该数据。
  • 删除缓存
    优点:操作简单,无论更新操作是否复杂,都是将缓存中的数据直接删除。
    缺点:删除缓存后,下一次查询缓存会出现未命中,这时需要重新读取一次数据库。

从上面的比较来看,一般情况下,删除缓存是更优的方案

5.2.2、操作数据库还是缓存

首先,我们将先删除缓存与先更新数据库,在出现失败时进行一个对比:
Java面试之Redis篇(offer 拿来吧你)_第3张图片
如上图,是先删除缓存再更新数据库,在出现失败时可能出现的问题:

  1. 进程A删除缓存成功;
  2. 进程A更新数据库失败;
  3. 进程B从缓存中读取数据;
  4. 由于缓存被删,进程B无法从缓存中得到数据,进而从数据库读取数据;
  5. 进程B从数据库成功获取数据,然后将数据更新到了缓存。

最终,缓存和数据库的数据是一致的,但仍然是旧的数据。而我们的期望是二者数据一致,并且是新的数据。
Java面试之Redis篇(offer 拿来吧你)_第4张图片如上图,是先更新数据库再删除缓存,在出现失败时可能出现的问题:
1、进程A更新数据库成功;
2、进程A删除缓存失败;
3、进程B读取缓存成功,由于缓存删除失败,所以进程B读取到的是旧的数据。

最终,缓存和数据库的数据是不一致的。
经过上面的比较,我们发现在出现失败的时候,是无法明确分辨出先删缓存和先更新数据库哪个方式更好,以为它们都存在问题。后面我们会进一步对这两种方式进行比较,但是在这里我们先探讨一下,上述场景出现的问题,应该如何解决呢?

实际上,无论上面我们采用哪种方式去同步缓存与数据库,在第二步出现失败的时候,都建议采用重试机制解决,因为最终我们是要解决掉这个错误的。而为了避免重试机制影响主要业务的执行,一般建议重试机制采用异步的方式执行,如下图:
Java面试之Redis篇(offer 拿来吧你)_第5张图片
这里我们按照先更新数据库,再删除缓存的方式,来说明重试机制的主要步骤:

  1. 更新数据库成功;
  2. 删除缓存失败;
  3. 将此数据加入消息队列;
  4. 业务代码消费这条消息;
  5. 业务代码根据这条消息的内容,发起重试机制,即从缓存中删除这条记录。

下面我们再将先删缓存与先更新数据库,在没有出现失败时进行对比:
Java面试之Redis篇(offer 拿来吧你)_第6张图片
如上图,是先删除缓存再更新数据库,在没有出现失败时可能出现的问题:

  1. 进程A删除缓存成功;
  2. 进程B读取缓存失败;
  3. 进程B读取数据库成功,得到旧的数据;
  4. 进程B将旧的数据成功地更新到了缓存;
  5. 进程A将新的数据成功地更新到数据库。

可见,进程A的两步操作均成功,但由于存在并发,在这两步之间,进程B访问了缓存。最终结果是,缓存中存储了旧的数据,而数据库中存储了新的数据,二者数据不一致。

Java面试之Redis篇(offer 拿来吧你)_第7张图片
如上图,是先更新数据库再删除缓存,再没有出现失败时可能出现的问题:

  1. 进程A更新数据库成功;
  2. 进程B读取缓存成功;
  3. 进程A更新数据库成功。

可见,最终缓存与数据库的数据是一致的,并且都是最新的数据。但进程B在这个过程里读到了旧的数据,可能还有其他进程也像进程B一样,在这两步之间读到了缓存中旧的数据,但因为这两步的执行速度会比较快,所以影响不大。对于这两步之后,其他进程再读取缓存数据的时候,就不会出现类似于进程B的问题了。

最终结论

经过对比你会发现,先更新数据库、再删除缓存是影响更小的方案。如果第二步出现失败的情况,则可以采用重试机制解决问题。

6、说一说Redis的持久化策略

6.1、持久化

持久化:就是把内存的数据写到磁盘中去,防止服务宕机了(重启机器、机器故障、系统故障等情况)内存数据的丢失。

Redis支持三种持久化操作:快照(snapshotting,RDB)、追加文件(append-only file, AOF)、RDB-AOF混合持久化。

6.2、什么是 RDB 持久化?

RDB(Redis Database):是Redis默认采用的持久化方式,它以快照的形式将进程数据持久化到硬盘中。RDB会创建一个经过压缩的二进制文件,文件以“.rdb”结尾,内部存储了各个数据库的键值对数据等信息。

RDB持久化的触发方式

  • 手动触发:通过SAVE或BGSAVE命令触发RDB持久化操作,创建“.rdb”文件;
  • 自动触发:通过配置选项,让服务器在满足指定条件时自动执行BGSAVE命令。

SAVE命令执行期间,Redis服务器将阻塞,直到“.rdb”文件创建完毕为止。
而BGSAVE命令是异步版本的SAVE命令,它会使用Redis服务器进程的子进程,创建“.rdb”文件。BGSAVE命令在创建子进程时会存在短暂的阻塞,之后服务器便可以继续处理其他客户端的请求。
BGSAVE命令是针对SAVE阻塞问题做的优化,Redis内部所有涉及RDB的操作都采用BGSAVE的方式,而SAVE命令已经废弃!
BGSAVE命令的执行流程,如下图:

Java面试之Redis篇(offer 拿来吧你)_第8张图片
BGSAVE命令的原理,如下图:Java面试之Redis篇(offer 拿来吧你)_第9张图片

RDB持久化的优缺点

  • 优点:RDB生成紧凑压缩的二进制文件,体积小,使用该文件恢复数据的速度非常快;
  • 缺点:BGSAVE每次运行都要执行fork操作创建子进程,属于重量级操作,不宜频繁执行

6.3、什么是 AOF 持久化?

AOF(Append Only File):解决了数据持久化的实时性,是目前Redis持久化的主流方式。AOF以独立日志的方式,记录了每次写入命令,重启时再重新执行AOF文件中的命令来恢复数据。
工作流程:命令写入(append)、文件同步(sync)、文件重写(rewrite)、重启加载(load)Java面试之Redis篇(offer 拿来吧你)_第10张图片
默认情况: Redis 没有开启 AOF(append only file)方式的持久化,可以通过 appendonly 参数开启:appendonly yes

AOF持久化的文件同步机制:为了提高程序的写入性能,现代操作系统会把针对硬盘的多次写操作优化为一次写操作。【调用write写入命令时–>存入内存缓冲区–>到达指定时间周期执行Flush操作–> 写入硬盘中】

具体操作:Redis向用户提供了appendfsync选项,来控制系统冲洗AOF的频率

appendfsync always    #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
appendfsync everysec  #每秒钟同步一次,显式地将多个写命令同步到硬盘
appendfsync no        #让操作系统决定何时进行同步

AOF持久化的优缺点

  • 优点:与RDB持久化可能丢失大量的数据相比,AOF持久化的安全性要高很多。通过使用everysec选项,用户可以将数据丢失的时间窗口限制在1秒之内。
  • 缺点:AOF文件存储的是协议文本,它的体积要比二进制格式的”.rdb”文件大很多。AOF需要通过执行AOF文件中的命令来恢复数据库,其恢复速度比RDB慢很多。AOF在进行重写时也需要创建子进程,在数据库体积较大时将占用大量资源,会导致服务器的短暂阻塞。

AOF以文本协议格式写入命令 :*3\r\n$3\r\nset\r\n$5\r\nhello\r\n$5\r\nworld\r\n
1、文本协议具有很好的兼容性;2、直接采用文本协议格式,可以避免二次处理的开销;3、文本协议具有可读性,方便直接修改和处理。

6.4、什么是 RDB-AOF混合持久化?

Redis从4.0开始引入RDB-AOF混合持久化模式,这种模式是基于AOF持久化构建而来的。用户可以通过配置文件中的“aof-use-rdb-preamble yes”配置项开启AOF混合持久化,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头,用户可以同时获得RDB持久化和AOF持久化的优点,服务器既可以通过AOF文件包含的RDB数据来实现快速的数据恢复操作,又可以通过AOF文件包含的AOF数据来将丢失数据的时间窗口限制在1s之内。

7、Redis在持久化时fork出一个子进程,这时已经有两个进程了,怎么能说是单线程呢?

Redis是单线程的,主要是指Redis的网络IO和键值对读写是由一个线程来完成的。而Redis的其他功能,如持久化、异步删除、集群数据同步等,则是依赖其他线程来执行的。

8、Redis的过期策略

Redis支持如下两种过期策略:

  • 惰性删除:客户端访问一个key的时候,Redis会先检查它的过期时间,如果发现过期就立刻删除这个key。
  • 定期删除:Redis会将设置了过期时间的key放到一个独立的字典中,并对该字典进行每秒10次的过期扫描,过期扫描不会遍历字典中所有的key,而是采用了一种简单的贪心策略。该策略的删除逻辑如下:

1、从过期字典中随机选择20个key;
2、删除这20个key中已过期的key;
3、如果已过期key的比例超过25%,则重复步骤1。

9、Redis的缓存淘汰策略

当写入数据将导致超出maxmemory限制时,Redis会采用maxmemory-policy所指定的策略进行数据淘汰,该策略一共包含如下8种选项:
Java面试之Redis篇(offer 拿来吧你)_第11张图片
其中,volatile前缀代表从设置了过期时间的键中淘汰数据,allkeys前缀代表从所有的键中淘汰数据。关于后缀,ttl代表选择过期时间最小的键,random代表随机选择键,需要我们额外关注的是lru和lfu后缀,它们分别代表采用lru算法和lfu算法来淘汰数据。

LRU(Least Recently Used)是按照最近最少使用原则来筛选数据,即最不常用的数据会被筛选出来!
标准LRU:把所有的数据组成一个链表,表头和表尾分别表示MRU和LRU端,即最常使用端和最少使用端。刚被访问的数据会被移动到MRU端,而新增的数据也是刚被访问的数据,也会被移动到MRU端。当链表的空间被占满时,它会删除LRU端的数据。

近似LRU:Redis会记录每个数据的最近一次访问的时间戳(LRU)。Redis执行写入操作时,若发现内存超出maxmemory,就会执行一次近似LRU淘汰算法。近似LRU会随机采样N个key,然后淘汰掉最旧的key,若淘汰后内存依然超出限制,则继续采样淘汰。可以通maxmemory_samples配置项,设置近似LRU每次采样的数据个数,该配置项的默认值为5。
LRU算法的不足之处在于,若一个key很少被访问,只是刚刚偶尔被访问了一次,则它就被认为是热点数据,短时间内不会被淘汰。

10、如何实现Redis的高可用?

实现Redis的高可用,主要有哨兵和集群两种方式。

持久化:持久化是最简单的高可用方法(有时甚至不被归为高可用的手段),主要作用是数据备份,即将数据存储在硬盘,保证数据不会因进程退出而丢失。
复制:复制是高可用Redis的基础,哨兵和集群都是在复制基础上实现高可用的。复制主要实现了数据的多机备份,以及对于读操作的负载均衡和简单的故障恢复。缺陷:故障恢复无法自动化;写操作无法负载均衡;存储能力受到单机的限制。
哨兵:在复制的基础上,哨兵实现了自动化的故障恢复。缺陷:写操作无法负载均衡;存储能力受到单机的限制。
集群:通过集群,Redis解决了写操作无法负载均衡,以及存储能力受到单机限制的问题,实现了较为完善的高可用方案。

10.1、哨兵

10.1.1 概念

概念:哨兵(sentinel)是一个分布式系统,用于对主从结构中的每台服务器进行监控,当出现故障时通过投票机制选择新的master并将所有slave连接到新的master。
机制:Sentinel(哨兵)它包含若干个哨兵节点和数据节点。每个哨兵节点会对数据节点(master、slave)和其余的哨兵节点进行监控,当发现节点不可达时,会对节点做下线标识。如果被标识的是主节点(master),它就会与其他的哨兵节点进行协商,当多数哨兵节点都认为主节点(master)不可达时,它们便会选举出一个哨兵节点来完成自动故障转移的工作,同时还会将这个变化实时地通知给应用方。整个过程是自动的,不需要人工介入,有效地解决了Redis的高可用问题!
一组哨兵可以监控一个主节点,也可以同时监控多个主节点,两种情况的拓扑结构如下图:Java面试之Redis篇(offer 拿来吧你)_第12张图片

10.1.2 特点

  1. 哨兵节点会定期监控数据节点,其他哨兵节点是否可达;
  2. 哨兵节点会将故障转移的结果通知给应用方;
  3. 哨兵节点可以将从节点晋升为主节点,并维护后续正确的主从关系;
  4. 哨兵模式下,客户端连接的是哨兵节点集合,从中获取主节点信息;
  5. 节点的故障判断是由多个哨兵节点共同完成的,可有效地防止误判;
  6. 哨兵节点集合是由多个哨兵节点组成的,即使个别哨兵节点不可用,整个集合依然是健壮的;
  7. 哨兵节点也是独立的Redis节点,是特殊的Redis节点,它们不存储数据,只支持部分命令。

10.1.3 执行流程

  1. 每个Sentinel以每秒钟一次的频率向它所知的Master, Slave以及其他Sentinel实例发送一个PING命令。
  2. 如果一个实例(instance) 距离最后一次有效回复PING命令的时间超过down-after-milliseconds选项所指定的值,则这个实例会被当前Sentinel标记为主观下线
  3. 如果一个Master被标记为主观下线,则正在监视这个Master的所有Sentinel要以每秒一次的频率确认Master的确进入了主观下线状态。
  4. 当有足够数量的Sentinel (大于等于配置文件指定的值)在指定的时间范围内确认Master的确进入了 主观下线状态,则Master会被标记为客观下线 。
  5. 当Master被Sentinel标记为客观下线时,Sentinel 向下线的Master的所有Slave发送INFO命令的频率会从10秒一次改为每秒一次(在一 般情况下,每个Sentinel会以每10秒一次的频率向它已知的所有Master, Slave发送 INFO命令)。
  6. 若没有足够数量的Sentinel同意Master已经下线,Master 的客观下线状态就会变成主观下线。若Master 重新向Sentinel的PING命令返回有效回复,Master 的主观下线状态就会被移除。
  7. sentinel节点会与其他sentinel节点进行”沟通",投票选举一个sentinel节 点进行故障处理,在从节点中选取一个主节点,其他从节点挂载到新的主节点上自动复制新主节点的数据。

选择新的主节点多标准是:跟master断开连接的时长(>10 * down-after-milliseconds 就不适合)、slave优先级、复制offset(哪个slave复制了越多的数据,offset越靠后, 优先级就越高)、run id(选择id值较小的slave)

10.1.4 哨兵的作用

  • 监控
    不断地检查master和slave是否正常运行
    master存活监测、master与slave运行情况检测
  • 通知(提醒)
    当被监控的服务器出现问题时,向其他(哨兵间、客户端)发送通知
    自动故障转移断开与master与slave连接。选举一个slave作为master,将其他slave连接到新的master,并告知客户端新的服务器地址
  • 注意
    哨兵也是一台redis服务器,只是不提供数据服务
    通常哨兵配置数量为单数

10.2、集群

10.2.1、概念

Redis集群采用虚拟槽分区来实现数据分片,它把所有的键根据哈希函数映射到0-16383整数槽内,计算公式为slot=CRC16(key)&16383,每一个节点负责维护一部分槽以及槽所映射的键值数据。
Java面试之Redis篇(offer 拿来吧你)_第13张图片

Redis集群中数据的分片逻辑
#### 10.2.2、虚拟槽分区特点 1. 解耦数据和节点之间的关系,简化了节点扩容和收缩的难度; 2. 节点自身维护槽的映射关系,不需要客户端或者代理服务维护槽分区元数据; 3. 支持节点、槽、键之间的映射查询,用于数据路由,在线伸缩等场景。

10.2.3、Redis集群的通信方案

元数据是指:节点负责哪些数据,是否出现故障等状态信息;常见的元数据维护方式分为:集中式和P2P方式
Gossip协议的工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息,这种方式类似流言传播。

Redis 集群节点间采取gossip协议进行通信,所有节点都持有一份元数据,不同的节点如果出现了元数据的变更之后不断地将元数据发送给其他节点让其他节点进行数据变更。节点互相之间不断通信,保持整个集群所有节点的数据是完整的。主要交换故障信息、 节点的增加和移除、hashslot信息等。

优点:元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续,打到所有节点上去更新,有一定的延时,降低了压力;
缺点:元数据更新有延时,可能导致集群的一些操作会有一些滞后 。

11、Redis内存碎片?

11.1、什么是内存碎片?

内存碎片:不可用的空闲内存,在操作系统中对于这个内容有很好的解释

操作系统为你分配了 32 字节的连续内存空间,而你存储数据实际只需要使用 24 字节内存空间,那这多余出来的 8 字节内存空间如果后续没办法再被分配存储其他数据的话,就可以被称为内存碎片。
Java面试之Redis篇(offer 拿来吧你)_第14张图片

11.2、产生内存碎片的原因?

Redis 使用 zmalloc 方法(Redis 自己实现的内存分配方法)进行内存分配的时候,除了要分配 size 大小的内存之外,还会多分配 PREFIX_SIZE 大小的内存。

  • Redis 存储存储数据的时候向操作系统申请的内存空间可能会大于数据实际需要的存储空间。
  • 频繁修改 Redis 中的数据也会产生内存碎片。

11.3、如何解决?

直接通过 config set 命令将 activedefrag 配置项设置为 yes 即可。

config set activedefrag yes

12、分布式锁

引出问题:锁在程序中的作用就是同步工具,保证共享资源在同-时刻只能被一个线程访问,Java中的锁我们都很熟悉了,像synchronized、Lock都是我们经常使用的,但是Java的锁只能保证单机的时候有效分布式集群环境就无能为力了,这个时候我们就需要用到分布式锁。

12.1、什么是分布式锁?

分布式锁:就是分布式项目开发中用到的锁,可以用来控制分布式系统之间同步访问共享资源。
思路: 在整个系统提供-个全局、唯一的获取锁的"东西",然后每个系统在需要加锁时,都去问这个“东西”拿到一把锁,这样不同的系统拿到的就可以认为是同一把锁。至于这个“东西",可以是Redis、Zookeeper, 也可以是数据库。
特性

  • 1、互斥性:在任何时刻,对于同一条数据,只有一台应用可以获取到分布式锁;
  • 2、高可用性:在分布式场景下,一小部分服务器宕机不影响正常使用,这种情况就需要将提供分布式锁的服务以集群的方式部署;
  • 3、防止锁超时:如果客户端没有主动释放锁,服务器会在一段时间之后自动释放锁,防止客户端宕机或者网络不可达时产生死锁;
  • 4、独占性:加锁解锁必须由同一台服务器进行,也就是锁的持有者才可以释放锁,不能出现你加的锁,别人给你解锁了。

12.2、常见的分布式锁有哪些解决方案?

12.2.1、基于关系型数据库

基于关系型数据库实现分布式锁,是依赖数据库的唯一性来实现资源锁定,比如主键和唯一索引等 。

缺点:

  • 这把锁强依赖数据库的可用性,数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。
  • 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。
  • 这把锁只能是非阻塞的,因为数据的insert操作, -旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。
  • 这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。

12.2.2、基于Redis实现

分布式锁的三个核心要素:加锁、解锁、锁超时

使用setnx来加锁,key是锁的唯一标识, 按业务来决定命名,value这里 设置为test。setx key test
当得到的锁的线程执行完任务,需要释放锁,使用del指令del key
锁超时知道的是:如果一个得到锁的线程在执行任务的过程中挂掉,来不及显式地释放锁,这块资源将会永远被锁住,别的线程北向进来。expire key 30

优点:

  • Redis锁实现简单,理解逻辑简单,性能好,可以支撑高并发的获取、释放锁操作。

缺点:

  • Redis 容易单点故障,集群部署,并不是强一致性的,锁的不够健壮;
  • key的过期时间设置多少不明确,只能根据实际情况调整;
  • 需要自己不断去尝试获取锁,比较消耗性能。

12.2.3、基于zookeeper

优点:

  • zookeeper天生设计定位就是分布式协调,强一致性,锁很健壮。如果获取不到锁,只需要添加一个监听器就可以了,不用一直轮询,性能消耗较小。

缺点:

  • 在高请求高并发下,系统疯狂的加锁释放锁,最后zk承受不住这么大的压力可能会存在宕机的风险。

Redis内容还有待完善,但是已经实现了从0到1的过程,后续会根据真实面试情况持续更新相关内容,实现从1到100的飞跃,我会继续更新面试板块内容,还请小伙伴们持续关注!

欢迎各位小伙伴们阅读以下内容,定能收获满满!
参考文档:

  • guide哥:https://javaguide.cn/database/mysql/mysql-questions-01.html
  • 牛客网:https://www.nowcoder.com/tutorial/94/36e2c89b4b4641f8b3c566758fdc879d
  • 帅地玩编程-- Java面试必知必会
  • 哨兵:https://blog.csdn.net/wenwenaier/article/details/122270439

你可能感兴趣的:(面试训练营,java,面试,redis,分布式,数据库)