八股面试题
八股面试题
网络
TCP/IP模型和OSI模型
OSI七层,分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
TCP/IP四层,网络接口层,网络层,传输层,应用层。
OSI模型在理论上更全面,但在实际网络通信中,TCP/IP模型实用性更强。
网络接口层:负责物理传输媒介的传输,例如以太网、Wi-Fi等,并提供错误检测和纠正的功能。此外,还包含硬件地址(MAC地址)的管理。
网络层:负责数据包的路由和转发,选择最佳路径。主要协议是IP协议,其使用IP地址来标识主机和网络,并进行逻辑地址寻址。
传输层:负责端到端的数据传输,提供可靠的、无连接的数据传输服务。主要的传输层协议有TCP和UDP。
应用层:提供直接与用户应用程序交互的接口,为网络上的各种应用程序提供服务,如电子邮件(SMTP)、网页浏览(HTTP)、文件传输(FTP)。
从输入 URL 到页面展示
1、输入网址,解析URL信息,准备发送HTTP请求
2、检查浏览器缓存,如果有直接返回;如果没有进入下一步网络请求。
3、DNS域名解析:网络请求前,进行DNS解析,以获取请求域名的IP地址。DNS解析时会按本地浏览器缓存->本地Host文件->路由器缓存->DNS服务器->根DNS服务器的顺序查询域名对应IP,直到找到为止。
4、TCP三次握手建立连接
5、客户端发送HTTP请求
6、服务器处理请求并返回HTTP资源
7、TCP四次挥手断开连接
8、浏览器解析响应并渲染页面:
浏览器解析响应头。若响应头状态码为301、302,会重定向到新地址;若响应数据类型是字节流类型,一般会将请求提交给下载管理器;若是HTML类型,会进入下一部渲染流程。
浏览器解析HTML文件,创建DOM树,解析CSS进行样式计算,然后将CSS和DOM合并,构建渲染树;最后布局和绘制渲染树,完成页面展示。
HTTP请求和响应报文结构
请求报文:请求行、请求头、空行、请求体
响应报文:状态行、响应头、空行、响应体
请求行主要字段:
要执行的方法,如GET、POST、PUT、DELETE;
资源路径:请求的资源的URI;
HTTP版本:使用的HTTP协议版本
请求头主要字段:
Host:请求的服务器的域名;Content-Length:请求体长度; Content-Type:请求体媒体类型;Cookie:存储在客户端cookie数据;
空行:
在请求头和请求体之间,表示请求头的结束。
请求体:
包含发送给服务器的数据。
状态行:
包含HTTP版本,状态码和状态信息。
响应头主要字段:
Server:指定服务器的信息;Content-Length:响应体长度; Content-Type:响应体的媒体类型;Set-Cookie:在响应中设置Cookie;
HTTP请求方式
GET:向特定资源发送请求,查询数据并返回实体,常用于获取网页内容。
POST:向特定资源提交数据并处理请求,通常用于提交表单或上传文件。
PUT:向服务器上传新的内容,用于更新指定的资源。
DELETE:向服务器请求删除指定的资源。
HEAD:获取响应头部信息,不返回实体内容。
OPTIONS:获取服务器支持的请求方法,可以用来测试服务器的功能性。
TRACE:回显服务器收到的请求,用于测试或诊断。
HTTP状态码
1XX:表示临时响应,用于传达请求已被接收、处理正在进行,或者需要额外的客户端操作。
2XX:表示请求已成功被服务器接收、理解并接受。
3XX:表示要完成请求,需要进一步操作。 通常,这些状态代码用来重定向。
4XX:表示客户端提交的请求存在错误,服务器无法或拒绝处理。
5XX:表示服务器在处理请求时遇到意外情况,无法完成请求。
GET、POST
用途
GET 请求:用于从服务器获取数据,并不修改数据。
POST 请求:发送数据,创建或更新资源。
安全性
GET 请求:不安全,数据直接暴露在URL中,可能导致敏感信息泄露。
POST 请求:安全,数据包含在请求体中。
数据传输方式
GET 请求:数据通过URL传递,限制长度,通常为 2048 字符左右。
POST 请求:数据包含在请求体中,可以传输大量数据。
缓存
GET 请求:默认可缓存,浏览器可以将响应缓存,以提高性能。
POST 请求:默认不缓存,每次请求都会发送到服务器。
强缓存、协商缓存
强缓存:浏览器根据服务器设置的过期时间来判断是否使用缓存,未过期则从本地缓存里拿资源,已过期则重新请求服务器获取最新资源。
协商缓存:浏览器每次都向服务器发起请求,由服务器来告诉浏览器是从缓存里拿资源还是返回最新资源给浏览器使用。
为什么用缓存:
为了减少资源请求次数,加快资源访问速度,浏览器会对引用的静态资源【图片、css文件、js文件等】进行缓存
优先级:
强缓存优先级大于协商缓存,即两者同时存在时,如果强缓存开启且在有效期内,则不会走协商缓存。
HTTP1.0,HTTP1.1,HTTP2.0,HTTP3.0
缓存策略
HTTP1.0 提供expires方法,即强缓存
HTTP1.1 提供cache-control方法,即协商缓存
连接方式
HTTP1.0 默认采用短链接,每次请求都要重新连接;HTTP1.1默认采用长连接,可以在一次连接中发生多个请求
HTTP1.1支持管道化,可以在响应到来之前发送多个请求
HTTP1.1支持断点续传
二进制协议:
HTTP/1.1 采用文本格式传输数据,HTTP/2.0 二进制格式,减少解析时间,使得解析更高效。
多路复用:
HTTP/2.0 支持多路复用,允许在单个TCP连接上并行交错发送多个请求和响应,解决了HTTP/1.1 中的队头阻塞问题。
头部压缩:
HTTP/2.0 引入了HPACK 压缩算法,对请求和响应的头部信息进行压缩,减少了冗余头部信息的传输,提高了传输效率。
HTTP/3基于QUIC协议
无队头阻塞:
QUIC 使用UDP协议来传输数据。一个连接上的多个stream之间没有依赖, 如果一个stream丢了一个UDP包,不会影响后面的stream,不存在 队头阻塞问题。
零 RTT 连接建立:
QUIC 允许在首次连接时进行零往返时间连接建立,从而减少了连接延迟,加快了页面加载速度。
连接迁移:
QUIC 允许在网络切换(如从 Wi-Fi 到移动网络)时,将连接迁移到新的 IP 地址,从而减少连接的中断时间。
安全性:
HTTP/3默认使用TLS加密,确保了数据传输的安全性。
HTTPS、HTTP
安全性和数据加密:
加密层:增加了SSL/TLS 协议作为加密层。而HTTP 数据传输是明文的。
连接过程:HTTPS在TCP三次握手之后,还需进行 SSL/TLS 的握手过程。
端口:HTTPS 通常使用端口443 ,而HTTP 使用端口80。
HTTPS 协议需要向 CA 申请数字证书,来保证服务器的身份是可信的。
HTTPS工作原理
HTTPS 主要基于SSL/TLS 协议,建立连接并传输数据的过程如下:
密钥交换:客户端发起HTTPS请求后,服务器会发送其公钥证书给客户端。
证书验证:客户端会验证服务器的证书是否由受信任的证书颁发机构(CA )签发,并检查证书的有效性。
加密通信:一旦证书验证通过,客户端会生成一个随机的对称加密密钥,并使用服务器的公钥加密这个密钥,然后发送给服务器。
建立安全连接:服务器使用自己的私钥解密得到对称加密密钥,此时客户端和服务器都有了相同的密钥,可以进行加密和解密操作。
数据传输:使用对称加密密钥对所有传输的数据进行加密,确保数据在传输过程中的安全性。
完整性校验:SSL/TLS协议还包括消息完整性校验机制,如消息认证码,确保数据在传输过程中未被篡改。
结束连接:数据传输完成后,通信双方会进行会话密钥的销毁,以确保不会留下安全隐患。
TCP、UDP区别
TCP是面向连接的协议,需要在数据传输前建立连接;UDP是无连接的,不需要建立连接。
TCP提供可靠的数据传输,保证数据包的顺序和完整性;UDP不保证数据包的顺序或完整性。
TCP具有拥塞控制机制,可以根据网络状况调整数据传输速率;UDP没有拥塞控制,发送速率通常固定。
TCP通过滑动窗口机制进行流量控制,避免接收方处理不过来;UDP没有流量控制。
TCP能够检测并重传丢失或损坏的数据包;UDP不提供错误恢复机制。
TCP有复杂的报文头部,包含序列号、确认号等信息;UDP的报文头部相对简单。
由于TCP的连接建立、数据校验和重传机制,其性能开销通常比UDP大;UDP由于简单,性能开销小。
适用场景:TCP适用于需要可靠传输的应用,如网页浏览、文件传输等;UDP适用于对实时性要求高的应用,如语音通话、视频会议等。
TCP、UDP可靠传输
TCP通过差错控制(序列号、确认应答、数据校验)、超时重传、流量控制、拥塞控制等机制,确保了数据传输的可靠性和效率。
序列号:每个TCP段都有一个序列号,确保数据包的顺序正确。
数据校验:TCP使用校验和来检测数据在传输过程中是否出现错误,如果检测到错误,接收方会丢弃该数据包,并等待重传。
确认应答:接收方发送ACK确认收到的数据,如果发送方在一定时间内没有收到确认,会重新发送数据。
超时重传:发送方设置一个定时器,如果在定时器超时之前没有收到确认,发送方会重传数据。
流量控制:TCP通过滑动窗口机制进行流量控制,确保接收方能够处理发送方的数据量。
拥塞控制:TCP通过算法如慢启动、拥塞避免、快重传和快恢复等,来控制数据的发送速率,防止网络拥塞。

TCP流量控制,拥塞控制

TCP拥塞控制的主要机制包括以下几个方面:
慢启动(Slow Start): 初始阶段,TCP发送方会以较小的发送窗口开始传输数据。随着每次成功收到确认的数据,发送方逐渐增加发送窗口的大小,实现指数级的增长,这称为慢启动。这有助于在网络刚开始传输时谨慎地逐步增加速率,以避免引发拥塞。
拥塞避免(Congestion Avoidance): 一旦达到一定的阈值(通常是慢启动阈值),TCP发送方就会进入拥塞避免阶段。在拥塞避免阶段,发送方以线性增加的方式增加发送窗口的大小,而不再是指数级的增长。这有助于控制发送速率,以避免引起网络拥塞。
快速重传(Fast Retransmit): 如果发送方连续收到相同的确认,它会认为发生了数据包的丢失,并会快速重传未确认的数据包,而不必等待超时。这有助于更快地恢复由于拥塞引起的数据包丢失。
快速恢复(Fast Recovery): 在发生快速重传后,TCP进入快速恢复阶段。在这个阶段,发送方不会回到慢启动阶段,而是将慢启动阈值设置为当前窗口的一半,并将拥塞窗口大小设置为慢启动阈值加上已确认但未被快速重传的数据块的数量。这有助于更快地从拥塞中恢复。

TCP三次握手,四次挥手
(1) 三次握手的过程
第一次握手:客户端向服务器发送一个SYN (同步序列编号)报文,请求建立连接,客户端进入SYN_SENT 状态。
第二次握手:服务器收到SYN 报文后,如果同意建立连接,则会发送一个SYN-ACK (同步确认)报文作为响应,同时进入SYN_RCVD 状态。
第三次握手:客户端收到服务器的SYN-ACK 报文后,会发送一个ACK (确认)报文作为最终响应,之后客户端和服务器都进入ESTABLISHED 状态,连接建立成功。
(2)为什么需要三次握手
通过三次握手,客户端和服务器都能够确认对方的接收和发送能力。
第一次握手确认了客户端到服务器的通道是开放的;
第二次握手确认了服务器到客户端的通道是开放的;
第三次握手则确认了客户端接收到服务器的确认,从而确保了双方的通道都是可用的。
而如果仅使用两次握手,服务器可能无法确定客户端的接收能力是否正常,比如客户端可能已经关闭了连接,但之前发送的连接请求报文在网络上延迟到达了服务器,服务器就会主动去建立一个连接,但是客户端接收不到,导致资源的浪费。
四次握手可以优化为三次。
1)四次挥手的过程
第一次挥手:客户端发送一个FIN报文给服务端,表示自己要断开数据传送,报文中会指定一个序列号 (seq=x)。然后,客户端进入FIN-WAIT-1 状态。
第二次挥手:服务端收到FIN报文后,回复ACK报文给客户端,且把客户端的序列号值+1,作为ACK报文的序列号(seq=x+1)。然后,服务端进入CLOSE-WAIT(seq=x+1)状态,客户端进入FIN-WAIT-2状态。
第三次挥手:服务端也要断开连接时,发送FIN报文给客户端,且指定一个序列号(seq=y+1),随后服务端进入LAST-ACK状态。
第四次挥手:客户端收到FIN报文后,发出ACK报文进行应答,并把服务端的序列号值+1作为ACK报文序列号(seq=y+2)。此时客户端进入TIME-WAIT状态。服务端在收到客户端的ACK 报文后进入CLOSE 状态。如果客户端等待2MSL没有收到回复,才关闭连接。
(2)为什么需要四次挥手
TCP是全双工通信,可以双向传输数据。任何一方都可以在数据传送结束后发出连接释放的通知,待对方确认后进入半关闭状态。 当另一方也没有数据再发送的时候,则发出连接释放通知,对方确认后才会完全关闭 TCP 连接。
因此两次挥手可以释放一端到另一端的TCP连接,完全释放连接一共需要四次挥手。
只有通过四次挥手,才可以确保双方都能接收到对方的最后一个数据段的确认,主动关闭方在发送完最后一个ACK后进入TIME-WAIT 状态,这是为了确保被动关闭方接收到最终的ACK ,如果被动关闭方没有接收到,它可以重发FIN 报文,主动关闭方可以再次发送ACK 。
而如果使用三次挥手,被动关闭方可能在发送最后一个数据段后立即关闭连接,而主动关闭方可能还没有接收到这个数据段的确认。
Keep-Alive
- HTTP 的 Keep-Alive,是由应用层实现的,称为 HTTP 长连接,每次请求都要经历这样的过程:
建立 TCP连接 -> HTTP请求资源 -> 响应资源 -> 释放连接,这就是HTTP短连接,
但是这样每次建立连接都只能请求一次资源,所以HTTP 的 Keep-Alive实现了使用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答,避免了连接建立和释放的开销,就就是 HTTP 长连接。
通过设置HTTP头Connection: keep-alive来实现。
- TCP 的 Keepalive,是由TCP 层(内核态)实现的,称为 TCP 保活机制,是一种用于在 TCP 连接上检测空闲连接状态的机制。
当TCP连接建立后,如果一段时间内没有任何数据传输,TCP Keepalive会发送探测包来检查连接是否仍然有效。
DNS查询过程
DNS 用来将主机名和域名转换为IP地址, 其查询过程一般通过以下步骤:
本地DNS缓存检查:首先查询本地DNS缓存,如果缓存中有对应的IP地址,则直接返回结果。
如果本地缓存中没有,则会向本地的DNS服务器(通常由你的互联网服务提供商(ISP)提供, 比如中国移动)发送一个DNS查询请求。
如果本地DNS解析器有该域名的ip地址,就会直接返回,如果没有缓存该域名的解析记录,它会向根DNS服务器发出查询请求。根DNS服务器并不负责解析域名,但它能告诉本地DNS解析器应该向哪个顶级域(.com/.net/.org)的DNS服务器继续查询。
本地DNS解析器接着向指定的顶级域名DNS服务器发出查询请求。顶级域DNS服务器也不负责具体的域名解析,但它能告诉本地DNS解析器应该前往哪个权威DNS服务器查询下一步的信息。
本地DNS解析器最后向权威DNS服务器发送查询请求。 权威DNS服务器是负责存储特定域名和IP地址映射的服务器。当权威DNS服务器收到查询请求时,它会查找"example.com"域名对应的IP地址,并将结果返回给本地DNS解析器。
本地DNS解析器将收到的IP地址返回给浏览器,并且还会将域名解析结果缓存在本地,以便下次访问时更快地响应。
浏览器发起连接: 本地DNS解析器已经将IP地址返回给您的计算机,您的浏览器可以使用该IP地址与目标服务器建立连接,开始获取网页内容。

CDN
CDN是一种分布式网络服务,通过将内容存储在分布式的服务器上,使用户可以从距离较近的服务器获取所需的内容,从而加速互联网上的内容传输。
就近访问:CDN 在全球范围内部署了多个服务器节点,用户的请求会被路由到距离最近的 CDN 节点,提供快速的内容访问。
内容缓存:CDN 节点会缓存静态资源,如图片、样式表、脚本等。当用户请求访问这些资源时,CDN 会首先检查是否已经缓存了该资源。如果有缓存,CDN 节点会直接返回缓存的资源,如果没有缓存所需资源,它会从源服务器(原始服务器)回源获取资源,并将资源缓存到节点中,以便以后的请求。通过缓存内容,减少了对原始服务器的请求,减轻了源站的负载。
可用性:即使某些节点出现问题,用户请求可以被重定向到其他健康的节点。

Cookie、Session
cookie
服务器会将一个或多个cookie设置到给客户端的响应中,浏览器将这些cookie存储在本地,每次携带这些cookie访问时,服务器通过分析请求头中的cookie得到客户端特有的信息,来生成与该客户端相对应的内容。
session
用于维护用户登陆状态和用户临时数据等,客户端访问服务器时,服务器会为每个用户分配一个唯一的sessionID,通常存储在cookie中。
区别[伪容安生传]
存储位置上,cookie数据存储在用户的浏览器中,而session数据存储在服务器上
数据容量上,cookie的存储容量相对较小,而session的存储容量一般没有固定限制,取决于服务器的配置和资源
安全性上,由于cookie存储在用户浏览器中, /因而很容易被读取和纂改/ ,而session存储在服务器上没有那么容易
生命周期上,cookie /可设置过期时间/ ,而session依赖会话的持续时间或用户活动
传输上,cookie在每次HTTP请求中都会被自动发送到服务器,而sessionID通常通过cookie或url参数传递
ARP
ARP是 TCP/IP 协议栈中一种重要的网络协议,用于将 IP 地址 转换为对应的 MAC 地址。ARP 是在 局域网(LAN) 中使用的一种协议,工作在 链路层 和 网络层 之间。
ARP 在网络通信中的作用
局域网内设备通信:
在同一局域网中,设备之间的通信需要依赖 MAC 地址,ARP 提供了 IP 地址到 MAC 地址的转换功能。
支持 IP 层协议工作:
ARP 是实现 IP 数据报在链路层上传输的关键协议。
动态解析目标地址:
ARP 通过动态查询 MAC 地址,确保通信过程中目标地址的准确性。
透明性:
ARP 的操作对用户和应用程序是透明的,用户无需关心底层地址解析的过程。
NAT
理论: NAT(网络地址转换,Network Address Translation)是一种将私有网络IP地址转换为公共网络IP地址的技术。它主要用于解决IPv4地址枯竭问题,同时提供一定的安全性。
作用:
节约IP地址:
通过在内部网络中使用私有IP地址(如192.168.x.x),多个内部设备可以共享一个或少数几个公共IP地址,减少对公网IP地址的需求。
隐藏内部网络结构:
NAT隐藏了内部网络的IP地址,使得外部网络无法直接访问内部设备,提高了网络的安全性。
允许多个设备共享一个公共IP:
通过端口号的映射,NAT能够让多个内部设备通过同一个公共IP地址与外部网络通信。
工作原理:
NAT设备(通常是路由器)在数据包通过时修改其源或目的IP地址:
源地址转换(SNAT):将内部设备的私有IP地址转换为公共IP地址。
目的地址转换(DNAT):将公共IP地址转换为内部设备的私有IP地址,通常用于端口转发。
操作系统
进程、线程、管程
进程是资源分配和调度的基本单位。 线程是程序执行的最小单位,线程是进程的子任务,是进程内的执行单元。
一个进程至少有一个线程,一个进程可以运行多个线程,这些线程共享同一块内存。
资源开销:
进程:由于每个进程都有独立的内存空间,创建和销毁进程的开销较大。进程间切换需要保存和恢复整个进程的状态,因此上下文切换的开销较高。
线程:线程共享相同的内存空间,创建和销毁线程的开销较小。线程间切换只需要保存和恢复少量的线程上下文,因此上下文切换的开销较小。
通信与同步:
进程:由于进程间相互隔离,进程之间的通信需要使用一些特殊机制,如管道、消息队列、共享内存等。
线程:由于线程共享相同的内存空间,它们之间可以直接访问共享数据,线程间通信更加方便。
安全性:
进程:由于进程间相互隔离,一个进程的崩溃不会直接影响其他进程的稳定性。
线程:由于线程共享相同的内存空间,一个线程的错误可能会影响整个进程的稳定性。
管程(Monitor)是一种用于进程同步的高级抽象机制,它将共享资源的访问与管理封装在一个模块中,通过条件变量和同步机制来确保进程对资源的互斥访问。它是操作系统中用于解决并发编程问题的重要工具。 管程的主要作用包括:
互斥访问:保证多个进程不能同时访问同一个共享资源。
条件同步:通过条件变量协调多个进程的执行顺序。
简化并发编程:通过封装复杂的同步逻辑,提供更易用的接口。
并行、并发
并行是指在同一时刻执行多个任务,这些任务可以同时进行,每个任务都在不同的处理单元(如多个CPU核心)上执行。在并行系统中,多个处理单元可以同时处理独立的子任务,从而加速整体任务的完成。
并发是指在相同的时间段内执行多个任务,这些任务可能不是同时发生的,而是交替执行,通过时间片轮转或者事件驱动的方式。并发通常与任务之间的交替执行和任务调度有关。
用户态、核心态
用户态和内核态的区别
用户态和内核态是操作系统为了保护系统资源和实现权限控制而设计的两种不同的CPU运行级别,可以控制进程或程序对计算机硬件资源的访问权限和操作范围。
用户态:在用户态下,进程或程序只能访问受限的资源和执行受限的指令集,不能直接访问操作系统的核心部分,也不能直接访问硬件资源。
核心态:核心态是操作系统的特权级别,允许进程或程序执行特权指令和访问操作系统的核心部分。在核心态下,进程可以直接访问硬件资源,执行系统调用,管理内存、文件系统等操作。
在什么场景下,会发生内核态和用户态的切换
系统调用:当用户程序需要请求操作系统提供的服务时,会通过系统调用进入内核态。
异常:当程序执行过程中出现错误或异常情况时,CPU会自动切换到内核态,以便操作系统能够处理这些异常。
中断:外部设备(如键盘、鼠标、磁盘等)产生的中断信号会使CPU从用户态切换到内核态。操作系统会处理这些中断,执行相应的中断处理程序,然后再将CPU切换回用户态。
进程调度算法
先来先服务:按照请求的顺序进行调度。 这种调度方式简单,但是能导致较长作业阻塞较短作业。
最短作业优先:非抢占式的调度算法,按估计运行时间最短的顺序进行调度。 但是如果一直有短作业到来,那么长作业永远得不到调度,造成长作业“饥饿”现象。
最短剩余时间优先:基于最短作业优先改进,按剩余运行时间的顺序进行调度。当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。
优先级调度:为每个进程分配一个优先级,按优先级进行调度。为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。
时间片轮转:为每个进程分配一个时间片,进程轮流执行,时间片用完后切换到下一个进程。
多级队列:时间片轮转调度算法和优先级调度算法的结合。 将进程分为不同的优先级队列,每个队列有自己的调度算法。
进程间通信
管道:是一种半双工的通信方式,数据只能单向流动而且只能在具有父子进程关系的进程间使用。
命名管道: 类似管道,也是半双工的通信方式,但是它允许在不相关的进程间通信。
消息队列:允许进程发送和接收消息,而消息队列是消息的链表,可以设置消息优先级。
信号:用于发送通知到进程,告知其发生了某种事件或条件。
信号量:是一个计数器,可以用来控制多个进程对共享资源的访问,常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此主要作为进程间以及同一进程内不同线程之间的同步手段。
共享内存:就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的进程通信方式,
Socket套接字:是支持TCP/IP 的网络通信的基本操作单元,主要用于在客户端和服务器之间通过网络进行通信。
进程同步、互斥
进程同步是指多个并发执行的进程之间协调和管理它们的执行顺序,以确保它们按照一定的顺序或时间间隔执行。
互斥指的是在某一时刻只允许一个进程访问某个共享资源。当一个进程正在使用共享资源时,其他进程不能同时访问该资源。
解决进程同步和互斥的问题有很多种方法,其中一种常见的方法是使用信号量和 PV 操作。
信号量是一种特殊的变量,它表示系统中某种资源的数量或者状态。PV 操作是一种对信号量进行增加或者减少的操作,它们可以用来控制进程之间的同步或者互斥。
P操作:相当于“检查”信号量,如果资源可用,就减少计数,然后使用资源。
V操作:相当于“归还”资源,增加信号量的计数,并可能唤醒等待的进程。
除此之外,下面的方法也可以解决进程同步和互斥问题:
临界区:将可能引发互斥问题的代码段称为临界区,里面包含了需要互斥访问的资源。进入这个区域前需要先获取锁,退出临界区后释放该锁。这确保同一时间只有一个进程可以进入临界区。
互斥锁(Mutex):互斥锁是一种同步机制,用于实现互斥。每个共享资源都关联一个互斥锁,进程在访问该资源前需要先获取互斥锁,使用完后释放锁。只有获得锁的进程才能访问共享资源。
条件变量:条件变量用于在进程之间传递信息,以便它们在特定条件下等待或唤醒。通常与互斥锁一起使用,以确保等待和唤醒的操作在正确的时机执行。
死锁
死锁(Deadlock) 是一种特殊的进程间并发问题,指的是两个或多个进程在执行过程中,因为争夺资源而造成的一种互相等待的局面,在这种局面下,它们都在等待对方释放自己所需要的资源,而没有任何进程能够继续执行,从而导致整个系统的停滞。死锁通常发生在多个进程同时请求并占有多个共享资源时。
死锁形成要满足四个条件:
互斥:进程直接等待的资源是要互斥操作的。
不剥夺:某个进程拥有的资源,必须等其使用完拥有的资源且被释放后,才能被其他进程获取。
持有且等待:进程拥有某个资源且在等待资源时,其不会自动释放掉拥有的资源。
循环等待:进程之间等待资源的顺序形成了环。
| 死锁预防方法 | 描述 | 优点 | 缺点 |
|---|---|---|---|
| 破坏四个条件之一 | 通过破坏死锁的四个必要条件之一来避免死锁 | 比较简单,能有效避免死锁 | 可能影响系统的性能和资源利用率,某些方法较难实现 |
| 死锁检测与恢复 | 定期检查系统是否有死锁,并采取措施恢复 | 适用于无法避免死锁的场景 | 需要额外的开销,可能导致系统停滞和性能降低 |
| 死锁避免 | 动态评估资源请求,确保不会进入不安全状态 | 系统始终处于安全状态,避免死锁的发生 | 算法复杂,计算开销大,可能降低并发性 |
在多线程编程中,锁用于控制对共享资源的访问,防止数据竞争和保证线程安全。典型的锁有:
互斥锁:互斥锁是一种最常见的锁类型,用于实现互斥访问共享资源。在任何时刻,只有一个线程可以持有互斥锁。如果锁已被占用,其他线程尝试获取时会阻塞,直到锁被释放。这确保了同一时间只有一个线程能够访问被保护的资源
自旋锁:自旋锁是一种基于忙等待的锁,即在获取自旋锁时不会阻塞线程,线程在尝试获取锁时会不断轮询,直到锁被释放。
其他的锁都是基于互斥锁和自旋锁的:
读写锁:允许多个线程同时读共享资源,只允许一个线程进行写操作。分为读(共享)和写(排他)两种状态。读锁可以被多个线程同时持有。写锁是独占的,持有写锁时,其他线程无法获取读锁或写锁。适用于读多写少的场景
悲观锁:认为多线程同时修改共享资源的概率比较高,所以访问共享资源时候要上锁,确保在操作完成之前其他线程无法修改资源。适用于写操作频繁、并发冲突概率高的场景。实现简单,但可能会降低系统的并发性能
乐观锁:假设并发冲突不太可能发生,因此在访问共享资源时不会加锁,而是在提交操作时检查是否有冲突。如果检测到冲突,则回滚操作或重试
虚拟内存
概念:
虚拟内存是操作系统提供的一种从逻辑上扩充内存的方法,具有请求调入功能 和 置换功能。“虚拟内存”技术为每个进程提供一个独立的、连续的地址空间,使得每个进程都认为自己独占整个内存。虚拟内存通过将物理内存和磁盘空间结合起来,扩展了可用的内存空间,并实现了内存隔离和简化内存管理。虚拟内存广泛应用于现代操作系统中,如 Windows、Linux 和 macOS。
实现方式:
主要实现方式包括分页(将内存划分为固定大小的页)和分段(将内存划分为不同大小的段)。
优缺点:
虚拟内存的优点包括提高内存利用率、支持多任务和简化编程,但也存在性能开销(如缺页中断)和复杂性等缺点。
线程同步
线程同步机制是指在多线程编程中,防止线程间互相干扰,而采取的一种机制。常见的线程同步机制有:互斥锁、条件变量、读写锁和信号量。
1.互斥锁:是最常见的线程同步机制。它允许只有一个线程同时访问被保护的临界区(共享资源)。
2.条件变量:用于线程间通信,允许一个线程等待某个条件满足,而其他线程可以发出信号通知等待线程。通常与互斥锁一起使用。
3.读写锁:允许同时多个线程读取共享资源,但只允许一个线程写入资源。
4.信号量:用于控制多个线程对共享资源进行访问的工具。
页面置换算法
| 算法名称 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| FIFO | 简单易懂,容易实现 | 可能导致频繁访问的页面被替换 | 简单的内存管理应用 |
| LRU | 直观,高效,性能较好 | 实现复杂,需要额外的空间管理 | 需要保持近期使用页面的场景 |
| LFU | 根据访问频率管理页面 | 实现复杂,可能导致低访问频率页面误被保留 | 对访问频率敏感的场景 |
| OPT | 最优的页面置换算法 | 无法实现,需预测未来访问序列 | 理论分析和算法评估 |
| 时钟算法 | 高效,近似 LRU,低开销 | 无法完美模拟 LRU | 需要较低开销且接近 LRU 的场景 |
| 改进 LRU | 降低空间和时间开销,接近 LRU | 无法完美模拟 LRU | 对性能和资源要求较高的场景 |
分页存储、分段存储
| 特性 | 分段存储管理 | 分页存储管理 |
|---|---|---|
| 基本单位 | 逻辑上的段,如代码段、数据段、堆栈段等 | 固定大小的页面(Page)和页框(Frame) |
| 内存分配方式 | 每个段的大小可以不同,程序的不同部分可以有不同的大小 | 每个页面的大小固定,所有页面大小相同 |
| 映射方式 | 使用段表将逻辑地址映射到物理地址 | 使用页表将虚拟页面映射到物理页框 |
| 外部碎片 | 可能会产生外部碎片(即物理内存中的空闲区域无法有效利用) | 不会产生外部碎片,解决了内存的连续性问题 |
| 内部碎片 | 无(因为段大小是动态的,和程序的需求相匹配) | 可能会产生内部碎片(如果页面未完全使用) |
| 保护性 | 不同段可以设置不同的访问权限,提供较好的程序保护 | 每个页面可以设置不同的访问权限,但保护机制较为简单 |
| 灵活性 | 更灵活,每个段可以按需增长或缩小,符合程序的逻辑结构 | 相对不太灵活,页面大小固定,所有页面大小相同 |
| 复杂性 | 需要管理段表,内存分配和回收复杂 | 较简单,内存分配和回收较为简单,页表管理相对复杂 |
| 适用场景 | 适用于需要处理逻辑上有结构的程序(如编译型语言、复杂应用等) | 适用于操作系统中对内存管理要求较高的应用(如大规模并发应用) |
中断、异常
1. 中断的定义
中断(Interrupt)是指在程序正常执行的过程中,发生了某些事件需要立即处理,这些事件会通知 CPU 暂停当前的指令流,跳转到一个专门的中断服务程序(ISR, Interrupt Service Routine)来处理该事件。当中断服务程序处理完成后,CPU 会继续执行原来的任务。
2. 中断的分类
根据中断的来源和性质,中断可以分为以下几种类型:
硬件中断(Hardware Interrupt):
由外部硬件设备产生,例如键盘、鼠标、网络卡、硬盘等设备向 CPU 发出请求。
常见的硬件中断事件包括键盘输入、鼠标点击、I/O 完成等。
软件中断(Software Interrupt):
由软件指令触发,例如通过系统调用(System Call)引发中断。
操作系统提供的服务(如文件管理、内存分配)通常通过软件中断来实现。
异常(Exception):
由 CPU 内部产生的中断,通常由于指令执行期间出现错误或特殊情况导致。
常见的异常有除零错误、非法指令、页错误(Page Fault)等。
时钟中断(Timer Interrupt):
由系统时钟定时器产生,用于时间片轮转调度等任务。
时钟中断是多任务系统中实现进程调度的重要机制。
select、poll、epoll
I/O 多路复用机制
| 特性 | select |
poll |
epoll |
|---|---|---|---|
| 文件描述符限制 | 有最大文件描述符限制(通常为 1024) | 没有文件描述符数量的限制 | 没有文件描述符数量限制 |
| 性能 | 低,随着文件描述符数量增加,效率急剧下降 | 较 select 有改善,但仍需遍历所有文件描述符 |
高,基于事件通知,O(1) 时间复杂度 |
| 编程模型 | 简单,适用于小规模应用 | 比 select 简单,适合中等规模应用 |
较复杂,适合高并发应用 |
| 支持平台 | 跨平台 | 跨平台 | Linux 专有 |
| 适用场景 | 小规模应用,低并发,简易实现 | 中等规模应用,适合大部分非高性能需求的应用 | 高并发、大量连接的服务器,性能要求高的场景 |
数据库
Mysql
SQL查询语句执行
连接器:连接器负责跟客户端建立连接、获取权限、维持和管理连接。
查询缓存: MySQL 拿到一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以key-value 对的形式,被直接缓存在内存中。
分析器:你输入的是由多个字符串和空格组成的一条SQL 语句,MySQL 需要识别出里面的字符串分别是什么,代表什么。
优化器:优化器是在表里面有多个索引的时候,决定使用哪个索引; 或者在一个语句有多表关联(join )的时候,决定各个表的连接顺序。
执行器: MySQL通过分析器知道了你要做什么,通过优化器知道了该怎么做,于是就进入了执行器阶段,开始执行语句。
事务的四大特性
事务的四大特性通常被称为 ACID 特性
原子性:确保事务的所有操作要么全部执行成功,要么全部失败回滚,不存在部分成功的情况。
一致性:事务在执行前后,数据库从一个一致性状态转变到另一个一致性状态。
隔离性:多个事务并发执行时,每个事务都应该被隔离开来,一个事务的执行不应该影响其他事务的执行。
持久性:一旦事务被提交,它对数据库的改变就是永久性的,即使在系统故障或崩溃后也能够保持。
事务隔离级别
读未提交(Read Uncommitted):
允许一个事务读取另一个事务尚未提交的数据修改。
最低的隔离级别,存在脏读、不可重复读和幻读的问题。
读已提交(Read Committed):
一个事务只能读取已经提交的数据。其他事务的修改在该事务提交之后才可见。
解决了脏读问题,但仍可能出现不可重复读和幻读。
可重复读(Repeatable Read):
事务执行期间,多次读取同一数据会得到相同的结果,即在事务开始和结束之间,其他事务对数据的修改不可见。
解决了不可重复读问题,但仍可能出现幻读。
序列化(Serializable):
最高的隔离级别,确保事务之间的并发执行效果与串行执行的效果相同,即不会出现脏读、不可重复读和幻读。
MySQL的执行引擎
MySQL的执行引擎主要负责查询的执行和数据的存储, 其执行引擎主要有MyISAM、InnoDB、Memory 等。
InnoDB引擎提供了对事务ACID的支持,还提供了行级锁和外键的约束,是目前MySQL的默认存储引擎,适用于需要事务和高并发的应用。
MyISAM引擎是早期的默认存储引擎,支持全文索引,但是不支持事务,也不支持行级锁和外键约束,适用于快速读取且数据量不大的场景。
Memery就是将数据放在内存中,访问速度快,但数据在数据库服务器重启后会丢失。
B+树
B+树是一个B树的变种,提供了高效的数据检索、插入、删除和范围查询性能。
单点查询:B 树进行单个索引查询时,最快可以在 O(1) 的时间代价内就查到。从平均时间代价来看,会比 B+ 树稍快一些。但是 B 树的查询波动会比较大,因为每个节点既存索引又存记录,所以有时候访问到了非叶子节点就可以找到索引,而有时需要访问到叶子节点才能找到索引。B+树的非叶子节点不存放实际的记录数据,仅存放索引,所以数据量相同的情况下,相比存储即存索引又存记录的 B 树,B+树的非叶子节点可以存放更多的索引,因此 B+ 树可以比 B 树更「矮胖」,查询底层节点的磁盘 I/O次数会更少。
插入和删除效率:B+ 树有大量的冗余节点,删除一个节点的时候,可以直接从叶子节点中删除,甚至可以不动非叶子节点,删除非常快。B+ 树的插入也是一样,有冗余节点,插入可能存在节点的分裂(如果节点饱和),但是最多只涉及树的一条路径。B 树没有冗余节点,删除节点的时候非常复杂,可能涉及复杂的树的变形。
范围查询:B+ 树所有叶子节点间有一个链表进行连接,而 B 树没有将所有叶子节点用链表串联起来的结构,因此只能通过树的遍历来完成范围查询,这会涉及多个节点的磁盘 I/O 操作,范围查询效率不如 B+ 树。存在大量范围检索的场景,适合使用 B+树,比如数据库。而对于大量的单个索引查询的场景,可以考虑 B 树,比如nosql的MongoDB。
索引失效
索引失效意味着查询操作不能有效利用索引进行数据检索,从而导致性能下降,下面一些场景会发生索引失效。
使用OR条件:当使用OR连接多个条件,并且每个条件用到不同的索引列时,索引可能不会被使用。
使用非等值查询:当使用!=或<>操作符时,索引可能不会被使用,特别是当非等值条件在WHERE子句的开始部分时。
对列进行类型转换: 如果在查询中对列进行类型转换,例如将字符列转换为数字或日期,索引可能会失效。
使用LIKE语句:以通配符%开头的LIKE查询会导致索引失效。
函数或表达式:在列上使用函数或表达式作为查询条件,通常会导致索引失效。
表连接中的列类型不匹配: 如果在连接操作中涉及的两个表的列类型不匹配,索引可能会失效。例如,一个表的列是整数,另一个表的列是字符,连接时可能会导致索引失效。
慢查询
数据库查询的执行时间超过指定的超时时间时,就被称为慢查询。
原因:
查询语句比较复杂:查询涉及多个表,包含复杂的连接和子查询,可能导致执行时间较长。
查询数据量大:当查询的数据量庞大时,即使查询本身并不复杂,也可能导致较长的执行时间。
缺少索引:如果查询的表没有合适的索引,需要遍历整张表才能找到结果,查询速度较慢。
数据库设计不合理:数据库表设计庞大,查询时可能需要较多时间。
并发冲突:当多个查询同时访问相同的资源时,可能发生并发冲突,导致查询变慢。
硬件资源不足:如果MySQL服务器上同时运行了太多的查询,会导致服务器负载过高,从而导致查询变慢
优化:
运行语句,找到慢查询的sql
查询区分度最高的字段
explain:显示mysql如何使用索引来处理select语句以及连接表,可以帮助选择更好的索引、写出更优化的查询语句
order by limit形式的sql语句,让排序的表优先查
考虑建立索引原则
undo log、redo log、binlog
undo log是Innodb存储引擎层生成的日志,实现了事务中的原子性,主要用于事务回滚和MVCC。
redo log是物理日志,记录了某个数据页做了什么修改,每当执行一个事务就会产生一条或者多条物理日志。
binlog (归档日志)是Server 层生成的日志,主要用于数据备份和主从复制。
MySQL、Redis
Redis基于键值对,支持多种数据结构;而MySQL是一种关系型数据库,使用表来组织数据。
Redis将数据存在内存中,通过持久化机制将数据写入磁盘,MySQL通常将数据存储在磁盘上。
Redis不使用SQL,而是使用自己的命令集,MySQL使用SQL来进行数据查询和操作。
Redis以高性能和低延迟为目标,适用于读多写少的应用场景,MySQL 适用于需要支持复杂查询、事务处理、拥有大规模数据集的场景。
Redis 更适合处理高速、高并发的数据访问,以及需要复杂数据结构和功能的场景,在实际应用中,很多系统会同时使用 MySQL 和 Redis。
Redis
Redis优缺点
(1) Redis有什么优缺点?
Redis 是一个基于内存的数据库,读写速度非常快,通常被用作缓存、消息队列、分布式锁和键值存储数据库。它支持多种数据结构,如字符串、哈希表、列表、集合、有序集合等, Redis 还提供了分布式特性,可以将数据分布在多个节点上,以提高可扩展性和可用性。但是Redis 受限于物理内存的大小,不适合存储超大量数据,并且需要大量内存,相比磁盘存储成本更高。
(2)为什么Redis查询快
基于内存操作: 传统的磁盘文件操作相比减少了IO,提高了操作的速度。
高效的数据结构:Redis专门设计了STRING、LIST、HASH等高效的数据结构,依赖各种数据结构提升了读写的效率。
单线程:单线程操作省去了上下文切换带来的开销和CPU的消耗,同时不存在资源竞争,避免了死锁现象的发生。
I/O多路复用:采用I/O多路复用机制同时监听多个Socket,根据Socket上的事件来选择对应的事件处理器进行处理。
数据类型
Redis 常见的五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)及 Zset(sorted set:有序集合)。
字符串STRING:存储字符串数据,最基本的数据类型。
哈希表HASH:存储字段和值的映射,用于存储对象。
列表LIST:存储有序的字符串元素列表。
集合SET:存储唯一的字符串元素,无序。
有序集合ZSET:类似于集合,但每个元素都关联一个分数,可以按分数进行排序。
Redis版本更新,又增加了几种数据类型,
BitMap: 存储位的数据结构,可以用于处理一些位运算操作。
HyperLogLog:用于基数估算的数据结构,用于统计元素的唯一数量。
GEO: 存储地理位置信息的数据结构。
Stream:专门为消息队列设计的数据类型。
单线程设计
Redis在其传统的实现中是单线程的(网络请求模块使用单线程进行处理,其他模块仍用多个线程),这意味着它使用单个线程来处理所有的客户端请求。这样的设计选择有几个关键原因:
简化模型:单线程模型简化了并发控制,避免了复杂的多线程同步问题。
性能优化:由于大多数操作是内存中的,单线程避免了线程间切换和锁竞争的开销。
原子性保证:单线程执行确保了操作的原子性,简化了事务和持久化的实现。
顺序执行:单线程保证了请求的顺序执行。
但是Redis的单线程模型并不意味着它在处理客户端请求时不高效。实际上,由于其操作主要在内存中进行,Redis能够提供极高的吞吐量和低延迟的响应。
此外,Redis 6.0 引入了多线程的功能,用来处理网络I/O这部分,充分利用CPU资源,减少网络I/O阻塞带来的性能损耗。
二级缓存
二级缓存相比只调用一层 Redis 缓存,访问速度更快。对于一些不经常修改的数据而查询十分频繁的可以直接放在本地缓存(一级)里面。
2)使用了本地缓存相比直接去 Redis 中取,能够减少与远程 Redis 的数据 I/O 网络交互,降低了网络之间的消耗。
作为面试者的扩展延伸:我在本地缓存的实现中,我使用到了本地缓存性能之王 Caffeine 作为一级缓存,在市面上很多像 Redisson、Druid、Hbase 等知名开源项目都用到了 Caffeine 。它实现了更加好用的缓存淘汰算法 W-TinyLFU 算法,结合了 LRU(最近最久未使用算法) 算法以及 LFU(最少使用算法) 算法的优点,所以选择它能使本地缓存的使用更加方便快速。
```java
先查询 Redis
"缓存命中:" + k + " 从 Redis 读取成功"
Redis没有则查询 DB
"缓存没有命中:" + k + " 从 DB 读取"
存入 Redis
存入本地缓存由cache.get执行
```
# Spring 的三级缓存
在 Spring 框架中,循环依赖是指两个或多个 Bean 之间相互依赖,形成一个循环引用的情况。这种情况下,Spring IOC 容器在实例化 Bean 时可能会出现问题,因为它无法决定应该首先实例化哪个 Bean。为了解决这个问题,Spring 引入了三级缓存。
三级缓存是指 Spring IOC 容器中用于解决循环依赖问题的一种机制,它包含三个缓存阶段:
1)singletonObjects:这是 Spring IOC 容器的一级缓存,用于存储已经完全创建并初始化的 Bean实例。当 Bean 完全创建后,它会被放置在这个缓存中。
2)earlySingletonObjects:这是 Spring IOC 容器的二级缓存,用于存储提前暴露的、尚未完全初始化的 Bean 实例。当 Bean 正在被创建但尚未完成初始化时,它会被放置在这个缓存中。
3)singletonFactories:这是 Spring IOC 容器的三级缓存,用于存储 Bean 的工厂对象。在创建Bean 实例时,Spring 首先会在这个缓存中查找工厂对象,如果找到则使用该工厂对象来创建 Bean 实例。
# 三级缓存的作用
三级缓存的作用是为了解决循环依赖时的初始化顺序问题。在初始化 Bean 时,Spring 会首先将 Bean 的实例放入三级缓存中,然后进行属性注入等操作。如果发现循环依赖,Spring 会在二级缓存中查找对应的 Bean 实例,如果找到则直接返回,否则会调用Bean的工厂方法来创建 Bean 实例,并将其放入二级缓存中。当 Bean 实例化完成后,会从二级缓存中移除,并放入一级缓存中。
# 为什么需要三级缓存而不是二级缓存
二级缓存只存储了尚未初始化完成的 Bean 实例,而三级缓存存储了 Bean 的工厂对象。这样做的好处是,当发现循环依赖时,可以通过 Bean 的工厂对象来创建 Bean 实例,从而避免了直接从二级缓存中获取可能尚未完成初始化的 Bean 实例而导致的问题。因此,三级缓存提供了更加灵活和可靠的解决方案,能够更好地处理循环依赖问题。
Linux
常见linux指令
文件操作:
ls:列出目录内容。
cd:改变当前目录。
pwd:显示当前工作目录。
cp:复制文件或目录。
mv:移动或重命名文件。
rm:删除文件或目录。
touch:创建空文件或更新文件时间戳。
文件内容查看:
cat:查看文件内容。
head:查看文件的前几行。
tail:查看文件的后几行,常用于查看日志文件。
文件编辑:
vi 或 vim:强大的文本编辑器。
权限管理:
chmod:更改文件或目录的访问权限。
chown:更改文件或目录的所有者和/或所属组。
磁盘管理:
df:查看磁盘空间使用情况。
网络管理:
ifconfig 或 ip addr:查看和配置网络接口。
ping:测试网络连接。
netstat:查看网络状态和统计信息。
ssh:安全远程登录。
进程管理:
ps:查看当前运行的进程。
kill:发送信号给进程。
软件包管理(根据Linux发行版不同,命令可能有所不同):
apt-get(Debian/Ubuntu):安装、更新和删除软件包。
查看、杀死进程
查看进程:
用 ps 命令查看当前运行的进程,比如 ps aux 可以列出所有进程及其详细信息。
杀死进程:
首先用 ps 或 top 命令找到进程的PID(进程ID)。 然后用 kill 命令加上进程ID来结束进程,例如 kill -9 PID。"-9" 是强制杀死进程的信号。
查看端口占用:
使用 lsof -i:端口号 可以查看占用特定端口的进程。 或者用 netstat -tulnp | grep 端口号,这会显示监听在该端口的服务及其进程ID。
Java
封装、继承、多态
Spring
Bean
### **1. 什么是Spring Bean?**
- **定义**:由Spring IoC容器管理的对象,容器负责其创建、配置和生命周期。
- **核心特点**:通过依赖注入(DI)实现松耦合,支持面向切面编程(AOP)。
---
### **2. Bean的作用域(Scope)**
Spring支持以下作用域:
1. **Singleton(默认)**:容器中仅存在一个实例。
2. **Prototype**:每次请求都创建新实例。
3. **Request**(Web应用):每个HTTP请求一个实例。
4. **Session**(Web应用):每个用户会话一个实例。
5. **Global Session**(已弃用):Portlet应用中的全局会话。
---
### **3. Bean的生命周期**
1. **实例化**:通过构造器或工厂方法创建Bean。
2. **属性赋值**:注入依赖(如`@Autowired`、XML配置)。
3. **初始化**:
- 实现`InitializingBean`接口的`afterPropertiesSet()`方法。
- 使用`@PostConstruct`注解。
- XML配置的`init-method`。
4. **销毁**:
- 实现`DisposableBean`接口的`destroy()`方法。
- 使用`@PreDestroy`注解。
- XML配置的`destroy-method`。
5. **扩展点**:`BeanPostProcessor`(初始化前后处理)、`BeanFactoryPostProcessor`(Bean定义修改)。
---
### **4. 配置Bean的方式**
- **XML配置**:传统方式,显式定义`<bean>`标签。
- **Java注解**:
- `@Component`及其衍生注解(`@Service`, `@Repository`, `@Controller`)。
- `@Bean`:在配置类中声明Bean。
- **Java Config**:通过`@Configuration`类替代XML,更类型安全。
---
### **5. 依赖注入(DI)方式**
1. **构造器注入**(推荐):通过构造方法注入强依赖,保证对象不可变。
2. **Setter注入**:通过Setter方法注入可选依赖。
3. **字段注入**:使用`@Autowired`直接注入字段(不推荐,隐藏依赖关系)。
---
### **6. 自动装配(Autowiring)**
- **模式**:`byType`(默认)、`byName`、`constructor`等。
- **注解**:
- `@Autowired`:按类型注入,配合`@Qualifier`指定Bean名称。
- `@Resource`:按名称注入(JDK原生注解)。
- `@Primary`:优先注入的候选Bean。
---
### **7. 常见问题扩展**
- **延迟初始化**:`@Lazy`注解延迟Bean的创建到首次使用时。
- **循环依赖**:Spring通过三级缓存解决单例Bean的Setter/字段注入循环依赖,但构造器注入不支持。
- **线程安全**:Singleton Bean默认非线程安全,需自行处理同步。
- **FactoryBean**:用于创建复杂对象(如MyBatis的`SqlSessionFactoryBean`)。
---
### **总结回答示例**
“Spring Bean是由IoC容器管理的对象,核心包括作用域(如Singleton、Prototype)、生命周期(实例化、依赖注入、初始化/销毁回调)、配置方式(XML、注解、Java Config)及依赖注入(构造器注入推荐)。理解Bean的生命周期扩展点(如BeanPostProcessor)和解决循环依赖的机制是深入掌握Spring的关键。”
常见注解
# 核心配置相关
@SpringBootApplication
功能:标记为 Spring Boot 应用的入口,集成了 @Configuration、@EnableAutoConfiguration 和 @ComponentScan。
# RESTful API 开发相关
@RestController
功能:标记类为 REST 控制器,等同于 @Controller + @ResponseBody。
@GetMapping / @PostMapping / @PutMapping / @DeleteMapping
功能:用于定义 HTTP 请求的映射,对应 GET、POST、PUT、DELETE 方法。
@RequestParam / @PathVariable
功能:获取 URL 参数或路径变量。
# 数据访问相关
@Entity
功能:标记类为 JPA 实体,与数据库表映射。
@Repository
功能:标记类为持久层组件,用于异常转换。
@Transactional
功能:声明事务,确保方法在事务范围内执行。
Spring MVC
Spring Security
Spring Boot
MyBatis
分页插件
# 分页的基本概念
在讨论MyBatis分页插件之前,我们先来回顾分页的基本概念。分页是将大量数据按照指定的每页记录数进行合理的拆分和展示的技术。其核心目的包括:
`减少单次查询的数据量,提高查询性能`
`优化用户体验,使数据展示更加友好`
`降低服务器的内存和网络传输压力`
1. 拦截器机制
MyBatis提供了插件机制,允许开发者在执行SQL前后进行拦截和处理。分页插件正是通过这一机制实现对查询的拦截和修改。
2. 动态SQL修改
分页插件的核心在于动态地修改原始SQL,添加分页相关的条件。这通常涉及两个关键操作:
计算总记录数
在原始SQL上添加分页限制条件 :Mysql使用 LIMIT 子句
项目相关(技术)
仿牛客网讨论社区项目—项目总结及项目常见面试题-CSDN博客
牛客高级项目课之牛客讨论区——项目总结_牛客论坛技术文档-CSDN博客
整个技术的核心是Spring框架,在Spring之上使用了SpringMvc(前后端交互与请求处理)、Spring Mybatis(访问数据库)、Spring Security(管理登录权限等)。
Spring Email
开箱即用,引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
开启 SMTP 服务,获取并输入邮箱的授权码,发送邮件
@Autowired
JavaMailSender javaMailSender;
@Test
public void test() throws Exception {
// 创建一个邮件消息
MimeMessage message = javaMailSender.createMimeMessage();
// 创建 MimeMessageHelper
MimeMessageHelper helper = new MimeMessageHelper(message, false);
// 发件人邮箱和名称
helper.setFrom("747692844@qq.com", "springdoc");
// 收件人邮箱
helper.setTo("admin@springboot.io");
// 邮件标题
helper.setSubject("Hello");
// 邮件正文,第二个参数表示是否是HTML正文
helper.setText("Hello <strong> World</strong>!", true);
// 发送
javaMailSender.send(message);
}
Interceptor
【SpringBoot篇】Interceptor拦截器 | 拦截器和过滤器的区别-CSDN博客
拦截器(Interceptor)是一种设计模式,处理请求或响应时对其进行拦截和修改。拦截器可执行跨层的通用任务,如身份验证、授权、缓存、日志记录、性能计量等。
`身份验证和授权`:拦截器可以用于检查请求是否具有适当的凭据,并根据需要拒绝或允许请求。这使得开发人员能够轻松地实现身份验证和授权功能。
`缓存`:拦截器可以用于缓存请求或响应数据,以便加快应用程序的性能。例如,在Web应用程序中,可以使用拦截器缓存静态资源,如CSS文件和图像。
`日志记录`:拦截器可以用于记录请求和响应的详细信息,以便开发人员能够更好地了解应用程序的行为和性能。
`性能计量`:拦截器可以用于测量应用程序的性能,并识别可能的瓶颈。例如,在Web应用程序中,可以使用拦截器跟踪页面加载时间,并标识慢速查询或资源。
`异常处理`:拦截器可以用于处理应用程序中的异常情况,并提供友好的错误消息。例如,在Web应用程序中,可以使用拦截器捕获异常并显示自定义错误页面。
拦截器的工作方式:注册到应用程序的处理管道中,并在请求或响应传递过程中执行相应的操作。拦截器可以访问请求、响应上下文和处理程序对象,从而允许它们访问和修改请求或响应的属性和状态。拦截器还可以决定是否将请求和响应继续传递到下一个处理程序或终止请求。


Advice
【Spring源码三千问】Advice、Advisor、Advised都是什么接口?_spring advice advisor-CSDN博客
第14章-Spring AOP通知(Advice)详解 - 知乎
AOP 中的通知是基于连接点(Join point)业务逻辑的一种增强,Spring AOP 提供了下面五种通知类型:
Before advice(前置通知):连接点前面执行,不能终止后续流程,除非抛异常
After returning advice(后置通知):连接点正常返回时执行,有异常不执行
Around advice(环绕通知):围绕连接点前后执行,也能捕获异常处理
After advice(最终通知):连接点退出时执行,无论是正常退出还是异常退出
After throwing advice(异常通知):连接点方法抛出异常时执行
AOP 的连接点一般是指目标类的方法,五种通知类型执行的节点如下:

AOP
### **一、Spring AOP的核心原理**
1. **动态代理机制**
Spring AOP基于动态代理实现,分为两种方式:
• **JDK动态代理**:适用于实现了接口的类,通过`Proxy`类和`InvocationHandler`接口生成代理对象,拦截接口方法调用。
• **CGLIB动态代理**:通过继承目标类生成子类代理,适用于未实现接口的类。无法代理`final`类或方法。
• **性能对比**:JDK动态代理在简单场景下性能略优,但CGLIB在复杂代理链中表现更稳定。
2. **静态代理与动态代理的区别**
• **静态代理(如AspectJ)**:编译时织入切面,性能更高但需要特定编译器支持。
• **动态代理**:运行时生成代理对象,灵活性高,无需修改源码。
### **二、AOP核心概念**
1. **核心术语**
• **切面(Aspect)**:封装横切关注点的模块(如日志、事务),由**切点**和**通知**组成。
• **连接点(Join Point)**:程序执行过程中的方法调用或异常抛出点。
• **切点(Pointcut)**:通过表达式(如`execution()`)定义哪些连接点需要被拦截。
• **通知(Advice)**:切面在连接点的执行逻辑,分为5种类型:
◦ `@Before`:方法执行前通知
◦ `@After`:方法执行后通知(无论成功或异常)
◦ `@Around`:包裹方法执行,需手动调用`proceed()`
◦ `@AfterReturning`:方法成功返回后通知
◦ `@AfterThrowing`:方法抛出异常后通知。
2. **织入(Weaving)**
将切面逻辑嵌入目标对象的过程。Spring AOP通过运行时动态织入,而AspectJ支持编译时或类加载时织入。
### **三、常见面试问题及回答技巧**
1. **Spring AOP与AspectJ的区别?**
• **实现方式**:Spring AOP基于动态代理,AspectJ基于静态代理。
• **功能范围**:AspectJ支持更丰富的切点表达式(如构造方法、字段访问),Spring AOP仅支持方法级别的拦截。
2. **如何解决同类内部方法调用无法被AOP代理的问题?**
• 使用`AopContext.currentProxy()`获取当前代理对象,并确保`exposeProxy`属性设置为`true`。
3. **切点表达式如何优化?**
• 使用`execution()`定义精确匹配规则(如类路径、方法参数)。
• 通过组合多个小粒度切点实现复杂逻辑,提升复用性。
### **四、应用场景与最佳实践**
1. **典型应用**
• **日志记录**:统一记录方法调用参数和耗时。
• **事务管理**:通过`@Transactional`注解实现声明式事务。
• **权限校验**:在方法执行前拦截并验证用户权限。
2. **性能优化建议**
• 避免在频繁调用的方法上使用`@Around`,因其可能增加调用链深度。
• 优先使用JDK动态代理(若目标类实现接口),减少CGLIB生成子类的开销。
### **五、扩展思考(加分项)**
• **动态代理的底层实现**:JDK动态代理通过反射调用目标方法,CGLIB通过字节码增强生成子类。
• **AOP与OOP的关系**:AOP弥补了OOP在横切关注点上的不足,实现代码解耦。
Transaction
### **一、Spring事务的核心原理**
1. **声明式事务与AOP动态代理**
Spring事务管理基于AOP实现,通过动态代理在运行时为被`@Transactional`注解标记的方法生成代理对象,拦截方法调用并管理事务边界。
• **核心接口**:`PlatformTransactionManager`(如`DataSourceTransactionManager`、`JpaTransactionManager`)定义了事务的开启、提交、回滚操作。
• **事务属性**:包括传播行为、隔离级别、超时时间、只读模式等,通过注解或XML配置定义。
2. **编程式事务与声明式事务的区别**
• **编程式事务**:通过`TransactionTemplate`或直接调用`PlatformTransactionManager`的API手动控制事务,灵活性高但代码侵入性强。
• **声明式事务**:通过注解或XML配置实现事务逻辑与业务代码解耦,推荐使用。
### **二、事务传播行为(Propagation)**
1. **7种传播行为**
定义事务方法之间的嵌套调用规则(通过`propagation`属性配置):
• `REQUIRED`(默认):当前有事务则加入,否则新建事务。
• `REQUIRES_NEW`:无论当前是否存在事务,都新建独立事务。
• `SUPPORTS`:当前有事务则加入,否则以非事务方式执行。
• `NOT_SUPPORTED`:以非事务方式执行,若当前有事务则挂起。
• `MANDATORY`:必须存在事务,否则抛出异常。
• `NEVER`:必须无事务,否则抛出异常。
• `NESTED`:嵌套事务(依赖数据库支持,如MySQL的Savepoint机制)。
### **三、事务隔离级别(Isolation)**
1. **4种隔离级别**
解决并发事务导致的数据一致性问题(通过`isolation`属性配置):
• `DEFAULT`:使用数据库默认隔离级别(如MySQL默认为`REPEATABLE_READ`)。
• `READ_UNCOMMITTED`:可能读到未提交数据(脏读)。
• `READ_COMMITTED`:避免脏读,允许不可重复读。
• `REPEATABLE_READ`:避免脏读和不可重复读,允许幻读。
• `SERIALIZABLE`:最高隔离级别,完全串行化。
### **四、事务配置方式**
**注解配置**
使用`@Transactional`注解标记类或方法,需在配置类启用`@EnableTransactionManagement`:
### **五、常见问题与解决方案**
1. **事务失效场景**
• **自调用问题**:同类内部方法调用(未经过代理对象),需通过`AopContext.currentProxy()`获取代理对象调用。
• **异常未抛出**:默认仅对`RuntimeException`回滚,若需捕获其他异常,需配置`rollbackFor`属性。
• **非public方法**:`@Transactional`仅对public方法生效。
2. **事务超时与只读模式**
• `timeout`:事务超时时间(秒),超时后自动回滚。
• `readOnly`:标记事务为只读,优化数据库性能(如启用查询缓存)。
### **六、扩展思考(加分项)**
• **分布式事务**:Spring整合Seata、XA协议等方案,解决跨服务事务一致性问题。
• **事务同步机制**:通过`TransactionSynchronizationManager`注册回调,实现事务提交后的资源清理或日志记录。
Redis
数据结构
· 点赞,关注,统计,缓存
Kafka
生产者消费者模式
· 系统通知
ElasticSearch
索引结构
· 全文搜索
Caffeine
缓存
Quartz
线程池
项目相关(模块)
注册登录与状态保持
用户表结构
| 字段名 | 作用 | 备注 |
|---|---|---|
id |
唯一标识用户 | 主键自增 |
username |
用户名 | 唯一约束 |
password |
加密后的密码 | MD5(明文密码 + salt) |
salt |
随机盐值 | 增加密码复杂度,防彩虹表攻击 |
email |
用户邮箱 | 用于激活账号或找回密码 |
status |
账号状态(激活/未激活) | 默认未激活,需邮件验证 |
activation_code |
激活码 | 随机字符串,用于激活账号 |
header_url |
用户头像URL | 默认提供通用头像 |
### **一、用户表设计与密码安全**
#### **2. 密码加密流程**
- **注册时**:
1. 用户输入明文密码。
2. 系统生成随机`salt`(如UUID)。
3. 计算加密密码:`MD5(明文密码 + salt)`。
4. 存储`salt`和加密后的`password`到数据库。
- **登录验证**:
1. 用户输入密码。
2. 从数据库取出`salt`。
3. 计算输入密码的MD5值并与数据库存储值比对。
#### **记忆点**
- **盐值作用**:相同密码不同盐值 → 不同加密结果,防止彩虹表攻击。
- **MD5缺陷**:虽不可逆,但建议生产环境用更安全的算法(如bcrypt)。
---
### **二、分布式Session解决方案(Redis)**
#### **1. 验证码存储优化**
- **问题**:Session在分布式环境下无法共享。
- **解决方案**:
- **生成验证码**时,创建临时`owner`(如UUID)存入Cookie。
- Redis存储:`Key=kaptcha:owner, Value=验证码文本`,设置过期时间。
- **验证时**:从Cookie取`owner`,拼接Redis Key获取验证码比对。
#### **2. 登录凭证(Token)优化**
- **旧方案**:`login_ticket`表存储凭证,每次请求查数据库。
- **新方案**:
- **登录成功**:生成随机`ticket`(UUID),将`LoginTicket`对象序列化为JSON存入Redis。
- Key格式:`ticket:随机字符串`,Value:用户信息JSON,设置过期时间。
- **验证请求**:从Cookie取`ticket`,查Redis获取用户信息。
- **退出登录**:修改Redis中该`ticket`状态为失效(不删除,保留记录)。
#### **记忆点**
- **Redis优势**:高性能、支持过期时间、天然解决Session共享。
- **Key设计**:使用前缀区分数据类型(如`kaptcha:`、`ticket:`)。
---
### **三、用户信息缓存与拦截器**
#### **1. 用户信息缓存策略**
- **查询用户**:
1. 先查Redis(Key=`user:用户ID`)。
2. 未命中则查数据库,并写入Redis。
3. 更新用户信息时,删除Redis缓存(下次查询自动重建)。
#### **2. 拦截器流程**
1. **preHandle**:
- 从Cookie获取`ticket`。
- 查Redis获取用户信息,存入`ThreadLocal`(线程隔离,本次请求全局可用)。
2. **postHandle**:
- 将用户信息传递给视图(如显示登录状态)。
3. **afterCompletion**:
- 清理`ThreadLocal`,防止内存泄漏。
#### **记忆点**
- **ThreadLocal作用**:在一次请求中跨方法传递用户信息。
- **拦截顺序**:preHandle → 业务处理 → postHandle → afterCompletion。
---
### **四、高频面试问题与扩展**
#### **2. Redis相关**
- **数据结构**:使用String类型存储验证码和登录凭证。
- **高可用**:哨兵模式或集群模式避免单点故障。
- **缓存穿透**:缓存空值或布隆过滤器防止恶意查询。
#### **3. 安全增强**
- **Cookie安全**:设置`HttpOnly`(防XSS)、`Secure`(仅HTTPS传输)。
- **登录限流**:Redis记录失败次数,超过阈值锁定账号。[滑动窗口-固定时间段内登陆错误次数]
#### **4. ThreadLocal原理**
- **每个线程独立副本**:通过`ThreadLocalMap`存储数据,Key为ThreadLocal实例。
- **内存泄漏风险**:使用弱引用Key,但需手动调用`remove()`。
---
### **五、流程图辅助记忆**
1. **用户登录流程**:
用户输入 → 验证码校验(Redis) → 密码验证(MD5+salt) → 生成ticket(Redis) → 返回Cookie
2. **请求处理流程**:
请求 → 拦截器获取ticket → Redis查用户 → ThreadLocal存用户 → 业务处理 → 清理ThreadLocal
分布式Session方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 粘性Session | 实现简单 | 负载不均,服务器宕机丢失Session |
| Session同步 | 无需额外存储 | 服务器耦合,性能开销大 |
| Redis存储 | 高性能、解耦、支持分布式 | 需维护Redis高可用 |
### **为什么需要分布式Session?**
在传统单机服务器中,用户登录后的会话数据(如用户ID、登录状态)存储在服务器的**内存(Session)**中。但分布式环境下有多台服务器,如果用户第一次请求落在服务器A,Session存在A的内存中;第二次请求被负载均衡到服务器B,服务器B没有这个Session,用户会被视为“未登录”。
**这就是分布式Session问题**:Session无法在多个服务器之间共享。
---
### **Redis如何解决分布式Session问题?**
**核心思想**:将会话数据(如登录凭证、验证码)存储到一个**集中式、独立的存储服务(如Redis)**中,所有服务器都从Redis读写数据,而不是各自的内存。
---
### **具体实现步骤(以登录为例)**
#### **1. 生成并存储验证码**
- **问题**:验证码需要频繁访问,且不能存在服务器内存(否则分布式环境下无法共享)。
- **解决方案**:
1. 用户访问验证码接口时,生成一个**唯一标识(owner)**(如随机UUID),通过Cookie返回给浏览器。
2. 将验证码文本存入Redis:
```redis
Key: `kaptcha:owner`(如 `kaptcha:abc123`)
Value: 验证码文本(如 `3A4B`)
Expire: 5分钟(自动过期)
```
3. 用户登录时,浏览器携带Cookie中的`owner`,服务器拼接Redis Key,取出验证码进行校验。
#### **2. 用户登录成功后生成Token**
- **问题**:传统Session存储用户登录状态在服务器内存,分布式环境下无法共享。
- **解决方案**:
1. 用户登录成功时,生成一个**随机Token**(如UUID),作为用户凭证。
2. 将Token和用户信息存入Redis:
```redis
Key: `ticket:token`(如 `ticket:xyz789`)
Value: 用户信息JSON(如 `{"userId": 123, "username": "Alice"}`)
Expire: 7天(根据业务设定)
```
3. 将Token通过Cookie返回给浏览器:
```http
Set-Cookie: ticket=xyz789; Path=/; HttpOnly; Max-Age=604800
```
#### **3. 后续请求验证登录状态**
1. 用户发起请求时,浏览器自动携带Cookie中的`ticket=xyz789`。
2. 服务器从Redis查询Key `ticket:xyz789`:
- 如果存在且未过期 → 用户已登录,取出用户信息。
- 如果不存在或已过期 → 用户未登录。
3. 将用户信息存储到`ThreadLocal`中,供本次请求全局使用。
---
### **Redis方案的优势**
| **优势** | **说明** |
|-------------------------|--------------------------------------------------------------------------|
| **高性能** | Redis基于内存,读写速度极快(10万+/秒)。 |
| **天然支持分布式** | 所有服务器共享同一个Redis,彻底解决Session共享问题。 |
| **自动过期** | 可设置Key的过期时间,自动清理无用数据(如验证码5分钟过期,Token7天过期)。|
| **高可用** | Redis支持主从复制、哨兵模式、集群模式,避免单点故障。 |
---
### **对比传统Session方案**
| **场景** | **传统Session** | **Redis方案** |
|------------------------|------------------------------------------|--------------------------------------------|
| 单机服务器 | 简单高效,无需额外依赖。 | 过度设计,但无性能问题。 |
| 分布式环境 | Session无法共享,用户频繁重新登录。 | 所有服务器共享Redis,用户状态一致。 |
| 性能 | 依赖服务器内存,性能高但扩展性差。 | 高性能、可水平扩展。 |
| 安全性 | Session可能被服务器内存泄漏暴露。 | Redis可设置密码、SSL传输,数据更安全。 |
---
### **关键代码示例(Java + Spring)
#### **1. 生成验证码并存入Redis**
```java
// 生成唯一标识owner(如UUID)
String owner = generateRandomOwner();
// 生成验证码文本
String text = kaptcha.createText();
// 存入Redis,5分钟过期
redisTemplate.opsForValue().set("kaptcha:" + owner, text, 5, TimeUnit.MINUTES);
// 将owner通过Cookie返回浏览器
response.addCookie(new Cookie("kaptchaOwner", owner));
```
#### **2. 用户登录成功后生成Token**
```java
// 生成随机Token
String ticket = UUID.randomUUID().toString();
// 构造Redis Key
String redisKey = "ticket:" + ticket;
// 用户信息序列化为JSON
String userJson = JSON.toJSONString(user);
// 存入Redis,7天过期
redisTemplate.opsForValue().set(redisKey, userJson, 7, TimeUnit.DAYS);
// 将Token通过Cookie返回浏览器
response.addCookie(new Cookie("ticket", ticket));
```
#### **3. 拦截器验证登录状态**
```java
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 从Cookie中获取Token
String ticket = getCookieValue(request, "ticket");
if (ticket != null) {
// 从Redis查询用户信息
String userJson = redisTemplate.opsForValue().get("ticket:" + ticket);
if (userJson != null) {
User user = JSON.parseObject(userJson, User.class);
// 将用户信息存入ThreadLocal
hostHolder.setUser(user);
}
}
return true;
}
```
---
### **常见面试问题**
1. **为什么不用数据库存储Session?**
- 数据库读写速度慢(尤其是高并发场景),Redis基于内存,性能高几个数量级。
2. **Redis宕机怎么办?**
- 使用Redis集群(如哨兵模式、Cluster模式)保证高可用;短暂宕机时,可降级从数据库读取(牺牲性能保可用性)。
3. **Token如何防止被盗用?**
- Cookie设置`HttpOnly`防止XSS攻击;`Secure`属性确保仅通过HTTPS传输;Token绑定IP或设备信息。
---
### **总结**
- **核心逻辑**:用Redis替代服务器内存,集中存储会话数据。
- **关键步骤**:生成唯一标识 → 数据存入Redis → 通过Cookie传递标识 → 后续请求从Redis查询数据。
- **优势**:高性能、分布式友好、自动过期、高可用。
发布帖子与敏感词过滤
## **一、发布帖子与敏感词过滤**
### **1. AJAX异步发帖**
#### **核心原理**
- **AJAX**(Asynchronous JavaScript and XML):通过浏览器与服务器**异步通信**,实现页面局部更新,无需刷新整个页面。
- **现代实现**:通常使用 **JSON** 替代 XML,结合 `fetch` 或 `axios` 库实现。
#### **代码示例**
```javascript
// 前端:使用fetch发送异步请求
document.getElementById("post-btn").addEventListener("click", async () => {
const title = document.getElementById("title").value;
const content = document.getElementById("content").value;
const response = await fetch("/post/create", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title, content })
});
const result = await response.json();
if (result.success) {
alert("发帖成功!");
// 局部刷新帖子列表
}
});
```
#### **优势**
- **用户体验**:无需刷新页面,操作更流畅。
- **性能优化**:减少网络传输数据量。
---
### **2. 敏感词过滤(Trie树实现)**
#### **Trie树原理**
- **字典树**:一种树形结构,用于高效检索字符串集合。
- **敏感词过滤流程**:
1. **构建Trie树**:读取敏感词文件,逐字符插入树中。
2. **过滤文本**:遍历待过滤文本,匹配Trie树中的敏感词并替换。
#### **代码示例(简化版)**
```java
public class TrieNode {
private Map<Character, TrieNode> children = new HashMap<>();
private boolean isEnd; // 标记是否为敏感词结尾
public void insert(String word) {
TrieNode node = this;
for (char c : word.toCharArray()) {
node = node.children.computeIfAbsent(c, k -> new TrieNode());
}
node.isEnd = true;
}
public String filter(String text) {
StringBuilder result = new StringBuilder();
int start = 0; // 敏感词起始位置
while (start < text.length()) {
TrieNode node = this;
int end = start;
while (end < text.length() && node.children.containsKey(text.charAt(end))) {
node = node.children.get(text.charAt(end));
end++;
}
if (node.isEnd) {
result.append("***"); // 替换为***
start = end;
} else {
result.append(text.charAt(start));
start++;
}
}
return result.toString();
}
}
```
#### **记忆点**
- **Trie树优势**:快速匹配敏感词,时间复杂度 O(n)。
- **扩展优化**:支持多模式匹配(如AC自动机)、动态更新敏感词。
---
### **3. XSS防御**
- **原理**:转义用户输入的HTML标签,防止恶意脚本执行。
- **实现**:
```java
// 使用工具类转义HTML标签
public class HTMLUtils {
public static String escape(String html) {
return html.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll("\"", """);
}
}
```
发表评论与私信
## **二、评论与私信功能**
### **1. 评论表设计**
| 字段 | 说明 |
|---------------|-------------------------------|
| `entity_type` | 评论目标类型(1=帖子,2=评论)|
| `entity_id` | 目标ID(如帖子ID或评论ID) |
| `target_id` | 被回复的用户ID(0表示无回复) |
| `content` | 评论内容(已过滤敏感词) |
#### **事务管理**
- **问题**:添加评论和更新帖子评论数需保证原子性。
- **实现**:使用 `@Transactional` 注解确保事务。
```java
@Transactional
public void addComment(Comment comment) {
commentMapper.insert(comment);
postMapper.updateCommentCount(comment.getEntityId(), 1); // 评论数+1
}
```
---
### **2. 私信表与会话管理**
| 字段 | 说明 |
|-----------------|--------------------------------|
| `conversion_id` | 会话ID(如 `111_222`,小ID在前)|
| `status` | 0=未读,1=已读,2=删除 |
#### **复杂SQL:查询最新私信**
```sql
SELECT id, from_id, to_id, conversion_id, content
FROM message
WHERE id IN (
SELECT MAX(id)
FROM message
WHERE status != 2
AND (from_id = #{userId} OR to_id = #{userId})
GROUP BY conversion_id
ORDER BY id DESC
LIMIT 10
```
#### **逻辑拆解**
1. **子查询**:找到每个会话 (`conversion_id`) 的最新消息ID。
2. **主查询**:根据ID获取完整的消息内容。
3. **过滤条件**:排除已删除消息 (`status != 2`),按用户ID筛选。
---
## **三、Spring AOP记录日志**
### **核心概念**
- **AOP(面向切面编程)**:通过动态代理,在方法执行前后插入通用逻辑(如日志、权限)。
- **关键注解**:
- `@Aspect`:标记切面类。
- `@Pointcut`:定义切入点(哪些方法需要拦截)。
- `@Before`/`@After`:定义通知类型。
### **代码实现**
```java
@Aspect
@Component
public class ServiceLogAspect {
private static final Logger logger = LoggerFactory.getLogger(ServiceLogAspect.class);
@Pointcut("execution(* com.example.service.*.*(..))")
public void pointcut() {}
@Before("pointcut()")
public void logBefore(JoinPoint joinPoint) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) return;
HttpServletRequest request = attributes.getRequest();
String ip = request.getRemoteHost();
String method = joinPoint.getSignature().toShortString();
logger.info("IP:[{}] 调用方法:[{}]", ip, method);
}
}
```
### **常见面试问题**
1. **AOP与拦截器区别**?
- **AOP**:可切入任意方法(如Service层),功能更灵活。
- **拦截器**:主要针对Controller层请求,处理HTTP相关逻辑。
2. **如何获取请求IP**?
- 通过 `RequestContextHolder` 获取当前请求的 `HttpServletRequest`。
---
## **四、记忆辅助与流程图**
### **敏感词过滤流程**
```
用户输入 → 转义HTML标签 → Trie树匹配敏感词 → 替换为*** → 存储到数据库
```
### **AOP日志流程**
```
请求进入Controller → 调用Service方法 → AOP拦截 → 记录日志 → 执行Service方法
```
### **对比表格:Trie树 vs. 其他数据结构**
| 数据结构 | 适用场景 | 时间复杂度 | 空间复杂度 |
|------------|------------------|------------|------------|
| **Trie树** | 多模式字符串匹配 | O(n) | 较高 |
| **哈希表** | 单字符串精确匹配 | O(1) | 低 |
### **二、私信表与会话管理详解**
---
#### **1. 私信表设计逻辑**
私信功能的核心是 **管理用户之间的点对点消息**,设计时需要解决以下问题:
- **如何高效存储和查询用户间的历史消息?**
- **如何快速获取某个会话的最新消息?**
- **如何统计未读消息数量?**
##### **私信表字段说明**
| 字段名 | 作用 | 示例值 |
|-----------------|------------------------------------------------------------------------------------------|------------------------|
| `id` | 消息唯一标识(主键自增) | `1` |
| `from_id` | 发送方用户ID | `111` |
| `to_id` | 接收方用户ID | `112` |
| `conversion_id` | **会话ID**(用户A和用户B的所有消息属于同一个会话,由用户ID生成唯一标识) | `111_112`(小ID在前) |
| `content` | 消息内容 | `"你好!"` |
| `status` | 消息状态:`0=未读`,`1=已读`,`2=删除` | `0` |
| `create_time` | 消息创建时间 | `2023-10-01 10:00:00` |
---
#### **2. 关键设计点解析**
##### **(1) 为什么用 `conversion_id` 标识会话?**
- **问题**:用户A(ID=111)和用户B(ID=112)之间的所有消息属于同一会话,如何高效查询?
- **解决方案**:
- **生成规则**:将两个用户ID按升序拼接(如 `111_112`),确保无论A发消息给B,还是B发消息给A,`conversion_id` 始终相同。
- **优势**:
- 直接通过 `conversion_id` 查询某个会话的所有消息。
- 避免维护额外的“会话表”,减少数据库复杂度。
##### **(2) `status` 字段的用途**
- **未读消息统计**:通过 `status=0` 快速筛选某个会话的未读消息数量。
- **逻辑删除**:用户删除消息时标记为 `status=2`,而非物理删除,保留数据完整性。
---
#### **3. 复杂SQL详解**
##### **目标**:查询当前用户的会话列表,每个会话仅显示最新一条消息。
```sql
SELECT id, from_id, to_id, conversion_id, content, status, create_time
FROM message
WHERE id IN (
SELECT MAX(id)
FROM message
WHERE status != 2
AND (from_id = #{userId} OR to_id = #{userId})
GROUP BY conversion_id
)
ORDER BY id DESC
LIMIT 0, 10
```
##### **拆解步骤**:
1. **子查询**:找到每个会话的最新消息ID
```sql
SELECT MAX(id)
FROM message
WHERE status != 2 -- 排除已删除的消息
AND (from_id = 111 OR to_id = 111) -- 用户111参与的所有会话
GROUP BY conversion_id -- 按会话分组
```
- **结果**:每个 `conversion_id` 对应的最大 `id`(即最新消息的ID)。
2. **主查询**:获取完整消息内容
```sql
SELECT *
FROM message
WHERE id IN (子查询结果) -- 通过子查询得到的最新消息ID
ORDER BY id DESC -- 按时间倒序排列
LIMIT 0, 10 -- 分页(每页10条)
```
##### **执行结果示例**:
| id | from_id | to_id | conversion_id | content | status | create_time |
|-----|---------|-------|---------------|----------|--------|----------------------|
| 100 | 112 | 111 | 111_112 | "好的!" | 1 | 2023-10-01 12:00:00 |
| 99 | 111 | 113 | 111_113 | "在吗?" | 0 | 2023-10-01 11:30:00 |
---
#### **4. 优化与扩展**
##### **(1) 性能优化**
- **索引设计**:
- 为 `conversion_id` 和 `status` 添加联合索引:`INDEX idx_conversion_status (conversion_id, status)`
- 为 `from_id` 和 `to_id` 添加索引:`INDEX idx_user (from_id, to_id)`
##### **(2) 未读消息数量统计**
- **查询某个会话的未读消息数**:
```sql
SELECT COUNT(*)
FROM message
WHERE conversion_id = '111_112'
AND to_id = 111
AND status = 0;
```
##### **(3) 分页优化**
- **避免深度分页**:使用 `WHERE id < #{lastId}` 替代 `LIMIT 100000, 10`,提升性能。
---
#### **5. 对比其他设计方案**
| **方案** | **优点** | **缺点** |
|-----------------------|-----------------------------------|-----------------------------------|
| **当前方案** | 无需维护会话表,直接通过消息表查询 | 复杂查询可能较慢(需子查询+分组) |
| **独立会话表** | 查询会话列表更快 | 需维护会话表和消息表的关联关系 |
---
#### **6. 实际应用场景**
- **场景1:用户打开私信列表**
- 执行复杂SQL → 展示所有会话的最新消息。
- 前端显示未读数量(通过额外查询统计)。
- **场景2:用户进入某个会话**
- 查询 `conversion_id=111_112` 的所有消息,按时间排序。
- 将 `status=0` 的消息标记为已读(`UPDATE message SET status=1`)。
---
### **总结**
- **设计核心**:通过 `conversion_id` 标识会话,减少冗余表,简化查询逻辑。
- **SQL关键点**:子查询 + 分组获取最新消息,结合索引优化性能。
- **扩展性**:通过状态字段 (`status`) 支持消息删除、未读统计等业务需求。
Redis实现点赞关注
### **一、Redis实现点赞功能**
#### **1. 核心设计**
- **目标**:快速判断用户是否点赞,统计点赞数,记录被点赞用户。
- **数据结构**:使用 **Redis Set** 存储点赞用户ID,**String** 存储用户总赞数。
#### **2. Key设计**
| 场景 | Key格式 | 值类型 | 作用 |
|--------------------------|----------------------------------|--------------|--------------------------|
| 实体(帖子、评论)的点赞 | `like:entity:{entityType}:{id}` | Set(用户ID)| 存储对某实体的所有点赞用户 |
| 用户收到的总赞数 | `like:user:{userId}` | String(整数)| 记录用户被点赞的总次数 |
#### **3. 关键操作**
- **点赞**:
```java
// 判断是否已点赞
Boolean isLiked = redisTemplate.opsForSet().isMember(likeKey, userId);
if (isLiked) {
// 取消点赞
redisTemplate.opsForSet().remove(likeKey, userId);
redisTemplate.opsForValue().decrement(userLikeKey); // 用户总赞数-1
} else {
// 点赞
redisTemplate.opsForSet().add(likeKey, userId);
redisTemplate.opsForValue().increment(userLikeKey); // 用户总赞数+1
}
- 统计点赞数:
Long likeCount = redisTemplate.opsForSet().size(likeKey);
4. 优势
- 高性能:Set判断是否存在时间复杂度O(1)。
- 扩展性:可扩展为点踩(用负数存储)。
二、Redis实现关注功能
1. 核心设计
- 目标:记录用户的关注列表(关注了谁)和粉丝列表(谁关注了我),支持按时间排序。
- 数据结构:使用 Redis ZSet(有序集合),以时间戳为分数(Score)。
2. Key设计
| 场景 | Key格式 | 值类型 | 作用 |
|---|---|---|---|
| 用户的关注列表 | followee:{userId}:{entityType} |
ZSet(实体ID, 时间戳) | 存储用户关注的实体(用户、帖子等) |
| 实体的粉丝列表 | follower:{entityType}:{entityId} |
ZSet(用户ID, 时间戳) | 存储关注某实体的所有用户 |
3. 关键操作
-
关注:
// 使用事务保证原子性 redisTemplate.execute(new SessionCallback<>() { @Override public Object execute(RedisOperations operations) { operations.multi(); // 用户A的关注列表添加实体B operations.opsForZSet().add(followeeKey, entityId, System.currentTimeMillis()); // 实体B的粉丝列表添加用户A operations.opsForZSet().add(followerKey, userId, System.currentTimeMillis()); return operations.exec(); } }); -
取消关注:
redisTemplate.opsForZSet().remove(followeeKey, entityId); redisTemplate.opsForZSet().remove(followerKey, userId); -
查询共同关注:
// 取两个用户的关注列表交集 Set<Long> commonFollowees = redisTemplate.opsForZSet().intersect( followeeKeyA, followeeKeyB );
4. 优势
- 时间排序:通过ZSet的分数(时间戳)实现按关注时间排序。
- 高效查询:ZSet支持范围查询、交集(共同关注)等操作。
三、场景题:关系型数据库设计
1. 表设计
| 字段名 | 类型 | 说明 |
|---|---|---|
id |
BIGINT | 主键 |
user_id |
BIGINT | 用户ID(关注者) |
fans_id |
BIGINT | 粉丝ID(被关注者) |
is_mutual |
TINYINT | 是否互相关注(0否,1是) |
2. 索引设计
- 主键索引:
PRIMARY KEY (id) - 联合索引:
INDEX idx_user_fans (user_id, fans_id) -- 加速“我关注了谁”查询 INDEX idx_fans_user (fans_id, user_id) -- 加速“谁关注了我”查询
3. 查询语句
-
谁关注了我:
SELECT user_id FROM follow WHERE fans_id = #{myUserId}; -
我关注了谁:
SELECT fans_id FROM follow WHERE user_id = #{myUserId}; -
谁与我互相关注:
SELECT a.fans_id FROM follow a JOIN follow b ON a.user_id = b.fans_id AND a.fans_id = b.user_id WHERE a.user_id = #{myUserId};
四、对比:Redis vs. 关系型数据库
| 场景 | Redis方案 | 关系型数据库方案 |
|---|---|---|
| 点赞 | 高频写入、快速判断状态 | 适合低频场景,数据持久化 |
| 关注列表 | 支持排序、共同关注等复杂操作 | 需复杂SQL实现 |
| 数据一致性 | 最终一致性(需异步同步到数据库) | 强一致性(事务保证) |
| 扩展性 | 天然支持分布式 | 需分库分表 |
五、高频面试问题
-
如何保证Redis和数据库的数据一致性?
- 异步同步:通过消息队列(如Kafka)异步将Redis操作同步到数据库。
- 双写策略:在关键操作(如点赞、关注)中同时更新Redis和数据库(需处理事务回滚)。
-
Redis的ZSet如何实现分页查询?
- 使用
ZRANGEBYSCORE按分数范围查询,结合LIMIT实现分页。
- 使用
-
如何处理大Key问题(如千万级粉丝列表)?
- 分片存储:按实体ID哈希分片到多个Key(如
follower:type:id_1,follower:type:id_2)。 - 冷热分离:近期活跃数据存Redis,历史数据存数据库。
- 分片存储:按实体ID哈希分片到多个Key(如
六、记忆点总结
- 点赞:用Set快速判断状态,String计数。
- 关注:用ZSet存储关注和粉丝列表,时间戳排序。
- 事务:Redis的multi/exec保证原子性,但无回滚。
- 共同关注:Set的
sinter取交集。
#### Kafka实现异步消息
````markdown
## **一、Kafka核心概念与设计**
### **1. Kafka的核心作用**
- **异步解耦**:将耗时操作(如发送通知)与主业务分离,提升系统响应速度。
- **削峰填谷**:应对流量高峰,避免数据库压力过大。
- **保证消息可靠传输**:通过副本机制防止数据丢失。
### **2. Kafka核心术语**
| 术语 | 说明 |
|--------------|----------------------------------------------------------------------|
| **Topic** | 消息类别(如评论、点赞、关注),逻辑隔离不同业务消息。 |
| **Partition**| Topic的分区,提升并发处理能力。 |
| **Producer** | 消息生产者(如触发事件的模块)。 |
| **Consumer** | 消息消费者(如处理通知的模块)。 |
| **Broker** | Kafka服务器节点,存储消息。 |
| **Offset** | 消费者在分区中的消费位置,保证消息不重复消费。 |
---
## **二、系统通知模块实现**
### **1. 整体流程**
用户触发事件(评论/点赞/关注) → 生产者发送消息到Kafka → 消费者监听并处理 → 存入数据库 → 用户查看通知
### **2. 事件对象设计**
```java
public class Event {
private String topic; // 事件类型:comment/like/follow
private int userId; // 触发事件的用户ID(如点赞者)
private int entityType; // 实体类型:1=帖子,2=评论,3=用户
private int entityId; // 实体ID(如帖子ID)
private int entityUserId; // 实体所属用户ID(被点赞/评论的用户)
private Map<String, Object> data; // 扩展字段(如内容摘要)
}
关键字段解析:
entityUserId:接收通知的用户(如被评论的帖子作者)。topic:决定消息路由到哪个主题。
3. 生产者实现
代码示例
@Component
public class EventProducer {
@Autowired
private KafkaTemplate<String, String> kafkaTemplate;
public void fireEvent(Event event) {
// 将事件发布到指定主题
kafkaTemplate.send(event.getTopic(), JSON.toJSONString(event));
}
}
配置要点
- 序列化:消息键值对均使用String序列化(需在
application.yml配置)。spring: kafka: producer: key-serializer: org.apache.kafka.common.serialization.StringSerializer value-serializer: org.apache.kafka.common.serialization.StringSerializer
4. 消费者实现
代码示例
@Component
public class EventConsumer {
@Autowired
private MessageService messageService;
@KafkaListener(topics = {TOPIC_COMMENT, TOPIC_LIKE, TOPIC_FOLLOW})
public void handleMessage(ConsumerRecord<String, String> record) {
// 解析事件
Event event = JSON.parseObject(record.value(), Event.class);
// 构造系统通知
Message message = new Message();
message.setFromId(SYSTEM_USER_ID); // 发送方为系统
message.setToId(event.getEntityUserId()); // 接收方为被操作的用户
message.setConversationId(event.getTopic()); // 会话ID=事件类型
message.setContent(buildContent(event)); // 消息内容(JSON格式)
message.setCreateTime(new Date());
// 存入数据库
messageService.addMessage(message);
}
}
关键配置
spring:
kafka:
consumer:
group-id: notification-group # 消费者组ID
auto-offset-reset: earliest # 从最早的消息开始消费
enable-auto-commit: false # 关闭自动提交Offset
三、核心面试问题与解答
1. 为什么选择Kafka而不是其他消息队列?
- 高吞吐:支持百万级TPS,适合高并发场景。
- 持久化存储:消息可保留多天,防止数据丢失。
- 分布式扩展:通过分区和副本机制支持水平扩展。
2. 如何保证消息不丢失?
| 环节 | 保障措施 |
|---|---|
| 生产者 | 设置acks=all,确保所有副本确认后才返回成功。 |
| Broker | 设置副本因子replication.factor>=3,防止单点故障。 |
| 消费者 | 关闭自动提交Offset,处理完业务逻辑后手动提交。 |
3. 如何处理消息重复消费?
- 幂等性设计:消费者逻辑需支持重复处理(如数据库唯一索引防止重复插入)。
- 业务去重:在消息体中添加唯一ID,消费前检查是否已处理。
4. 如何提高消费者吞吐量?
- 增加分区数:分区数决定最大并行度。
- 多消费者实例:同一消费者组内启动多个实例,分摊负载。
- 调整参数:增大
max.poll.records(单次拉取消息数)。
四、性能优化与监控
1. 分区策略
- 默认策略:轮询分配,保证分区负载均衡。
- 自定义策略:根据业务键(如用户ID)哈希到特定分区,保证相同用户的消息顺序性。
2. 监控指标
| 指标 | 工具 | 作用 |
|---|---|---|
| 消息堆积量 | Kafka Manager | 发现消费延迟,及时扩容消费者。 |
| 生产/消费速率 | Prometheus + Grafana | 实时监控系统吞吐量。 |
| Broker负载 | JConsole | 监控CPU、内存、磁盘IO。 |
五、常见问题排查
1. 消费者收不到消息
- 检查消费者组ID是否冲突。
- 确认Topic存在且分区已分配。
- 检查
auto-offset-reset配置(earliest或latest)。
2. 消息处理失败
- 重试机制:捕获异常后重新放入队列或记录到死信队列(DLQ)。
- 日志记录:详细记录消息内容及错误信息,便于后续排查。
六、流程图辅助记忆
消息处理流程
用户操作 → 生成Event → 生产者发送到Kafka → 消费者监听Topic → 解析Event → 构造Message → 存入数据库 → 用户查看
Kafka消息生命周期
Producer → Broker(持久化) → Consumer(处理并提交Offset)
#### ES实现网站搜索功能
````markdown
### **Elasticsearch 实现搜索功能与 Redis 数据统计模块详解**
---
### **一、Elasticsearch 搜索服务**
#### **1. Elasticsearch 核心概念**
| **术语** | **说明** |
|---------------|--------------------------------------------------------------------------|
| **索引** | 类似数据库中的表,存储结构化的文档数据(如 `post_index` 存储帖子信息)。 |
| **文档** | 索引中的一条数据(如一篇帖子),以 JSON 格式存储。 |
| **分片** | 索引的分区,支持水平扩展(如一个索引分为3个分片)。 |
| **副本** | 分片的备份,提高可用性和查询性能。 |
| **倒排索引** | 通过关键词快速定位文档的机制(如“Java”映射到包含该词的文档ID列表)。 |
#### **2. Spring Boot 整合 Elasticsearch**
##### **配置步骤**
1. **添加依赖**:
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
```
2. **配置文件**:
```yaml
spring:
elasticsearch:
uris: http://localhost:9200 # ES服务器地址
```
3. **定义实体类**:
```java
@Document(indexName = "post_index")
public class Post {
@Id
private String id;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String title;
@Field(type = FieldType.Text, analyzer = "ik_max_word")
private String content;
@Field(type = FieldType.Integer)
private Integer userId;
@Field(type = FieldType.Date)
private Date createTime;
}
```
4. **定义 Repository**:
```java
public interface PostRepository extends ElasticsearchRepository<Post, String> {
// 自定义高亮查询
@Highlight(fields = @HighlightField(name = "title"))
SearchHits<Post> findByTitleOrContent(String title, String content, Pageable pageable);
}
```
#### **3. 异步同步数据到 Elasticsearch**
##### **流程设计**
1. **发布事件到 Kafka**:
```java
// 发帖或更新帖子时触发事件
public void publishPostEvent(Post post) {
Event event = new Event();
event.setTopic("post_publish");
event.setData(Map.of("postId", post.getId()));
kafkaTemplate.send(event.getTopic(), JSON.toJSONString(event));
}
```
2. **消费事件并更新 ES**:
```java
@KafkaListener(topics = "post_publish")
public void handlePostPublish(ConsumerRecord<String, String> record) {
Event event = JSON.parseObject(record.value(), Event.class);
Post post = postService.getPostById(event.getData().get("postId"));
postRepository.save(post); // 保存或更新到ES
}
```
#### **4. 高亮显示搜索结果**
##### **代码实现**
```java
public List<Post> searchPosts(String keyword, int page, int size) {
// 构造查询条件
NativeSearchQuery query = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.multiMatchQuery(keyword, "title", "content"))
.withHighlightFields(
new HighlightBuilder.Field("title").preTags("<em>").postTags("</em>"),
new HighlightBuilder.Field("content").preTags("<em>").postTags("</em>")
)
.withPageable(PageRequest.of(page, size))
.build();
SearchHits<Post> searchHits = elasticsearchRestTemplate.search(query, Post.class);
return searchHits.stream()
.map(hit -> {
Post post = hit.getContent();
// 设置高亮内容
hit.getHighlightFields().forEach((field, fragments) -> {
if (field.equals("title")) post.setTitle(fragments.get(0));
if (field.equals("content")) post.setContent(fragments.get(0));
});
return post;
})
.collect(Collectors.toList());
}
```
---
### **二、Redis 高级数据统计**
#### **1. HyperLogLog(UV统计)**
##### **核心特性**
- **用途**:统计独立访客数(UV),基于IP去重。
- **优势**:固定占用12KB内存,误差率0.81%。
- **命令**:`PFADD`(添加元素)、`PFCOUNT`(统计总数)、`PFMERGE`(合并统计)。
##### **实现代码**
```java
// 记录单日UV
public void recordUV(String ip) {
String key = "uv:" + LocalDate.now();
redisTemplate.opsForHyperLogLog().add(key, ip);
}
// 统计区间UV
public long calculateUV(LocalDate start, LocalDate end) {
String unionKey = "uv:" + start + ":" + end;
List<String> keys = new ArrayList<>();
LocalDate date = start;
while (!date.isAfter(end)) {
keys.add("uv:" + date);
date = date.plusDays(1);
}
// 合并多日数据
redisTemplate.opsForHyperLogLog().union(unionKey, keys.toArray(new String[0]));
return redisTemplate.opsForHyperLogLog().size(unionKey);
}
```
#### **2. Bitmap(DAU统计)**
##### **核心特性**
- **用途**:精确统计日活跃用户(DAU),基于用户ID去重。
- **优势**:每位代表一个用户,节省内存(100万用户仅需122KB)。
- **命令**:`SETBIT`(标记用户)、`BITCOUNT`(统计活跃数)、`BITOP`(位运算)。
##### **实现代码**
```java
// 记录单日DAU
public void recordDAU(int userId) {
String key = "dau:" + LocalDate.now();
redisTemplate.opsForValue().setBit(key, userId, true);
}
// 统计区间DAU(OR运算:任意一天活跃即算活跃)
public long calculateDAU(LocalDate start, LocalDate end) {
String unionKey = "dau:" + start + ":" + end;
List<String> keys = new ArrayList<>();
LocalDate date = start;
while (!date.isAfter(end)) {
keys.add("dau:" + date);
date = date.plusDays(1);
}
// 合并多日数据(按位或运算)
redisTemplate.execute(new DefaultRedisScript<>("return redis.call('BITOP', 'OR', KEYS[1], unpack(ARGV))"),
Collections.singletonList(unionKey), keys.toArray());
return redisTemplate.execute(new DefaultRedisScript<>("return redis.call('BITCOUNT', KEYS[1])"),
Collections.singletonList(unionKey));
}
```
---
### **三、高频面试问题**
#### **1. ES 如何保证数据一致性?**
- **异步双写**:通过Kafka异步同步数据库和ES,容忍短暂不一致。
- **补偿机制**:定期检查数据库与ES差异,修复不一致数据。
#### **2. HyperLogLog 为什么适合UV统计?**
- **空间效率**:12KB固定大小,适合海量数据。
- **去重能力**:自动去重,无需存储完整IP列表。
#### **3. Bitmap 用户ID较大时如何处理?**
- **偏移量优化**:使用 `userId % 1,000,000` 作为位偏移,分多个Key存储。
#### **4. 如何优化ES搜索性能?**
- **分片策略**:根据数据量设置合理分片数(建议每个分片10-50GB)。
- **冷热分离**:高频访问数据存SSD,历史数据存HDD。
- **缓存优化**:启用ES的查询缓存和文件系统缓存。
---
### **四、流程图辅助记忆**
#### **ES数据同步流程**
```
用户发帖 → 保存到数据库 → 发送事件到Kafka → 消费者读取事件 → 更新到ES → 用户搜索时查询ES
```
#### **UV/DAU统计流程**
```
用户访问 → 拦截器记录UV(IP)和DAU(用户ID) → Redis存储 → 统计时合并计算
```
Quartz实现定时热帖排行
### **Spring Quartz 实现定时热帖排行模块详解**
---
### **一、定时任务技术选型对比**
| **技术** | **优点** | **缺点** | **适用场景** |
|---------------------------|-----------------------------------|-----------------------------------|---------------------------|
| **ScheduledExecutorService** | 简单轻量,无需额外依赖 | 不支持分布式,任务配置无法持久化 | 单机简单任务 |
| **ThreadPoolTaskScheduler** | 集成Spring,支持Cron表达式 | 不支持分布式,任务无法共享 | 单机复杂定时任务 |
| **Quartz** | 支持分布式、任务持久化到数据库 | 配置较复杂,依赖数据库 | 分布式环境下的定时任务 |
---
### **二、Quartz 核心组件与流程**
#### **1. 核心组件**
| **组件** | **作用** |
|----------------|--------------------------------------------------------------------------|
| **Job** | 定义任务逻辑(如计算热帖分数),需实现 `execute` 方法。 |
| **JobDetail** | 封装Job的配置信息(如任务名称、组别)。 |
| **Trigger** | 定义任务触发规则(如每隔10分钟执行一次)。 |
| **Scheduler** | 调度器,负责绑定JobDetail和Trigger,管理任务生命周期。 |
#### **2. 数据库表(关键表)**
| **表名** | **作用** |
|------------------|-----------------------------------|
| `qrtz_job_details` | 存储JobDetail配置信息。 |
| `qrtz_triggers` | 存储Trigger配置信息。 |
| `qrtz_cron_triggers` | 存储Cron表达式触发的任务信息。 |
---
### **三、热帖排行实现步骤**
#### **1. 热帖分数计算公式**
```
分数 = log(精华分 + 评论数×10 + 点赞数×2 + 收藏数×2) + (发布时间戳 - 纪元时间戳)
```
- **设计逻辑**:
- **log函数**:防止高分帖子与普通帖子差距过大。
- **权重系数**:评论数权重最高(鼓励互动),点赞和收藏次之。
- **时间衰减**:发布时间越新,分数越高(随时间推移分数逐渐降低)。
#### **2. 核心流程**
```
定时任务触发 → 查询需要更新的帖子 → 计算分数 → 更新Redis缓存 → 前端展示热帖排行
```
#### **3. 实现代码**
##### **(1) 定义Quartz Job**
```java
public class HotPostJob implements Job {
@Autowired
private PostService postService;
@Autowired
private RedisTemplate redisTemplate;
@Override
public void execute(JobExecutionContext context) {
// 1. 查询所有需要更新分数的帖子(如最近3天有更新的帖子)
List<Post> posts = postService.findPostsToUpdateScore();
// 2. 计算分数并更新到Redis
for (Post post : posts) {
double score = calculateScore(post);
redisTemplate.opsForZSet().add("post:score", post.getId(), score);
}
// 3. 保留前1000名热帖(按需清理)
redisTemplate.opsForZSet().removeRange("post:score", 0, -1001);
}
private double calculateScore(Post post) {
double commentWeight = post.getCommentCount() * 10;
double likeWeight = post.getLikeCount() * 2;
double collectWeight = post.getCollectCount() * 2;
double value = post.getIsEssence() + commentWeight + likeWeight + collectWeight;
double score = Math.log(Math.max(value, 1)) + (post.getCreateTime().getTime() / 1000 - 1609459200);
return score;
}
}
```
##### **(2) 配置JobDetail与Trigger**
```java
@Configuration
public class QuartzConfig {
@Bean
public JobDetail hotPostJobDetail() {
return JobBuilder.newJob(HotPostJob.class)
.withIdentity("hotPostJob")
.storeDurably()
.build();
}
@Bean
public Trigger hotPostTrigger() {
// 每10分钟执行一次
CronScheduleBuilder schedule = CronScheduleBuilder.cronSchedule("0 */10 * * * ?");
return TriggerBuilder.newTrigger()
.forJob(hotPostJobDetail())
.withIdentity("hotPostTrigger")
.withSchedule(schedule)
.build();
}
}
```
##### **(3) Spring Boot 集成Quartz**
```yaml
spring:
quartz:
job-store-type: jdbc # 使用数据库存储任务配置
jdbc:
initialize-schema: never # 禁止自动初始化表(需手动导入SQL)
properties:
org.quartz.scheduler.instanceId: AUTO # 自动生成实例ID
```
---
### **四、Redis数据结构设计**
| **Key** | **类型** | **值** | **作用** |
|----------------|----------|----------------------------|---------------------------|
| `post:score` | ZSet | `(帖子ID, 分数)` | 按分数排序的热帖列表 |
#### **操作示例**
- **查询Top 10热帖**:
```java
Set<Long> topPosts = redisTemplate.opsForZSet().reverseRange("post:score", 0, 9);
```
- **更新分数**:
```java
redisTemplate.opsForZSet().add("post:score", postId, newScore);
```
---
### **五、高频面试问题**
#### **1. 为什么选择Quartz而不是Spring Scheduler?**
- **分布式支持**:Quartz通过数据库存储任务状态,多节点协同不会重复执行。
- **任务持久化**:服务器重启后任务不丢失,Spring Scheduler基于内存,重启后失效。
#### **2. 如何避免热帖分数计算的高负载?**
- **增量更新**:仅计算最近有变化的帖子(如最近3天有评论/点赞)。
- **缓存优化**:将分数结果缓存到Redis,避免频繁查询数据库。
#### **3. 热帖分数公式中为何使用log函数?**
- **平滑分数**:防止某帖子因短时间内大量互动导致分数激增,淹没其他优质内容。
- **长尾效应**:让新帖和老帖的分数差距更合理。
#### **4. 如何处理Redis中ZSet的数据一致性?**
- **异步双写**:数据库更新后发送事件到Kafka,消费者异步更新Redis。
- **补偿机制**:定时任务校验数据库与Redis数据差异并修复。
---
### **六、流程图辅助记忆**
#### **热帖计算流程**
```
Quartz Scheduler → 触发Job → 查询待更新帖子 → 计算分数 → 更新Redis ZSet → 清理旧数据
```
#### **分布式任务调度流程**
```
节点1 Scheduler → 抢锁(数据库) → 执行任务 → 释放锁
节点2 Scheduler → 检测锁状态 → 等待或跳过
```
Caffeine实现本地缓存
### **本地缓存 Caffeine 优化网站性能模块详解**
---
### **一、缓存分类与选型**
| **缓存类型** | **特点** | **适用场景** |
|----------------|--------------------------------------------------------------------------|-----------------------------------|
| **本地缓存** | 数据存储在应用服务器内存,访问速度极快(纳秒级) | 高频访问、数据量小、无需跨节点共享(如热门帖子列表) |
| **分布式缓存** | 数据存储在独立缓存服务(如Redis),跨服务器共享 | 低频访问、数据量大、需一致性(如用户登录凭证) |
| **多级缓存** | 本地缓存 → 分布式缓存 → 数据库,逐级回源 | 超高并发场景,避免缓存雪崩,提高可用性 |
---
### **二、为什么选择 Caffeine?**
#### **1. 核心优势**
- **高性能**:基于内存的缓存库,读写速度接近直接操作内存。
- **灵活的过期策略**:支持基于大小、时间、引用等策略淘汰数据。
- **自动加载**:缓存未命中时,可自动从数据源加载并缓存。
- **内存优化**:采用 **Window TinyLFU** 算法,高效管理内存,避免内存溢出。
#### **2. 对比其他本地缓存**
| **工具** | **优势** | **劣势** |
|--------------|-----------------------------------|-----------------------------------|
| **Ehcache** | 功能丰富,支持持久化 | 性能较低,内存管理不够高效 |
| **Guava** | 简单易用,集成Spring方便 | 已停止维护,部分功能不如Caffeine |
| **Caffeine** | 高性能,内存管理优秀,社区活跃 | 无持久化功能 |
---
### **三、Caffeine 实现热帖缓存**
#### **1. 缓存设计**
| **Key** | **Value** | **说明** |
|-------------------|--------------------|-----------------------------------|
| `hotPosts:0:10` | `List<Post>` | 存储第1页的热门帖子(offset=0, limit=10) |
| `hotPosts:10:20` | `List<Post>` | 存储第2页的热门帖子(offset=10, limit=20) |
#### **2. 核心代码实现**
##### **(1) 配置 Caffeine 缓存**
```java
@Configuration
public class CacheConfig {
@Bean
public Cache<String, List<Post>> hotPostCache() {
return Caffeine.newBuilder()
.maximumSize(100) // 最大缓存100个Key(按页码)
.expireAfterWrite(10, TimeUnit.MINUTES) // 写入后10分钟过期
.recordStats() // 开启统计功能(命中率监控)
.build();
}
}
```
##### **(2) 缓存使用(Service层)**
```java
@Service
public class PostService {
@Autowired
private Cache<String, List<Post>> hotPostCache;
public List<Post> getHotPosts(int offset, int limit) {
String cacheKey = "hotPosts:" + offset + ":" + limit;
// 尝试从缓存获取
return hotPostCache.get(cacheKey, key -> {
// 缓存未命中,从数据库查询
List<Post> posts = postMapper.selectHotPosts(offset, limit);
// 异步更新缓存(避免阻塞)
CompletableFuture.runAsync(() -> hotPostCache.put(cacheKey, posts));
return posts;
});
}
}
```
---
### **四、压力测试验证**
#### **1. 测试工具:JMeter**
- **测试场景**:模拟100个并发线程,连续请求首页热门帖子列表。
- **测试结果**:
| **场景** | **QPS(每秒请求数)** | **说明** |
|----------------|----------------------|-------------------------------|
| **无缓存** | 9.5 | 直接查询数据库,性能瓶颈明显 |
| **启用缓存** | 189 | 性能提升约20倍,响应时间大幅降低 |
#### **2. 性能优化原理**
- **减少数据库访问**:热门帖子数据缓存在内存,避免重复查询。
- **降低网络开销**:本地缓存无网络IO,数据直接内存读取。
---
### **五、多级缓存架构设计**
#### **1. 典型多级缓存流程**
```
用户请求 → 本地缓存(Caffeine) → 分布式缓存(Redis) → 数据库
```
- **优势**:
- **抗高并发**:本地缓存拦截大部分请求,保护下游服务。
- **避免雪崩**:多级缓存逐级回源,降低数据库瞬时压力。
#### **2. 缓存更新策略**
- **主动更新**:当帖子数据变化(如新增评论),发送事件到消息队列,触发缓存刷新。
- **被动过期**:设置合理过期时间(如10分钟),保证数据最终一致。
---
### **六、高频面试问题**
#### **1. 本地缓存会导致数据不一致吗?如何解决?**
- **问题原因**:各服务器本地缓存独立,数据更新后不同步。
- **解决方案**:
- **短过期时间**:设置较短的缓存有效期(如1分钟),牺牲部分一致性换取性能。
- **消息通知**:通过Kafka广播缓存失效事件,各节点监听后清理本地缓存。
#### **2. 如何避免缓存击穿?**
- **问题场景**:某个热点Key失效,大量请求穿透到数据库。
- **解决方案**:
- **互斥锁**:缓存未命中时,使用分布式锁(如Redis)控制只有一个线程回源。
- **永不过期**:对极热点数据设置逻辑过期时间,异步刷新缓存。
#### **3. Caffeine 的淘汰策略有哪些?**
- **基于大小**:`maximumSize(100)` 限制最大条目数。
- **基于时间**:`expireAfterWrite`(写入后过期)、`expireAfterAccess`(访问后过期)。
- **基于引用**:软引用(内存不足时回收)、弱引用(GC时回收)。
---
### **七、流程图辅助记忆**
#### **本地缓存查询流程**
```
用户请求 → 检查本地缓存 → 命中则返回 → 未命中则查数据库 → 异步更新缓存
```
#### **多级缓存架构**
```
请求 → Caffeine → Redis → DB
↑___________↓ ↑______↓
```
设计模式
原则
单开李姐,DI >... di
典型设计模式
单例
- 核心思想:确保一个类只有一个实例,并提供全局访问点。
- 实现方式:
- 饿汉式:类加载时直接创建实例(线程安全,但可能浪费资源)。
- 懒汉式:延迟创建实例,需双重检查锁(DCL)或静态内部类保证线程安全。
- 适用场景:数据库连接池、全局配置对象等需唯一实例的场景。
工厂
- 核心思想:将对象创建与使用解耦,通过工厂类统一管理对象的实例化。
- 常见类型:
- 简单工厂:
- 一个工厂类根据参数创建不同类型对象。
- 缺点:新增类型需修改工厂类代码。
- 工厂方法:
- 定义抽象工厂接口,子类决定创建哪种对象。
- 示例:Spring 中通过
BeanFactory创建Bean。
- 抽象工厂:
- 创建一组相关或依赖的对象家族(如不同操作系统的UI组件)。
- 简单工厂:
- 优势:
- 隐藏对象创建细节,符合开闭原则(扩展优于修改)。
- 便于统一管理对象生命周期(如Spring IoC容器)。
生产者,消费者
- 核心思想:解耦生产数据与消费数据的线程,通过共享缓冲区协调两者速度差异。
- 实现方式:
- 生产者:生成数据并放入缓冲区(如队列)。
- 消费者:从缓冲区取出数据并处理。
- 缓冲区:通常使用线程安全的阻塞队列(如
BlockingQueue),或通过wait/notify实现同步。
- 关键点:
- 解决线程间协作,避免忙等待。
- 防止缓冲区溢出(满时阻塞生产者)或下溢(空时阻塞消费者)。

浙公网安备 33010602011771号