# 好未来 Java 面试

#

大家好,我是小宇宙。

好未来主要是搞 K12 教育相关的教育软件产品和工具,来看看好未来 25 届开发岗的校招薪资情况如下:

普通 offer:22k x 14.5 = 31.9w,北京

sp offer:26k x 14.5 = 37.7w,北京

训练营也有同学好未来的offer:

好未来面试难度不算简单,看到一篇同学的好未来的后端开发面经,同学反馈面试官水平挺高的,408基础比他狂背八股的要深多了,最后也有手撕算法代码。

这是一面的面经,面试时间长达 50 分钟, 面试官挺好的,问完会给出自己的答案以及自己想问的点,主要考察了计算机基础、网络协议、MySQL、Redis 的问题。

你们觉得难度如何?

# 计算机基础

#

# 单核CPU如何执行多个程序?

#

操作系统会为每个程序分配一个时间片,这个时间片是一个很短的时间间隔,例如几十毫秒。

单核 CPU 会轮流执行每个程序,在一个时间片内执行一个程序,当这个时间片用完后,就切换到下一个程序。这种方式给用户一种多个程序在同时运行的假象,因为切换速度非常快,用户难以察觉程序之间的切换。

# CPU的流水线设计有了解吗?

#

一条指令的执行需要经过取指令,翻译指令,执行指令三个基本流程。CPU内部的电路也分为不同单元:取指单元、译码单元、执行单元等,指令的执行也是按照流水线的工序一步一步执行的。

取指令
翻译指令
执行指令
取指单元
译码单元
执行单元

如果采用流水线技术,则每个时钟周期内只有一个单元在工作,其余两个单元在“观望”

流水线设计将这些操作分成多个独立的阶段,每个阶段由专门的硬件单元负责,使不同指令的不同阶段可以并行执行,流水线的本质就是拿空间换时间。将每条指令的步骤分解到不同的电路单元,从而使得多个指令并行执行。

这就好像我们的后端程序员不需要等待功能上线,就会从产品经理手中拿到下一个需求,开始开发 API。这样的协作模式,就是我们所说的 指令流水线 。这里面每一个独立的步骤,我们就称之为 流水线阶段 或者流水线级。

如果我们把一个指令拆分成“取指令 - 指令译码 - 执行指令”这样三个部分,那这就是一个三级的流水线。如果我们进一步把“执行指令”拆分成“ALU 计算(指令执行)- 内存访问 - 数据写回”,那么它就会变成一个五级的流水线。

例如,在一个简单的 5 级流水线中,当一条指令在执行阶段时,下一条指令可以在译码阶段,再下一条指令可以在取指阶段。

尽管流水线无法减少单条指令执行时的 “延时” 这一性能指标,不过,借助于同时对多条指令的不同阶段展开执行,我们能够有效地提升 CPU 的 “吞吐率”。从外部视角来看,CPU 仿佛具备了 “一心多用” 的能力,在同一时刻,可以同时对 5 条不同指令的不同阶段进行处理。在 CPU 内部,其运行机制就好似一条生产线,不同分工的组件持续处理着从上游传递过来的内容,而无需像传统模式那样,等到一件商品完全生产完毕之后,才开始下一件商品的生产流程。

# CPU绑定的操作有了解吗?

#

在单核 CPU,虽然只能执行一个线程,但是操作系统给每个线程分配了一个时间片,时间片用完了,就调度下一个线程,于是各个线程就按时间片交替地占用 CPU,从宏观上看起来各个线程同时在执行。

而现代 CPU 都是多核心的,线程可能在不同 CPU 核心来回切换执行,这对 CPU Cache 不是有利的,虽然 L3 Cache 是多核心之间共享的,但是 L1 和 L2 Cache 都是每个核心独有的,如果一个线程在不同核心来回切换,各个核心的缓存命中率就会受到影响,相反如果线程都在同一个核心上执行,那么其数据的 L1 和 L2 Cache 的缓存命中率可以得到有效提高,缓存命中率高就意味着 CPU 可以减少访问 内存的频率。

当有多个同时执行「计算密集型」的线程,为了防止因为切换到不同的核心,而导致缓存命中率下降的问题,我们可以把线程绑定在某一个 CPU 核心上,这样性能可以得到非常可观的提升。

在 Linux 上提供了 sched_setaffinity 方法,来实现将线程绑定到某个 CPU 核心这一功能。

image-20250122200538844

# 网络

#

# URL从输入到响应的流程?

#

null

解析URL:分析 URL 所需要使用的传输协议和请求的资源路径。如果输入的 URL 中的协议或者主机名不合法,将会把地址栏中输入的内容传递给搜索引擎。如果没有问题,浏览器会检查 URL 中是否出现了非法字符,则对非法字符进行转义后在进行下一过程。

缓存判断:浏览器会判断所请求的资源是否在缓存里,如果请求的资源在缓存里且没有失效,那么就直接使用,否则向服务器发起新的请求。

DNS解析:如果资源不在本地缓存,首先需要进行DNS解析。浏览器会向本地DNS服务器发送域名解析请求,本地DNS服务器会逐级查询,最终找到对应的IP地址。

获取MAC地址:当浏览器得到 IP 地址后,数据传输还需要知道目的主机 MAC 地址,因为应用层下发数据给传输层,TCP 协议会指定源端口号和目的端口号,然后下发给网络层。网络层会将本机地址作为源地址,获取的 IP 地址作为目的地址。然后将下发给数据链路层,数据链路层的发送需要加入通信双方的 MAC 地址,本机的 MAC 地址作为源 MAC 地址,目的 MAC 地址需要分情况处理。通过将 IP 地址与本机的子网掩码相结合,可以判断是否与请求主机在同一个子网里,如果在同一个子网里,可以使用 APR 协议获取到目的主机的 MAC 地址,如果不在一个子网里,那么请求应该转发给网关,由它代为转发,此时同样可以通过 ARP 协议来获取网关的 MAC 地址,此时目的主机的 MAC 地址应该为网关的地址。

建立TCP连接:主机将使用目标 IP地址和目标MAC地址发送一个TCP SYN包,请求建立一个TCP连接,然后交给路由器转发,等路由器转到目标服务器后,服务器回复一个SYN-ACK包,确认连接请求。然后,主机发送一个ACK包,确认已收到服务器的确认,然后 TCP 连接建立完成。

HTTPS 的 TLS 四次握手:如果使用的是 HTTPS 协议,在通信前还存在 TLS 的四次握手。

发送HTTP请求:连接建立后,浏览器会向服务器发送HTTP请求。请求中包含了用户需要获取的资源的信息,例如网页的URL、请求方法(GET、POST等)等。

服务器处理请求并返回响应:服务器收到请求后,会根据请求的内容进行相应的处理。例如,如果是请求网页,服务器会读取相应的网页文件,并生成HTTP响应。

# 追问:TCP连接除了IP地址还需要什么?

#

还需要端口号,端口号用于标识一个主机上的特定服务或进程。

源端口号是发送端应用程序使用的端口,它是一个 16 位的数字,范围从 0 到 65535。一般来说,客户端通常使用一个大于 1023 的临时端口号。

目的端口号是接收端应用程序使用的端口,不同的服务通常使用固定的知名端口号,例如 HTTP 使用端口 80 或 8080,HTTPS 使用端口 443,FTP 使用端口 21 等。

# 追问:流程在传输层做了什么事情?

#

主要有以下这些事情:

建立连接:TCP 三次握手连接的建立,这个过程中会确定双方的初始序列号和滑动窗口大小

数据传输 :当应用层的数据传递给传输层时,TCP 会将较大的数据分割成适合在网络中传输的较小的段,并为每个段添加 TCP 头部,包括序列号、确认号、窗口大小等信息。当接收方收到 TCP 段后,会发送 ACK 数据包来确认收到的数据。客户端根据服务器的 ACK 和窗口大小调整数据发送速度,继续发送后续 TCP 段。如果发送方在一定时间内没有收到 ACK,会认为数据丢失,会重传相应的 TCP 段,这个时间间隔称为超时重传时间(RTO)。

关闭连接:关闭 TCP 连接时,会进行四次挥手过程。

# 追问:流程在网络层做了什么事情?

#

传输层的 TCP 段传递到网络层,网络层将其封装成 IP 数据包,设置源 IP 地址和目的 IP 地址,添加其他 IP 头部信息。

传输层的 TCP 段传递到网络层,网络层将其封装成 IP 数据包,设置源 IP 地址和目的 IP 地址,添加其他 IP 头部信息。

路由器根据 IP 数据包的目的 IP 地址和自己的路由表进行路由选择,如果数据超过 MTU 大小,就会对数据包进行分片。

路由器根据 IP 数据包的目的 IP 地址和自己的路由表进行路由选择,如果数据超过 MTU 大小,就会对数据包进行分片。

数据包在网络中传输,经过多个路由器,每个路由器都会根据路由表转发数据包,并递减 TTL。

数据包在网络中传输,经过多个路由器,每个路由器都会根据路由表转发数据包,并递减 TTL。

当数据包到达目的主机,网络层进行解封和重组操作,将 TCP 段传递给传输层。

当数据包到达目的主机,网络层进行解封和重组操作,将 TCP 段传递给传输层。

# 追问:TCP有TCP的分段,IP层有IP分片。这两个有什么区别?现在用的是哪个?

#

两者的区别:

TCP 分段:发生在传输层,由 TCP 协议执行。当 TCP 从应用层接收到较大的数据块,超过 MSS 大小之后,会将其分成多个较小的 TCP 段,每个 TCP 段都包含 TCP 头部,接收方根据 TCP 段的序列号将它们重新组装成完整的数据块,然后传递给应用层。

IP 分片:发生在网络层,由 IP 协议执行。当 IP 数据包的大小超过链路层的最大传输单元(MTU)时,IP 会将数据包分成多个较小的分片。每个 IP 分片包含 IP 头部,由目的主机的网络层进行重组。目的主机根据 IP 分片的标识、标志和片偏移将它们重新组合成原始的 IP 数据包,然后传递给传输层。

TCP 通常会尽量避免 IP 分片,原因是 IP 分片之后,只有第一个分片才有TCP头部信息,那么一旦一个分片丢失,整个数据包都需要重传,会影响传输效率,并且由于 IP 分片是基于网络链路的 MTU,可能会在不同的网络链路中发生多次分片和重组,增加网络设备(如路由器)的处理开销。

在现代网络中,TCP 通常会尽量避免 IP 分片,主要作法是:TCP 在建立连接时,会协商 MSS,它是 TCP 段中数据部分的最大长度,通常是根据链路的 MTU 计算得到。例如,在以太网中,MTU 通常是 1500 字节,TCP 的 MSS 通常是 1460 字节(MTU 减去 IP 头部和 TCP 头部的长度)。这样 TCP 会将数据分成 MSS 大小的段,从而避免在网络层进行 IP 分片,提高传输效率。

# 拥塞控制介绍一下 ?

#

连接建立,cwnd = MSS,进入慢启动阶段,每收到一个 ACK,cwnd 翻倍,发送数据量不断增加。

当 cwnd 达到 ssthresh,进入拥塞避免阶段,cwnd 线性增长。

如果发生超时,ssthresh 减半,cwnd 重置为 MSS(1 个MSS),重新进入慢启动。

当发送方收到三个重复的 ACK 时,认为数据包丢失,但不是因为网络拥塞,而是可能某个数据包的乱序。在快速重传之后,发送方进入快速恢复阶段。通常将 ssthresh 减半,cwnd 设置为 ssthresh 加上 3 倍的 MSS(因为收到了三个重复的 ACK),并继续发送新数据。

# mysql

#

# MySQL是如何保障数据不丢失的?

#

主要是通过 redolog 来实现事务持久性的,事务执行过程,会把对 innodb 存储引擎中数据页修改操作记录到 redolog 里,事务提交的时候,就直接把 redolog 刷入磁盘,即使脏页中途没有刷盘成功, mysql 宕机了,也能通过 redolog 重放,恢复到之前事务修改数据页后的状态,从而保障了数据不丢失。

# RedoLog是在内存里吗?

#

事务执行过程中,生成的 redolog 会在 redolog buffer 中,也就是在内存中,等事务提交的时候,会把 redolog 写入磁盘。

# 为什么要写RedoLog,而不是直接写到B+树里面?

#

因为 redolog 写入磁盘是顺序写,而 b+树里数据页写入磁盘是随机写,顺序写的性能会比随机写好,这样可以提升事务提交的效率。

最重要的是redolog具备故障恢复的能力,Redo Log 记录的是物理级别的修改,包括页的修改,如插入、更新、删除操作在磁盘上的物理位置和修改内容。例如,当执行一个更新操作时,Redo Log 会记录修改的数据页的地址和更新后的数据,而不是 SQL 语句本身。

在数据页实际更新之前,先将修改操作写入 Redo Log。当数据库重启时,会进行恢复操作。首先,根据 Redo Log 检查哪些事务已经提交但数据页尚未完全写入磁盘。然后,使用 Redo Log 中的记录对这些事务进行重做(Redo)操作,将未完成的数据页修改完成,确保事务的修改生效。

# mysql 两次写(double write buffer)了解吗?

#

我们常见的服务器一般都是Linux操作系统,Linux文件系统页(OS Page)的大小默认是4KB。而MySQL的页(Page)大小默认是16KB。

MySQL程序是跑在Linux操作系统上的,需要跟操作系统交互,所以MySQL中一页数据刷到磁盘,要写4个文件系统里的页。

需要注意的是,这个操作并非原子操作,比如我操作系统写到第二个页的时候,Linux机器断电了,这时候就会出现问题了。造成”页数据损坏“。并且这种”页数据损坏“靠 redo日志是无法修复的。

Doublewrite Buffer的出现就是为了解决上面的这种情况,虽然名字带了Buffer,但实际上Doublewrite Buffer是内存+磁盘的结构。

Doublewrite Buffer 作用是,在把页写到数据文件之前,InnoDB先把它们写到一个叫doublewrite buffer(双写缓冲区)的共享表空间内,在写doublewrite buffer完成后,InnoDB才会把页写到数据文件的适当的位置。如果在写页的过程中发生意外崩溃,InnoDB在稍后的恢复过程中在doublewrite buffer中找到完好的page副本用于恢复,所以本质上是一个最近写回的页面的备份拷贝。

如上图所示,当有页数据要刷盘时:

页数据先通过memcpy函数拷贝至内存中的Doublewrite Buffer(大小为约 2MB)中,Doublewrite Buffer 分为两个区域,每次写入一个区域(最多 1MB 的数据)。

Doublewrite Buffer的内存里的数据页,会fsync刷到Doublewrite Buffer的磁盘上,写两次到到共享表空间中(连续存储,顺序写,性能很高),每次写1MB;

写入完成后,再将脏页刷到数据磁盘存储.ibd文件上(随机写);

当MySQL出现异常崩溃时,有如下几种情况发生:

情况一:步骤1前宕机,刷盘未开始,数据在redo log,后期可以恢复

情况二:步骤1后,步骤2前宕机,因为是在内存中,宕机清空内存,和情况1一样

情况三:步骤2后,步骤3前宕机,因为DWB的磁盘有完整的数据,可以修复损坏的页数据

由此我们可以得出结论,double write buffer是针对实际的buffer数据页的原子性保证,就是避免MySQL异常崩溃时,写的那几个data page不会出错,要么都写了,要么什么都没有做。

为什么redolog无法代替double write buffer?

为什么redolog无法代替double write buffer?

redolog的设计之初,是“账本的作用”,是一种操作日志,用于MySQL异常崩溃恢复使用,是InnoDB引擎特有的日志,本质上是物理日志,记录的是 “ 在某个数据页上做了什么修改 ” ,但如果数据页本身已经发生了损坏,redolog来恢复已经损坏的数据块是无效的,数据块的本身已经损坏,再次重做依然是一个坏块。

所以此时需要一个数据块的副本来还原该损坏的数据块,再利用重做日志进行其他数据块的重做操作,这就是double write buffer的原因作用。

# MySQL的加锁机制?

#

在 MySQL 里,根据加锁的范围,可以分为全局锁、表级锁和行锁三类。

全局锁:通过flush tables with read lock 语句会将整个数据库就处于只读状态了,这时其他线程执行以下操作,增删改或者表结构修改都会阻塞。全局锁主要应用于做全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。

全局锁:通过flush tables with read lock 语句会将整个数据库就处于只读状态了,这时其他线程执行以下操作,增删改或者表结构修改都会阻塞。全局锁主要应用于做全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。

表级锁:MySQL 里面表级别的锁有这几种: 表锁:通过lock tables 语句可以对表加表锁,表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。 元数据锁:当我们对数据库表进行操作时,会自动给这个表加上 MDL,对一张表进行 CRUD 操作时,加的是 MDL 读锁;对一张表做结构变更操作的时候,加的是 MDL 写锁;MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。 意向锁:当执行插入、更新、删除操作,需要先对表加上「意向独占锁」,然后对该记录加独占锁。意向锁的目的是为了快速判断表里是否有记录被加锁。

表级锁:MySQL 里面表级别的锁有这几种:

表锁:通过lock tables 语句可以对表加表锁,表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。

表锁:通过lock tables 语句可以对表加表锁,表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。

元数据锁:当我们对数据库表进行操作时,会自动给这个表加上 MDL,对一张表进行 CRUD 操作时,加的是 MDL 读锁;对一张表做结构变更操作的时候,加的是 MDL 写锁;MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。

元数据锁:当我们对数据库表进行操作时,会自动给这个表加上 MDL,对一张表进行 CRUD 操作时,加的是 MDL 读锁;对一张表做结构变更操作的时候,加的是 MDL 写锁;MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。

意向锁:当执行插入、更新、删除操作,需要先对表加上「意向独占锁」,然后对该记录加独占锁。意向锁的目的是为了快速判断表里是否有记录被加锁。

意向锁:当执行插入、更新、删除操作,需要先对表加上「意向独占锁」,然后对该记录加独占锁。意向锁的目的是为了快速判断表里是否有记录被加锁。

行级锁:InnoDB 引擎是支持行级锁的,而 MyISAM 引擎并不支持行级锁,行级锁如下: 记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的,满足读写互斥,写写互斥 间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。 Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。

行级锁:InnoDB 引擎是支持行级锁的,而 MyISAM 引擎并不支持行级锁,行级锁如下:

记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的,满足读写互斥,写写互斥

间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。

Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。

# 为什么要锁间隙锁而不是加行锁?

#

可重复读隔离级会有间隙锁,间隙锁主要是为了避免其他事务插入新记录,导致同一个事务前后两次查询的结果集数不一致的幻读问题。

假设,表中有一个范围 id 为(3,5)间隙锁,那么其他事务就无法插入 id = 4 这条记录了,这样就有效的防止幻读现象的发生。

# MVCC的实现机制?

#

MVCC允许多个事务同时读取同一行数据,而不会彼此阻塞,每个事务看到的数据版本是该事务开始时的数据版本。这意味着,如果其他事务在此期间修改了数据,正在运行的事务仍然看到的是它开始时的数据状态,从而实现了非阻塞读操作。

对于「读提交」和「可重复读」隔离级别的事务来说,它们是通过 Read View 来实现的,它们的区别在于创建 Read View 的时机不同,大家可以把 Read View 理解成一个数据快照,就像相机拍照那样,定格某一时刻的风景。

「读提交」隔离级别是在「每个select语句执行前」都会重新生成一个 Read View;

「可重复读」隔离级别是执行第一条select时,生成一个 Read View,然后整个事务期间都在用这个 Read View。

Read View 有四个重要的字段:

m_ids :指的是在创建 Read View 时,当前数据库中「活跃事务」的事务 id 列表,注意是一个列表,“活跃事务”指的就是,启动了但还没提交的事务。

min_trx_id :指的是在创建 Read View 时,当前数据库中「活跃事务」中事务 id 最小的事务,也就是 m_ids 的最小值。

max_trx_id :这个并不是 m_ids 的最大值,而是创建 Read View 时当前数据库中应该给下一个事务的 id 值,也就是全局事务中最大的事务 id 值 + 1;

creator_trx_id :指的是创建该 Read View 的事务的事务 id。

对于使用 InnoDB 存储引擎的数据库表,它的聚簇索引记录中都包含下面两个隐藏列:

null

trx_id,当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里;

roll_pointer,每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录。

在创建 Read View 后,我们可以将记录中的 trx_id 划分这三种情况:

一个事务去访问记录的时候,除了自己的更新记录总是可见之外,还有这几种情况:

如果记录的 trx_id 值小于 Read View 中的 min_trx_id 值,表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见。

如果记录的 trx_id 值小于 Read View 中的 min_trx_id 值,表示这个版本的记录是在创建 Read View 前已经提交的事务生成的,所以该版本的记录对当前事务可见。

如果记录的 trx_id 值大于等于 Read View 中的 max_trx_id 值,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见。

如果记录的 trx_id 值大于等于 Read View 中的 max_trx_id 值,表示这个版本的记录是在创建 Read View 后才启动的事务生成的,所以该版本的记录对当前事务不可见。

如果记录的 trx_id 值在 Read View 的 min_trx_id 和 max_trx_id 之间,需要判断 trx_id 是否在 m_ids 列表中: 如果记录的 trx_id 在 m_ids 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。 如果记录的 trx_id 不在 m_ids列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。

如果记录的 trx_id 值在 Read View 的 min_trx_id 和 max_trx_id 之间,需要判断 trx_id 是否在 m_ids 列表中:

如果记录的 trx_id 在 m_ids 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。

如果记录的 trx_id 在 m_ids 列表中,表示生成该版本记录的活跃事务依然活跃着(还没提交事务),所以该版本的记录对当前事务不可见。

如果记录的 trx_id 不在 m_ids列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。

如果记录的 trx_id 不在 m_ids列表中,表示生成该版本记录的活跃事务已经被提交,所以该版本的记录对当前事务可见。

这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。

# Redis

#

# Redis和数据库如何保证数据一致性?

#

对于读数据,我会选择旁路缓存策略,如果 cache 不命中,会从 db 加载数据到 cache。对于写数据,我会选择更新 db 后,再删除缓存。

缓存是通过牺牲强一致性来提高性能的。这是由CAP理论决定的。缓存系统适用的场景就是非强一致性的场景,它属于CAP中的AP。所以,如果需要数据库和缓存数据保持强一致,就不适合使用缓存。

所以使用缓存提升性能,就是会有数据更新的延迟。这需要我们在设计时结合业务仔细思考是否适合用缓存。然后缓存一定要设置过期时间,这个时间太短、或者太长都不好:

太短的话请求可能会比较多的落到数据库上,这也意味着失去了缓存的优势。

太长的话缓存中的脏数据会使系统长时间处于一个延迟的状态,而且系统中长时间没有人访问的数据一直存在内存中不过期,浪费内存。

但是,通过一些方案优化处理,是可以最终一致性的。针对删除缓存异常的情况,可以使用 2 个方案避免:

删除缓存重试策略(消息队列)

订阅 binlog,再删除缓存(Canal+消息队列)

消息队列方案

消息队列方案

我们可以引入消息队列,将第二个操作(删除缓存)要操作的数据加入到消息队列,由消费者来操作数据。

如果应用删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存,这个就是重试机制。当然,如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。

如果删除缓存成功,就要把数据从消息队列中移除,避免重复操作,否则就继续重试。

举个例子,来说明重试机制的过程。

重试删除缓存机制还可以,就是会造成好多业务代码入侵。

订阅 MySQL binlog,再操作缓存

订阅 MySQL binlog,再操作缓存

「先更新数据库,再删缓存」的策略的第一步是更新数据库,那么更新数据库成功,就会产生一条变更日志,记录在 binlog 里。

于是我们就可以通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除,阿里巴巴开源的 Canal 中间件就是基于这个实现的。

Canal 模拟 MySQL 主从复制的交互协议,把自己伪装成一个 MySQL 的从节点,向 MySQL 主节点发送 dump 请求,MySQL 收到请求后,就会开始推送 Binlog 给 Canal,Canal 解析 Binlog 字节流之后,转换为便于读取的结构化数据,供下游程序订阅使用。

下图是 Canal 的工作原理:

将binlog日志采集发送到MQ队列里面,然后编写一个简单的缓存删除消息者订阅binlog日志,根据更新log删除缓存,并且通过ACK机制确认处理这条更新log,保证数据缓存一致性

# 如果做一个大流量的网站,单Redis无法承压了如何解决?

#

读写分离:部署多个 Redis 从节点(Slave),主节点(Master)负责写操作,从节点负责读操作。主节点将数据同步到从节点,从节点可以处理大量的读请求,减轻主节点的压力。

构建集群:部署 Redis Cluster 集群,Redis Cluster 将数据自动划分为 16384 个槽(slots),每个槽都可以存储键值对。这些槽会被分配到多个 Redis 节点上,通过哈希函数将键映射到相应的槽,再由槽映射到具体的 Redis 节点。例如,使用 CRC16(key) % 16384 来确定键属于哪个槽,然后根据槽与节点的映射关系将键值对存储到相应节点。通过数据分片,将数据和请求分散到多个节点,避免单个节点的负载过高。不同节点负责不同的槽,各自处理一部分请求,实现负载均衡。

CRC16(key) % 16384

# 算法

#

lc. 152 乘积最大子数组

对了,最新的互联网大厂后端面经都会在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。

img