全网最硬核 Redis 高频面试题解析(2024年最新版),2024年最新java面试一般多久出结果

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注Java)
img

正文

事件处理器:事件处理器就是一个个函数, 定义了某个事件发生时, 服务器应该执行的动作。例如:建立连接、命令查询、命令写入、连接关闭等等。

15、Redis 删除过期键的策略(缓存失效策略、数据过期策略)

定时删除:在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。对内存最友好,对 CPU 时间最不友好。

惰性删除:放任键过期不管,但是每次获取键时,都检査键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。对 CPU 时间最优化,对内存最不友好。

定期删除:每隔一段时间,默认100ms,程序就对数据库进行一次检査,删除里面的过期键。至 于要删除多少过期键,以及要检査多少个数据库,则由算法决定。前两种策略的折中,对 CPU 时间和内存的友好程度较平衡。

Redis 使用惰性删除和定期删除。

16、Redis 的内存淘汰(驱逐)策略

当 redis 的内存空间(maxmemory 参数配置)已经用满时,redis 将根据配置的驱逐策略(maxmemory-policy 参数配置),进行相应的动作。

网上很多资料都是写 6 种,但是其实当前 redis 的淘汰策略已经有 8 种了,多余的两种是 Redis 4.0 新增的,基于 LFU(Least Frequently Used)算法实现的。

  • noeviction:默认策略,不淘汰任何 key,直接返回错误

  • allkeys-lru:在所有的 key 中,使用 LRU 算法淘汰部分 key

  • allkeys-lfu:在所有的 key 中,使用 LFU 算法淘汰部分 key,该算法于 Redis 4.0 新增

  • allkeys-random:在所有的 key 中,随机淘汰部分 key

  • volatile-lru:在设置了过期时间的 key 中,使用 LRU 算法淘汰部分 key

  • volatile-lfu:在设置了过期时间的 key 中,使用 LFU 算法淘汰部分 key,该算法于 Redis 4.0 新增

  • volatile-random:在设置了过期时间的 key 中,随机淘汰部分 key

  • volatile-ttl:在设置了过期时间的 key 中,挑选 TTL(time to live,剩余时间)短的 key 淘汰

17、Redis 的 LRU 算法怎么实现的?

Redis 在 redisObject 结构体中定义了一个长度 24 bit 的 unsigned 类型的字段(unsigned lru:LRU_BITS),在 LRU 算法中用来存储对象最后一次被命令程序访问的时间。

具体的 LRU 算法经历了两个版本。

版本1:随机选取 N 个淘汰法。

最初 Redis 是这样实现的:随机选 N(默认5) 个 key,把空闲时间(idle time)最大的那个 key 移除。这边的 N 可通过 maxmemory-samples 配置项修改。

就是这么简单,简单得让人不敢相信了,而且十分有效。

但是这个算法有个明显的缺点:每次都是随机从 N 个里选择 1 个,并没有利用前一轮的历史信息。其实在上一轮移除 key 的过程中,其实是知道了 N 个 key 的 idle time 的情况的,那在下一轮移除 key 时,其实可以利用上一轮的这些信息。这也是 Redis 3.0 的优化思想。

版本2:Redis 3.0 对 LRU 算法进行改进,引入了缓冲池(pool,默认16)的概念。

当每一轮移除 key 时,拿到了 N(默认5)个 key 的 idle time,遍历处理这 N 个 key,如果 key 的 idle time 比 pool 里面的 key 的 idle time 还要大,就把它添加到 pool 里面去。

当 pool 放满之后,每次如果有新的 key 需要放入,需要将 pool 中 idle time 最小的一个 key 移除。这样相当于 pool 里面始终维护着还未被淘汰的 idle time 最大的 16 个 key。

当我们每轮要淘汰的时候,直接从 pool 里面取出 idle time 最大的 key(只取1个),将之淘汰掉。

整个流程相当于随机取 5 个 key 放入 pool,然后淘汰 pool 中空闲时间最大的 key,然后再随机取 5 个 key放入 pool,继续淘汰 pool 中空闲时间最大的 key,一直持续下去。

在进入淘汰前会计算出需要释放的内存大小,然后就一直循环上述流程,直至释放足够的内存。

18、Redis 的持久化机制有哪几种,各自的实现原理和优缺点?

Redis 的持久化机制有:RDB、AOF、混合持久化(RDB+AOF,Redis 4.0引入)。

1)RDB

描述:类似于快照。在某个时间点,将 Redis 在内存中的数据库状态(数据库的键值对等信息)保存到磁盘里面。RDB 持久化功能生成的 RDB 文件是经过压缩的二进制文件。

命令:有两个 Redis 命令可以用于生成 RDB 文件,一个是 SAVE,另一个是 BGSAVE。

开启:使用 save point 配置,满足 save point 条件后会触发 BGSAVE 来存储一次快照,这边的 save point 检查就是在上文提到的 serverCron 中进行。

save point 格式:save ,含义是 Redis 如果在 seconds 秒内数据发生了 changes 次改变,就保存快照文件。例如 Redis 默认就配置了以下3个:

save 900 1 #900秒内有1个key发生了变化,则触发保存RDB文件

save 300 10 #300秒内有10个key发生了变化,则触发保存RDB文件

save 60 10000 #60秒内有10000个key发生了变化,则触发保存RDB文件

关闭:1)注释掉所有save point 配置可以关闭 RDB 持久化。2)在所有 save point 配置后增加:save “”,该配置可以删除所有之前配置的 save point。

save “”

SAVE:生成 RDB 快照文件,但是会阻塞主进程,服务器将无法处理客户端发来的命令请求,所以通常不会直接使用该命令。

BGSAVE:fork 子进程来生成 RDB 快照文件,阻塞只会发生在 fork 子进程的时候,之后主进程可以正常处理请求,详细过程如下图:

fork:在 Linux 系统中,调用 fork() 时,会创建出一个新进程,称为子进程,子进程会拷贝父进程的 page table。如果进程占用的内存越大,进程的 page table 也会越大,那么 fork 也会占用更多的时间。如果 Redis 占用的内存很大,那么在 fork 子进程时,则会出现明显的停顿现象。

RDB 的优点

1)RDB 文件是是经过压缩的二进制文件,占用空间很小,它保存了 Redis 某个时间点的数据集,很适合用于做备份。 比如说,你可以在最近的 24 小时内,每小时备份一次 RDB 文件,并且在每个月的每一天,也备份一个 RDB 文件。这样的话,即使遇上问题,也可以随时将数据集还原到不同的版本。

2)RDB 非常适用于灾难恢复(disaster recovery):它只有一个文件,并且内容都非常紧凑,可以(在加密后)将它传送到别的数据中心。

3)RDB 可以最大化 redis 的性能。父进程在保存 RDB 文件时唯一要做的就是 fork 出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无须执行任何磁盘 I/O 操作。

4)RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。

RDB 的缺点

1)RDB 在服务器故障时容易造成数据的丢失。RDB 允许我们通过修改 save point 配置来控制持久化的频率。但是,因为 RDB 文件需要保存整个数据集的状态, 所以它是一个比较重的操作,如果频率太频繁,可能会对 Redis 性能产生影响。所以通常可能设置至少5分钟才保存一次快照,这时如果 Redis 出现宕机等情况,则意味着最多可能丢失5分钟数据。

2)RDB 保存时使用 fork 子进程进行数据的持久化,如果数据比较大的话,fork 可能会非常耗时,造成 Redis 停止处理服务N毫秒。如果数据集很大且 CPU 比较繁忙的时候,停止服务的时间甚至会到一秒。

3)Linux fork 子进程采用的是 copy-on-write 的方式。在 Redis 执行 RDB 持久化期间,如果 client 写入数据很频繁,那么将增加 Redis 占用的内存,最坏情况下,内存的占用将达到原先的2倍。刚 fork 时,主进程和子进程共享内存,但是随着主进程需要处理写操作,主进程需要将修改的页面拷贝一份出来,然后进行修改。极端情况下,如果所有的页面都被修改,则此时的内存占用是原先的2倍。

2)AOF

描述:保存 Redis 服务器所执行的所有写操作命令来记录数据库状态,并在服务器启动时,通过重新执行这些命令来还原数据集。

开启:AOF 持久化默认是关闭的,可以通过配置:appendonly yes 开启。

关闭:使用配置 appendonly no 可以关闭 AOF 持久化。

AOF 持久化功能的实现可以分为三个步骤:命令追加、文件写入、文件同步。

命令追加:当 AOF 持久化功能打开时,服务器在执行完一个写命令之后,会将被执行的写命令追加到服务器状态的 aof 缓冲区(aof_buf)的末尾。

文件写入与文件同步:可能有人不明白为什么将 aof_buf 的内容写到磁盘上需要两步操作,这边简单解释一下。

Linux 操作系统中为了提升性能,使用了页缓存(page cache)。当我们将 aof_buf 的内容写到磁盘上时,此时数据并没有真正的落盘,而是在 page cache 中,为了将 page cache 中的数据真正落盘,需要执行 fsync / fdatasync 命令来强制刷盘。这边的文件同步做的就是刷盘操作,或者叫文件刷盘可能更容易理解一些。

在文章开头,我们提过 serverCron 时间事件中会触发 flushAppendOnlyFile 函数,该函数会根据服务器配置的 appendfsync 参数值,来决定是否将 aof_buf 缓冲区的内容写入和保存到 AOF 文件。

appendfsync 参数有三个选项

  1. always:每处理一个命令都将 aof_buf 缓冲区中的所有内容写入并同步到AOF 文件,即每个命令都刷盘。

  2. everysec:将 aof_buf 缓冲区中的所有内容写入到 AOF 文件,如果上次同步 AOF 文件的时间距离现在超过一秒钟, 那么再次对 AOF 文件进行同步, 并且这个同步操作是异步的,由一个后台线程专门负责执行,即每秒刷盘1次。

  3. no:将 aof_buf 缓冲区中的所有内容写入到 AOF 文件, 但并不对 AOF 文件进行同步, 何时同步由操作系统来决定。即不执行刷盘,让操作系统自己执行刷盘。

AOF 的优点

  1. AOF 比 RDB可靠。你可以设置不同的 fsync 策略:no、everysec 和 always。默认是 everysec,在这种配置下,redis 仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据。

  2. AOF文件是一个纯追加的日志文件。即使日志因为某些原因而包含了未写入完整的命令(比如写入时磁盘已满,写入中途停机等等), 我们也可以使用 redis-check-aof 工具也可以轻易地修复这种问题。

  3. 当 AOF文件太大时,Redis 会自动在后台进行重写:重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。整个重写是绝对安全,因为重写是在一个新的文件上进行,同时 Redis 会继续往旧的文件追加数据。当新文件重写完毕,Redis 会把新旧文件进行切换,然后开始把数据写到新文件上。

  4. AOF 文件有序地保存了对数据库执行的所有写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非常容易被人读懂, 对文件进行分析(parse)也很轻松。如果你不小心执行了 FLUSHALL 命令把所有数据刷掉了,但只要 AOF 文件没有被重写,那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。

AOF 的缺点

  1. 对于相同的数据集,AOF 文件的大小一般会比 RDB 文件大。

  2. 根据所使用的 fsync 策略,AOF 的速度可能会比 RDB 慢。通常 fsync 设置为每秒一次就能获得比较高的性能,而关闭 fsync 可以让 AOF 的速度和 RDB 一样快。

  3. AOF 在过去曾经发生过这样的 bug :因为个别命令的原因,导致 AOF 文件在重新载入时,无法将数据集恢复成保存时的原样。(举个例子,阻塞命令 BRPOPLPUSH 就曾经引起过这样的 bug ) 。虽然这种 bug 在 AOF 文件中并不常见, 但是相较而言, RDB 几乎是不可能出现这种 bug 的。

3)混合持久化

描述:混合持久化并不是一种全新的持久化方式,而是对已有方式的优化。混合持久化只发生于 AOF 重写过程。使用了混合持久化,重写后的新 AOF 文件前半段是 RDB 格式的全量数据,后半段是 AOF 格式的增量数据。

整体格式为:[RDB file][AOF tail]

开启:混合持久化的配置参数为 aof-use-rdb-preamble,配置为 yes 时开启混合持久化,在 redis 4 刚引入时,默认是关闭混合持久化的,但是在 redis 5 中默认已经打开了。

关闭:使用 aof-use-rdb-preamble no 配置即可关闭混合持久化。

混合持久化本质是通过 AOF 后台重写(bgrewriteaof 命令)完成的,不同的是当开启混合持久化时,fork 出的子进程先将当前全量数据以 RDB 方式写入新的 AOF 文件,然后再将 AOF 重写缓冲区(aof_rewrite_buf_blocks)的增量命令以 AOF 方式写入到文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。

优点:结合 RDB 和 AOF 的优点, 更快的重写和恢复。

缺点:AOF 文件里面的 RDB 部分不再是 AOF 格式,可读性差。

19、为什么需要 AOF 重写

AOF 持久化是通过保存被执行的写命令来记录数据库状态的,随着写入命令的不断增加,AOF 文件中的内容会越来越多,文件的体积也会越来越大。

如果不加以控制,体积过大的 AOF 文件可能会对 Redis 服务器、甚至整个宿主机造成影响,并且 AOF 文件的体积越大,使用 AOF 文件来进行数据还原所需的时间就越多。

举个例子, 如果你对一个计数器调用了 100 次 INCR , 那么仅仅是为了保存这个计数器的当前值, AOF 文件就需要使用 100 条记录。

然而在实际上, 只使用一条 SET 命令已经足以保存计数器的当前值了, 其余 99 条记录实际上都是多余的。

为了处理这种情况, Redis 引入了 AOF 重写:可以在不打断服务端处理请求的情况下, 对 AOF 文件进行重建(rebuild)。

20、介绍下 AOF 重写的过程、AOF 后台重写存在的问题、如何解决 AOF 后台重写存在的数据不一致问题

描述:Redis 生成新的 AOF 文件来代替旧 AOF 文件,这个新的 AOF 文件包含重建当前数据集所需的最少命令。具体过程是遍历所有数据库的所有键,从数据库读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令。

命令:有两个 Redis 命令可以用于触发 AOF 重写,一个是 BGREWRITEAOF 、另一个是 REWRITEAOF 命令;

开启:AOF 重写由两个参数共同控制,auto-aof-rewrite-percentage 和 auto-aof-rewrite-min-size,同时满足这两个条件,则触发 AOF 后台重写 BGREWRITEAOF。

// 当前AOF文件比上次重写后的AOF文件大小的增长比例超过100

auto-aof-rewrite-percentage 100

// 当前AOF文件的文件大小大于64MB

auto-aof-rewrite-min-size 64mb

关闭:auto-aof-rewrite-percentage 0,指定0的百分比,以禁用自动AOF重写功能。

auto-aof-rewrite-percentage 0

REWRITEAOF:进行 AOF 重写,但是会阻塞主进程,服务器将无法处理客户端发来的命令请求,通常不会直接使用该命令。

BGREWRITEAOF:fork 子进程来进行 AOF 重写,阻塞只会发生在 fork 子进程的时候,之后主进程可以正常处理请求。

REWRITEAOF 和 BGREWRITEAOF 的关系与 SAVE 和 BGSAVE 的关系类似。

AOF 后台重写存在的问题

AOF 后台重写使用子进程进行从写,解决了主进程阻塞的问题,但是仍然存在另一个问题:子进程在进行 AOF 重写期间,服务器主进程还需要继续处理命令请求,新的命令可能会对现有的数据库状态进行修改,从而使得当前的数据库状态和重写后的 AOF 文件保存的数据库状态不一致。

如何解决 AOF 后台重写存在的数据不一致问题

为了解决上述问题,Redis 引入了 AOF 重写缓冲区(aof_rewrite_buf_blocks),这个缓冲区在服务器创建子进程之后开始使用,当 Redis 服务器执行完一个写命令之后,它会同时将这个写命令追加到 AOF 缓冲区和 AOF 重写缓冲区。

这样一来可以保证:

1、现有 AOF 文件的处理工作会如常进行。这样即使在重写的中途发生停机,现有的 AOF 文件也还是安全的。

2、从创建子进程开始,也就是 AOF 重写开始,服务器执行的所有写命令会被记录到 AOF 重写缓冲区里面。

这样,当子进程完成 AOF 重写工作后,父进程会在 serverCron 中检测到子进程已经重写结束,则会执行以下工作:

1、将 AOF 重写缓冲区中的所有内容写入到新 AOF 文件中,这时新 AOF 文件所保存的数据库状态将和服务器当前的数据库状态一致。

2、对新的 AOF 文件进行改名,原子的覆盖现有的 AOF 文件,完成新旧两个 AOF 文件的替换。

之后,父进程就可以继续像往常一样接受命令请求了。

21、RDB、AOF、混合持久,我应该用哪一个?

一般来说, 如果想尽量保证数据安全性, 你应该同时使用 RDB 和 AOF 持久化功能,同时可以开启混合持久化。

如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。

如果你的数据是可以丢失的,则可以关闭持久化功能,在这种情况下,Redis 的性能是最高的。

使用 Redis 通常都是为了提升性能,而如果为了不丢失数据而将 appendfsync 设置为 always 级别时,对 Redis 的性能影响是很大的,在这种不能接受数据丢失的场景,其实可以考虑直接选择 MySQL 等类似的数据库。

22、同时开启RDB和AOF,服务重启时如何加载

简单来说,如果同时启用了 AOF 和 RDB,Redis 重新启动时,会使用 AOF 文件来重建数据集,因为通常来说, AOF 的数据会更完整。

而在引入了混合持久化之后,使用 AOF 重建数据集时,会通过文件开头是否为“REDIS”来判断是否为混合持久化。

完整流程如下图所示:

23、Redis 怎么保证高可用、有哪些集群模式

主从复制、哨兵模式、集群模式。

24、主从复制

在当前最新的 Redis 6.0 中,主从复制的完整过程如下:

1)开启主从复制

通常有以下三种方式:

  • 在 slave 直接执行命令:slaveof

  • 在 slave 配置文件中加入:slaveof

  • 使用启动命令:–slaveof

注:在 Redis 5.0 之后,slaveof 相关命令和配置已经被替换成 replicaof,例如 replicaof 。为了兼容旧版本,通过配置的方式仍然支持 slaveof,但是通过命令的方式则不行了。

2)建立套接字(socket)连接

slave 将根据指定的 IP 地址和端口,向 master 发起套接字(socket)连接,master 在接受(accept) slave 的套接字连接之后,为该套接字创建相应的客户端状态,此时连接建立完成。

3)发送PING命令

slave 向 master 发送一个 PING 命令,以检査套接字的读写状态是否正常、 master 能否正常处理命令请求。

4)身份验证

slave 向 master 发送 AUTH password 命令来进行身份验证。

5)发送端口信息

在身份验证通过后后, slave 将向 master 发送自己的监听端口号, master 收到后记录在 slave 所对应的客户端状态的 slave_listening_port 属性中。

6)发送IP地址

如果配置了 slave_announce_ip,则 slave 向 master 发送 slave_announce_ip 配置的 IP 地址, master 收到后记录在 slave 所对应的客户端状态的 slave_ip 属性。

该配置是用于解决服务器返回内网 IP 时,其他服务器无法访问的情况。可以通过该配置直接指定公网 IP。

7)发送CAPA

CAPA 全称是 capabilities,这边表示的是同步复制的能力。slave 会在这一阶段发送 capa 告诉 master 自己具备的(同步)复制能力, master 收到后记录在 slave 所对应的客户端状态的 slave_capa 属性。

8)数据同步

slave 将向 master 发送 PSYNC 命令, master 收到该命令后判断是进行部分重同步还是完整重同步,然后根据策略进行数据的同步。

9)命令传播

当完成了同步之后,就会进入命令传播阶段,这时 master 只要一直将自己执行的写命令发送给 slave ,而 slave 只要一直接收并执行 master 发来的写命令,就可以保证 master 和 slave 一直保持一致了。

以部分重同步为例,主从复制的核心步骤流程图如下:

25、哨兵

哨兵(Sentinel) 是 Redis 的高可用性解决方案:由一个或多个 Sentinel 实例组成的 Sentinel 系统可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器。

Sentinel 可以在被监视的主服务器进入下线状态时,自动将下线主服务器的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。

1)哨兵故障检测

检查主观下线状态

在默认情况下,Sentinel 会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器、从服务器、其他 Sentinel 在内)发送 PING 命令,并通过实例返回的 PING 命令回复来判断实例是否在线。

如果一个实例在 down-after-miliseconds 毫秒内,连续向 Sentinel 返回无效回复,那么 Sentinel 会修改这个实例所对应的实例结构,在结构的 flags 属性中设置 SRI_S_DOWN 标识,以此来表示这个实例已经进入主观下线状态。

检查客观下线状态

当 Sentinel 将一个主服务器判断为主观下线之后,为了确定这个主服务器是否真的下线了,它会向同样监视这一服务器的其他 Sentinel 进行询问,看它们是否也认为主服务器已经进入了下线状态(可以是主观下线或者客观下线)。

当 Sentinel 从其他 Sentinel 那里接收到足够数量(quorum,可配置)的已下线判断之后,Sentinel 就会将服务器置为客观下线,在 flags 上打上 SRI_O_DOWN 标识,并对主服务器执行故障转移操作。

2)哨兵故障转移流程

当哨兵监测到某个主节点客观下线之后,就会开始故障转移流程。核心流程如下:

  1. 发起一次选举,选举出领头 Sentinel

  2. 领头 Sentinel 在已下线主服务器的所有从服务器里面,挑选出一个从服务器,并将其升级为新的主服务器。

  3. 领头 Sentinel 将剩余的所有从服务器改为复制新的主服务器。

  4. 领头 Sentinel 更新相关配置信息,当这个旧的主服务器重新上线时,将其设置为新的主服务器的从服务器。

26、集群模式

哨兵模式最大的缺点就是所有的数据都放在一台服务器上,无法较好的进行水平扩展。

为了解决哨兵模式存在的问题,集群模式应运而生。在高可用上,集群基本是直接复用的哨兵模式的逻辑,并且针对水平扩展进行了优化。

集群模式具备的特点如下:

  1. 采取去中心化的集群模式,将数据按槽存储分布在多个 Redis 节点上。集群共有 16384 个槽,每个节点负责处理部分槽。

  2. 使用 CRC16 算法来计算 key 所属的槽:crc16(key,keylen) & 16383。

  3. 所有的 Redis 节点彼此互联,通过 PING-PONG 机制来进行节点间的心跳检测。

  4. 分片内采用一主多从保证高可用,并提供复制和故障恢复功能。在实际使用中,通常会将主从分布在不同机房,避免机房出现故障导致整个分片出问题,下面的架构图就是这样设计的。

  5. 客户端与 Redis 节点直连,不需要中间代理层(proxy)。客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。

集群的架构图如下所示:

27、集群选举

故障转移的第一步就是选举出新的主节点,以下是集群选举新的主节点的方法:

1)当从节点发现自己正在复制的主节点进入已下线状态时,会发起一次选举:将 currentEpoch(配置纪元)加1,然后向集群广播一条 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 消息,要求所有收到这条消息、并且具有投票权的主节点向这个从节点投票。

2)其他节点收到消息后,会判断是否要给发送消息的节点投票,判断流程如下:

  1. 当前节点是 slave,或者当前节点是 master,但是不负责处理槽,则当前节点没有投票权,直接返回。

  2. 请求节点的 currentEpoch 小于当前节点的 currentEpoch,校验失败返回。因为发送者的状态与当前集群状态不一致,可能是长时间下线的节点刚刚上线,这种情况下,直接返回即可。

  3. 当前节点在该 currentEpoch 已经投过票,校验失败返回。

  4. 请求节点是 master,校验失败返回。

  5. 请求节点的 master 为空,校验失败返回。

  6. 请求节点的 master 没有故障,并且不是手动故障转移,校验失败返回。因为手动故障转移是可以在 master 正常的情况下直接发起的。

  7. 上一次为该master的投票时间,在cluster_node_timeout的2倍范围内,校验失败返回。这个用于使获胜从节点有时间将其成为新主节点的消息通知给其他从节点,从而避免另一个从节点发起新一轮选举又进行一次没必要的故障转移

  8. 请求节点宣称要负责的槽位,是否比之前负责这些槽位的节点,具有相等或更大的 configEpoch,如果不是,校验失败返回。

如果通过以上所有校验,那么主节点将向要求投票的从节点返回一条 CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 消息,表示这个主节点支持从节点成为新的主节点。

3)每个参与选举的从节点都会接收 CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 消息,并根据自己收到了多少条这种消息来统计自己获得了多少个主节点的支持。

4)如果集群里有N个具有投票权的主节点,那么当一个从节点收集到大于等于N/2+1 张支持票时,这个从节点就会当选为新的主节点。因为在每一个配置纪元里面,每个具有投票权的主节点只能投一次票,所以如果有 N个主节点进行投票,那么具有大于等于 N/2+1 张支持票的从节点只会有一个,这确保了新的主节点只会有一个。

5)如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。

这个选举新主节点的方法和选举领头 Sentinel 的方法非常相似,因为两者都是基于 Raft 算法的领头选举(leader election)方法来实现的。

28、如何保证集群在线扩容的安全性?(Redis 集群要增加分片,槽的迁移怎么保证无损)

例如:集群已经对外提供服务,原来有3分片,准备新增2个分片,怎么在不下线的情况下,无损的从原有的3个分片指派若干个槽给这2个分片?

Redis 使用了 ASK 错误来保证在线扩容的安全性。

在槽的迁移过程中若有客户端访问,依旧先访问源节点,源节点会先在自己的数据库里面査找指定的键,如果找到的话,就直接执行客户端发送的命令。

如果没找到,说明该键可能已经被迁移到目标节点了,源节点将向客户端返回一个 ASK 错误,该错误会指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令,从而获取到结果。

ASK错误

在进行重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面。

当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽时。源节点会先在自己的数据库里面査找指定的键,如果找到的话,就直接执行客户端发送的命令。

否则,这个键有可能已经被迁移到了目标节点,源节点将向客户端返回一个 ASK 错误,指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令,从而获取到结果。

29、Redis 事务的实现

一个事务从开始到结束通常会经历以下3个阶段:

1)事务开始:multi 命令将执行该命令的客户端从非事务状态切换至事务状态,底层通过 flags 属性标识。

2)命令入队:当客户端处于事务状态时,服务器会根据客户端发来的命令执行不同的操作:

  • exec、discard、watch、multi 命令会被立即执行

  • 其他命令不会立即执行,而是将命令放入到一个事务队列,然后向客户端返回 QUEUED 回复。

3)事务执行:当一个处于事务状态的客户端向服务器发送 exec 命令时,服务器会遍历事务队列,执行队列中的所有命令,最后将结果全部返回给客户端。

不过 redis 的事务并不推荐在实际中使用,如果要使用事务,推荐使用 Lua 脚本,redis 会保证一个 Lua 脚本里的所有命令的原子性。

30、Redis 的 Java 客户端有哪些?官方推荐哪个?

Redis 官网展示的 Java 客户端如下图所示,其中官方推荐的是标星的3个:Jedis、Redisson 和 lettuce。

31、Redis 里面有1亿个 key,其中有 10 个 key 是包含 java,如何将它们全部找出来?

1)keys *java* 命令,该命令性能很好,但是在数据量特别大的时候会有性能问题

2)scan 0 MATCH *java* 命令,基于游标的迭代器,更好的选择

SCAN 命令是一个基于游标的迭代器(cursor based iterator): SCAN 命令每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程。

当 SCAN 命令的游标参数被设置为 0 时, 服务器将开始一次新的迭代, 而当服务器向用户返回值为 0 的游标时, 表示迭代已结束。

32、使用过 Redis 做消息队列么?

Redis 本身提供了一些组件来实现消息队列的功能,但是多多少少都存在一些缺点,相比于市面上成熟的消息队列,例如 Kafka、Rocket MQ 来说并没有优势,因此目前我们并没有使用 Redis 来做消息队列。

关于 Redis 做消息队列的常见方案主要有以下:

1)Redis 5.0 之前可以使用 List(blocking)、Pub/Sub 等来实现轻量级的消息发布订阅功能组件,但是这两种实现方式都有很明显的缺点,两者中相对完善的 Pub/Sub 的主要缺点就是消息无法持久化,如果出现网络断开、Redis 宕机等,消息就会被丢弃。

2)为了解决 Pub/Sub 模式等的缺点,Redis 在 5.0 引入了全新的 Stream,Stream 借鉴了很多 Kafka 的设计思想,有以下几个特点:

  • 提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。

  • 引入了消费者组的概念,不同组接收到的数据完全一样(前提是条件一样),但是组内的消费者则是竞争关系。

Redis Stream 相比于 pub/sub 已经有很明显的改善,但是相比于 Kafka,其实没有优势,同时存在:尚未经过大量验证、成本较高、不支持分区(partition)、无法支持大规模数据等问题。

33、Redis 和 Memcached 的比较

1)数据结构:memcached 支持简单的 key-value 数据结构,而 redis 支持丰富的数据结构:String、List、Set、Hash、SortedSet 等。

2)数据存储:memcached 和 redis 的数据都是全部在内存中。

网上有一种说法 “当物理内存用完时,Redis可以将一些很久没用到的 value 交换到磁盘,同时在内存中清除”,这边指的是 redis 里的虚拟内存(Virtual Memory)功能,该功能在 Redis 2.0 被引入,但是在 Redis 2.4 中被默认关闭,并标记为废弃,而在后续版中被完全移除。

3)持久化:memcached 不支持持久化,redis 支持将数据持久化到磁盘

4)灾难恢复:实例挂掉后,memcached 数据不可恢复,redis 可通过 RDB、AOF 恢复,但是还是会有数据丢失问题

5)事件库:memcached 使用 Libevent 事件库,redis 自己封装了简易事件库 AeEvent

6)过期键删除策略:memcached 使用惰性删除,redis 使用惰性删除+定期删除

7)内存驱逐(淘汰)策略:memcached 主要为 LRU 算法,redis 当前支持8种淘汰策略,见本文第16题

8)性能比较

  • 按“CPU 单核” 维度比较:由于 Redis 只使用单核,而 Memcached 可以使用多核,所以在比较上:在处理小数据时,平均每一个核上 Redis 比 Memcached 性能更高,而在 100k 左右的大数据时, Memcached 性能要高于 Redis。

  • 按“实例”维度进行比较:由于 Memcached 多线程的特性,在 Redis 6.0 之前,通常情况下 Memcached 性能是要高于 Redis 的,同时实例的 CPU 核数越多,Memcached 的性能优势越大。

  • 至于网上说的 redis 的性能比 memcached 快很多,这个说法就离谱。

34、Redis 实现分布式锁

1)加锁

加锁通常使用 set 命令来实现,伪代码如下:

set key value PX milliseconds NX

几个参数的意义如下:

key、value:键值对

PX milliseconds:设置键的过期时间为 milliseconds 毫秒。

NX:只在键不存在时,才对键进行设置操作。SET key value NX 效果等同于 SETNX key value。

PX、expireTime 参数则是用于解决没有解锁导致的死锁问题。因为如果没有过期时间,万一程序员写的代码有 bug 导致没有解锁操作,则就出现了死锁,因此该参数起到了一个“兜底”的作用。

NX 参数用于保证在多个线程并发 set 下,只会有1个线程成功,起到了锁的“唯一”性。

2)解锁

解锁需要两步操作:

1)查询当前“锁”是否还是我们持有,因为存在过期时间,所以可能等你想解锁的时候,“锁”已经到期,然后被其他线程获取了,所以我们在解锁前需要先判断自己是否还持有“锁”

2)如果“锁”还是我们持有,则执行解锁操作,也就是删除该键值对,并返回成功;否则,直接返回失败。

由于当前 Redis 还没有原子命令直接支持这两步操作,所以当前通常是使用 Lua 脚本来执行解锁操作,Redis 会保证脚本里的内容执行是一个原子操作。

脚本代码如下,逻辑比较简单:

if redis.call(“get”,KEYS[1]) == ARGV[1]

then

return redis.call(“del”,KEYS[1])

else

return 0

end

两个参数的意义如下:

KEYS[1]:我们要解锁的 key

ARGV[1]:我们加锁时的 value,用于判断当“锁”是否还是我们持有,如果被其他线程持有了,value 就会发生变化。

上述方法是 Redis 当前实现分布式锁的主流方法,可能会有一些小优区别,但是核心都是这个思路。看着好像没啥毛病,但是真的是这个样子吗?让我们继续往下看。

35、Redis 分布式锁过期了,还没处理完怎么办

为了防止死锁,我们会给分布式锁加一个过期时间,但是万一这个时间到了,我们业务逻辑还没处理完,怎么办?

首先,我们在设置过期时间时要结合业务场景去考虑,尽量设置一个比较合理的值,就是理论上正常处理的话,在这个过期时间内是一定能处理完毕的。

之后,我们再来考虑对这个问题进行兜底设计。

关于这个问题,目前常见的解决方法有两种:

  1. 守护线程“续命”:额外起一个线程,定期检查线程是否还持有锁,如果有则延长过期时间。Redisson 里面就实现了这个方案,使用“看门狗”定期检查(每1/3的锁时间检查1次),如果线程还持有锁,则刷新过期时间。

  2. 超时回滚:当我们解锁时发现锁已经被其他线程获取了,说明此时我们执行的操作已经是“不安全”的了,此时需要进行回滚,并返回失败。

同时,需要进行告警,人为介入验证数据的正确性,然后找出超时原因,是否需要对超时时间进行优化等等。

36、守护线程续命的方案有什么问题吗

Redisson 使用看门狗(守护线程)“续命”的方案在大多数场景下是挺不错的,也被广泛应用于生产环境,但是在极端情况下还是会存在问题。

问题例子如下:

  1. 线程1首先获取锁成功,将键值对写入 redis 的 master 节点

  2. 在 redis 将该键值对同步到 slave 节点之前,master 发生了故障

  3. redis 触发故障转移,其中一个 slave 升级为新的 master

  4. 此时新的 master 并不包含线程1写入的键值对,因此线程2尝试获取锁也可以成功拿到锁

  5. 此时相当于有两个线程获取到了锁,可能会导致各种预期之外的情况发生,例如最常见的脏数据

解决方法:上述问题的根本原因主要是由于 redis 异步复制带来的数据不一致问题导致的,因此解决的方向就是保证数据的一致。

当前比较主流的解法和思路有两种:

1)Redis 作者提出的 RedLock;2)Zookeeper 实现的分布式锁。

37、RedLock

首先,该方案也是基于文章开头的那个方案(set加锁、lua脚本解锁)进行改良的,所以 antirez 只描述了差异的地方,大致方案如下。

假设我们有 N 个 Redis 主节点,例如 N = 5,这些节点是完全独立的,我们不使用复制或任何其他隐式协调系统,为了取到锁,客户端应该执行以下操作:

  1. 获取当前时间,以毫秒为单位。

  2. 依次尝试从5个实例,使用相同的 key 和随机值(例如UUID)获取锁。当向Redis 请求获取锁时,客户端应该设置一个超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在 5-50 毫秒之间。这样可以防止客户端在试图与一个宕机的 Redis 节点对话时长时间处于阻塞状态。如果一个实例不可用,客户端应该尽快尝试去另外一个Redis实例请求获取锁。

  3. 客户端通过当前时间减去步骤1记录的时间来计算获取锁使用的时间。当且仅当从大多数(N/2+1,这里是3个节点)的Redis节点都取到锁,并且获取锁使用的时间小于锁失效时间时,锁才算获取成功。

  4. 如果取到了锁,其真正有效时间等于初始有效时间减去获取锁所使用的时间(步骤3计算的结果)。

  5. 如果由于某些原因未能获得锁(无法在至少N/2+1个Redis实例获取锁、或获取锁的时间超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功,防止某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。

可以看出,该方案为了解决数据不一致的问题,直接舍弃了异步复制,只使用 master 节点,同时由于舍弃了 slave,为了保证可用性,引入了 N 个节点,官方建议是 5。

该方案看着挺美好的,但是实际上我所了解到的在实际生产上应用的不多,主要有两个原因:1)该方案的成本似乎有点高,需要使用5个实例;2)该方案一样存在问题。

该方案主要存以下问题:

  1. 严重依赖系统时钟。如果线程1从3个实例获取到了锁,但是这3个实例中的某个实例的系统时间走的稍微快一点,则它持有的锁会提前过期被释放,当他释放后,此时又有3个实例是空闲的,则线程2也可以获取到锁,则可能出现两个线程同时持有锁了。

  2. 如果线程1从3个实例获取到了锁,但是万一其中有1台重启了,则此时又有3个实例是空闲的,则线程2也可以获取到锁,此时又出现两个线程同时持有锁了。

针对以上问题其实后续也有人给出一些相应的解法,但是整体上来看还是不够完美,所以目前实际应用得不是那么多。

38、使用缓存时,先操作数据库 or 先操作缓存

总结

上述知识点,囊括了目前互联网企业的主流应用技术以及能让你成为“香饽饽”的高级架构知识,每个笔记里面几乎都带有实战内容。

很多人担心学了容易忘,这里教你一个方法,那就是重复学习。

打个比方,假如你正在学习 spring 注解,突然发现了一个注解@Aspect,不知道干什么用的,你可能会去查看源码或者通过博客学习,花了半小时终于弄懂了,下次又看到@Aspect 了,你有点郁闷了,上次好像在哪哪哪学习,你快速打开网页花了五分钟又学会了。

从半小时和五分钟的对比中可以发现多学一次就离真正掌握知识又近了一步。

人的本性就是容易遗忘,只有不断加深印象、重复学习才能真正掌握,所以很多书我都是推荐大家多看几遍。哪有那么多天才,他只是比你多看了几遍书。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
某些节点获取到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。

可以看出,该方案为了解决数据不一致的问题,直接舍弃了异步复制,只使用 master 节点,同时由于舍弃了 slave,为了保证可用性,引入了 N 个节点,官方建议是 5。

该方案看着挺美好的,但是实际上我所了解到的在实际生产上应用的不多,主要有两个原因:1)该方案的成本似乎有点高,需要使用5个实例;2)该方案一样存在问题。

该方案主要存以下问题:

  1. 严重依赖系统时钟。如果线程1从3个实例获取到了锁,但是这3个实例中的某个实例的系统时间走的稍微快一点,则它持有的锁会提前过期被释放,当他释放后,此时又有3个实例是空闲的,则线程2也可以获取到锁,则可能出现两个线程同时持有锁了。

  2. 如果线程1从3个实例获取到了锁,但是万一其中有1台重启了,则此时又有3个实例是空闲的,则线程2也可以获取到锁,此时又出现两个线程同时持有锁了。

针对以上问题其实后续也有人给出一些相应的解法,但是整体上来看还是不够完美,所以目前实际应用得不是那么多。

38、使用缓存时,先操作数据库 or 先操作缓存

总结

上述知识点,囊括了目前互联网企业的主流应用技术以及能让你成为“香饽饽”的高级架构知识,每个笔记里面几乎都带有实战内容。

很多人担心学了容易忘,这里教你一个方法,那就是重复学习。

打个比方,假如你正在学习 spring 注解,突然发现了一个注解@Aspect,不知道干什么用的,你可能会去查看源码或者通过博客学习,花了半小时终于弄懂了,下次又看到@Aspect 了,你有点郁闷了,上次好像在哪哪哪学习,你快速打开网页花了五分钟又学会了。

从半小时和五分钟的对比中可以发现多学一次就离真正掌握知识又近了一步。

[外链图片转存中…(img-82tyUpH1-1713304517540)]

人的本性就是容易遗忘,只有不断加深印象、重复学习才能真正掌握,所以很多书我都是推荐大家多看几遍。哪有那么多天才,他只是比你多看了几遍书。

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-ISZTOTUZ-1713304517540)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 12
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值