Redis ZSet 底层数据结构实验

实验元数据 (Meta Data) 实验编号/标题:Redis set 底层数据结构实验 ...

二月 22, 2026

冥想 SOP

目标 得到充分的休息 步骤 环境与姿态准备 安全屋:不会有人打扰 物理隔绝:手机开启免打扰 舒适坐姿:脊柱挺直,不要僵硬,想象有一根绳子在头顶轻轻拽,手部自然垂放在大腿上,掌心朝上 开启仪式 定个闹钟:选一个温和的铃声 深呼吸 3 次:用鼻子吸气,感觉腹部隆起,用嘴呼气,想象身体像一个漏气的皮球 轻轻闭眼:低垂眼帘,视线虚焦 寻找锚点 感受气息:注意空气经过鼻尖时的清凉感,或是胸腔起伏的节奏 不要控制呼吸:观察它 默念计数:吸气时心里默念 1,呼气时默念 2,数到 10 再循环 捕捉并回归 察觉:发现自己在想某个工作时,不要沮丧 标记:心里轻轻对自己说:“在想事情“。 温柔回归:把注意力重新带回到呼吸 温和收尾 重拾感知:慢慢感受身体接触椅子的压力,听听周围声音 微动:动动手指、脚趾、扭脖子 睁眼:给自己 30s 适应光线,带着平静进入下一项活动 备注 不要强求平静: 有时候坐下来发现脑子更乱了,这很正常。看到乱,本身就是进步。 时间点的选择: 晨起(定调一天)或睡前(辅助入睡)效果最好。 利用工具: 如果初期觉得静不下心,可以尝试使用 Headspace、Calm 或国内的 潮汐 等 App 进行引导式冥想。

二月 22, 2026

Java IO

IO 模型与概念框架 - IO 模型与概念框架 - 四个基本概念 - 阻塞 vs 非阻塞 - 定义:一次调用在数据未就绪时是否立即返回 - 阻塞:调用线程进入等待,直到条件满足才返回,例如:InputStream.read(),数据没到就卡住 - 非阻塞:调用立即返回,不等数据 - 读:没数据时返回 0/-1 或抛特定异常 - 写:写不完就写一部分,返回已写字节数 - JavaNIO - SocketChannel.configureBlocking(false) 之后,read() 不会阻塞,可能返回 0 - 阻塞非阻塞指的就是线程是否在这次调用上被卡主 - 同步 vs 异步 - 定义:结果是由谁来完成并通知 - 同步:调用负责发起+等待/轮询+拿结果,即使是非阻塞轮询,仍然是同步,因为结果获取的责任方在调用方 - 异步:调用方发起后立即返回,后续由系统/框架在操作完成时回调/事件通知取结果,调用方不需要自己轮询等待结果 - JavaNIO - AsynchronousSocketChannel + CompletionHandler 是典型的 AIO 异步模型:完成时回调你 - 四象限组合 - 同步阻塞:BIO(经典 read() 卡住直到有数据) - 同步非阻塞:NIO 非阻塞 read() + 你自己轮询/配合 Selector 处理就绪事件,本质仍然是调用方驱动流程 - 异步非阻塞:AIO(回调通知你“读完了/写完了“) - 异步阻塞:理论上存在,比如发起异步后又 get() 阻塞等待,但是工程上不这么用 - BIO/NIO/AIO 的准确定义 - BIO(Blocking IO) - API 代表:ServerSocket/Socket、InputStream/OutputStream - 特征:accept()/read()/write() 默认阻塞 - 典型服务端模型:1 连接 1 线程(或线程池+阻塞读写) - 痛点: - 连接数上来后线程数膨胀:上下文切换、栈内存、调度开销 - 大量线程处于阻塞态,浪费资源 - NIO(Non-blocking I/O + Multiplexing) - API 代表:Channel/Buffer/Selector - 关键点:非阻塞 Channel + Selector 多路复用 - 本质:少量线程监听大量连接的就绪事件,再去执行非阻塞读写 - 价值: - 连接多时不需要等比例增加线程 - 事件驱动,提升并发连接处理能力 - NIO 三件套 - Channel:数据容器,可读可写 - Buffer:数据容器 - Selector:事件分发器 - AIO(Asynchronous I/O) - API 代表 AsynchronousServerSocketChannel/AsynchronousSocketChannel - 特征:发起读写后立即返回,完成后通过 CompletionHandler 回调通知结果 - 工程事实:Java AIO 在不同 OS 上底层支持与效果不同;很多高性能网络主流仍然是 Netty(基于 NIO 的 Reactor + 工程化) - 多路复用(IO Multiplexing) - 核心:一个线程可以等待多个 fd 的事件 - 没有多路复用:要等待某个 socket 可读,只能在这个 socket 上阻塞 read - 有多路复用:线程阻塞在 selector/poll/epoll_wait 上,内核告诉你那些 fd 可读/可写,再去处理 - Java NIO 的 Selector.select()=在做等事件,底层映射到 OS 的 select/poll/epoll/kqueue - Reactor 模式(NIO 常见线程模型) - Reactor:事件循环(event loop) + 分发(dispatch) - 流程 - selector.select() 事件 - 拿到 selected keys - 根据事件类型调用对于 handler - 单 Reactor 单线程:简单,适合低负载 - 主从 Reactor(boss/worker):boss 负责 accept,worker 负责读写 Java BIO - Java BIO - IO 流体系 - 两大抽象 - 字节流:InputStream/OutputStream - 面向原始字节 - 字符流:Reader/Writer - 面向字符,内部会涉及字符集解码/编码 - 涉及文本语义/字符集,就优先用 Reader/Writer 或者用 InputStream + 明确 Charset 解码;处理二进制就用 InputStream/OutputStream - 节点流 vs 处理流 - 节点流:直接连接数据源/目的地 - FileInputStream、FileOutputStream、Socket.getInputStream - 处理流:包装节点流,增强能力(缓冲、转码、数据类型、对象序列化、压缩、校验) - BufferedInputStream、InputStreamReader、DataInputStream、ObjectInputStream - 装饰器模式:不改原类,通过包装叠加功能 - Buffered 缓冲为什么快 - 不加缓冲为什么慢 - 许多 read()/write() 最终都会调用 OS 层的 read(2)/write(2) 系统调用,系统开销不小 - 不缓冲:每读 1KB 就要系统调用一次,次数巨大 - 加缓冲:一次系统调用读 8KB/64KB 放到用户态缓冲区,后续小读都在内存中完成,降低系统调用次数 - BufferedInputStream/BufferedOutputStream - BufferedInputStream:内部有 byte[] buffer(默认 8KB),先批量从底层读,再按需给你 - BufferedOutputStream:先写入 buffer,满了再 flush 到底层,减少了 write 的系统调用次数 - flush 是什么 - 把用户缓冲区里的数据推下去 - flush() != 一定落盘 - 字符集与桥接流 - InputStreamReader/OutputStreamWriter - InputStreamReader:字节 -> 字符(解码),需要 Charset - OutputStreamWriter:字符 -> 字节(编码),需要 Charset - 典型用法 - 读文本 - BufferedReader br = new BufferedReader(new InputStreamReader(new FileInputStream(f), UTF_8)) - 写文本 - BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(f), UTF_8)) - 为什么不能随便 new String(byte[]) - 不指定 charset,会走平台默认编码,导致乱码 - 常见功能处理流 - DataInputStream/DataOutputStream(理解二进制协议) - 能按固定类型读写:writeInt/readInt,writeLong/readLong - 用途:自定义二进制协议、文件格式 - DataInputStream 读写的是 Java 规定格式,跨语言要慎重,更常见的跨语言协议是 protobuf、msgpack 等 - ObjectInputStream/ObjectOutputStream - Java 原生序列化:实现 Serializable - 问题:性能一般、版本兼容差、安全风险 - 不建议用于 RPC/外部输入,内部常见简单可用,生产用 protobuf/kryo/json 等 - PrintWriter/PrintStream(输出遍历但注意编码/flush) - System.out 是 PrintStream - PrintWriter 可选 autoFlush(遇到 println/printf/format 自动 flush) - PrintWriter 默认吞异常 - 资源管理与异常 - try-with-resources - 自动 close,且 close 顺序与声明顺序相反 - close 过程中异常会作为 suppressed 异常附加到主屏幕 - close vs flush - close vs flush - close() 会隐式 flush(),不能依赖进程崩溃前的 close - 网络场景:写完重要数据要主动 flush 或者使用协议层 ack。 - BIO 与网络 IO:Socket 阻塞读写的关键点 - 阻塞读到集中返回情况 - InputStream.read(byte[]) - 返回 >0:读到的字节数 - 返回 -1:对端关闭输出 - 可能一直阻塞:对端不发数据也不关 - 半包/粘包在 BIO 也存在 - BIO 只是阻塞,TCP 仍然是字节流,read() 不保证一条业务消息完整到达 NIO 基础 Buffer/Channel - NIO 基础 - NIO 的核心心智模型:Buffer+Channel - 在 NIO 中,数据永远在 Buffer 里转 - 读:Channel.read(ByteBuffer) 把数据写进 buffer - 写:Channel.write(ByteBuffer) 从 buffer 读出数据写到 channel - Channel 是管道,Buffer 是货箱,NIO 必须管理 buffer 状态 - ByteBuffer 属性与状态 - 关键属性 - capacity:容量,创建后固定 - position:当前位置(下次读/写从这里开始) - limit:边界(读/写不能超过它) - mark:标记位置(可选) - remaing():limit - position - 两种模式 - 写模式:准备往 buffer 里放数据(例如从 channel read 进来) - position 向后走,limit 通常等于 capacity - 读模式:准备从 buffer 里取数据(例如写到 channel 或解析协议) - position 向后走,limit 表示可读数据的末尾 - 三个最重要的方法 - flip:写模式 -> 读模式 - 把 limit 设置为当前 position,再把 position 设置为 0 - 我写完了,现在从头读刚写的数据 - clear:读完后重置为写模式(不清数据,只重置指针) - position = 0; limit = capacity; mark = -1 - 不关心旧数据了,直接当成空 buffer 继续写 - compact:读了一部分,保留剩余未读数据,并切换为写模式 - 把未读数据 [position,limit) 移动到开头 - position=remaining;limit=capacity - 我还剩一点没处理,先挪到前面,下次继续在后面写新数据 - HeapBuffer vs DirectBuffer - Heap ByteBuffer(堆内) - ByteBuffer.allocate(n) - 好处:分配快,受 JVM GC 管理,排查简单 - 坏处:做网络 IO 时需要把数据从堆复制到内核缓冲区 - Direct ByteBuffer(堆外) - ByteBuffer.allocate(n) - 好处:减少一次拷贝,适合大块网络 IO,频繁 IO。 - 坏处 - 分配/释放更贵 - 受 MaxDirectMemorySize 限制 - 回收依赖 Cleaner/GC 时机,不是=null 就立刻释放 - Channel:FileChannel 与 SocketChannel - FileChannel - 来源 - FileInputStream.getChannel() - FileOutputStream.getChannel() - 常用能力 - read(ByteBuffer)/write(ByteBuffer) - position()/position(long):随机读写位置 - size() 文件大小 - force(boolean metaData):把修改刷到存储设备 - transferTo/transferFrom:为后续零拷贝铺垫 - SocketChannel - 支持 configureBlocking(false) NIO 非阻塞基石 - NIO 文件读写的标准流程 - FileChannel 读取 - buffer.clear() 进入写模式 - int n = channel.read(buffer) 把数据写进 buffer - buffer.flip() 进入读模式 - 从 buffer 读取处理 - 循环直到 n == -1 NIO 进阶 Selector/多路复用 - NIO 进阶 Selector/多路复用 - Selector/SelectionKey/Channel:三者关系与生命周期 - 三件套职责 - Selector:事件复用器,阻塞在 select() 等待就绪事件,把就绪的 key 放进 selectedKeys 集合 - SelectableChannel:可注册到 Selector 的通道 - SelectionKey:一次注册的凭证/句柄,包含 - channel():对于 channel - selector():归属 selector - interestOps():你关心那些事件 - readyOps():当前就绪了哪些事件 - attachment():绑定业务状态 - 注册与事件的基本规则 - channel.register(selector, OP_READ, attachment):把 channel 注册给 selector,并声明兴趣事件 - interestOps 是订阅,可以动态改 - readyOps 是本轮 select 的结果,不能直接改 - key 的取消与资源释放 - key.cancel():从 selector 的 key 集合中移除 - 移动要同时 close channel:channel.close(),否则 fd 泄露 - 被 cancel 的 key 不会立刻从所有集合消失,通常在下一次 select 时清理 - 多路复用:就绪事件语义 - 就绪不是完成 - OP_READ 就绪:表示现在读不阻塞,不保证读到一条完整的业务消息 - OP_WRITE 就绪:表示现在写更可能成功,不保证一次 write 写完你要写的所有数据 - 各个事件含义 - OP_ACCEPT:ServerSocketChannel 有新连接可以 accept - OP_CONNECT:客户端连接建立过程完成(非阻塞 connect) - OP_READ:SocketChannel 可读 - OP_WRITE:SocketChannel 可写(注意:很多时候“总是可写”) - Reactor 模式:从单线程到主从 Reactor - 单线程 Reactor - 一个线程做三件事 - selector.select() 等待事件 - 遍历 selectedKeys - 按事件类型分发处理:accept/read/write - 优点:简单 - 缺点:业务处理慢会阻塞整个事件循环 - 主从 Reactor(boss/worker) - boss:只负责 accept,把新 SocketChannel 分发到 worker 的 selector - worker:负责读写与协议解析、业务派发 Netty - Netty - 解决什么问题 - 原生 NIO 的痛点 - 需要自己管理:Selector 轮询、Key 生命周期、半包/粘包、写不完与 OP_WRITE 开关 - Buffer 管理复杂:flip/compact、输入输出缓冲复用、DirectBuffer 回收时机 - 性能与稳定性:Selector 空轮询、内存膨胀、背压、线程隔离 - Netty 提供的工程化能力 - 线程模型(EventLoopGroup + Reactor)成熟 - Pipeline/Handler 体系:解码、业务、编码职责分离 - ByteBuf:读写指针友好、池化、零拷贝视图、引用计数 - 编解码器、背压、写队列、水位线、内存泄露检测等工具链 - 线程模型 - EventLoop - EventLoop = 一个线程 + 一个 Selector + 一组 Channel - EventLoop 负责 - 处理 IO 事件 - 执行任务队列 - 同一个 Channel 的所有 Handler 回调默认都在通一个 EventLoop 线程执行,一致性,减少锁 - EventLoopGroup - bossGroup:负责 accept(ServerSocketChannel) - workerGroup:负责已建立连接的读写(SocketChannel) - 为什么少量线程能抗大量连接 - 事件驱动:线程主要阻塞在 epoll/kqueue 上,事件就绪才处理 - 连接多不会导致线程线性增长 - Channel、Future、Promise:异步编程语义 - ChannelFuture - Netty 大量 API 返回 ChannelFuture,比如 bind、connect、writeAndFlush,表示操作已发起,完成会通知 - future.addListener(...):完成回调(推荐) - future.sync():阻塞等待 - Promise - 可写的 future,用于自己控制完成/失败 - Pipeline/Handler:职责分离 - Pipeline 是什么 - 每个 Channel 都有一条 ChannelPipeline - Inbound(入站):数据从 socket 进来 -> 解码 -> 业务处理 - Outbound(出站):业务写出 -> 编码 -> flush 到 socket - Handler 分类 - ChannelInboundHandler:处理入站事件(channelRead、channelActive…) - ChannelOutboundHandler:处理出站事件(write、flush…) - ChannelDuplexHandler:双向 - SimpleChannelInboundHandler<T>:自动释放入站消息(配合 ByteBuf 引用计数要理解) - ByteBuf - 为什么 ByteBuf 比 ByteBuffer 友好 - 两个指针:readerIndex/writerIndex - 不需要 flip:写完读直接通过读指针移动 - 支持切片/复制语义清晰 - 三种常见 ByteBuf - Heap:堆内 - Direct:堆外 - CompositeByteBuf:多个 buffer 组合成一个逻辑 buffer(减少拷贝) select/poll/epoll - select/poll/epoll - 简介 - 都是操作系统提供的 I/O 多路复用机制 - 让一个线程同时等待很多文件描述符的可读/可写/异常事件,然后再去对就绪的 fd 做 read/write - 术语 - fd:内核里对打开的文件/套接字等资源的编号。网络 socket 也是 fd。 - 就绪:表示现在读/写大概率不会阻塞,不等于你一次就能读到完整业务包/一次写完所有数据 - select:最老牌的多路复用 - 工作方式:把三组 fd(读/写/异常) 集合传给内核,内核扫描那些就绪,然后返回就绪集合。 - 特点: - fd 数量上限:select 受到 FD_SETSIZE 限制,常见 1024 - 每次调用要拷贝与扫描:用户态要把位图/集合传入内核,内核要线性扫描 - 集合会被修改:返回时就绪集合是被改过的,下次调用要重新准备集合 - 适用: - fd 少,跨平台,简单场景。 - poll:对 select 的数组版改良 - 工作方式:把 fd 列表放在一个 pollfd[] 数组传递给内核,内核扫描并在数组里标记那些就绪。 - 相比 select 改进 - 不收 FD_SETSIZE 位图限制 - 表达更灵活 - 缺点 - 仍然是 O(n) 扫描 - 每次调用仍然需要把数组从用户态传给内核 - epoll:为海量连接而生 - epoll 是 Linux 的高性能事件通知机制,核心思想是:把关注列表留在内核里,避免每次把大集合传来传去,返回就绪列表而不是让自己全量扫描 - 关键点 - 关注列表在内核维护:避免每轮 select/poll 大集合拷贝+扫描 - epoll_wait 返回的是就绪事件列表,只处理这些 fd,更接近 O(就绪数) - LT vs ET 触发模式 - LT (Level-Triggered 水平触发,默认) - 只要还可读/可写,就会持续通知你 - 编程更简单:读不完下次还会通知你 - ET (Edge-Triggered,边缘触发) - 只有从不可读 -> 可读的边缘变化才通知一次 - 必须把数据一次性读到不能再读,否则可能丢通知导致卡住 - 性能更高,但对实现要求更严格(非阻塞+循环读/写)

二月 21, 2026

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 计数器

二月 21, 2026

搜索引擎复习

信息检索核心原理 - 信息检索 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;避免原地更新导致随机写/锁竞争 - 崩溃恢复快

二月 21, 2026

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(优先级队列) - 无界优先级堆 - 高优先级先出队

二月 20, 2026

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 级授权

二月 20, 2026

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

二月 19, 2026

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 日志复制,从而提高一致性并提供自动选主 - 代价:写路径更重,延迟更高,但一致性更强、切换更自动化

二月 18, 2026