nosql 复习.md

CAP 与一致性模型 - CAP 与一致性模型 - CAP 定理 - 组成 - C(Consistency,一致性):所有客户端在同一时刻读到的数据一致 - A(Availability,可用性):每个请求都能在有限时间内得到非错误响应 - P(Partition tolerance,分区容错):系统在网络分区时仍然能提供服务 - 关键结论 - 发生网络分区时,无法同时满足 C 和 A - 分布式系统里 P 几乎是必选项(网络一定会分区/超时/抖动) - 一致性 - 线性一致性 - 定义:所有操作看起来像某个全局时间顺序瞬间发生,读一定能看到最近完成的读 - 工程含义:通常需要 leader+共识+或严格 quorum+顺序约束 - 代价:跨节点协调->延迟变高;分区时更可能不可用 - 顺序一致:比线性一致弱 - 只要存在一个全局顺序能解释所有的操作,但不要去符合真实时间,不要去最近完成的写立即可见 - 因果一致:社交消息系统常见 - 保证有因果关系的操作顺序一致;无因果关系的并发操作允许不同节点看到不同顺序 - 先发帖再评论,所有人必须看到帖再评论,但两个人同时发帖,顺序可能不一样 - 最终一致:AP 常见的一致性目标 - 定义:如果不再有新的更新,所有副本都最终收敛到一个值 - 允许:短时间读到旧值,不同客户端看到不同值 - 收敛:复制、反熵、读修复、后台合并、冲突解决策略 - Quorum(N/R/W) - 数据库用 quorum 思路来调一致性/可用性 - 定义 - N:副本数 - W:写成功需要确认的副本数 - R:读成功需要读取的副本数 - 关键结论 - 若 R+W>N,读写集合必然有交集->通常能读到最新写(接近强一致,但还要考虑版本选择/时钟/并发写) - 若 R+W<=N,可能读不到最新写(更偏最终一致/读旧值概率更高) - 举例 - W=2,R=2->R+W=4>3:读写交集至少一个副本,读更可能拿到最新 - W=1,R=1->2<=3:读很可能读到旧副本 - 分区时怎么选 C/A - 更一致:提高 W 或 R,分区更容易失败 - 更可用:降低 W 或 R,分区时能继续响应,可能读旧/冲突 - 读修复与 hinted handoff - Read Repair(读修复):读时发现副本版本不一致,把新值回写到旧副本,加速收敛 - Hinted Handoff:某副本暂时不可达,协调者先把代写的 hint 记录下来,等它恢复再补写 - 冲突从哪里来?怎么解决? - AP 系统分区时允许两边都写 -> 一定会出现冲突,关键是冲突语义 - LWW(Last Write Wins) - 用事件戳/版本号选最新覆盖旧值 - 风险:依赖时钟 - 优点:实现简单,很多常见足够 - 向量时钟/版本向量 - 判断两个更新是否并发(不可比较)还是有先后关系(可比较) - 并发写需要应用层合并 - CRDT - 通过数学结构保证并发合并可收敛 - 一致性 != 事务 ACID - CAP 的 C:指副本之间对外可见的读写一致语义 - ACID 里的 C:Consistency(约束一致性)是事务前后满足约束 - 事务强一致可能跨分区不可用;最终一致系统也可以在单分区内做局部事务 - 典型场景 - 必须强一致 - 余额扣款、库存扣减不超卖、唯一性约束、权限变更立即生效 - 宁可失败重试/排队,也不能错 - 可以最终一致 - 点赞数、播放量、推荐特征、日志埋点、监控指标 - 短时间误差可接受,最终对齐即可 - 折中:读写分离 + 关键路径强一致 - 下单扣库存走强一致存储:报表/BI 走 ClickHouse 最终一致导入 - 缓存允许短暂不一致,通过失效策略、异步刷新、补偿修正 分布式存储底层 - 分布式存储底层 - 分布式存储底层总览 - Client/SDK - 路由层:根据 Key 找到 Shard/Leader - 分片层:每个分片负责一段 key 空间 - 复制层:每个分片有 N 个副本 - 一致层/共识层:决定写要等谁确认,谁是 Leader,如何提交日志 - 存储引擎层:WAL + Memtable + SST/BTREE 等 - 后台维护:compaction、rebalance、修复、反熵、GC - 数据分片 - 为什么必须分片 - 目标:水平扩展,吞吐=节点数线性增长 - 分片策略对比 - Hash 分片 - 优点:数据分布均匀,天然抗热点 - 缺点:范围查询弱,扩容时大量搬迁 - 适用:KV,用户 ID 随机读写,计数器,缓存 - Range 分片 - 优点:范围查询强 - 缺点:容易热点,需要 split/merge - 适用:按事件排序的日志、时序、OLAP 分区表 - 一致性哈希 + 虚拟节点 - 目标:扩容/缩容时减少迁移量 - 一致性哈希结论:加/减一个节点只影响环上相邻的一段 key - 虚拟节点:每台机器对应多个 vnode,负载更均衡,迁移更细粒度 - 路由:请求如何找到分片 - Client-side routing(客户端算) - Client 拿到分片拓扑,自己算目标节点 - 优点:少一跳、性能好 - 缺点:拓扑变更要更新客户端(gossip/配置中心) - Proxy/Router(中间代理) - 由 Proxy 接请求,负责路由与重试 - 优点:客户端简单,拓扑变更透明 - 缺点:proxy 可能成为瓶颈/单点 - Coordinator (协调节点) - 写入/查询可能由协调者拆分到多个分片再聚合 - 常用于分析系统/分布式 SQL - Rebalance(扩缩容/再均衡) - 扩容 - 数据迁移:搬迁期间读写怎么保证正确性 - 双写/转发:迁移期间某些 key 可能在 old/new 两处 - 限流:迁移会抢光 IO/CPU,影响线上延迟 - 策略 - 迁移期间双写,再切流 - 迁移期间转发(写 old,old 转发到 new) - 分批搬迁+限速+热点优点处理 - 稳定窗口切换元数据 - 热点问题 - 热点是什么 - Hot key:单个 key QPS 极高 - Hot partition:单个分片承担大量 key 或大量访问 - 热点为什么致命 - 分布式扩展依赖均匀分布,热点让整体吞吐被最热分片锁死 - 延迟抖动:最热节点 CPU/网卡/锁竞争导致 p99 爆炸 - 治理手段 - Key 打散(加盐/分桶) - counter:global 拆分成 counter:global:{0..99},读时聚合,写时随机桶 - 代价:读放大/需要聚合 - 读写分离 + 多副本读 - 热读 key:允许从 follower/replica 读,或者使用 cache 层 - 本地缓存 + 请求合并 - 在网关/服务端合并通同 key 并发请求,防止缓存击穿引起雪崩 - 分区拆分 - range 分片:把热区间再拆细 - 配合动态 split - 异步化/预计算 - 把热点聚合改成写日志,异步聚合出结果 - 副本复制 - Leader-Follower(主从/Primary-Replica) - 写入路径:Client->Leader 写入->复制到 followers->达到确认策略后返回 - 关键问题 - 同步复制 vs 异步复制 - 同步:延迟高但丢数据风险低 - 异步:延迟低但主挂可能丢最后一段数据 - 读策略 - 读 leader:更一致 - 读 follower:延迟小/抗压,但可能读旧 - 适用:大多数传统分布式 DB/缓存系统 - Multi-Leader(多主) - 多个 Leader 都可写,各自复制 - 冲突不可避免,需要冲突解决(LWW/向量时钟/CRDT/业务合并) - 适用:跨地域多活、离线写入,复杂度高 - Leaderless(无主,Dynamo 风格) - 任意节点可接写,写到 N 个副本 - 用 Quorum(N/R/W)控制一致性 - 配套:读修复、hinted handoff、反熵 - 适用:高可用、可水平扩展、允许最终一致的系统 - 一致写入确认:Quorum - 定义 - N:副本数 - W:写成功需要确认的副本数 - R:读成功需要读取的副本数 - 关键结论 - 若 R+W>N,读写集合必然有交集->通常能读到最新写(接近强一致,但还要考虑版本选择/时钟/并发写) - 若 R+W<=N,可能读不到最新写(更偏最终一致/读旧值概率更高) - 举例 - W=2,R=2->R+W=4>3:读写交集至少一个副本,读更可能拿到最新 - W=1,R=1->2<=3:读很可能读到旧副本 - 共识与选主 - 共识 - 只有一个 Leader(避免脑裂乱写) - 日志顺序一致(写入顺序全局一致) - 多数派提交(保证故障后不回滚) - Raft 最小正确集 - Leader 选主 - 节点有 term(任期),超时发起选举 - 多数派投票选出 Leader - 心跳维持 leader 身份 - 日志复制 - 客户端写入先到 leader - leader 追加日志并复制给 follower - follower 按 index/term 对齐(不一致就回滚到匹配点再补) - 提交规则 - leader 只有在日志被多数派复制后才能提交 - 提交后对外可见 Cassandra/HBase - Cassandra/HBase - LSM-Tree + WAL - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/22/20260222101553862.png,604,382) - LSM-Tree 要解决什么问题 - 传统 OLTP 存储引擎常见是 B+Tree - B+Tree 更新/插入会改动树上的多个节点页 - 页可能在磁盘不同位置->随机写很多 - 随机在 HDD 上很慢,在 SSD 上也不便宜 - LSM 的核心目标 - 把随机写变成顺序写/批量写 - 把整理数据到成本挪到后台异步做(Compaction) - LSM 通过先写日志+写内存+顺序刷盘成不可变文件,再后台合并文件,大幅提高写吞吐,但是读取会更复杂,且 compaction 会带来放大和抖动 - 写入流程 - 先写 WAL(顺序追加) - 把这条写操作追加到 WAL 文件末尾 - 顺序写,非常快 - 目的:宕机可恢复 - 写入 Memtable(内存有序结构) - MemTable 一般用 SkipList/红黑树等保证有序 - 写入是内存操作,快 - MemTable 满了就 Freeze(变成 Immutable) - 当前 Memetable 变成不可变,新写进入新的 MemTable - immutable 的存在时为了让刷盘不阻塞写入 - 刷盘生成 SSTable(顺序写文件) - 后台线程把 immutbale MemTable 以排序后的顺序写成 SSTable,不可变有序文件 - SSTable 通常附带 - 稀疏索引 - Bloom Filter - 数据块 - 读流程 - 步骤 - 先查 MemTable - 再查 Immutable MemTable - 再查磁盘上的多个 SSTable - 找到 Key 后还要做版本合并 - 同一个 Key 可能在很多层都有 - SSTable 不可变,更新不是原地修改,而是写一个新版本 - 旧版本会在后台 compaction 时被清理 - 读放大:SSTable 越多,读需要查的地方越多 - Bloom Filter 的作用 - 对于某个 SSTable,Bloom Filter 判断:这个 key 不可能在里面->直接跳过,不读磁盘 - 如果 Bloom 说在,才去查索引/数据块 - Delete 怎么做 - LSM 不会立刻物理删除数据,而是写入一个 Tombstone(删除标记) - Delete(key) 本质是写入 key->tombstone - 读到 tombstone 说明该 key 已经被删除 - 真正清理旧值与 tombstone,依靠 Compaction 完成 - Compaction - 目标 - 合并多个 SSTable - 丢弃被覆盖的旧版本 - 清理 tombstone - 降低读放大、回收空间 - Compaction 为什么会导致 p99 抖动 - 需要大量读旧文件 + 写新文件 - CPU 做合并、校验、压缩 - 可能把 SSD/HDD 打满,影响在线请求 - compaction 策略 - Sized-Tiered Compaction - 当 L0/L1 有若干大小相近的 SSTable,就合并成更大的 - 优点:写放大相对小,吞吐高 - 缺点:读放大可能更大 - Leveled Compaction - 各层有大小上限,保证 L1+ 之间范围重叠更可控 - 优点:读放大更小 - 缺点:写放大更高 数据建模与查询建模 - 数据建模与查询建模 - NoSQL 建模的通用原则 - Query-driven modeling(按查询建模) - RDB 常按照范式建模,再用 SQL 去适配查询 - 在 NoSQL 里更像:先确定查询,再设计主键/分区/排序/冗余表 - Denormalization - 目的:消灭 join,降低在线查询复杂度 - 代价:写入多处更新、需要处理一致性补偿 - 同一份实体多份查询视图表 - 把维度字段复制进事实表 - 预计算/物化视图 - 把聚合从读时搬到写时 - 写路径:事件 -> 聚合状态更新 - 读路径:直接读聚合结果 - 适合:排行榜、计数器、报表、指标面板 - 幂等与版本化 - NoSQL + 分布式写入经常至少一次交付,天然要支持幂等 - 热点治理 - 避免分区键过于集中 - 避免自增 RowKey - 避免单 key 计数器

搜索引擎复习

信息检索核心原理 - 信息检索 IR 核心原理 - IR 的基本对象与术语 - 文档、语料、字段 - Document(文档):检索的基本单位 - Fields(字段):title、body、tags - Tokenization/Analysis (分词与分析链) - 典型分析链 - 字符过滤(去 HTML、统一全角半角、大小写) - Tokenizer(切词:英文按空格/规则,中文依赖分词器) - Token Filter - stopword(停用词) - stemming/lemmatization(词干/词形还原) - synonym(同义词) - ngram(前缀/模糊) - 倒排索引相关名词 - Term(词项):token 的规范化结果 - Posting list:某 term 出现在那些 doc 中的列表 - DocID:文档编号 - tf(term frequency):该词在 doc 内出现的次数 - df(document frequency):包含该词的 doc 数 - idf(inverse document frequency):词的区分能力 - 相关性:从 TF-IDF 到 BM25 - TF-IDF 的直觉 - 核心思想 - 一个词在文档里出现的越多(tf 越大)-> 越相关 - 一个词在整个语料越稀有(df 越小/idf 越大)-> 越能区分文档 - tf = count(t in d) - idf = log(N/df) - TF-IDF 容易出现两类不稳 - tf 线性增长导致刷词得分优势 - 文档长短差异导致长文天然占优势 - BM25:工程默认答案 - 核心思想 - tf 饱和:词出现 1->2 词提升很大,20->21 词提升小(防刷词) - 长度归一化:长文不会因为词多天然优势 - 得分计算 - score(d, q) = Σ[ idf(t) * ( tf*(k1+1) / ( tf + k1*(1 - b + b*|d|/avgdl ))) ] - k1:控制 tf 饱和速度 - b:控制长度归一化强度 - |d|/avgdl:文档长度与平均长度的比例 - 字段权重 - 现实检索几乎都是多字段:title/body/tags - 常见做法:对字段分别计算 BM25,再加权组合 - 查询模型 - 布尔检索 - AND/OR/NOT,精准过滤 - 工程上常用于 filter(不参与打分)而非主排序) - filter 可缓存,query 参与打分不易缓存 - 向量空间模型 - doc/query 看作高维向量,维度是 term,相似度用 cosine - sim = cos(q,d) - 概率检索 - BM25 术语概率检索思想的经典落地 索引结构与写入链路 - 索引结构与写入链路 - ES 的索引 - InvertedIndex(倒排):解决词->文档的快速检索 - Doc Values(列式):解决按字段排序/聚合/脚本读取的高效访问 - 倒排负责召回和打分;doc values 负责排序/聚合/分面 - 倒排索引的组成:字典 + posting - Term Dictionary - 存储所有 Term 的集合,能够快速定位某个 term 对应的 posting list - Lucence 用 FST 等结构做压缩与快速查找 - Posting List(倒排表) - docID 列表:包含该 term 的文档 - freq(tf):该 term 在 doc 中出现的次数 - positions(位置):term 在 doc 中的位置 - offsets(字符便宜):高亮需要(从哪里到哪里) - Doc Values - 是什么 - 按 docId 顺序存储某个字段的值(列式) - price、timestamp、category、keyword 字段 - 排序聚合为什么不用倒排 - 倒排是 term -> doc,适合检索;排序聚合需要 doc->field value - doc values 提供了高效的按照 doc 读取字段值,同时适合磁盘顺序访问 + OS page cache - Segment - Segment 是什么 - 一个 index 由多个 segment 组成 - 每个 segment 内部包含:倒排、doc values、stored fields、norms 等文件 - segment 一旦写出就不可变 - 不可变的好处 - 并发读及其简单:读线程不需要大锁,数据结构稳定 - 缓存友好:文件内容不会变,OS cache 命中更稳定 - 写入快:写在新 segment;避免原地更新导致随机写/锁竞争 - 崩溃恢复快

Java 并发复习

并发基础与 JMM 入门 - 并发基础与 JMM 入门 - 并发三大问题 - 原子性 - 定义:一个操作要么全部完成,要么完全不完成,中间过程对其他线程不可见 - 典型反例:i++ 不是原子操作 - 可见性 - 定义:一个线程对共享变量的修改,能否及时被其他线程看到 - 根因 - CPU 缓存(L1/L2/L3) 与编译器/CPU 重排序 - 线程可能一直读到自己缓存中的旧值 - 解决方法 - volatile: 保证写入对其他线程可见 - synchronized/Lock:解锁前的写对之后加锁的读可见 - final:初始化安全 - 有序性 - 定义:代码执行顺序是否一定和你写的一样 - 根因 - 编译器优化:指令重排(JIT) - CPU 乱序执行 - JMM(Java 内存模型) - JMM 解决的问题 - JMM 的目标不是描述硬件,而是给 Java 程序提供一个跨平台一致的并发可见性与有序性规范 - 规范: - 写的代码跑在不同 CPU 上,缓存协议,重排序规则不同 - JMM 通过 happens-before 规定:只要满足 HB,结果必须可见且有序;不满足 HB,结果运行不可预测 - HB = Java 并发正确性的法律条文 - happends-before (HB) - HB 是可见性 + 有序性的保证关系 - A happens-before B - A 的结果对 B 可见 - A 的执行顺序对 B 先行约束 - 没有 HB,就不保证可见/有序 - happends-before 规则 - 程序次序规则 - 同一个线程能,按程序顺序,前面的操作 HB 后面的操作 - 监视器锁规则 - 对同一把锁,unlock HB 之后对同一锁的 lock - volatile 变量规则 - 对同一 volatile 变量,对 volatile 的写 HB 后续对该 volatile 的读 - 线程启动规则 - Thread.start() HB 该线程内的所有操作 - 线程终止规则 - 线程内所有操作 HB 其他线程成功从 Thread.join() 返回 - 中断规则 - 对线程调用 interrupt() HB 被中断线程检测到中断 - 传递性 - 若 A HB B,B HB C,则 A HB C - volatile - volatile 保证什么 - 可见性:写入会刷新到主内存,读取会从主内存获取 - 有序性:禁止 volatile 写/读周围的特定重排序(内存屏障) - 建立 happens-before:写 HB 读 - volatile 不保证 - 不保证复合操作原子性 - 不保证所有代码不重拍:只约束与 volatile 相关的重排边界 - volatile 的经典使用场景 - 状态标志:停止线程 volatile boolean stop - 单例的安全发布 - 读多写少的配置刷新 - DLC(双重检查锁)为什么必须加 volatile - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/20/20260220165657231.png,292,241) - new Singleton() 在底层上被视为三步: - 分配内存 - 调用构造器初始化对象 - 把引用赋给 instance - 如果发生重排序,可能变为 - 分配内存 - 赋引用给 instance - 初始化对象 - 另一个线程看到 instance != null 就返回,但是拿到半初始化对象 - volatile 禁止这种关键重排并建立 HB,确保安全发布 - final 的初始化安全 - final 字段在构造完成后有更强的可见性保障 线程模型与线程安全 - 线程模型与线程安全 - Java 线程模型:Thread/Runnable/Callable/Future - 四者关系与使用场景 - Thread: 线程载体+执行单元 - Runnable:无返回值、不能抛受检异常 - Callable<V>: 有返回值,可抛异常(配合 Future) - Future<V>:异步结果句柄(拿结果、取消、超时) - Thread 是执行容器,Runnable/Callable 是任务;Callable 比 Runnable 多返回值与异常;Future 是任务的控制面板 - Future API - get():阻塞拿结果 - get(timeout, unit):超时等待 - cancel(true/false):尝试取消 - true:会向线程发 interrupt - false:不 interrupt,只是标记取消 - isDone()/isCancelled() - 线程生命周期与状态 - Java Thread.State - NEW:未 start - RUNNABLE:可运行(包括运行中+就绪) - BLOCKED:等待获取 monitor 锁(synchronized 竞争) - WAITING:无限期等待(Object.wait()/Thread.join()/LockSupport.park()) - TIME_WAITED:限时等待(sleep()/wait(timeout)/join(timeout)/parkNanos) - TERMINATED:结束 - BLOCKED 和 WAITING 的区别 - BLOCKED:卡在 synchronized,等锁 - WAITING:已经拿到锁,主动等待某个条件 - 线程切换成本 - 上下文切换:保存恢复寄存器、栈、调度 - CPU cache 命中下降 - 线程越多,竞争越激烈,吞吐反而下降 - 中断机制:interrupt 不是杀进程 - 三个 API 的语义 - thread.interrupt():给线程设置中断标志位 - thread.isInterrupted():读取中断标志位(不清除) - Thread.interrupted():读取当前线程的中断标志位,并且清除 - 那些操作对中断敏感 - 会抛 InterruptedException (清除中断标志) - Object.wait() - Thread.sleep() - Thread.join() - BlockingQueue.put/take 等可中断阻塞 - Lock 的可中断 - lockInterruptibly():阻塞等锁时可响应 interrupt - lock():等锁期间不相应 interrupt - 优雅停线程 - 循环里检查中断标志 - 捕获 InterruptedException 后要么退出,要么重新设置中断标志位再退出/上抛 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/20/20260220171246171.png,387,319) - 线程安全方法论 - 不变性 - 对象状态创建后不再改变 - 示例: String、Interger - 业务做法:配置对象、DTO 用不可变 + 构造完成后安全发布 - 优点:天然线程安全、无锁高性能 - 缺点:需要复制,内存开销上升 - 线程封闭 - 数据只在一个线程访问 - 典型:方法栈内局部变量、线程私有变量 - 示例:每个线程一个 SimpleDateFormat - 同步互斥 - 通过临界区序列化访问共享状态 - 无锁/低锁 - 原子类/CAS、分段化 - Synchronized - Synchronized 锁的是什么 - synchronized(obj):锁 obj 的 monitor - synchronized 实例方法:锁 this - static synchronized: 锁 Class 对象(xxx.class) - 锁的是对象的 monitor,不是代码块本身 - synchornized 三大语义 - 互斥:同一时刻只有一个线程进入临界区 - 可见性:释放锁前的写,对之后获取同一锁的线程可见 - 可重入:同一线程可重复获得同一把锁 - 锁升级 - 目标:在无竞争/低竞争时,让 synchronzied 尽可能便宜;在竞争激烈时保证正确性与吞吐。 - 策略 - 无竞争:尽量不做原子指令/不进内核 - 少量竞争:用 CAS + 自旋解决(避免线程阻塞/唤醒成本) - 激烈竞争:进入 monitor,阻塞/唤醒 - 对象头与 Mark Word:锁状态存在哪里 - 每个 Java 对象在 HotSpot 里都有对象头,核心是 Mark Word,用于存: - 哈希码 - GC 分代/标记信息 - 锁相关信息(锁标志位、线程 ID、指针等) - synchronized 不单独存一个锁对象结构,优先把锁状态编码在对象头里;必要时才膨胀到 monitor(重量级)。 - 四种主要锁形态 - 无锁 - 对象头处于可锁定但未加锁状态 - 第一次进入 synchronized,会走向偏向/轻量的获取逻辑 - 偏向锁 - 适用场景:几乎总是同一个线程反复进入同一把锁 - 核心思想:预期每次都 CAS/自旋,不如直接偏向某个线程 - MarkWord 记录偏向的线程 id - 同一线程再次进入,只需要快速检查是不是我,几乎零成本 - 偏向锁的获取: - 第一次进入:尝试把对象头偏向当前线程(轻量 CAS 一次) - 再次进入:只做线程 ID 校验 - 偏向锁的撤销 - 另一个线程也来竞争这把锁 - 偏向线程已经结束,且有其他线程来加锁 - JVM 需要批量撤销/重偏向 - 轻量级锁 - 适用场景:有短暂竞争,但竞争不激烈;临界区很短 - 核心机制 - 线程在栈上创建一个 Lock Record(锁记录) - 把对象头的 Mark Word 复制到 Lock Record(保存旧值) - 用 CAS 尝试把对象头替换为指向 Lock Record 的指针(同时设置轻量锁标志) - CAS 成功:拿到轻量锁 - CAS 失败:说明有竞争 -> 可能自旋等待 -> 若仍失败/竞争升高 -> 膨胀为重量级锁 - 重量级锁 - 使用场景:竞争激烈,自旋会浪费 CPU,或者线程阻塞更划算 - 核心机制 - 对象关联一个 ObjectMonitor - 竞争失败的线程进入 EntryList/WaitSet(不同状态的队列) - 由 OS 互斥/park-unpark 等机制进行阻塞与唤醒 - 特点 - 成本高:涉及线程挂起/唤醒、上下文切换 - 高竞争下更稳定:避免大量线程空转自旋把 CPU 烧穿 - wait/notify:线程协作的基础 - wait/notify 使用规则 - 必须在 synchronized 内调用,否则抛 IllegalMonitorStateException - wait() 会 - 释放当前 monitor - 把线程放入该 monitor 的等待队列 - 被唤醒后需要重新竞争锁 - 永远用 while 检查条件,不用 if - 防止虚假唤醒 - 防止被唤醒时条件已被其他线程改变 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/20/20260220192817406.png,154,106) - notify vs notifyAll - notify(): 随机唤醒一个等待线程 - notifyAll(): 唤醒全部等待线程,让它们竞争并再次检查条件 - 多数条件 prefer notifyAll,除非非常确定只存在一种等待条件且不会误唤醒 - wait/notify vs sleep - sleep():不释放锁,属于 Thread 方法,主要用于延时 - wait():释放锁,属于 Object 方法;用于条件等待 - Condition 与 LockSupport:更现代的协作手段 - Condition 相对 wait/notify 的优势 - 可拥有多个条件队列 - API 更清晰 await/signal/singalAll - 与 ReentrantLock 组合可实现更复杂的并发控制 - LockSupport - park() 阻塞当前线程 - unpark(thread):给某个线程发许可,让它从 park 返回 - unpark() 可以先于 park 发生,比 wait/notify 更灵活 - 可中断的生产者-消费者 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/20/20260220194750850.png,375,460) - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/20/20260220194816875.png,451,697) - Condition 版本 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/20/20260220195331750.png,365,599) JUC 核心与 AQS - JUC 核心与 AQS - CAS 与原子类 - CAS 是什么 - CAS 本质是一个 CPU 原子指令 - 比较内存位置的值是否等于 expected - 是则写入 update - 返回成功/失败 - CAS 解决了什么 - 解决复合更新在并发下的原子性 - 避免锁的阻塞/唤醒,吞吐通常更高 - CAS 的代价 - 自旋重试:竞争大时空转,CPU 飙高 - 可能导致某些线程长期失败 - AtomicInteger/AtomicLong 的工作方式 - AtomicInteger.incrementAndGet(),本质是 - 循环:读旧值 -> 计算新值 -> CAS 尝试更新 -> 失败重试 - 原子类=CAS 自旋 + volatile 语义,失败重试直到成功 - ABA 问题 - 问题描述 - 线程 T1 看到值为 A,准备 CAS 成 B;期间 T2 把 A 改成 C 又改为 A。 - T1 CAS 会成功,它不知道中间发生过变化 - ABA 什么时候有害 - 关心的是值有没与变过,而不是当前值是不是 A - 解决方案 - 版本号:AtomicStampedReference(值+stamp) - 也有 AtomicMarkableReference(值+boolean 标记) - LongAddr 为什么比 AtomicLong 快 - AtomicLong:所有线程竞争同一个 value,CAS 冲突高 - LongAdder:把热点拆分成多个 Cell,线程尽量更新自己的 Cell,最后 sum 时聚合 - AQS:JUC 的核心骨架 - AQS 最重要的三件东西 - state (volatile int):同步状态 - CLH 变体 FIFO 队列:等待线程排队 - acquire/release 模板方法:统一的获取/释放流程 - AQS 用 state 表示资源,用队列管理竞争失败的线程,用 acquire/release 模板屏蔽排队、阻塞与唤醒细节,同步器只要实现 tryAquire/tryRelease - AQS 的两种模式 - 独占:一次只允许一个线程获取 - ReentrantLock、ReentrantReadWriteLock 的写锁 - 共享:运行多个线程同时获取 - Semaphore、CountDownLatch、读锁 - acquire/release - acquire - tryAcquire() 尝试直接获取资源 - 失败:把当前线程包装成 Node 入队 - 自旋检查是否轮到自己(前驱是 head 且再 tryAquire) - 若仍然失败,park 阻塞 - 被唤醒后继续循环 - 要点 - 不是一直自旋:会 park - 唤醒不是广播,通常唤醒一个合适的后继节点,避免惊群 - release - tryRelease() 修改 state(释放资源) - 如果释放后资源可用,唤醒队列中的后继节点 - 为什么 AQS 用队列 - 避免一堆线程同时自旋抢锁造成 CPU 爆炸 - FIFO 队列提供基本公平性基础 - park/unpark 做阻塞唤醒,减少上下文切换次数(相对 monitor 更加可控) - ReentrantLock - ReentrantLock 相比 synchronized 的能力点 - 可中断:lockInterruptibly() - 可超时:tryLock(timeout) - 可选择公平/非公平 - 多 Condition(多个等待队列) - 缺点:必须手动释放锁,代码更啰嗦 - 公平 vs 非公平 - 公平锁:大致按照队列先来先服务,吞吐更低 - 非公平锁:允许插队,先 CAS 抢一把,吞吐更高,可能造成某些线程等待更久 - 可中断锁为什么重要 - 线程在等锁时被取消/超时,希望立即退出而不是继续卡死 - lock():等待锁期间不响应 interrupt - lockInterruptibly():能响应 interrupt,抛 interruptedException - Condition:两个队列(同步队列+条件队列) - Condition 的语义 - await():释放锁+当前线程进入条件队列+park - signal():把条件队列头部线程转移到同步队列 - 线程真正执行 - 被 signal 转移到同步队列 - 重新竞争锁成功 (acquire) - await 返回继续跑 - 常见同步工具类 - CountDownLatch(一次性门闩) - state = count - await():共享获取,直到 count == 0 全部放行 - countDown():释放,count--,到 0 唤醒所有等待者 - 适用场景 - 主线程等待多个子任务完成 - 服务启动前等待依赖就绪 - count 到 0 后不能重置 - Semaphore(信号量:限流/资源池) - state = permits - acquire():permits--,不够则排队阻塞 - release():permits++,唤醒等待者 - 适用场景 - 限制并发数 - 连接池/令牌桶 - CyclicBarrier(客服用栅栏,非 AQS 典型) - 让一组线程在栅栏处互相等待,达到数量后一起继续 - 可复用 - 用于并行计算分段后汇总 线程池与并发工程实战 - 线程池与并发工程实战 - 不用线程池的问题 - 线程创建/销毁成本高(栈、TLAB、调度) - 不可控:并发暴涨会把机器压垮 - 缺乏治理能力:无法统一命名,监控、拒绝、限流、隔离 - ThreadPoolExecutor - 七大参数 - corePoolSize(核心线程数) - 默认不会受 - 任务来时优先创建核心线程直到达到 core - maximumPoolSize(最大线程数) - 队列慢后,才会创建非核心线程,直到 max - 超过 max 再来任务 -> 触发拒绝策略 - keepAliveTime(空闲存活时间) - 非核心线程空闲超时会回收 - 若允许核心线程超时,则核心线程也会回收 - workQueue (任务队列) - 决定任务是排队还是扩线程的关键 - threadFactory(线程工厂) - 线程命名,daemon、优先级、UncaughtExceptionHandler - 工程强制:必须设置可读名称 - handler(拒绝策略) - 线程数到 max 且队列满->触发拒绝 - unit(时间单位) - 线程池执行流程 - 若 workerCount < corePoolSize - 创建核心线程执行 task - 否则尝试入队 workQueue.offer(task) - 入队成功:等待线程从队列取任务 - 入队失败,且 workerCount < maximumPoolSize - 创建非核心线程执行 task - 否则,拒绝策略 handler.rejectExecution(task, executor) - workQueue 选型 - LinkedBlockingQueue(无界/大容量队列) - 特点 - 极容易入队成功,导致线程数通常停在 core,不会扩到 max - 如果队列无界:任务堆积 -> 内存膨胀/延迟飙升,最终 OOM 或雪崩 - 适用:任务流量可控,任务执行稳定,且能接受排队延迟 - ArrayBlockingQueue(有界队列) - 特点 - 队列有界,能把系统压力显式化 - 延迟更可控,便于做背压与降级 - 线上服务强烈推荐:有界队列+明确拒绝策略 - SynchronousQueue(不存任务,直接提交) - 特点 - 没容量,offer 必须被 worker 立即 take 才算成功 - 适合快速扩线程,直到 max,然后拒绝 - newCachedThreadPool 就是它,风险是线程数可能飙升 - 拒绝策略 - 四种内置策略 - AbortPolicy(默认):抛 RejectedExecutionException - 适合你希望调用方显式失败,触发降级/重试逻辑 - CallerRunsPolicy:调用方自己执行任务 - 适合给上游施加背压(请求线程被拖慢,QPS 自然下降) - DiscardPolicy:静默丢弃 - 适合:允许丢数据的场景(采样、非关键日志) - DiscardOldestPolicy:丢弃队列最老任务,再尝试入队 - 适合:追求新任务更重要的场景 - 工程化拒绝策略(建议自定义 handler) - 记录指标(reject count、queue size、active count) - 打点日志 - 触发降级 - 投递到补偿通道 - Executors 工厂的问题 - newFixedThreadPool - 默认用无界 LinkedBlockingQueue:堆积导致 OOM/延迟失控 - newCachedThreadPool - 默认 SynchronousQueue + max=IntegerMAX_VALUE:线程数可以无限膨胀 - newSingleThreadExecutor - 无界队列,同样堆积危险 - 生产环境建议显式 new ThreadPoolExecutor:有界队列、合理 max、明确拒绝策略、线程命名与监控 - 线程数怎么配置 - CPU 密集型 - 线程数 = CPU 核心数 - 目标:减少上下文切换 - IO 密集型 - 线程数 = 核心数 * (1+等待时间/计算时间) - 工程上推荐压测(不同线程数)-> 看吞吐/延迟/CPU/上下文切换->找拐点 - CompletableFuture - 解决的问题 - 传统 Future 的痛点 - 只能 get() 阻塞拿结果,组合多个异步任务很痛苦 - 异常处理分散,超时与取消不优雅 - 任务依赖关系需要手写线程/回调 - CompletableFuture 的定位 - 提供声明式的异步流水线 - 支持任务依赖、并行、聚合、异常处理、完成回调 - 核心心智模型 - Stage(阶段) - 每个 thenXX 都是在构建一个新阶段 - 上一个阶段完成后触发下一个阶段 - 每个阶段都可能 - 产出一个值 - 抛异常 - 被取消 - Completion(完成态) - Completable 可能以三种方式结束 - 正常完成(有结果) - 异常完成(exceptionally completed) - 被取消(cancelled) - Executor - 必须管理在哪个线程执行阶段 - 线程池与执行线程 - 默认线程池 - supplyAsync/runAsync:不传 executor 时,默认走 ForkJoinPool.commonPool - commonPool 是全局共享池:你的业务、第三方库、框架都可能在用 - 工程风险 - 隔离性差 - 阻塞任务不友好 - 排障难 - thenApply vs thenApplyAsync - thenApply(fn):默认在完成上游阶段的线程里执行 - thenApplyAsync(fn):异步执行 - 不传 executor:用 commonPool - 传 executor:用指定的线程池 - 常用 API 分组 - 创建与启动 - completableFuture(value):已完成(同步值包装) - runAsync(Runnable, executor?):无返回值任务 - supplyAsync(Supplier<T>, executor?):有返回值任务 - new CompletableFuture<>() + complete/completeExceptionally:手动控制完成 - 转换 map 与消费 consume - thenApply(fn): T->U - thenAccept(consume): 消费结果(无返回) - thenRun(runnable):不关心结果,只做后续动作 - 串联 - thenCompose(fn): T->CompletableFuture<U>,扁平化 - 用于上一个异步结果决定下一个异步请求 - 并行组合 - thenCombine(other, (a,b)->c):两个都完成后合并 - allOf(f1,f2,...):全部完成 - anyOf(f1,f2,...):任意一个完成 - applyToEither(other, fn):谁先完成用谁的结果 - acceptEither:谁先完成就消费 - 异常处理 - exceptionally(ex -> fallback):异常时给兜底值 - handle((res, ex) -> ...):无论成功失败都处理 - whenComplete((res, ex) -> ...):做副作用,不改变结果 - 最佳实践 - 主链用 handler 或 exceptionally 做兜底 - whenComplete 做日志与 metrics 并发容器、阻塞队列、并发设计 - 并发容器、阻塞队列、并发设计 - 并发容器解决的问题 - 线程安全访问:多线程读写不会破坏结构、不丢数据 - 性能:比 Collections.synchronizedXxx/Hashtable 更高吞吐 - 语义更强:有些容器提供阻塞、延时、优先级等能力 - 选型 - Map -> ConcurrentHashMap - List -> 读多血少用 CopyOnWriteArrayList - Queue -> 无界非阻塞 ConcurrentLinkedQueue,有界阻塞用 ArrayBlockingQueue/LinkedBlockingQueue - Set -> ConcurrentHashMap.newKeySet()/CopyOnWriteArraySet - Deque -> ConcurrentLinkedDeque/LinkedBlockingDeque - ConcurrentHashMap - CHM 为什么比 Hashtable 快 - Hashtable:方法级 sychornized,所有操作串行 - CHM:更细粒度控制 - 大部分读操作无锁 - 写操作只锁定桶/节点,并结合 CAS - CHM 的核心结构 - 底层是 Node<K,V>[] table(桶数组) - 桶内结构:链表(node)或红黑树 - 当链表过长且 table 足够大时,会树化降级查找复杂度 - put 的并发控制逻辑 - table 未初始化:先初始化 - 根据 hash 找桶下标 i - 如果桶为空:尝试 CAS 放入新 Node - 若桶非空 - 若桶是迁移中:帮助扩容 - 否则对桶头做 synchronzied 局部锁,在桶内插入/更新 - 插入后可能触发 size 技术更新与扩容检查 - CopyOnWriteArrayList(COW):读多写少 - 核心机制 - 写操作会 - 加锁 - 复制底层数组 - 在新数组上修改 - 用引用替换发布新数组 - 读操作直接读数组快照,无损 - 适用与不适用 - 适用 - 读远多于写 - 不适用 - 写频繁 - 数组很大 - 并发队列:非阻塞 vs 阻塞 - ConcurrentLinkedQueue(CLQ):非阻塞队列 - 基于 CAS 的链表队列 - 适合:高并发无界、低延迟、允许忙等/自旋语义的场景 - 特点 - offer/poll 非阻塞 - size() 可能是线性复杂度 - BlockingQueue:核心语义 - BlockingQueue 的关键是两种阻塞语义 - 当队列为空:take() 阻塞直到有元素 - 当队列满:put() 阻塞直到有空间 - offer/poll 返回特殊值 - 三大常用阻塞队列 - ArrayBlockingQueue(ABQ)-有界数组队列 - 底层数组环形缓冲区 - 一般用一把锁 + 两个 condition - 特点 - 有界:最重要的工程优势 - 内存局部性好 - 吞吐稳定,适合线程池工作队列 - 生成服务最常用的稳健默认 - LinkedBlockingQueue(LBQ)-链表队列(可无界/大容量) - 链表节点存储,内存分配频繁(节点对象) - 常见实现会用两把锁来提高并行度 - 风险:堆积导致 OOM 与延迟失控 - SynchronousQueue(SQ)-不存储元素的移交队列 - 容量为 0,put 必须等待 take 直接接手 - 特点:几乎不用排队,延迟低 - DelayQueue/PriorityBlockingQueue:定时与优先级 - DelayQueue 延时队列 - 元素实现 Delayed,按到期时间排序 - take() 会阻塞直到最近到期元素到期 - 适用:超时重试、订单超时关闭、缓存过期处理(小规模) - 工程上:大量定时任务用时间论/调度框架 - PriorityBlockingQueue(优先级队列) - 无界优先级堆 - 高优先级先出队

Sping 复习

Spring Core - Spring Core(IoC/DI/Bean 生命周期) - 概述 - Spring 的核心是 IoC 容器,它负责把对象 Bean 的创建、依赖装配、生命周期管理、扩展点回调统一托管 - 写的业务对象只关心需要什么,不关心怎么 new、怎么组装、什么时候初始化/销毁 - SpringIOC = 依赖反转+容器管理对象生命周期 - DI = 容器把依赖注入进来(构造器/Setter/字段) - Spring 强大来自于:大量可插拔的扩展点(PostProcessor、Aware、Event...) - 核心概念与组件 - Bean、BeanDefinition、Container - Bean:被 Spring 容器管理的对象实例 - BeanDefinition:Bean 的配方/说明书,包括 - beanClass 类 - scope (singleton/prototype/...) - dependsOn、lazyInit、autowireCandidate... - initMethods/destroyMethod - 容器(Container)=读取 BeanDefinition->创建 Bean->注入依赖->生命周期回调 - BeanFactory vs ApplicationContext - BeanFactory: 最底层的 IoC 容器接口,延迟实例化为主(按需 getBean 才撞见) - ApplicationContext:更企业级,在 BeanFactory 上提供 - 资源加载 - 国际化 - 事件发布(ApplicationEventPublisher) - AOP/事务等更完整的生态整合 - 通常启动时预实例化单例(非 lazy 的 singletone) - 装配方式与注入策略 - 常见装配来源 - @Component + @ComponentScan:扫描注册 - @Configuration + @Bean:显式配置注册(配置复杂时使用) - @Import:导入配置/Selector/Registrar(Boot 自动配置常用) - XML(旧系统常用) - 注入方式对比 - 构造器注入(推荐) - 依赖不可变,对象更健壮 - 便于测试 - 早失败:缺依赖无法启动 - 循环依赖无法解决,早暴露设计问题 - Setter 注入 - 优点:可选依赖,可后置设置 - 缺点:对象可能处于半初始化状态 - 字段注入 - 难测试 - 破坏封装 - 易隐藏循环依赖与设计问题 - Bean 生命周期全链路 - 生命周期流程 - 解析配置 -> 注册 BeanDefinition - 实例化:反射/构造器创建对象 - 属性填充:进行依赖注入 - Aware 回调 - BeanNameAware/BeanFactoryAware/ApplicationContextAware 等 - BeanPostProcessor#postProcessBeforeInitialization - 初始化 - @PostConstructor - InitializingBean#afterPropertiesSet - 自定义 initMethod - BeanPostProcessor#postProcessAfterInitialization - AOP 代理通常在这生成(返回代理对象替代原对象) - 容器运行期使用 - 容器关闭,销毁回调 - @PreDestroy - DisposableBean#destroy - 自定义 destroyMethod - 超级扩展点 - BeanFactoryPostProcessor(更早、更底层) - 作用对象:BeanDefinition - 执行时机:Bean 实例化之前 - 典型用途: - 修改 BeanDefinition - 解析配置占位符 - BeanPostProcessor(最常用,最关键) - 作用对象:Bean 实例 - 执行时机:初始化前后(before/after init) - 典型用途 - AOP 代理创建 - 注解处理,例如 @Autowired 的处理 - 作用域与懒加载 - singleton vs prototype - singleton:容器内单例,默认 - prototype:每次 getBean 创建一个新对象,容器只负责创建与注入,不负责销毁 - @Lazy - 对 singleton:延迟到第一次使用才创建 - 大量 Bean 时可以缩短启动,但是首次请求会变慢 - 依赖解析与注入规则 - @Autowired 的匹配策略 - 先按类型找候选 Bean - 如果有多个 - @Primary 优先 - @Qualifier("name") 指定 - 再不行:按照字段名/参数名尝试匹配 - 找不到:默认报错,可以用 required=false 或 Optional<T> 表示可选 - @Resource vs @Autowired - @Resource:默认按 name,再按照 type - @Autowired(spring):默认按 type,可以配合 @Qualifier - 循环依赖 - 什么是循环依赖 - A 依赖 B,B 依赖 A,常在字段/Setter 注入中 - Spring 为什么能解决部分循环依赖 - 依赖注入发生在实例化之后 - 容器允许暴露一个早期引用给对方注入,从而打破死循环 - 三级缓存 - 一级缓存:完整初始化好的单例 Bean - 二级缓存:早期单例 Bean(半成本引用,可能是原对象,也可能是代理的早期引用) - 三级缓存:ObjectFactory(工厂),用于需要时才创建早期引用,并且能让 AOP 有机会在早期阶段接入 - 典型场景 - A 依赖 B,B 依赖 A,流程用容器创建 A 为起点 - 触发创建 A - 调用 getBean("A") - 以及缓存没有 A,进入 createBean("A") - 实例化 A(还没有注入依赖) - Spring 先 instantiate (反射/构造器)得到 A 的空壳对象 A_raw - 提前暴露 - 在进入属性注入之前,Spring 会做循环依赖防护的准备 - 向三级缓存放入 singletonFactories["A"] = ObjectFactory - 这个 ObjectFactory 的职责是:别人需要 A 的早期引用时,能返回 A_raw 或 A_early_Proxy - 此时,A 还没被放入二级缓存,只是放了一个可生产早期引用的工厂到三级缓存 - 给 A 做属性填充 - A 需要注入 B -> 容器去 getBean("B") - 创建 B - getBean("B") 缓存都没有 -> createBean("B") - 实例化得到 B_raw - 为 B 做提前暴露准备 - 开始 B 注入依赖 - B 注入 A(触发早期引用获取) - B 需要 A -> 容器调用 getBean("A") - A 仍然在创建中 - 一级缓存 singletonObjects:没有 - 但容器发现 A "currently in creation" 会拿走 getSingleton("A", allowEarlyReference=true) 的分支,尝试拿 earlyreference(给 AOP 留入口) - 拿到 earlyRef 后,放入二级缓存,移除三级缓存,返回 earlyRef 给 B 注入 - A 完成初始化,进入一级缓存 - A 继续完成初始化回调链/BPP afterInitialization - 为什么一定要三级 - 如果只有二级,无法优雅处理什么时候生成代理/早期暴露代理问题 - 三级缓存工厂,让 Spring 能够在真正需要注入时再决定暴露原对象还是代理对象 - 哪些循环依赖 Spring 解决不了 - 构造器注入的循环依赖:实例化阶段就互相需要对方,无法先构造空壳 - prototype 的循环依赖:每次创建都新对象,不走单例缓存那套 - @Configuration 为什么特殊(CGLIB 增强) - 同一个 @Bean 方法调用两次 - 普通类写 @Bean 方法,直接调用方法会 new 两次 - 在 @Configuration 类中,Spring 会用 CGLIB 增强它 - @Bean 方法被拦截 - 如果容器里已有该 Bean,就返回容器里的单例,而不是再次执行方法创建 Spring AOP - Spring AOP - AOP 在 Spring 里解决什么问题 - 目标:把横切关注点从业务逻辑中剥离出来,日志,鉴权,事务,监控,限流,缓存,重试等 - Spring AOP 的定位:基于运行时代理,拦截方法调用,织入增强逻辑 - Spring AOP 只能对方法执行做增强 - 核心术语与对象模型 - Join Point/Pointcut/Advice/Aspect - JoinPoint:可被拦截的位置。Spring AOP 里基本就是方法执行点。 - Pointcut:切点表达式,决定哪些 JoinPoint 被拦截。常见:execution/@annotation/within/this/target 等 - Advice:增强逻辑,要做什么 - Aspect:切面=Pointcut+Advice 的集合 - Advisor - Advisor:把 Pointcut 和 Advice 打包成一个可应用单元 - Spring 内部处理的是 Advisor 列表,而不是直接处理注解切面 - 事务就是一个 Advisor - @Aspect 最终会被解析成多个 Advisor,由容器在创建 Bean 时用这些 Advisor 决定是否生成代理以及代理链顺序 - 代理机制:JDK vs CGLIB - JDK 动态代理 - 只能代理接口 - 通过 InvocationHandler 拦截接口方法调用 - 优点:标准 JDK 能力、生成快、类结构简单 - 缺点:无接口时不可用,对直接调用实现类方法这类场景不覆盖 - CGLIB 代理 - 通过生成目标类的子类并覆写方法拦截 - 适用于无接口的类 - final 类无法被继承 -> 不能代理 - final 方法、构造器逻辑、私有方法不能被增强 - Spring 如何选择代理方式 - 有接口优先 JDK,没接口用 CGLIB - 强制开启 CGLIB @EnableAspectJAutoProxy(proxyTargetClass= true)/spring.aop.proxy-target-class=true - Spring AOP 的工作流程 - Auto Proxy Creator - AnnotationAwareAspectJAutoProxyCreator - 本质是一个 BeanPostProcessor - 介入时机:在 PostProcessAfterInitialization 决定是否为 Bean 创建代理 - 简化流程 - 容器启动:扫描到 @Aspect,解析成多个 Advisor - 创建普通 BeanX 时 - AutoProxyCreator 收集可应用到 X 的 Advisor - 如果存在匹配 - 创建代理对象 Proxy(X) - 将 Advisor 转成 MethodInterceptor 链 - 返回 Proxy 替换原对象放入单例池 - 调用 X.method() 时 - 进入代理 -> 执行 interceptor chain -> 最后调用目标方法 - 切点表达式 - execution - execution(访问修饰符 返回类型 包名.类名.方法名(参数)) - execution(* com.demo.service..*(..)):service 包以及子包所有方法 - execution(* *..*Service.*(..)):所有以 Service 结尾类的方法 - execution(public * *(..)):所有 public 方法 - 注解切点(工程最常见) - @annotation(com.demo.Loggable):方法上标注注解才拦截 - @within(...):类上注解 - @target(...):目标对象类型带注解(与代理类型可能有差异 - within/this/target - within(TypePattern):按声明类型匹配 - this(Type):按代理对象类型匹配 - target(Type):按目标对象类型匹配 - Advice 类型与执行顺序 - 五种 Advice - @Before:目标方法前 - @After:finally(无论是否异常) - @AfterReturning:正常返回后 - @AfterThrowing:抛异常后 - @Around:最强,包裹整个调用 - 一个调用的典型顺序 - Around(进入) - Before - method 执行 - AfterReturning/AfterThrowing (二选一) - After - Around 退出 - 多个切面怎么排序 - @Order 数字越小优先级越高(越外层,越先进入 Around) - Ordered 接口同理 - 没有 order:可能按注册顺序 - 多个 Around 像洋葱圈,order 小的在最外层 - 代理链/拦截器链 - Spring 会把 Advisor 适配成一组 MethodInterceptor,形成链 - MethodInvocation.proceed() 是关键递归点 - 每个 interceptor 做一段逻辑,然后调用 process() 交给下一个 - 最后一个调用 invokeJoinPoint() 执行目标方法 - AOP 经典失效点 - 自调用失效(No proxy,no AOP) - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/21/20260221112533671.png,183,88) - 调用 m1() 内部 m2() 是 this.m2(),不会经过代理 -> @Transactional/@Around 不生效 - 解决策略 - 拆分 Bean:把 m2 放到另一个 Spring Bean,通过注入调用 - 通过 AopContext.currentProxy():获取当前代理再调用,需要 exposeProxy = true,侵入性强 - 注入自身代理 - 非 public 方法 - 直接 new 出来的对象没有 AOP Spring Transaction - Spring Transaction - 定义:Spring 事务本质是 AOP 拦截方法调用,在方法执行前后通过 TransactionManager 控制开启/加入事务、提交/回滚、资源绑定/解绑 - 事务体系的关键对象 - PlatformTransactionManager:事务管理器抽象 - DataSourceTransactionManager - JpaTransactionManager - TransactionDefinition:事务定义(传播、隔离、超时、只读) - TransactionStatus:事务状态(是否新建、是否完成、是否回滚标记等) - TransactionSynchronzationManager:线程级资源管理 - Spring 事务是线程绑定资源,所以跨线程(异步)默认不会继承事务上下文 - @Transactional 做了什么 - 发生在 AOP 的哪个位置 - @Transactional 会被解析为一个 Advisor - 方法调用进入代理后由 TransactionInterceptor 拦截 - 会读取事务属性,决定 - 加入已有事务还是新开事务 - 方法结束时 commit 还是 rollback - 时序 - 进入代理方法 - TransactionInterceptor 读取事务属性 - getTransaction():根据传播行为决定新开/挂起/加入 - 执行目标方法 - 正常返回 -> commit() - 抛异常 -> 根据回滚规则 rollback() 或者 commit() - 传播行为 - 方法 A 调用方法 B 时,事务边界怎么处理 - REQUIRED 默认 - 规则:有事务就加入,没有就新建 - 典型:大多数业务方法 - REQUIRES_NEW - 规则:总是新建事务,若外部已有事务,则挂起外部事务 - 典型:记录操作日志/发送消息/保存审计记录 - NESTED - 规则:在外层事务存在时,使用 Savepoint 实现部分回滚 - 外层回滚:内层也回滚 - 内层回滚:回到保存点,外层可继续 - SUPPORTS:有事务就加入,没有就非事务执行 - NOT_SUPPORTED:总是非事务执行;若有事务则挂起 - NEVER:如果存在事务则抛异常 - MANDATORY:必须在事务中运行,否则抛异常 - 隔离级别 - Read Uncommitted:可能脏读 - Read Committed:防脏读 - Repeatable Read:防不可重复读 - Serializable:最强,吞吐最差 - 回滚规则 - 默认规则 - 默认只对 RuntimeException 和 Error 回滚 - 受检异常默认不回滚 - 配置回滚规则 - @Transactional(rollbackFor=Exception.class):受检异常也回滚 - noRollbackFor = XxxException.class:指定异常不回滚 - 事务提交/回滚的关键机制:线程绑定资源 - TransactionSynchronizedManager (TSM) - 开启事务时 - 从 DataSource 取 Connection - Connection.setAutoCommit(false) - 把 Connection 绑定到当前线程(TSM) - 同一线程后续 DAO 操作会复用这个 Connection - 提交/回滚后 - commit/rollback - 解绑资源,释放连接,恢复 autoCommit 等 Spring MVC - SpringMVC(请求链路、参数绑定、异常处理) - 总控思维模型 - Spring MVC 是一个基于 DispatcherServlet 的前端控制器框架,它把一次 HTTP 请求拆分 - 映射(找谁处理)-> 适配(怎么调用)-> 参数解析(怎么组装入参)-> 执行(调用 controller)-> 返回值处理(怎么写响应)-> 异常处理(失败怎么兜底) - 核心入口是 DispatcherServlet#doDispatch:通过 HandlerMapping 找到 handler,再通过 HandlerAdapter 执行;执行前后分别由 ArgumentResolver 和 ReturnValueHandler 处理出参入参,异常交给 HandlerExceptionResolver 链 - 核心组件和职责 - DispatcherServlet(总入口) - 接受请求 - 找 Handler(Controller 方法) - 调 HandlerAdapter 执行 - 走视图解析/写回响应 - 捕获异常并交给异常解析器链 - HandlerMapping(找谁处理) - RequestMappingHandlerMapping:处理 @RequestMapping/@GetMapping,它会维护映射表:URL+HTTP Method+consumers/produces+headers+params->HandlerMethod - HandlerAdapter(怎么调用 Handler) - RequestMappingHandlerAdapter:负责调用 @RequestMapping 方法 - 使用 HandlerMethodArgumentResolver 解析方法入参 - 使用 HandlerMethodReturnValueHandler 处理返回值 - ViewResolver(视图解析,前后端分离可选) - 返回 String 视图名才需要(JSP/Thymeleaf) - REST 场景通常直接 @ResponseBody/@RestController,通过 HttpMessageConverter 写响应 - HttpMessageConverter(对象<->报文) - @RequestBody:把 JSON/XML/表单正文 -> Java 对象 - @ResponseBody:把 Java 对象 -> JSON/XML - 常见 Jackson JSON converter - 一次请求到完整链路 - 按 DispatcherServlet#doDispatch 的逻辑 - 接受请求(Servlet 容器调用 DiapatcherServlet#service) - 拿 Handler:遍历 HandlerMapping,找到匹配的 Handler,通常是 HandlerMethod - 拿 HandlerAdapter:选择能执行该 Handler 的适配器,通常是 RequestMappingHandlerAdapter - 执行前置 - 绑定/转换:WebDataBinder(类型转换、校验) - 参数解析:ArgumentResolver - 拦截器 preHandler(HandlerInterceptor) - 调用 Controller 方法 - 处理返回值 - REST:ReturnValueHandler -> HttpMessageConverter 写回 JSON - 视图:返回 ModelAndView -> ViewResolver 渲染 - 执行后置:拦截器 postHandle/afterCompletion - 异常处理:catch 异常 -> HandlerExceptionResolver 链处理->统一响应 - 请求映射匹配细节 - @RequestMapping 能参与匹配的额求 - path(含 Ant 风格、PathVariable) - HTTP method (GET/POST/PUT/DELETE) - params(必须带参数/参数值) - headers (必须带 header) - consumes(请求 content-type) - produces(响应 Accept/Content-Type) - 参数绑定 - 请求行/URL/Query/Form 类 - @PathVariable:从 URL 路径中取值 - @RequestParam:从 queryString 或表单中取值 - @RequestHeader:从 header 取值 - @CookieValue:从 cookie 取值 - HttpServletRequest/Response、Principal 等:直接注入 - 默认绑定 - 对简单类型:往往当做 @RequestParam - 对复杂类型:走 @ModelAttribute 逻辑,把 query/form 参数按字段名绑定进去 - @RequestBody (正文序列化) - 请求体 (JSON) 通过 HttpMessageConverter 反序列化成对象 - 常见错误 - Content-Type 不是 application/json - 字段类型不匹配导致 HtppMessageNotReadableException - 缺少无餐构造器/反序列化失败 - 类型转换与校验 - 类型转换链路 - ConversionService:负责类型转换 - Converter/Formatter:自定义转换规则 - 校验 JSR-303 - @Valid/@Validated - 常见异常 - MethodArgumentNotValidException(@RequestBody + @Valid) - BindException(@ModelAttribute 绑定错误) - 配合 @ControllerAdvice 做统一错误响应 - 返回值处理 - 常见返回类型 - String:视图名 - ModelAndView:显式指定视图和模型 - ResponseEntity<T>:最推荐的 REST 返回方式之一,可控状态码/headers/body - T + @ResponseBody:直接序列化成 JSON - 返回值处理关键点 - @RestController = @Controller + @ResponseBody - 选择哪个 converter 取决于 - 返回对象类型 - Accept 头 - produces 配置 - 异常处理体系 - HandlerExceptionResolver 链 - 按顺序尝试解析异常 - ExecptionHandlerExceptionResolver:处理 @ExceptionHandler/@ControllerAdvice - ResponseStatusExceptionResolver:处理 @ResponseStatus - DefaultHandlerExceptionResolver:处理框架内置异常 - @ControllerAdvice 正确用法 - 全局异常 -> 统一 error code + message + traceId - 参数校验异常 -> 返回字段级错误列表 - 业务异常 -> 返回业务错误码 - 兜底异常 -> 返回 500 + 记录日志 - Filter vs Interceptor - Filter(Servlet 规范) - 发生在 Servlet 容器层面 - 能包裹整个请求响应链 - 用于:编码/CORS/鉴权/请求体包装/日志 traceId 注入 - Interceptor(Spring MVC) - 发生在 Handler 执行前后,更靠近 Controller - 包含三段内容 - preHandler(调用前) - postHandler(方法返回后,视图渲染前) - afterCompletion(整个请求完成后,用于清理资源/记录异常) - Filter 是容器级,Interceptor 是框架级,能拿到 HandlerMethod,适合做与业务 Handler 相关的逻辑 Spring boot (自动配置、启动过程、外部化配置) - Spring boot - 解决了什么问题 - Boot 的核心价值是:约定优于配置+自动配置+Starter依赖聚合 - Starter:把一组常用的依赖按照场景打包 - Auto-Configuration:根据 classpath、bean、配置项等条件自动创建 Bean - Externalized Configuration:同一的多来源配置体系 - Production-ready:Actuator、metrics、health、logging 等 - @SpringBootApplication 注解做了什么 - @SpringBootConfiguration 本质就是 @Configuration - @ComponentScan 默认扫描启动类所在包及其子包 - @EnableAutoConfiguration:开启自动配置导入 - 自动配置原理 - 概述 - Boot 会在启动阶段把候选自动配置类加载进容器,但是不是无脑全开,而是通过大量的 @Conditional 做条件装配 - 类路径上有没有哪个类 - 容器里有没有某个 Bean - 配置属性是否存在且满足 - 是否 Web 环境、是否 Servlet/Reactive - 自动配置类从哪里来 - 自动配置类的来源本质是声明式列表,Boot 在启动时读取他们并导入 - Boot 在启动阶段通过自动配置导入机制读取 classpath 上各个 starter 提供的自动配置声明,然后把这些自动配置类作为配置类导入容器;最终每个自动配置类再通过 @Conditional 决定是否生效 - 条件注解 - @ConditionalOnClass(A.class):classpath 有 A 才启动 - @ConditionalOnMissingBean(X.class):容器里没有 X 才创建默认实现 - @ConditionalOnBean(Y.class):必须已存在 Y 才启用 - @ConditionalOnProperty(prefix="a", name="b) - @ConditionalOnWebApplication - 自动配置覆盖/接管机制 - Boot 默认提供一个 Bean,例如某个 DataSource - 只要自己定义同类型 Bean,通常由于自动配置里用了 @ConditionalOnMissingBean,默认不会创建 - 启动过程 - 启动过程的分段 - 构建 SpringApplication - 推断应用类型:Servlet/Reactive/None - 加载初始化器/监听器(Initializers/Listeners) - 准备 Environment - 读取配置源:application.yml、环境变量、命令行参数、profile 等 - 绑定 spring.* 相关关键属性(例如 banner、log、profiles) - 创建 ApplicationContext - Servlet Web:AnnotationConfigStyleWebServletApplicationContext - Reactive Web:对于 reactive context - 准备 Context - 注册启动类 - 执行 ApplicationContextInitializer - 刷新 Context - 进入 Spring Core 经典流程 - 自动配置也在这一阶段被导入并参与 Bean 创建 - 启动 WebServer - 发布 ApplicationReadyEvent - 应用可以对外提供服务 - 外部化配置 - 配置来源 - application.properties/application.yml - profile 文件:application-dev.yml - 环境变量 ENV - JVM 系统属性(-Dkey=value) - 命令行参数(--key=value) - 优先级 - 越靠近运行时/越具体的优先级越高 - 命令行/系统属性/环境变量高于配置文件 - profile 配置文件会在激活 profile 后叠加 - Boot 会把多来源配置汇总成 Environment 的 PropertySources,按优先级从高到低解析;同名 key 取最高优先级来源的值 - Profile - 激活方式 - spring.profiles.active=dev - --spring.profiles.active=dev - 环境变量 - 生效规则 - application.yml 的默认块+application-dev.yml 覆盖叠加 - 配置绑定 @ConfigurationProperties - 对比 @Value - @Value:零散,难管理,不支持复杂层级/校验/IDE提示弱 - @ConfigurationProperties:可以把一个前缀下的配置绑定到一个对象,支持嵌套、集合、校验 数据访问整合 - 数据访问整合 - 通用底座 - DataSource:连接的来源 - DataSource 是连接池的抽象 - Spring 从 DataSource 拿 Connection,但事务期间会把 Connection 绑定到线程,避免每次 DAO 都新拿连接 - 事务贯穿的关键:TransactionSynchronizationManager(TSM) - 开启事务时:取连接->setAutoCommit(false)->bind 到线程 - DAO 执行时:通过 Spring 提供的工具从线程拿到同一连接 - 提交/回滚后:解绑并释放连接 - 同一异常体系:DataAccessException - Spring 会把各类底层异常统一成 DataAccessException 体系 - 屏蔽不同驱动/框架的异常差异 - 让事务默认回滚更符合直觉 - JDBC: JdbcTemplate - JdbcTemplate 的价值 - 封装样板代码:拿连接、创建 statement、执行、关闭资源、异常翻译 - 与 Spring 事务自然融合:事务内复用同一连接 - MyBatis 与 Spring 集成 - MyBatis 的核心对象 - SqlSessionFactory:会话工厂 - SqlSession:一次会话(执行 SQL 的入口) - Mapper:接口代理,最终通过 SqlSession 执行 - Executor:执行器(缓存、批处理等) - Spring-MyBatis 如何整合 - Spring 提供 SqlSessionTemplate - 在事务中,通过 Spring 的事务同步机制把 SqlSession 与当前事务绑定 - 事务提交/回滚时,SqlSession 会随着事务一起 commit/rollback - Mapper 扫描与处理 - @MapperScan 会注册 Mapper 接口代理 Bean - 调用 Mapper 方法 -> 代理 -> SqlSessionTemplate -> 执行 SQL Spring Security - Spring Security - 核心概念 - SecurityContext:安全上下文,存当前请求/线程的认证信息 - Authentication:一次认证的结果对象 - principle:用户主体(UserDetail 或 userId) - credentials:凭证(密码/token) - authorities:权限集合(角色/权限点) - authenticated:是否已认证 - SecurityContextHolder:存取 SecurityContext,默认 ThreadLocal - 认证成功后,SecurityContextHolder 里面会有 Authentication,后续授权判断都基于它 - UserDetails 体系 - UserDetails:用户信息抽象(用户名、密码、权限、是否过期) - UserDetailsService:按用户名加载用户 - PasswordEncoder:密码编码/校验 - 过滤器链:SecurityFilterChain - 请求进入一堆 SecurityFilters 才会进入到 SpringMVC - 认证机制 - AuthenticationManager/ProviderManager - AuthenticationManager:认证入口 - 常用实现:ProviderManager 内部持有多个 AuthenticationProvider - AuthenticationProvider:一种认证方式的具体实现 - 认证流程 - 过滤器拿到凭证,构造一个未认证的 Authentication - 交给 AuthenticationManager - ProviderManager 遍历 Providers,找到 supports 的 provider - provider 校验成功 -> 返回已认证的 Authentication - 写入 SecurityContexHolder - 后续授权阶段使用 - 授权机制 - authorities/roles 的关系 - Spring Security 里面权限统一叫做 GrantedAuthority - 角色以 ROLE_ 前缀表示 - hasRole("ADMIN") 等价于检查 authority ROLE_ADMIN - hasAuthority("xxx:read") 检查精确权限点 - URL 级授权

JVM 学习

类加载与字节码 - 类加载与字节码 - 类加载机制 - 类的生命周期 - 流程 - Loading(加载) - 把 .class 的字节流变成 JVM 内部的 Class 对象 - 验证 - 确保字节码符合 JVM 规范、类型安全、不会破坏虚拟机 - 准备 - 为类变量 static 字段分配内存并设置默认零值 - 解析 - 把常量池中的符号引用(字符串/描述符)转换成直接引用(指针/句柄) - 初始化 - 执行类初始化方法 <clinit> - 准备阶段:有 static 变量的空间+默认值,初始化阶段执行你的 static 赋值逻辑 - 准备阶段 vs 初始化阶段 - 默认值发生在准备阶段 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/19/20260219205053393.png,301,57) - 显式赋值发生在初始化阶段 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/19/20260219205149229.png,283,54) - 常量折叠 - <clinit> 类初始化方法 - <clinit> 是编译器合成的类初始化方法 - 来自于所有 static 字段的显式赋值语句和所有 static{} 块 - 按源码出现顺序合并执行 - JVM 保证同一个类的 <clinit> 在多线程下只会被一个线程执行,其他线程阻塞等待 - 初始化触发时机 - 初始化只在主动使用时触发 - 常用触发初始化场景 - new A() - 读取写入 A.staticField - 调用 A.staticMethod() - Class.forName("A") - 初始化子类前,先初始化父类 - JVM 启动时执行 main 类 - Class.forName 和 ClassLoader.loadClass - Class.forName("A"):默认加载+连接+初始化 - ClassLoader.loadClass("A"),默认只做加载和必要时连接,不初始化 - 类加载体系与双亲委派 - 三类内置类加载器 - Bootstrap:加载核心库(rt/基础模块),通常由 JVM 实现,不是 Java 对象 - Platform(JDK9+)/Ext(JDK8):平台扩展库 - App:应用 classpath 下的类(写的业务代码) - 双亲委派模型 - 流程 - loadClass(name) 先检查本加载器是否已经加载 - 没加载则委派给父加载器 - 父加载器加载不到,再由当前加载器尝试 findClass 加载 - 最终返回 Class<?> - 动机 - 安全:防止核心类被伪造替换 - 一致性:保证同名类在 JVM 中尽量只加载一份,避免类型混乱 - 破坏双亲委派 - 破坏方式 - 自定义 ClassLoader,重写 loadClass,不再先委派 - 使用线程上下文类加载器绕过父类限制 - 典型场景 - SPI/ServierLoader:核心库需要加载应用层实现(反向委派) - JDBC Driver:早期通过 Class.forName 强制注册类驱动 - 应用容器(Tomcat):每个 WebApp 一个 ClassLoader,实现隔离与热部署 - 破坏带来的风险 - 类重复加载 - 内存泄露:容器热部署时,类加载器无法回收->元空间泄露 - 类相等判断 - JVM 中,一个类的唯一性由(类的全限定名)+(加载它的 ClassLoader)共同决定 JVM 内存结构与对象模型 - JVM 内存结构与对象模型 - 运行时数据区 - 整体结构 - 线程私有 - 程序计数器 - Java 虚拟机栈 - 本地方法栈 - 线程共享 - 堆 - 方法区(Hotspot 对应元空间 + 运行时常量池) - 程序计数器 - 存放线程正在执行的字节码指令地址 - 线程私有:线程切换后能恢复执行位置 - 几乎不会 OOM - 如果执行 Native 方法,PC 值不一定有意义 - Java 虚拟机栈 - 栈帧结构:一次方法调用对应一个栈帧 - 局部变量表:基本类型值、对象引用、returnAddress - 操作数栈:字节码执行的临时计算区 - 动态链接:指向运行时常量池的方法/字段引用信息 - 方法返回地址 - 异常表 - 栈中存的对象 - 栈里一般不存对象本体,存的是对象引用 - 对象本体在堆上 - 栈相关错误 - StackOverflowError:递归太深/栈帧太大 - OutOfMemoryError:unable to create new native thread:系统资源耗尽 - 常见参数 - -Xss:每个线程的栈大小(越大单线程可递归更深,但是可创建线程数更少) - 本地方法栈 - 执行 JNI/Native 方法用 - 堆 - 存放几乎所有对象实例 + 数组 - 常见结构 - 新生代:Eden + Survivor(S0/S1) - 老年代 - G1 下是 Region 化,不强行分固定代区 - 常用参数 - -Xms 初始堆 - -Xmx 最大堆 - -XX:NewRatio/-XX:SurvivorRatio (传统分代) - -XX:+UseG1GC 选择收集器 - 堆相关 OOM - OutOfMemoryError: Java heap space 最常见 - OutOfMemoryError: GC overhead limit exceed - 方法区/元空间 - 存放类元数据:类结构信息、字段/方法信息、运行时常量池、方法字节码元信息等 - JDK 8 以后 HotSpot 用 Metaspace (本地内存)替代永久代 PermGen - 常见参数 - -XX:MaxMetaspaceSize 限制元空间上限 - -XX:MetaspaceSize 触发元空间 GC 的阈值 - 典型 OOM - 动态生成大量类 CGLIB、ByteBuddy、JSP 编译、反射代理 - 热部署/自定义类加载器泄露导致旧类加载器无法回收 - 对象创建与内存分配 - 对象创建的典型步骤 - 类检查:该类是否加载、解析、初始化 - 分配内存:在堆上为对象划分出一块连续空间 - 零值初始化:对象实例字段设置为默认值 - 设置对象头:写入 Mark Word,类指针等元信息 - 执行 <init>:构造方法把字段设置为期望的初始值 - 内存分配方式:指针碰撞 vs 空闲列表 - 指针碰撞 - 堆内存规整,已用区和闲置区是连续的,只要把指针往后移 - 优点:速度快 - 空闲列表 - 堆存在碎片,需要维护空闲列表,从中找合适块 - 优点:有碎片(标记-清除类收集器) - 是否能用指针碰撞,取决于 GC 是否会整理/复制使空间规整;不规整举要用空闲列表 - 并发分配:TLAB(Thread Local Allocation Buffer) - TLAB:为每个线程在 Eden 预留一小块私有分配区域,线程在 TLAB 内分配对象时基本无需加锁。 - 原因 - 多线程并发 new 对象频繁,每次都竞争全局堆指针,会有锁/原子操作开销 - TLAB 把大多数分配变成线程本地,提升吞吐 - TLAB 不够时会申请新的 TLAB 或者退到全局分配路径 - 对象内存布局与引用类型 - 对象内存布局 - 对象头 - MarkWord:哈希码、GC 分代年龄、锁信息 - Klass Pointer:指向类元数据(对象属于哪个类) - 实例数据:定义的字段 - 对齐填充:按 8 字节对齐 - 引用类型 - 强引用 - 默认引用:只要强引用存在,对象不回收 - OOM 大户:强引用过长导致对象一直存活 - 软引用 - 内存不足时才回收 - 弱引用 - 下一次 GC 旧可能回收(只要没有强引用) - 典型:WeakHashMap、ThreadLocal 的 key - 虚引用 - 不能通过它获取对象 - 用于对象回收后的通知/资源清理配合 ReferenceQueue - ThreadLocal 为什么会内存泄露 - 关键结构 - 每个 Thread 内部有一个 ThreadLocalMap - ThreadLocalMap 的 Key 是 ThreadLocal 的弱引用 - value 是强引用 - 泄露发生条件 - ThreadLocal 实例没有强引用 - 线程还活着 - ThreadLocalMap 里面出现 key=null,value=大对象的 entry - value 可能长期挂挂着 - 正确姿势 - 用完必须 remove() - 线程池环境更好要 remove GC 体系 - GC 体系 - GC 基础原理 - GC 要解决的问题 - Java 对象生命周期动态、数量大、手动 free 易错 - GC 的核心目标:在可接受停顿和吞吐下,回收不可达对象占用的内存 - 判定垃圾 - 引用计数 - 优点:实现直接 - 缺点:循环引用难处理 - Hotspot 主流不靠它做 GC 判定 - 可达性分析 - 从 GC Roots 出发做图遍历,遍历不到的对象是垃圾 - GC Roots 常见来源 - 虚拟机栈(栈帧局部变量表)引用的对象 - 方法区/元空间中静态字段引用的对象 - 方法区中常量引用的对象 - JNI 引用的对象 - 活跃线程对象,同步锁持有的对象 - 三大经典回收算法 - 标记-清除(Mark-Sweep) - 过程:标记可达 -> 清掉不可达 - 优点:简单 - 缺点:内存碎片 - 标记-整理(Mark-Compact) - 过程:标记可达->把存活对象向一侧移动->整理出连续空闲区 - 优点:无碎片 - 缺点:移动对象成本高,停顿更长 - 复制 - 过程:把存活对象复制到另一块区域,清空原区域 - 优点:分配快、碎片少 - 缺点:需要额外空间 - 分代为什么有效 - 核心假设:大多数对象朝生夕死,少数对象存活很久 - 堆分为 - 新生代(Young):频繁回收,复制算法为主 - 老年代(Old):存活对象多,回收频率低,整理/并发标记策略为主 - STW 从哪里来 - STW 的根源 - 根枚举/安全点:要一致地拿到 GC Roots,需要让线程在安全点停下 - 对象移动/引用更新:复制/整理需要更新指针,必须在一致视图下完成 - 并发标记时代的写屏障/记忆集 - 当 GC 和应用线程并发时,会出现对象引用关系变化的一致性问题。需要 - 写屏障:在写入引用字段时执行一小段额外逻辑 - Card Table/Remembered Set (RSet):记录跨代引用,避免全堆扫描 - 为避免扫描整个老年代/全堆,新生代回收时需要知道老年代指向新生代的引用;并发标记需要处理引用更新,因此用写屏障把变更记录到卡表/RSet,保证正确性与效率 - 收集器体系 - G1 (Garbage First) - 设计目标 - 在吞吐与低停顿之间折中,提供可预测停顿 - 关键机制 - Region 化:堆切成很多等大小 Region - 按收益优先回收:优先回收“垃圾最多、收益最高“的 Region - RSet:每个 Region 维护“谁引用我“的集合,避免全堆扫描 - 并发标记:找出老年代可回收对象,随后 Mixed GC 回收部分 old Regions - G1 的堆结构 - Region 是物理切片;在不同时间扮演不同角色 - Eden Region - Survivor Region - Old Region - Humongous Region (大对象) - G1 不是固定的新生代/老年代,而是逻辑分代 - G1 的回收类型与流程 - Young GC(仅回收 Young) - 触发常见原因:Eden 满 - 步骤 - STW 暂停 - 处理根与 RSet,找出可达对象 - 复制存活对象:Eden/Survivor -> 新的 Survivor/Old (晋升) - 更新引用,释放旧 Eden/Survivor Region - 恢复执行 - Concurrent Mark (并发标记周期) - 找出 Old 区的存活情况,为 Mixed GC 提供候选 Old Regions - 阶段 - Initial Mark:STW,很短,标记 GC Roots 直接可达(通常搭车在一次 Young GC 上) - Concurrent Mark(并发标记):与应用并发,从根开始遍历可达视图 - Remark(再标记):STW,修正并发期间引用变化 - Cleanup(清理/回收候选统计):统计每个 Region 的存活率,为 Mixed 选择回收集合 - Mixed GC (回收 Young + 部分 Old) - 触发条件:完成并发标记后,G1 会在若干 GC 中选择一批 Old Regions 与 Young 一起回收 - 特点 - Mixed 仍然是 STW(复制/整理 + 引用更新) - 部分 Old 由 G1 按收益挑选 - G1 的 Old 回收不是一次性全回收,而是多次 Mixed 逐步回收 - Full GC(G1 不想发生,但会发生) - Full GC 是兜底 - 并发标记跟不上分配速度 - 晋升失败/分配失败 - to-space exhausted(复制目标空间不足) - Humongous 压力导致碎片/可用 Region 不够 - Full GC 特点 - STW 时间长 - 会做全堆标记整理 - Humongous(大对象) - 在 G1 中,大对象通常定义为:大小 > Region 大小的 50% - 表现 - 大对象会直接分配到 Humongous Regions - Humongous 回收不如普通对象灵活,容易造成 - Old 快速膨胀 - Mixed 效果变差 - 频繁并发标记或 Full GC 风险上升 - RSet - 作用:支持只回收一部分 Region 仍然能正确找到 Roots - 代价:维护 RSet 需要写屏障与额外内存/CPU - G1 调优 - 4 个参数 - -XX:+UseG1GC - -XX:MaxGCPauseMillis=<N>: 停顿目标 - -Xms/-Xmx: 堆大小 - -XX:InitiatingHeapOccupancyPercent=<N>:老年代占用多少开始并发标记 - CMS - CMS 是什么 - 是 HotSpot 里老年代并发收集器 - 核心目标是降低老年代回收的停顿时间-让标记尽可能与应用线程并发执行 - Young 通常配 ParNew - CMS 总体结构 - 经典周期 - Initial Mark(初始标记)-STW - Concurrent Mark(并发标记)- 并发 - Remark(重新标记)-STW - Concurrent Sweep (并发清除) - Initial Mark(初始标记,STW) - 从 GC Roots 触发,标记直接可达的对象 - STW:一致性地枚举根,必须在 safepoint 停住线程 - Concurrent Mark(并发标记) - 从初始标记得到的对象开始,遍历对象图,标记所有可达对象 - 与应用线程并发执行 - 问题:应用线程在改引用->可能影响标记正确性 - Remark(重新标记,STW) - 修正并发标记期间因为引用变化导致的“漏标/标记不完整“ - 处理并发期间记录到变更(dirty card 等),把可达闭包补齐) - STW 原因:并发期间引用一直在变,如果不暂停,就无法在一个确定点收束标记结果 - Concurrent Sweep(并发清除) - 回收未标记对象的空间,把它们加入空闲链表 - 与应用线程并发执行 - 设计缺陷 - 内存碎片 - CMS 是 Mark-Sweep,不做压缩整理 - 老年代空闲空间可能变成很多不连续小块 - 后果:触发 Full GC(Serial Old/Parallel Old)来做一次标记-整理压缩 - 浮动垃圾 - CMS 标记是并发进行的 - 并发标记开始后,应用线程还在不断创建新对象,产生新垃圾 - 这些对象可能在本轮标记后才产生,不会被本轮回收 - CMS 回收触发的典型机制 - 不会等老年代完全满才开始 - 老年代使用率达到阈值开始一次 CMS 周期 -XX:CMSInitiatingOccupancyFraction 配合 -XX:+UseCMSInitiatingOccupancyOnly - 如果阈值太高,容易并发失败;阈值太低,吞吐下降 - 常用参数 - -XX:+UseConcMarkSweepGC:启用 CMS - -XX:+UseParNewGC:新生代用 ParNew - -XX:+CMSParallelRemarkEnabled: Remark 并行化 - ParNew 回收 - 作用范围:新生代(Eden+S0+S1) - 算法:标记复制 - 并发:STW,但是 GC 线程并行执行 - 核心流程 - STW(停顿) - 所有应用线程到达 safepoint 暂停,保证 Roots 枚举一致 - 根扫描(Roots) - 扫各线程栈、静态引用等 GC Roots - 额外要处理老年代 -> 新生代的引用 - YoungGC 不能扫描整个老年代,需要靠卡表 Card Table 记录老年代那些卡页变脏,只扫描这些变脏区域找到 Old->Young 的引用作为额外 Roots - 复制存活对象 - Eden 中存活对象复制到 Survivor(或者晋升到 Old) - Survivor(From)中存活对象复制到另一个 Survivor,年龄 + 1 - 复制过程中会设置 forwarding pointer,避免重复复制并更新引用 - 交换 Survivor - Eden 和旧 From Survior 清空 - To Survivor 变成新的 From - 恢复引用线程 - 对象如何从 Young 晋升到 Old - 年龄阈值:对象每熬过一次 YoungGC 年龄 +1,达到阈值 -XX:MaxTenuringThreshold 晋升 - Survivor 放不下:Survivor 容量不足,直接晋升到 old - 大对象直接进老年代:如果开启/配置了相关策略,大对象可能绕过 Young - ZGC - 目标:极低停顿 - 核心:并发标记+并发重定位,依赖读写屏障等机制 - 适合:大堆、强延迟敏感 JIT 与性能 - JIT 与性能 - 解释器 vs JIT - 解释器:启动快、编译开销低,但是每次执行都要逐条解释代码,长期运行慢 - JIT 编译器:把热点方法编译成机器码,长期运行快,但是编译有成本 - 热点探测 - HotSpot 用计数器发现热点 - 方法调用计数器:方法被多次调用 -> 变热 - 会变计数器:循环回跳次数多->循环热点 - 分层编译 - 解释执行(Tier 0):冷启动,收集部分 profile - C1(Client Compiler,Tier 1-3):编译快,优化较轻,适合尽快提升性能并继续收集 profile - C2(Server Compiler,Tier 4):编译慢,优化重,最终追求极致性能 线上排障与工具链 - 线上排障与工具链 - 保存现场 - 采集 4 件套 - GC 日志 - 线程栈 jstack - Heap Dump jcmd GC.heap.dump 或 jmap -dump - 关键进程信息:jcmd VM.flags、VM.command_line - 判断 CPU、内存、锁、I/O - CPU 高:先看线程在干什么 - RT 抖动:优先看 STW + 锁阻塞 + IO 等待 - 内存涨:看堆/元空间/直接内存 + 是否泄露 - Full GC:看触发原因、晋升、存活率 - OOM:先 dump,再分析引用链 - jps:快速找到 Java 进程 - 列出 Java 进程 PID,主类/jar 名称 - jps -l:显示主类全名或 jar - 定位 PID 的第一步,用于后续 jcmd/jstack/jstat - jcmd:万能入口 - jcmd 是 Hotspot 的控制台 - jcmd <pid> VM.version:JDK/Hotspot 版本 - jcmd <pid> VM.command_line: 启动参数 - jcmd <pid> GC.heap_info: 堆概要 - jcmd <pid> GC.class_histogram: 类直方图 - jcmd <pid> GC.heap_dump xx.hprof 生成 heap dump - jcmd <pid> Thread.print: 线程栈 - jcmd <pid> PerfCounter.print: 性能计数器 - jstack: 线程栈、死锁、阻塞、热点定位 - 用途 - CPU 高:找 RUNNABLE 热点线程在跑啥 - RT 抖动:看 BLOCKED/WAITING/TIMED_WAITING - 死锁:Found one Java-level deadlock 直接给出环 - 间隔 5-10s 连续采样 3 次,看热点栈是否稳定 - jstat: 快速查看 GC/类加载/编译统计 - 用户 - 判断 GC 是否频繁、老年代是否持续增长、晋升是否异常 - 看 metaspace/class load 是否异常增长 - 常用命令 - jstat -gcutil <pid> 1s 20: 每秒输出 20 次 GC 利用率 - jstat -gc <pid> 1s 20: 更详细的各区大小/容量 - jstat -class <pid> 1s 20: 类加载数量 - jstat -compiler <pid> 1s 20: JIT 编译情况 - jmap vs jcmd heap_dump: dump 与直方图 - 直方图 - 快速看到哪个类对象最多/占用最大 - 常用于 OOM 前后或内存上涨时的第一眼判断 - heap dump - 确定谁在引用链上把对象留住 - Arthas: 线上动态诊断 - dashboard: CPU/内存/线程/GC 全览 - thread -n 5: 找 CPU 占用最高的线程 - thread <id>: 看指定线程栈 - jvm: JVM 信息、参数、内存、GC、类加载器 - heapdump xx.hprof: 导出 heap dump - trace com.xx.Service method: 方法调用链耗时 - watch com.xx.Service method '{params,returnObj,throwExp}' -x 2: 观察入参/返回/异常 - tt:录制与回放 - 高频场景 - CPU 100% - 目标:找到哪个线程在烧 CPU,以及烧 CPU 的代码路径 - 标准流程: - OS 层确认 - top/htop 看进程 CPU - 定位 java 线程 - Arthas: thread -n 5 - top -H -p <pid> 找到线程 TID - 将 TID 转 16 进制 - printf "%x\n" 12345 - jstack <pid> 或 jcmd Thread.print 中搜索 16 进制 nid - 找到对应线程栈 - 判断栈顶 - 如果是业务循环/正则灾难/大 JSON 解析/排序/加密->代码问题 - 如果是 GC 线程占 CPU -> 看 GC 是否频繁 - 需要更准时,上 profiler - JFR/async-profiler 火焰图确认热点 - RT 抖动/毛刺 - 目标:分辨是 STW、锁阻塞、还是 IO/下游慢 - 标准流程 - 先看是否有 STW - GC 日志看 pause - jstat -gcutil 看 GC 是否在尖峰时频繁 - 非 GC,查看线程状态分布 - jstack 多次采样 - BLOCKED 多:锁竞争 - WAITING/TIME_WAITING 多:可能在等 IO/队列/park - 下游排查 - DB 慢查询、线程池队列积压,连接池耗尽 - 频繁 Full GC - 目标:解释为什么 Full 以及回收效果如何 - 标准流程: - 看 GC 日志:Full GC 的触发原因 - jstat -gcutil 看 - Old 是否增长 - FGC 是否增长 - YGC 后 Old 增长是否很快 - 先做直方图 - jcmd GC.class_histogram 看大对象/大集合 - 必要时 heap dump: - MAT 看 Dominator Tree + Path to Roots

MCP 原理

1. 学习主题 我要学习的内容: (例如:Transformer、Redis 持久化、JVM 垃圾回收、操作系统进程调度) ...

二月 18, 2026

RocketMQ 复习

概述 - 概述 - 定位 - RocketMQ 是一个分布式消息中间件,用于在分布式系统中以异步方式传递时间/数据 - 典型问题 - 异步解耦 - 场景:下单后做扣库存、发优惠券、发短信、写审计日志 - 没用 MQ:下单接口串行调用 N 个下游 -> 延迟高、耦合重、下游挂了全挂 - 用 MQ:下单服务只负责写订单 + 发消息,下游异步消费,链路变短、隔离故障、便于扩展 - 削峰填谷 - 场景:大促瞬时 QPS 飙升,下游撑不住 - 用 MQ:把瞬时峰值写入 MQ,消费者按能力稳定拉取 -> 系统不被打爆 - 广播/集群消费 - 广播:配置刷新、缓存预热、全量通知 - 集群:订单履约、发货、对账(同组只要处理一次) - 顺序/延迟/事务 - 顺序:同一订单状态流转必须按照顺序处理 - 延迟:30 分钟未支付自动关单 - 事务:下单写库 + 发消息要一致性 - 领域模型 - Topic/MessageQueue(并发与顺序的载体) - Topic:逻辑分类 - MessageQueue(队列/分区):Topic 下的物理分片 - 并发的本质:多个队列=>多个消费者实例并行消费 - 顺序的边界:只保证同一队列内有序,跨队列无需 - 原因:MessageQueue 带来了水平扩展吞吐(分片并行)+提供队列内顺序语义 - Producer 生产者 - 生产者发送消息时,核心要决定:这条消息进入哪个队列 - 普通消息:轮询/负载均衡选择队列(提升吞吐) - 顺序消息:按业务 key 做一致性路由到同一队列,保证顺序 - ConsumerGroup 消费组 - 消费组:一组消费者实例共享同一套消费进度 - 集群消费:同组内分摊队列 - 广播消费:每个实例都处理全量消息 - 同一个 Topic,两个不同的 ConsumerGroup 相当于两套独立的订阅者,各自消费一份完整消息流 - 卖点 - 高吞吐 - 顺序写:消息落到 CommitLog -> 磁盘友好 - 零拷贝/高效网络传输:减少内核态/用户态拷贝开销 - 批量与异步:批量写入、批量拉取、异步刷盘/复制 - 读写分离:写走 CommitLog,读通过索引定位 - 可扩展 - Topic 多队列 -> 消费端扩容实例可提升并发 - Broker 集群扩容 -> 分摊存储与 I/O 压力 - NameServer 做路由发现 -> Producer/Consumer 动态感知集群变化 - 靠队列分片 + Broker 横向扩展 + 消费组负载均衡 - 语义能力 - 至少一次投递与重复消费 - RocketMQ 工程实践通常是至少一次:可能重复,但不丢 - 业务必须幂等 - 重试与死信 - 消费失败 -> 重试 - 达到最大次数仍然失败 -> 进入死信队列 - 生产实践:DLQ 监控高进 + 人工/自动补偿 + 重新投递 - 事务消息(最终一致性) - 目标:本地事务与消息发送一致 - 核心机制:半消息(先存 broker 不投递)-> 本地事务 -> commit 才投递;不确定则回查 - 它解决的是最终一致性,不是强一致 ACID 核心概念 - 核心概念 - Topic - 定义:消息的逻辑分类,Producer 发送到 Topic,Consumer 订阅 Topic。 - 关键点:Topic 本身不决定并发,并发来自 Topic 下的 MessageQueue 数量 - 一个 Topic 通常会在多个 Broker 上分布存储,每个 Broker 上持有一些队列分片 - MessageQueue - 定义:Topic 的物理分片,是投递与顺序的基本单元。 - 并发能力=队列数上限:同哦个 ConsumerGroup 里,一个队列同一个时刻只会被一个消费者实例消费 - 顺序边界:RocketMQ 的顺序语义通常是队列内有序,跨队列不保证 - 路由载体:Producer 发送时必须选择一个队列(轮询/哈希/自定义) - Producer 生产者 - 职责:构造消息、选择 Topic 队列、发送、处理失败重试 - Producer 会先拿到 Topic 的路由信息(队列分布在哪些 Broker) - 发送失败通常会重试/换 Broker(取决于客户端策略与路由状态) - 发送模式 - 同步:拿到 sendResult 才返回(最常用) - 异步:回调(高吞吐/低延迟) - 单向:不关心结果(日志/打点) - Consumer(消费者) - 职责:拉取消息(Pull 体系,Push 是封装)、本地处理、成功后提交 offset,失败触发重试 - RocketMQ PushConsumer 本质是客户端内部循环 Pull + 回调你业务代码 - 消费并发:由消费线程数 + 队列分配决定 - ConsumerGroup(消费组) - 定义:一组消费者实例,共享同一套消费进度(offset),一起消费 Topic。 - CLUSTERING(集群消费):同组分摊消费(每条只处理一次) - BROADCASTING(广播消费):每个实例都处理全量(各自进度) - NameServer - 定位:路由注册与发现(轻量注册中心,不像 Kafka 把协调状态放在上面) - Broker 启动后像 NameServer 注册:Topic -> Broker -> 队列信息 - Producer/Consumer 定期从 NameServer 拉取最新路由 - NameServer 挂了是否可以发消息 - 短期通常能发(客户端还有路由缓存),但路由无法更新;长期不行,且新 Topic/新 Broker 不可见。 - Broker - 职责:接收消息,持久化存储,为消费者提供拉取、维护消费进度相关能力、做复制。 - 消息一般先写 CommitLog (顺序写),再构建队列索引。 - 吞吐高的原因:顺序写+批量+高效 IO 路径 - Offset (消费进度) - 定义:ConsumerGroup 在每个 MessageQueue 上的消费位置 - 成功消费 -> 提交 offset - 失败 -> 不提交 -> 后续重试/再次提交 - 数据流 - 生产链路 - 拿路由 - Producer 启动后从 NameServer 拉取 Topic 路由:有哪些 Broker、每个 Broker 上有哪些队列 - 选队列 - Produer 根据策略选择一个 MessageQueue - 普通消息:轮询/负载均衡 - 顺序消息:按业务 key 哈希到固定队列 - 发送到 Broker - Producer 把消息发送到队列所在 Broker - 持久化与应答 - Broker 持久化成功后返回 sendResult,Producer 认为发送成功 - 消费链路 - 订阅与分配 - Consumer 加入 ConumserGroup,订阅 Topic - 在集群模式下,同组实例会进行队列分配:每个 MessageQueue 分配给某个实例 - 拉取 - 被分配到队列的 Consumer 维护该队列的 offset,向 Broker 发起拉取请求 - 拉取这个队列从 offset=N 开始的下一批消息 - 本地处理 - Consumer 收到消息后调用你的业务处理逻辑(线程池并发执行) - 提交 Offset - 业务成功 -> 提交 offset(推进进度) 可靠性、语义、幂等、重试、死信 - 可靠性/语义/幂等/重试/死信 - 消息语义 - 三种语义 - At-most-once(至多一次):不重复,但可能丢 - At-least-once(至少一次):尽量不丢,但可能重复(RocketMQ 采用) - Exactly-once(恰好一次):不丢不重 - 为什么会重复 - 本质:分布式不确定性,无法在网络超时常见里判断对方是否已经成功 - 生产端导致重复:超时不等于失败 - Producer 发出了 send 请求 - Broker 实际写成功了,但 ack 在路上丢了/Producer 超时 - Producer 认为失败 -> 重试发送 - Broker 上存在两条业务等价消息 - 消费端导致重复:处理成功但提交 offset 失败/延迟 - Consumer 拉到消息并执行成功 - 在提交 offset 前发生进程重启/网络抖动/提交失败 - 下次启动从旧 offset 拉 -> 同一消息再次被处理 - 重试与死信:失败后到底怎么流转 - 消费失败 -> 重试:两类消费方式的区别 - 并发消费:消息可并行处理;失败后会触发稍后重投 - 顺序消费:同队列内严格顺序;失败会卡住队列,知道成功或打到策略 - 重试次数与延迟 - 消费失败后,消息会进入“重试主题/重试队列“ - 每次重试都会有一定的延迟 - 达到最大重试次数后仍然失败 -> 进入死信队列 - DLQ 常见命名 %DLQ%<ConsumerGroup> - DLQ 工程治理 - 监控报警 - DLQ 堆积量、进入 DLQ 的速率、重试次数分布、消费失败率 - 分类原因 - 可重试:下游超时、偶发网络、锁冲突 - 不可重试:参数非法、业务校验失败、数据缺失 - Offset 管理 - Offset 是什么 - Offset 是 ConsumerGroup 在每个 MessageQueue 上的消费位置 - 不是全局一个值,而是 (group,topic,queueId) -> offset - 正确提交时机 - 处理成功后再提交 性能与扩展、顺序与分区、堆积治理 - 性能与扩展、顺序与分区、堆积治理 - 吞吐与并发怎么调 - 吞吐链路拆解,多个环节的 min - Producer 发送能力 - Broker 写入能力 - Consumer 拉取与处理能力 - 队列分片与分配:队列数决定 - 并发的硬上限:Queue 数决定可扩展性 - 集群消费模式下,同一个 ConsumerGroup 内,一个 MessageQueue 同一时刻只会分配给一个消费者实例 - 有效消费实例并行度 <= 队列数 Q - 有 C 个实例:有效利用 = min(Q,C) - 线程数只能提高单实例处理并行,队列数太少时,扩容实例不涨吞吐 - 调整的三类杠杆 - 队列数 - 现象:加消费者实例吞吐不涨/涨很少 - 原因:Q 太小,很多实例分不到队列 - 操作:提高 Topic 队列数 - 消费者实例数 - 前提:Q 足够 - 收益:线性提升 - 消费者线程数/批量参数 - 适用 Q 足够但实例受限或业务处理轻的场景 - 堆积定位 - 堆积的数学本质 - 堆积增长 = 生产速率 > 消费速率,差值积累在 broker - 堆积原因分类 - 消费变慢 - 失败重试太多 - 生产突增 - Broker 压力抖动 - 堆积排查步骤 - 确定堆积是全局还是局部 - 如果是局部队列对接,大概率是热点 key/顺序路由/某实例异常 - 消费失败率与重试量 - 失败率高 -> 先止血,否则越修越崩 - 识别同一类异常栈是否集中 - 看单消息处理耗时分布 - 平均不重要,重点看 p95/p99 - p99 高往往来自:慢 SQL、远程调用尾延迟、锁竞争 - 看下游容量 - 看队列数与分配 - 看 Broker 层资源 - 堆积治理手段 - 止血 - 限流生产端:控制入口 - 失败隔离:把异常消息旁路到补偿 Topic/DLQ,避免拖垮主消费 - 降级业务逻辑:先做核心动作,其他异步/延后 - 恢复吞吐 - 扩容消费者实例 - 提高批量拉取/批量处理 - 优化热点:把热点 key 拆散 - 若某些队列极端堆积:考虑迁移/重新分配队列所在 broker - 根治 - 优化热点:慢 SQL、缓存、异步化、批处理 - 调整队列数与 key 策略 - 建立 DLQ/补偿闭环与告警 - 压测与容量规划:明确峰值可承受的 TPS 与延迟 存储与高可用 - 存储与高可用 - Broker 存储核心 - 三类文件 - CommitLog:消息本体的唯一真相源 - 所有 Topic 的消息都会追加吸入 CommitLog(不是每个队列一个日志,而是统一日志) - 写入模型是 append-only(追加写),是 RocketMQ 的高吞吐基础之一 - ConsumeQueue:面向消费的队列索引 - 消费者按照 MessageQueue 语义来消费,CommitLog 是全局混合写入的,需要一个队列视角的索引结构把队列映射回 CommitLog - ConsumeQueue 的本质是:每个 (topic, queueId) 一套逻辑队列索引,记录这条队列消息在 CommitLog 的物理位置等信息 - ConsumeQueue 是可重建的-Broker 异常后基于 CommitLog 恢复并重建缺失的 ConsumeQueue 索引,因此 ConsumeQueue 对绝对不丢要求没那么高 - IndexFile:按 key 查询的哈希索引 - 用于按 message key 做快速查询 - 写入流程 - 流程 - Producer 把消息发送到 Broker - Broker 顺序追加写 CommitLog - 再异步/批量地更新 ConsumeQueue/IndexFile - 原因 - 顺序写 CommitLog 最快 - 索引可以延迟/批量构建,并且 ConsumeQueue 丢了也能重建 - 读取链路 - 消费者按队列 (topic, queueId) 消费 - Consumer 拉取时携带 offset - Broker 通过 ConsumeQueue 把队列 offset 映射到 CommitLog 的物理位置 - 再从 CommitLog 把消息读出来返回给 Consumer - 吞吐高:顺序写+mmap+零拷贝+批量 - 顺序写:CommitLog append-only 模式写磁盘 - mmap 内存映射与 MappedFile 抽象 - Broker 用 mmap 把文件映射为虚拟内存,写入像写内存一样,减少传统 read/write 的系统调用与拷贝成本 - 零拷贝方向 - 批量 - 批量写入、批量拉取能显著摊薄网络往返与系统调用开销 - HA:复制、切换、延迟与一致性取舍 - 传统 Master/Slave:复制延迟与一致性 - Master 负责写入,Slave 复制数据做冗余 - 关键变量:复制是同步还是异步,直接决定 - RPO(数据丢失窗口):异步复制可能丢失最近一段 - RTO(恢复时间):切换/重连需要时间 - 自动主从切换:Controller - Controller:把谁是主、怎么选主、怎么切这个协调逻辑集中管理,从而支持自动故障转移 - 切换时关注 - 选主/仲裁机制 - 路由更新 - 复制进度 - 业务容忍度 - RocketMQ-on-DLedger(Raft):更强一致的 HA 思路 - DLedge 把 CommitLog 的复制变成 Raft 日志复制,从而提高一致性并提供自动选主 - 代价:写路径更重,延迟更高,但一致性更强、切换更自动化

SSE 原理

1. 学习主题 我要学习的内容: SSE 原理 我为什么要学它: (写清楚用途、场景、目标,比如面试、项目、论文、考试) ...

计算机组成原理

全局框架 - 全局框架 - 系统层次 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/18/20260218105815855.png,424,537) - 核心矛盾:速度差 - CPU 运算快、内存慢、外设更慢 - 系统用层次结构(寄存器/Cache/内存/磁盘)和并行性(流水线/乱序/多核/DMA)来隐藏或者摊薄延迟 - 指令在 CPU 里按“取值-译码-执行-访存-写回“的数据通道运行 - 为了加速,做流水线把这 5 步重叠 - 内存访问通过 Cache 利用局部性降低平均延迟 - 虚拟内存通过 MMU/TLB 把程序看到的地址映射到物理内存 - I/O 慢是由于设备与总线带宽/延迟远弱于 CPU,因此依赖中断、缓冲与 DMA 来减少 CPU 参与 指令系统与数据表示 - 指令系统与数据表示 - 整数与位运算的数据表示 - 位、字节、字 - bit:0/1 - Byte:8bit - Word(字):CPU 自然处理的宽度,与寄存器/ALU/地址宽度相关 - 为什么 byte 是最小编址单位:大多数现代机器按照字节编址,方便表示字符/结构体/网络数据等 - 原码/反码/补码 - 原码 - 表示方法:最高位符号位,剩下是绝对值 - 问题: - 有 +0 和 -0 两种零 - 加减法需要额外处理符号,硬件复杂 - 反码 - 正数同原码 - 负数:对正数位取反 - 问题 - 仍然有 +0 和 -0 - 加法需要回卷进位 - 补码 - 正数:与无符号相同 - 负数:按位取反 + 1 - 关键性质: - 只有一个 0(没有 -0) - 加减法统一为同一套加法器:a-b=a+(~b+1) - 符号扩展简单:高位补符号位即可 - 范围不对称 [-2^{n-1},2^{n-1}-1] - 快速转换 - 补码->十进制 - 最高位 0:按无符号直接算 - 最高位 1:取反 +1 得到绝对值,再加负号 - 十进制->补码 - 正数:直接写二进制,左侧补 0 - 负数:先写绝对值二进制 -> 取反+1 - 有符号/无符号加法与溢出判断 - 无符号溢出(Unsigned overflow) - 本质:结果超出 [0, 2^n-1] - 判定方法:看进位 Carry-out - 有符号溢出 - 本质:结果超出 [-2^{n-1},2^{n-1}-1] - 判断口径统一 - 同号相加,异号结果 => 溢出 - 异号相加不会溢出 - 等价位级判定 - 符号位的进位与最高位进位输出不同=>溢出 - 字节序:大端 vs 小端 - 对 32-bit 只 0x12 34 56 78 高位在左 - 大端 - 低地址存高字节 - 内存从低地址到高地址 12 34 56 78 - 直觉:与人类书写一致 - 小端 - 低地址存低字节 - 内存从低地址到高地址:78 56 34 12 - 直觉:低地址就是低位,做逐字节扩展/截断更自然 - x86 等主流架构长期使用小端 - 对齐与 Padding - 对齐 - 要求:某类型对象的地址必须是某个中的倍数(4/8) - 为什么要对齐 - 硬件访问效率:CPU/总线按照字/双字读写;未对齐可能需要两次内存读写再拼接 - Cache line 与内存访问粒度:对齐能减少跨行/跨 cache line 的概率 - Padding(填充) - 为了满足对齐,编译器会在结构体字段之间或末尾插入空字节 - 结构体大小通常是其最大对齐需求的倍数 - 浮点数(IEEE 754) 与误差本质 - IEEE 754 基本格式 - 浮点数表示 - S(符号位):0 正 1 负 - E(指数/阶码):带拍支 bias - F(尾数/小数部分):有效数 significand 的小数部分 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/18/20260218112914998.png,218,39) - 单精度 float(32-bit) - S: 1 位 - E: 8 位,bias = 127 - F: 23 位 - 双精度 double(64-bit) - S:1 位 - E: 11 位,bias=1023 - F: 52 位 - 规格化、非规格、0、INF、NaN - 规格化 - 指数 E 既不是全 0 也不是全 1 - 有隐含的 leading 1:1.F - 非规格化 - 指数 E 全 0,尾数 F 非 0 - 没有隐含 1:有效数是 0.F - 目的:渐进下溢,让非常接近 0 的数仍然能表示 - 0 - E 全 0,F 全 0 - 有 +0 和 -0 - Inf 无穷 - E 全 0,F 全 0 - 有 +0 和 -0 - NaN(非数) - E 全 1,F 非 0 - 来自非法操作:0/0 - NaN 会传播 - 0.1 + 0.2 != 0.3 - 十进制小数在二进制中往往是无线不循环小数 - 0.1 的二进制是 0.0001100111 - float/double 只能保存有限位尾数 - 存储时发生舍入,计算时再舍入,误差叠加 - 比较时用 ==,就可能不相等 CPU 执行与控制 - CPU 执行与控制 - 单周期 Datapath - 必备组件 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/18/20260218153017482.png,429,229) - PC 当前指令地址寄存器 - Instruction Memory/I-Cache:根据 PC 取指令 - Register File (寄存器堆):两读一写(rs1、rs2 读;rd 写) - Immediate Generator(立即数生成/符号扩展) - ALU:算数逻辑/比较/地址计算 - Data Memory/D-Cache:load/store 访问 - Adders: - PC + 4 - PC + imm (分支目标) - 关键 MUX(多路选择器) - ALUSrc:ALU 第二操作数是来自 rs2 还是 imm - MemToReg:写回 rd 的数据来自 ALU 还是内存 - PCSrc:下一 PC 来自 PC+4 还是分支目标 - Control Unit(控制器):根据 opcode/funct 产生控制信号 - datapath = 一堆功能模块 + 多根线 + MUX 选路 - 四条指令在 datapath 里怎么走 - R 型算数:add rd, rs1, rs2 - 目标:rd = rs1 + rs2 - 数据流 - PC -> 取指令 - 寄存器堆读 rs1、rs2 - ALU 计算 ALUOut = rs1 + rs2 - 写回:rd <- ALUOut - PC 更新:PC <- PC + 4 - 关键控制 - RegWrite = 1 写寄存器 - ALUSrc = 0 第二操作数来自 rs2 - MemRead = 0, MemWrite = 0 - MemToReg = 0(写回来自 ALU) - Branch = 0(不分支) - ALUOp = R-type(由 funct 决定是 add/sub/and/or) - 访存读:lw rd, imm(rs1) - 目标:rd = Mem[rs1+imm] - 数据流 - 取指 - 读寄存器 rs1 (base),rs2 通常也读但不用 - ALU 计算有效地址:ALUOut = rs1 + imm - 数据内存读:MemOut = Mem[ALUOut] - 写回:rd <- MemOut - PC <- PC + 4 - 访存写:sw rs2, imm(rs1) - 目标:Mem[rs1+imm] = rs2 - 数据流 - 取指 - 读 rs1(base) 和 rs2(要写入的数据) - ALU 算地址:ALUOut = rs1 + imm - 数据内存写:Mem[ALUOut] <- rs2 - PC <- PC + 4 - 条件分支:beq rs1, rs2, imm - 目标:如果 rs1 == rs2,则 PC <- PC + imm;否则 PC <- PC + 4 - 数据流 - 取指 - 读 rs1,rs2 - ALU 做比较:Zero = (rs1-rs2==0) - 同时计算目标分支:Target = PC + imm - PC 选择 - if Branch & Zero:PC <- Target - else: PC <- PC + 4 - 单周期 vs 多周期 - 单周期 - 每条指令都在一个周期完成 - 周期必须长到容纳最慢指令路径 - 结果:简单但浪费,端指令也被迫跑长周期 - 多周期 - 把一条指令拆分成多个步骤,每一步一个周期 - 不同指令用不同步骤数量 - 可以复用硬件 - 性能 - 性能公式 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/18/20260218153851413.png,354,137) - IC:执行的指令条数 - CPI:平均每条指令需要多少周期 - Cycle Time:每周期多长时间 (=1/主频) - Amdahl 定律(优化上限) - 如果系统中某部分占比位 f,该部分加速 S 倍,总加速比 - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/18/20260218154107114.png,227,64) - IPC、吞吐、延迟 - 延迟:完成单个任务/单条指令/一次请求需要的时间 - 吞吐:单位时间完成多少任务 - IPC:每周期完成多少条指令 - 吞吐 = IPC x 频率 流水线与冒险 - 流水线与冒险 - 五级流水线核心内容 - 五段功能定义 - IF:取值;PC->I-Cache;计算 PC+4 - ID:译码;读寄存器;生成立即数 - EX:ALU 运算;地址计算;分支比较 - MEM:访问 D-Cache - WB:写回寄存器 rd - 流水线寄存器 - 为了让每段每周期独立推进,段与段之间插入寄存器 - IF/ID、ID/EX、EX/MEM、MEM/WB 它们保存上一段的输出给下一段用(包含数据与控制信号) - 理想吞吐与延迟 - 非流水:一条指令用 5 个周期,下一条等它结束 - 流水:启动后每周期完成 1 条 - 延迟:单条仍然=5 个阶段 - 吞吐:理想从 1/5 提升到 1 - 流水线的代价 - 需要通多寄存器与控制逻辑 - 分支错预测要清刷流水线 - 结构冲突要加资源或停顿 - 结构冒险 - 定义 - 同一周期内两个阶段需要同一个硬件资源 - 例子 - 单端口存储器:IF 要取值,MEM 要访存 - 同一周期冲突 -> 必须 stall 或改造硬件 - 解决方案 - 分离 I-Cache/D-Cache - 多端口存储器 - 数据冒险 - 三种依赖 - RAW(Read After Write)真依赖:后读依赖前写,最关键 - WAR(Write After Read)反依赖:后写不能早于前读 - WAW(Write After Write)输出依赖:两条写同一目的寄存器 - 为什么会 RAW:写回晚,读取早 - 读寄存器在 ID - 写回寄存器在 WB - Fowwarding (旁路/转发)机制 - 不等写回到寄存器堆,直接把产生的结果从后端绕回到前段使用 - 转发路径 - EX/MEM -> ID/EX(给下一条的 EX 用) - MEM/WB -> ID/EX (给下一条或下下条的 EX 用) - Stall(插泡)与 load-use hazard - load-use 典型序列 - lw r1, 0(r2) - add r3, r1, r4 - 原因 - lw 的数据到 MEM 末才可用 - add 需要在 EX 用到 r1 - 时间上赶不上,即使有 forwarding 也不够快 - 结论:需要插入一个 bubble - 实现 - 冻结 PC 和 IF/ID - 往 ID/EX 注入一个 NOP (控制信号清零)形成 bubble - 控制冒险 - 定义:PC 的下一值不确定:到底走 PC+4 还是分支目标 - 分支决定在哪里做决定 penalty - 如果分支在 EX 才能确定 - 在分支结果出来之前,流水线已经取了后续若干条 - 一旦发现分支 taken,就要把错路径指令 flush 掉 - 基础策略 - stall until branch resolved:等分支算出来再取后续 - predict not taken:默认不跳,先取顺序;若实际 taken -> flush - 动态分支预测:用分支历史表、2-bit 饱和计数器等降低错预测 - BTB:预测 taken 时快速给出目标地址 - 延迟槽 - penalty - 分支罚时:错预测或 taken 导致需要清空的阶段数 存储体系 - 存储体系 - 核心矛盾 - CPU 周期是纳秒级,DRAM 访问通常是几十到上百纳秒级,差一个数量级 - 解决思路:分层存储 + 局部性 - Cache 用更贵更快的 SRAM 缓存最近用过的数据块,依赖时间/空间局部性降低平均访存时间 - Cache 基础概念 - Cache line (缓存行) - Cache 以“行/块“位单位搬运数据 - CPU 读写某地址时,实际会把该地址所在的整条 cache line 搬到 Cache。 - 推论:顺序访问数组通常很友好;跨行访问容易 miss - Hit/Miss 与三大指标 - Hit time:命中时访问延迟 - Miss rate:未命中比例 - Miss penalty:未命中后去下一级/内存取回的代价 - 平均访存时间 AMAT - AMAT = Hit Time + Miss Rate * Miss Penalty - MISS 分类 - Compulsory miss(冷启动):第一次访问必 miss - Capacity miss(容量):工作集超过 Cache 容量 - Conflict miss(冲突):映射/组相联导致不同块争同一行 - 映射方式:直接映射/全相联/组相联 - 前置数据 - Cache 总容量 (不含tag/元数据) = C bytes - Cache line 大小 = B bytes - 总行数 (lines) = L = C/B - 相联度 (ways) = A - 组数 (sets) = S = L/A - 直接映射 - 每个内存块只能映射到唯一一个 cache 行 - 优点:硬件简单、命中快 - 缺点:冲突 miss 多 - 全相联 - 任意内存块可放任意行 - 优点:几乎无冲突 miss - 缺点:需要对所有行比较 tag(实现复杂、命中更慢、耗能更高) - 组相联 - 内存块映射到某个 set,但可以放在 set 内任一 way。 - 折中方案 - 地址拆分 - 对于字节编址系统,地址通常拆分为:tag+index+block offset - offset 块内偏移 - 表示在一条 cache line 內的字节位置 - 位数 offset bits = log2(B) - index 组索引 - 选择 set - index bits = log2(S) - tag 标记 - 用于判断该 set 中某一 way 是否是想要的内存块 - tagbits = Address bits - index bits - offset bits - 替换策略 - LRU:替换最久没用的行 - Pseudo-LRU(伪 LRU):用更少 bit 近似 LRU - FIFO:先进先出 - 写策略 - write-throught(写穿) - 写命中:同时写 Cache 和下一级 - 优点:下一级始终最新,实现简单 - 缺点:写流量大 - write-back(写回) - 写命中:只写 Cache,把该行标记为 dirty - 该行被替换时才写回下一级 - 优点:显著减少写带宽压力 - 缺点:一致性更复杂(多核) - 写不命中 write miss 的两种策略 - write-allocate(写分配):先把整条 line 读入 Cache,再在 Cache 上写,与 write-back 搭配 - no-write-allocate(不写分配):不把 line 加入 Cache,直接写下一级,常与 write-throught 搭配 - 多级 Cache 的 AMAT - L1/L2 两级 - AMAT=HitTime_L1​+MissRate_L1​×(HitTime_L2​+MissRate_L2​×MissPenalty_L2​) - 多核:一致性 vs 一致性协议 - Cache Coherence(一致性) - 目标:对于同一内存地址 X,各核的 cache 中 X 的副本要看起来像一个指在变化,不能读各自的旧值 - 解决办法:一致性协议(MESI)保证写入能让其他核的副本失效/更新 - Memory Consistency(一致性模型,针对不同地址的顺序) - 目标:规定不同核对不同地址的读写在时间顺序上允许怎么样被观察到 - 是内存模型讨论的问题,涉及重排序与屏障。 - Coherence 解决同一地址副本一致,Consistency 解决不同地址操作的可见顺序,前者靠协议,后者靠内存模型和 fence。 - MESI 协议 - False Sharing - 定义 - 不同核访问的是不同变量,但是这些变量恰好落在同一条 cache line 里 - 其中一个核写它自己的变量,会把整条 line 的其他核副本失效,导致另一核反复 miss - 性能问题 - 引发 invalidation(整条 line 粒度) - 两个核交替写同一条 line 上不同字节 - line 在核之间反复迁移,带来大量一致性流量 虚拟内存 - 虚拟内存 - 要解决的问题 - 抽象:每个进程看到连续的虚拟内存空间(VA) - 保护:权限控制(R/W/X,用户态/内核态) - 共享:共享库/共享内存/页共享 - 扩展:按需分页 + 换入换出 - 页 - 虚拟页:进程看到的虚拟地址空间被切分成的固定大小块 - 物理页框:正式物理内存被切成同样大小的块 - 二者的大小相同,比如常见 4KB - 虚拟内存用虚拟页编号,物理内存用页框编号,页表负责把虚拟页号 -> 物理页框号 - 地址和页对应 - 页大小是 4KB - 4KB = 4096 字节 = 2^12 - 一个地址的低 12 位代表页内偏移 - 高位表示虚拟页号(VPN) - 地址转换链路 - VA -> TLB -> page table -> PTE -> PA -> Cache/Memory - 术语解释 - VA:进程发出的地址 - PA:真实内存地址 - VPN:虚拟页号 - VPO:页内偏移 - PPN:物理页框号 - PTE:页表项,记录 VPN -> PPN 的映射与权限等元数据 - TLB:页表项的高速缓存 - 页大小与地址拆分 - 页大小是 PageSize = 2^k 字节 - offsetBits = k - VPNBits = AddrBits - k - 转换过程 - CPU 产生 VA - MMU 用 VA 的 VPN 去查 TLB - TLB hit:直接得到 PPN + 权限 - 物理地址 PA = (PPN << offsetBits) | Offset - TLB miss:用 VPN 去查页表 - 拿到 PTE - 有效 -> 填入 TLB -> 形成 PA -> 继续访问 - 无效 -> 触发异常 - 页表结构 - PTE 包含什么 - PPN:物理页框号 - Present/Valid:是否在内存 - R/W/X:读写执行权限 - U/S:用户/内核权限 - Accessed/Referenced(A):是否被访问过 - Dirty(D):是否被写过 - 为什么需要多级页表 - 单级页表 - VA 空间大,VPN 组合数巨大 - 每个进程都有一张覆盖整个虚拟空间的大表 - 多级页表做法 - 把 VPN 再拆分成多端:VPN1/VPN2/.../VPNn - 顶层页表只在需要时为某个范围分配下一级页表 - 没用到的地址区间不分配页表 -> 按需分配页表 - 多级页表的 Page Walk - 先用 VPN 的高段索引顶层页表,取到二级页表的地址 - 再用下一段索引二级页表页 - 最终找到叶子 PTE 得到 PPN - 代价:要读多次内存 - TLB - 没有 TLB - 每次 load/store 都要走 page walk,可能是 4/5 级 - 意味着每次访问内存前都要额外多次访存 - TLB 的本质 - 缓存最近用过的 VPN -> PPN + 权限 - 命中时地址翻译几乎是常数空间 - 缺页异常 - 典型原因 - Demand paging(按需分页):页还没加载到内存 - Swap out:页被换出到磁盘 - 权限违规:写只读页 - Copy-on-write:写共享只读页 - 缺页处理流程 - CPU 访问 VA,发现 PTE present = 0 或者权限不符合 - 触发异常,陷入内核(保存上下文,切换到内核栈) - 内核 fault handler 判断 fault 类型 - 是合法的按需页/非法地址 - 合法 - 找到磁盘位置 - 分配物理页框 - 发起 IO 把页读入内存 - 更新页表 PTE - 更新 TLB - 恢复进程执行 - 页面置换 - 当需要一个物理页框但是没有空闲页时,OS 必须选择一个牺牲页换出 - LRU - Clock/Second-Chance(近似 LRU) - 维护环形指针 - 每个页有 Accessed/Referenced 位 - 扫描时 - 若 A=0:淘汰 - 若 A=1:把 A 清 0,给第二次机会,指针继续走 - 共享与写时复制(COW) - 为什么需要 COW - fork 的语义:子进程得到父进程内存的副本 - 如果把所有页都复制一遍,成本极高 - COW - fork 后,父子进程的页表都指向同一批物理页框 - 把这些页的 PTE 权限改为只读 - 当父或子尝试写入某页 - 写入触发 page fault - 内核分配新物理页 - 复制旧页内容到新页 - 修改该进程页表让它指向新页,恢复写权限 - 另一个进程仍然指向旧页 IO 总线 - IO 总线 - IO 本质上慢 - 设备物理特性:SSD/网卡/磁盘的延迟远远高于 CPU - 路径长:系统调用、内核态切换、驱动、协议栈、队列管理 - 数据搬运:拷贝次数多、缓存一致性/同步开销高 - I/O 的基本硬件/软件组件 - 设备、控制器、驱动 - 设备:网络、磁盘、USB 设备等 - 控制器:设备侧的执行单元,负责 DMA、队列、寄存器接口等 - 驱动:内核中的软件,负责配置设备、提交请求、处理中断、管理队列 - 数据通路常见参与者 - CPU 核心 - Cache - 内存 - I/O 互联 (PCIe) - 设备控制器 - 设备介质 - 轮询 vs 中断 - 轮询 - 做法:CPU 不断读取设备状态寄存器 - 优点:实现简单、延迟可控 - 缺点:浪费 CPU、高并发/多设备会严重占用 CPU - 中断 - 设备完成后发中断信号,CPU 暂停当前执行,进入中断处理程序(ISR) - 优点: - CPU 不需要忙等->提升整体吞吐 - 缺点: - 有中断开销:恢复/保存上下文、切换栈、处理 ISR - 中断频繁造成抖动 - DMA - 不用 DMA 的程序搬运 - CPU 循环从设备寄存器读数据,再写入内存 - 缺点:CPU 被数据搬运绑死,带宽高、功耗高 - DMA 的本质 - DMA 让设备/控制器直接读写内存,CPU 只做设置与收尾 - CPU 配置 DMA 描述符:内存地址、长度、方向、队列位置 - DMA 引擎通过总线把数据直接写入/读出 DRAM - 完成后用中断通知 CPU(或写状态寄存器) - DMA 为什么更快 - 释放 CPU 周期 - DMA 更贴近总线/设备,能更高效做 burst 传输 - 配合 ring buffer/descriptor queue 能批量提交 - 内存映射 I/O 与端口 I/O - MMIO - 设备寄存器被映射到某段物理地址空间 - CPU 用普通的 load/store 指令读写这些地址,就等价于读写设备寄存器 - Port-mapped I/O - 使用专门的 IN/OUT 指令访问 I/O 端口空间 - 现代系统多以 MMIO 为主 - 零拷贝 - 传统读文件再发网络的拷贝路径 - read():磁盘->内核页缓存->拷贝到用户缓冲区 - write():用户缓冲区->再拷贝回内核 socket buffer -> 网卡 DMA 发走 - 至少需要两次 CPU 拷贝(内核<->用户,用户<->内核) - sendfile 思路 - 直接让内核把 page cache 中的文件页挂到 socket 发送队列 - 避免拷贝到用户态再拷回内核态 - 网卡通过 DMA 从内核缓冲直接发 - mmap 思路 - mmap 把文件映射进进程地址空间 - 应用像访问内存一样访问文件内容,缺页时 OS 按需把页载入 page cache - 适合随机访问,但是直接转发场景 sendfile 更适合

计算机网络

网络分层 + 端到端路径 - 网络分层 + 端到端路径 - 分层模型 - 链路层 (LINK) - 负责:同一链路/局域网内把 IP 包送到下一跳 - 典型概念:以太网、WIFI、ARP、MAC、MTU - 网络层 (IP) - 负责:跨网络把包从源 IP 路由到目的 IP - 典型概念:IP 地址、路由表、NAT、ICMP、分片 - 传输层 (Transport) - 负责:端到端进程通信,提供端口与可靠性 - 应用层 (Application) - 负责:定义应用语义 - 端到端路径(一次 HTTPS URL 发生了什么) - URL 解析与策略决策(应用层前置) - 浏览器先拆 URL: - schema:https -> 需要 TLS - host:example.com -> 需要 DNS - port:默认 443 - path:/index.html - 是否可用缓存 - 是否复用已有连接 - DNS 解析:把域名变成 IP - 目标:得到服务器的 IP (A=IPv4/AAAA=IPv6) - 典型流程 - 浏览器/OS 先查本地缓存 - 没命中 -> 向递归解析器发起查询 - 递归解析器询问:根 -> TLD -> 权威 DNS - 返回 IP + TTL - TTL 决定缓存时长 - 同一域名可能返回多个 IP:简单的负载均衡,但是不可控 - TCP 连接(传输层) - 目标:建立一条到服务器IP:443的 TCP 连接 - 典型流程 - 客户端选一个临时端口作为源端口 - 形成 socket 四元组 - 三次握手完成后,连接进入 ESTABLISHED - 连接的唯一标识符是四元组 - NAT 环境下:srcIP 可能是私网 IP,但是公网会被 NAT 改写 - RTT 大时,建连成本高:三次握手至少消耗 1 个 RTT - NAT:内网 IP 也能访问公网(网络层/链路层交叉) - 大多数客户端在 NAT - 内网 192.168.x.x:ramdomPort - 出口 NAT 把它映射成:公网 IP:另一个 Port - NAT 设备维护一张映射表(内网四元组<->公网四元组) - 映射有超时:长时间无数据可能被回收 - 并发连接太多可能端口耗尽 - TLS 握手:在 TCP 上建立安全通道 - 目标 - 验证服务器身份 - 协商密钥 - 后续数据用对称加密 + 完整性保护 - TLS 发生在 TCP 建立之后、HTTP 之前 - 证书用于“证明对方是它声称的域名“ - 发送 HTTP 请求并接受响应 - HTTP 内容 - 请求行:GET /index.html - 头:Host、User-Agent、Accept、Cookie - 体:GET 通常无,POST 常见 - 服务器返回 - 状态码:200/301/404/500 - 头:Content-Type、Content-Length、Set-Cookie - 体:HTML/JSON/图片等 - 连接复用与关闭:不是每次都重新握手 - HTTP/1.1 默认倾向长连接(Keep-Alive) - 浏览器有连接池,会复用到同一个 host:port 的连接 - 关键概念锚点 - RTT:网络交互的时间单位 - RTT = 一个来回的延迟 - 建连/TLS/重传都依赖 RTT - 高 RTT 下,交互次数越多越慢 - 吞吐与 BDP(带宽-时延积) - MTU 月 MSS - MTU:链路层最大承载 - MSS:TCP 单段可承载的应用数据最大值 - 包太大可能被分片或者丢弃 - 合理 MSS 能减少分片风险,提升稳定性 TCP 精讲 - TCP 精讲 - TCP 本质:在不可靠 IP 上提供可靠字节流 - TCP 提供内容 - 面向连接:双方都维护连接状态(序号、窗口、定时器等) - 可靠、有序:丢包重传、乱序重排、去重 - 字节流:没有消息边界(粘包/拆包的根源) - 流量控制:避免把对方接收缓冲打爆(rwnd),端到端 - 拥塞控制:避免把网络打爆,端到网络 - 报文段关键字段 - Sequence Number:本段数据在字节流里的起点序号 - Acknowledgment Number:我期望收到的下一个收到的序号 - Flags:SYN/ACK/FIN/RST/PSH/URG - Window(rwnd):接收端通告窗口 - Options:MSS,Window Scale - 连接建立:三次握手 - 标准流程 - C->S: SYN(seq=x) - S->C: SYN+ACK (seq=y,ack=x+1) - C->S: ACK (ack=y+1) - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/17/20260217111458329.png,514,267) - 为什么必须三次 - 双方都要确认两件事 - 对方能收:对方收到了我的东西 - 对方能发:对方能把东西发回来让我收到 - 两次握手只能证明客户端 -> 服务端这条路通了;但是服务端无法证明服务端->客户端也通、且客户端确实收到了服务端的回应,第三次 ACK 就是让服务端拿到这个确认(服务端无法确认客户端能收) - 如果只有两次握手 - 服务端回 SYN + ACK 丢失了 - 服务端仍为连接建立,开始分配连接资源,等待数据 - 客户端实际上没有收到任何回应,会重试或放弃 - 三次握手还能防止历史重复 SYN 造成的“幽灵连接“ - 历史重复 SYN 指的是:某次旧连接尝试时发出的 SYN,由于网络拥塞、路由绕行、链路重传、设备缓存/异常等原因,在很久之后才到达服务器 - 如果服务器把旧 SYN 当新建连: - 服务器会分配资源 - 如果后续又出现旧 ACK 包,让服务器误以为连接已建立,交付错误的数据 - 幽灵连接:就是服务端自嗨式建立,客户端不认可 - 半连接队列与 SYN Flood - 服务端收到 SYN 后进入 SYN_RECV,会占用半连接资源(backlog) - SYN Flood:大量 SYN 不完成握手 —> 半连接队列被占满 -> 正常连接进不来 - 典型缓解:SYN cookies、增大 backlog、限速、丢弃策略、前置抗 DDoS - 连接关闭:四次挥手、半关闭 - 主动关闭方 A->B:FIN(不再发了) - B -> A:ACK (知道了) - B -> A:FIN (我也不再发了) - A -> B:ACK (知道了) - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/17/20260217113908534.png,440,230) - 为什么常见是四次 - TCP 是全双工:A 不发 != B 不发 - B 可能还有数据要发完,所以 ACK 和 FIN 分开 - 半关闭 - A 发 FIN 表示 A 的发送方向关闭,但仍然可以接受 B 的数据 - 工程上常见于“请求发送完但继续接受响应流“的场景 - RST:不是正常关闭,是异常打断 - RST(Reset)是 TCP 里面非常硬的控制信号:立刻终止连接/拒绝连接 - 不是正常的四次挥手关闭,告诉对方:“这条连接不存在/状态不一致,马上停“ - RST 常见原因 - 端口没监听 - 应用崩溃/强制关闭 - 中间设备丢弃状态后注入 RST - TIME_WAIT、CLOSE_WAIT - 主动关闭方在发送最后一个 ACK 后进入 TIME_WAIT,并等待 2MSL。 - 必须 TIME_WAIT - 保证对方收到最后 ACK:最后 ACK 丢了,对方重发 FIN;TIME_WAIT 还能再次 ACK - 让旧连接的延迟报文过期:避免旧数据混入新连接 - 为什么是 2MSL - MSL = 报文在网络中可能存活的最大时间 - 等待 2MSL 是为了覆盖对方 FIN 重传 + 我 ACK 再次发送等往返的不确定性。 - TIME_WAIT 多了怎么办 - 常见场景:短连接高 QPS - 治理思路 - 优先做连接复用:HTTP Keep-Alive/连接池 - 调整应用行为:减少主动 close、合并请求、复用长连接 - 系统参数:允许端口更快复用等 - 通常是我方主动关 + 短连接 - CLOSE_WAIT 多是什么 - CLOSE_WAIT 表示:对方已经 FIN 了,但是我方应用还没有 close - 常见原因:应用层忘记关闭连接、连接泄露、线程阻塞导致没有走到 close - 通常是我方没关 - 可靠传输机制:序号、ACK、重传、SACK - 序号与累计 ACK - TCP 把数据看成连续字节流:每个字节都有序号 - ACK 是累计的:ack = N 表示 0..N-1 都收到了,下一步要 N - 好处:ACK 开销小 - 坏处:遇到乱序/丢包,单靠累计 ACK 不够精细 - 重传触发机制 - 超时重传(RTO):等不到 ACK 就重发(RTO 动态估计,基于 RTT 抖动) - TCP 发送一个数据段后,会期待收到 ACK,如果在某个时间窗口没有等到 ACK,TCP 可能认为丢了,于是触发超时重传。 - 等多久的时间阈值就是 RTO(Retransmission Timeout)。 - 触发条件:发送后,计时器到期,仍未收到能确认该段的 ACK - 行为:重传未确认的数据段 - RTO 动态估计 - 网络 RTT 会变,而且会抖动,RTO 必须跟随着链路实际 RTT 变化 - SRTT + RTTVAR -> RTO - 名词解释 - RTT sample:一次测得的 RTT(从发出某段到收到它对应 ACK 的时间) - SRTT (Smoothed RTT):平滑后的 RTT 均值。 - RTTVAR:RTT 的波动。 - RTO = SRTT + 4 * RTTVAR - SRTT 给出正常情况下 ACK 来回要多久 - RTTVAR 给出不确定性有多大 - 乘以 4 是给一个安全幅度:抖动越大,RTO 增长越明显,避免误重传 - 指数退避:超时一次,RTO 翻倍 - RTO 重传 vs 快速重传 - RTO 重传 - 触发:计时器到期,没等到 ACK - 特点:更“保底“,但通常更慢;而且代表网络更糟糕 - 常伴随:指数退避,cwnd 通常会大幅下降 - Fast Retransmit (快速重传) - 触发:重复 ACK - 特点:更快,更轻判拥塞 - 一半比 RTO 更温和 - 快速重传(Fast Retransmit):收到多个重复 ACK (典型 3 个)推断某段丢失了,立即重传) - RTO 重传局限性 - 必须等待一段时间没收到 ACK 才重传 - 在 RTT 较大或 RTO 比较保守时,恢复会很慢 - 快速重传的思路 - 不等超时,利用接收端反馈的 ACK 形态更早推断丢包 - 重复 ACK - 如果接收端收到了更后面的段,但是缺了中间某段 - 不能把乱序段交付给应用 - 会继续发送同一个累计 ACK,告诉发送端还在等从 1000 开始的那段,后面来的我先缓存 - ACK 值没前进,但又反复出现的 ACK 就是重复 ACK - 3 个重复 ACK 会触发快速重传 - 收到 3 个重复 ACK 代表某段大概率丢了 - 立即重传缺口对应的最早未确认段 - 快恢复 - 原因 - 收到快重传代表完了给没有完全堵死(还能持续收到 ACK) - 不必像 RTO 那样极度保守把发送速率打到很低 - Reno 直觉 - 触发快速重传后,认为发生拥塞 -> 降低 cwnd - 但利用重复 ACK 的到来,允许发送端在恢复期间适度继续发,避免完全停摆 - 当收到新的 ACK 后,退出快恢复,回到拥塞避免阶段 - SACK(选择确认) - 痛点:累计 ACK 信息不够 - 当出现丢包 + 乱序到达时,累计 ACK 只能不断重复同一个 ACK=N,发送端只知道缺口在 N,不知道 - 后面哪些段已经到了 - 究竟丢失了几段,丢失了哪几段 - 重传时是否把已到达的也重传 - 目标是把接收端实际收到的乱序块显式告诉发送端 - SACK 是什么:在 ACK 里带已收到的区间块 - SACK 是 TCP 的一个 Option,启用后 - 接收端在 ACK 报文的 TCP Options 中携带若干个 SACK blocks - 每个 block 描述一个已经收到的连续字节区间 - SACK block 的语义 - SACK block = (Left Edge, Right Edge) - 表示接收端已经收到:[LeftEdge, RightEdge) 这个区间 - ACK 字段仍然存在,表示最左侧连续已收前缀的下一个期待序号 - 启用流程:SACK Permitted - SACK 需要在三次握手时协商 - 双方在 SYN/SYN+ACK 中携带 SACK Permitted 选项 - 只要双方都声明支持,后续才会在 ACK 中携带 SACK blocks - 乱序与重排 - 网络层可能乱序,TCP 负责在接收端按序交付给应用 - 接收端会缓存乱序段,等缺口补齐再交付 - 滑动窗口 - 痛点 - 如果每发一段就必须等 ACK 再发下一段,吞吐会被 RTT 严重限制:链路大部分时间在等回音。 - 滑动窗口允许发送端在未收到 ACK 前,先发出一批数据,让数据在路上飞,从而把链路利用起来。 - 窗口 = 允许在途未确认数据的上限;窗口越大,在高 RTT 链路上容易跑满带宽 - rwnd(receive window,接收窗口,流量控制) - 由接收端通告(在 TCP 头的 Window 字段里) - 表示接收端缓冲区还剩多少空间 - 目的:防止发送端把接收端内存撑爆 - cwnd(congestion window,拥塞窗口,拥塞控制) - 由发送端维护 - 表示发送端认为网络当前能够承受的在途数据量上限 - 目的:防止把网络打爆 - swnd(send window,实际发送窗口) - 发送端真正允许“在途未确认“的上限 - 近似于 min(rwnd,cwnd) - 发送端视角:窗口如何限制发送 - 发送端维护两个关键指针 - SND.UNA:最早未确认的序号(UnACKed) - SND.NXT:下一个准备发送的序号(Next to send) - 窗口约束可以理解为: - 允许发送的序号范围:[SND.UNA, SND.UNA + swnd) - 已发送未确认的范围:[SND.UNA, SND.NXT) - 还能继续发送的额度:swnd - (SND.NXT - SND.UNA) - ACK 到来时发生什么 - 收到 ACK,把 SND.UNA 往前推进 - 已确认数据从窗口左边滑出 - 窗口右边腾出了空间,允许继续发送新的数据 - 接收端视角:为什么会出现 dupACK/乱序缓存 - 接收端维护两个关键指针 - RCV.NXT:下一段按序期望的序号 - 接收缓冲:可以暂存乱序到达的段 - 出现乱序 - 接收端会缓存后到的段,但 ACK 仍然回 RCV.NXT(累计 ACK) - 发送端看到重复 ACK (dupACK),可能触发快速重传 - 窗口、吞吐、RTT - 吞吐上限 = 窗口大小/RTT - RTT=100ms,窗口=64KB,吞吐=64KB/0.1s=640KB/s - BDP(带宽-时延积),窗口需要=BDP 才能跑满带宽 - 表示一条网络路径在任意时刻在路上飞着的数据量大概有多少,也叫管道容量 - BDP = 带宽 * 往返时延 (RTT) - 想要把链路跑满,发送端需要让“在途未确认数据“接近 BDP - 在途未确认数据主要受窗口限制:in_flight <= min(cwnd, rwnd) - 要跑满带宽,需要窗口大小 >= BDP - 零窗口与窗口探测 - 如果接收端缓冲满了,会通告 rwnd=0 - 发送端此时必须停止发送新数据,但是要避免死锁 - TCP 有 persist timer:周期性发送 Zero Window Probe 探测包 - 一旦接收窗口恢复,发送端继续发 - 粘包/拆包 - 粘包:在发送端 send() 了两条业务消息,接收端一次 recv() 读到了两条合在一起的字节。 - 拆包:在发送端 send() 了一条业务消息,接收端需要 recv() 多次才能读完 - TCP 不保证 send 一次就对应端 recv 一次 - 原因 - TCP 的抽象是流 - 像读文件一样读一串字节 - send():把字节追加到发送缓冲区 - recv():从接收缓冲区取走当前可读的字节 - 分段由 TCP 决定,不由应用决定 - 接收端读取粒度由应用决定 - 解法 - 在应用层定义消息边界 - 长度前缀(Length-Prefixed,最通用) - 格式 | 4-byte length (big-endian) | payload bytes... | - length 表示 payload 长度 - 接收端先读满 4 字节得到 length,再继续读满 length 字节 - 分隔符(Delimiter-Based,适合文本协议) - 行协议:以 \r\n 结尾 - 固定长度(Fixed-Size) - 每条消息固定 N 字节 - 拥塞控制 - 拥塞不是对端瘦不下,那是流量控制 rwnd,拥塞是网络路径上的队列/链路承载不住 - 路由器/交换机队列堆积 -> RTT - 队列溢出 -> 丢包 - 丢包/排队抖动 -> 吞吐下降、延迟暴涨 - 拥塞控制控制的是发送端允许在网络中飞着的未确认数量,cwnd - Reno 的两个核心状态变量 - cwnd:拥塞窗口(发送端认为网络现在能承受的在途数据量) - ssthresh:慢启动阈值,决定从指数增长到线性增长的分界点 - 四个阶段 - 慢启动 - 目的:刚开始不知道网络能承受多少,先快速试探 - 规则: - 初始 cwnd 为一个较小值,历史是 1MSS - 每收到一个 ACK,cwnd 增加 1MSS - 因为一个 RTT 内会收到大约 cwnd/MSS 个 ACK,所以每个 RTT cwnd 近似翻倍 - 每 RTT 把在路上的包数翻倍,迅速逼近瓶颈带宽 - 太快可能把队列灌满,引发丢包 - 拥塞避免 - 当 cwnd 到达或超过 ssthresh,从慢启动切换到拥塞避免 - 每 RTT 让 cwnd 大约增加 1 MSS,每个 ACK 增加 MSS*MSS/cwnd - 大多数时间连接处于的阶段 - 快重传:用 dupACK 提前判断丢包,不等 RTO - 触发条件 - 收到 3 个重复 ACK dupACK - ACK 号不前进,连续出现多次 - 快恢复:丢包后减速但不中断,比 RTO 温和 - 发生快重传后的关键动作 - 设定新的值 - ssthresh = cwnd/2 - cwnd 不回到很小,而是进入快恢复逻辑 - 当收到能够推进 ACK 的新 ACK,退出快恢复,进入拥塞避免 - RTO vs dupACK - RTO:最严重,认为网络可能很糟 - ssthresh = cwnd / 2 - cwnd 下降到很小,经典是 1MSS - 回到慢启动(重新探测) - RTO 通常会指数退避,导致恢复更慢 HTTP 与 Web 体系 - HTTP 与 Web 体系 - HTTP 基础语义 - HTTP 是无状态应用层协议:每个请求都自包含信息,状态由 Cookie/Token/Session 等机制在应用层实现 - HTTP 定义的语义,具体传输方式由 HTTP/1.1、HTTP/2、HTTP/3 决定 - 方法:语义 + 幂等 + 安全性 - GET:获取资源,应当安全+幂等 - HEAD:只要响应头,不要响应体,用作探测/缓存探测 - POST:提交数据,创建子资源或触发动作,通常非幂等 - PUT:整体替换/创建指定资源,通常幂等 - PATCH:部分更新,是否幂等取决于语义设计 - DELETE:删除资源,通常幂等 - OPTIONS:探测服务器能力(常用于 CORS 预检) - 状态码 - 1xx:信息(少见) - 2xx:成功 - 200 OK - 201 Created 创建成功 - 204 No Content 成功但无响应体 - 3xx:重定向 - 301 永久 - 302 临时(历史语义混乱) - 303 让客户端用 GET 拿 - 307/308 保持方法不变 - 4xx:客户端错误 - 400 参数/格式问题 - 401 需要认证 - 403 拒绝 - 404 不存在 - 409 冲突 - 429 限流 - 5xx:服务器错误 - 500、502 上游坏/网关问题 - 503 不可用 - 504 上游超时 - Header - 通用与内容相关 - Content-Type:实体类型(json/html...) - Content-Length:长度(HTTP/1.1 重要) - Content-Encoding:gzip/br 等压缩 - Accept/Accept-Encoding/Accept-Language:内容协商 - Host:HTTP/1.1 必须(虚拟主机) - Connect:连接管理(HTTP/2 中不再使用) - 会话与安全 - Cookie/Set-Cookie - Authorization (Bearer/Basic 等) - Origin/Refer:CORS/CSRF 相关 - Strict-Transport-Security:强制 HTTPS - Content-Security-Policy(CSP):前端安全策略 - 缓存相关 - Cahce-Control - Expires - ETag/If-None-Match - Last-Modified/If-Modified-Since - Vary - Age (代理缓存的年龄) - Pragma:no-cache (历史兼容) - HTTP 缓存 - 两类缓存:强缓存vs协商缓存 - 强缓存 - 命中强缓存时浏览器直接用本地缓存,不走网络 - 典型控制 - Cache-Control: max-age= - Expires:老机制 - 协商缓存(发请求但可能不下载) - 浏览器发请求带校验文件,服务器判断资源是否变化 - ETag 流程 - 响应:ETag: "xxx" - 下次请求:If-None-Match: "xxx" - 若未变:返回 304 Not Modified - Last-Modified 流程 - 响应:Last-Modified: ... - 下次请求:If-Modified-Since:... - 未变:304 - Cache-Control 关键指令 - max-age=N:资源在 N 秒内视为新鲜(强缓存) - no-store:绝对不缓存 - no-cache:可以缓存,但每次使用前必须去服务器验证 - must-revalidate:过期后必须验证,不能用陈旧副本 - public:允许被共享缓存(CDN/代理)缓存 - private:只允许浏览器私有缓存,不允许共享缓存缓存 - s-maxage=N:给共享缓存的 max-age - Vary 存储维度的分桶键 - Vary 告诉缓存,同一个 URL 的响应会因为某些请求头不同而不同 - Vary:Accept-Encoding,gzip/br 不同版本要分开缓存 - Vary:Origin:CORS 常见 - Vary 过多会让缓存碎片化,命中率下降 - ETag 的强弱、代理与一致性 - 强 ETag:字节级一致才算命中 - 若 ETag (W/):语义上等价即可 - HTTP/1.1:连接、报文、队头阻塞 - HTTP/1.1 报文与传输 - 文本协议:请求行 + 头 + 空行 + body - 连接默认与持久化:Keep-Alive (减少握手成本) - Chunked Transfer-Encoding:服务端可流式传输,不提前知道 Content-Length - HTTP/1.1 的性能问题:应用层 HOL - 同一连接上请求/响应按顺序进行 - 一个慢请求会阻塞后续的请求(队头阻塞) - HTTP/2:二进制分帧与多路复用 - HTTP/2 为什么快 - 二进制分帧:把大消息拆成帧 - 多路复用:多个 stream 在同一 TCP 连接上并发交错传输 - 头部压缩:HPACK(减少重复头部开销) - HTTP/2 性能问题:TCP 层 HOL(队头阻塞) - TCP 一旦丢包,需要重传并按序交付 - 丢的那段之前的洞补不上,后续数据即使到了也不能交给上层 - 结果:一个丢包会让同连接上所有 stream 都受影响 - HTTP/3/QUIC - 核心变化 - HTTP/3 跑在 QUIC 上,QUIC 基于 UDP - QUIC 自己实现可靠传输与拥塞控制 - 按 stream 提供独立的有序交付,一个 stream 丢包不影响其他 stream 的交付 - TLS 与 HTTPS - 位置与成本 - HTTPS = TCP 建连后再 TLS 握手,再 HTTP - 首次访问慢常见原因:DNS + TCP + TLS 多次 RTT - 解决的问题 - 身份认证:验证连接的是域名的真实服务器 - 机密性:传输内容加密 - 完整性:防篡改 - TLS 握手 - TLS 1.2 - ClientHello:客户端支持的版本/套件,随机数,SNI 等 - ServerHello:选定版本/套件、随机数,让双方对本次会话参数单程一致 - Certificate:服务器发送证书链,客户端据此验证:链是否可信、域名是否匹配、是否过期,用途是否正确 - ServerKeyExchange:服务器提供 ECDHE 参数与公钥,客户端通过验证签名,确保密钥协商参数的安全性 - ServerHelloDone:告诉客户端,材料已经发完 - ClientKeyExchange:客户端协商参数,客户端发送自己的 ECDHE 公钥,双方据此计算共享 secret - ChangeCipherSpec + Finished:双方切换到加密通信并校验握手完整性 - Web 会话:Cookie/Session/Token - Web 会话解决的问题 - 认证:你是谁 - 授权:你能做什么 - 会话保持:登录态怎么持续、怎么过期、怎么注销 - 安全边界:防窃取、防伪造、防重放、防跨站 - Cookie:浏览器自动携带的会话容器 - Cookie 是浏览器保存的一组 key=value,由服务器通过响应头 Set-Cookie 下发。 - 后续请求中,浏览器会按照规则自动带上 Cookie 头,无需 JS 参与 - Cookie 关键属性 - Domain/Path:决定发送范围 - Domain=example.con:子域也可能携带 - 不设置 Domain:仅当前 host - Path=/api:只对当前路径生效 - Expires/Max-age:决定生命周期 - Max-Age=0:立即删除 - Session Cookie:不设过期时间,随浏览器进程结束而清理 - Secure:只在 HTTPS 发送 - HttpOnly:禁止 JS 读取 - 有 HttpOnly 的 cookie,JS 读取不到 - 不能防止 CSRF - SameSite:控制跨站请求是否携带 - Strict:几乎不跨站携带 - Lax:默认常用 - None:允许跨站携带,必须同时设置 Secure - Session:服务端保存状态,客户端只存门票 - Session 的典型模式 - 服务端维护 session_id -> session_data - 客户端用 Cookie 存 session_id - 每次请求:浏览器自动带 cookie -> 服务端查 session store -> 识别用户 - Token(尤其是 JWT):把身份证明带在请求里 - Token 的核心模式 - 服务端签发 token 给客户端 - 客户端后续请求带 token - 服务端验证 token 来识别用户 - JWT 结构 - JWT=header.payload.signature - header:算法等元信息 - payload:claims 声明 - signature:服务端用密钥签名 - Refresh Token 与 Access Token - Access Token:短有效期,每次用于请求 - Refresh Token:长有效期,用于换取新的 Access Token - 安全边界:XSS、CSRF、会话固定、重放 - XSS(跨站脚本) - 若 token/cookie 可以被 JS 读取,xss 可以直接窃取并盗用 - cookie 用 HttpOnly + Secure - CSP 减少脚本注入 - 输入输出编码 + 模板转义 - CSRF 跨站请求伪造 - CSRF 利用浏览器自动携带 cookie - 攻击者诱导用户在登录态下访问恶意页面,发起跨站请求到受害站点 - 防护手段 - SameSite Cookie (优先) - CSRF Token(表单/请求头携带,服务端校验) - 检查 Origin/Rerfer - 同源策略与 CORS - 痛点 - 浏览器安全模型默认假设:不同站点之间互不信任 - 浏览器用同源策略(Same-Origin Policy,SOP)限制一个网页脚本能读取另一个来源的资源内容 - 业务需要跨域调用 API,有了 CORS(CrossOrigin Resource Sharing):在 SOP 的基础上提供一个受控的放行机制。 - Orgin 源是什么:同源的三元组 - Origin = scheme + host + port - https://example.com:443 - 同源策略具体限制 - 跨源读被禁止 - JS 用 fetch 请求 https://xxx.com,请求可能能发出去,但是浏览器会阻止 JS 读取响应,除非响应满足 CORS 规则 - 跨源写/发送很多情况下是允许的 - <form action="xxx.com" method="POST"> 可以提交,因此 CSRF 能成立,请求可以发出去,但是页面读取不到 bank.com 的响应内容 - 部分资源加载天然跨源允许 - <img> <scirpt> <link> 浏览器允许加载这些资源 - CORS 本质:服务器声明允许,浏览器执行拦截 - 浏览器在请求带上 Origin - 服务器在响应返回 Access-Control-* 头 - 浏览器据此决定:是否把响应暴露给 JS - CORS 两种请求路径:简单请求 vs 预检请求 - 简单请求 - 方法是 GET/HEAD/POST - Content-Type 只能是三种之一 - application/x-www-form-urlencoded - multipart/form-data - text/plain - 请求头只能是 CORS-safelistedhead,不能随意加自定义头 - 预检请求 - 不满足简单请求条件,浏览器会先发送一个 OPTIONS 请求,询问服务器是否允许 - 预检请求携带关键头 - Origin: https://a.com - Access-Control-Request-Method:PUT - Access-Control-Request-Headers:x-token(将要携带的自定义头列表) - 服务器如果允许,需要在 OPTIONS 响应返回 - Access-Control-Allow-Origin - Access-Control-Allow-Methods - Access-Control-Allow-Headers - Access-Control-Max-Age(预检缓存多久) - 通过后,浏览器才会发真实请求 - CORS 响应头:每个头怎么用 - Access-Control-Allow-Origin - 指定允许的 Origin - 可以是精确 Origin 或者 * (允许任意源),如果要带 Cookie/凭证,不能用 * - Access-Control-Allow-Methods - 允许的方法列表:GET/POST - 主要用于预检响应 - Access-Control-Allow-Headers - 允许的自定义请求头列表 - 必须覆盖预检中 Access-Control-Request-Headers 请求到的那些头 - Access-Control-Allow-Credentials - true 表示允许携带凭证,例如 Cookie - 配合前端请求 credentials: 'include' - Access-Control-Expose-Headers - 默认情况下,JS 能够读取的响应头很有限,如果希望暴露,必须显式添加 - Vary: Origin - 按 Origin 分桶缓存 - DNS 解析 - 基础对象 - 记录类型 - A(IPv4) - AAAA(IPv6) - CNAME(别名) - NS(权威) - 解析角色 - Stub resolver(客户端/OS) - Recursive resolver(递归解析器:运营商/公司 DNS/公共 DNS) - Authoritative server (权威 DNS) - 正向代理、反向代理、透明代理与隧道 - 代理:本质上是位于客户端与服务端之间的一种中间节点,代表一侧发起连接、转发数据、并可能施加策略 - 承担职责 - 路由/转发:把请求送到正确上游 - 安全控制:鉴权、WAF、访问控制 - 性能优化:连接复用、压缩、缓存、限流 - 可观测性:日志、指标、追踪、采样 - 协议转换:HTTP <-> grpc 等 - 正向代理 - 正向代理站在客户端一侧,客户端明确知道自己在用代理,由代理代表客户端访问外部资源 - 反向代理 - 反向代理站在服务端一侧,对外它像真正的服务器入口,对内把请求转发到后端服务 - 反向代理常用原因 - 隐藏后端拓扑:外部只有一个入口 - 统一 TLS 终止:证书集中管理 - 统一流量治理:限流、熔断、灰度、路由、鉴权 - 连接复用:对外维持大量连接、对内复用少量连接 - 观测与审计:同一日志/指标/链路追踪 - 透明代理 - 隧道:Connect 到底干了什么 - 隧道:代理不解析上层协议,只做字节透传。 - HTTP Connect 隧道流程 - 客户端先对代理发 CONNECT - 客户端 -> 代理 - CONNECT bank.com:443 HTTP/1.1 - Host: back.com:443 - 代理建立到目标的 TCP 连接 - 代理 -> bank.com:443:发起 TCP 连接 - 代理回客户端 - HTTP/1.1 200 Connect Established - 隧道建立,字节透传 - 客户端与 bank.com 的 TLS 握手数据会被原封不动地通过代理转发,代理看不懂也不需要看懂 - TLS 会话在客户端<->目标服务器之间建立 - 负载均衡:L4 vs L7、算法、会话保持、健康检查 - 目标 - 扩展性:把流量分散到多台实例,提高吞吐 - 高可用:实例故障自动摘除,流量不中断或快速恢复 - 性能:就近、减少重连、减少排队、提升尾延迟 - 治理:灰度、限流、熔断、路由、可观测 - 边界 - LB 解决的是把请求推送到谁,不解决业务正确性 - LB 层任何自动重试/超时,都可能改变业务语义 - L4 与 L7 的区别 - L4 LB(传输层):基于 IP/端口/连接做转发 - 优点:性能高,对协议透明 - 缺点:无法基于 URL/Header 做路由 - 常见实现:内核转发、四层 VIP、DSR、NAT 模式等 - 适合长连接:WebSocket、MQ、数据库连接等 - L7 LB(应用层):理解 HTTP,按照 Host/Path/Header/Cookie 路由 - 常见常见:API Gateway、Ingress、CDN 回源入口 - 负载均衡算法 - Round Robin(轮询) - 依次分发到每个后端 - 适合:请求耗时相近、后端同质、短连接 - 问题 - 后端性能差异大时不均衡 - 长连接场景下连接一旦分配就会粘住,RR 也会失衡 - Weighted Round Robin - 按权重分配(机器规格不同,冷热分层) - 适合:异构机器,逐步扩容 - 问题:权重需要维护;负载时间随时间变化时仍然不准 - Least Connections(最少连接) - 连接数最少的后端连接 - 适合:长连接、连接数与负载相关的业务 - 问题: - 连接数不等于负载 - 需要 LB 维护连接计数状态 - Least Request/Least Load(最少请求/最小负载) - 按当前活跃请求数或综合指标选择 - 适合:请求耗时差异大,尾延迟敏感 - 问题:实现复杂、依赖观测准确性;指标延迟可能导致震荡 - Random/Power of Two Choices(随机/二选一) - 随机挑两个后端,选最优者 - 优点:简单且实际效果接近最优,分布更稳定,大规模下能显著降低热点概率 - 适合:大规模集群、需要避免中心化状态 - Consistent Hash(一致性 hash) - 按 key 映射到固定节点 - 适合: - 需要缓存命中(本地 cache) - 会话保持(不想引入共享 session) - 分片存储(按 key 分片) - 关键性质:节点增减时迁移量小 - 问题: - 热点 key 会压垮单节点(热点拆分/虚拟节点) - 节点故障时 key 重映射会引发抖动。 - 会话保持 - 为什么需要 sticky - 后端有状态 - 本地缓存必须命中 - WebSocket/长连接绑定到某实例 - 常见 sticky 手段 - Source IP hash - 按客户端 IP 做 hash 选节点 - 优点:简单 - 缺点: - NAT/代理下大量用户共享同一出口 IP -> 严重倾斜 - 用户换网 IP 变化导致漂移 - Cookie 粘性 - LB 注入 cookie ,后续按照 cookie 路由 - 优点:比 IP 稳定 - 缺点:跨域,隐私策略,cookie 清理,灰度/回源复杂 - 一致性哈希(按 userId/会话键) - 工程风险 - 降低容错:节点挂了,该节点的用户都受影响 - 产生热点:某些用户/租户流量大->单节点热 - 扩缩容抖动:重新分配导致缓存冷启动、延迟尖刺 - 推荐的替代方案 - 应用无状态 + 共享 session (Redis) - 把状态外置(DB/Cache),实例可以随时替换 - 长连接常见:连接层 sticky 不可避免,要做好限流与迁移策略 - 健康检查 - L4 健康检查 - 探测 TCP 能否 connect 成功 - 优点:便宜、通用 - 缺点:应用假活时仍然会通过(线程池满、依赖挂) - L7 健康检查 - 请求 /healthz 或 /readyz - 可检查 - 进程存活 - 是否可接新流量 - 关键依赖是否可用 - 设计要点 - liveness vs readliness 分离 - liveness:进程是否需要重启 - readliness:是否接流量 - 避免健康检查过重 - 如果每秒大量探测并且检查依赖,会把依赖也打挂 - 阈值与抖动控制 - 连续 N 次失败才摘除 - 连续 M 次成功才恢复 - 慢启动与预热 - 新实例启动后先不接流量,预热缓存/JIT,再标 ready - 网络排障 - L3/L4 基础联通与路径 - ping - 测试的是什么 - ping 用的是 ICMP Echo Request/Echo Reply - 发 Echo Request(带序号 seq、时间戳等) - 对端回 Echo Reply - 测量往返时延 RTT - ![](https://an-img.oss-cn-hangzhou.aliyuncs.com/2026/02/18/20260218103650584.png,492,184) - 查看延迟 - 看 avg 和 min/max - 抖动 - RTT 的波动程度 - stddev(标准差) 大,说明网络队列/无线/拥塞波动明显 - 丢包 - packet loss > 0 说明有包没有收到回包 - tracerout/mtr 的原理 - traceroute:用 TTL 把路径探测出来 - IP 包里有 TTL,每过一跳路由器把 TTL - 1 - 当 TTL 变为 0,路由器丢包并回 ICMP Time Exceeded - traceroute 依次发 TTL=1,2,3 的探测包 - 可以看到每一跳的地址与该跳的 RTT - 每一跳显式的 RTT 是你->该跳->你的往返,不是单程,也不是该跳->下一跳的延迟 - mtr:traceroute + ping 的结合 - mtr 会对每一跳持续发探测包并统计 - 丢包率 - 最快/平均/最慢 RTT - 抖动 - mtr 更适合定位抖动/偶发丢包/拥塞点 - 用 traceroute 看路径与绕路 -