实习复习
该内容自用为主
[TOC]
计算机网络
多层网络模型
四层网络模型是TCP/IP定义的:网络接口层,网络层,传输层,应用层。
七层网络模型是OSI定义的:物理层,数据链路层, 网络层,传输层,会话层,表现层,应用层。
网络接口层的传输单位是帧(frame),IP 层的传输单位是包(packet),TCP 层的传输单位是段(segment),HTTP 的传输单位则是消息或报文(message)。但这些名词并没有什么本质的区分,可以统称为数据包。
越低的层次,包的size越小。

应用层向传输层传播message 时,需要告知传输层的目标IP
TCP和UDP
TCP 是面向连接的,因而双方要维护连接的状态,这些带状态位的包的发送,会引起双方的状态变更。
TCP和 UDP 都工作于传输层。
TCP和UDP可以使用同一个端口
TCP Transmission Control Protocol
面向连接,字节流,可靠
定位 TCP连接:源/目标 socket(IP: Port)
TCP 优先分片,尽量不要让 IP层分片
TCP 报文格式

重传机制
序列号和确认应答。
- 超时重传:一段时间(RTT,环回时间)后还没收到确认号就重传
- 快速重传:三次重复的ACK,代表新的消息没有被接收,就要重传
- SACK
- D-SACK
滑动窗口确认
ACK(N) 代表接受到了所有 SEQ< N 的报文
拥塞控制
在网络繁忙的时候,控制数据包的数量,尽量避免大量数据丢失。
- 慢启动:一点点加快数据包的数量
- 拥塞避免
- 拥塞发送
- 快速恢复

流量控制
控制发送端向接收端发送数据的速度,保障接收端的处理能力
滑动窗口
三次握手和四次挥手


第三次握手可以携带数据
为什么是三次握手
同步两个方向的初始化序号,防止重复历史连接。
为什么是四次挥手
确保一来一回两个方向上的连接关闭。第二次和第三次不能简单合并的原因是,客户端关闭连接后,服务端可能还有数据包没发送给客户端。
第四次挥手后等待2MSL
确保最后一个ACK
报文能被服务端收到,2MSL如果服务端没收到的情况下,客户端还可以收到 FIN,再进行一次重传。
SYN
攻击
伪造IP,占满服务器的半连接队列。
SEQ
ACK
字段
序列号是随机初始化的(通过初始序列号,ISN),后续的序列号按字节流顺序递增。例如,如果第一个报文段的序列号是
1000
,且数据长度为100
字节,那么下一个报文段的序列号将是1100
。确认号表示接收方期望接收的下一个字节的序列号。例如,如果接收方发送的确认号是
1100
,则表示接收方已经成功接收了序列号1000
到1099
的数据,期望接收序列号为1100
的数据。
UDP User Datagram Protocol
无连接

HTTP 和 HTTPS
HTTP的改进过程
- HTTP/1.0:基本功能,每次请求需新建连接。
- HTTP/1.1:持久连接、管道化、Host头字段压缩、缓存控制,断点重传。
- HTTP/2:二进制协议、多路复用、头部压缩、服务器推送。
- HTTP/3:基于QUIC、内置加密、改进的多路复用、连接迁移。
GET 和 POST 方法都是安全和幂等的吗?
GET 不修改服务器上的资源,所以是幂等的。POST需要求改服务器资源,所以不是。
GET 请求可以记录为标签,可以被缓存。
HTTPS 链接建立方式
- ClientHello:客户端向服务器发送ClientHello消息,包含支持的TLS版本、加密套件列表和随机数1。
- ServerHello:服务器回应ServerHello消息,选择TLS版本、加密套件,并发送服务器随机数2。
- 服务器证书:服务器发送其数字证书,包含公钥和证书颁发机构(CA)信息。
- 密钥交换:客户端验证服务器证书,生成预主密钥3(Pre-Master Secret),用服务器公钥加密后发送给服务器。非对称加密
- 会话密钥生成:客户端和服务器使用预主密钥3和随机数1,2 生成会话密钥(Session Keys),用于后续通信的加密和解密。之后就都是对称加密了。
- 完成消息:双方发送Finished消息,验证握手过程是否成功。

HTTPS的安全风险在于?
CA的泄露,信任了不该信任的证书。
HTTP 粘包问题
接收端不知道报文的边界在哪里
解决方案:
通过给出特殊字符标记边界(回车或者换行符)
自定义消息结构,在包头中定义长度
RPC remote procedure call
比 HTTP/1.1 更加精简,像调用本地方法一样调用远程方法。
但是约束和自定义的内容更多。
websocket
从 http 升级,用于网页游戏等场景
OS 操作系统
操作系统的功能:向下管理各种硬件资源,向上为软件提供统一的资源访问接口和服务
OS 软件服务
I/O设计三者的核心区别
维度 | 同步阻塞 I/O | 同步非阻塞 I/O | I/O 多路复用 | 异步I/O |
---|---|---|---|---|
阻塞行为 | 全程阻塞直到数据就绪并拷贝完成 | 仅数据拷贝阶段可能短暂阻塞 | 仅阻塞在 select /epoll 等待就绪事件 |
无阻塞 |
数据就绪检查 | 无检查,被动等待完成 | 需应用主动轮询(循环重试) | 内核通知就绪的 fd(无需轮询) | 通知 |
资源消耗 | 高(每个连接一个线程) | 低(单线程处理,但 CPU 轮询开销大) | 低(单线程高效管理多连接) | 内核异步将数据从内核空间拷贝到应用程序空间,内核自动完成的。 |
适用场景 | 低并发、简单任务 | 低并发但需快速响应其他任务 | 高并发、短耗时操作 Netty |
低 |
核心过程:
- 内核准备数据
- 数据从内核态拷贝到用户态
调用内核态功能的方式
- 系统调用:直接访问内核功能。
- 库函数:封装系统调用,提供高级接口。
- 设备文件:通过文件操作访问硬件。
- 信号:处理异步事件。
- 共享内存和消息队列:实现进程间通信。
- 文件映射:高效访问文件内容。
- 内核模块:通过
ioctl()
实现自定义功能。
select poll epoll
select 数组实现
select
是最早的 I/O 多路复用机制,其实现相对简单,但效率较低。
数据结构
select
使用三个位掩码(fd_set
)来表示需要监控的文件描述符集合:
readfds
:监控可读事件。writefds
:监控可写事件。exceptfds
:监控异常事件。
每个 fd_set
是一个固定大小的位数组(通常是 1024 位,由 FD_SETSIZE
定义),每一位对应一个文件描述符。
工作流程
- 用户空间到内核空间的拷贝:
- 用户调用
select
时,需要将三个fd_set
从用户空间拷贝到内核空间。
- 用户调用
- 内核轮询检查:
- 内核遍历所有被监控的文件描述符,检查它们的状态(是否可读、可写或异常)。
- 内核的时间复杂度为 O(n),其中 n 是最大的文件描述符值。
- 内核空间到用户空间的拷贝:
- 内核将修改后的
fd_set
拷贝回用户空间,表示哪些文件描述符已就绪。
- 内核将修改后的
- 用户空间遍历:
- 用户需要遍历所有文件描述符,检查哪些位被置位,以确定哪些文件描述符已就绪。
poll 链表实现
poll
是对 select
的改进,解决了文件描述符数量限制的问题。
数据结构
poll
使用一个pollfd
结构体数组来表示需要监控的文件描述符集合:c
复制
1
2
3
4
5struct pollfd {
int fd; // 文件描述符
short events; // 监控的事件(如 POLLIN、POLLOUT)
short revents; // 返回的事件
};每个
pollfd
结构体包含一个文件描述符和需要监控的事件。
工作流程
- 用户空间到内核空间的拷贝:
- 用户调用
poll
时,需要将pollfd
数组从用户空间拷贝到内核空间。
- 用户调用
- 内核轮询检查:
- 内核遍历
pollfd
数组,检查每个文件描述符的状态。 - 内核的时间复杂度为 O(n),其中 n 是
pollfd
数组的长度。
- 内核遍历
- 内核空间到用户空间的拷贝:
- 内核将修改后的
pollfd
数组拷贝回用户空间,表示哪些文件描述符已就绪。
- 内核将修改后的
- 用户空间遍历:
- 用户需要遍历
pollfd
数组,检查revents
字段,以确定哪些文件描述符已就绪。
- 用户需要遍历
epoll
epoll
是 Linux 2.6 引入的高效 I/O 多路复用机制,采用事件驱动模型,解决了 select
和 poll
的性能问题。
数据结构
epoll
使用三个系统调用:epoll_create
:创建一个epoll
实例,返回一个文件描述符。epoll_ctl
:向epoll
实例中添加、修改或删除需要监控的文件描述符。epoll_wait
:等待事件发生,返回就绪的文件描述符。
epoll
使用红黑树和双向链表来管理文件描述符和事件:- 红黑树:用于高效地存储和查找文件描述符。
- 就绪链表:用于存储已就绪的文件描述符。
工作流程
- 初始化:
- 调用
epoll_create
创建一个epoll
实例。 - 调用
epoll_ctl
向epoll
实例中添加需要监控的文件描述符和事件。
- 调用
- 事件注册:
- 内核将文件描述符和事件注册到红黑树中。
- 事件等待:
- 用户调用
epoll_wait
,内核检查红黑树中的文件描述符,将就绪的文件描述符添加到就绪链表中。 - 内核只返回就绪的文件描述符,时间复杂度为 O(1)。
- 用户调用
- 事件通知:
- 用户从
epoll_wait
中获取就绪的文件描述符,无需遍历所有文件描述符。
- 用户从
触发模式
- 水平触发(LT):
- 只要文件描述符处于就绪状态,
epoll_wait
就会一直通知。 - 类似于
select
和poll
的行为。
- 只要文件描述符处于就绪状态,
- 边缘触发(ET):
- 只有当文件描述符状态发生变化时,
epoll_wait
才会通知。 - 需要用户一次性处理所有数据,否则可能会丢失事件。
- 只有当文件描述符状态发生变化时,
零拷贝
存储结构

越往上的内存越快,但是越珍贵,所以体积越小。
虚拟内存
虚拟内存的空间寻址空间很大,超过了物理内存的空间。
CPU的 MMU管理虚拟内存地址空间和物理内存空间的映射关系
基于局部性原则设计。
- 时间局部性(Temporal Locality):如果一个数据被访问过,那么它在不久的将来很可能再次被访问。
- 空间局部性(Spatial Locality):如果一个数据被访问过,那么它附近的数据也可能会被访问。
虚拟内存可以隔离开各个进程的地址空间,提高安全性和稳定性。
虚拟内存会使用磁盘空间作为支持。
内存分段和分页
分段是按照进程需求分配内存空间,分页是按照进程需求向上取整分配内存空间。
内存页是固定大小,内存段时可变大小。
分段产生外部碎片,分页产生内部碎片
处理外部碎片的方式是内存交换。(JVM 内存管理)
段页式内存
虚拟地址由两部分组成:段号(Segment Number) 和 段内偏移量(Offset)。
段号用于查找段表,得到该段的页表基址。
段内偏移量被进一步划分为 页号(Page Number) 和 页内偏移量(Page Offset)。
页号用于查找页表,得到物理页框号。
物理地址 = 物理页框号 × 页大小 + 页内偏移量。
每个进程都有自己的页表
缺页中断
内存页不在物理内存种,需要发生缺页终端,将内存页从虚拟内存加载到物理内存中。
虚拟内存地址分为两个部分,虚拟页号 + 页内偏移量。
虚拟页号通过 MMU 的页表转换为物理内存地址,得到物理内存地址。
内存替换策略有:
- FIFO:简单但性能较差。
- LRU:性能较好但实现复杂。
- OPT:理论最优但无法实现。
- Clock:性能和实现复杂度适中。
- NRU:简单且性能较好。
- LFU:适合访问频率差异大的场景。
- 工作集模型:适合长时间运行的进程
双队列实现的LRU算法
处理预读失效
G1 / INooDB / Linux kernal
主队列(Main Queue)
- 存储当前在内存中的页面。
- 使用 FIFO(先进先出) 的顺序管理页面。
辅助队列(Auxiliary Queue)
- 存储最近被访问的页面。
- 使用 LRU(最近最少使用) 的顺序管理页面。
页面访问规则
- 当页面被访问时,如果它在主队列中,则将其移动到辅助队列的头部。
- 如果页面在辅助队列中,则将其移动到辅助队列的头部。
- 如果页面不在任何队列中,则将其加入主队列。
实现上只需要一个链表
多级页表 页表缓存

越高级别的页表区别越细粒度
多级页表效率太低,TLB(快表,页表缓存,Translation Lookaside Buffer) 提高效率
用户空间和内核空间

缓存一致性
todo
如何处理预读失效和缓存污染
预读失效
使用两队列的LRU算法:
inactive list/active list : 两个 FIFO 队列
空间局部性可以让要读的页周围的页加载到inactive list ,只有真正被使用的被使用的页才会被拉到 active list 的头部。
缓存污染
提高进入 active list 的难度
例如 Linux kernal 只有第二次使用的页才能加入 active list。 INooDB 只有超过间隔1s的两次访问才会把表拿到 active list 中
既可以通过两个链表,也可以通过一个链表实现:

进程管理和调度
并发和并行
并发在一个CPU上交替使用时间片
并行在多个CPU上同步执行
进程的状态

PCB 进程控制块 Process Control Block
- 进程标识符(PID):唯一标识一个进程。
- 进程状态:如运行、就绪、阻塞等。
- 程序计数器(PC):指向下一条要执行的指令。
- CPU 寄存器:保存进程的寄存器状态。
- 内存管理信息:如页表基址、内存分配情况等。
- I/O 状态信息:如打开的文件、使用的设备等。
- 调度信息:如优先级、调度队列等。
- 记账信息:如 CPU 使用时间、内存使用量等。
- ……
进程上下文
- CPU 寄存器:如通用寄存器、程序计数器、栈指针等。
- 程序计数器(PC):指向下一条要执行的指令。
- 内存状态:如页表基址、内存映射等。
- 堆栈:保存函数调用、局部变量等信息。
- 其他状态:如浮点寄存器、控制寄存器等
- ……
线程
线程是进程拥有的,进程的一条执行路径
同一个进程内多个线程之间可以共享代码段、数据段、打开的文件等资源,但每个线程各自都有一套独立的寄存器和栈,这样可以确保线程的控制流是相对独立的。
线程间共享进程的地址空间和文件资源。
线程是CPU调度的最小单位。进程是资源分配的最小单位。
线程分类
用户线程
内核线程
轻量级进程
线程和进程的区别
特性 | 进程 | 线程 |
---|---|---|
地址空间 | 独立 | 共享 |
内存开销 | 高 | 低 |
上下文切换 | 慢,开销大 | 快,开销小 |
通信 | 需要 IPC 机制,开销较大 | 共享内存,直接通信 |
创建销毁 | 开销大,较慢 | 开销小,较快 |
并发性 | 低 | 高 |
崩溃影响 | 一个进程崩溃不会影响其他进程 | 一个线程崩溃可能导致整个进程崩溃,JVM不会 |
调度策略
- 抢占式调度策略
- 非抢占式调度策略
进程调度的原因
- I/O处理时间,CPU利用率低,让出CPU时间片可以提高CPU利用率;
- 不让长任务一直占用CPU,不让短任务长时间等待;
- 交互式应用应当有较快的响应效率
调度算法
- 先来先服务(First-Come, First-Served, FCFS)
- 短作业优先(Shortest Job First, SJF)
- 优先级调度(Priority Scheduling)
- 时间片轮转(Round Robin, RR)
- 多级队列调度(Multilevel Queue Scheduling)
- ……
协程,线程,进程
协程(Coroutine)、线程(Thread) 和 进程(Process) 是计算机科学中用于实现并发执行的三种不同机制。它们在资源占用、调度方式、并发模型等方面有显著区别。以下是它们的详细对比:
1. 进程(Process)
- 定义:
- 进程是操作系统资源分配的基本单位,是程序的一次执行实例。
- 每个进程都有独立的内存空间、文件描述符、环境变量等。
- 特点:
- 独立性:进程之间相互隔离,一个进程崩溃不会影响其他进程。
- 资源开销大:创建和切换进程需要较大的系统开销(如内存、CPU)。
- 通信复杂:进程间通信(IPC)需要通过管道、消息队列、共享内存等机制。
- 适用场景:
- 需要高隔离性的任务。
- 多任务操作系统中的任务管理。
2. 线程(Thread)
- 定义:
- 线程是进程内的执行单元,是 CPU 调度的基本单位。
- 一个进程可以包含多个线程,这些线程共享进程的内存空间和资源。
- 特点:
- 轻量级:线程的创建和切换开销比进程小。
- 共享资源:线程共享进程的内存空间,因此需要同步机制(如锁)来避免竞争条件。
- 并发性:多线程可以实现真正的并行执行(在多核 CPU 上)。
- 适用场景:
- 需要高并发处理的任务(如网络服务器、GUI 应用程序)。
- 任务间需要共享数据的场景。
3. 协程(Coroutine)
- 定义:
- 协程是一种用户态的轻量级线程,由程序员显式控制调度。
- 协程在同一个线程内运行,通过协作式调度实现并发。
- 特点:
- 用户态调度:协程的调度由程序控制,不依赖操作系统。
- 低开销:协程的创建和切换开销极小,通常只需要保存和恢复少量寄存器。
- 非抢占式:协程主动让出执行权,而不是被操作系统强制切换。
- 单线程并发:协程在单线程内实现并发,适合 I/O 密集型任务。
- 适用场景:
- 高并发的 I/O 密集型任务(如网络爬虫、异步编程)。
- 需要高效处理大量任务的场景。
三者的对比
特性 | 进程(Process) | 线程(Thread) | 协程(Coroutine) |
---|---|---|---|
定义 | 操作系统资源分配的基本单位 | 进程内的执行单元 | 用户态的轻量级线程 |
内存空间 | 独立的内存空间 | 共享进程的内存空间 | 共享线程的内存空间 |
创建/切换开销 | 高 | 中 | 低 |
调度方式 | 操作系统调度 | 操作系统调度 | 用户程序调度 |
并发性 | 多进程并行 | 多线程并行 | 单线程内并发 |
通信机制 | 管道、消息队列、共享内存等 | 共享内存 | 直接共享变量 |
隔离性 | 高 | 低 | 低 |
适用场景 | 高隔离性任务 | 高并发任务 | I/O 密集型任务 |
进程间通信
通信方式
- 管道
- 消息队列
- 共享内存
- 信号
- 信号量
- socket
竞争和协作
**竞争:**多线程间共享数据,需要定义临界区,只有一个线程进入临界区。
**同步:**互相等待和唤醒
实现方式
- 锁
- 信号量
- PV
具体参见 JVM
中断
数据库
数据库不应当是黑盒的,开发人员必须深入了解你所使用的数据库的体系结构和特征。
范式
范式 | 描述 |
---|---|
1NF | 每一列都是原子性的,没有重复的组。 |
2NF | 满足1NF,且非主键列完全依赖于主键。 |
3NF | 满足2NF,且非主键列之间没有传递依赖。消除传递依赖 |
BCNF | 满足3NF,且所有函数依赖的左部必须是超键。 |
4NF | 表中不存在非平凡的多值依赖。 |
执行过程

如果一个用户已经建立了连接,即使管理员中途修改了该用户的权限,也不会影响已经存在连接的权限。修改完成后,只有再新建的连接才会使用新的权限设置。
MySQL 8.0 版本直接将查询缓存删掉了,也就是说 MySQL 8.0 开始,执行一条 SQL 查询语句,不会再走到查询缓存这个阶段了。
主键索引的 B+ 树
索引
在建立表的时候,引擎就会建立聚簇索引。
- 如果没有显式指定主键:
- InnoDB会自动选择一个唯一且非空的列作为聚簇索引。
- 如果没有这样的列,InnoDB会自动创建一个隐藏的自增列(通常是6字节的ROW_ID)作为聚簇索引。
- 如果指定了主键:
- 主键会自动成为聚簇索引。
- 主键必须是唯一的,且不能包含
NULL
值。
索引分类
- 按「数据结构」分类:B+tree索引、Hash索引、Full-text索引
- 按「物理存储」分类:聚簇索引(主键索引)二级索引(辅助索引)
- 按「字段特性」分类:主键索引、唯一索引、普通索引、前缀索引
- 按「字段个数」分类:单列索引、联合索引
B+ Tree Index 的优势

一个值对应一个指针。
主键索引的 B+Tree 和二级索引的 B+Tree 区别
主键索引的 B+Tree 的叶子节点存放的是实际数据,所有完整的用户记录都存放在主键索引的 B+Tree 的叶子节点里;
二级索引的 B+Tree 的叶子节点存放的是主键值,而不是实际数据。
什么时候不需要回表查询
覆盖索引
当查询的数据是能在二级索引的 B+Tree 的叶子节点里查询到,这时就不用再查主键索引查。这种在二级索引的 B+Tree 就能查询到结果的过程就叫作「覆盖索引」,也就是只需要查一个 B+Tree 就能找到数据。
联合索引失效
使用联合索引时,存在最左匹配原则,也就是按照最左优先的方式进行索引的匹配。在使用联合索引进行查询的时候,如果不遵循「最左匹配原则」,联合索引会失效,这样就无法利用到索引快速查询的特性了。
比如,如果创建了一个(a,b,c)联合索引,如果查询条件是以下这几种,就可以匹配上联合索引:
1 | where a=1; |
需要注意的是,因为有查询优化器,所以a字段在 where 子句的顺序并不重要。但是,如果查询条件是以下这几种,因为不符合最左匹配原则,所以就无法匹配上联合索引,联合索引就会失效:
1 | where b=2; |
上面这些查询条件之所以会失效,是因为(a,b,c)1联合索引,是先按a排序,在a相同的情况再按 b排序,在b相同的情况再按c排序。所以,b和c是全局无序,局部相对有序的,这样在没有遵循最左匹配原则的情况下,是无法利用到索引的。
索引下推
MySQL 5.6后通过联合索引可以判断另一个值是否满足条件,去除不符合的,减少回表次数;区别于覆盖索引。
MyISAM 引擎和 InnoDB B+Tree索引有什么区别
虽然,InnoDB 和 MyISAM 都支持 B+ 树索引,但是它们数据的存储结构实现方式不同。
不同之处在于:
- InnoDB 存储引擎:B+树索引的叶子节点保存数据本身
- ;MyISAM 存储引擎:B+树索引的叶子节点保存数据的物理地址
索引失效
对索引使用左右模糊匹配
对索引使用函数
因为索引保存的是索引字段的原始值,而不是经过函数计算后的值,自然就没办法走索引了。
不过,从 MySQL 8.0 开始,索引特性增加了函数索引,即可以针对函数计算后的值建立一个索引,也就是说该索引的值是函数计算后的值,所以就可以通过扫描索引来查询数据。
对索引使用表达式
对索引进行隐式转换
VARCHAR
转换为Int
会导致索引失效,准确的说是如果查询语句中使用INT
但是索引本身是VARCHAR
会有可能导致索引失效。1
2
3
4
5
6
7DATETIME > TIMESTAMP > DATE
|
DECIMAL > DOUBLE > FLOAT > BIGINT > INT > MEDIUMINT > SMALLINT > TINYINT
|
CHAR/VARCHAR/TEXT(字符串)
|
BINARY/BLOB(二进制)向上转换
联合索引非最左匹配
WHERE 子句中的 OR
在 WHERE 子句中,如果在 OR 前的条件列是索引列,而在 OR 后的条件列不是索引列,那么索引会失效。
Count()
count(*)= count(1)>count(主键字段)>count(字段)
count(*)= count(1): MySQL 会对 count(*) 和 count(1) 尽量使用体积较小的二级索引
count(主键字段) 必然走主键索引,体积可能比二级索引大。
为什么要通过遍历的方式来计数?
使MyISAM 引擎 O(1)复杂度,每张 MyISAM 的数据表都有一个 meta 信息有存储了row_count值,由表级锁保证一致性。
wInnoDB 存储引擎是支持事务的,同一个时刻的多个查询,由于多版本并发控制(MVCC)的原因,InnoDB 表“应该返回多少行”也是不确定的,所以无法像 MyISAM一样,只维护一个 row_count 变量。
存储


为什么2000W行会影响性能
索引结构不会影响单表最大行数,2000W 也只是推荐值,超过了这个值可能会导致 B + 树层级更高,影响查询性能。
事务
事务的四个性质 ACID
原子性(atomicity)
一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。
一致性(Consistency)
是指事务操作前和操作后,数据满足完整性约束,数据库保持一致性状态。
隔离性(Isolation)
数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,因为多个事务同时使用相同的数据时,不会相互干扰,每个事务都有一个完整的数据空间,对其他并发事务是隔离的。
持久性(durability)
事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
InnoDB 引擎通过什么技术来保证事务的这四个特性的呢?
持久性是通过 redo log (重做日志)来保证的;
原子性是通过 undo log(回滚日志) 来保证的;
隔离性是通过 MVCC(多版本并发控制) 和锁机制来保证的;
一致性则是通过持久性+原子性+隔离性来保证;
并行事务产生的问题和事务隔离级别
读未提交 >脏读 >读已提交 > 不可重复读 > 可重复读>幻读> 可串行化
读未提交:一个写事务还没有提交,它的修改就被其它事务看到了,这就是脏读;
读已提交:一个事务提交之后,它做的变更才能被其他事务看到;
可重复读:一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的,InnoDB 默认事务隔离级别
可串行化:完全单线程执行,一个事务执行时,另外的事务被阻塞。
读提交和可重复读事务隔离如何实现 MVCC
通过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.
只有提交了事务才可以被这个版本的MVCC看到

对于使用 InnoDB 存储引擎的数据库表,它的聚簇索引记录中都包含下面两个隐藏列:
- trx_id,当一个事务对某条聚簇索引记录进行改动时,就会把该事务的事务 id 记录在 trx_id 隐藏列里;
- roll_pointer,每次对某条聚簇索引记录进行改动时,都会把旧版本的记录写入到 undo 日志中,然后这个隐藏列是个指针,指向每一个旧版本记录,于是就可以通过它找到修改前的记录
这种通过「版本链」来控制并发事务访问同一个记录时的行为就叫 MVCC(多版本并发控制)。
不同的 Read View 会记录哪些应用这个版本的事务Id
可重复读隔离级别是启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View。
读提交隔离级别是在每次读取数据时,都会生成一个新的 Read View。
基于可重复读,如何实现避免幻读
可以分为两个情况:
- 快照读,在这个事务开始的时候指定好 Read View,以后的读操作都是基于这个 Read View ,不论其他事务是否改变数据,当前事务都是基于固定的 Read View,自然不会出现幻读。
- 当前读,会给表添加间隙锁,组织一些写操作。
基于可重复读的事务隔离级别,幻读不能被完美避免。在间隙锁不能锁定的地方添加到新的数据,就会出现幻读。
锁
有哪些锁
全局锁
锁定整个表。全数据库/模式在只读状态下。
用于做全库逻辑备份。
表级别锁
表锁
表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。
用于调整表结构。
元数据锁 MDL
Meta Data Lock
MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。
为了能安全的对表结构进行变更,在对表结构变更前,先要看看数据库中的长事务,是否有事务已经对表加上了 MDL 读锁,如果可以考虑 kill 掉这个长事务,然后再做表结构的变更。
不修改表结构的事务相当于读,改变表结构的事务相当于写操作。
意向锁
对记录上锁之前需要给表上对应的意向锁,用于提示表中的记录是否被上锁。
意向共享锁和意向独占锁是表级锁,不会和行级的共享锁和独占锁发生冲突,而且意向锁之间也不会发生冲突,只会和共享表锁和独占表锁发生冲突。
AUTO-INC 锁
用于实现自增字段。
在MySQL 5.1.22 版本前,修改自增字段的请求需要获得锁之后才能执行。
在MySQL 5.1.22 版本后,InnoDB 存储引擎提供了一种轻量级的锁来实现自增。
一样也是在插入数据的时候,会为被 AUTO_INCREMENT
修饰的字段加上轻量级锁,然后给该字段赋值一个自增的值,就把这个轻量级锁释放了,而不需要等待整个插入语句执行完后才释放锁。
InnoDB 存储引擎提供了个 innodb_autoinc_lock_mode 的系统变量,是用来控制选择用 AUTO-INC锁,还是轻量级的锁。
- 当 innodb_autoinc_lock_mode=0,就采用 AUTO-INC锁,语句执行结束后才释放锁;·
- 当 innodb_autoinc_lock_mode =2,就采用轻量级锁,申请自增主键后就释放锁,并不需要等语句执行后才释放。
- 当innodb autoinc lock_mode =1:
- 普通 insert 语句,自增锁在申请之后就马上释放0
- 类似 insert .. select 这样的批量插入数据的语句,自增锁还是要等语句结束后才被释放;
行级别锁
InnoDB 引擎是支持行级锁的,而 MyISAM 引擎并不支持行级锁。
行级别锁有三类:
- Record Lock:记录锁
- Gap Lock: 间隙锁
- Next-Key Lock: Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。
记录锁
间隙锁
Next-Key Lock
间隙锁是的目的是为了防止幻读;
X型和S型没什么区别。间隙锁的意义只在于阻止区间被插入,因此是可以共存的。一个事务获取的间隙锁不会阻止另一个事务获取同一个间隙范围的间隙锁,共享(S型)和排他(X型)的间隙锁是没有区别的,他们相互不冲突,且功能相同。
插入意向锁
间隙锁给某一区间的值加读锁,插入意向锁名字虽然有意向锁,但是它并不是意向锁,它是一种特殊的间隙锁,属于行级别锁,它锁住的是插入的点。
插入意向锁和间隙锁是不能共存的
日志
日志有哪些
日志类型 | 逻辑日志 | 物理日志 | 主要作用 |
---|---|---|---|
二进制日志 bin | ✔️ | ❌ | 主从复制、数据恢复 |
重做日志 redo | ❌ | ✔️ | 崩溃恢复,确保事务持久性 |
回滚日志 undo | ✔️ | ❌ | 事务回滚、MVCC |
undo log 和 redo log 都是 InnoDB 引擎做的日志,bin log 是服务层做的日志。
undo log 一开始并不会持续化到磁盘中,redo log 一开始就持续化磁盘中。
undo log 内容


在MVCC 中见到过,undo log 和 Read View 配合实现 MVCC。
针对插入和更新会有特殊操作:
对于delete操作,并不是直接删除掉一条记录,而是对记录标记未删除,删除的实际操作由purge 线程执行
对如update操作,如果是对主键的更新,是删除原来的记录后新增一条新的记录,如果是普通的更新,则是普通的修改数据。
原因在于:
- 索引结构和B+ Tree的结构,主键值决定了数据行的物理存储位置,如果直接修改主键值,会导致数据行在 B+ 树中的位置发生变化。严重的时候可能需要对B+树进行重新平衡
- InnoDB 通过 多版本并发控制(MVCC) 实现事务隔离。修改主键时,旧版本的数据需要保留(通过 Undo Log)
- InnoDB 的二级索引叶子节点存储的是主键值,先删除后插入 的机制可以统一处理二级索引
redo log
redo log 防止缓存没有写入磁盘而崩溃的情景
WAL(Write Ahead Logging): 为了防止断电导致数据丢失的问题,当有一条记录需要更新的时候,InnoDB 引擎就会先更新内存(同时标记为脏页),然后将本次对这个页的修改以 redo log 的形式记录下来。再在合适的时候将缓存写入磁盘.

当系统崩溃时,虽然脏页数据没有持久化,但是 redo log 已经持久化,接着 MySQL 重启后,可以根据 redo log 的内容,将所有数据恢复到最新的状态。
redo 刷盘时机
- Mysql 关闭;
- 当 redo log buffer 中记录的写入量大于 redo log buffer 内存空间的一半时,会触发落盘;
- 每隔1s
- 每次事务提交时都将缓存在 redo log buffer 里的 redo log 直接持久化到磁盘(这个策略可由 innodb_flush_log_at_trx_commit 参数控制)。

写满了 redo log 怎么办

InnoDB 存储引擎会先写 ib_logfile0 文件,当 ib_logfile0 文件被写满的时候,会切换至 ib_logfile1 文件,当 ib_logfile1 文件也被写满时,会切换回 ib_logfile0 文件。
如果 write pos 追上了 checkpoint,就意味着 redo log 文件满了,这时 MySQL 不能再执行新的更新操作,也就是说 MySQL 会被阻塞(因此所以针对并发量大的系统,适当设置redo log 的文件大小非常重要),此时会停下来将 Buffer Pool 中的脏页刷新到磁盘中,然后标记 redo log 哪些记录可以被擦除,接着对旧的 redo log 记录进行擦除,等擦除完旧记录腾出了空间,checkpoint 就会往后移动(图中顺时针),然后 MySQL恢复正常运行,继续执行新的更新操作。
bin log
可以认为是数据库服务器的日志binlog,写满了会换个文件接着写;用于备份恢复、主从复制;
事务执行过程中,先把日志写到 binlog cache(Server 层的 cache),事务提交的时候,再把 binlog cache 写到 binlog 文件中。
三种格式:
- STATEMENT:每一条修改数据的 SQL 都会被记录到 binlog 中(相当于记录了逻辑操作,所以针对这种格式, binlog 可以称为逻辑日志),主从复制中 slave 端再根据 SQL 语句重现。但 STATEMENT 有动态函数的问题,比如你用了 uuid 或者 now 这些函数,你在主库上执行的结果并不是你在从库执行的结果,这种随时在变的函数会导致复制的数据不一致;
- ROW:记录行数据最终被修改成什么样了(这种格式的日志,就不能称为逻辑日志了),不会出现 STATEMENT 下动态函数的问题。但 ROW 的缺点是每行数据的变化结果都会被记录,比如执行批量 update 语句,更新多少行数据就会产生多少条记录,使 binlog 文件过大,而在 STATEMENT 格式下只会记录一个 update 语句而已;
- MIXED:混合模式
两阶段提交
对 redo log 做两阶段提交
在持久化 redo log 和 binlog 这两份日志的时候,如果出现半成功的状态,就会造成主从环境的数据不一致性。这是因为 redo log 影响主库的数据,binlog 影响从库的数据,所以 redo log 和 binlog 必须保持一致才能保证主从数据一致。

主从复制
主从复制原理
- 主库(Master):
- 记录所有数据变更到 二进制日志(Binary Log, binlog)。
- 从库通过读取 binlog 同步数据。
- 从库(Slave):
- 连接到主库,读取 binlog 并写入 中继日志(Relay Log)。
- 重放中继日志中的事件,实现数据同步。
模式 | 特点 | 适用场景 |
---|---|---|
异步复制 | 主库提交事务后立即响应客户端,不等待从库同步(默认模式)。 | 对性能要求高,允许短暂延迟。 |
半同步复制 | 主库提交事务后至少等待一个从库确认收到数据,再响应客户端。 | 要求数据一致性较高。 |
组复制(MGR) | 基于 Paxos 协议的多主同步,支持自动故障转移和高可用(需 MySQL Group Replication)。 | 高可用集群环境。 |
Buffer pool
用缓存来降低IO瓶颈;
使用两队列的 LRU(Least Recent Used)算法进行替换;
在开启了慢 SQL 监控后,如果你发现「偶尔」会出现一些用时稍长的 SQL,这可因为脏页在刷新到磁盘时导致数据库性能抖动。如果在很短的时间出现这种现象,就需要调大 Buffer Pool 空间或 redo log 日志的大小。
SQL 优化
查询语句优化
索引相关优化
- 合理添加索引:为 WHERE、JOIN、ORDER BY、GROUP BY 子句中的列创建适当索引
- 避免索引失效:
- 避免在索引列上使用函数或计算
- 避免使用
!=
、<>
、NOT IN
、IS NULL
等可能导致索引失效的操作符 - 注意 LIKE 查询以通配符开头(
%abc
)会导致索引失效
- 覆盖索引:尽量让查询只需要通过索引就能获取所需数据
- 索引合并:合理使用复合索引,注意最左前缀原则
- 自增索引只在最右边增加,不分裂原有节点
查询结构优化
**避免 SELECT **:只查询需要的列
合理使用 JOIN:
- 小表驱动大表
- 避免多表 JOIN 时产生笛卡尔积
优化子查询:
- 将某些子查询改写为 JOIN
- 使用 EXISTS 代替 IN 当外表大而内表小时
分页优化:
- 避免大偏移量分页(如
LIMIT 10000, 20
) - 使用延迟关联或记录上次查询位置
- 避免大偏移量分页(如
最好必要用可变长字符串作为索引
索引占用空间大
查询性能较低:比较的效率低
维护成本高
- B+ 树频繁的分裂
- 空间碎片化
(优化方向:前缀索引,hash,编码映射
数据库设计优化
表结构设计
- 合理的数据类型:使用最小够用的数据类型(如能用 TINYINT 不用 INT)
- 适当的范式化:平衡范式化和反范式化,避免过度范式化导致过多 JOIN
- 垂直拆分:将不常用的大字段拆分到单独表
- 水平拆分:对超大表考虑分表策略(按时间、ID范围等)
- 分区
索引设计
- 主键选择:使用自增整型或业务无关ID作为主键
- 复合索引顺序:将选择性高的列放在前面
- 避免冗余索引:定期检查并删除未使用的索引
执行计划优化
- 分析执行计划:使用
EXPLAIN
或EXPLAIN ANALYZE
查看查询执行路径 - 识别性能瓶颈:
- 关注全表扫描(
ALL
访问类型) - 注意临时表(
Using temporary
)和文件排序(Using filesort
)
- 关注全表扫描(
- 强制索引:必要时使用
FORCE INDEX
引导优化器选择更好的索引
数据库参数优化
服务器配置
- 缓冲池大小:合理设置
innodb_buffer_pool_size
(通常为物理内存的50-70%) - 连接数配置:调整
max_connections
避免过多连接导致资源耗尽 - 日志配置:平衡事务日志(
redo log
)和二进制日志(binlog
)的性能影响
存储引擎优化
- InnoDB 优化:
- 调整
innodb_flush_log_at_trx_commit
(安全性 vs 性能平衡) - 配置合适的
innodb_file_per_table
- 调整
- MyISAM 优化(如使用):
- 调整
key_buffer_size
- 定期执行
OPTIMIZE TABLE
- 调整
监控与持续优化
- 慢查询日志:定期分析慢查询并优化
- 性能监控:监控QPS、TPS、连接数等关键指标
- 定期维护:
- 执行
ANALYZE TABLE
更新统计信息 - 对碎片化严重的表进行优化(
OPTIMIZE TABLE
- 执行
分表 vs 分库分表
特性 | 分表 | 分库分表 |
---|---|---|
目标 | 优化单表性能 | 突破单库存储和性能上限 |
数据分布 | 单库多表 | 多库多表 |
查询复杂度 | 跨表查询需要手动处理 | 跨库查询需要复杂的逻辑 |
事务处理 | 单库事务,较简单 | 跨库事务,较复杂 |
扩展性 | 有限,受限于单库性能 | 高,支持大规模扩展 |
开发难度 | 较低 | 较高 |
运维成本 | 较低 | 较高 |
适用场景 | 单表数据量大,性能优化 | 数据量非常大,需要高并发高可用 |
IO瓶颈
热点数据太多,数据缓存不够,每次查询产生大量IO——分库、垂直分表
网络IO瓶颈,请求的数据太多,带宽不够、连接数过多——分库
CPU瓶颈
- SQL问题,join、group by、order by——SQL优化,构建索引
- 单表数据量过大,扫描行太多,SQL效率过低——水平分表
缓存 Redis
数据结构与实现
key-value 存储
有哪些数据结构
基础数据类型和实现方式
- String: SDS实现。 不仅可以存储字符流,还可以存储二进制流,长度复杂度为O(1),不会有缓存区溢出问题;对整数,可以转化为 Int 类型
- Hash:少:listpack;多:哈希表实现。listpack是 ziplist 的改进版本,更加紧凑,没有tail指针
- Set:少:整数集合;多:哈希表
- ZSet:少 listpack;多:跳表
- List:少 listpack;多:双向链表
新数据类型
- Bitmap 位图:二值状态统计系统
- GEO 地理信息
- stream 流:可以用于实现消息队列
- hyperloglog 计数器
线程模型
Redis基于内存,瓶颈主要在于内存和网络的带宽。如果采用多线程模型,可能带来额外的开销。网络采用I/O多路复用
Redis 6.0 以后,网络IO的吞吐量成为瓶颈,所以引入了多线程处理网络I/O。命令的执行还是基于主线程。
持久化
AOF 日志和 RDB快照
AOF 日志
将对 redis的CRUD操作以命令的形式存入到磁盘中
WAL(Write Ahead Logging): 先写数据,再更新日志。
RDB 快照
将某一时刻的内存数据以二进制形式存入磁盘
bgsave 不会阻塞主线程
如何实现持久化
结合 AOF 日志和 RDB 快照
以RDB快照为基线(前半部分),AOF 日志记录基线上的修改。
在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。
实战
功能
消息队列(延迟队列)
- 通过lua脚本订阅Key过期事件,消息主题应当存储在 Hash 表中
- Sorted Set 指定每个消息的过期时间,消费端轮询
实现分布式锁:因为处理过程单线程,所以保证只有一个用户可以获得共享资源
何时更新缓存
- 先更新数据库再删除缓存
- 在高命中率的条件下,可以同时更新数据库和缓存,但是需要分布式锁,或者及时从数据库更新缓存中的数据,保证数据一致性
总的来说,缓存是数据库的补充,一切以数据库为准。
缓存失效
缓存雪崩
大量缓存同时失效,导致大量请求送到数据库
解决方案:
- 不要让缓存数据同时失效
- 设置缓存不过期
缓存击穿
大量请求访问热点数据,但是缓存中没有
解决方案:
- 预加载
- 互斥锁,对同一个数据的访问,只有一个线程被接受
缓存穿透
数据既不在缓存中,也不在数据库中
解决方案:
- 非法请求限制
- 设置返回空值
- 布隆过滤器
布隆过滤器
初步过滤。Bloom Filter 会使用一个较大的 bit 数组来保存所有的数据,数组中的每个元素都只占用 1 bit ,并且每个元素只能是 0 或者 1(代表 false 或者 true),这也是 Bloom Filter 节省内存的核心所在。这样来算的话,申请一个 100w 个元素的位数组只占用 1000000Bit / 8 = 125000 Byte = 125000/1024 KB ≈ 122KB 的空间.
对对象做hash,将对应的hash位置做修改,1表示存在,0表示不存在。
综上,我们可以得出:布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。
缓存一致性
旁路缓存 Cache aside
读时,如果数据在缓存中,则直接返回,如果不在缓存中,那就从数据库获取并更新到缓存
写时,写直达数据库,然后删除缓存。

为什么要删除缓存?
写数据性能较差,两个连续的写请求可能导致无效缓存(缓存落后)。
为什么先更新数据库?
写请求的处理效率较低。连续到来的写读事务,第一个事务若先删除了缓存,第二个事务可能把旧的值重新写回缓存中。导致缓存落后。
延迟双删,在更新数据库前后都删除缓存。
是否还有风险?
是,比如一读一写,读把数据写入缓存,但是写操作并没有成功删除缓存,导致写之前的数据被更新到缓存。
解决办法:
删除重试机制
基于bin log 日志分析
数据传输服务,将缓存和数据库打包管理。
Read Though

Write Though

Write Behind

Write Around
如果一些非核心业务,对一致性的要求较弱,可以选择在 cache aside 读模式下增加一个缓存过期时间,在写请求中仅仅更新数据库,不做任何删除或更新缓存的操作,这样,缓存仅能通过过期时间失效。
大Key 和 热Key
Redisson
分布式锁
JAVA
基础
封装、继承、多态
**封装:**将数据和行为捆绑在一起,并对外隐藏内部实现细节。
**继承:**子类自动获得父类的属性和方法,实现代码复用和层次化设计。
多态:重写和重载
JDK JRE JVM
**JDK: ** Java Development Kit
**JRE: ** Java Runtime Environment
**JVM: ** Java Virtual Machine
跨平台,编译型和解释型
Java的跨平台能力源于其独特的**”编译+解释”混合执行模式**,完美结合了编译型语言和解释型语言的特点:
编译阶段(编译型语言特性)
- 源代码被编译成字节码(.class文件)
- 字节码是平台无关的中间代码,类似抽象指令集
javac HelloWorld.java
→ 生成HelloWorld.class
执行阶段(解释型语言特性)
- JVM(Java虚拟机)解释执行字节码
- 不同平台有对应的JVM实现(Windows/Linux/macOS等)
java HelloWorld
→ JVM实时将字节码转换为机器码执行
一次编译,到处运行:同一份字节码可在所有支持JVM的平台运行
数据类型
装箱和拆箱
装箱和拆箱是Java中基本数据类型与包装类之间的转换机制
享元模式
享元模式是一种结构型设计模式,主要用于减少内存使用,它通过共享多个对象共有的相同状态(内在状态),来支持大量细粒度对象的高效复用。
典型例子:各种包装类,常量池
String
字符串常量池在方法区/永生代(8以前),堆(8及以后)
1 |
|
StringBuilder 和 StringBuffer
StringBuffer 是线程安全的StringBuilder
在所有修改的方法上添加 synchronized 保证线程安全
toString 利用 toStringCache: CopyOnWrite
反射
反射的常见用途
- 加载数据库驱动
- Spring 配置文件加载和管理
- 代理
- 注解
注解
实现@Annotation接口
通过反射获得注解
代理
JDK 动态代理和 CGLIB 动态代理对比
- JDK 动态代理基于组合,只能代理实现了接口的类或者直接代理接口,而 CGLIB 基于继承,代理未实现任何接口的类。 另外, CGLIB 动态代理是通过生成一个被代理类的子类来拦截被代理类的方法调用,因此不能代理声明为 final 类型的类和方法。
- 就二者的效率来说,大部分情况都是 JDK 动态代理更优秀,随着 JDK 版本的升级,这个优势更加明显。
静态代理和动态代理的对比
- 灵活性:动态代理更加灵活,不需要必须实现接口,可以直接代理实现类,并且可以不需要针对每个目标类都创建一个代理类。另外,静态代理中,接口一旦新增加方法,目标对象和代理对象都要进行修改,这是非常麻烦的!
- JVM 层面:静态代理在编译时就将接口、实现类、代理类这些都变成了一个个实际的 class 文件。而动态代理是在运行时动态生成类字节码,并加载到 JVM 中的。
Throwable

异常
分类
运行时异常(RuntimeException, Unchecked Exception):这类异常在编译时期就必须被捕获或者声明抛出。它们通常是外部错误,如文件不存在(FileNotFoundException)、类未找到(ClassNotFoundException)等。非运行时异常强制程序员处理这些可能出现的问题,增强了程序的健壮性。可以不 try-catch 或者 throws
非运行时异常:这类异常在编译时期就必须被捕获或者声明抛出。它们通常是外部错误,如文件不存在(FileNotFoundException)、类未找到(ClassNotFoundException)等。非运行时异常强制程序员处理这些可能出现的问题,增强了程序的健壮性。
必须检查。
面向对象
设计原则
- 单一职责原则:一个类应该只有一个引起它变化的原因(即只负责一项职责)
- 开闭原则:对扩展开放,对修改关闭
- 里氏替换法则:子类必须能够完全替换它们的父类而不影响程序的正确性
- 接口隔离原则:客户端不应该被迫依赖它不需要的接口
- 依赖倒置原则:高层模块不应依赖低层模块,二者都应依赖抽象
- 迪米特法则,最小知识原则:一个对象应该对其他对象保持最少的了解
非静态内部类/静态内部类的区别
- 非静态内部类依赖于外部类的实例,而静态内部类不依赖于外部类的实例。
- 非静态内部类可以访问外部类的实例变量和方法,而静态内部类只能访问外部类的静态成员。
- 非静态内部类不能定义静态成员,而静态内部类可以定义静态成员。
- 非静态内部类在外部类实例化后才能实例化,而静态内部类可以独立实例化。
- 非静态内部类可以访问外部类的私有成员,而静态内部类不能直接访问外部类的私有成员,需要通过实例化外部类来访问。
实现深拷贝的方式
- 实现 Cloneable 接口并重写 clone() 方法
- 使用序列化和反序列化
- 手动递归复制
泛型和泛型擦除
类似于 CPP 中的模板。但是 CPP 中的模板可以根据你的实例化方式创建一份对应类型的具体代码。
泛型的主要目的是在编译时提供更强的类型检查,并且在编译后能够保留类型信息,避免了在运行时出现类型转换异常。减少强制类型转换。
泛型擦除(Type Erasure) 是 Java 泛型实现的一种机制,它的核心思想是:在编译时保留泛型信息以进行类型检查,但在运行时将泛型类型替换为其原始类型(Raw Type)或边界类型(Bound Type)。这种机制是为了兼容 Java 的早期版本(Java 5 之前没有泛型)而设计的。
运行时并不检查泛型的类型。
序列化和反序列化
不用java 原生的序列化方法
实现Serializable接口
transient 关键字
阻止序列化,只修饰 field
static 也不会被序列化
集合

Collections
java 提供的工具类
集合遍历方式
- for
- for-each 语法糖
- 迭代器
- 列表迭代器
- forEach
- Stream API (JAVA8)
ArrayList
扩容
1.5倍扩容
转换线程安全
1 | List<String> synchronizedList = Collections.synchronizedList(arrayList); |
全局锁
HashMap
链表与红黑树
当冲突链表长度超过8时,转化为红黑树,红黑树的节点数 <= 6时,转化为链表
优先考虑扩容的问题
put过程

默认大小 扩容
16
扩容因子 0.75 超过 0.75 扩容 (方便计算?)
为什么String适合做key
String 是 static final的,字面一样的 String hashcode 和 equals 方法返回一定。
Set
通过HashMap 的 keySet 实现
有序集合: TreeSet LinkedHashSet
多线程集合
List -> Vector, CopyOnWriteList; Vector 给所有方法加锁
Map -> HashTable, ConcurrentHashMap; HashTable 给全局加锁
CopyOnWriteList实现
CopyOnWriteList:
ReentrantLock 保证只有一个线程进行写操作。
写时复制一份副本,对副本进行修改,再将指针指向副本。
ConcurrentHashMap 实现
1.7 以前

分段锁是可以重入的
1.8 以后

如果存储位置为空则使用 volatile 加 CAS (乐观锁) 来初始化
如果容器不为空,则根据存储的元素计算该位置是否为空
如果存储的元素为空,用 CAS 乐观锁设置节点
如果不为空,使用 synchronize 悲观锁,搜出旧的节点并设置新的值,并要计算是否需要转型。
ConcurrentSkipListMap
特性 | ConcurrentSkipListMap |
ConcurrentHashMap |
---|---|---|
实现方式 | 基于跳表 | 基于哈希表 |
有序性 | 有序(按键排序) | 无序 |
时间复杂度 | 查找、插入、删除:O(log n) | 查找、插入、删除:平均 O(1) |
适用场景 | 需要有序映射或范围查询 | 需要高并发访问 |
Java 升级
ver8
- lambda
- stream
- 接口默认方法
- string 优化
- 去除永生代,改到 meta space,String常量池移动到堆里,String新的实现
- ……
ver11
- 局部变量推断 lambda
- 新的字符串和文件 API
- ZGC
- 单文件运行
- ……
ver17
- Sealed 类,指定继承者
- 增强的伪随机数生成器
- instanceof 类型转换
- switch -> yield,
- stream 增强
- ……
JVM
JAVA 内存设计

meta space 设计在直接内存(direct memory)上
除BootStrapClassLoader 其他在堆内存中,类的元数据在 meta space
元空间中的内容
使用本地内存
- 类元数据
- 类信息
- 类方法
- 类字段
- 常量池:数字常量、符号引用
- 类静态变量
- 方法字节码
- 注解信息: 类 方法 字段的注解
- 类加载器
- 运行常量池
程序计数器是每个线程独有的
程序计数器用于记录线程执行到的地方
四种引用
- 强引用指的就是代码中普遍存在的赋值方式,比如A a = new A()这种。强引用关联的对象,永远不会被GC回收。
- 软引用可以用SoftReference来描述,指的是那些有用但是不是必须要的对象。系统在发生内存溢出前会对这类引用的对象进行回收。
- 弱引用可以用WeakReference来描述,他的强度比软引用更低一点,弱引用的对象下一次GC的时候一定会被回收,而不管内存是否足够。
- 虚引用也被称作幻影引用,是最弱的引用关系,可以用PhantomReference来描述,他必须和ReferenceQueue一起使用,同样的当发生GC的时候,虚引用也会被回收。可以用虚引用来管理堆外内存。
弱引用的应用场景:
缓存
对象池
防止内存泄漏
类加载和初始化
类的生命周期
- 加载:获取.class 字节流,在元数据区创建 Class 对象
- 连接
- 验证: 验证 class 字节流的正确性
- 准备 :给静态字段分配空间并设置默认值
- 解析:将符号引用,转化为直接引用
- 初始化:执行静态代码块,并给
final static
字段赋值。按照顺序执行静态代码块. - 使用
- 卸载:如果有下面的情况,类就会被卸载:1. 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。2. 加载该类的ClassLoader已经被回收。 3. 类对应的Java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。 1 和 3 有共通点
双亲委派模型
其实就是父委派模型,只有当父加载器无法加载类时,才自身加载类,当然父加载器也是看他的父加载器能不能加载。
BootstrapClassLoader 是根,用CPP实现

如何打破双亲委派模型?
- 自定义类加载器并重写
loadClass
方法。 - 使用线程上下文类加载器。
- 使用 OSGi 模块化加载。
- 使用 Java 9+ 的模块化系统。
- 在热部署场景中使用独立的类加载器。
static final
变量的赋值
- 如果它是编译时常量,其值在编译阶段就已经确定,并直接嵌入到字节码中,准备阶段。
- 如果它是非编译时常量,其值会在类初始化阶段通过静态代码块或方法调用赋值,初始化阶段。
垃圾回收
判断垃圾的方式
引用计数器
可达性分析
GC root 绝大多数是静态变量;本地方法引用的对象
垃圾回收区域
堆和方法区
分代
新生代
- Eden
- survivor
老生代
为什么要survivor 区域
绝大多数对象,即使一次不会gc,第二次也会被gc
Minor GC, Major GC, Full GC
Minor GC 对新生代进行垃圾挥手
Major GC 对老年代进行垃圾回收 Stop the world
Full GC Minor + Major GC
垃圾回收器
CMS

对老年代进程垃圾回收
一个大对象如果太大,可能会横跨多个 Region 来存放。
CMS 的优点是:并发收集、低停顿。
缺点:对CPU资源敏感,标记清除算法产生内存碎片
G1
分区垃圾回收器大于分代,标记-整理,提高CPU领用率,减少STW时间
内存被划分为多个 region,每个region 可以是每一个代的元素
使用标记整理法处理内存碎片
停顿时间: 复制,初始标记,在标记,清理
ZGC
低延迟 高吞吐量
在 Java21 中,引入了分代 ZGC,暂停时间可以缩短到 1 毫秒以内。
I/O和直接内存
在JVM 内存外的一个部分,直接在物理机内存上,通过直接内存减少用户态和内核态切换的问题。
JVM 新技术
逃逸分析
是目前Java虚拟机中比较前沿的优化技术。这是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。通过逃逸分析,Java Hotspot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆上。
同步消除:如果一个对象被逃逸分析发现只能被一个线程所访问,那对于这个对象的操作可以不同步。
栈上分配:如果确定一个对象不会逃逸出线程之外,那让这个对象在栈上分配内存将会是一个很不错的主意,对象所占用的内存空间就可以随栈帧出栈而销毁。
标量替换:如果一个对象被逃逸分析发现不会被外部方法访问,并且这个对象可以拆散,那么程序真正执行的时候将可能不去创建这个对象,而改为直接创建它的若干个比这个方法使用的成员变量来代替。
JIT
JIT(Just-In-Time) 是一种动态编译技术,全称为 Just-In-Time Compilation(即时编译)。它主要用于提高程序的运行效率,特别是在解释型语言或虚拟机环境中(如 Java 的 JVM、.NET 的 CLR)。JIT 的核心思想是在程序运行时将热点代码(频繁执行的代码)动态编译为机器码,从而提升执行速度。
线程局部字段优化(Thread-Local Field Optimization)
或者称之为 “选择性线程局部变量”(Selective Thread-Local Variables)。
这是一种在 Java 或其他编程语言中用于优化多线程程序性能的技术。
背景
在传统的多线程编程中,线程本地存储(Thread-Local Storage, TLS)通常用于为每个线程创建独立的变量副本,以避免线程间的竞争条件。然而,传统的线程本地存储会将整个对象或数据结构复制到每个线程的本地内存中,这可能会导致内存浪费和性能开销,尤其是当对象较大或线程数量较多时。
技术原理
线程局部字段优化 的核心思想是:
- 只拷贝需要的字段:而不是将整个对象复制到线程本地内存中,只复制那些真正需要在线程间隔离的字段。
- 减少内存开销:通过只复制必要的字段,减少了内存占用和复制开销。
- 提高缓存局部性:由于只复制少量字段,数据更有可能被缓存在 CPU 缓存中,从而提高访问速度
JVM 调优
如何处理OOM异常
发现问题
有可能是内存泄露,内存不足,外部链接未释放,配置不当等问题。
前期在服务运行过程中,可以使用Prometheus、VisualVM或者Arthas这种工具来监控运行过程内存异常情况,比如Full GC后内存占用没有明显下降,并且内存占用在持续走高,说明服务中可能出现了内存泄漏。
定位问题
启用堆转储:
添加JVM参数 -XX:+HeapDumpOnOutOfMemoryError,在OOM时自动生成堆转储文件(heap dump)。
使用 -XX:HeapDumpPath=
指定堆转储文件的保存路径。 分析堆转储文件:
使用工具(如 Eclipse MAT、VisualVM、JProfiler)分析堆转储文件,查看内存中占用最多的对象及其引用链
通过线程Thread的methodHandler去定位OOM时执行的代码,找到深堆较大的对象,然后再去对应的业务代码中分析这些对象创建使用存在的内存泄漏问题,最后修复代码并验证测试。
GC日志分析:
添加JVM参数 -XX:+PrintGCDetails 和 -XX:+PrintGCDateStamps,生成GC日志。
使用工具(如 GCViewer)分析GC日志,查看GC频率、停顿时间和内存回收情况。
垃圾回收优化
分析工具:使用GCViewer
、gceasy.io
等工具分析GC日志,关注:
- Full GC频率:频繁Full GC可能需增大堆或优化对象生命周期。
- STW时间:暂停时间过长需调整收集器或目标停顿时间。
JIT 和 AOT
JIT:对于热点代码,开启JIT内联优化
AOT:通过静态分析的方式分辨哪些代码应当直接编译为机器码,减少JIT预热的困难
监控与诊断工具
- 基础工具:
jps
:查看JVM进程。jstat
:监控内存和GC状态(如jstat -gcutil <pid> 1000
)。jmap
:生成堆转储(jmap -dump:format=b,file=heap.hprof <pid>
)。jstack
:抓取线程栈(分析死锁或高CPU问题)。
- 可视化工具:
- VisualVM:实时监控堆、线程、CPU。
- MAT(Memory Analyzer):分析内存泄漏。
- Arthas:在线诊断工具(动态查看类加载、方法执行耗时)
Java 并发
多线程基础
多线程开发原则
- 原子性
- 可见性
- 一致性
为什么实现 Runnable/Callable
接口比直接继承Thread
更好
- 避免了 Java 单继承的局限性,Java 不支持多重继承,因此如果我们的类已经继承了另一个类,就不能再继承 Thread 类了。
- 适合多个相同的程序代码去处理同一资源的情况,把线程、代码和数据有效的分离,更符合面向对象的设计思想。Callable 接口与 Runnable 非常相似,但可以返回一个结果。
- 通常不使用直接显示创建线程的方式实现多线程。
线程状态

blocked 和 waiting 区别
blocked: 获得 synchronized 失败
waiting: wait join park sleep
线程安全
原子性:一个操作的完整性
可见性:一个操作对其他线程的可见性
死锁,活锁,饥饿
死锁
多个线程因 互相等待对方释放资源 而陷入无限阻塞的状态,导致所有线程都无法继续执行。
条件:互斥,持有并等待,不可剥夺,循环等待;
活锁
线程 不断重复执行某个操作,但由于外部条件未满足(如其他线程的干扰),始终无法推进任务。
与死锁的区别:线程未被阻塞,而是“忙等”(主动重试但无效)。
饥饿
某个线程 长期无法获取所需资源(如 CPU 时间片、锁),导致任务无法执行
JMM java memory model
与区分 JVM内存区分, 实现多线程通信和同步
主内存与本地内存
主内存是所有线程共享的内容。
每个线程都有一个私有的本地内存,本地内存存储了该线程以读 / 写共享变量的副本。
- JMM 不拷贝完整对象:仅按需复制被访问的字段到线程本地内存。
- 同步决定可见性:通过
volatile
、synchronized
等机制控制内存同步范围。 - 性能与正确性平衡:合理使用同步机制避免过度拷贝,同时保障多线程数据一致性
Happens-Before
如果操作 A happens-before 操作 B,那么 A 的结果对 B 可见,且编译器和处理器不会对 A 和 B 重排序到违反这一关系的顺序。
happens-before
不意味着时间上的先后,而是逻辑上的可见性保证。
不可见问题
每个线程对堆中的数据设置有高速缓存,导致线程私有的脏数据没有写入公共堆中,从而其他线程看不到本线程做出的修改。
volatile 关键字
指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致 ,所以在多线程下,指令重排序可能会导致一些问题。
禁止指令重排序,保证变量可见性
volatile
关键字能保证数据的可见性,但不能保证数据的原子性。synchronized
关键字两者都能保证
ThrealLocal
每个线程独有的 ThreadLocalMap 以 ThreadLocal 对象为key 去Map 中取 Value
ThreadLocalMap 通过开放地址法解决冲突问题
Future
Future
是 Java 5 引入的一个接口,用于表示一个异步计算的结果。它提供了一种检查计算是否完成的方式,并可以获取计算结果。
核心方法
boolean isDone()
:判断任务是否完成。V get()
:获取计算结果,如果任务未完成,则阻塞当前线程直到任务完成。V get(long timeout, TimeUnit unit)
:在指定时间内获取计算结果,超时抛出TimeoutException
。boolean cancel(boolean mayInterruptIfRunning)
:尝试取消任务。
CompletableFuture
CompletableFuture
是 Java 8 引入的一个类,实现了 Future
接口,并提供了更强大的功能。它支持任务之间的依赖关系、异常处理、组合操作等,是 Future
的增强版。
核心方法:
- 创建任务:
CompletableFuture.runAsync(Runnable runnable)
:执行无返回值的任务。CompletableFuture.supplyAsync(Supplier<U> supplier)
:执行有返回值的任务。
- 任务回调:
thenApply(Function<T, U> fn)
:对任务结果进行处理。thenAccept(Consumer<T> action)
:消费任务结果。thenRun(Runnable action)
:任务完成后执行操作。
- 任务组合:
thenCompose(Function<T, CompletableFuture<U>> fn)
:将两个任务串行执行。thenCombine(CompletionStage<U> other, BiFunction<T, U, V> fn)
:将两个任务并行执行并合并结果。
- 异常处理:
exceptionally(Function<Throwable, T> fn)
:处理异常并返回默认值。handle(BiFunction<T, Throwable, U> fn)
:无论是否发生异常,都会执行。
- 手动完成:
complete(T value)
:手动完成任务并设置结果。completeExceptionally(Throwable ex)
:手动完成任务并抛出异常。
Condition
Condition
是 Java 中用于线程间协调的工具类,属于 java.util.concurrent.locks
包。它与 Lock
接口配合使用,提供了比 Object
的 wait()
、notify()
和 notifyAll()
更灵活的线程等待和唤醒机制。
Condition 的核心功能
Condition
允许线程在某些条件下等待,并在条件满足时被唤醒。它的主要方法包括:
- 等待:
await()
:使当前线程等待,直到被唤醒或中断。await(long time, TimeUnit unit)
:使当前线程等待,直到被唤醒、中断或超时。awaitUninterruptibly()
:使当前线程等待,直到被唤醒(不可中断)。
- 唤醒:
signal()
:唤醒一个等待的线程。signalAll()
:唤醒所有等待的线程。
Condition 的使用场景
Condition
通常用于实现复杂的线程同步机制,例如:
- 生产者-消费者模型。
- 线程间的条件等待和通知。
- 替代
Object
的wait()
和notify()
,提供更细粒度的控制。
Condition 的基本用法
Condition
必须与 Lock
一起使用。通过 Lock.newCondition()
方法可以创建一个 Condition
对象
常见并发容器
ConcurrentHashMap
TODO
CopyOnWriteArrayList
TODO
ConcurrentLinkedQueue
TODO
锁
AQS 抽象队列同步器
AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是基于 CLH 锁 (Craig, Landin, and Hagersten locks) 进一步优化实现的。
AQS 队列是双向链表 CLH锁列表是单列表

重写获取/释放方法实现各种锁
独占模式
在独占模式下,一个任务弹出后,直接通知他的
共享模式
乐观锁和悲观锁
悲观锁适用于写多读少,而乐观锁适用于读多写少。
悲观锁
悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改)。共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。
像 Java 中synchronized
和ReentrantLock
等独占锁就是悲观锁思想的实现。
乐观锁
乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。
高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。但是,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试,这样同样会非常影响性能,导致 CPU 飙升。
乐观锁一般会使用版本号机制或 CAS 算法实现,CAS 算法相对来说更多一些。
synchronized 关键字
可以修饰的对象
- 实例方法(对象本身)
- 静态方法 (对象.class)
- 代码块 (自定义)
可重入
不可以对构造函数使用,但是可以对构造函数中的代码块使用
JDK1.6 偏向锁和轻量级锁 解决内核态和用户态切换的问题
四个状态: 锁升级顺序:无锁-> 偏向锁 -> 轻量级锁 -> 重量级锁
锁降级发生在垃圾回收的 STW 堆闲置的锁降级。
偏向锁:其实也没有锁,只有一个线程访问同步块。
轻量级锁:通过自旋实现,不断获取锁。有冲突升级到轻量级
重量级锁:线程阻塞实现。和1.6以前的实现方式一致。10次自旋失败后升级至重量级
锁信息存储在对象头 markword 中。
CAS
JNI 内联实现
乐观锁,无锁,冲突->失败->重试 循环
UnSafe 保证原子性
实现
- 访问一个值,记录当前值
- 基于访问记录计算新值
- 准备把新值写入;写入前,确认
公平锁和非公平锁
公平锁 : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。
非公平锁:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。
问题
ABA问题->解决办法:时间戳和版本号
线程池
线程池就是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。
目的是降低创建和销毁线程所浪费的资源和时间,提高响应效率和提高对线程资源的管理。
建议通过 ThreadPoolExecutor
构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。Executors
容易产生 OOM问题。
线程池的参数 744
7个参数
- 核心线程数
- 最大线程数
- 等待队列(长度,排序方式任务优先级)
- 最大非核心线程存活时间
- 最大非核心线程存活时间单位
- 拒绝策略
- 线程工厂
核心线程数,最大线程数,等待队列可以动态修改
4个拒绝策略
直接拒绝并抛弃:
ThreadPoolExecutor.DiscardPolicy
抛出异常:
ThreadPoolExecutor.AbortPolicy
使用 Caller 线程执行:
ThreadPoolExecutor.CallerRunsPolicy
阻塞 Caller 线程,让 Caller线程执行任务,这样可以减缓线程池任务提交速度;如果Caller 线程关闭,则直接抛弃这个任务如果走到
CallerRunsPolicy
的任务是个非常耗时的任务,且处理提交任务的线程是主线程,可能会导致主线程阻塞,影响程序的正常运行。丢弃等待队列中存活时间最长的任务:
ThreadPoolExecutor.DiscardOldestPolicy
4个阻塞队列(和 **ThreadPoolExecutor
**中的等待队列不同,不重要)
LinkedBlockingQueue
(有界阻塞队列):链表;FixedThreadPool
只有核心线程数量,没有最大线程数SingleThreadExecutor
只有一个核心线程。SynchronousQueue
(同步队列):没有容量,不存储元素,目的是保证对于提交的任务,用于CachedThreadPool
总有线程执行DelayedWorkQueue
(延迟队列):堆,ScheduledThreadPool
和SingleThreadScheduledExecutor
。优先执行到了执行时间的任务。ArrayBlockingQueue
(有界阻塞队列):数组,容量固定。
线程池调优
CPU 密集型任务 (N): 这种任务消耗的主要是 CPU 资源,线程数应设置为 N(CPU 核心数)。由于任务主要瓶颈在于 CPU 计算能力,与核心数相等的线程数能够最大化 CPU 利用率,过多线程反而会导致竞争和上下文切换开销。
I/O 密集型任务(M * N): 这类任务大部分时间处理 I/O 交互,线程在等待 I/O 时不占用 CPU。 为了充分利用 CPU 资源,线程数可以设置为 M * N,其中 N 是 CPU 核心数,M 是一个大于 1 的倍数,建议默认设置为 2 ,具体取值取决于 I/O 等待时间和任务特点,需要通过测试和监控找到最佳平衡点。
程池中线程异常后,销毁还是复用?
简单来说:使用execute()
时,未捕获异常导致线程终止,线程池创建新线程替代;使用submit()
时,异常被封装在Future
中,线程继续复用。
这种设计允许submit()
提供更灵活的错误处理机制,因为它允许调用者决定如何处理异常,而execute()
则适用于那些不需要关注执行结果的场景。
线程池和 ThreadLocal 共用的坑
线程池和 ThreadLocal
共用,可能会导致线程从ThreadLocal
获取到的是旧值/脏数据。这是因为线程池会复用线程对象,与线程对象绑定的类的静态属性 ThreadLocal
变量也会被重用,这就导致一个线程可能获取到其他线程的ThreadLocal
值。
定时任务
cheduledThreadPoolExecutor
继承了ThreadPoolExecutor
,并实现了ScheduledExecutorService
接口。`
DelayedWorkQueue
当一个线程成为 leader,它只需等待队首任务的 delay 时间即可,其他线程会无条件等待。leader 取到任务返回前要通知其他线程,直到有线程成为新的 leader。每当队首的定时任务被其他更早需要执行的任务替换,leader 就设置为 null,其他等待的线程(被当前 leader 通知)和当前的 leader 重新竞争成为 leader。
DelayedWorkQueue
是一个优先级队列,它可以保证每次出队的任务都是当前队列中执行时间最靠前的,由于它是基于堆结构的队列,堆结构在执行插入和删除操作时的最坏时间复杂度是 O(logN)
所有线程都会有三种身份中的一种:leader、follower,以及一个干活中的状态:processor。它的基本原则是,永远最多只有一个 leader。所有 follower 都在等待成为 leader。leader成为 processor后,通知follower选举新的 leader
Spring SpringBoot

设计模式
- 代理
- 工厂方法
- 单例
- 模板方法
- 观察者
- 包装器
- 适配器
动态代理和静态代理
代理产生的时间,编译时还是运行时
动态谈代理基于反射和代码生产实现
静态代理需要在编译时就确认好被代理的类。
DI和IoC 依赖注入和控制反转
IoC是目标,依赖注入是实现方式。要使用,就通过名字或者类型从容器中拿。
Bean 生命周期
Spring Bean的生命周期包括实例化、属性赋值、初始化、使用和销毁等多个阶段。
对于非单例的(非全局作用域的)Bean,如原型类型的,Spring 只负责创建,销毁应当用户自行完成。 destory()
Bean 线程安全
Bean 应当是无状态的
如何避免循环依赖
三级缓存
使用三个缓存来解决循环依赖:
一级缓存(singletonObjects):存放完全初始化的单例 Bean。
二级缓存(earlySingletonObjects):存放提前暴露的 Bean 实例(已实例化但未完全初始化)。
三级缓存(singletonFactories):存放 Bean 工厂对象,用于生成提前暴露的 Bean 实例。
一个对象初始化的时候会把自己的工厂放到三级缓存。循环依赖发现时,会利用A的工厂生产一个早期引用,并放入二级缓存,同时删除三级缓存工厂。都完成初始化后,将两个对象放入一级缓存。
为什么不二级缓存
可能产生多个不同的 Bean
也就是说,A 依赖的 B,B依赖的A并不是原来的A。
常见注解
- @AutoWired 自动装配 Bean
- @Configuration 配置类
- @Bean 标记方法返回的对象是Bean 可以指定名称
- Service Controller @Repository
Bean 作用域
- singleton 单例 全局作用域
- Prototype 原型 每个请求适用一个
- Request 每个HTTP请求一个
- Session 绘画
- Application
- WebSocket
- 自定义作用域
Bean是线程不安全的,但是应当是无状态的,在需要控制线程安全的时候,开发人员应当选择合适的作用域。
AOP面向切面
主要基于动态代理
AOP 常见注解
- @Aspect:用于定义切面,标注在切面类上
- @Pointcut:定义切点,标注在方法上,用于指定连接点。
- @Before:在方法执行之前执行通知。
- @After:在方法执行之后执行通知。
- @Around:在方法执行前后都执行通知。
- @AfterReturning:在方法执行后返回结果后执行通知。
- @AfterThrowing:在方法抛出异常后执行通知。
- @Advice:通用的通知类型,可以替代@Before、@After等。
AOP 有哪些环绕方式?
AOP 一般有 5 种环绕方式:
- 前置通知 (@Before)
- 返回通知 (@AfterReturning)
- 异常通知 (@AfterThrowing)
- 后置通知 (@After)
- 环绕通知 (@Around)
多个切面的情况下,可以通过 @Order
指定先后顺序,数字越小,优先级越高
Spring AOP 发生在什么时候?
Spring AOP 基于运行时代理机制,这意味着 Spring AOP 是在运行时通过动态代理生成的,而不是在编译时或类加载时生成的
AOP的使用场景有哪些?
AOP 的使用场景有很多,比如说日志记录、事务管理、权限控制、性能监控等。
说说 Spring AOP 和 AspectJ AOP 区别?
Spring AOP 属于运行时增强
,主要具有如下特点:
- 基于动态代理来实现,默认如果使用接口的,用 JDK 提供的动态代理实现,如果是方法则使用 CGLIB 实现
- Spring AOP 需要依赖 IoC 容器来管理,并且只能作用于 Spring 容器,使用纯 Java 代码实现
AspectJ 属于静态织入,通过修改代码来实现,在实际运行之前就完成了织入,所以说它生成的类是没有额外运行时开销的,一般有如下几个织入的时机:
注解
@Autwired和@Resource注解的区别
特性 | @Autowired |
@Resource |
---|---|---|
来源 | Spring 特有 | JSR-250 标准 |
默认注入方式 | 按类型(byType) | 按名称(byName) |
指定名称 | 需结合 @Qualifier |
直接通过 name 属性指定 |
适用范围 | 字段、构造方法、Setter 方法、普通方法 | 字段、Setter 方法 |
灵活性 | 支持 required=false ,支持 @Primary |
不支持 required ,不支持 @Primary |
推荐场景 | Spring 项目,按类型注入 | 按名称注入,非 Spring 环境 |
事务 @Transactional
Java 事务的核心原理是 通过事务管理器绑定数据库连接,利用 AOP 代理拦截方法实现事务的开启、提交和回滚
声明式事务基于AOP实现。不足的地方是,声明式事务管理最细粒度只能作用到方法级别,无法像编程式事务那样可以作用到代码块级别。
事务的传播机制
传播行为:定义事务方法调用时的边界策略。
传播类型 | 是否需要事务 | 事务独立性 | 典型场景 |
---|---|---|---|
REQUIRED |
可选 | 如果当前存在事务,则加入该事务。 如果当前没有事务,则新建一个事务。 |
通用业务逻辑 |
SUPPORTS |
可选 | 如果当前存在事务,则加入该事务。 如果当前没有事务,则以非事务方式执行。 |
查询操作 |
MANDATORY |
必须 | 必须加入已有事务 | 关键数据变更 |
REQUIRES_NEW |
强制新建 | 新建独立事务,挂起外层事务 | 独立提交的操作(如日志) |
NOT_SUPPORTED |
禁止 | 非事务执行,挂起外层事务 | 非事务资源操作 |
NEVER |
禁止 | 必须非事务环境 | 严格无事务场景 |
NESTED |
可选 | 如果当前存在事务,则创建一个嵌套事务(基于 Savepoint)。 嵌套事务回滚不影响外层事务,但外层事务回滚会触发嵌套事务回滚。 如果当前没有事务,则等同于 REQUIRED。 |
部分回滚的子操作 |
事务失效
- 未捕获异常
- 非收件异常
- 传播属性设置不当
- 多数据源配置错误
- 事务调用非事务
事务的使用场景
1. 需要原子性(Atomicity)的操作
- 金融交易:如转账(A账户扣款,B账户加款)。
- 订单处理:下单后同时扣减库存、生成订单记录。
- 批量数据操作:如导入数据时多个表的关联更新。
2. 需要隔离性(Isolation)的并发场景
- 多个用户同时操作同一数据时,避免脏读、不可重复读、幻读等问题。
(例如:订票系统中多个用户抢同一张票)
3. 复杂业务逻辑
- 跨多个步骤的操作,若中间步骤失败需回滚(如注册用户时需同时初始化权限表、日志表等)
如何防止长事务
为什么长事务?
- 锁资源占用:长时间持有锁,导致其他事务阻塞(如行锁、表锁)。
- 死锁风险:多个事务相互等待资源,引发死锁。
- 回滚开销大:事务越久,回滚日志(undo log)体积越大,失败时恢复越慢。
- 性能瓶颈:高并发场景下,长事务会显著降低数据库吞吐量。
如何避免
- 拆分大事务,将事务异步处理。
- 减少持有锁的时间
- 将非数据库事务移出
- 设置事务超时
- 优化SQL
SpringBoot vs Spring
- 提供了 约定优于配置 的理念,默认配置了大量常用的功能(如内嵌服务器、数据源、安全等)
- 自动装配
@EnableAutoConfiguration
- 内置了 Tomcat、Jetty 或 Undertow 等服务器,可以直接运行 JAR 文件。
- 使用 Maven 或 Gradle 时,依赖管理更加简单和一致。通过 Starter 依赖(如
spring-boot-starter-web
、spring-boot-starter-data-jpa
),自动引入相关依赖和兼容版本。 - 默认使用 application.properties 或 application.yml 文件进行配置。
- 支持微服务
- ……
特性 | Spring | Spring Boot |
---|---|---|
配置方式 | 手动配置(XML/Java) | 自动配置,约定优于配置 |
内嵌服务器 | 无,需外部服务器 | 内置 Tomcat/Jetty/Undertow |
依赖管理 | 手动管理 | Starter 依赖,Maven, Gradle 自动管理 |
打包方式 | WAR 文件 | 可执行 JAR 文件 |
监控和管理 | 需手动集成 Actuator | 内置 Actuator,开箱即用 |
微服务支持 | 需手动集成 Spring Cloud | 与 Spring Cloud 无缝集成 |
学习曲线 | 较陡 | 较平缓 |
适用场景 | 大型企业级应用 | 快速开发、微服务架构 |
如何理解约定大于配置
减少手动配置,自动化配置,默认配置,约定项目结构
分布式
分布式理论
CAP
- 一致性 Consistency
- 可用性 Availability
- 分区容错性 Partition Tolerance
只能保证其中两个,但是一定要保证分区容错性。
BA SE
BASE 的主要含义:
Basically Available(基本可用)
假设系统出现了不可预知的故障,但还是能用,只是相比较正常的系统而言,可能会有响应时间上的损失,或者功能上的降级。
Soft State(软状态)
要求多个节点的数据副本都是一致的,这是一种“硬状态”。
软状态也称为弱状态,相比较硬状态而言,允许系统中的数据存在中间状态,并认为该状态不影响系统的整体可用性,即允许系统在多个不同节点的数据副本存在数据延时。
Eventually Consistent(最终一致性)
上面说了软状态,但是不应该一直都是软状态。在一定时间后,应该到达一个最终的状态,保证所有副本保持数据一致性,从而达到数据的最终一致性。这个时间取决于网络延时、系统负载、数据复制方案设计等等因素。
Paxos 算法

提议者的提案被超过半数的接收者接受就交给学习者,学习者将结果返回给客户端
Raft 算法
基于 Multi-Paxos 实现
三个角色
Leader
:负责发起心跳,响应客户端,创建日志,同步日志。Candidate
:Leader 选举过程中的临时角色,由 Follower 转化而来,发起投票参与竞选。Follower
:接受 Leader 的心跳和日志同步数据,投票给 Candidate。
容灾
如果 Leader 崩溃,集群中的节点在 electionTimeout 时间内没有收到 Leader 的心跳信息就会触发新一轮的选主,在选主期间整个集群对外是不可用的。
如果 Follower 和 Candidate 崩溃,处理方式会简单很多。之后发送给它的 RequestVoteRPC 和 AppendEntriesRPC 会失败。由于 raft 的所有请求都是幂等的,所以失败的话会无限的重试。如果崩溃恢复后,就可以收到新的请求,然后选择追加或者拒绝 entry。
选举
一个 Follower 收不到 Leader 的心跳信息和请求,就认为 Leader 失效,将自己设置为 Candidate
Candidate 成为 Leader 的条件是获得过半数投票。
一次投票至裁决一个 Candidate。
工作
entry :<index,term,cmd>
只有leader 可以创建entry
在两个日志里,有两个 entry 拥有相同的 index 和 term,那么它们一定有相同的 cmd
在两个日志里,有两个 entry 拥有相同的 index 和 term,那么它们前面的 entry 也一定相同
分布式协议
- 2PC 两阶段提交
- TCC
分布式ID
- 全局唯一
- 高性能,生产块
- 高可用
- 方便
- 有序递增
- 有含义
- 能独立部署
算法
雪花算法
时间戳+设备ID+信息序列号
分布式锁
互斥,高可用,可重入
实现方式
Zookeeper
Redis:
- 如果 key 不存在,则可以插入,表示加锁成功;若 key 存在 则表示加锁失败
- 锁要设置过期时间
- 加锁要实现原子性 NX 选项
- 分辨客户端,以便实现可重入,避免误删除
消息队列
MQ 的核心机制是通过 生产者(Producer)发布消息 到指定主题(Topic)或队列(Queue),消费者(Consumer)订阅并消费消息。以下是具体实现原理:
使用场景
- 系统解耦,多个系统依赖同一核心服务,需避免直接耦合。
- 异步处理,非核心操作耗时较长,需避免阻塞主流程。
- 流量削峰:突发高流量场景(如秒杀、抢购),避免压垮下游系统。
- 日志服务
- 分布式事务最终一致性
组件
以RocketMQ 为例
注册中心 NameServer
NameServer 是一个无状态的服务器,角色类似于 Kafka 使用的 Zookeeper,但比 Zookeeper 更轻量。
每个 NameServer 结点之间是相互独立,彼此没有任何信息交互。
功能:
和 Broker 结点保持长连接。
维护 Topic 的路由信息。
生产者 Producer
消息生产者,业务端负责发送消息,由用户自行实现和分布式部署。
Producer由用户进行分布式部署,消息由Producer通过多种负载均衡模式发送到Broker集群,发送低延时,支持快速失败。
RocketMQ 提供了三种方式发送消息:同步、异步和单向
- 同步发送:同步发送指消息发送方发出数据后会在收到接收方发回响应之后才发下一个数据包。一般用于重要通知消息,例如重要通知邮件、营销短信。
- 异步发送:异步发送指发送方发出数据后,不等接收方发回响应,接着发送下个数据包,一般用于可能链路耗时较长而对响应时间敏感的业务场景,例如用户视频上传后通知启动转码服务。
- 单向发送:单向发送是指只负责发送消息而不等待服务器回应且没有回调函数触发,适用于某些耗时非常短但对可靠性要求并不高的场景,例如日志收集
消费者 Consumer
消息消费者,负责消费消息,一般是后台系统负责异步消费。
Consumer也由用户部署,支持 PUSH 和 PULL 两种消费模式,支持集群消费和广播消费,提供实时的消息订阅机制。
- Pull:拉取型消费者(Pull Consumer)主动从消息服务器拉取信息,只要批量拉取到消息,用户应用就会启动消费过程,所以 Pull 称为主动消费型。
- Push:推送型消费者(Push Consumer)封装了消息的拉取、消费进度和其他的内部维护工作,将消息到达时执行的回调接口留给用户应用程序来实现。所以 Push 称为被动消费类型,但其实从实现上看还是从消息服务器中拉取消息,不同于 Pull 的是 Push 首先要注册消费监听器,当监听器处触发后才开始消费消息。
存储者 Broker
消息存储和中转角色,负责存储和转发消息。
Broker 内部维护着一个个 Consumer Queue,用来存储消息的索引,真正存储消息的地方是 CommitLog(日志文件)。
单个 Broker 与所有的 Nameserver 保持着长连接和心跳,并会定时将 Topic 信息同步到 NameServer,和 NameServer 的通信底层是通过 Netty 实现的。
Broker 可以配置两种角色:Master 和 Slave,Master 角色的 Broker 支持读和写,Slave 角色的 Broker 只支持读,Master 会向 Slave 同步消息。
也就是说 Producer 只能向 Master 角色的 Broker 写入消息,Consumer 可以从 Master 和 Slave 角色的 Broker 读取消息。
只要消息被刷盘持久化至磁盘文件 CommitLog 中,那么 Producer 发送的消息就不会丢失。正因为如此,Consumer 也就肯定有机会去消费这条消息。当无法拉取到消息后,可以等下一次消息拉取,同时服务端也支持长轮询模式,如果一个消息拉取请求未拉取到消息,Broker 允许等待 30s 的时间,只要这段时间内有新消息到达,将直接返回给消费端。
刷盘策略
- 同步刷盘:在消息达到 Broker 的内存之后,必须刷到 commitLog 日志文件中才算成功,然后返回 Producer 数据已经发送成功。
- 异步刷盘:异步刷盘是指消息达到 Broker 内存后就返回 Producer 数据已经发送成功,会唤醒一个线程去将数据持久化到 CommitLog 日志文件中。
产品
实践
消息堆积是
消费堆积是什么?
消息堆积通常是由于 消息生产速度 > 消息消费速度 导致的,具体原因可能包括:
- 生产者流量剧增
- 消费者处理能力不足
- 消费者性能太差
- 消费者宕机
- 消费队列设不合理
- 消息分布不均匀
- 分区/队列数量不足:不能合理扩容消费端
- 其他原因
如何解决?
- 扩容消费者,解决消费者性能问题
- 优先处理重要消息
- 限流和降级
一些工具
微服务
层级 | 典型组件 | 功能 |
---|---|---|
客户端层 | 浏览器、App、第三方服务 | 发起请求 |
入口层(网关层) | Nginx、Spring Cloud Gateway | 路由转发、SSL终止、全局限流 |
服务治理层 | Sentinel、Hystrix、Istio | 流量控制、熔断降级、负载均衡 |
业务服务层 | 微服务(如订单服务、用户服务) | 处理具体业务逻辑 |
基础设施层 | 数据库、消息队列、缓存 | 数据持久化与通信 |
Kubernetes
Google 开发,基于Golang,可拓展。
容器编排,自动扩展,负载均衡,服务发现,多平台(本地,云,融合云)。
Nacos
Nacos 是一个功能强大的服务发现和配置管理平台,适用于微服务架构和云原生应用。
支持AP/CP双模式,默认AP模式。AP 是通过 Nacos 自研的 Distro
协议来保证的,CP 是通过 Nacos 的 JRaft
协议来保证的。
Zookeeper
- 分布式协调服务,支持服务发现和配置管理。
- 强一致性(CP 系统)意味着在选举期间是不可用的
- 提供分布式锁、选举等功能。
Eureka
高可用优先,容忍短暂数据不一致
Elastic Stack
倒排索引
索引类型 | 数据结构 | 典型应用场景 |
---|---|---|
正排索引 | 文档ID → 关键词列表 | 文档内容展示 |
倒排索引 | 关键词 → 文档ID列表(及位置) | 关键词搜索、全文检索 |
Nginx
Nginx 是一个功能强大且灵活的服务器软件,适用于多种场景,包括:
- 静态资源服务。HTTP/HTTPS服务器
- 反向代理和负载均衡。
- 缓存加速和安全防护。
- 微服务网关和 API 管理。
负载均衡算法有
- 轮询
- hash 可配置加权
- 最少连接
网关功能有
- 访问控制
- 限流、熔断
- 校验、授权
实现限流的方式
固定窗口
时间划分为固定的窗口(如 1 秒、1 分钟),在每个窗口内统计请求数量。如果请求数量超过阈值,则拒绝后续请求。
滑动窗口
将时间划分为更小的窗口(如 1 秒划分为 10 个 100 毫秒的窗口),统计最近一段时间内的请求数量。如果请求数量超过阈值,则拒绝后续请求。
令牌桶
系统以固定速率向桶中添加令牌。每个请求需要消耗一个令牌。如果桶中没有令牌,则拒绝请求。
漏桶
将请求看作水滴,漏桶以固定速率漏水(处理请求)。如果桶满了(请求超过容量),则丢弃新请求。
lua 脚本
Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。
- 动态请求处理和内容生成。
- 访问控制、限流和负载均衡。
- 缓存、日志记录和请求/响应修改。
- 身份验证、授权和 WebSocket 支持。
Lua 脚本的灵活性和高性能使其成为扩展 Nginx 功能的强大工具,特别适用于需要动态逻辑和高并发的场景。OpenResty 是一个集成了 Lua 的 Nginx 发行版,推荐使用它来简化 Lua 脚本的开发和部署。
一致性hash 算法
一致性哈希算法正好解决了简单哈希算法在分布式集群中存在的动态伸缩的问题。降低节点上下线的过程中带来的数据迁移成本,同时节点数量的变化与分片原则对于应用系统来说是无感的,使上层应用更专注于领域内逻辑的编写,使得整个系统架构能够动态伸缩,更加灵活方便。
一致性哈希算法是分布式系统中的重要算法,使用场景也非常广泛。主要是是负载均衡、缓存数据分区等场景。
Dubbo
Dubbo 是一款由阿里巴巴开源的高性能、轻量级的 Java RPC(远程过程调用)框架,主要用于分布式服务之间的通信。它提供了服务治理、负载均衡、服务注册与发现等功能,帮助开发者构建高性能的分布式系统。
- 服务注册与发现:
- 服务提供者将服务注册到注册中心(如 Zookeeper、Nacos)。
- 服务消费者从注册中心订阅服务,获取提供者的地址列表。
- 远程调用:
- 支持多种协议(如 Dubbo 协议、HTTP、RMI 等)进行远程调用。
- 负载均衡:
- 提供多种负载均衡策略(如随机、轮询、最少活跃调用等)。
- 容错机制:
- 支持集群容错策略(如失败重试、快速失败、失败安全等)。
- 服务治理:
- 提供动态配置、服务降级、流量控制等功能。
- 监控与管理:
- 支持服务调用统计、链路追踪、服务治理中心等功能。
Netty
Netty是一个非阻塞I/O客户端-服务器框架,主要用于开发Java网络应用程序,如协议服务器和客户端。异步事件驱动的网络应用程序框架和工具用于简化网络编程,例如TCP和UDP套接字服务器。Netty包括了反应器编程模式的实现。Netty最初由JBoss开发,现在由Netty项目社区开发和维护。