Redis 主要提供 RDB(快照持久化) 和 AOF(日志持久化) 以防止数据丢失。
1️⃣ RDB(Redis Database Snapshot)
-
机制:定期快照(Snapshot)存储整个 Redis 内存数据 到磁盘。
-
优点: ✅ 适合 大规模数据恢复(冷启动场景)。 ✅ 影响主线程较小,因为快照由子进程 fork 处理。
-
缺点: ❌ 可能导致数据丢失,因为 RDB 是定期快照,崩溃时会丢失最近的修改。
-
触发方式
:
SAVE(同步,阻塞)BGSAVE(异步,子进程)save 900 1(900秒内至少 1 次写入)
2️⃣ AOF(Append Only File)
-
机制:记录每次写操作(
SET、HSET、INCR)到日志文件,定期重写优化。 -
优点: ✅ 数据更安全,默认
fsync every second,最多丢失 1 秒数据。 -
缺点: ❌ AOF 文件更大,恢复速度较慢。 ❌ 写入操作更多,影响性能。
-
配置方式
:
appendfsync always(每次写入同步,最安全,最慢)appendfsync everysec(默认,每秒同步)appendfsync no(由 OS 决定同步时机)
跳表的操作
(1) 查找(O(log n))
假设要查找 20:
- 从最高层开始,沿着索引层查找,直到找到最接近但不大于 20 的节点。
- 降级到下一层,继续从该节点往后找。
- 重复步骤 1-2,直到到底层。
示例(查找 20):
lessCopyEditLevel 4: [1]---------------------->[50] (跳过)
Level 3: [1]--------->[10]-------->[50] (跳过)
Level 2: [1]-->[5]-->[10]-->[20]-->[50] (找到)
Level 1: [1] -> [2] -> [3] -> [5] -> [10] -> [15] -> [20] -> [50] (找到)
最终在 Level 2 找到 20,比逐个遍历快得多。
(2) 插入(O(log n))
插入元素 x 时:
- 查找插入位置(与查找过程类似)。
- 随机决定
x的层级(采用抛硬币策略)。 - 从底层到对应层级插入
x,并调整索引。
层数如何决定?
采用 随机概率,如果 p = 0.5,则:
50%的概率只出现在 底层。25%的概率出现在 两层。12.5%的概率出现在 三层。
这样保证跳表的结构接近平衡二叉树。
(3) 删除(O(log n))
- 查找
x的所有层级位置(与查找过程类似)。 - 从每层链表删除
x,如果某层的索引变空,则删除该层。
示例(删除 20):
lessCopyEditLevel 3: [1]--------->[10]-------->[50]
Level 2: [1]-->[5]-->[10]--------->[50] (删除 20)
Level 1: [1]->[2]->[3]->[5]->[10]->[15]------->[50] (删除 20)
悲观锁(Pessimistic Lock)
假设并发冲突是常态,因此在访问数据前,会强制加锁,防止其他事务修改数据,从而保证数据的安全性。
-
适用于高并发写入时,防止数据被修改(如银行转账)。
-
实现方式:
- 数据库锁(如 S 锁和 X 锁)
- 分布式锁(如 Redis 分布式锁、Zookeeper 分布式锁)
- synchronized 关键字 或 ReentrantLock
示例:
javaCopyEditsynchronized(lock) { // 访问共享资源 }
乐观锁(Optimistic Lock)
假设并发冲突很少,因此不加锁,而是通过版本号(Version) 或 CAS(Compare And Swap)机制来保证数据的正确性。
-
适用于读多写少的场景,比如商品库存扣减、用户数据修改等。
-
实现方式:
- 版本号机制(数据库
version字段) - CAS(Compare And Swap) 机制(如 Java
AtomicInteger)
示例(版本号)
sqlCopyEditUPDATE product SET stock = stock - 1, version = version + 1 WHERE id = 1 AND version = 当前版本;如果
version发生变化,表示数据已被修改,更新失败,客户端需要重试。 - 版本号机制(数据库
QUIC(Quick UDP Internet Connections) 是 Google 开发的一种基于 UDP 的传输协议,旨在提高网络传输效率,并解决 TCP 的一些性能问题,特别是在高延迟和丢包环境下。
🔹 QUIC 是 “TCP + TLS + HTTP/2” 的结合体,但它不基于 TCP,而是基于 UDP,并在应用层实现了:
- 连接多路复用
- 0-RTT 连接建立
- 内置加密(TLS 1.3)
- 更好的丢包处理
- 避免队头阻塞
消息队列(Message Queue, MQ)详解
消息队列(MQ)是一种用于异步解耦和流量削峰的架构模式,广泛应用于高并发系统、微服务架构、日志处理、事件驱动系统等。
1. 为什么需要消息队列?
(1) 解耦(Decoupling)
在传统的同步调用中,服务 A 调用服务 B,两者强耦合:
A ---> B
如果 B 发生故障,A 也会受影响。
使用消息队列后,A 只需要发送消息到 MQ,B 异步消费:
A ---> [MQ] ---> B
✅ A 和 B 完全解耦,即使 B 挂掉,消息仍然保留在 MQ 中。
(2) 削峰(Traffic Shaping)
假设秒杀场景:
- 高并发时,数据库
DB承受瞬间巨大流量,导致崩溃。 - MQ 作为缓冲区,让请求排队,均匀写入 DB:
用户请求 ---> [MQ] ---> 处理服务 ---> DB
✅ 削峰填谷,避免 DB 瞬间过载。
(3) 异步处理
有些任务 不需要同步等待结果(如发送邮件、日志存储),可以异步执行:
A(下单) ---> [MQ] ---> 邮件服务(异步消费)
✅ 提升系统响应速度。
2. 消息队列的核心概念
| 概念 | 描述 |
|---|---|
| Producer(生产者) | 发送消息到 MQ |
| Broker(消息中间件) | 负责存储、路由和投递消息 |
| Queue / Topic | 存放消息的地方(点对点 / 发布订阅) |
| Consumer(消费者) | 订阅 MQ 并消费消息 |
| 消息确认(ACK) | 消费者确认收到消息,MQ 才会删除消息 |
| 消息重试(Retry) | 失败的消息可重新投递 |
| 消息顺序(FIFO) | 按发送顺序消费消息 |
| 消息持久化 | 确保 MQ 崩溃时数据不丢失 |
3. 消息队列的常见模型
(1) 点对点(P2P,Queue)
- 一个生产者,一个消费者(或者多个消费者竞争消费消息)。
- 消息被消费后即删除,不会被其他消费者再次消费。
Producer ---> [Queue] ---> Consumer
✅ 适用于任务队列,如订单处理、秒杀。
(2) 发布订阅(Pub/Sub,Topic)
- 一个生产者,多个消费者。
- 每个消费者都会收到相同的消息。
Producer ---> [Topic] ---> Consumer1
---> Consumer2
---> Consumer3
✅ 适用于:
- 日志存储(多个系统订阅日志)
- 事件驱动架构(如用户注册后触发多个操作)
4. 常见的消息队列
| MQ | 特点 | 适用场景 |
|---|---|---|
| RabbitMQ | 基于 AMQP,支持消息确认、延迟队列,适合事务性场景 | 订单系统、事务消息 |
| Kafka | 高吞吐、分布式、持久化,适用于日志流、事件流 | 日志收集、实时数据分析 |
| RocketMQ | 高可用、事务支持,适合大规模金融业务 | 支付系统、秒杀 |
| ActiveMQ | 轻量级 MQ,兼容 JMS,适合小型应用 | 传统企业级应用 |
| Pulsar | 分布式、支持多租户,适用于超大规模数据流 | 大规模事件流 |
5. MQ 可靠性保障
(1) 消息丢失问题
如何保证 MQ **“不丢消息”? 💡 解决方案: ✅ 生产者保证:
- 消息确认(ACK):确保 MQ 收到消息,否则重试。
- 事务消息:RabbitMQ、RocketMQ 支持本地事务+回滚。
✅ Broker(MQ)保证:
- 消息持久化(Kafka、RocketMQ 支持 磁盘存储)。
- 多副本机制(Kafka 支持 Leader-Follower 复制)。
✅ 消费者保证:
- 手动 ACK 机制:消费完成后手动确认,否则消息重试。
(2) 消息重复消费
MQ 可能发生“消息重复消费”(如消费者超时未 ACK,被重新投递)。 💡 解决方案: ✅ 幂等性设计:
- 使用 全局唯一 ID,避免重复执行。
- Redis 去重(存储已处理 ID)。
生产者为每条消息生成全局唯一 messageId;消费者在处理前先去 Redis 占坑。 如果占坑成功=第一次见到 → 允许执行业务;占坑失败=重复 → 直接 ACK 丢弃。
Redis Key 设计
mq:processed:{messageId} → 1(设置一个合理 TTL,如 2~7 天 ≥ MQ 最大重试/保留期)
(3) 消息积压
如果消费者消费太慢,消息堆积在 MQ: 💡 解决方案: ✅ 增加消费者数量(提高消费速率)。 ✅ 消息分区(Kafka),让多个消费者并行消费。
6. MQ 在实际业务中的应用
(1) 秒杀系统
✅ MQ 削峰限流,避免数据库崩溃:
用户请求 ---> [MQ] ---> 订单服务 ---> 数据库
(2) 订单处理
✅ MQ 解耦,订单创建后异步通知:
下单 ---> [MQ] ---> 库存扣减
---> 物流通知
---> 发票系统
(3) 分布式事务
✅ 基于 RocketMQ 的事务消息
- 生产者发送事务消息(Pending)。
- 本地事务执行(扣库存)。
- MQ 确认事务成功,消息才正式投递。
7. MQ 面试常见问题
🔹 MQ 和数据库之间如何保证数据一致性?
✅ 方案 1:本地事务 + MQ
- 先写数据库(本地事务),再发送 MQ 消息。
- 问题:如果 MQ 发送失败,如何回滚?
- 优化:使用 RocketMQ 事务消息,确保最终一致性。
✅ 方案 2:MQ 先发,业务后执行
- 先发送消息到 MQ,再执行业务逻辑(如写数据库)。
- 问题:如果业务失败?
- 优化:使用 MQ 事务消息 或 消息补偿机制。
🔹 Kafka 为什么比 RabbitMQ 吞吐量高?
✅ Kafka 采用 顺序写磁盘 + PageCache 读写(比随机写快 10 倍)。 ✅ RabbitMQ 采用 传统 AMQP 协议,基于 Erlang,消息必须存入内存/磁盘,影响性能。
8. 总结
✅ 消息队列用于解耦、削峰、异步处理。 ✅ 常见 MQ:RabbitMQ(事务)、Kafka(高吞吐)、RocketMQ(事务+金融)。 ✅ 保证消息可靠性(幂等性、事务消息、ACK)。 ✅ 优化高并发场景(秒杀、订单、日志处理)。
💡 面试时重点关注 MQ 的应用、消息一致性、消息丢失、重复消费等问题! 🚀
InnoDB 的 B+ Tree 实现方式:
- 每个表的 主键索引 是一个 聚簇(clustered index)
- 数据存储在 B+ Tree 的叶子节点上
- 叶子节点中不仅有主键,还有整行数据
聚簇索引是一种特殊的索引,它决定了数据在表中的物理存储顺序。 换句话说:
- 表中的数据 按照聚簇索引的顺序存放;
- 索引和数据“长在一起”,索引叶子节点存的就是实际的数据行。
这和普通索引(非聚簇索引)不同,普通索引的叶子节点存放的是“指向数据行的地址(指针/主键)”。
1. 所有数据都在叶子节点,非叶子节点只做索引
- 非叶节点仅存 key,不存 value,单个节点可以容纳更多关键字
- 树高更低,磁盘 I/O 更少
2. 叶子节点天然有序 + 双向链表
- B+ 树叶子节点是按大小排序的链表,支持:
- 范围查询
- 区间扫描(
WHERE x BETWEEN 10 AND 100) - 排序操作(
ORDER BY)
- 这是 哈希索引做不到的
3. 更好的磁盘局部性(Cache Friendliness)
- B+ 树的一个节点可以恰好等于一个页(Page)大小(如16KB)
- 一个节点中可存多个 key,一次读入一个节点可以比较多个 key
对比:
- 红黑树一个节点只存一个 key,导致节点数量多,树高更高,磁盘访问更频繁。
4. 查询效率稳定
- 所有数据都在叶子节点,查找路径长度固定,便于优化缓存
- 即使更新/删除也不会大幅度破坏树的平衡性
B+树是一种多路搜索树(多叉平衡树),它的特点包括:
- 每个节点最多有
m个子节点(m阶 B+树) - 所有叶子节点高度一致,数据都存储在叶子节点
- 非叶子节点只存索引,不存数据
B+ 树在 插入或删除 时可能导致以下变化:
1. 插入
- 如果目标页(节点)有空间:直接插入,树结构不变
- 如果页满了:分裂节点(Split),只影响当前页和父节点,局部变化
2. 删除
- 如果删除后页还能维持最小占用:直接删除,不动结构
- 如果删除导致页“太空”(小于一半):
- 尝试向兄弟节点借数据
- 如果兄弟也太小:合并节点(Merge)
- 最多会调整当前页及其父节点,仍是局部影响
🔍 一、B 树 vs B+ 树 的核心区别
| 特性 | B 树 | ✅ B+ 树 |
|---|---|---|
| 数据存储位置 | 数据存储在所有节点(叶子 & 非叶子)中 | 数据只存在叶子节点 |
| 非叶子节点是否存数据 | ✅ 是(key + value) | ❌ 否(只存 key + 指针) |
| 叶子节点是否链表连接 | ❌ 否 | ✅ 是(有序双向链表) |
| 范围查询性能 | ❌ 差(需中序遍历整棵树) | ✅ 高效(直接顺序遍历叶子) |
| 节点可存 key 数量 | 少(因为含有 value) | 多(只有 key,能放更多 key) |
| 树的高度 | 较高(I/O 多) | 更矮(I/O 更少) |
| 查找路径是否统一 | ❌ 不统一(可能在中间节点命中) | ✅ 统一(都走到叶子) |
磁盘访问友好:每次 I/O 更值钱
数据库核心瓶颈是 磁盘 I/O(即页读写):
- B 树:节点数据量少,树高更高 → 访问磁盘次数更多
- B+ 树:非叶节点只存 key,一个节点可以存 更多 key
- → 树更矮
- → 查找路径更短
- → I/O 次数更少
👉 每次磁盘访问能用得更“值”,性能高
查找路径更稳定统一
- B 树:查找可能在叶子,也可能在中间节点
- → 缓存命中复杂,优化困难
- B+ 树:所有查找都到叶子节点终止
- → 查找路径一致、稳定,便于 统一优化 和 预读缓存
数据更容易批量读入内存
由于叶子节点是链表有序排列:
- B+ 树支持顺序预读,可以一次性将多个连续页加载入内存
- 利于处理 ORDER BY、范围查询、分页等操作
HashMap 是 Java 中最常用的集合之一,其底层结构结合了 数组(Array) 和 链表(LinkedList),从 Java 8 开始还引入了 红黑树(Red-Black Tree) 来优化性能。
计算 hash 值:
int hash = hash(key); // 通过扰动函数计算 hash
定位数组下标:
index = (table.length - 1) & hash; // 取模(效率高)
处理冲突:
- 如果该位置为空,直接放入;
- 如果已存在节点(哈希冲突):
- 使用
equals()判断 key 是否相同,相同则覆盖; - 否则,添加到链表尾部或插入到红黑树中。
- 使用
链表转红黑树的条件:
- 链表长度 ≥ 8 且数组长度 ≥ 64 时,链表 → 红黑树;
- 反之,如果树节点数量降到 6 以下,则红黑树 → 链表。
✅ 1. 缓存穿透(Penetration)
📌 定义:
用户请求的数据 在缓存中查不到,数据库中也没有,请求不断打到数据库上。
| 方案 | 说明 |
|---|---|
| 缓存空值 | 把数据库查不到的 key 缓存为 null,并设置短过期时间 |
| 参数校验 | 比如 ID < 0 就直接拦截 |
| 布隆过滤器 | 使用布隆过滤器拦截不存在的 key(空间效率高) |
| 限流器 | 限制频繁请求同一 key 的速率 |
布隆过滤器(Bloom Filter)是一种非常经典的概率型数据结构,常用于集合成员测试(判断一个元素是否在集合中)。
🌱 基本定义
- 目标:快速判断“某个元素是否存在”。
- 特点:
- 可能会误判存在(False Positive):说“在集合里”但可能其实不在。
- 绝不会误判不存在(No False Negative):如果说“不在”,那一定真的不在。
换句话说: 👉 “不在”=100% 准确; 👉 “在”=有一定概率错误。
⚙️ 它是怎么工作的?
- 准备一个长度为 m 位的位数组(初始全是 0)。
- 定义 k 个哈希函数。
- 当要插入元素时:
- 用这 k 个哈希函数分别计算位置,把位数组相应位置标记为 1。
- 当要查询元素时:
- 同样用这 k 个哈希函数算位置,检查对应的位是否都是 1。
- 如果有某个位是 0 → 一定不在集合。
- 如果全部是 1 → 可能在集合(但可能是多个元素“凑巧”碰撞导致的误判)。
✅ 2. 缓存击穿(Breakdown)
📌 定义:
某个热点 key 失效,此时大量请求打到数据库上。
| 方案 | 说明 |
|---|---|
| 加互斥锁 | 缓存失效时只允许一个线程去加载数据,其余等待或快速失败 |
| 永不过期 + 后台异步更新 | 不设置 TTL,定时异步刷新缓存 |
| 热点预热/提前续约 | 提前一段时间自动续期,避免真正过期 |
| 二级缓存(本地 + Redis) | 如 Guava 缓存 + Redis 结合,提升抵御能力 |
✅ 3. 缓存雪崩(Avalanche)
📌 定义:
大量 key 同时过期或 Redis 故障,导致请求全部打到数据库上。
| 方案 | 说明 |
|---|---|
| 过期时间加随机 | 避免大量 key 同时过期(如 ttl = 3600 + rand(0, 600)) |
| 热点数据提前续期 | 提前刷新热门 key 的 TTL |
| 多级缓存/降级策略 | 降级为本地缓存、返回默认值等 |
| Redis 集群 + 容灾切换 | 降低单点故障风险,快速恢复缓存能力 |
✅ Redis 不会“立即”删除过期 key,而是通过“定时 + 惰性 + 定期”三种策略组合实现的。
✅ 1. 惰性删除(Lazy Deletion)【核心机制】
当客户端访问某个 key 时,Redis 会检查该 key 是否已过期,如果过期了就立刻删除。
📌 特点:
- 性能开销小,只有在访问时才检查
- 但缺点是:不会主动清除未访问的过期 key
✅ 2. 定期删除(Active Expire Cycle)
Redis 每隔一定时间(默认 100ms)随机抽取一批设置了过期时间的 key 进行检查和删除。
📌 特点:
- 通过采样 + 限时循环控制 CPU 占用
- 删除频率越高,Redis 删除过期 key 越及时
每个带过期时间的 key,会被存入一个“过期字典(expire dict)”中:
- 主字典(dict)保存 key-value 对
- 过期字典(expire dict)保存 key 的过期时间戳(以毫秒为单位)
🔍 当 Redis 执行任何操作如 GET/SET,先检查 expire dict 决定是否已过期。
synchronized 是 JVM 层面的原生锁机制,简单但功能有限;
Lock 是 Java API 提供的显示锁接口,更灵活、功能更强。
| 特性 | synchronized | Lock(如 ReentrantLock) |
|---|---|---|
| 属于 | JVM 层级(字节码中有 monitorenter 指令) |
Java 层 API |
| 底层结构 | 对象的 MarkWord + Monitor(重量锁) | AbstractQueuedSynchronizer (AQS) |
| 阻塞方式 | 进入 monitor,线程阻塞 | AQS 队列(基于 CAS + CLH 队列) |
| 性能优化策略 | 偏向锁、轻量锁、自旋锁等 | 自定义实现,多种可控策略 |
🔐 一、synchronized 的优化机制(JVM 层优化)
synchronized 在 JDK 1.6 以后性能大幅提升,主要得益于 JVM 实现的 “锁升级策略”:
🚦 偏向锁 → 轻量级锁 → 重量级锁
🟢 1. 偏向锁(Biased Lock)
✅ 场景:
- 单线程反复进入同步块(比如只有一个线程访问某个对象)
✅ 原理:
- 对象的头部(Mark Word)中记录线程 ID
- 下一次进入同步块时,如果还是同一个线程,无需再加锁(认为偏向该线程)
✅ 优点:
- 完全不加锁,无需 CAS 操作 → 性能几乎等于无锁
❌ 缺点:
- 如果其他线程来竞争,就需要升级为轻量级锁(产生成本)
🟡 2. 轻量级锁(自旋锁)
✅ 场景:
- 多线程访问,但不存在真正的并发竞争
✅ 原理:
- 每个线程会在进入
synchronized时尝试用 CAS 将对象头的锁信息改成指向自己的栈帧(Lock Record) - 如果 CAS 成功 → 获得锁
- 如果 CAS 失败 → 说明有竞争,进入自旋阶段(尝试一段时间后升级为重量级锁)
✅ 优点:
- 使用 CAS 操作避免线程阻塞(减少上下文切换)
- 自旋几次成功 → 快速拿到锁
🔴 3. 重量级锁(Monitor 锁)
✅ 场景:
- 多线程真正同时竞争锁,CAS + 自旋失败
✅ 原理:
- JVM 使用 Monitor(监视器锁) 实现,线程进入阻塞队列(WaitSet)
- 使用操作系统的 互斥量(mutex) 实现线程挂起、唤醒
❌ 缺点:
- 线程上下文切换代价大 → 性能最差89现场
CAS 是一种无锁(lock-free)机制,用于多线程并发下的安全数据更新,核心逻辑是:
👉 “如果内存中的值等于预期值,那么就将其更新为新值;否则什么也不做。”
❓原文:
每个线程会在进入
synchronized时尝试用 CAS 将对象头的锁信息改成指向自己的栈帧(Lock Record)
✅通俗解释:
👉 你想用会议室,就先试着把门上的牌子改成写你的名字(用 CAS)。 👉 这个“贴牌子”的过程是 原子操作:要么一瞬间贴上成功,要么完全失败。 👉 这个动作就是 CAS:如果门上的名字是空的,就让我贴;不是空的,我就失败。
❓原文:
如果 CAS 成功 → 获得锁
✅通俗解释:
👉 你成功把门上的牌子换成自己的名字,那就说明没人用,你进屋开会就行了(加锁成功)
❓原文:
如果 CAS 失败 → 说明有竞争,进入自旋阶段(尝试一段时间后升级为重量级锁)
✅通俗解释:
👉 如果你发现门上已经贴了别人的名字(锁被别人抢了),你不急着离开,而是先在门口等一等,看看他是不是很快就出来 👉 如果他很快出来了,你就马上贴上你的名字进去(自旋成功) 👉 如果等了几轮他还不出来,你就放弃等,去前台登记排号排队等通知(进入等待队列,升级为重量级锁)
✅ 所以“需要等谁就让谁 join”
- 想让 A 等 B:
在 A 里调用
threadB.join() - 想让 B 等 A:
在 B 里调用
threadA.join() - 想让主线程等子线程:
在
main()中调用threadX.join()
join() = “等你干完活我再干”
await() 会让线程 挂起等待,直到某个条件满足时,由其他线程通过“通知”唤醒它。
| 类别 | 作用 | 唤醒方式 |
|---|---|---|
CountDownLatch.await() |
等待计数器归零 | 调用 countDown(),直到计数为 0 → 唤醒所有 await 的线程 |
Condition.await() |
等待某个条件 | 其他线程调用 condition.signal() 或 signalAll() |
CyclicBarrier.await() |
所有线程都到达屏障 | 到达设定线程数后 → 自动唤醒全部 |
Future.get()(内部也使用 await()) |
等待任务完成 | 任务执行完,设置结果 → 唤醒调用者 |
String(字符串类型)
- 是 Redis 中最基础、最常用的数据结构
- 实际上不仅仅能存字符串,还能存数字、二进制、JSON
SET name "Alice"
GET name # "Alice"
INCR count # 自增操作(数值)
APPEND name " Smith" # 字符串追加
key → value
"name" → "Alice"
"count" → "100"
"token:uid:1" → "abc123xyz"
List(列表)
- 有序的字符串列表,支持从头部/尾部插入和弹出
- 类似于 Java 的 LinkedList,双端队列
LPUSH queue task1
RPUSH queue task2
LPOP queue # 出队
RPOP queue # 出栈
Set(集合)
- 无序、去重的字符串集合
- 自动去重、支持集合运算(交、并、差)
SADD tags "java"
SADD tags "redis"
SISMEMBER tags "java" # 判断是否存在
SMEMBERS tags # 获取所有成员
Hash(哈希)
- 类似于对象或字典(Map)结构
- 一个 key 下面可以有多个 field-value 对
HSET user:1001 name "Alice"
HSET user:1001 age 23
HGET user:1001 name # "Alice"
HGETALL user:1001
key → hash表(field-value对)
"user:1001" → {
name: "Alice",
age: "24",
email: "alice@example.com"
}
ZSet(有序集合)
- 每个元素关联一个分数(score),元素按分数排序
- 元素唯一,分数可重复
ZADD ranking 100 Tom
ZADD ranking 80 Alice
ZRANGE ranking 0 -1 # 按分数升序返回所有人
ZREVRANGE ranking 0 2
数据结构组成
- 哈希表(dict)
- 负责存储 成员(member) → 分值(score) 的映射。
- 用来快速判断某个元素是否存在,以及拿到它对应的分值。
- 查找复杂度是 O(1)。
- 跳表(skiplist)
- 负责按照 分值(score) 以及 成员名(member,作为 tie-breaker) 排序。
- 用来支持范围查找、排名(rank)、按顺序遍历等操作。
- 插入、删除、范围查找复杂度都是 O(logN)。
Redis 设计得很巧妙: 👉 插入一个元素时,会同时写进 哈希表(方便查找)和 跳表(方便排序和范围操作)。 👉 删除时,也会在这两个结构里都删掉。
1. HashMap 扩容时链表转红黑树的阈值为什么是 8?退化为 6 的原因?
- 阈值是 8:
当链表长度达到 8 且数组长度 ≥ 64 时,会将该链表转换成红黑树。
- 原因:
- 链表查询时间是 O(n),而红黑树是 O(log n),性能更优。
- 8 是经验值,是在性能与空间权衡下选择的折中值。太小容易浪费内存,太大性能劣化。
- 原因:
- 退化为 6:
当链表长度从红黑树缩减到 ≤ 6(并且容量不能再缩小时),会退化回链表。
- 原因:
- 树结构维护成本较高,小规模数据不值得用树。
- 防止频繁转换造成抖动,设置 6 是为了避免边界频繁变化。
- 原因:
🔹3. G1 垃圾回收器如何预测停顿时间?
- G1 的目标是控制每次 GC 的最大停顿时间,如设置
-XX:MaxGCPauseMillis=200。 - 预测机制:
- G1 会根据历史数据统计回收每个 Region 所需的时间。
- 然后选择一组 Region,使得总回收时间不会超过目标。
- 通过以下因素预测:
- Region 的类型(Eden/Survivor/Old)
- 之前回收该类型 Region 所花时间
- 活跃数据比例
- 并行线程数量
4. Region 大小如何设置?
- G1 会自动设置 Region 大小,默认范围是 1MB 到 32MB,总共 2048 个 Region。
- 可以手动设置:
🧠 一、什么是“可见性”?
在并发编程中,“可见性”指的是:
一个线程对共享变量的修改,能否被其他线程立即看见。
如果没有保证可见性,就可能出现:
- 线程 A 修改了某个变量的值,
- 线程 B 却仍然看到旧值(因为缓存没有刷新)。
❓二、volatile 能做什么?
java
CopyEdit
volatile int a;
- 保证:
- 写操作后,其他线程能立即看到新值(可见性)
- 禁止指令重排序
- 不保证:
- 操作是原子的(比如
a++)
- 操作是原子的(比如
| 情况 | 是否保证元素可见性 | 是否保证原子性 | 推荐 |
|---|---|---|---|
volatile int[] arr |
❌ 否 | ❌ 否 | 🚫 不推荐 |
AtomicIntegerArray |
✅ 是 | ✅ 是 | ✅ 推荐 |
synchronized 控制访问 |
✅ 是 | ✅ 是 | ✅ 推荐(更通用) |
🔹6. ThreadLocal 内存泄漏的根本原因?JDK 改进方案?
- 根本原因:
ThreadLocalMap的 key 是弱引用(WeakReference<ThreadLocal>),value 是强引用。- 当 ThreadLocal 对象被回收,但线程未结束时,value 会一直存在于 Thread 中,无法访问但无法回收 → 泄漏。
6. ThreadLocal 内存泄漏的根本原因?JDK 改进方案?
- 根本原因:
ThreadLocalMap的 key 是弱引用(WeakReference<ThreadLocal>),value 是强引用。- 当 ThreadLocal 对象被回收,但线程未结束时,value 会一直存在于 Thread 中,无法访问但无法回收 → 泄漏。
- 改进方案(JDK 8 及以后):
- ThreadLocalMap 的
set()、remove()、get()都会自动清理 key 为 null 的 entry。 - 但仍然推荐使用后显式调用
remove():
- ThreadLocalMap 的
⚠️ 四、ABA 问题
什么是 ABA?
一个线程看到 top 还是 A,以为没变,但实际上它已经变成 B 又变回 A —— 导致 CAS 错误成功!
🌰 示例场景:
- 线程 A:读到 top = A,准备 CAS 替换成 X
- 线程 B:把 A 弹出,再压入 A(top 又变成 A)
- 线程 A CAS 成功 → 但中间结构已变 → 数据错乱 ❌
✅ 五、解决 ABA 问题:引入版本号(stamp)
| 编号 | 场景说明 | 原因分析 |
|---|---|---|
| 1 | 索引列上有函数操作 | 如 WHERE LEFT(name,3) = 'Tom',索引失效 |
| 2 | 使用 %like% 模糊匹配 |
%abc 无法利用 B+ 树 |
| 3 | 隐式类型转换 | WHERE id = '123',索引字段为 INT,会导致转换 |
| 4 | OR 条件未全部命中索引 | WHERE a=1 OR b=2,b 无索引失效 |
| 5 | 对索引列进行计算 | WHERE salary * 2 > 10000 |
| 6 | 使用 != 或 <> |
索引优化器认为结果集大,不走索引 |
| 7 | 使用 IS NULL / IS NOT NULL |
覆盖索引失效 |
| 8 | 联合索引未遵守最左前缀 | WHERE b = ?,(a, b) 联合索引失效 |
| 9 | 索引列参与函数/表达式 | 如 DATE(create_time) 会导致失效 |
| 10 | 查询字段未包含在索引中 | 无法使用覆盖索引优化 |
| 属性 | MySQL | Redis |
|---|---|---|
| 原子性 (A) | 支持 | 支持(单命令)/事务需注意 |
| 一致性 (C) | 严格 ACID | 不保证与 DB 强一致 |
| 隔离性 (I) | 支持隔离级别(RR 默认) | Redis 无并发控制(事务无隔离) |
| 持久性 (D) | Binlog + WAL 保证 | 需 RDB / AOF 配合才持久化 |
✅ 背景问题:缓存与数据库如何保持一致?
我们常见的一致性策略是:
写库的时候,把缓存删了
比如你要更新一个商品价格:
- 更新数据库价格
- 删除 Redis 缓存中的这条数据(如
item:12345)
这是典型的:更新数据库 + 删除缓存
❗但是,这种方式会在高并发下出问题!
❓为什么会出问题?——读写并发冲突
假设发生如下时序:
cssCopyEdit1. 请求 A:更新商品价格(update DB)
2. 请求 B:读取商品信息(从 Redis)
3. 请求 A:删除缓存
❗风险点:
- 请求 B 在缓存删除之前读取了旧数据(缓存未失效)
- 此时请求 B 会把旧数据写回 Redis(如果设置了缓存穿透保护)
- 结果就是:缓存数据比数据库还旧,形成“脏缓存”!
✅ 怎么解决?——延迟双删机制!
📌 核心思路:
删两次缓存,中间隔一段时间(比如 1 秒)再删一次。
✅ 延迟双删流程如下:
markdownCopyEdit1. 更新数据库(保证源数据正确)
2. 删除缓存(第一次)
3. 等待一段时间(如 1 秒)
4. 再删一次缓存(第二次,防止并发脏读)
这样,即使中间有并发读操作把旧值写回缓存,第二次删除也会清掉它。
| 属性 | 全称 | 含义(通俗解释) |
|---|---|---|
| C | Consistency(一致性) | 所有节点看到的数据是一致的(像单机一样) |
| A | Availability(可用性) | 每个请求都能在有限时间内获得响应(无论成功/失败) |
| P | Partition tolerance(分区容忍性) | 系统能容忍网络分区(节点/链路间网络断开) |
为什么三者不能同时满足?
因为 一旦发生网络分区(P),你必须在 C 和 A 之间做出权衡。
情况一:选择 CP(放弃 A)
- 系统保证数据一致性,但部分请求会因为分区而失败或挂起(牺牲可用性)
- 示例:ZooKeeper、Etcd
情况二:选择 AP(放弃 C)
- 系统保证请求可用,但可能返回“旧数据”或不同步的数据(牺牲一致性)
- 示例:DNS、Cassandra、Dynamo、Redis(主从异步)
情况三:选择 CA(放弃 P)
- 只适用于单机系统或网络永不出错的理想环境,现实中几乎不可能实现
| 隔离级别 | 中文名 | 能否读未提交 | 能否防止脏读 | 能否防止不可重复读 | 能否防止幻读 |
|---|---|---|---|---|---|
| READ UNCOMMITTED | 读未提交 | ✅ 能 | ❌ 否 | ❌ 否 | ❌ 否 |
| READ COMMITTED | 读已提交 | ❌ 否 | ✅ 是 | ❌ 否 | ❌ 否 |
| REPEATABLE READ | 可重复读(MySQL默认) | ❌ 否 | ✅ 是 | ✅ 是 | ❌(部分解决) |
| SERIALIZABLE | 串行化 | ❌ 否 | ✅ 是 | ✅ 是 | ✅ 是 |
事务 = 数据库执行的原子性工作单元
隔离级别 决定事务内部读到的数据一致性 vs 最新性
“不可重复读”不是绝对坏事,取决于业务需求
- 统计、结算 → 通常希望可重复读
- 查询最新状态 → 读已提交也OK
| 问题类型 | 说明 |
|---|---|
| 脏读 | 读到了其他事务尚未提交的数据 |
| 不可重复读 | 两次读取同一数据,结果不同(因为其他事务修改了它) |
| 幻读 | 两次相同条件查询,返回的记录数不同(因为其他事务新增或删除了满足条件的记录) |
在 Java 的线程池(ThreadPoolExecutor)中,当线程池和任务队列都满了,无法再接收新的任务时,就会触发拒绝策略(RejectedExecutionHandler)。
| 策略名 | 类名 | 行为说明 | 是否抛异常 | 是否丢任务 | 使用场景建议 | |
|---|---|---|---|---|---|---|
| AbortPolicy(默认) | ` | 直接抛出 RejectedExecutionException |
✅ 是 | ✅ 是 | 适用于任务不可丢的场景(如金融) | |
| CallerRunsPolicy | `` | 由提交任务的线程执行该任务 | ❌ 否 | ❌ 否 | 适用于能容忍延迟的系统 | |
| DiscardPolicy | 直接丢弃任务,不抛异常 | ❌ 否 | ✅ 是 | 日志系统、低优先级异步任务 | ||
| DiscardOldestPolicy | `` | 丢弃队列中最早的任务,再尝试提交当前任务 | ❌ 否 | ✅ 是(旧任务丢) | 对新任务实时性要求更高的场景 |
| 特性 | 说明 |
|---|---|
| ✅ 支持事务(ACID) | 是 MySQL 中唯一支持事务的存储引擎(MyISAM 不支持) |
| ✅ 支持行级锁 | 提供更高并发能力(相比表锁) |
| ✅ 支持外键 | 可定义外键约束,保证数据完整性 |
| ✅ 支持崩溃恢复 | 基于 redo/undo 日志自动恢复 |
| ✅ 支持 MVCC | 多版本并发控制,提升并发读性能 |
| ✅ 支持自动数据页缓存 | 有自己的 Buffer Pool,提升 I/O 性能 |
| ✅ 支持聚簇索引 | 主键索引和数据存储在一起,读取更快 |
| ✅ 支持表空间管理 | 可独立设置每张表的物理存储结构 |
| ✅ 支持全文索引(MySQL5.6+) | InnoDB 也能用全文检索了 |
| 名称 | 含义说明 |
|---|---|
| 分库 | 把一张表拆到多个数据库(实例)中,可以部署在不同的服务器上 |
| 分表 | 把一张表拆成多个表(表结构相同),仍在同一个数据库中 |
单库/单表的性能瓶颈:
- 数据量大(单表千万行后查询效率急剧下降)
- 单表索引过多,维护开销大
- 主从复制延迟无法满足写入压力
- 数据库连接数限制
- 事务锁冲突严重
🔥 分库分表的目标:
- 降低单表数据量 → 提高查询性能
- 减少热点数据竞争
- 支撑高并发、高可用、高可扩展性
虚拟内存是一种操作系统提供的机制,它让每个进程都认为自己拥有一整块连续、完整的内存空间,实际上这些地址被映射到物理内存 + 硬盘中的某些区域。
就像一个人租了一整层楼(4GB 虚拟空间),但实际上只住了一间(实际使用的物理页),其他的房间(地址)可能在仓库(硬盘),需要时再搬进来。
| 作用 | 说明 |
|---|---|
| ✅ 扩展内存容量 | 程序可以使用比实际物理内存更大的空间(如 16GB 虚拟空间 + 4GB RAM) |
| ✅ 隔离进程地址空间 | 每个进程有独立的虚拟空间,互不影响,防止非法访问 |
| ✅ 提高安全性 | 不同进程间不会互相访问内存,提高系统稳定性 |
| ✅ 简化内存管理 | 操作系统可以灵活调度、分配、换出内存页 |
| ✅ 支持按需加载(懒加载) | 只加载实际访问的内存,节省资源 |
Hash的计算 int h = key.hashCode(); int hash = h ^ (h »> 16); // 扰动,混合高低位 目的:把 hashCode() 的 高位信息下沉,避免只用低位导致分布不均。
你能做的“避免/减轻碰撞”的手段
- 写好
hashCode/equals- 相等对象必须同
hashCode;确保哈希分布均匀(别只用一两个字段,别返回常数)。 - Key 不要可变(放入后改变字段会“找不着”)。
- 相等对象必须同
- 合适的容量与装载因子
- 预计元素数
m→ 预估容量cap ≈ ceil(m / loadFactor),让它 接近且不超过阈值,降低链长/树化概率。 - 例如:要放 10w,
cap ≈ 100000 / 0.75 ≈ 133334,构造时会向上取 2 的幂(tableSizeFor)。
- 预计元素数
- 选择分布更好的 Key
- 比如用不可变、离散性强的 ID(UUID/雪花),或为业务复合 Key 设计合理哈希。
- 避免过度装载
- 装载因子越大越省内存但碰撞越多;对延迟敏感场景可降到 0.5。
- 必要时选别的容器
- 高并发用 ConcurrentHashMap;极端低冲突需求可考虑开放寻址或布谷鸟哈希(第三方/其他语言)。
扩容(resize)到底干了啥?(JDK 8 细节)
触发条件:size > threshold(阈值 = capacity * loadFactor)
步骤:
- 新表容量 = 旧容量 * 2(到上限
1<<30)。 - 阈值同步翻倍(
newThreshold = newCap * loadFactor)。 - 重新分布每个桶的节点,但不重算完整哈希:
- 关键位是
oldCap这个二进制最高新增位。 - 对桶内每个节点,看
(node.hash & oldCap):- 为 0 → 留在原 index;
- 为 1 → 去新位置
index + oldCap。
- 这样把一个桶 一分为二,且 保持相对顺序。树节点用
TreeNode.split对应拆分。
- 关键位是
复杂度:一次 resize 需要把每个桶走一遍,O(n);但均摊到多次插入,平均摊销仍近似 O(1)。
初始容量、装载因子、阈值
- 默认初始容量:16(懒分配;第一次 put 时分配)。
- 默认装载因子:0.75(时间/空间的折中)。
- 阈值:
threshold = capacity * loadFactor,超过就扩容。 - 最大容量:
1 << 30。
静态代码块的执行顺序规则
- 父类 → 子类(先执行父类的静态代码块,再执行子类的)
- 同一个类中:
- 按源码出现顺序执行:
静态变量显式赋值 和
static {}是合并在一起、从上到下依次执行的 - 不区分“变量赋值”和“静态块”优先级,谁在前谁先执行
- 按源码出现顺序执行:
静态变量显式赋值 和
- 执行完静态部分 → 再执行构造代码(构造代码块 & 构造方法)
JVM 加载一个类时,会按以下顺序执行:
- 加载(Loading) → 读
.class文件进内存 - 验证 / 准备 / 解析(Linking) → 给静态变量分配内存并设默认值(不是赋初值)
- 初始化(Initialization) → 执行静态变量的显式赋值 &
static {}静态代码块(按源码顺序)
初始化阶段才会执行静态代码块,且只会执行 一次(类第一次主动使用时)。
| 结构 | 典型用途 | 搜索 | 插入 | 删除 | 备注 |
|---|---|---|---|---|---|
| 普通 BST(未平衡) | 有序字典 | 平均 O(log n) / 最坏 O(n) | 平均 O(log n) / 最坏 O(n) | 平均 O(log n) / 最坏 O(n) | 退化成链就凉了(序列有序时) |
| AVL 树 | 查找密集型 | O(log n) | O(log n) | O(log n) | 高度更紧(平衡因子 ∈ {-1,0,1}),旋转更频繁,查询快 |
| 红黑树 | 通用映射/集合(Java TreeMap/TreeSet) | O(log n) | O(log n) | O(log n) | 近似平衡,旋转少,工业界常用 |
| Treap(树堆) | 随机化有序集合 | 期望 O(log n) | 期望 O(log n) | 期望 O(log n) | 键按 BST,优先级是堆;实现简单 |
| Splay 树 | 自适应热点 | 均摊 O(log n)(最坏 O(n)) | 均摊 O(log n) | 均摊 O(log n) | 访问把节点旋到根,热点更快 |
| Scapegoat/Weight-Balanced | 替代平衡树 | O(log n) | O(log n) | O(log n) | 通过重建维持平衡 |
| 堆(Binary Heap) | 优先队列 | 最小值查找 O(1) | O(log n)(push) | O(log n)(pop/top 删除) | 不是 BST;查任意键是 O(n) |
| 完全二叉树 | 堆的存储形态 | —— | —— | —— | 仅形态定义:叶尽量靠左;高度 ⌊log₂n⌋ |
| 满/完美二叉树 | 题目性质 | —— | —— | —— | 所有非叶都有两个子;节点=2^{h+1}-1 |
| 线段树(Segment Tree) | 区间查询/修改 | 区间查询 O(log n) | 点/区间更新 O(log n) | —— | 建树 O(n);可懒标记做区间更改 |
| 树状数组(Fenwick/BIT) | 前缀和 | 前缀和 O(log n) | 单点更新 O(log n) | —— | 空间 O(n),代码短;不是传统节点树 |
| Order-Statistic Tree(带秩) | 选第 k 小/求 rank | O(log n) | O(log n) | O(log n) | 红黑树 + 子树大小 |
| 特性 | AVL 树 | 红黑树 |
|---|---|---|
| 平衡定义 | 任意节点左右子树高度差 ≤ 1 | 满足“红黑性质”5 条:①根黑 ②叶黑(NIL)③红节点子节点黑 ④任一路径黑节点数相同 ⑤从任意节点到叶子路径上不能有两个连续红节点 |
| 平衡严格程度 | 更严格(高度差 ≤ 1) | 相对宽松(只保证黑高一致) |
| 高度范围 | ~1.44·log₂n(更矮) | ≤ 2·log₂n(略高) |
- AVL 树:
- 插入/删除后可能触发多次旋转(O(log n) 最坏需要旋转 log n 次)
- 维护平衡的代价更高
- 适合查找多、更新少的场景
- 红黑树:
- 插入/删除后旋转次数少(最多 2 次)
- 恢复平衡的成本低
- 更适合插入/删除频繁的场景(工业界普遍选择)
什么是慢查询
- 在 MySQL 里,
long_query_time(默认 10 秒)以上的 SQL 会被记录到 慢查询日志。 - 慢查询通常意味着:扫描数据量大、索引失效、执行计划不佳、锁等待等。
| 方法 | 说明 |
|---|---|
| 建合适的索引 | 常用条件列、JOIN 关联列、ORDER BY / GROUP BY 列建索引 |
| 覆盖索引 | 让查询只走索引,不回表 |
| 遵循最左前缀 | 复合索引查询条件必须按索引定义的最左列开始 |
| 避免索引失效 | 不在索引列做计算、函数、类型转换 |
| 优化 WHERE 条件 | 避免 OR、用 UNION ALL 替代;避免 !=、<>、NOT IN(索引失效) |
| 分页优化 | LIMIT offset,size 改为 子查询 + join 或 记住上次 id |
| *减少 SELECT ** | 只查必要字段,降低网络传输和回表开销 |
| 分解复杂 SQL | 大查询拆成小查询,减少锁竞争 |
| JOIN 优化 | 确保关联列有索引,小表驱动大表 |
| JOIN 类型 | 说明 | 结果特点 |
|---|---|---|
| INNER JOIN(内连接) | 返回两个表中 满足连接条件 的记录 | 只保留匹配行 |
| LEFT JOIN(左连接) | 返回左表全部记录,右表匹配的显示,不匹配补 NULL | 常用于查找“左表有但右表可能没有”的情况 |
| RIGHT JOIN(右连接) | 返回右表全部记录,左表匹配的显示,不匹配补 NULL | 功能对称于 LEFT JOIN |
| FULL JOIN(全连接) | 返回两个表所有记录,匹配的显示,不匹配补 NULL | MySQL 无原生 FULL JOIN,可用 UNION 模拟 |
| CROSS JOIN(笛卡尔积) | 不加 ON 条件时,返回两个表的所有组合 | 行数 = m × n |
| SELF JOIN(自连接) | 同一张表的不同别名之间连接 | 常用于树状层级关系查询 |
| id | name | class_id |
|---|---|---|
| 1 | Alice | 1 |
| 2 | Bob | 2 |
| 3 | Carol | 3 |
| id | class_name |
|---|---|
| 1 | Math |
| 2 | English |
| 4 | Physics |
SELECT s.name, c.class_name FROM students s INNER JOIN classes c ON s.class_id = c.id; 结果(只显示匹配的行,class_id=3 的 Carol 被过滤掉):
name class_name Alice Math Bob English
单例模式(Singleton Pattern)是一种创建型设计模式,它的目标是:
在整个程序运行期间,某个类只会被创建一个实例,并且提供一个全局访问点。
为什么要用单例模式
- 全局唯一性:某个对象在业务上只需要一个(如配置管理器、线程池、日志管理器、数据库连接池等)。
- 全局访问:程序中任意位置都能方便访问这个实例。
- 节省资源:避免重复创建和销毁带来的性能开销。
核心特点
- 构造方法私有化:外部不能直接
new。 - 类内部持有一个静态实例。
- 提供一个静态方法/属性获取实例。
| 实现方式 | 懒加载 | 线程安全 | 实现难度 | 性能 | 优点 | 缺点 |
|---|---|---|---|---|---|---|
| 饿汉式 | ❌ | ✅ | 简单 | 高 | 实现简单,类加载即实例化,JVM 保证线程安全 | 类加载即创建实例,可能浪费资源 |
| 懒汉式(线程不安全) | ✅ | ❌ | 简单 | 高 | 按需创建,节省内存 | 多线程下会创建多个实例 |
| 懒汉式 + synchronized | ✅ | ✅ | 简单 | 低 | 实现简单,线程安全 | 每次获取实例都要加锁,性能差 |
| 双重检查锁(DCL) | ✅ | ✅ | 中等 | 高 | 线程安全,延迟加载,性能好 | 实现相对复杂,需要 volatile 防止指令重排序 |
| 静态内部类 | ✅ | ✅ | 简单 | 高 | 线程安全,延迟加载,写法优雅 | 可能在反射或序列化下被破坏 |
| 枚举单例 | ❌(JVM 类加载即创建) | ✅ | 简单 | 高 | 最简单,线程安全,防反射/反序列化破坏 | 不支持延迟加载,语法相对特殊 |
| 区域 | 线程共享 | 生命周期 | 主要存放内容 |
|---|---|---|---|
| 程序计数器(Program Counter Register) | 否 | 线程私有,线程结束释放 | 当前线程所执行的字节码行号指示器 |
| Java 虚拟机栈(Java Virtual Machine Stack) | 否 | 线程私有,线程结束释放 | 栈帧(方法参数、局部变量表、操作数栈、动态链接、方法出口等) |
| 本地方法栈(Native Method Stack) | 否 | 线程私有,线程结束释放 | 为执行本地方法(Native)服务 |
| Java 堆(Java Heap) | 是 | JVM 启动创建,进程结束释放 | 所有对象实例、数组(垃圾收集器主要管理的区域) |
| 方法区(Method Area) | 是 | JVM 启动创建,进程结束释放 | 类信息、常量、静态变量、JIT 编译后的代码等 |
| 运行时常量池(Runtime Constant Pool) | 是 | 随类加载而创建,随类卸载而释放 | 字面量、符号引用、编译期生成的常量 |
| 参数 | 作用 | 关键点 |
|---|---|---|
| corePoolSize | 核心线程数(常驻线程数) | 线程池初始化后不会立即创建线程,除非 prestartAllCoreThreads() 或提交任务时才创建 |
| maximumPoolSize | 最大线程数 | 当任务队列满时,线程池会创建非核心线程直到达到此值 |
| keepAliveTime | 非核心线程的空闲存活时间 | 当线程空闲时间超过该值会被回收;默认核心线程不会超时,可通过 allowCoreThreadTimeOut(true) 让核心线程也回收 |
| unit | 存活时间的单位 | TimeUnit.SECONDS、MILLISECONDS 等 |
| workQueue | 任务队列 | 决定任务的排队策略,如 ArrayBlockingQueue(有界)、LinkedBlockingQueue(无界)、SynchronousQueue(直接交付) |
| threadFactory | 创建线程的工厂 | 可定制线程名、优先级、是否为守护线程 |
| handler | 拒绝策略 | 当线程池已满且队列已满时的处理方式 |
| 日志类型 | 作用 | 特点 | 存储位置 / 触发时机 |
|---|---|---|---|
| 错误日志(Error Log) | 记录 MySQL 启动、运行、关闭过程中的错误信息 | 方便排查故障 | --log-error 指定文件,常用于生产监控 |
| 通用查询日志(General Query Log) | 记录所有连接和执行的 SQL 语句 | 数据量大,几乎不用在生产开启 | --general-log |
| 慢查询日志(Slow Query Log) | 记录执行时间超过 long_query_time 的 SQL |
用于 SQL 优化 | --slow-query-log |
| 二进制日志(Binlog) | 记录所有 更改数据 的操作(逻辑日志) | 用于主从复制、增量备份、恢复 | --log-bin |
| 中继日志(Relay Log) | 备库接收主库 binlog 后保存的日志 | 用于主从复制 | 备库自动维护 |
| 重做日志(Redo Log) | 物理日志,记录数据页修改后的物理变化 | 保证事务 持久性(D) | InnoDB 独有,存储在 ib_logfile* |
| 回滚日志(Undo Log) | 记录数据被修改前的旧值 | 保证事务 原子性(A),支持 MVCC | 存储在 InnoDB 的表空间(ibd 文件) |
| 对比项 | Redo Log | Undo Log |
|---|---|---|
| 作用 | 保证事务的 持久性(Crash-Safe)——断电也能恢复已提交事务 | 保证事务的 原子性(回滚未提交事务)+ 支持 MVCC |
| 记录内容 | 数据页的 物理变化(修改了哪个页、偏移量、改成什么值) | 数据修改前的 逻辑记录(旧值) |
| 类型 | 物理日志 | 逻辑日志 |
| 什么时候写 | 在事务执行过程中,每次修改数据页时 先写 redo log | 在事务执行过程中,每次修改数据前 先写 undo log |
| 什么时候用 | 崩溃恢复(WAL: Write-Ahead Logging) | 事务回滚、MVCC 的快照读 |
| 生命周期 | 提交事务后依然保留到 checkpoint 刷盘 | 事务提交后不再需要,后台线程清理 |
| 存储位置 | 独立 redo log 文件(ib_logfile0/ib_logfile1) |
表空间文件(undo tablespace,默认在 ibdata1 或单独文件) |
| 维度 | synchronized | ReentrantLock | 普通 Lock(java.util.concurrent.locks.Lock 接口简单实现) |
|---|---|---|---|
| 类型 | JVM 内置锁(Monitor) | AQS 独占锁实现类 | 可能是自定义/简单封装的锁实现,不一定基于 AQS |
| 可重入 | ✅ 支持 | ✅ 支持 | ❌ 一般不支持(需自行实现重入逻辑) |
| 公平性 | 固定非公平 | 可选公平/非公平 | 一般不支持公平策略 |
| 可中断 | ❌ 不支持在等待锁时中断 | ✅ lockInterruptibly() |
取决于实现,大多不支持 |
| 尝试/超时获取 | ❌ 不支持 | ✅ tryLock() / tryLock(timeout) |
取决于实现,简单版本大多不支持 |
| 条件变量 | 1 个(wait/notify) |
多条件队列(newCondition()) |
取决于实现,一般无内建条件队列 |
| 可见性与内存语义 | 进入/退出监视器有 happens-before 保障 | lock / unlock 有 happens-before |
需开发者保证内存语义(易踩坑) |
| 调试与灵活性 | 简单语法糖,自动释放 | API 丰富,功能强 | 一般功能简单,灵活性低 |
| 性能 | 低争用性能好(JIT 优化、自旋) | 低争用接近,高争用可调策略 | 取决于实现,通常不如优化后的 JUC 锁 |
| 用法习惯 | synchronized(obj){...} 或同步方法 |
try { lock(); } finally { unlock(); } |
同样需手动 lock()/unlock() |
可重入锁(Reentrant Lock) 的意思是:
👉 同一个线程 在已经持有锁的情况下,可以再次获取这把锁,而不会发生死锁。
1. 为什么要可重入?
假设我们有下面的情况:
public synchronized void outer() {
inner(); // inner() 也需要同一把锁
}
public synchronized void inner() {
// do something
}
outer()被某个线程调用时,已经获取了对象锁。- 在执行过程中又调用了
inner(),如果没有“可重入”机制,那线程会被自己阻塞住 → 死锁。 - 有了可重入锁后,线程在调用
inner()时发现“这把锁自己已经持有了”,就可以 直接再次进入,锁的计数器+1。 vvvvvvvvvvvv
| OSI 七层模型 | TCP/IP 四层模型 | TCP/IP 五层模型 | 功能描述 | 典型协议/标准 |
|---|---|---|---|---|
| 应用层 Application | 应用层 | 应用层 | 提供网络服务给应用程序 | HTTP, HTTPS, FTP, SMTP, POP3, DNS, SSH |
| 表示层 Presentation | 应用层 | 应用层 | 数据表示、加密、压缩 | JPEG, GIF, TLS/SSL |
| 会话层 Session | 应用层 | 应用层 | 会话管理、建立/维护/终止连接 | RPC, NetBIOS, PPTP |
| 传输层 Transport | 传输层 | 传输层 | 提供端到端的可靠或不可靠传输 | TCP, UDP, QUIC |
| 网络层 Network | 网络层 | 网络层 | 路由、逻辑寻址 | IP, ICMP, ARP, RIP, OSPF, BGP |
| 数据链路层 Data Link | 网络接口层 | 数据链路层 | 邻接节点之间可靠传输、帧定界、差错检测 | Ethernet (IEEE 802.3), PPP, HDLC, VLAN (802.1Q) |
| 物理层 Physical | 网络接口层 | 物理层 | 定义物理传输介质的电气、光学、机械规范 | 光纤, 双绞线, 无线电频率, RJ45 |
1️⃣ 红黑树是什么
红黑树(Red-Black Tree)是一种自平衡二叉搜索树(BST),用于在插入、删除等操作后依然保持近似平衡,以保证查找、插入、删除的时间复杂度都是 O(log n)。
它的特点是给每个节点加了一个颜色属性(红或黑),并且必须满足一定的红黑性质。
2️⃣ 红黑树的五条性质(保持平衡的关键)
假设 NIL(叶子外部的空节点)视为黑色:
- 每个节点非红即黑
- 根节点是黑色
- 所有叶子节点(NIL)是黑色
- 红色节点的子节点必须是黑色(不能有两个连续的红节点)
- 从任一节点到其所有后代叶子的路径上,黑色节点数量相同(黑高一致性)
这五条性质共同作用,防止树在操作后退化成链表。
3️⃣ 它如何保持平衡
红黑树通过局部调整 + 颜色翻转保持平衡,调整策略主要有:
- 颜色翻转(Color Flip) 当父节点和叔叔节点都是红色时,将父和叔改成黑色,祖父改成红色(可能需要继续向上调整)。
- 旋转(Rotation)
分为左旋(Left Rotate)和右旋(Right Rotate),用于减少某一侧过深的高度。
- 左旋:让右子树上移,适用于右倾情况。
- 右旋:让左子树上移,适用于左倾情况。
插入和删除时会触发这两种操作的组合:
- 插入:先按 BST 规则插入为红色 → 如果违反性质 4 或 5 → 旋转/变色修正。
- 删除:如果删除黑色节点会破坏黑高 → 通过兄弟节点变色、旋转修正黑高。
红黑树并不是“完全平衡”的,而是保证最长路径不超过最短路径的两倍,足以保证 O(log n) 的性能。
2️⃣ 程序运行 & 虚拟内存分配
- 加载器(Loader)
- 程序运行时,操作系统把可执行文件的各个段(代码段、数据段、BSS 段等)映射到进程的虚拟地址空间。
- 给栈和堆分配虚拟地址区域。
- 变量地址
- 你在代码里看到的“地址”其实是 虚拟地址(VA),进程自己看到的是连续的地址空间。
3️⃣ 虚拟地址到物理地址的转换(MMU & 页表)
- CPU 访问变量时,会把虚拟地址交给 内存管理单元(MMU)。
- MMU 通过 页表(Page Table) 查找虚拟页号 → 物理页号的映射。
- 页表存在内存中,但 页表缓存(TLB, Translation Lookaside Buffer)里可能已经有了映射。
- 如果 TLB 命中 → 直接得到物理地址。 如果 TLB 未命中 → 硬件/操作系统查页表;若该页不在内存,还会触发缺页中断,从磁盘加载。
4️⃣ 物理地址到 CPU 缓存/内存访问
- 拿到物理地址后,CPU 会先查 多级缓存(L1 → L2 → L3):
- 如果缓存命中(cache hit) → 直接取值,延迟很低(L1 只有几个 CPU 周期)。
- 如果缓存未命中(cache miss) → 去下一层缓存,最后可能访问主存(几十到几百个 CPU 周期)。
- 如果数据不在内存(比如被换出到磁盘的 swap 区) → 操作系统触发磁盘 I/O 把数据页读回内存。
| 目标 | 方案 | 能力 | 典型场景 | 优缺点摘要 |
|---|---|---|---|---|
| 高可用(HA) | Sentinel + 主从复制 | 主从故障转移、自动选主 | 单机容量够,但要防宕机 | ✅简单稳定;❌不自带分片,容量/吞吐靠“横向多主 + 客户端分片/代理” |
| 高可用 + 水平扩展(分片) | Redis Cluster | 16384 槽位一致性哈希、自动重分片、故障转移 | 通用生产首选 | ✅官方、自动分片;❌多键操作/事务需同槽位,客户端需支持重定向 |
| 分片(代理层) | Twemproxy / Codis / Predixy 等 | 代理负责分片/连接复用 | 旧客户端、不想改代码 | ✅对客户端透明;❌代理成额外瓶颈/单点(需冗余) |
| 云托管 | ElastiCache / Azure Cache / Redis Enterprise | 一站式 HA + 分片 + 监控 | 上云 | ✅省心;❌成本高、细粒度可控性略弱 |
| 工具/机制 | 作用(专业+通俗解释) | 特点 | 适用场景 |
|---|---|---|---|
| synchronized | 加锁防冲突 —— 给一段代码上锁,保证同一时间只有一个线程能进来,像给厕所门挂锁一样。 | JVM关键字,自动释放锁,支持重入 | 临界区保护,简单易用 |
| ReentrantLock | 可控加锁 —— 和synchronized一样是锁,但你自己拿钥匙开关,能设置排队顺序、超时等待、可中断,就像有门禁系统的锁。 |
灵活,可定时/可中断锁,支持公平锁 | 需要高级锁特性、超时/可中断获取锁 |
| ReentrantReadWriteLock | 分开读写 —— 读的时候大家都能进,写的时候就锁上,像图书馆看书可以多人看,但写书只能一个人写。 | 多读单写,提高读多写少场景的并发性 | 缓存、配置读取等读多写少场景 |
| StampedLock | 乐观读写 —— 读的时候先假设没人改,只有写入才验证,读起来飞快,但用法比普通锁复杂。 | 乐观读性能高,但API复杂 | 对读性能要求极高且写较少 |
| CountDownLatch | 倒计时等人 —— 设一个倒计时,每有人完成任务就减一,倒到零才开始下一步,像等朋友都到齐才开饭。 | 计数归零前阻塞等待 | 主线程等待多个子任务完成 |
| CyclicBarrier | 集体出发 —— 大家先到集合点,等人齐了再一起走,像跑步比赛等所有选手到起跑线才开跑。 | 所有线程到齐后再一起执行 | 多线程阶段同步(分阶段任务) |
| Semaphore | 限流牌 —— 限定只能多少人同时干活,比如餐厅只有3张桌子,来了多的人要排队。 | 控制并发线程数 | 限流、资源访问控制 |
| Exchanger | 交换物品 —— 两个线程互相交换手里的数据,像交换卡片一样,双方都准备好才交换。 | 两个线程交换数据 | 双向数据传递 |
| Atomic 类 | 无锁计数器 —— 直接用硬件支持的原子操作来加减,不用加锁,速度很快。 | 无锁CAS实现,性能高 | 计数器、自增ID等 |
| BlockingQueue | 排队通道 —— 一个线程放数据,一个线程取数据,中间的队列自动帮你处理等待和唤醒,像餐厅厨房和传菜口。 | 线程安全队列,支持阻塞操作 | 生产者-消费者模型 |
| Phaser | 分阶段等人 —— 多批人分多轮集合,每轮都要等人齐了才能开始下一轮,像接力赛分棒次同步。 | 可动态注册/注销线程 | 动态任务批次同步 |
原子类(AtomicInteger、AtomicLong、AtomicReference 等)底层的核心是 CAS(Compare-And-Swap/Compare-And-Set) + volatile 保证可见性 + Unsafe 类直接操作内存。我帮你分成几个层面解释:
1. 原子类的组成原理
1.1 CAS(比较并交换)
-
逻辑:
- 读取当前值(旧值 oldValue)
- 比较当前值是否还是 oldValue
- 如果是,更新为新值 newValue
- 如果不是,说明被其他线程改过,更新失败,重新尝试
-
类似于:
“你柜子里现在是苹果3个吗?是的话我就换成4个,不是就放弃这次改。”
-
硬件支持: 通过 CPU 指令(如 x86 的
CMPXCHG)保证这个操作是原子的,执行过程中不会被打断。
1.2 volatile 保证可见性
- 原子类内部用
volatile修饰值(如private volatile int value;) - 确保一个线程修改 value 后,其他线程立刻能看到最新值(避免 CPU 缓存里的旧值)。
1.3 Unsafe 类直接操作内存
AtomicInteger内部用sun.misc.Unsafe来直接访问对象内存地址。Unsafe.getAndAddInt(Object obj, long offset, int delta)等方法调用底层 CAS。- offset 是变量在对象中的内存偏移量,通过
Unsafe.objectFieldOffset获取。
Java GC(复制算法,Copying GC)里分两个区的原因、好处和缺点我帮你分开说一下,你就能完全理解了。
1️⃣ 为什么要分两个区
复制算法的核心思想是:
只在一块区域分配对象,回收时将存活对象复制到另一块空区域,然后一次性清空原区域。
在 HotSpot 的 新生代(Young Generation)里,这两个区就是 From 区 和 To 区(有时候还会加一个 Eden 区,形成 Eden + From + To 的组合)。
原因:
- 避免内存碎片化
- 复制算法直接把活对象压缩到另一块连续内存,剩下的整块空间一次性清掉,内存自然是连续的,没有碎片。
- 分配速度快
- 复制后,下一次分配只要用指针碰撞(Bump-the-pointer),即从连续内存的头部往后分配,速度很快,不需要复杂的空闲链表。
- 实现简单
- 不用维护空闲块列表,只需要知道From → To怎么切换。
- 适合新生代高回收率场景
- 新生代对象朝生夕死,存活率低,大部分空间一次清空,复制成本低。
2️⃣ 为什么是两个区(而不是一个区就直接移动)
如果只有一块区域:
- 你在移动存活对象的时候,很可能会覆盖还没复制过的对象,导致数据丢失。
- 有两个区的话,可以保证:
- From 区:本次 GC 的扫描源
- To 区:新放置存活对象的目标区
- 这样就不会出现数据被覆盖的情况,复制过程安全可控。
3️⃣ 缺点
- 浪费一半空间
- 因为必须留出一块空的 To 区来接收对象,意味着同一时间只有一半的新生代可用。
- 例如新生代 100MB,From + To 各 50MB,实际能用的只有 50MB。
- 存活对象多时,复制成本高
- 如果存活对象很多(比如老年代迁入的对象、长生命周期对象),复制算法的效率就下降,因为需要挨个复制。
- 这也是为什么它适合新生代而不适合老年代。
- 内存利用率低
- 在内存紧张的环境下,浪费 50% 会很明显,不如标记-整理(Mark-Compact)节省空间。
- 需要额外的写屏障处理跨区引用
- 年轻代 GC 时,如果老年代对象引用了年轻代对象,需要做Card Table 记录,否则复制时会漏掉引用。
1️⃣ 栈(Stack)
- 方向:向低地址增长(地址从大往小走)
- 原因:
- 栈是连续的内存区域,主要用于存储局部变量、函数参数、返回地址等。
- 大多数体系结构(如 x86、x86_64)规定栈顶指针(
SP/ESP/RSP)往低地址移动来分配空间,往高地址移动来释放空间。
2️⃣ 堆(Heap)
- 方向:向高地址增长(地址从小往大走)
- 原因:
- 堆是用于动态分配的内存(
malloc/new)。 - 在大多数系统上,堆的起始地址在程序数据段(BSS / data segment)之后,分配新内存时向高地址扩展。
- 堆是用于动态分配的内存(
Redis 一般怎么用?在论坛里的落地清单
1) 缓存与反压
- 对象缓存:帖子详情
post:{id}(HASH/STRING)、评论详情comment:{id}(HASH)。 - 列表/排序缓存:帖子列表、热帖榜、评论列表(ZSET)。
- 多级缓存:本地 Caffeine + Redis,先本地后远程。
- 缓存策略:
- TTL + 随机抖动(防雪崩)
- 缓存空值(防穿透)
- 互斥锁 / 单航标记(防击穿):
SETNX lock:xxx 60s,回源建缓存后释放。
2) 计数与排行榜
- 阅读数、点赞数、评论数:
HINCRBY/INCRBY; - 日/周/月热榜:
ZINCRBY hot:post:20250814 postId 1,周期性ZUNIONSTORE汇总; - 评论热度:同理维护
post:{postId}:top。
3) 用户态功能
- 会话/Token:
session:{token} -> userId(TTL); - 限流:令牌桶 / 漏斗(Lua 实现原子扣减);
- 去重幂等:
SETNX processed:{bizId},处理成功再EXPIRE; - 关注/订阅:
SADD follow:{userId};时间轴ZSET feed:{userId}(按时间/权重推送)。
4) 搜索与推荐的辅助
- 倒排/轻检索:小规模标签/关键词用
SET/ZSET拼接;大规模还是上 ES。 - 相似帖子候选:临时集合运算(
SINTER,SUNION)做粗筛,细排在服务内完成。
5) 消息与异步
- 通知(评论/回复/点赞):
- 使用 Redis Streams:
XADD notif:events * {...};消费者组做多实例消费,保证可追溯; - 或 Pub/Sub(不落盘,轻量)。
- 使用 Redis Streams:
- 异步计算热度:把“赞/评论”事件写入 Streams,后台批处理更新 ZSET 分值,降低前台写延迟。
6) 防作弊/风控辅助
- 滑动窗口计数:
ZADD act:{user}:{action} ts ts,再ZREMRANGEBYSCORE清窗口;超阈值封禁或验证码。 - 布隆过滤器(模块/旁路实现):阻挡异常 ID 请求,减少穿透。
7) 分布式锁与任务协调
- 短事务锁:
SET lock:key val NX PX 30000;释放前校验val(避免误删别的锁)。 - 大任务建议用 Redlock 或把关键流程改为队列化/状态机,减少锁依赖。
键设计
-
文章被谁点赞(用于去重/查询状态):
like:art:{aid}:users(Set,成员=用户ID) -
用户点过哪些文章(便于用户页):
like:user:{uid}:arts(Set,成员=文章ID) -
点赞排行榜/计数:
like:rank(ZSet,member=文章ID,score=点赞数)也可以不用 ZSet,用
INCRBY like:cnt:{aid}保存计数;但要做榜单时 ZSet 更方便。
线程不安全
StringBuilder
线程安全
StringBuffer(方法加了synchronized)
线程不安全
ArrayListHashMapHashSetLinkedList
线程不安全
ArrayDequeLinkedList(作为 Queue 使用时)
线程安全
ConcurrentHashMap
1. Hashtable
- 底层结构:数组 + 链表(和
HashMap类似)。 - 线程安全实现方式:所有公开的方法(如
put、get、remove)都用synchronized修饰。- 插入:调用
put时会锁住整个Hashtable对象,只有一个线程能进行插入。 - 读取:
get也加了synchronized,读取操作同样需要获得锁。
- 插入:调用
- 特点:
- 所有操作互斥(全表锁)。
- 并发性能差,在多线程场景下容易成为瓶颈。
2. ConcurrentHashMap(JDK 1.8 之后的实现)
- 底层结构:
- 数组(Node[] table)。
- 每个桶是一个链表或红黑树(当冲突元素多时会转为红黑树)。
- 线程安全实现方式:
- 不是简单的全表锁,而是分段锁 + CAS 操作结合。
- 插入(put):
- 如果桶为空,用 CAS 操作直接插入新节点,避免加锁。
- 如果桶不为空,会在桶的头节点上加锁(synchronized),只锁冲突的那个桶。
- 当链表过长,会转换为红黑树,在加锁下完成。
- 读取(get):
- 读取操作基本是无锁的(直接通过 volatile 语义保证可见性)。
- 查找时只要定位到 table[index],就能遍历链表/红黑树。
- 特点:
- 读操作非阻塞,性能高。
- 写操作仅对局部桶加锁,不影响全表。
- 并发扩容时,采用分段迁移机制,多个线程可以协作完成 rehash。
红黑树是不是平衡二叉树?
- 严格答案:红黑树不是严格意义上的“平衡二叉树”,但它是一种自平衡二叉搜索树。
- 平衡二叉树(AVL 树):通常指任意节点左右子树高度差不超过 1 的树。
- 红黑树的平衡条件:
- 每个节点要么是红色要么是黑色。
- 根节点必须是黑色。
- 红色节点的子节点必须是黑色(不能有两个连续的红节点)。
- 任意节点到叶子节点的路径上,黑色节点数量相同。
这样保证了:
- 红黑树的最长路径不超过最短路径的 2 倍。
- 查询、插入、删除操作的时间复杂度为 O(log n)。
- 相比 AVL 树,红黑树更“松弛”,不追求绝对平衡,但旋转次数更少,插入/删除效率更高。
因此: 👉 红黑树是一种近似平衡的二叉搜索树,而不是严格意义上的平衡二叉树。
| 特点 | 红黑树 | B+ 树(MySQL 索引) |
|---|---|---|
| 结构 | 二叉搜索树(2 分叉) | 多路平衡树(可有上百个分叉) |
| 数据存放 | 所有节点存储数据 | 叶子节点存数据,非叶子节点存索引 |
| 高度 | 高,数据量大时树很深 | 低,节点分叉多,IO 次数少 |
| 适用场景 | 内存数据结构(如 TreeMap、Linux 调度器) | 数据库索引(磁盘存储优化) |
| 顺序遍历 | 需要中序遍历整个树 | 叶子节点链表即可高效顺序扫描 |
| 磁盘友好性 | 差(频繁磁盘 IO) | 好(节点对齐磁盘页,减少 IO 次数) |
总结
- 红黑树:更适合内存中的动态数据结构,保证 O(log n) 操作。
- B+ 树:更适合数据库磁盘存储场景,通过多路分叉和页优化,极大减少磁盘 IO。
单例模式的核心思想是: 👉 保证一个类在整个程序运行过程中只有一个实例,并且提供一个全局的访问点。
换句话说:
- 构造函数被隐藏(私有化),外部不能随意
new。 - 只通过类提供的静态方法(如
getInstance())来获取唯一的对象。 - 整个系统里不管调用多少次,得到的都是同一个实例。
📦 使用场景
单例常用在需要唯一全局对象的地方,比如:
- 配置类(系统配置只需要一份)。
- 日志类(统一输出日志)。
- 线程池/连接池(复用资源,避免反复创建销毁)。
- 缓存类(全局共享数据)。
1️⃣ 最基础的懒汉模式(非线程安全)
public class Singleton {
private static Singleton instance; // 没有提前创建
private Singleton() {} // 私有构造函数
public static Singleton getInstance() {
if (instance == null) { // 第一次调用时才创建
instance = new Singleton();
}
return instance;
}
}
- 特点:按需加载(第一次用到才实例化)。
- 问题:在多线程下可能出现 多个线程同时进入 if,导致创建多个对象 → 不安全。
2️⃣ 加锁版懒汉(线程安全,但性能差)
public class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
synchronized保证同一时刻只有一个线程能进入方法。- 缺点:每次获取实例都要加锁,性能低。
4️⃣ 更优雅的懒汉式:静态内部类
public class Singleton {
private Singleton() {}
private static class Holder {
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
return Holder.INSTANCE;
}
}
Holder在第一次调用getInstance()时才加载,INSTANCE 被创建。- JVM 保证类加载过程的线程安全。
- ✅ 实现简单、懒加载、无锁,常被认为是懒汉的最佳写法。
1. 明确问题现象
- 用户反馈了什么?接口超时、报错、数据不一致、页面打不开?
- 是否能复现?复现条件是什么?
- 问题范围:是个别用户、某个功能、还是全量受影响?
2. 快速确认影响范围
- 看监控(QPS、RT、错误率、CPU、内存、磁盘、网络流量)。
- 日志中是否有大量异常堆栈?
- 是否只影响某些机房、某些版本或某些服务?
3. 定位问题入口
- 从用户请求入口往下追踪(链路追踪、请求 ID、日志上下文)。
- 先确认网关/负载均衡是否正常,再到应用服务,再到数据库/缓存/下游依赖。
4. 分层排查
- 应用层:看错误日志、线程 dump、进程状态。是否有死锁、GC 频繁、线程池耗尽?
- 中间件层:Redis、MQ、Kafka 是否积压?连接是否超时?
- 数据库层:慢查询、锁等待、连接池耗尽?
- 系统层:CPU 飙高、内存不足、磁盘写满、网络丢包?
5. 验证并复现
- 如果是代码逻辑问题,能否在测试环境复现?
- 如果是配置/数据问题,能否通过对比正常和异常环境找到差异?
6. 应急处理
- 先止血:限流、降级、回滚、扩容,优先恢复服务可用性。
- 再修复:定位根因,修改代码/配置/数据,逐步上线。
7. 复盘与预防
- 总结根因:是代码 bug、依赖故障、配置失误还是流量突增?
- 建立监控告警:提前发现问题。
- 补充预案:文档化常见问题排查步骤。
👉 一个口诀是:“先止血,再定位,最后复盘”。
如果你要并发调用十几个下游,线程池参数别拍脑袋配。最靠谱的是用“隔离 + 估算 + 验证”的套路:
一、先做隔离(Bulkhead)
- 按下游拆池:每个关键下游一个独立线程池(或至少同延迟/同SLA的一组下游共用一个池),避免 A 堵死拖垮全局。
- 再配限流/熔断:每个下游单独限并发与超时,配熔断与降级。
二、用数据估算池大小(核心公式)
拿到每个下游的 3 个数:目标 QPS、p95 延迟(或超时上限)、突发系数/冗余比例。 用 Little’s Law 估并发需求:
- 需要并发数 C ≈ QPS × p95(秒)
- 再乘冗余系数(1.2~1.5)吸收抖动
例:某下游 QPS=300,p95=80ms=0.08s C≈300×0.08=24,并发冗余 1.3 → 31
三、如何落到线程池参数
以 Java ThreadPoolExecutor 为例(阻塞式 I/O 场景):
- corePoolSize
≈ 上面算出的 C(向上取整)。
- 业务稳定:取 C
- 有突发:取 1.1~1.3×C
- maximumPoolSize
= 1.5~2×core,但有上限:
- CPU 密集:≤ 2×CPU 核心数
- I/O 阻塞:可以高一些(几十~上百),但要用监控盯上下文切换和 GC
- 工作队列(非常关键)
- 对尾延迟敏感:
SynchronousQueue(0 队列)+ 合理的 max,失败快、保护下游 - 需吞吐+吸收小突发:
LinkedBlockingQueue(cap),cap ≈ C ~ 2C - 队列太大 = 隐藏拥塞 + 放大尾延迟
- 对尾延迟敏感:
- keepAliveTime
30~60s;流量波动大且想省线程,可
allowCoreThreadTimeOut(true) - 拒绝策略
AbortPolicy:立刻失败,上报/打点(推荐保护下游)CallerRunsPolicy:向上游反压,但要确保调用方线程能承受- 或自定义:记录下游名、池名、当前 in-flight,打点报警
- 超时(不属于线程池但必须配)
- 下游调用超时要 小于 你对外 SLA
- 读超时 ≈ p99 延迟 + 少量余量;连接超时单独设更小
- 并发上限(替代或补充线程池) 使用信号量隔离(如 Resilience4j Bulkhead)把“同时在途请求数”钳住到 C~1.5C,效果常比仅靠队列更稳定。
给你一份超精简版多机房数据同步攻略(够用且能落地):
1)先选一种模式(别全都上)
- 单主 + 异步复制(推荐默认) 一地写,多地读;复制是异步的。适合订单、用户资料等大多数 OLTP。 优点:简单稳;缺点:读可能短暂陈旧。
- 多主(active-active)+ 冲突解决 各地可写,最终一致。适合点赞/计数/购物车这类“可合并”的数据。 关键:幂等写、全局ID、版本号/CRDT 合并。
- 强一致(跨机房共识) 少量关键元数据用(例如全局唯一约束/配置主开关)。 成本高、延迟受 RTT 影响,只用在小表。
2)数据库怎么落地(MySQL 举例)
- 默认:每分片单主,跨机房异步复制(GTID)。读本地,写路由到主。
- 切主:用 Orchestrator/MHA;禁止双写,切换时先追平位点再放流量。
- 多主必须分区写(按用户/租户/地域),减少冲突面。
3)缓存 & 消息
- 缓存:只做读加速,主数据仍在 DB;失效通过 消息总线广播。
- 消息/Kafka:用 MirrorMaker/Cluster Linking 选定需要的 Topic 镜像,不要全量跨洋。
- 语义默认“至少一次”,用幂等键去重。
4)最重要的 5 条铁律
- 幂等:所有跨机房写都带
request_id,DB 上加唯一索引/去重表。 - Outbox/CDC 代替双写:应用内先写本地 outbox,再异步发 Kafka,同步两地靠日志,不靠应用并行双写。
- 读写一致性:需要写后立读的请求,绑回同机房/同分片或带版本条件读。
- 演练切换:有回滚预案(能 5 分钟内切回),季度演练 RTO/RPO。
- 监控三件套:复制延迟(秒/位点)、Kafka lag、冲突/去重命中率。
5)一分钟落地清单
- 先定:哪类表单主,哪类表可多主(计数/购物车)。
- 给写请求加
request_id,实现幂等。 - 写入旁路改为 Outbox → Kafka → 订阅方。
- 设置跨机房读本地,对强一致小表单独服务化(少量共识)。
- 加复制延迟告警、切主脚本、灰度开关;写一份“切主 SOP”。
记忆法:“默认单主,能异步别同步;能幂等别重试;能日志别双写;能就近读别跨洋。”
线程与进程的区别
-
基本概念
- 进程(Process):操作系统分配资源的最小单位。每个进程都有独立的内存空间(代码段、数据段、堆、栈),以及文件描述符、寄存器等。
- 线程(Thread):CPU 调度的最小单位,是进程中的一个执行流。线程共享所属进程的资源(如内存、文件句柄),但有自己独立的栈和寄存器。
-
区别总结
对比点 进程 线程 资源 拥有独立的内存地址空间和资源 共享进程的内存和资源 开销 创建、销毁、切换开销大 创建、销毁、切换开销小 通信 进程间通信(IPC)需要特殊机制,如管道、消息队列、共享内存 线程间通信更容易,直接读写共享内存 稳定性 一个进程崩溃不会直接影响其他进程 一个线程崩溃可能导致整个进程崩溃 调度 由操作系统调度 同样由操作系统调度,但粒度更小
栈(Stack)
- 分配/释放:由编译器自动分配和释放。
- 存储内容:函数参数、局部变量、返回地址等。
- 空间大小:通常比较小(几 MB),连续分配,可能出现栈溢出(Stack Overflow)。
- 访问方式:先进后出(LIFO),访问速度快。
堆(Heap)
- 分配/释放:由程序员手动申请(如 C 中的
malloc/ C++ 的new),需要手动释放(否则内存泄漏)。 - 存储内容:动态分配的对象或数据。
- 空间大小:通常比栈大得多,不要求连续,可以达到 GB 级。
- 访问方式:自由存取,效率比栈低(需要分配、查找空闲空间)。
区别总结
| 对比点 | 栈 | 堆 |
|---|---|---|
| 管理方式 | 系统自动管理 | 程序员手动管理 |
| 存储内容 | 局部变量、函数调用信息 | 动态分配的对象、数组 |
| 空间大小 | 较小,连续存储 | 较大,分散存储 |
| 速度 | 快 | 相对慢 |
| 问题 | 可能溢出 | 可能内存泄漏、碎片化 |
Redis 中的 Sentinel 模式
如果你是问 Redis Sentinel,那它是 Redis 高可用方案。
核心作用
- Redis 本身是单点的,如果 Master 宕机,整个服务不可用。
- Redis Sentinel 模式提供了一种 自动故障转移 + 监控 + 通知 的机制。
架构组成
- Sentinel 进程:独立运行,专门负责监控 Redis 节点。
- Master 节点:主节点,负责写入和复制。
- Slave 节点:从节点,负责读和备份。
工作机制
- 监控:Sentinel 定期 ping Master 和 Slave,检查是否存活。
- 选举:如果 Master 宕机,多数 Sentinel 一致认为“主挂了”,会发起一次 投票选举新的 Master。
- 故障转移:把某个 Slave 升级为 Master,并通知其他 Slave 去同步它。
- 通知:Sentinel 还会通过 API 通知客户端“新的 Master 地址”。
👉 打个比方:
- Master 是“国王”,Slave 是“太子”,Sentinel 是“御史大夫”。
- 国王挂了,御史们(Sentinel)开会投票,推选一个太子当新国王,然后全国公告。
ACID 四大特性
- Atomicity(原子性)
- 定义:事务中的所有操作要么全部成功,要么全部失败,不会出现“只做了一半”的情况。
- 例子:银行转账,A 给 B 转 100 块。
- A 账户减 100
- B 账户加 100
- 原子性保证:这两步要么都成功,要么都不执行。不会出现 A 扣钱了但 B 没收到。
- Consistency(一致性)
- 定义:事务执行前后,数据库必须保持一致性状态,不会违反数据完整性约束。
- 例子:转账后,A 和 B 的余额总和应该保持不变(转账前后总额相等),即使发生异常也不能破坏规则。
- Isolation(隔离性)
- 定义:多个事务并发执行时,每个事务都应该像自己独占数据库一样,不受其他事务影响。
- 例子:
- 如果 A 正在查询商品库存,B 正在修改库存,那么 A 应该要么看到修改前的数据,要么看到修改后的数据,而不是一个“中间状态”。
- 补充:数据库通过 隔离级别(读未提交、读已提交、可重复读、串行化) 来实现不同程度的隔离。
- Durability(持久性)
- 定义:事务一旦提交,修改就会永久保存,即使数据库宕机、断电也不会丢失。
- 实现:通常依赖 WAL(Write Ahead Log,预写日志)、Redo Log 等机制。
- 例子:转账事务提交后,即使系统瞬间掉电,重启后也能从日志恢复数据。
泄露的内存什么时候会释放
- 进程退出时,操作系统会回收该进程的虚拟内存页和大多数内核资源(fd、映射等)。
- 但对长跑服务而言,泄露会持续涨内存直到 OOM;并且某些跨进程资源(如未删除的 SysV/Posix 共享内存段、临时文件)可能不会自动清理。
- 因此“交给 OS 回收”只对短命进程勉强成立,服务程序必须修。
IPv4(最常见字段)
Version(4) | IHL | DSCP/ECN | Total Length | Identification | Flags | Fragment Offset | TTL | Protocol | Header Checksum | Source IP | Dest IP | Options
IPv6(更简化的主头 + 扩展头)
Version(6) | Traffic Class | Flow Label | Payload Length | Next Header | Hop Limit | Source IP(128b) | Dest IP(128b)
其余功能(分片、路由、认证等)放在扩展头链里。
IPv4 头部字段及作用
(标准头部最小 20 字节)
- Version (4 bit) 表示 IP 协议版本号。常见是 4(IPv4),IPv6 中为 6。
- IHL (Internet Header Length, 4 bit) 头部长度,单位是 4 字节。最小值 5(即 20 字节),最大 15(即 60 字节)。若有 Options 字段,则 IHL > 5。
- DSCP/ECN (8 bit)
- DSCP (Differentiated Services Code Point):服务质量(QoS),用于流量优先级。
- ECN (Explicit Congestion Notification):显式拥塞通知,用于拥塞控制。
- Total Length (16 bit) 整个 IP 包(头 + 数据)的长度,单位字节,最大 65,535。
- Identification (16 bit) 分片标识号。分片时所有片的 ID 相同,接收方靠它把分片组装回去。
- Flags (3 bit)
控制分片:
- Bit 0:保留(必须为 0)
- DF (Don’t Fragment):1 表示禁止分片
- MF (More Fragments):1 表示后面还有分片
- Fragment Offset (13 bit) 当前分片相对于原始数据的偏移量,单位 8 字节。
- TTL (Time To Live, 8 bit) 存活时间,每经过一个路由器减 1,减到 0 则丢弃,防止环路。
- Protocol (8 bit)
表示 IP 数据部分使用的上层协议:
- 6 = TCP
- 17 = UDP
- 1 = ICMP
- 2 = IGMP
- Header Checksum (16 bit) 只覆盖 IP 头部的校验和,用于检测头部是否损坏。
- Source IP Address (32 bit) 源主机 IP 地址。
- Destination IP Address (32 bit) 目标主机 IP 地址。
- Options (可选, 0–40 字节) 用于测试、路由记录、安全性要求等。现在很少用。
- Padding 用 0 填充,使头部长度为 4 字节的整数倍。
IPv6 头部字段及作用
(固定 40 字节,更简化)
- Version (4 bit) 固定为 6。
- Traffic Class (8 bit) 类似 IPv4 的 DSCP/ECN,用于 QoS、拥塞控制。
- Flow Label (20 bit) 表示数据流标识,路由器可识别同一流量并保证顺序或 QoS。
- Payload Length (16 bit) 负载长度(不包括头部),最大 65,535。若更大需扩展头部支持。
- Next Header (8 bit) 类似 IPv4 的 Protocol 字段,表示紧跟在 IPv6 头后面的协议(TCP/UDP/ICMPv6 等),或扩展头类型。
- Hop Limit (8 bit) 类似 IPv4 的 TTL,每跳减 1,归零丢包。
- Source Address (128 bit) 源 IPv6 地址。
- Destination Address (128 bit) 目的 IPv6 地址。
🔑 总结:
- IPv4 头部复杂(分片、校验、选项等),所以路由器要更多处理。
- IPv6 头部更简洁,许多功能移到扩展头,以减少转发负担。
如何提升服务性能
我一般从几方面考虑:
- 算法与数据结构:降低复杂度,选合适容器。
- 并发与架构:用异步 I/O、线程池、减少锁争用。
- 缓存:本地缓存 + 分布式缓存,减少数据库压力。
- 数据库优化:建索引、读写分离、批量操作。
- 网络与系统调优:连接池、零拷贝、参数优化。
- 可观测性:先监控和定位,再有针对性优化。
两个 800GB 大文件找重复行 —— 思路分层讲
1. 明确限制
- 800GB ≫ 内存,肯定不能直接全读进来。
- 所以必须走 外部存储算法(External Memory Algorithms)。
2. 常见方法
方法一:外部排序 + 归并
- 先对两个文件分别做外部排序(分块、内存排序、归并)。
- 排好序后像 归并两个有序数组 那样同时扫描,遇到相同的行就输出。
- 这是最通用的方法,时间复杂度 ≈ O(N log N),空间靠磁盘,可靠但比较耗时。
方法二:哈希分桶
- 用哈希函数对行做
hash(line) % N,把两个文件都分桶(写成 N 对小文件)。 - 每对桶里数据规模都小得多,可以进内存处理。
- 在桶内用哈希表/排序找交集。
- 这个方法如果选好 N,可以大大提高效率,还能并行。
3. 辅助优化
- 可以先对一个文件建 Bloom Filter,扫另一个文件时快速过滤掉大部分不可能重复的行(但要二次校验,因为 Bloom Filter 有误判)。
- 注意行格式要统一(换行符、编码、去空格等)。
阻塞(Blocking)与非阻塞(Non-blocking)
阻塞 IO
- 当用户线程发起 IO 调用(如
read、recv)时,如果数据还没准备好,就会 阻塞当前线程,直到数据到来并复制到用户缓冲区。 - 优点:逻辑简单。
- 缺点:线程被挂起,CPU 利用率低。
非阻塞 IO
- 当用户线程发起 IO 调用时,如果数据没准备好,立即返回一个错误码(比如
EAGAIN),不会挂起线程。 - 用户需要不断轮询(polling)检查数据是否就绪。
- 优点:线程不会挂起。
- 缺点:轮询会浪费 CPU,效率不高。
同步(Synchronous)与异步(Asynchronous)
同步 IO
- 用户线程必须自己参与 IO 就绪的等待和数据拷贝。
- 即使使用了非阻塞 IO(反复轮询),最终数据拷贝还是在用户线程上下文中完成,所以这依然是 同步。
异步 IO
- 用户线程发起 IO 请求后,内核负责完成所有等待与数据拷贝工作,完成后再通知用户(回调/事件/信号)。
- 线程完全不用管,像是“下单—送货上门”的模式。
- 典型例子:Linux 的
aio_*系列函数,Windows 的 IOCP。
1. 先想象一个场景:你点外卖
- 同步: 你打电话点餐之后,必须自己守在餐厅门口等,直到厨师做好并交给你。 👉 不管你是坐着等(阻塞),还是一会儿进去问一声“好了吗”再出来(非阻塞轮询),最后都是你自己负责盯着,食物做好了你自己去取。
- 异步: 你下单后该干嘛干嘛,餐厅会在做好后直接派人送到你家,或者发短信通知你去拿。 👉 等待和把饭送到你手上的过程,都是别人(系统/内核)替你完成的,你不需要一直去盯着。
2. 换到 IO 上
- 同步 IO: 程序要么一直傻等,要么不断问“准备好了吗?” 数据的复制(从内核到你的程序内存)还是你自己去做。
- 异步 IO: 程序只需要说一句:“我要这个数据”。 操作系统自己会等数据准备好,并且把它放到你的内存里,然后告诉你“OK,可以用了”。
3. 关键点区别
- 同步:你(程序)得自己“参与整个等待和拿数据的过程”。
- 异步:你(程序)只需要发请求,剩下的等别人(操作系统)帮你搞定,最后只接个通知。
二者的组合关系
阻塞/非阻塞是“调用时行为”;同步/异步是“完成时通知方式”。所以它们可以组合:
| IO 模式 | 说明 |
|---|---|
| 阻塞同步 IO | 最传统的方式,read() 直接阻塞到有结果。 |
| 非阻塞同步 IO | 通过不断轮询或配合 select/poll/epoll 检查状态,数据拷贝仍由用户线程完成。 |
| 阻塞异步 IO | 很少用,等异步结果的时候还要阻塞调用线程,没有意义。 |
| 非阻塞异步 IO | 真正的异步 IO,发起后立即返回,结果由回调/事件驱动通知。 |
阻塞同步 IO
小规模程序,比如脚本里直接 read 文件。
非阻塞同步 IO + 多路复用 (select/poll/epoll) 高并发服务器常用模式(如 Nginx、Redis)。线程只等待“哪个 socket 就绪”,真正的读写操作还是用户线程完成,所以是同步。
异步 IO 大规模 IO 场景,比如高性能文件服务器、Windows IOCP、libaio。程序只提交 IO 请求,数据到达后由操作系统通知。
一个生活类比
- 阻塞同步:去窗口排队买奶茶,必须等到拿到手才能走。
- 非阻塞同步:去窗口问有没有好啦?没好就回去再问,直到拿到。
- 异步 IO:下单后走开,等店员送到你手上或者通知你来取。
新版 Redis 为何采用「RDB + AOF 混合」策略?
- Redis 4.0 起引入 混合持久化:AOF 重写时,先把现有数据以 RDB 格式写入,再追加增量写命令。
- 原因:
- 恢复快:RDB 格式比 AOF 更小更快加载。
- 数据全:结合 AOF 追加日志,保证较高的数据完整性。
- 折中方案:既避免纯 RDB 的丢数据风险,又避免纯 AOF 文件过大恢复慢。
什么是缓存热点 key?电商秒杀场景下如何减少单 key 高频写入?
- 缓存热点 key:指某个 key 被短时间内大量访问或修改,导致缓存层/数据库层压力过大,甚至成为系统瓶颈。
- 典型场景:秒杀商品库存、明星直播点赞数等。
- 减少单 key 高频写入的方法:
- 分片计数(counter sharding):把一个库存 key 拆成多个 key,随机写其中之一,查询时再合并求和。
- 批量异步写:写操作先打到消息队列/内存队列,异步批量更新数据库或缓存。
- 本地缓存 + 定时合并:先在本地(进程内/线程内)累加,定期汇总写回。
- 乐观扣减 + 校正:前端或应用层先做预扣减,异步与真实库存对账。
如何把热点 key 拆分或合并请求以降低压力?
- 拆分热点 key(sharding):
- 把单个 key 拆成多个,比如库存
stock:1001拆成stock:1001:1~stock:1001:N。 - 写操作随机落到某个分片。
- 读操作需要合并多个分片(或缓存层做聚合)。
- 好处:单个 key 不会被写爆。
- 把单个 key 拆成多个,比如库存
- 合并请求(request merge / coalescing):
- 如果多个用户同时请求更新/查询同一个 key,可以在应用层做 请求合并,减少实际落到 Redis 的次数。
- 举例:1000 个用户同时点赞,可以先在应用层聚合成“+1000”,再写一次 Redis,而不是 1000 次
INCRBY 1。
MySQL 常见索引分为以下几类:
- 主键索引(Primary Key)
- 每个表只能有一个,唯一且非空,通常基于 B+ 树。
- InnoDB 中,主键索引的叶子节点直接存储整行数据(聚簇索引)。
- 唯一索引(Unique Key)
- 保证列值唯一,但允许有一个 NULL。
- 叶子节点存储的是索引列和主键值。
- 普通索引(Index / Key)
- 最基本的索引,加快查询,没有唯一性约束。
- 联合索引(Composite Index)
- 在多个列上建立的索引,遵循最左前缀原则。
- 全文索引(Fulltext Index)
- 用于大文本字段(如 CHAR、VARCHAR、TEXT),支持分词、模糊搜索。
- 空间索引(Spatial Index)
- 基于 R-Tree,主要用于地理空间数据(GIS)。
B+ 树节点大小通常与操作系统页对齐,MySQL 默认页大小是多少?
- InnoDB 默认页大小:16KB
- 一个 B+ 树节点就是一个页,节点大小与页对齐能:
- 最大化利用磁盘读写(顺序读取一个完整页)。
- 让单个节点能存放尽可能多的索引 key,从而降低树的高度。
如果让你设计,B+ 树非叶子节点大小应遵循什么原则?
- 原则:非叶子节点应尽量设计成 刚好一个页大小,以便一次 IO 能完整读取一个节点。
- 原因:
- 节点越大,每个节点能存储的索引 key 越多,树的高度就越低,减少 IO 次数。
- 不能太大,否则一个节点跨多个页,导致读取时仍然要多次 IO,反而降低性能。
- 不能太小,否则树层数增加,访问路径变长,IO 变多。
- 实际策略:InnoDB 就是定死 16KB 页大小,每个非叶子节点控制在 16KB 内。
要让“用户 → 正确分片”这件事稳定又可扩展,常见有 4 种做法。你可以任选其一或组合使用。
1) 应用层取模路由(最简单)
- 规则:
shard = hash(user_id) % N - 应用保有一个「分片表」(shard → {host,port})。
- 例:
hash(1008611) % 16 = 7→ 走第 7 号分片。 - 优点:快、实现简单。
- 缺点:N 变化(扩容/缩容)会导致大规模搬家(几乎所有用户都重映射)。
2) 一致性哈希(平滑扩容)
- 把分片节点放在一个“哈希环”上,
shard = first_node_clockwise(hash(user_id))。 - 新增/下线分片时,只迁移邻近区间的 Key,修改量小。
- 实现要点:虚拟节点(每个物理分片放多个虚拟点)让负载更均匀。
- 常用于应用层路由或中间件(如某些客户端 SDK)。
3) 代理/中间层路由(对应用透明)
- 典型:Twemproxy、自研代理、或云厂商网关。应用只连代理,代理按哈希把请求转到对应分片。
- 优点:应用无感知,分片拓扑变化由代理维护。
- 缺点:多一跳,有额外延迟与单点风险(需多副本、健康检查)。
4) 存储自带分片(以 Redis 为例)
- Redis Cluster:把键映射到 16384 个 hash slot;slot 分配到不同节点。
- 路由由客户端/集群完成:客户端发到任意节点,收到
MOVED/ASK后学到正确节点,之后直连正确分片。 - 想把同一用户的数据固定在同一 slot,可用哈希标签:
inbox:{user_id}(花括号内参与 slot 计算)。
- 路由由客户端/集群完成:客户端发到任意节点,收到
- 优点:原生、成熟;支持在线迁移 slot。
- 缺点:需要 Cluster-aware 客户端;某些命令有跨 slot 限制。
最终一致:在分布式系统里,数据在不同副本之间不会立刻同步,但经过一段时间(秒/分钟)后,一定会收敛到一致的状态。
- 系统允许短暂不一致(比如某个副本“落后”几条数据)。
- 但保证不会无限制不一致,最终大家会看到相同的结果。
Nginx(发音类似 engine-x)是一个高性能的Web服务器和反向代理服务器,同时也可以用作负载均衡器和HTTP缓存。它最初由俄罗斯工程师 Igor Sysoev 在 2004 年开发,目标是解决当时 Apache 在高并发场景下的性能瓶颈。
主要特点
- 高并发性能
- Nginx 使用事件驱动的异步非阻塞架构,在处理成千上万并发连接时,资源占用率远低于传统的多进程/多线程服务器(如 Apache)。
- 反向代理
- 它可以作为客户端和后端服务器之间的中间层,接收请求并转发到后端应用服务器(如 Tomcat、Node.js、Flask 等)。
- 常用于隐藏后端服务器 IP、做 SSL 终止、实现请求分流。
- 负载均衡
- 支持多种策略(轮询、最少连接、IP hash 等),可将请求分配到多台服务器,从而提升系统的可用性和扩展性。
- 静态资源服务
- 对静态文件(HTML、CSS、JS、图片、视频)的处理速度非常快,常用于 CDN 和静态资源服务器。
- 模块化设计
- 可以扩展功能,例如支持缓存、限流、安全防护等。
你访问一个大型网站(比如电商平台),前端请求先到达 Nginx。
- 如果请求的是静态图片,Nginx 直接返回。
- 如果是用户下单请求,Nginx 会把请求转发给后端的应用服务器(比如 Java 的 Spring Boot 服务)。
- 如果后端有多台服务器,Nginx 会根据负载均衡策略分配请求。
什么时候不需要锁全表(绝大多数业务)
- 按主键/唯一键更新或删除:
UPDATE users SET name=? WHERE id=?;—— 只锁命中的那几行(行锁 / Next-Key 锁),其他用户照常读写。 - 插入:
INSERT只会涉及到插入点及相关索引页的锁,不会锁整表。 - 查询并修改单个用户:
为了防止并发写冲突,用事务 + 行锁即可:
- MySQL / Postgres:
SELECT ... FROM users WHERE id=? FOR UPDATE; - 乐观并发也可:加
version字段或updated_at比对。
- MySQL / Postgres:
- 批量操作但按主键或有索引过滤/分批: 分页或按范围分批更新(每批 N 条,提交一次事务),不会锁全表。
哪些情况可能需要“锁全表”或导致看起来像被锁住
这些一般是运维/DDL 级别,而不是普通业务写操作:
- 某些 DDL(表结构变更)
- MySQL InnoDB 大多数变更支持 Online DDL(如
ALGORITHM=INPLACE/INSTANT、LOCK=NONE),但并不是所有变更都无锁:- 例如 变更主键、修改列类型(复杂改动)、变更存储引擎、DROP/ADD 一些约束 等,可能需要较强的表级锁。
- 即便
LOCK=NONE,也会有元数据锁(MDL):在提交前阻止新的 DML/DDL 开始,若处理不当也会“全场卡住”的观感。
- Postgres 中如
VACUUM FULL、CLUSTER、REINDEX CONCURRENTLY(较温和,但也有约束) 等会对并发有较强影响。
- MySQL InnoDB 大多数变更支持 Online DDL(如
- 需要全局一致性的运维动作
- 全量去重/修复唯一性(比如手机号唯一,需要先清洗脏数据再加唯一索引)——为确保一致性,可能短时间阻断写入或加较强锁。
- 大范围脱敏/回填且必须“一次原子完成”的场景。通常更推荐分批 + 幂等,避免长事务/大锁。
- 没有合适索引的大批量更新/删除
- 虽然不一定是“表锁”,但会产生大量行锁 + 间隙锁,把热点区段“锁死”,外界体验像“被锁表”。
- 解决:先补索引、按主键范围分批、控制事务大小。
- 老旧或特殊引擎
- MySQL MyISAM 是表级锁;SQLite 写入时会有较大粒度的锁。生产上一般用 InnoDB/PG 避免这类问题。
Kafka 分区的作用:
场景类比:快递分拣中心
- Topic:相当于一个快递大仓库,比如「订单信息」。
- 分区(Partition):就像把仓库里的快递分到不同的传送带上(分区数=传送带数)。
- Broker:每个仓库节点,相当于一个快递站点,存储一些传送带上的快递。
- 消费者(Consumer):快递员,每个人负责一条传送带上的快递。
如果没有分区(只有 1 条传送带)
- 所有订单都堆到一条传送带上。
- 只能安排一个快递员去处理,处理速度受限。
- 仓库吞吐量有限,扩容也没用。
有了分区(比如 3 个分区 = 3 条传送带)
- 提升吞吐量:
- 同时有 3 个传送带运转,3 个快递员并行处理,速度快很多。
- 支持扩展:
- 如果订单太多,可以增加传送带数量(增加分区)和快递员数量(消费者)。
- 保证顺序:
- 同一客户的所有订单会始终进入同一条传送带(同一个分区),这样就能保证顺序不乱。
- 容错性:
- 每条传送带旁边都有备用通道(副本)。哪怕一个坏了,还有其他副本能继续处理,不会丢快递。
总结成一句话
Kafka 分区就像把快递分到多条传送带上:既能并行提高速度,又能保证同一条传送带上的顺序,还能方便扩容和容错。
1. 概念层面
- 递归 (Recursion)
- 一种 编程技巧:函数调用自身,把大问题拆成小问题。
- 关键点:有终止条件 (base case),问题规模逐渐缩小。
- 举例:斐波那契数列、树的遍历、归并排序。
- 回溯 (Backtracking)
- 一种 算法思想:在递归的基础上,通过“尝试 → 判断 → 撤销”的过程,系统地搜索解空间。
- 关键点:回退和剪枝,探索所有可能的解。
- 举例:N 皇后、全排列、子集枚举、数独、图搜索。
2. 区别总结
- 递归是工具,回溯是策略。
- 递归不一定涉及搜索,只要问题可以分解就能用递归;
- 回溯几乎都用递归实现,但它强调“试探 + 撤销”。
3. 各自适合的问题
- 递归适合:
- 问题能分解成规模更小的子问题;
- 子问题之间相对独立;
- 常见场景:树/图遍历、分治算法、动态规划。
- 回溯适合:
- 需要遍历所有可能解,或从大量候选中找到满足条件的解;
- 解空间呈树形结构,需要逐步构造解;
- 常见场景:排列组合、N 皇后、背包问题、数独求解。
7. Redis 集群(Redis Cluster)
作用:把数据分片到多台节点,既扩容内存/吞吐,也提供故障转移(高可用)。
核心机制
- 槽位分片:把 key 的 CRC16 取模到 16384 个槽,每个槽由某个主节点负责;可用 Hash Tag(如
{user:1}:profile)把一组 key 固定到同一槽位/节点。 - 主从复制:每个主节点有 0~N 个从节点;主故障时由从提升为主(投票过半)。
- Gossip + 投票:节点间互探活性、传播元数据;故障需过半主节点认定。
- MOVED/ASK 重定向:客户端访问到非负责节点时收到跳转,智能客户端会更新槽位映射。
- 在线扩缩容:迁移槽位(resharding/rehash)即可把数据在节点间移动。
优点
- 水平扩展(容量与 QPS),分区内顺序自洽,可用性高(主从 + 自动切主)。
注意点 / 踩坑
- 跨槽事务/脚本受限:多 key 操作尽量放同一 hash tag。
- 热 key:单分区热点会拖垮一个主;可做读写分离(读走从)、多副本读、或热点拆分(key 前缀打散 + 聚合)。
- 超大 value:迁槽、复制压力大,建议小而频的键值设计,或把大对象拆字段存。
- 客户端要支持 Cluster:否则不认识 MOVED/ASK。
- 网络分区与选举超时:合理设置
cluster-node-timeout,并监控复制延迟/故障切换时长。
何时用 Cluster 而非单机/哨兵
- 单实例内存接近极限、或单机 QPS 成瓶颈 → 用 Cluster 分片。
- 数据量不大只是要高可用 → 用 Sentinel(主从 + 自动切主)即可。
8. “三大缓存”与缓存一致性
这里常被问的是三种主流缓存模式(有时面试官叫“三大缓存”)+ 如何保证与数据库的一致性:
8.1 三种缓存模式(读/写路径)
- Cache-Aside(旁路/旁路缓存) – 业务最常用
- 读:先查缓存,未命中再查 DB,结果回填缓存。
- 写:先写 DB,后删缓存(推荐;删除而不是更新,避免并发脏写)。
- 优点:灵活、通用;缺点:一致性要自己保证。
- Read-Through – 由缓存层代替业务读 DB
- 读:应用只访问缓存;缓存未命中由缓存组件去加载 DB 并回填。
- 写:仍多见 “写 DB、删缓存” 或由组件代理。
- 优点:读路径简单;缺点:需要带 Loader 的中间件/代理。
- Write-Through / Write-Behind(回写/异步回写)
- Write-Through:写请求先落缓存,再同步写 DB(或由缓存组件负责两边写)。
- Write-Behind:写入缓存后异步批量刷 DB。
- 优点:写性能好(尤其 Behind);缺点:一致性/丢数据风险更高,需 MQ/落盘队列保障。
面试金句:读多写少 → Cache-Aside;延迟极致 & 接受最终一致 → Write-Behind;有统一的缓存代理/网关 → Read-Through。
8.2 缓存与数据库一致性(强/最终)
目标:避免读到过期数据或“回写覆盖”。
通用实践(按重要性)
- 写顺序:先 DB,后删缓存(而不是更新)
- 解决并发下“旧值覆盖新值”的经典问题。
- 失败重试:删除失败要有重试/补偿(重试队列、任务表)。
- 延时双删(Double-Delete)
- 写 DB → 删缓存 → 延时再删一次(例如 200~500ms)。
- 目的:覆盖并发读导致的“旧值回填”。
- Binlog 异步订阅修正(强烈推荐)
- 监听 DB binlog(如 Canal/Debezium),把变更投递到 MQ,消费者精确失效/重建缓存。
- 优点:最终一致且可靠,对业务入侵小。
在「数据库 + 缓存(Redis)」架构里,常见的痛点是:
- 数据库更新成功了,但缓存没更新/没删除,导致 缓存和数据库数据不一致。
- 如果靠业务代码里写
update db → del redis,就会有业务入侵(每个写数据库的地方都要顺带处理缓存逻辑),而且容易出错。
2. Binlog 异步订阅修正是什么?
- Binlog:MySQL 的二进制日志,记录了所有对数据库的变更(insert、update、delete)。
- 异步订阅:通过类似 Canal(阿里开源)、Debezium(Kafka 社区)这样的工具,订阅 MySQL Binlog 事件。
- 投递到 MQ:把这些变更消息(某条记录更新了/删除了)投递到消息队列(Kafka/RabbitMQ/等)。
- 消费者处理缓存:下游消费者订阅 MQ,然后精确地对 Redis 做失效/重建。
比如:
- 用户 ID=123 的记录更新了 → Binlog 事件 → Canal 捕获 → 推送到 MQ → 消费者收到 →
DEL redis:user:123。
3. 优点
- 最终一致且可靠
- Binlog 是 MySQL 的事实来源(ground truth),不会漏。
- 即使业务代码没管缓存,Binlog 订阅也能保证缓存和数据库最终一致。
- 对业务入侵小
- 业务代码只管写数据库,不用到处加
del redis。 - 缓存修正由异步订阅系统负责,和业务解耦。
- 业务代码只管写数据库,不用到处加
- 解耦 + 可扩展
- 除了缓存修正,还能把 Binlog 投递给别的系统(搜索引擎、实时计算、数据同步等)。
4. 总结一句话
👉 Binlog 异步订阅修正 = 监听数据库 Binlog → 通过 Canal/Debezium 把变更投递到 MQ → 消费者更新/失效缓存。 好处是 最终一致、可靠、低业务入侵,是业界强烈推荐的「DB-Cache 一致性方案」。
- TTL + 逻辑过期
- 设置合理 TTL 兜底;对热点可用逻辑过期(值+过期时间),过期后由单协程重建,其余读老值,避免击穿。
- 防并发写脏
- 分布式锁/乐观锁(版本号、CAS)防止多写覆盖;
- 幂等(按业务主键/请求 ID)。
- 读写隔离与一致性选择
- 读多写少常选最终一致(性能优先);金融/下单等关键路径用“强一致”:
- 写:DB 成功→删缓存成功才返回;
- 读:强制读 DB 或携带版本戳校验。
- 读多写少常选最终一致(性能优先);金融/下单等关键路径用“强一致”:
8.3 典型故障与治理
- 缓存击穿(单热点 key 过期瞬间大量穿透 DB)
- 热点互斥重建(锁)、逻辑过期、预热。
- 缓存雪崩(大量 key 同时过期)
- TTL 加随机抖动;多层缓存;限流/降级;分批加载。
- 缓存穿透(查询不存在的数据)
- 缓存空值(短 TTL)、布隆过滤器、参数校验。
JVM 内存主要分为 线程私有 和 线程共享 两大类区域:
- 线程私有:每个线程独立拥有,随线程创建/销毁。
- 程序计数器 (PC Register)
- 虚拟机栈 (JVM Stack)
- 本地方法栈 (Native Method Stack)
- 线程共享:所有线程可见,随 JVM 启动/关闭。
- 堆 (Heap)
- 方法区 (Method Area,JDK 8 之后改为 Metaspace)
1. 指针
- 和 C 的区别
- Go 有指针,但 不能做指针运算(比如
p++是非法的),只能取地址和解引用。 - 这样设计是为了安全和简化。
- Go 有指针,但 不能做指针运算(比如
- 值类型 vs 引用类型
- 值类型:
int,float,bool,array,struct—— 赋值时会拷贝一份数据。 - 引用类型:
slice,map,channel,func—— 本质上是一个指针+结构体封装,赋值时拷贝的是“引用”,底层数据共享。
- 值类型:
- 常见考点
- 函数参数如果是值类型,修改不会影响外部;如果传指针或引用类型,外部数据可能被修改。
2. 切片 (slice) & 数组
-
数组:长度固定,属于值类型,
[3]int和[4]int是不同类型。 -
切片:动态视图,本质结构:
type slice struct { ptr *T // 底层数组的指针 len int // 当前切片长度 cap int // 底层数组容量 } -
扩容机制
append超过容量时,会重新分配新数组:- 小于 1024 时,一般按 2 倍 扩容。
- 超过 1024 时,按 1.25 倍左右 扩容。
- 扩容会产生新底层数组,旧的数组数据拷贝过去 → 所以 append 后原切片和新切片可能不再共享数据。
-
常见考点
- 切片拷贝/append 后原数据是否改变。
- 切片 reslice 不会复制底层数组。
3. map
- 底层实现
- Go 的
map是哈希表,采用 哈希桶(bucket) 存储。 - 每个 bucket 存最多 8 个 kv 对,溢出时挂溢出链。
- Go 的
- 扩容
- 当装载因子过大时触发扩容,新建更大容量的 buckets,并逐步迁移数据(增量扩容,避免 STW)。
- 遍历无序
- Go 特意让
map遍历无序(每次都打乱),防止开发者依赖遍历顺序。
- Go 特意让
- 键必须可比较
- 可以作为 key 的类型:布尔、数值、字符串、指针、channel、接口、struct、array(前提是内部字段都可比较)。
- 不可作为 key:
slice,map,func。
扩容的时候发生了什么?
- 触发条件:装载因子太大(元素数 / bucket 数超过阈值,大约 6.5)。
- 扩容方式:哈希表会新建一个 桶数量翻倍 的数组。
- 原来可能是 8 个 bucket,扩容后变成 16 个 bucket。
- 数据迁移:旧 bucket 里的 kv 会逐步搬到新 bucket 里(渐进式,避免一次性 STW 卡顿)。
举个例子
- 假设你有一个
map[string]int,初始有 8 个 bucket。 - 每个 bucket 最多 8 个 kv 对(再多就溢出)。
- 当你不停往 map 里插入数据,装载因子超过阈值,就会扩容。
- 扩容后,bucket 数量翻倍,比如从 8 → 16。
- 但 每个 bucket 还是只能放 8 个 kv 对,这个上限不会变。
1. 溢出桶(overflow bucket)
- 当某个 bucket 已经装满 8 个 kv 对,再有新的 key 映射到这个 bucket 时,Go 会在该 bucket 后面挂一个 溢出桶 (overflow bucket)。
- 新的 kv 就存放在溢出桶里。
- 一个 bucket 可以挂多个溢出桶,形成链表。
结构大致像这样(伪图):
[主 bucket] -> [overflow bucket1] -> [overflow bucket2] -> ...
4. 字符串
-
不可变
-
Go 的
string是只读字节序列,底层结构:type string struct { ptr *byte // 指向底层数组 len int } -
一旦创建不可修改,只能重新构造新字符串。
-
-
和
[]byte/[]rune的关系-
[]byte:字节切片,按 UTF-8 编码存储。 -
[]rune:Unicode 码点切片,解决中文/emoji 占多个字节问题。 -
典型考点:
s := "你好" fmt.Println(len(s)) // 6(UTF-8 占 3+3 字节) fmt.Println(len([]rune(s))) // 2(两个 Unicode 字符)
-
5. 接口 (interface)
- 底层实现
- 接口分两种:
- 空接口:
interface{},结构体是(type, data)。 - 非空接口:带方法表
(itab, data),itab 里存类型和方法指针。
- 空接口:
- 调用接口方法时通过 动态派发(查 itab)。
- 接口分两种:
- 接口断言
x.(T):断言接口保存的动态类型是T。x.(T)会 panic,x.(T, ok)返回布尔值避免 panic。
- 常见考点
- 空接口可以装任何值。
nil interface和interface holding nil的区别:前者为nil,后者不为nil。
6. defer / panic / recover
-
defer
-
先进后出(LIFO)。
-
会在函数返回前执行,即使遇到
panic也会执行。 -
和
return结合时:先计算返回值 → 再执行 defer → 再返回。func f() (x int) { defer func() { x++ }() return 1 // 返回前 x=1,defer 修改后 x=2 }
-
-
panic
- 类似抛异常,会中止正常执行,沿调用栈向上传递。
-
recover
- 必须在 defer 中调用,捕获 panic 并恢复程序。
- 超出 defer 后调用 recover 无效。
1. goroutine
- 定义:Go 的轻量级线程,运行在用户态,由 Go runtime 调度。
- 创建:
go f(),开销比 OS 线程小得多(初始栈约 2KB,会动态扩容)。 - 调度模型 GMP:
- G:goroutine(协程)。
- M:machine(内核线程,真正执行 G 的载体)。
- P:processor(逻辑调度器,保存本地运行队列,负责把 G 分配给 M)。
- 数量关系:
M数量受 CPU 核数 & GOMAXPROCS 限制,P一般等于 GOMAXPROCS。
👉 考点:为什么 Go 能开成千上万个 goroutine,而 Java/C++ 线程不行? ➡️ 因为 goroutine 栈很小且可动态增长,调度在用户态完成,开销远小于内核线程。
2. channel
-
定义:Go 的 CSP 模型核心,goroutine 之间通信的管道。
-
无缓冲 channel:
-
发送
ch <- v必须等接收<- ch同时就绪,否则阻塞。 -
实现 goroutine 间的同步。
-
ch是一个 channel。ch <- v表示 把值 v 发送到 channel ch。发送之后,另一个 goroutine 可以用
<-ch把这个值取出来。
-
-
有缓冲 channel:
ch := make(chan int, 3)→ 容量 3。- 发送时:如果没满,不阻塞;满了才阻塞。
- 接收时:如果没空,不阻塞;空了才阻塞。
-
close 的作用:
close(ch)表示不能再发送,但还能接收剩余数据。- 用法:
v, ok := <-ch,如果 channel 已关闭且没数据,ok=false。
👉 考点:
- 读一个关闭的 channel → 读到零值,不 panic。
- 写一个关闭的 channel → panic。
1. 无缓冲 channel
特点
- 同步通信:发送必须等接收,就像一个“握手协议”。
- 能天然保证 发送方和接收方在同一时间点交互数据。
- 发送的值不会存储,必须有接收者马上消费。
典型应用场景
-
任务同步/信号通知
- 比如一个 goroutine 完成了任务,要通知另一个 goroutine:
done := make(chan struct{}) go func() { work() done <- struct{}{} // 通知完成 }() <-done // 等待通知 -
生产者-消费者一对一传递
- 适合实时、点对点的数据交付。
- 确保“谁发的,谁收到了”,不会堆积。
-
控制并发节奏
- 用无缓冲 channel 让 goroutine 按顺序执行,而不是乱跑。
👉 可以理解为“电话沟通”:必须两边同时在线才能传话。
2. 有缓冲 channel
特点
- 异步通信:发送方只要缓冲区没满就可以立刻返回。
- 值可以临时存储在缓冲区里,接收方晚点消费也没关系。
- 更像“消息队列”。
典型应用场景
-
生产者-消费者(异步)
- 多个生产者 goroutine 往里写,消费者 goroutine 慢慢取:
ch := make(chan int, 100) go producer(ch) go consumer(ch) -
任务队列/工作池
- 把待处理任务放到有缓冲 channel,工人 goroutine 从中取任务并处理。
- 类似一个简易版的队列。
-
削峰填谷
- 短时间内生产速度快于消费速度,用缓冲来“平滑”压力。
- 比如日志收集、网络请求缓存。
👉 可以理解为“邮箱”:发信人可以先丢进去,收信人慢慢取。
3. 对比总结
| 类型 | 特点 | 应用场景 |
|---|---|---|
| 无缓冲 channel | 发送接收必须同时就绪;同步 | 信号通知、顺序控制、即时任务交付 |
| 有缓冲 channel | 缓冲区存储;异步 | 消息队列、工作池、削峰填谷 |
3. select
- 作用:同时监听多个 channel 的收发操作。
- 随机公平性:多个 case 就绪时,会随机选一个执行,避免饥饿。
- default 分支:所有 case 都阻塞时,执行 default。
👉 常见场景:超时控制
select {
case v := <-ch:
fmt.Println(v)
case <-time.After(1 * time.Second):
fmt.Println("timeout")
}
4. context
- 作用:跨 goroutine 的取消信号 & 超时控制。
- 常用方法:
context.Background():根 context。context.WithCancel(parent):手动取消。context.WithTimeout(parent, d):超时自动取消。<-ctx.Done():接收取消信号。
👉 典型用法:
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
select {
case <-doSomething():
case <-ctx.Done():
fmt.Println("timeout")
}
5. sync 包
(1) Mutex vs RWMutex
sync.Mutex:互斥锁,保证同一时间只有一个 goroutine 访问。sync.RWMutex:读写锁,允许多个读并发,写独占。
👉 使用场景:读多写少时 RWMutex 效果好;写多时反而可能更慢。
(2) WaitGroup
- 用于等待一组 goroutine 结束。
var wg sync.WaitGroup
wg.Add(3)
go func(){ defer wg.Done(); work() }()
go func(){ defer wg.Done(); work() }()
go func(){ defer wg.Done(); work() }()
wg.Wait()
(3) Once
- 保证某个操作只执行一次(如单例初始化)。
var once sync.Once
once.Do(func(){ initConfig() })
(4) Cond
- 条件变量,用于复杂同步。
cond := sync.NewCond(&sync.Mutex{})
cond.L.Lock()
for !condition {
cond.Wait() // 等待唤醒
}
cond.L.Unlock()
cond.Signal() // 唤醒一个
cond.Broadcast() // 唤醒全部
(5) Map
- 并发安全的 map(内部用分片+原子操作)。
- 适合读多写少场景。写多场景用自己加锁的 map 更好。
6. 原子操作
- 包:
sync/atomic,底层用 CPU 原子指令(CAS)。 - 常用函数:
atomic.AddInt32(&x, 1):原子加。atomic.LoadInt32(&x):原子读。atomic.StoreInt32(&x, v):原子写。atomic.CompareAndSwapInt32(&x, old, new):CAS 操作。
👉 面试常考点:用 atomic 实现自旋锁、计数器。
✅ 总结一张表:
| 模块 | 核心点 | 考点 |
|---|---|---|
| goroutine | GMP 模型 | 为什么能开很多协程 |
| channel | CSP 模型 | 无缓冲 vs 有缓冲,close 行为 |
| select | 多路复用 | 随机公平,超时控制 |
| context | 跨协程控制 | cancel, timeout, deadline |
| Mutex/RWMutex | 锁机制 | 读写性能对比 |
| WaitGroup | 协程等待 | Add 和 Done 配合 |
| Once | 单例 | 保证只执行一次 |
| Cond | 条件变量 | 唤醒机制 |
| Map | 并发安全 map | 读多写少 |
| atomic | 原子操作 | CAS, 计数器 |
1. GMP 调度模型
- G:goroutine(协程)。
- M:machine(内核线程,真正执行 G 的载体)。
- P:processor(逻辑调度器,保存就绪的 G 队列,决定哪个 G 给哪个 M 执行)。
关系:
- G 一定要绑定到 P,才能被 M 执行。
- P 的数量 =
GOMAXPROCS(默认等于 CPU 核数)。 - M 数量不固定,可以比 CPU 多。
为什么高效?
- Go 自己管理 goroutine 调度,不依赖 OS 内核调度。
- goroutine 栈很小(2KB 起),能动态增长。
- 上下文切换在用户态完成,比线程切换轻量。
2. 垃圾回收 (GC)
Go 的 GC 是 并发标记清除,核心是 三色标记法 + 写屏障。
三色标记
- 白色:未标记对象,可能是垃圾。
- 灰色:已发现但子节点未扫描的对象。
- 黑色:已扫描完成,不会被回收。
流程:
- GC 起始时,根对象(全局变量、栈)标记为灰色。
- 遍历灰色对象,把它们引用的对象标记成灰色,然后自身变黑。
- 最后白色对象就是垃圾,被回收。
写屏障
- 在 GC 过程中,程序还在运行(mutator)。
- 写屏障确保:只要一个对象能从黑色对象访问到,它就不会漏标。
- 这样 GC 和用户程序可以并发执行。
4. 内存对齐
- Go struct 的字段存放需要 按照类型对齐,保证 CPU 访问高效。
- 规则:
- 每个字段的地址必须是该字段大小的整数倍。
- 整个 struct 的大小必须是最大字段大小的整数倍。
例子:
type A struct {
a int8 // 1B
b int64 // 8B
c int8 // 1B
}
fmt.Println(unsafe.Sizeof(A{})) // 输出 24
解释:
a占 1B,但要对齐到 8B → 前后填充 7B。b占 8B → 正常放置。c占 1B,但 struct 总大小必须是 8 的倍数 → 填充到 24B。
👉 优化办法:把大的字段放前面,小的放后面,减少填充。
Go 的 net/http 库底层是如何和网络沟通的。面试里这一块也常会考。简单讲,它其实是对 TCP 套接字 (socket) 的一层封装。
1. 启动 HTTP 服务的入口
最常见的启动方法是:
http.ListenAndServe(":8080", handler)
它背后做了几件事:
- 创建监听器:调用
net.Listen("tcp", ":8080"),监听 TCP 端口。 - 循环 accept:不断
Accept()等待新的客户端连接。 - 每个连接开一个 goroutine:并发处理请求。
- 读写数据:在 goroutine 内,对 socket 做
Read/Write,并解析 HTTP 协议。
2. 底层网络调用流程
Go 的 net 包其实就是对 系统调用(syscall) 的封装:
- 服务端:
net.Listen("tcp", ":8080")→ 调用内核的socket()、bind()、listen()。Accept()→ 调用内核的accept(),拿到客户端的 socket。
- 客户端:
net.Dial("tcp", "host:port")→ 调用socket()、connect()。
- 数据传输:
conn.Read(b)→ 调用内核read(),从 TCP 缓冲区读数据。conn.Write(b)→ 调用内核write(),把数据写到 TCP 缓冲区,由内核协议栈发出去。
3. Go 的并发网络模型
- Go 的网络 I/O 并不是“一个连接一个线程”,而是基于 epoll/kqueue/IOCP 的多路复用。
- Runtime 里有 netpoller:
- Linux → 用 epoll
- macOS → 用 kqueue
- Windows → 用 IOCP
- 每个连接被注册到内核的事件通知机制上,I/O 就绪后由 runtime 唤醒对应的 goroutine 处理。
👉 这就是为什么 Go 的 http server 可以轻松支撑成千上万并发连接 —— goroutine 是轻量的,I/O 调度靠事件驱动,而不是线程阻塞。
4. Handler 的调用链
当数据读到用户态,net/http 会:
- 解析 HTTP 请求行、Header、Body → 生成
http.Request对象。 - 调用用户注册的
Handler.ServeHTTP(w, r)。 - 用户写
w.Write()→ 最终写回到 TCP 连接。
✅ 总结一句话:
Go 的 net/http 底层是用 net 包的 TCP socket 实现的,连接管理靠 epoll/kqueue/IOCP,I/O 事件由 runtime 的 netpoller 分发给 goroutine,最终由用户定义的 Handler 来处理业务逻辑。
1. goroutine 泄漏场景与排查方法
- 泄漏场景:
- 阻塞在 channel 读写上,没人来匹配。
- 无限循环里没有退出条件。
- HTTP 请求/数据库连接没关闭。
- 排查方法:
pprof分析 goroutine 数量 (runtime.NumGoroutine(),或go tool pprof)。- 用
golang.org/x/net/trace或 log 打印栈追踪。
👉 面试答法:goroutine 不会被 GC 自动回收,必须保证退出机制,比如用 context 控制生命周期。
2. channel 死锁的几种情况
- 在无缓冲 channel 上,只有发送,没有接收。
- 只有接收,没有发送。
- 有缓冲 channel 满了还继续写,没人读。
- 空 channel 一直读,没人写。
close后继续写入 → panic(严格来说是 runtime error,也会被当成“死锁”场景)。
3. interface 底层实现机制
- 接口底层由两部分组成:
- 非空接口:
itab (type + method table)+data。 - 空接口:
type+data。
- 非空接口:
- 调用接口方法时,通过 itab 的方法表动态派发。
👉 常考陷阱:
nil interface和interface holding nil不一样:前者完全为 nil,后者 type 有值但 data 是 nil。
4. slice 扩容机制,append 触发条件
- 触发条件:
len+1 > cap。 - 扩容策略:
- 小于 1024 → 容量 *2。
- ≥1024 → 容量 *1.25 左右。
- 新建底层数组,拷贝原数据。旧切片和新切片分离。
5. map 并发读写问题,如何解决
- Go 的普通 map 不是线程安全的。并发读写会报错:
fatal error: concurrent map read and map write。 - 解决办法:
- 加锁:
sync.Mutex/sync.RWMutex。 - 用并发安全的
sync.Map(适合读多写少)。
- 加锁:
6. defer 的执行顺序,和 return 结合时的执行时机
- 顺序:先进后出(LIFO)。
- 执行时机:函数 return 时 → 先计算返回值 → 再执行 defer → 最后返回。
例子:
func f() (x int) {
defer func() { x++ }()
return 1
}
// 返回 2
7. 内存逃逸分析的例子
- 编译器会决定变量在栈/堆上的分配。
- 典型例子:
func f() *int {
x := 10
return &x // x 逃逸到堆
}
func g() int {
x := 10
return x // x 在栈上
}
👉 可以用 go build -gcflags=-m main.go 查看逃逸情况。
8. select 随机性以及默认分支
- 多个 case 就绪时:Go 会随机选择一个执行(防止饿死)。
- default 分支:如果所有 case 都阻塞,就执行 default,相当于非阻塞 select。
9. Go 的 GC 与 C++/Java 的区别
- C++:手动管理内存,或用智能指针。
- Java:GC 停顿明显,分代 GC(新生代、老年代)。
- Go:三色标记 + 写屏障,并发 GC,追求 低延迟(STW 时间小于 1ms)。
10. Go 的零值机制,为什么避免了“野指针”问题
- Go 中所有变量在声明时都会初始化为 零值:
- int → 0
- string → “”
- bool → false
- 指针、slice、map、chan、func、interface → nil
- 好处:不用担心未初始化的随机值,避免 C 语言常见的“野指针”。
常见性能优化手段
-
减少逃逸 逃逸分析:决定变量分配在栈还是堆。
- 栈分配:快,随函数返回自动释放
- 堆分配:需要 GC,代价大 优化:尽量避免返回局部变量的指针、接口存储值、闭包引用大对象。
-
减少 GC 压力
- 复用对象(对象池、sync.Pool)
- 避免短生命周期的大量小对象
-
使用
sync.Pool- 适合缓存临时对象,减少频繁分配/回收
- 但 GC 会清空池,不能依赖它做长期缓存
var pool = sync.Pool{New: func() interface{} { return new(bytes.Buffer) }} buf := pool.Get().(*bytes.Buffer) buf.Reset() pool.Put(buf) -
合理使用 channel vs 锁
- channel 适合:goroutine 间数据传递(CSP 模型)
- 锁更快:在共享内存读写简单时,用 Mutex/RWMutex 更高效
- 面试官常问:“能不能用 channel 替代锁?” → 可以,但性能未必更好
🌰 情况一:goroutine 相互等待资源(两个人互相等钥匙)
想象一下:
- 有两把钥匙:钥匙 A、钥匙 B。
- 小明拿到了钥匙 A,准备去开一扇需要 A+B 的门,他伸手去要钥匙 B。
- 小红拿到了钥匙 B,也要去开同一扇门,她伸手去要钥匙 A。
结果呢? 👉 小明说:“你先给我钥匙 B,我再给你钥匙 A。” 👉 小红说:“你先给我钥匙 A,我再给你钥匙 B。”
谁也不松手,双方僵住了。门永远打不开。
这就是 两个 goroutine 拿锁顺序不一致 → 相互等待 → 死锁。
避免方法:规定规则,比如“大家必须先拿钥匙 A,再拿钥匙 B”。这样就不会互相僵持。
🌰 情况二:channel 双向阻塞(两个人互相等对方说话)
想象一下:
- 有两个人面对面聊天,规则是 必须先听到对方说话,自己才能说话。
- 小明想先说,但他得等小红说。
- 小红也想说,但她也得等小明先说。
于是两人都张着嘴巴,互相盯着,尴尬沉默,谁都不说话。
👉 这就是 无缓冲 channel 的读写双方都在等 → 死锁。
解决方法:
-
给 channel 放个“缓冲座位”,就像中间放个录音机,可以先把话录下来,再轮流听:
ch := make(chan int, 1) ch <- 1 // 先写进去 fmt.Println(<-ch) // 再读出来 -
或者加个超时: 就像“如果 3 秒没人说话,我就自己走人”。
下面把“Redis 集群一致性”拆成 4 个层面:写一致性、读一致性、持久化一致性、迁移/故障一致性,并给出可落地做法。
1) 写一致性(主从复制)
-
Redis Cluster 的复制是异步的 ⇒ 天生是最终一致,主节点写成功并不保证副本立刻有。
-
可用两种手段把“最终一致”收紧到“准同步”:
-
WAIT <numreplicas> <timeout>:写后阻塞到有 N 个副本确认接收(只是“收到”,不是落盘)。SET k v WAIT 1 50 # 等 1 个副本在 50ms 内确认适合关键写路径(但会增加写时延)。
-
写口子控制(老版本叫
min-slaves-to-write/min-slaves-max-lag):min-replicas-to-write 1min-replicas-max-lag 2含义:若可用副本数 < 1 或主从延迟 > 2s,则拒绝写入,防止孤岛写入造成更大不一致。
-
2) 读一致性(主/从读与读己之)
- 强烈建议:读走主(尤其是写后立刻读的请求),保证读己之(Read-Your-Writes)。
- 若必须读从(减压/就近):
- 接受可能的读陈旧;或在重要读前先
WAIT收紧复制,再读从; - 使用客户端“读偏好”策略:更新后短时间强制读主、或附加版本/时间戳检查回退读主;
- 可用
CLIENT CACHING/ 失效推送(或应用本地 cache+订阅失效)做缓存一致性。
- 接受可能的读陈旧;或在重要读前先
3) 持久化一致性(崩溃后是否一致)
- 打开 AOF,并权衡
appendfsync:always:最安全、最慢;everysec(常用):最多丢 1s 日志;no:性能最好、风险最大。
- 为降低 fork 开销与传播时延,可配
repl-diskless-sync yes(无盘复制快一些,但与一致性关系间接)。 - 如果你已经用
WAIT保证 N 个副本都“接收”,仍不能 100% 防断电丢失,除非副本也落盘成功(Redis 没有多副本“落盘确认再返回”的强一致路径)。
4) 迁移/故障切换一致性
- 槽位迁移使用
MIGRATING/IMPORTING + ASK:迁移期间客户端收到 ASK 重定向,单 Key 操作仍原子,不会出现半迁移读写错位。 - 故障转移:Replica 选主需要集群多数投票;因为复制异步,主挂前最后一批写可能丢失(常见“少量回滚”)。
- 缩小窗口:
- 合理设置
cluster-node-timeout(更快探测=更快切主,但误判风险 ↑); - 写前/后配合
WAIT; min-replicas-*防孤岛写;- 业务端对关键资源加 幂等键 / 版本号(CAS),允许重试+去重。
- 合理设置
- 缩小窗口:
- 多键原子性限制:多键事务、Lua、pipeline 只在同槽才能保证原子与一致。用 hash tag(如
user:{42}:profile,user:{42}:orders)把相关键锁定到一个槽。
5) 应用侧“补齐一致性”的通用做法
- 幂等写:用业务唯一键(如订单号/请求 ID),重复请求不副作用。
- 版本化更新(CAS):把版本号放在 value/哈希字段里,
WATCH/Lua 检查版本一致再更新。 - 双写/旁路缓存:先写 DB 再删/设 Redis,或采用延迟双删(写后删缓存 + 延迟再删一次)减少脏读。
- 读己之保障:写成功后 N 秒内强制读主或携带写入版本做校验。
- 审计与修复:离线对账(Hive/ClickHouse)发现不一致→后台修复任务(你已有成熟经验,可以把 Redis Cluster 的对账也纳入)。
🔹 (1) 全量复制(初次同步 / 断线重连)
- 从节点发送
PSYNC或SYNC给主节点。 - 主节点执行
bgsave生成 RDB 快照,并把快照文件发送给从节点。 - 在生成快照期间,主节点会把新的写操作写入 复制缓冲区 (replication backlog buffer)。
- 从节点加载 RDB 文件,更新数据。
- 从节点再接收缓冲区里的增量命令,保证数据和主节点一致。
🔹 (2) 增量复制(部分同步)
- 如果主从之间网络闪断,从节点重连时可以尝试部分同步:
- 主节点维护一个 复制偏移量 offset 和 replication backlog buffer(固定大小的环形缓冲区)。
- 从节点重连时带上自己上次的 offset。
- 如果这个 offset 仍在主节点的 backlog buffer 范围内,主节点就只发缺失的数据。
- 否则,执行全量同步。
3. 主从复制的实现细节
- 复制 ID
- 每个主节点有一个 replication ID。
- 主节点重启时会生成新的 ID,从节点可用这个 ID 判断是否能部分同步。
- 异步复制
- 默认是异步,主节点不等待从节点确认就返回客户端。
- 可以配置
WAIT命令来要求同步到多少个从节点才返回。
- 读写分离
- 主节点负责写,从节点可以读,提高吞吐量。
- 缺点:可能读到旧数据(延迟复制)。
1. TCP 头部字段(最小 20 字节,常见如下)
| 字段 | 长度 | 作用 |
|---|---|---|
| 源端口号 (Source Port) | 16 bit | 标识发送端应用进程 |
| 目的端口号 (Destination Port) | 16 bit | 标识接收端应用进程 |
| 序号 (Sequence Number) | 32 bit | 标识报文段中第一个字节的序号,用于数据重组 |
| 确认号 (Acknowledgment Number) | 32 bit | 期望收到的下一个字节序号,用于确认机制 |
| 首部长度 (Data Offset) | 4 bit | TCP头部自身长度(单位为 4 字节) |
| 保留 (Reserved) | 6 bit | 保留未使用,置0 |
| 控制位 (Flags) | 6 bit(或 9 bit, 取决于表示法) | URG, ACK, PSH, RST, SYN, FIN 等,控制连接和数据流 |
| 窗口大小 (Window Size) | 16 bit | 通知对方可接收数据的字节数(流量控制) |
| 校验和 (Checksum) | 16 bit | TCP首部 + 数据的校验 |
| 紧急指针 (Urgent Pointer) | 16 bit | 当 URG=1 时,指向紧急数据末尾 |
| 选项 (Options) | 可变 | 如最大报文段长度 (MSS)、窗口扩大因子、时间戳等 |
| 填充 (Padding) | 可变 | 保证头部长度是 4 字节整数倍 |
2. UDP 头部字段(固定 8 字节)
| 字段 | 长度 | 作用 |
|---|---|---|
| 源端口号 (Source Port) | 16 bit | 标识发送端应用进程(可为 0,表示未指定) |
| 目的端口号 (Destination Port) | 16 bit | 标识接收端应用进程 |
| 长度 (Length) | 16 bit | UDP头部 + 数据的总长度 |
| 校验和 (Checksum) | 16 bit | UDP首部 + 数据校验(IPv4 可选,IPv6 必须) |
1. Spring Boot 是怎么加载 Bean 的?
- 启动过程:Spring Boot 启动时,会加载
ApplicationContext→ 通过类路径扫描和自动配置发现要托管的类。 - 加载机制:
- 扫描带有
@Component、@Service、@Repository、@Controller等注解的类; - 解析
@Configuration中的@Bean方法; - 根据
spring.factories/@EnableAutoConfiguration加载自动配置类; - 统一注册到 BeanDefinitionMap,通过 反射 或 CGLIB 动态代理创建对象并注入依赖。
- 扫描带有
2. 反射为啥会影响性能?
- 正常调用:方法调用是 JVM 已优化的直接调用(有内联、JIT 优化)。
- 反射调用:绕过编译期检查 → 运行时查找类信息、校验安全性、方法分派,需要频繁访问 Method/Field 元信息表。
- 影响:多了一层动态解析,CPU cache 友好性差,JIT 优化也受限 → 性能比直接调用低。
3. 线程安全的工作原理是啥?
- 定义:多线程访问同一对象/方法时,不会出现数据不一致、状态错乱。
- 实现方式:
- 互斥同步:如
synchronized、ReentrantLock,通过锁保证同一时间只有一个线程访问。 - 非阻塞同步:CAS(Compare-And-Swap)+ 原子类,保证更新操作的原子性。
- 线程封闭:线程私有变量,避免共享。
- 不可变对象:对象状态不可修改,天然安全。
- 互斥同步:如
4. 主内存和工作内存(JMM)
- 主内存:所有共享变量存放的地方,所有线程可见。
- 工作内存:每个线程的本地副本(类似 CPU 缓存),线程对变量操作时必须从主内存拷贝到工作内存,再回写。
- 关键点:线程不能直接操作主内存,只能通过工作内存;
volatile/synchronized保证内存可见性和有序性。
9. 网络编程里的 IO 模型
常见五种:
- 阻塞 IO(BIO):调用阻塞,直到数据就绪。
- 非阻塞 IO:调用立即返回,需轮询。
- IO 多路复用:
select/poll/epoll,一个线程管理多个连接。 - 信号驱动 IO:数据就绪时内核发信号通知应用。
- 异步 IO(AIO):应用提交请求后,内核完成后直接通知应用处理。
11. TCP 是怎么保证可靠传输的?
- 分片与序号:数据拆分为段,带序号,接收方按序重组。
- 确认应答(ACK):接收方收到数据后发 ACK,丢失则重传。
- 超时重传:发送方超时未收到 ACK,自动重发。
- 流量控制:滑动窗口机制,防止发送方压垮接收方。
- 拥塞控制:慢启动、拥塞避免、快重传、快恢复,避免网络过载。
- 校验和:保证传输过程数据未被篡改。
流量控制 (Flow Control)
- 目标:防止发送方把数据发得太快,把接收方“撑爆”。
- 机制:由 接收方 通知发送方自己能接收多少数据。
- TCP实现:
- 接收方在 TCP 头部的 Window Size 字段告诉对方自己的接收缓冲区大小。
- 发送方根据这个窗口大小,控制发送速率。
- 如果窗口为 0,表示暂时不能收,发送方会停发,等对方发 窗口更新 再继续。
- 类比:水桶装水,接收方是桶,桶快满了就喊“慢点”,否则会溢出。
拥塞控制 (Congestion Control)
- 目标:防止在网络中注入过多数据,把整个网络“挤爆”。
- 机制:由 发送方 自己感知网络状况来调节速率。
- TCP实现(四大算法):
- 慢启动 (Slow Start):开始时窗口指数级增长。
- 拥塞避免 (Congestion Avoidance):接近阈值后线性增长。
- 快重传 (Fast Retransmit):收到 3 个重复 ACK 立即重传,不等超时。
- 快恢复 (Fast Recovery):拥塞发生后,减小窗口一半而不是清零。
- 类比:高速公路上的车流,发现堵车就得减速,避免进一步拥堵。
✅ 窗口是不是固定的?
不是固定的。
- 窗口大小 会随着网络和接收方的处理能力动态变化。
- 接收方在 TCP 报文头里的 Window Size 字段里告诉发送方自己当前还能接多少数据。
- 另外 TCP 发送方还会维护一个 拥塞窗口(cwnd),受网络状况控制。 最终生效的是:
发送窗口大小 = min(接收窗口, 拥塞窗口)
也就是说,既不能超过接收方的承受能力,也不能超过网络的拥堵程度。
✅ 窗口长度怎么算?
是的,你理解的没错,窗口长度 = 已发送未确认的数据 + 还能发送的数据。
1. synchronized 的特点
- 关键字层面:是 Java 内置关键字,JVM 层面支持,语法简单(方法或代码块加
synchronized)。 - 锁对象:锁定的是对象(实例对象锁或类对象锁)。
- 可重入:同一线程可以重复获取同一把锁,不会死锁。
- 自动释放:代码块/方法执行完自动释放锁,不需要手动操作。
- 性能:JDK1.6 之后经过偏向锁、轻量级锁、自旋锁优化,性能已经大幅提升。
- 限制:功能单一,无法中断等待的线程、无法设置超时、公平性不可控。
2. ReentrantLock 的特点
- 类层面:是
java.util.concurrent.locks包下的一个实现类,显示地调用lock()和unlock()来加解锁。 - 可重入:同样支持可重入。
- 高级功能:
- 可中断:支持
lockInterruptibly(),线程可被中断。 - 超时获取:支持
tryLock(long timeout, TimeUnit unit),超时放弃。 - 公平锁/非公平锁:构造时可指定,保证先来先得。
- 条件队列:配合
Condition,比Object.wait/notify更灵活。
- 可中断:支持
- 需要手动释放锁:如果
unlock()忘记调用,可能导致死锁。
3. 两者对比
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 使用方式 | 关键字,自动释放 | 显式 lock/unlock |
| 可重入 | 支持 | 支持 |
| 可中断 | 不支持 | 支持 |
| 超时获取 | 不支持 | 支持 |
| 公平性 | 不支持 | 支持 |
| 条件队列 | wait/notify |
Condition 多路等待 |
| 性能 | JDK1.6 后优化,常用场景足够 | 在高并发/复杂控制下更灵活 |
1. 什么是 AQS?
- AQS (AbstractQueuedSynchronizer) 是 JDK 并发包中的一个抽象类。
- 核心思想:用一个 volatile 状态值 + FIFO 双向队列 来管理多线程的竞争和唤醒。
- 提供 独占模式(exclusive,如
ReentrantLock)和 共享模式(shared,如Semaphore、CountDownLatch)两种资源获取方式。
2. AQS 的核心组成
- state 变量
volatile int state;- 表示同步状态,比如锁是否被占用、剩余的许可数等。
- 通过 CAS (compare-and-swap) 保证修改的原子性。
- CLH 队列
- AQS 使用 CLH(双向链表队列) 来保存等待线程。
- 每个线程排队时会封装成一个
Node,放入队尾。 head和tail指针维护队列。
- Node 节点
- 保存线程本身(
Thread)、等待状态(waitStatus)、前驱/后继引用。 - 状态标记有:
SIGNAL(等待唤醒)、CANCELLED(取消等待)、CONDITION(在条件队列中)等。
- 保存线程本身(
3. 获取锁(acquire)的过程(独占模式为例)
- 尝试获取资源
- 调用
tryAcquire()(子类实现,如ReentrantLock)。 - 如果成功:直接返回。
- 如果失败:进入等待队列。
- 调用
- 入队
- 将当前线程封装为
Node,CAS 插入到队尾。
- 将当前线程封装为
- 自旋等待
- 线程在队列中循环检查是否能获取到锁:
- 如果前驱是
head且资源可用,则 CAS 修改state成功并获取锁。 - 否则阻塞(
LockSupport.park())。
- 如果前驱是
- 线程在队列中循环检查是否能获取到锁:
- 唤醒
- 前驱节点释放锁时,会唤醒后继节点(
LockSupport.unpark()),后继线程继续尝试获取锁。
- 前驱节点释放锁时,会唤醒后继节点(
4. 释放锁(release)的过程
- 修改 state
- 调用
tryRelease()(子类实现)。 - 如果
state == 0(完全释放),说明锁已可用。
- 调用
- 唤醒后继节点
- 唤醒队列中第一个有效的等待线程(head 的 next)。
- 被唤醒的线程会重新尝试获取锁。
5. 共享模式(Semaphore、CountDownLatch)
- 共享模式下允许多个线程同时获取资源。
tryAcquireShared()返回值:- < 0:获取失败,入队等待。
- = 0:获取成功,但没有剩余资源。
- > 0:获取成功,还有剩余资源。
- 释放时调用
releaseShared(),会唤醒多个节点。
6. 核心思想总结
- state 表示资源状态(锁、信号量、计数器)。
- CAS 保证并发修改安全。
- CLH 队列保证公平排队。
- LockSupport.park/unpark 实现线程挂起和唤醒。
也就是说,AQS 就是一个 可扩展的框架:它自己不定义获取/释放的具体逻辑,而是交给子类(
ReentrantLock、Semaphore等)实现tryAcquire/tryRelease,然后用 AQS 封装好的排队 + 阻塞 + 唤醒机制来管理并发。
双亲委派模型(Parent Delegation)
定义:类加载器在加载类时,先把请求交给父类加载器去尝试加载,只有父类加载器找不到时,才由当前加载器自己去加载。
主要目的:
- 避免重复加载:父类加载器加载过的类,子类加载器就不会重复加载。
- 保证核心类安全:比如
java.lang.Object必须由最顶层的引导类加载器(Bootstrap ClassLoader)加载,防止被恶意替换。
典型流程(从下往上委派):
- 自定义类加载器 → 应用类加载器(AppClassLoader) → 扩展类加载器(ExtClassLoader) → 启动类加载器(Bootstrap ClassLoader)。
- 加载一个类时,先问“爸爸”有没有加载过,如果有就直接用;如果没有,再自己加载。
慢 SQL 常见原因
- 缺少合适索引:条件字段没有索引,导致全表扫描。
- 索引未命中:例如
like '%abc'、函数操作where date(create_time)=...,会导致索引失效。 - 返回数据过多:一次查询几百万行,网络传输和应用层处理都慢。
- join 设计不当:没有合理索引的多表 join。
- 排序 / 分组开销大:
order by、group by、distinct没有索引辅助。 - 统计类函数:
count(*)在大表上性能差(InnoDB 要全表扫描)。 - 锁/阻塞:事务未提交,SQL 被锁住。
3. 优化思路
- 加索引
- 单列索引、联合索引(最左匹配原则)。
- 覆盖索引(
select id,name from user where id=...)。
- 改写 SQL
- 避免
select *,只取需要的列。 - 避免
!=、<>、or等导致索引失效。 - 尽量把计算放在等号右边,例如
where create_time >= '2024-01-01',而不是date(create_time) = ...。
- 避免
- 分库分表 / 限流
- 大表按时间、用户 ID 做分表。
- 分页优化:
limit 100000, 10→ 用子查询或 ID 范围。
- 缓存
- 读多写少的场景,用 Redis/Memcached 缓存热点数据。
- 架构优化
- 主从复制:读写分离。
- 数据仓库:复杂统计查询放到 Hive/ClickHouse。
1. 联合索引的本质
- 联合索引:在多个列上建立的索引,比如
(a, b, c)。 - MySQL 底层存储是 B+Tree,所有索引字段会按照从左到右的顺序组合排序。
定义:MySQL 在使用联合索引时,会从 最左的列开始匹配,能连续利用多少列,就用多少列。
一旦中间断了,就无法继续利用后面的索引列。
因为 B+Tree 的有序性:
- 索引的排序顺序是
(a → b → c),查询必须遵循这个顺序才能走到具体的节点。 - 如果跳过了
a,就无法确定b的范围。 - 这就是为什么 MySQL 要求 从最左边连续匹配。
- redo log(重做日志,InnoDB 专有)
- 物理日志,记录“数据页修改了什么”。
- 作用:保证 崩溃恢复(crash-safe)。即使 MySQL 异常宕机,也能用 redo log 把已提交事务恢复出来。
- binlog(归档日志,Server 层)
- 逻辑日志,记录“执行了什么 SQL/行操作”。
- 作用:保证 主从复制、数据恢复。
- 是 MySQL Server 层统一的日志,所有引擎都能用。
两阶段提交(保证两种日志一致)
问题:如果先写 redo log 再写 binlog,中途宕机,可能 redo log 有事务,binlog 没有,主从不一致。 解决:两阶段提交(2PC),保证 redo log 和 binlog 的一致性。
流程:
- 写入 redo log(prepare 阶段)
- 把 redo log 写入磁盘,但标记为 “prepare”,表示事务还没提交。
- 写入 binlog
- 把 binlog 写入磁盘,刷盘成功。
- 提交 redo log(commit 阶段)
- 修改 redo log 标记为 “commit”。
- 至此事务才算真正提交。
MySQL(InnoDB 引擎)主要通过 undo log + 两阶段提交 来实现原子性。
(1) Undo Log(回滚日志)
- 当事务执行
UPDATE/DELETE/INSERT时,InnoDB 会生成一份 相反操作的日志。UPDATE:记录旧值,便于回滚。DELETE:记录被删除的数据,用来恢复。INSERT:记录新插入的主键,回滚时删除。
- 如果事务执行失败或显式
ROLLBACK,就通过 undo log 把数据恢复到原始状态。
👉 这保证了事务失败时,可以回到“没执行之前”的样子。
(2) Redo Log + Binlog 的两阶段提交
- 为了保证事务成功时,数据不会丢,InnoDB 还配合 redo log 和 binlog。
- 两阶段提交流程:
- 写入 redo log(prepare):标记事务即将提交。
- 写入 binlog:写入逻辑日志。
- 提交 redo log(commit):事务正式提交。
- 如果中途崩溃,恢复时根据 redo log 和 binlog 的一致性判断事务是“完成”还是“回滚”。
👉 这保证了事务提交后,要么 redo/binlog 都有,要么都没有,不会出现“一半成功”。
redo log(重做日志,物理日志,InnoDB 专有) 记录“某个页上做了什么修改”,保证 崩溃恢复(crash-safe)。
undo log(回滚日志,逻辑日志,InnoDB 专有) 记录“相反操作”,用于事务回滚,保证 原子性。
binlog(归档日志,逻辑日志,Server 层) 记录“执行了什么 SQL/行操作”,用于 主从复制、数据恢复。
(1)事务执行过程
- 写 undo log(记录老版本)
- 更新前先写 undo log,以便失败时回滚。
- 修改内存数据(Buffer Pool)
- 把数据页加载到内存并修改,此时还没写入磁盘。
- 写 redo log(prepare 阶段)
- 把修改写入 redo log buffer,并刷盘(标记为 prepare)。
- 保证即使宕机,也能用 redo log 重放修改。
- 写 binlog
- 把 SQL/行修改写入 binlog cache,提交时统一刷盘。
- 提交 redo log(commit 阶段)
- 修改 redo log 状态为 commit,事务正式提交。
Redis 本质上是一个 单线程事件驱动 的服务器,它的网络模型基于:
- I/O 多路复用(epoll/kqueue/select)
- 文件事件处理器(File Event Handler)
- 单线程 + 非阻塞 I/O
避免线程切换开销:单线程就没有锁竞争。
大部分操作在内存中完成:CPU 不是瓶颈,主要瓶颈在网络 I/O。
I/O 多路复用高效:epoll 能同时处理上万连接。
分布式系统里的 几个核心问题 和 常见解决方案详细讲一下:
1. 数据一致性问题
问题:多个副本同时更新,可能出现不一致(读到旧数据、丢更新)。
解决方案:
- 强一致性(CP)
- 用 共识算法 保证:如 Paxos、Raft。
- 一个写请求必须在大多数节点确认后才算成功(例如:Etcd、Zookeeper)。
- 最终一致性(AP)
- 写请求先到主节点,异步复制到从节点。
- 短时间内可能读到旧数据,但最终会收敛一致。
- 典型应用:Redis 集群、消息队列、DynamoDB。
- 读写分离优化
- 对一致性要求高的读 → 走主库。
- 对实时性要求低的读 → 走从库。
👉 一句话:一致性依靠 共识协议 或 最终一致性模型来实现,取决于业务对一致性的要求。
2. 网络问题
问题:延迟、丢包、网络分区(部分节点之间无法通信)。
解决方案:
- 心跳检测 + 超时机制
- 每个节点周期性发送心跳,超时没回应就判定异常(如 Raft 选举)。
- 重试 / 重传
- 网络丢包时自动重试。
- 幂等性设计很关键,避免重试导致重复操作。
- CAP 权衡
- 出现网络分区时,系统必须在 一致性 C 和 可用性 A 之间做取舍。
- 例如:Zookeeper 选择保证 C,某些 NoSQL 选择保证 A。
3. 节点故障
问题:节点可能宕机,服务不能用。
解决方案:
- 副本机制(Replication)
- 每份数据至少存多个副本(主从复制)。
- 一个节点挂了,数据不会丢失。
- 故障转移(Failover)
- 检测到主节点挂了,自动把从节点提升为主节点。
- 例如:Redis Sentinel、Kafka Controller。
- Leader 选举
- 通过共识算法(如 Raft、ZAB)选举新的 Leader,维持集群一致性。
4. 数据分片与路由
问题:数据量太大,单机存不下。
解决方案:
- 哈希取模分片
hash(key) % N,简单直接,但扩容时迁移数据量大。
- 一致性哈希
- 数据和节点都映射到哈希环,扩容/缩容时只迁移部分数据。
- 常见于 缓存系统(Memcached、Redis 集群)。
- 分片键(Sharding Key)
- 选择一个业务字段作为分片维度(如用户 ID、订单 ID)。
- 请求时通过分片键路由到对应分片。
5. 分布式事务
问题:多个数据库 / 服务参与同一个事务时,难以保证 ACID。
解决方案:
- 2PC(两阶段提交)
- 阶段一:协调者询问各节点能否提交。
- 阶段二:若都同意则提交,否则回滚。
- 缺点:阻塞、单点故障风险。
- 3PC(三阶段提交)
- 在 2PC 基础上增加预提交和超时机制,减少阻塞风险,但实现复杂。
- TCC(Try-Confirm-Cancel)补偿事务
- 每个操作拆成三个步骤:预留资源 → 确认 → 取消。
- 常用于电商支付、库存场景。
- 本地消息表 / 可靠消息 + 最终一致性
- 把事务状态写入消息表,由消息系统保证最终一致。
- 典型应用:电商下单 + 扣库存。
消息队列(MQ)的 推/拉模式 是个经典话题。我们可以从概念、流程、优缺点、应用场景四个角度来看:
1. 概念
- 拉模式(Pull):消费者主动向 MQ 请求消息,类似“我去拿”。
- 推模式(Push):MQ 主动把消息推送给消费者,类似“送上门”。
很多 MQ(Kafka、RocketMQ、RabbitMQ)其实都 支持两种模式,或者底层是拉,但对外封装成推。
2. 流程
拉模式
- 消费者周期性向 Broker 发起拉取请求。
- Broker 返回一批消息,如果没有消息可能返回空。
- 消费者自己决定什么时候再拉。
推模式
- Broker 监听到有新消息。
- 主动把消息推送到消费者(通常通过长连接)。
- 消费者处理后返回确认(ACK)。
3. 优缺点
| 模式 | 优点 | 缺点 |
|---|---|---|
| 拉(Pull) | - 消费者可控,自己决定拉取速率。 - 适合批量处理(一次拉多条)。 - 不容易压垮消费者。 | - 可能出现空拉(拉不到消息,浪费资源)。 - 实时性差(取决于拉取频率)。 |
| 推(Push) | - 实时性强,有消息就送。 - 消费者不用自己轮询。 | - 如果消费者处理能力不足,可能被压垮。 - 需要流控(Flow Control)、ACK 机制。 |
在 分布式系统 + 消息队列 中,“消息幂等”是面试必考点。因为 MQ 天然可能出现:
- 消息重复投递(Producer 重试 / Broker 重发 / Consumer 超时 ACK)。
- 消息重复消费(Consumer 崩溃重启 / ACK 丢失)。
(1) 唯一消息 ID + 去重表
- 做法:
- Producer 给消息加全局唯一 ID(如订单号、UUID)。
- Consumer 消费时,先查去重表(如 Redis/数据库)。
- 处理过则丢弃,没处理过则执行并写入去重表。
- 优点:适合强一致性场景。
- 缺点:需要维护额外存储,性能压力大。
(2) 利用业务唯一键
- 做法:业务本身就有唯一约束,比如:
- 支付场景:
order_id已唯一,不可能重复扣款。 - 数据写库:用
INSERT ... ON DUPLICATE KEY UPDATE。
- 支付场景:
- 优点:不需要额外存储,利用业务天然幂等。
- 缺点:依赖业务特性,不通用。
(3) 记录消息处理状态
- 做法:维护
msg_id → status,状态可为:未处理 / 处理中 / 已处理。 - 步骤:
- 消费消息时先查状态。
- 如果已处理 → 跳过。
- 如果未处理 → 标记处理中 → 执行业务 → 标记已处理。
- 典型实现:用数据库表或 Redis。
(4) 幂等操作设计
- 做法:让操作本身支持幂等。
- 设置用户余额时用
set balance=100而不是balance+=100。 - 更新库存时用
库存 = max(0, 当前库存 - N)。
- 设置用户余额时用
- 优点:操作本身无论执行多少次结果相同。
- 缺点:不是所有业务都能改造为幂等操作。
(5) 分布式锁
- 做法:消费消息前获取分布式锁(如 Redis 锁),保证同一消息只能一个消费者执行。
- 缺点:增加系统复杂度,吞吐量下降,不适合高并发场景。
常见 OOM 出现场景
不同内存区域都可能 OOM:
(1) Java Heap Space
- 原因:对象过多,堆空间不足。
- 场景:集合不断加元素、缓存不清理、加载过多大对象。
- 异常:
java.lang.OutOfMemoryError: Java heap space
(2) GC Overhead Limit Exceeded
- 原因:JVM 花费过多时间在 GC,但回收内存效果很差。
- 场景:内存不足,大量对象存活,GC 一直在忙。
- 异常:
java.lang.OutOfMemoryError: GC overhead limit exceeded
(3) Metaspace / PermGen(JDK8 前是 PermGen,JDK8+ 是 Metaspace)
- 原因:类太多,元数据区放不下。
- 场景:动态生成很多类(反射、CGLIB 动态代理)、频繁加载卸载类(Web 容器热部署)。
- 异常:
java.lang.OutOfMemoryError: Metaspace
(4) Direct Buffer Memory
- 原因:使用 NIO 时分配了太多堆外内存,没释放。
- 场景:Netty、Kafka 使用 DirectByteBuffer,超出
-XX:MaxDirectMemorySize。 - 异常:
java.lang.OutOfMemoryError: Direct buffer memory
(5) Unable to create new native thread
- 原因:系统线程数达到上限,无法再创建新线程。
- 场景:线程池参数设置不当,疯狂 new Thread。
- 异常:
java.lang.OutOfMemoryError: unable to create new native thread
(6) Out of swap space
- 原因:物理内存 + 交换分区都耗尽。
- 场景:进程占用太多内存,系统 OOM。
- 异常:
java.lang.OutOfMemoryError: Out of swap space
3. 为什么会出现 OOM?
- 代码问题:内存泄漏(对象没释放)、死循环创建对象。
- 配置不足:堆大小、元空间大小设置太小。
- 使用不当:线程池/缓存/DirectBuffer 配置错误。
- 高并发/大数据量:超出单机内存极限。
栈溢出后程序会怎样?
-
不会整个进程立刻终止。
-
表现为当前线程抛出 StackOverflowError:
- 如果异常没有被捕获,线程会终止。
- 如果是主线程栈溢出 → 主线程终止 → 整个程序退出(因为主线程挂了)。
- 如果是子线程栈溢出 → 只有该子线程终止,进程继续运行,其他线程不受影响。
StackOverflowError可以捕获,捕获后程序有机会继续运行;但栈空间已经濒临极限,继续运行并不可靠;
正确做法是 避免栈溢出,而不是“捕获它”。
现在主流的 HotSpot JVM(包括 OpenJDK)中:
- 每个 Java 线程对应一个操作系统原生线程(kernel thread)。
- 线程调度完全交给操作系统(Linux 用 pthread,Windows 用 Win32 thread)。
所以 Java 的 Thread 是 对操作系统线程的封装。
🔹 第一范式(1NF:原子性)
定义:字段必须是原子值,不可再分。
- 表中每一列都不能再拆分成更小的字段。
- 每一行、每一列的交叉点必须是单一值,而不是集合或重复组。
例子: ❌ 错误设计:
| 学号 | 姓名 | 电话号码 |
|---|---|---|
| 1 | 张三 | 123,456 |
电话有两个值,不满足 1NF。
✅ 正确设计:
| 学号 | 姓名 | 电话号码 |
|---|---|---|
| 1 | 张三 | 123 |
| 1 | 张三 | 456 |
🔹 第二范式(2NF:消除部分依赖)
定义:在 1NF 基础上,表中的非主属性必须完全依赖于主键,不能只依赖主键的一部分。
- 针对复合主键(由多个字段组成主键)。
- 消除“部分依赖”。
例子: ❌ 错误设计: 主键 = (学号, 课程号)
| 学号 | 课程号 | 成绩 | 姓名 |
|---|---|---|---|
问题:
- 姓名只依赖于学号,不依赖课程号。
- 出现冗余(一个学生选多门课,姓名会重复)。
✅ 正确设计: 拆成两张表:
- 学生表: (学号, 姓名)
- 选课表: (学号, 课程号, 成绩)
🔹 第三范式(3NF:消除传递依赖)
定义:在 2NF 基础上,非主属性之间不能存在传递依赖。
- 换句话说,非主键字段只能直接依赖于主键,不能依赖于其他非主键字段。
例子: ❌ 错误设计:
| 学号 | 姓名 | 系别 | 系主任 |
|---|---|---|---|
问题:
- 学号 → 系别 → 系主任
- 系主任对学号是“传递依赖”。
✅ 正确设计: 拆成两张表:
- 学生表:(学号, 姓名, 系别)
- 系别表:(系别, 系主任)
🔹 总结对照表
| 范式 | 要求 | 解决的问题 |
|---|---|---|
| 1NF | 字段原子性 | 列不可再分,消除多值列 |
| 2NF | 消除部分依赖 | 非主属性必须完全依赖于主键 |
| 3NF | 消除传递依赖 | 非主属性不能依赖于其他非主属性 |
多个客户端的请求管理其实就是 并发请求的接收、分发和处理,同时要保证系统的 高效性、可靠性和可扩展性。我分几个层次来解释:
1. 请求接入层(入口管理)
- 连接管理
服务端会维护客户端与服务器之间的连接(例如 TCP 连接、HTTP 长连接、WebSocket)。
- 常见做法:通过 连接池 或 线程池 来复用连接,避免每次请求都重新建立连接的开销。
- 负载均衡 在分布式部署中,请求通常会先经过 负载均衡器(如 Nginx、LVS、K8s Ingress),将流量分配到不同服务节点。
2. 请求调度层(并发控制)
- 线程池 / 协程池
服务器通常不会“一个请求 → 一个线程”直接处理(那样容易导致资源耗尽)。
- Java 里用 线程池(ThreadPoolExecutor) 管理请求。
- Go 使用 goroutine + channel 并发调度。
- 队列机制 请求进入时可能会先放入队列(如阻塞队列、消息队列),再由工作线程按序消费,防止系统过载。
3. 请求处理层(业务逻辑)
- 无状态处理 HTTP 请求常常是无状态的,服务器根据请求参数独立完成处理。状态信息(如用户会话)一般存储在 Session、Redis、JWT Token 等。
- 事务与锁 多个请求可能访问同一份资源(数据库记录、缓存 key)。服务端要用 锁、CAS、乐观锁、悲观锁 等方式保证一致性。
- 异步与并发优化 耗时操作(IO、数据库、远程调用)可通过 异步化、事件驱动、批量化 等方式优化。
4. 请求返回层(响应管理)
- 同步响应 请求处理完成后直接返回结果(常见于 REST API)。
- 异步响应 请求先返回“已接受”,真正结果通过 回调 / 消息通知 / WebSocket 再告知客户端。
- 限流与熔断 当请求量过大,服务端会进行 限流(如令牌桶算法)、熔断(避免级联失败),保证核心服务可用。
5. 日志与监控
- 每个请求都会被记录日志(请求时间、耗时、返回状态)。
- 服务端会用 监控系统(Prometheus, Grafana, ELK) 实时跟踪请求量、延迟、错误率等,辅助自动伸缩或故障排查。
✅ 总结一句话: 服务端管理多个客户端请求的核心就是:入口限流 → 并发调度 → 正确处理 → 高效返回 → 监控保障。
Nginx 的核心功能
1. Web 服务器
- 可以直接处理 静态资源(HTML、CSS、JS、图片、视频等),速度快、占用内存少。
- 常用场景:部署静态网站、作为应用的前端静态资源服务器。
2. 反向代理
- Nginx 不仅能作为正向代理(帮用户访问外网),更常见的是 反向代理:
- 用户请求 → Nginx → 转发到后端服务器(如 Tomcat、Spring Boot、Flask 等)。
- 好处:隐藏后端服务,提升安全性和可扩展性。
3. 负载均衡
- 当有多台后端服务器时,Nginx 可以把请求分配给不同机器,常见策略有:
- 轮询(Round Robin)
- 最少连接数(Least Connections)
- IP 哈希(保证同一客户端固定到同一台服务器)
4. 高并发能力
- Nginx 基于 事件驱动模型(epoll/kqueue),可以在少量进程下支撑成千上万的并发连接。
- 这点比传统的 Apache(一个连接一个线程/进程)更高效。
Nginx 一般部署在哪里?
Nginx 常见的部署位置有:
- 前端服务器(入口层)
- 部署在最靠近用户的一层(公网可访问的机器上)。
- 功能:作为 网关、反向代理,统一接收客户端请求,再转发到后端。
- 示例:电商网站首页静态资源由 Nginx 提供,API 请求再反向代理到 Java/Python 后端服务。
- 应用前的负载均衡器
- 部署在 多台应用服务器前,把流量均衡分配。
- 功能:均衡请求,避免某一台机器过载。
- 示例:Nginx → {Tomcat1, Tomcat2, Tomcat3}。
- 微服务网关的一部分
- 在微服务架构中,Nginx 常与 K8s Ingress、API Gateway 配合。
- 功能:做 动静分离、限流、路由分发,保护后端微服务。
🔹 什么是正向代理 (Forward Proxy)?
- 代理对象:代理 客户端。
- 工作方式:客户端先把请求发给代理服务器 → 代理服务器再去访问目标服务器 → 把结果返回给客户端。
- 核心作用:隐藏客户端,目标服务器不知道真实的客户端是谁。
- 常见用途:
- 科学上网 / 翻墙(代理帮你访问目标网站)。
- 内网机器访问外网。
- 缓存代理,加速常用网站访问。
例子:
你在中国访问 google.com,由于无法直连,你先访问正向代理(比如一个国外代理服务器),由它帮你访问 Google,再把结果转发回来。
🔹 什么是反向代理 (Reverse Proxy)?
- 代理对象:代理 服务器。
- 工作方式:客户端请求发给代理服务器(如 Nginx),代理服务器再决定把请求转发给哪一台后端服务器 → 返回结果。
- 核心作用:隐藏服务器,客户端不知道真实的后端服务器是谁。
- 常见用途:
- 负载均衡(流量分发到不同后端)。
- 动静分离(静态资源直接由代理返回,动态请求转发到后端)。
- 安全防护(隐藏真实后端 IP,避免攻击)。
例子:
你访问 www.taobao.com,实际上请求先到阿里云的反向代理(比如 Nginx / SLB),再由它转发到真正的后端服务器集群。
索引建立的核心原则:
1️⃣ 索引适合建立在「高频查询」的列上
-
WHERE 条件、JOIN 条件、ORDER BY/GROUP BY 中用到的列。
-
例如:
SELECT * FROM user WHERE email = 'xxx@xxx.com';如果
email经常被用作查询条件,就应该给它建索引。
2️⃣ 选择区分度高的列
- 区分度(基数):指该列不同值的数量 / 总行数。
- 区分度高 → 查询过滤效果好,索引效率高。
- 举例:
身份证号(几乎唯一) → 适合建索引。性别(只有“男/女”两种值) → 不适合单独建索引。
3️⃣ 组合索引 > 单列索引
- 如果查询条件经常涉及 多个字段,优先考虑建立 联合索引。
- 原则:最左前缀原则(索引从左到右生效)。
- 索引
(a, b, c)可以支持以下查询:WHERE a = ?WHERE a = ? AND b = ?WHERE a = ? AND b = ? AND c = ?
- 但不能单独高效支持
WHERE b = ?。
- 索引
4️⃣ 避免在频繁更新的列上建索引
- 因为每次
INSERT/UPDATE/DELETE都要维护索引,会增加写入成本。 - 例如:日志表里的
last_modified_time经常更新,不适合单独建索引。
5️⃣ 控制索引数量
- 一张表的索引数不宜过多(通常建议 <5)。
- 太多索引会:
- 占用磁盘空间。
- 影响写入性能。
- 查询优化器在选择索引时也会变慢。
6️⃣ 使用前缀索引(针对长字符串列)
-
对于
TEXT/VARCHAR很长的字段,可以只索引前几个字符。 -
例如:
CREATE INDEX idx_email ON user(email(10));节省空间,但要注意区分度是否足够。
7️⃣ 覆盖索引(索引即结果)
-
如果一个查询需要的字段都在索引里,可以避免回表,提高性能。
-
例如:
SELECT id, email FROM user WHERE email = 'xxx';如果索引
(email, id)已经存在,就能直接返回结果,无需再查主表。
8️⃣ 考虑排序和分组需求
-
ORDER BY、GROUP BY经常用到的列,也可以建立索引。 -
例如:
SELECT * FROM orders ORDER BY create_time DESC LIMIT 10;给
create_time建索引,可以避免排序开销。
9️⃣ 结合业务场景
- 唯一性约束 → 用唯一索引(如手机号、邮箱)。
- 范围查询(BETWEEN, >, <) → 适合建索引,但要注意范围查询会影响联合索引的利用。
- 全文搜索 → 考虑全文索引(MySQL InnoDB 的 FULLTEXT 或 ES)。
redis分布式锁
1️⃣ 最基础实现:SETNX
- SETNX (SET if Not Exists):只有当 key 不存在时才写入成功。
- 典型写法:
SETNX lock_key unique_id # 成功=拿到锁
EXPIRE lock_key 10 # 设置过期时间,防止死锁
问题:这两步不是原子操作,可能 SETNX 成功了还没来得及 EXPIRE 就挂了 → 死锁。
2️⃣ 改进版:原子 SET 带参数
Redis 2.6.12+ 提供:
SET lock_key unique_id NX PX 10000
NX:仅当 key 不存在时设置PX 10000:设置过期时间(10 秒)- 一步到位,原子操作,解决了死锁问题。
👉 注意:unique_id 是客户端生成的唯一值(如 UUID),用来标识“是谁加的锁”。
🎯 想象一个现实场景 —— 公共厕所的隔间
- 厕所隔间 = 共享资源(比如数据库记录、文件、库存)
- 门上的锁 = 分布式锁
- 人 = 分布式系统里的不同进程/服务
1️⃣ 加锁:先到先得
小明要上厕所,他先看门上有没有挂“有人”的牌子。
- 如果没有(
SETNX成功),他立刻挂上自己的名字牌(uuid),并且写上“使用时间 30 分钟后自动清空”(PX 30000)。 - 如果已经有人挂了牌子,小明只能等一会儿再试(重试 + 退避)。
👉 这就是 Redis 加锁:保证同一时间只有一个人(进程)能进。
2️⃣ 解锁:只能自己开
小明用完出来了,要把牌子摘掉。
- 但要注意:如果直接把牌子拿掉(
DEL),可能误删别人的!- 比如小明挂的 30 分钟牌子过期了,他还没出来;小红后来进去了并挂上了自己的牌子。
- 这时如果小明一出门就粗暴地把牌子扔了,就把小红的锁删掉了。
👉 所以必须先确认“牌子上还是我的名字”(校验 uuid),才能删。
这就是 Lua 脚本解锁。
3️⃣ 锁过期:占太久会被清理
假设小明忘记出来,超过 30 分钟了,厕所管理员会把“有人”牌子自动摘掉。
- 这时其他人(小红)可以进来了。
- 但是小明其实还在里面没出来,于是小明和小红同时使用厕所 —— 锁失效了!
👉 这就是 锁过期 + 进程卡顿 带来的并发进入问题。
4️⃣ 看门狗机制:自动续期
为了避免小明被赶出来,他带了一只“看门狗”,每隔 10 分钟就帮他去刷新牌子上的时间(PEXPIRE)。
- 如果小明还在里面,牌子时间就会延长。
- 如果小明已经出来了,狗发现牌子上不是小明的名字,就不会续期。
👉 这就是 锁自动续期(watchdog)。
5️⃣ 主从复制延迟:双重预订
厕所有两个管理员(Redis 主从)。
- 小明在主管理员那里挂上牌子,但牌子还没来得及同步到从管理员。
- 突然主管理员晕倒了,从管理员接手,却没看到牌子,于是允许小红也挂上牌子。
- 结果小明和小红都进去了!
👉 这就是 Redis 异步复制带来的双持有问题。
解决方法:
- 要么找更靠谱的管理员(ZooKeeper/etcd 这种 CP 系统),
- 要么给每个人发 入场号(fencing token),厕所只认最大的号。
在计算机中,幂等(Idempotency) 指的是: 一个操作,无论执行一次还是执行多次,产生的效果都是一样的,不会因为重复调用而产生副作用。
🔹 日常生活类比
- 电梯按钮:你按一次电梯「上」按钮,它亮了;你再按十次,电梯还是只亮一个灯,不会派十部电梯来。
- 开灯开关:第一次按下开关 → 灯亮了,再按下去不会让灯更亮,操作结果保持一致。
👉 这就是幂等:重复执行不影响最终结果。
想象你要和银行(网站)打电话(建立连接),电话线(网络)不安全,可能有人窃听。于是你们约定了一个安全流程:
- 打招呼(Client Hello): 你告诉银行:「我支持 AES、RSA、TLS1.3 等加密方式,还有一个随机数 random1。」
- 回应(Server Hello): 银行说:「我选用 AES256 这种加密方式,这是我的数字证书(证明我是合法银行),再给你一个随机数 random2。」
- 验证证书: 你拿到银行的证书,去权威机构(CA 公钥)验证,确认证书有效且没被篡改,确保对方不是骗子。
- 生成密钥(Key Exchange):
- 你和银行用 random1 + random2 + (可能再加 random3) 生成一个「会话密钥」。
- 这个过程可能用 RSA 或者 Diffie-Hellman(ECDHE) 算法,保证别人即使偷听,也无法推算出密钥。
- 握手确认(Finished): 双方用刚才生成的会话密钥互相加密发一句:「OK,我准备好了」。 收到能解开,说明密钥一致,握手成功。
- 安全通信开始: 后续所有数据(HTTP 请求、响应)都用会话密钥对称加密传输。
底层实现 & 迭代器有效性(为啥会“失效”)
不同容器的迭代器,本质上指向“元素位置”。位置可能是:
- 连续内存的地址 + 偏移(
vector,string,deque一部分):扩容/搬迁时,旧地址全部失效。 - 节点指针(
list,map,unordered_*):每个元素单独的节点,插入/删除其它节点一般不影响现有迭代器;被删除的那个迭代器当然失效。
典型规则(C++):
vector- push_back 导致扩容或
insert/erase发生移动:可能使全部或部分迭代器失效。 reserve()可减少扩容次数,从而减少失效。
- push_back 导致扩容或
deque- 分段连续,插入/删除首尾较安全;中间插入移动代价大且可能失效。
list(双向链表)- 除了被删节点自身,其它迭代器稳定(stable)。
map/set(红黑树,节点式)- 插入/删除只使受影响节点的迭代器失效;其它迭代器通常稳定。
unordered_map/set(哈希)- rehash(扩容重排桶)会使所有迭代器失效;单次插删通常只影响该元素。
Java 的 fail-fast 原理(简化):容器维护 modCount,每个迭代器保存创建时的 expectedModCount;遍历过程中发现不一致就抛错。它不修复,只是快速发现“你边遍历边改”的不安全行为。
1. A – 原子性 (Atomicity)
👉 事务中的操作要么全部成功,要么全部失败。
- 实现机制:Undo Log(回滚日志)
- 在事务执行前,InnoDB 会把需要修改的数据的旧值写入 Undo Log。
- 如果事务执行失败或被回滚,系统可以根据 Undo Log 把数据恢复到原来的状态。
- 类似于“后悔药”。
2. C – 一致性 (Consistency)
👉 事务执行前后,数据库要从一个一致状态变为另一个一致状态(比如满足约束条件)。
- 实现机制:
- 原子性、隔离性 和 持久性的综合作用。
- 依赖 约束机制(外键、唯一约束、触发器)+ 日志恢复,确保数据不违反规则。
- 举例:银行转账,A 扣 100,B 加 100;即使中途失败,也不会出现钱凭空消失或凭空产生的情况。
3. I – 隔离性 (Isolation)
👉 多个事务之间相互隔离,避免相互干扰。
- 实现机制:锁机制 + MVCC(多版本并发控制)
- 锁:行锁、间隙锁、表锁,保证数据不会被并发事务破坏。
- MVCC:通过保存数据的多个版本 + Undo Log + 隐藏列(事务 ID、回滚指针),让读操作不阻塞写,写也不阻塞读。
- 不同的 隔离级别(READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ、SERIALIZABLE)就是在锁和 MVCC 策略上的不同选择。
4. D – 持久性 (Durability)
👉 事务一旦提交,就必须永久保存,不会因为宕机而丢失。
- 实现机制:Redo Log(重做日志) + WAL(Write-Ahead Logging)机制
- InnoDB 先把数据变更记录写入 Redo Log(顺序写,效率高),再写入数据文件。
- 即使宕机,重启时 MySQL 会通过 Redo Log 把数据恢复到最新提交的状态。
- 结合 binlog(归档日志),还能做主从复制、数据恢复。
🌟 为什么需要 MVCC?
如果只有锁(读锁、写锁),会导致:
- 读和写互相阻塞 → 并发性能差。
- 读多写少的场景下,效率尤其低。
👉 MVCC 的目标:让读操作几乎不阻塞写,写操作也几乎不阻塞读。
⚙️ MVCC 的实现核心
在 InnoDB 中,MVCC 主要依赖以下机制:
- 隐藏列
- 每行数据除了用户定义的字段,还有两个隐含字段:
DB_TRX_ID:最近一次修改这行的事务 IDDB_ROLL_PTR:回滚指针,指向 Undo Log(历史版本)
- 有时还会有
DB_ROW_ID(唯一标识行)。
- 每行数据除了用户定义的字段,还有两个隐含字段:
- Undo Log(回滚日志)
- 当事务修改一行数据时,会把旧值保存到 Undo Log。
- 这样就能“回溯”到某个历史版本的数据。
- ReadView(读视图)
- 一个事务在 执行查询时 会生成一个“视图”,记录当前活跃事务 ID 的范围。
- 之后的查询就根据这个视图来决定能看到哪个版本的数据。
📌 不同隔离级别下的 MVCC 表现
- READ COMMITTED(读已提交)
每次
SELECT都生成新的 ReadView → 可能出现不可重复读。 - REPEATABLE READ(可重复读,InnoDB 默认)
第一次
SELECT时生成 ReadView,整个事务中都复用它 → 避免不可重复读。 - SERIALIZABLE 不使用 MVCC,而是直接用锁来强制串行执行。
🔎 举个例子
假设表中有一条数据 balance=100,事务 ID = 10。
- 事务 A(ID=20)开始查询 balance,此时生成 ReadView,只能看到 ID≤10 的数据。
- 事务 B(ID=30)更新 balance=200,并提交 → 新版本写入数据行,旧版本保存到 Undo Log。
- 事务 A 再次查询时,依旧用最初的 ReadView → 只能看到旧版本 balance=100,而不会受事务 B 的影响。
- 新事务 C(ID=40)查询时 → 它的 ReadView 已包含 B 的更新,因此能读到 balance=200。
👉 这样就实现了 读写互不阻塞,读者能“看到属于自己时空的历史版本”。
MySQL 性能监控一般要从 数据库本身运行状态 + 业务访问情况 + 系统资源使用 三个维度来看。常见指标可以分几类:
1. 连接与线程相关
- Threads_connected:当前已建立的客户端连接数。
- Threads_running:正在执行 SQL 的线程数(高了说明并发压力大)。
- Max_used_connections:历史峰值连接数,用来判断
max_connections配置是否合理。 - Aborted_connects:失败的连接次数,可能说明有应用异常或权限配置错误。
2. 查询与事务相关
- Queries / Questions:单位时间内的 SQL 执行次数。
- Com_select / Com_insert / Com_update / Com_delete:不同类型 SQL 的执行次数,可以分析读写比例。
- Transactions(事务提交/回滚次数):
Com_commit、Com_rollback。 - Slow_queries:慢查询数量,需要结合
slow_query_log分析。
3. InnoDB 存储引擎指标
- Buffer Pool 命中率:
- 指标:
Innodb_buffer_pool_read_requestsvsInnodb_buffer_pool_reads - 命中率低 → 说明内存不够,经常要去磁盘读。
- 指标:
- 行锁冲突情况:
Innodb_row_lock_waits(行锁等待次数)、Innodb_row_lock_time(总等待时间)。
- Redo/Undo 日志:
Innodb_log_waits:redo log 太小导致写入阻塞。
- 事务等待:是否有大量事务长时间未提交,导致锁竞争。
4. 延迟与执行情况
- 平均查询执行时间(QPS、TPS):通过
performance_schema或监控系统统计。 - 等待事件:哪些 SQL 在等 IO、等锁,可以用
performance_schema.events_waits_summary_global_by_event_name分析。 - 慢查询日志:具体 SQL 语句、执行时间、扫描行数。
5. 复制与高可用(主从架构时)
-
Seconds_Behind_Master:从库延迟。
-
Relay_Log_Space:从库 relay log 大小,说明复制积压情况。
-
Slave_IO_Running / Slave_SQL_Running:从库复制线程是否正常。
找到慢 SQL 后,用以下方法分析:
🔍 (1) EXPLAIN 执行计划
EXPLAIN SELECT * FROM orders WHERE user_id = 123 AND status = 'paid';
重点关注字段:
- type:访问类型(ALL=全表扫描,ref=索引,const=常量最优)。
- key:实际用到的索引。
- rows:预估扫描行数。
- Extra:是否出现
Using filesort、Using temporary(说明性能差)。
🔍 (2) SHOW PROFILE
SET profiling = 1;
SELECT * FROM orders WHERE user_id=123;
SHOW PROFILES;
SHOW PROFILE FOR QUERY 1;
可以看到 SQL 执行的时间分布:解析、优化、锁等待、执行。
🔍 (3) Performance Schema
MySQL 5.6+ 内置,可以分析等待事件、I/O 热点。
SELECT * FROM performance_schema.events_statements_summary_by_digest
ORDER BY AVG_TIMER_WAIT DESC
LIMIT 5;
覆盖索引(Covering Index)是数据库(尤其是 MySQL InnoDB 引擎里)一个重要的优化手段。
定义
当一个索引包含了 查询中需要的所有列 时,这个索引就叫做覆盖索引。 换句话说,查询只需要通过索引就能拿到结果,不必回表(再去主键索引或数据页中取完整行)。
优点
- 避免回表 → 减少随机 I/O,提升查询性能。
- 减少锁开销 → 因为只访问索引页,不必访问数据页。
- 更快的响应 → 特别适合高并发读多写少的场景。
使用场景
- 频繁执行的查询,只涉及索引中字段的 SELECT。
- 需要快速分页、统计的查询。
- 数据分析类查询(比如
SELECT COUNT(*) FROM ... WHERE ...)。
Elasticsearch 是一个分布式搜索和分析引擎,核心价值:
- 存数据:能像数据库一样存 JSON 文档。
- 查数据:能用极快的速度搜索和过滤。
- 分析数据:能做聚合统计,支持实时数据分析。
📖 生活化类比
你可以把 ES 想象成一个“超级图书馆检索系统”:
- 数据库(比如 MySQL)更像是“存档室”:你要精确知道“书的编号”,它能快速取出来。
- Elasticsearch 更像“图书馆检索”:你只记得书名里有几个词,或者想找“所有讲机器学习的书”,它能在几百万本书里快速给你结果,还能告诉你“哪个最相关”。
🔎 ES 能做什么?
- 全文搜索(Full-text Search)
- 模糊匹配、分词、相关性排序。
- 例如:电商搜索框里输入“红色运动鞋”,ES 能智能拆分关键词并排序。
- 结构化检索(Filtering)
- 精确过滤(比如:价格 < 200,品牌=NIKE)。
- 和全文搜索结合,用来做复杂的搜索场景。
- 实时分析(Aggregations)
- 类似 SQL 里的
GROUP BY、COUNT、AVG。 - 例如:统计“每个城市今天新增的订单数”。
- 类似 SQL 里的
- 日志/监控/指标存储
- 常见场景:ELK/EFK 日志系统(Elasticsearch + Logstash/Fluentd + Kibana)。
- 把大量日志写入 ES,再通过 Kibana 可视化检索和分析。
异常(Exception) 大体分为三类:
1. 异常分类
- Checked Exception(受检异常)
- 编译时必须显式处理(try-catch 或 throws)。
- 比如:
IOException,SQLException,ClassNotFoundException。 - 特点:通常是外部因素导致,程序本身无法完全避免。
- Unchecked Exception(非受检异常 / Runtime Exception)
- 不需要编译期强制捕获。
- 比如:
NullPointerException,ArrayIndexOutOfBoundsException。 - 特点:大多是由代码逻辑错误引起。
- Error
- JVM 无法恢复的严重错误。
- 比如:
OutOfMemoryError,StackOverflowError。
RuntimeException 常见子类
RuntimeException 及其子类常见的有:
- 空指针相关
NullPointerException
- 数组/集合越界相关
ArrayIndexOutOfBoundsExceptionStringIndexOutOfBoundsExceptionIndexOutOfBoundsException
- 类型转换相关
ClassCastException
- 算术相关
ArithmeticException(如除零)
- 非法参数相关
IllegalArgumentExceptionNumberFormatException(字符串转数字失败)
- 状态相关
IllegalStateException
- 迭代器/集合操作
ConcurrentModificationException(迭代时修改集合)UnsupportedOperationException(不支持的操作)
- 安全相关
SecurityException
- 反射相关
IllegalAccessException(注意:这个是 Checked 的,但InaccessibleObjectException是 Runtime 的)
- 其他常见的
NegativeArraySizeException(数组长度为负)MissingResourceExceptionEnumConstantNotPresentException
自定义业务异常继承哪个类,要取决于你希望它在编译期还是运行期被强制处理:
1. 继承 Exception(Checked Exception,受检异常)
- 特点:调用方必须
try...catch或throws。 - 适用场景:
- 业务逻辑上确实需要调用方显式处理。
- 比如:订单不存在、库存不足、用户余额不足等。
- 优点:强制调用方注意并处理业务错误。
- 缺点:代码会被大量
try...catch或throws污染。
public class BusinessException extends Exception {
public BusinessException(String message) {
super(message);
}
}
2. 继承 RuntimeException(Unchecked Exception,运行时异常)
- 特点:调用方可以选择性处理,不强制。
- 适用场景:
- 绝大多数 Web 项目、Spring 项目中的业务异常。
- 一般通过全局异常处理器(
@ControllerAdvice、@ExceptionHandler)来统一捕获并返回错误信息。
- 优点:
- 代码简洁,不需要每次调用都写
throws。 - 与 Spring 等框架的异常机制更契合。
- 代码简洁,不需要每次调用都写
- 缺点:
- 如果开发者忘了处理,异常可能直接抛到最上层。
public class BusinessException extends RuntimeException {
public BusinessException(String message) {
super(message);
}
}
3. 实际推荐做法
在实际业务开发(尤其是 Spring Boot 项目)中,几乎都选择继承 RuntimeException,然后通过全局异常处理来规范输出。
例如:
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class BusinessException extends RuntimeException {
private final int code;
public BusinessException(int code, String message) {
super(message);
this.code = code;
}
public int getCode() {
return code;
}
}
常见分布式 ID 方案
1) 数据库自增 / 发号器
- 实现:单表
AUTO_INCREMENT;或建一张【号段表】批量发号(segment)。 - 优点:实现简单,具备递增特性;号段方案吞吐高、可容灾。
- 缺点:单点或中心化依赖;跨机房延迟;切库要小心。
- 适用:强有序、易运维的业务(订单号等)。
- 号段表示例:
CREATE TABLE id_segment (
biz_key VARCHAR(64) PRIMARY KEY,
max_id BIGINT NOT NULL,
step INT NOT NULL,
version INT NOT NULL
);
-- 应用一次拿 step 个号: (max_id - step + 1) ~ max_id,乐观锁 version 保证并发安全
2) Snowflake(雪花算法)
- 结构(经典 64 位):
[1位符号][41位时间戳][10位机器标识][12位序列]- 41 位毫秒时间戳 ≈ 69 年
- 10 位机器(可分 datacenterId + workerId)
- 12 位序列:同毫秒内最大 4096 个
- 优点:本地生成、低延迟、趋势有序(按时间)。
- 缺点:时钟回拨需处理;机器位分配、漂移恢复要设计。
- 适用:高并发、低延迟、去中心化场景。
- 关键点:时钟回拨(拒绝、等待、备用序列或切换机器位)、序列溢出阻塞、机器号分配(ZooKeeper/Etcd/配置中心)。
1. Twitter Snowflake(经典雪花算法)
背景:Twitter 最早提出的分布式 ID 算法,用 64 位 long 型 ID 来保证唯一性和趋势有序。
结构(64 位):
1位符号位 | 41位时间戳 | 10位机器标识 | 12位序列号
- 41位时间戳:毫秒级,可用约 69 年
- 10位机器号:可支持 1024 台节点
- 12位序列号:同一毫秒最多生成 4096 个 ID
优点:
- 本地生成,无需远程依赖,延迟极低
- ID 趋势递增,适合数据库索引
缺点:
- 时钟回拨问题:若系统时间被调整,可能生成重复 ID
- 机器 ID 分配需可靠机制(配置、ZK、Etcd 等)
2. 百度 UidGenerator(雪花算法的改进版)
背景:百度在雪花算法基础上改进,推出了 UidGenerator,优化了位分配和时钟回拨问题。
主要改进点:
- 位结构可配置化
- Snowflake 是固定的 41-10-12 分配。
- 百度改成 可配置的时间位 + 工作节点位 + 序列位,适应不同业务。
- 例如电商高并发可增大序列位,低并发长周期业务可增大时间位。
- 时钟回拨处理
- Snowflake 一旦发生时间回拨,常见做法是阻塞或报错。
- UidGenerator 支持 环形队列缓存 ID,即便出现时间小范围回拨,也能继续发号。
- 工程化支持
- 提供 Spring Boot Starter,易接入
- 机器号可通过数据库或 ZK 自动分配
优点:灵活、容错能力强、工程化好。 缺点:需要引入额外的缓存/队列,稍复杂。
3. 美团 Leaf
背景:美团点评内部开源的分布式 ID 生成服务,解决大规模分布式系统中的 ID 需求。
两种模式:
- Leaf-segment(号段模式)
- 利用数据库中的号段表,每次批量取一段 ID(如 1000 个),缓存在本地发号。
- 优点:性能高(QPS 可到数十万),DB 压力小(批量更新)。
- 缺点:依赖 DB,高可用需主从架构;ID 不严格单调(跨服务时可能有跳号)。
- Leaf-snowflake(雪花模式)
- 改造版 Snowflake,利用 Zookeeper 分配 workerId,避免冲突。
- 优点:本地生成,低延迟,无需频繁访问 DB。
- 缺点:依赖 ZK 的可用性;时钟回拨仍需处理。
工程化特性:
- 提供 HTTP 接口,可作为独立服务
- 高可用:DB 主从、ZK 集群
- 监控告警支持
Snowflake 遇到时钟回拨时,为什么常见的做法是阻塞或直接报错?
这里的根源在于 Snowflake 的 ID 设计强依赖本地系统时间👇
1. Snowflake ID 的组成回顾
[1位符号][41位时间戳][10位机器号][12位序列号]
- 时间戳部分是核心,它保证 不同毫秒生成的 ID 大体有序。
- 如果时间正常递增,ID 也随时间单调上升。
2. 时钟回拨为什么危险
如果系统时钟因为 NTP 校准、人工改动、硬件问题等被拨回:
- 当前时间戳比上一次生成 ID 的时间戳 还要小。
- 那么新的 ID 里 时间部分变小,可能与之前生成的 ID 重复,破坏全局唯一性和趋势递增性。
例如:
- 在
2025-09-06 10:00:00生成了 ID → 时间戳 = T - 时钟被回拨到
2025-09-05 23:59:59→ 时间戳 < T - 此时生成的 ID 可能和之前的毫秒段 重叠,导致冲突。
3. 为什么选择“阻塞或报错”
- 阻塞:等到系统时钟追赶回之前的最大时间戳后再继续发号 → 确保新 ID 时间戳不会小于历史值。
- 报错:直接抛异常让调用方注意到系统时间异常 → 避免产生错误 ID。
这两种做法虽然会影响可用性,但它们保证了 ID 的正确性(唯一性 + 趋势有序性)。
你问的就是「环形队列缓存 ID」到底怎么做。给你一份工程化落地方案(接近百度 UidGenerator 的思路),含原理 + 关键数据结构 + 生成/续桶流程 + 代码骨架。
核心思路(一句话)
不是临时现算一个 ID 再返回,而是按时间片(毫秒/秒)预先批量生成一大把未来可用的 ID,放进一个环形队列(ring buffer)*里;业务线程只需从队列里 take()。当时间*小幅回拨时,队列里还有“未来时间片”生成的 ID 可用,从而不中断发号。
🍱 号段模式(Leaf-segment)
想象一下食堂打饭:
- 食堂阿姨每次从仓库里 搬一大筐鸡腿(比如 1000 个),放到窗口。
- 学生来打饭时,阿姨就从筐里一个一个发。
- 鸡腿快要发完时,阿姨就提前去仓库再搬一筐备用,这样窗口不会断货。
👉 在这里:
- 仓库 = 数据库(存着全局最大的号段值)
- 阿姨手里的筐 = 应用本地缓存的号段
- 学生拿到的鸡腿 = 分配出去的 ID
好处:发号非常快(内存操作),坏处:每次搬一筐会“跳号”,比如 A 窗口发到 1000,B 窗口发的是 2001,中间的 1001~2000 就没用了。
🕰️ 雪花模式(Leaf-snowflake)
再想象一个工厂生产流水号的机器:
- 每个工厂(机房)有很多条流水线(机器)。
- 每个工厂和流水线都有编号(datacenterId + workerId)。
- 每条流水线在同一毫秒里可以打出 0~4095 个序列号。
- 最终的编号就是:时间戳 + 工厂号 + 流水线号 + 序列号。
👉 在这里:
- 时间戳 = 当前时钟
- 工厂号/流水线号 = Zookeeper 分配的 workerId
- 序列号 = 当前毫秒内的计数器
好处:本地自己就能造号,速度极快,不用找“仓库”;坏处:如果时钟倒退(比如手表拨慢了),可能会打出重复的号。
🌟 总结对比
- 号段模式:像“提前批量领货”,靠数据库仓库统一管控;优点是容易管,缺点是有时候会浪费、跳号。
- 雪花模式:像“流水线即时生产”,每台机器自己拼装号码;优点是快,缺点是要小心时钟问题。
1. 号段的本质
- 每个应用实例(比如窗口 A、窗口 B)从数据库里一次性申请一段号:
- 窗口 A 拿到号段
(1 ~ 1000) - 窗口 B 拿到号段
(1001 ~ 2000)
- 窗口 A 拿到号段
- 这两个号段在数据库里已经被锁定下来,不会再分给别人。
2. 跳号是怎么发生的
- 如果 窗口 A 提前下班/宕机,手里还有 300 个号没发完,那么
(701 ~ 1000)这段就“浪费”了。 - 下一个用户只能去窗口 B 拿号,从
2001开始。 - 结果就是:中间的号 没人用了,看起来像“跳过去”了一截。
👉 这就是“跳号”的根源:号段是一次性预分配的,没用完也不能退回去。
1. LIMIT
- 作用:限制返回的行数。
- 场景:只想要前几条结果,比如“最新的 10 条新闻”。
-- 取出前 10 行
SELECT * FROM news ORDER BY created_at DESC LIMIT 10;
👉 这里即使表里有 100 万条新闻,结果只会返回 10 条。
2. OFFSET
- 作用:跳过前面若干行,从指定位置开始返回。
- 常和 LIMIT 搭配:用来做分页。
-- 跳过前 20 行,取接下来的 10 行
SELECT * FROM news ORDER BY created_at DESC LIMIT 10 OFFSET 20;
👉 这表示“第 3 页的数据”(假设每页 10 条):
- 第 1 页:
LIMIT 10 OFFSET 0 - 第 2 页:
LIMIT 10 OFFSET 10 - 第 3 页:
LIMIT 10 OFFSET 20
3. 两者关系
LIMIT n:取 n 行。LIMIT n OFFSET m:跳过 m 行,再取 n 行。- 简写:
LIMIT m, n和LIMIT n OFFSET m等价。
例如:
SELECT * FROM users LIMIT 5, 10;
等价于:
SELECT * FROM users LIMIT 10 OFFSET 5;
意思是:跳过前 5 行,返回接下来的 10 行。
一、立刻见效的改写
1) 用“键集分页(seek/cursor)”替代大 OFFSET
- 适合时间线/翻下一页等场景;避免跳过成千上万行的扫描。
-- 原:第10001页
SELECT * FROM post ORDER BY id ASC LIMIT 100000, 20;
-- 优:基于上页最后一条记录继续翻
SELECT * FROM post
WHERE id > :last_id
ORDER BY id ASC
LIMIT 20;
多列排序用“组合游标”:
-- (created_at DESC, id DESC)
WHERE (created_at < :last_created_at)
OR (created_at = :last_created_at AND id < :last_id)
ORDER BY created_at DESC, id DESC
LIMIT 20;
2) 让 ORDER BY 走索引(覆盖最好)
- 给排序键建立(联合)索引,并把唯一键放在最后保证确定性。
CREATE INDEX idx_feed ON post (created_at DESC, id DESC);
SELECT id, title, created_at
FROM post
FORCE INDEX (idx_feed)
ORDER BY created_at DESC, id DESC
LIMIT 20; -- 尽量只取需要列,减少回表
3) 先取主键再回表(Deferred Join)
- 大宽表分页时显著减 I/O。
-- 子查询只用覆盖索引拿本页主键
WITH ids AS (
SELECT id
FROM post FORCE INDEX (idx_feed)
ORDER BY created_at DESC, id DESC
LIMIT 20
)
SELECT p.*
FROM post p JOIN ids USING(id)
ORDER BY p.created_at DESC, p.id DESC;
二、深分页与特殊需求
4) 必须“跳页”时
- 用页锚 + seek:预存每 N 页的锚点
id,跳到最近锚点后再 seek 前进几页。 - 或返回 cursor(
next_cursor=created_at|id),前端用 cursor 翻页,不再用 offset。
5) 随机取样/推荐,避免 ORDER BY RAND() LIMIT n
-- 简化随机:先随机起点,再顺序取
SELECT * FROM post
WHERE id >= FLOOR(RAND() * (SELECT MAX(id) FROM post))
ORDER BY id
LIMIT 20;
更优:维护一个“抽样池”(Redis/轻量表),先抽 key 再回表。
6) DISTINCT/GROUP BY + 分页
- 先聚合出主键,再回表分页:
WITH g AS (
SELECT MIN(id) AS id
FROM events
WHERE user_id = ?
GROUP BY session_id
ORDER BY MIN(created_at) DESC
LIMIT 20
)
SELECT e.* FROM events e JOIN g USING(id);
三、索引与查询设计原则
- 复合索引顺序:等值列在前,范围/排序列在后,最后加唯一键保证稳定排序。
例:
(user_id, status, created_at DESC, id DESC) - 避免在排序列上使用函数/表达式(导致索引失效),避免数据类型隐式转换。
- 不要
SELECT *,按需列出字段。 - 计总条数不要
SQL_CALC_FOUND_ROWS,单独COUNT(*)(可异步/缓存)。
1. OFFSET 的原理
原本的分页方式是:
SELECT * FROM post ORDER BY id ASC LIMIT 20 OFFSET 100000;
数据库内部会先扫描 100000 行、丢掉,再取 20 行。 所以页数越深,扫描越多,性能越来越差。
2. 替换为 “游标分页(键集分页)”
我们观察分页本质:
- 第 1 页取
id最小的 20 条。 - 第 2 页其实就是:取 “大于第 1 页最后一个 id” 的 20 条。
于是写成:
-- last_id = 上一页最后一条记录的 id
SELECT *
FROM post
WHERE id > :last_id
ORDER BY id ASC
LIMIT 20;
这样数据库只需要从上次的位置往后扫 20 条,不用丢掉前面的大量记录。
3. 举个例子
假设表里 id 是顺序的:
| id | title |
|---|---|
| 1 | A |
| 2 | B |
| 3 | C |
| 4 | D |
| 5 | E |
| 6 | F |
- 第一页:
SELECT * FROM post ORDER BY id ASC LIMIT 3;
👉 得到 (1,2,3),last_id = 3
- 第二页:
SELECT * FROM post WHERE id > 3 ORDER BY id ASC LIMIT 3;
👉 得到 (4,5,6)
4. 为什么高效?
因为数据库可以直接用 WHERE id > 3 来“定位”,再顺序取 3 行,避免了 OFFSET 3 这种“前 3 行读了又丢掉”的浪费。
✅ 所以:用 WHERE id > 上一页最后一个 id 替代 OFFSET,就是从“上一页最后一条记录”的位置继续往后读,这就叫 键集分页 / 游标分页。
1. 什么是“排序的确定性”
- SQL 查询里常用
ORDER BY created_at DESC LIMIT 20。 - 如果某一列(比如
created_at)的值有重复,数据库可能返回 任意一条相同值的记录,结果顺序不稳定。 - 当你做分页时就危险了:第 1 页和第 2 页可能出现重复或者漏掉。
2. 为什么要加唯一键
如果我们写成:
ORDER BY created_at DESC, id DESC
created_at决定大方向的顺序。- 当两条记录
created_at相同时,就用id(唯一)来打破平局。 - 这样整个结果集的排序就是全局唯一确定的。
👉 这就避免了分页时“相同时间戳的数据前后飘”的问题。
3. 为什么“唯一键放在最后”
在联合索引里,MySQL 按最左前缀原则排序:
-
如果索引是
(created_at, id),那么查询ORDER BY created_at DESC, id DESC可以完全走索引,结果自然有序,无需 filesort。
-
把唯一键(
id)放在最后,保证了:- 前面按主要排序列(
created_at)聚集; - 如果前面的值相同,就由唯一键来保证稳定性;
- 联合索引同时还能覆盖
created_at和id,提高效率。
- 前面按主要排序列(
4. 举个例子
假设表里数据是:
| id | created_at |
|---|---|
| 1 | 2025-09-01 |
| 2 | 2025-09-01 |
| 3 | 2025-09-02 |
- 仅用
ORDER BY created_at DESC:数据库在id=1和id=2的顺序上可能前后不一致。 - 用
ORDER BY created_at DESC, id DESC:顺序必然是 (3,2,1)。
这样分页时不会丢记录或重复。
1. 什么叫“回表”
在 MySQL InnoDB 里:
- 聚簇索引(clustered index):主键索引的叶子节点存放整行数据。
- 二级索引(secondary index):叶子节点只存放【索引列 + 主键值】,不存放整行。
当你用二级索引做查询时:
- MySQL 先在 二级索引里找到满足条件的主键 id;
- 再根据这个主键去 聚簇索引(主键索引) 里取整行。 👉 这个过程就叫 回表(back to table lookup)。
2. 为什么“回表”会慢
- 每次二级索引命中一行,都要再去主键索引里查一次。
- 如果返回行数多,会触发大量随机 I/O。
- 在分页场景里,
LIMIT 100000, 20这种深分页,可能要扫描几十万行做回表,非常耗时。
下面给出一套可落地的“海量数据分布式排序 + 防倾斜”方案(框架无关,MapReduce/Spark/自研 gRPC 都通用)。
一、怎么在分布式里排好序(单机放不下)
总流程(Sample → RangePartition → Shuffle → Local Merge → Concatenate):
- 本地外排序(External Sort)
- 每个节点把数据分块读入内存(≤ 可用内存),块内排序后落盘成有序 run;
- 用k 路归并把多个 run 合并成更大的有序段(仍可落盘)。
- 抽样选分界(Splitters)
- 每个节点随机/均匀抽样
s个 key(可从已排序 run 里等距抽样更准),汇总到协调者排序; - 选出
p-1个近似分位数作为分界(i/p分位),广播给所有节点。
- 每个节点随机/均匀抽样
- 按范围分区(Range Partition)+ Shuffle
- 依据分界把本机有序段切成
p个子段(区间不重叠),发给对应“归并节点”。
- 依据分界把本机有序段切成
- 节点侧最终归并(Final Merge)
- 每个归并节点把收到的多路已排序子段做堆归并,写出一个全局有序分片。
- 拼接即全局有序
- 分片 #0, #1, … 顺序拼接(或作为分区化的有序输出)。
关键点:局部 run 有序 + 全局按范围路由,最终每个分片内有序、分片之间按范围递增 ⇒ 全局有序。
读阶段:乐观锁通常不加锁(普通 SELECT,依赖 MVCC/快照读)。
写阶段:不提前占锁;在提交那一刻用“条件更新/版本校验”原子地写入。数据库为保证原子性会短暂拿行锁/闩锁,但这不是你在业务层显式加的“先锁再干活”的悲观锁。
ToC(面向消费者、上亿级并发)做分布式锁,和 ToB(内部系统、相对可控流量)相比,需要额外关注一堆“在大流量+强对抗+多地域”场景才会暴露的问题。下面给你一份工程化清单(带做法与参数建议),直接当评审/上线前的 checklist 用。
0. ToC vs ToB 的根本差异
- 流量模型:ToC 有洪峰/抖动/秒杀、长尾延迟,ToB 更稳定可控。
- 键分布:ToC 极易出现热点 Key(同一库存、同一活动、同一用户),ToB 多为分散操作。
- 对抗性:ToC 有恶意重放/刷子/爬虫/DDoS,ToB 客户端可信。
- 地域:ToC 常多机房/多地域,跨区 RTT 大,时钟偏差与网络分区更常见。 => 这直接影响锁的算法选择、超时策略、容灾、限流和观测。
2) 热点 Key & 惊群(thundering herd)
ToC 秒杀类请求会把同一个 lock:sku123 打爆,导致锁服务先被打死。
缓解手段
- 排队替代忙等:加锁失败不要自旋;有界排队(本地/网关)+
maxWaitMillis超时直接失败。 - 指数退避 + 抖动:初始 20–50ms,指数增长,添加 20–30% 随机抖动,避免齐步重试。
- 请求合并(coalescing):同 key 在网关处合并为单个后端尝试,其它等待结果(失败/成功广播)。
- 预分流:在入口层先做业务幂等/资格过滤/配额,减少真正触发锁的请求量。
- 限速 & 配额:按用户/IP/设备/地理维度配额,保护锁后端。
注意:给热点 key 加“盐”分片并不能保持互斥(会破坏语义)。盐值只用于吞吐型操作,互斥必须单 key,因此前置分流与排队更关键。
关键差异 & 额外要点(ToC 必做)
- 热点键与倾斜
- 问题:同一资源键极热(如同一商品/活动),单分片被打爆。
- 做法:
- 分片锁(Striped Lock):
lock:sku:123#{hash(reqId)%N}把一个逻辑锁拆成 N 把;进入临界区前先“当选”一把(如用 ZSET 选最小哈希)。 - 过度分区:多个物理分片承接同一热点,服务侧做二次仲裁。
- 把锁转成队列:对热点资源排队(ZSET/Stream),一次只放少量请求过闸。
- 分片锁(Striped Lock):
- 惊群效应(锁释放瞬间风暴)
- 问题:成千上万请求同时重试,打爆 Redis/下游。
- 做法:指数退避 + 抖动(jitter)、发布订阅通知(锁释放推送),或使用等待队列(先入先出,避免全体轮询)。
- 租约续期与进程暂停
- 问题:GC/容器 freeze 导致看门狗续期失败,锁过期被他人拿走;旧持有者继续执行→并发写。
- 做法:
- 所有临界写操作强制携带 Fencing Token(单调递增票据);下游只接受票据最大的写。
- 续期失败立刻中止临界区;在每个关键步骤验证仍持有锁 & token 未过期。
- 一致性模型选择
- ToC 常取 可用性优先:允许失败重试/幂等,而不是追求“强互斥到毫秒级”。
- 做法:幂等化(业务幂等键、去重表/布隆)、补偿/重放、超时回滚;锁只是“减并发”,最终以校验+幂等兜底。
- TTL 与任务时长不确定
- 问题:请求时长长尾,固定 TTL 不是过短(误释放)就是过长(占用资源)。
- 做法:短 TTL + 心跳续期,续期周期 < TTL/3;自适应 TTL(按历史P95时长给 TTL 基线)。
- 多机房/跨区域
- 问题:跨区网络分区与时钟漂移放大锁风险;RedLock 跨实例仲裁在强一致诉求下仍存争议。
- 做法:就近加锁(每区独立锁集群 + 资源分区归属),跨区通过消息总线同步结果;确需强一致用 etcd/ZooKeeper 或数据库单点仲裁。
- 公平性与饿死
- 问题:高并发下“永远抢不过别人”。
- 做法:为热点资源实现FIFO 等待队列(ZSET/Stream + Lua 取号发号),或给不同请求分配权重/优先级。
- 限流与降级
- 问题:高冲突时单靠锁会形成放大器。
- 做法:先限流再加锁(漏桶/令牌桶);失败率超阈值时直接降级(读缓存/异步排队/灰度关闭部分入口)。
- 键/值体积与连接管理
- 问题:上亿流量下,键值尺寸、命令选择、连接/管线影响巨大。
- 做法:短 key、SET NX PX + Lua 原子释放、Pipeline 批量化、连接池限速;单集群只做“锁”,业务数据分离。
- 监控与 SLO
- 观测项:获取锁延迟 P99/P999、获取失败率、续期失败次数、重复持有检测、热点 TopN、单分片 QPS、Lua 超时、锁遗留量。
- 告警策略:连续高失败或长尾上升→自动限流/熔断。
- 安全与多租户隔离
- 租户前缀(
tenant:app:lock:...)、配额与速率限制、token 随机度 & 过期清理,防误删/越权。
- 成本与演练
- 专用锁集群,容量按并发 × 平均持有时长 / TTL估算;
- 定期做 Chaos/故障演练:Redis 主从切换、网络抖动、GC 停顿、时钟回拨。
Redis的慢查询可能是由什么原因导致的
为什么会慢(4 类原因 + 例子)
- 命令层:一次性干太多
- 大 key/全量:
KEYS *、LRANGE 0 -1、HGETALL、超大Z*运算 - 阻塞脚本:长时间 Lua
- 删除大 key:同步
DEL一卡全卡
- 大 key/全量:
- 服务层:单线程被“堵”
- 持久化抖动:AOF
fsync、BGSAVE/AOF rewrite的 fork+COW - 内存/OS:Swap、开启 THP、过期/淘汰“雪崩”
- 慢客户端:输出缓冲爆了,拖住事件循环
- 持久化抖动:AOF
- 客户端/网络:来回跑太多
- 没用 pipeline、连接池太小、跨机房高 RTT、value 超大序列化
- Cluster 场景:分片与复制带来的额外成本
MOVED/ASK重定向频繁、单槽热点、从库落后、WAIT等强确认
怎么办(对号入座)
- 查:
SLOWLOG GET、LATENCY DOCTOR、INFO commandstats/memory/replication、CLIENT LIST - 改命令:用
SCAN代替KEYS,分页/分批;大 key 用UNLINK异步删;Lua 拆短;只取必要字段 - 稳服务:AOF 用
everysec+ SSD;禁用 THP、避免 Swap;分散 TTL,限输出缓冲 - 优客户端:开启 pipeline、就近访问、合理连接池、控制 value 大小
怎么判断是网络波动问题还是key设置不合理问题
结论口诀:端到端慢但服务端快=网络;服务端也慢且集中在某些命令/某些键=键设计问题。
第 1 步|比“端到端”与“服务器执行”
- 客户端测 RTT:
redis-cli --latency -h <host> -p <port>- 若 RTT 高峰明显,而服务端慢日志为空,八成是 网络/客户端。
- 服务端执行时间:
SLOWLOG GET 128(只记服务器执行时长,不含网络)INFO commandstats看usec_per_call、调用次数是否异常
第 2 步|看是否“命令/键”集中
- 抓 大/热点 key:
redis-cli --bigkeys(抽样找巨 key)redis-cli --hotkeys(抽样找热点键)MEMORY USAGE <key>、集合类再看SCARD/HLEN/ZCARD
- 若慢日志里总是
HGETALL/SMEMBERS/ZRANGE/KEYS/SORT等全量/高复杂度命令 ⇒ 键设计/用法问题。
第 3 步|排除“慢客户端”与复制/持久化干扰
CLIENT LIST看 obuf/omem 是否大 ⇒ 客户端读慢/网络慢,拖住事件循环LATENCY DOCTOR是否报aof-fsync/fork/expire-cycle尖刺(这是服务端内部抖动,不是网络)
容器/消费者宕机时,靠“未确认不算消费、重放+幂等/事务”来保证不丢不乱: 消息系统负责持久化+复制与投递确认(ack/offset/visibility timeout),业务侧负责幂等/事务,二者配合实现最终一致,必要时做到“准 Exactly-once”。
三层机制(通用思路)
- 存储层(不丢)
- 持久化:落盘/AOF、日志;
- 复制/多数派:主从/仲裁(如 Kafka ISR、RabbitMQ Quorum Queue);
- 生产确认:publisher confirm /
acks=all,失败重试。
- 投递层(能恢复)
- 未确认=可重投:
- 拉模型:offset 未提交就视为未消费(Kafka);
- 推模型:未 ack会重投(RabbitMQ);
- 可见性超时:到时未删即重投(SQS 的 visibility timeout);
- Streams:
pending list未XACK可XCLAIM(Redis Streams)。
- 重试与死信:最多 N 次→DLQ,便于排障与补偿。
- 未确认=可重投:
- 消费层(不重/不乱)
- 幂等处理:业务幂等键/去重表/唯一约束;
- 事务性消费:读→处理→写库/产生新消息 要么都成功要么都回滚:
- 方案A:消费后提交 offset(至少一次)+ 幂等;
- 方案B:事务性 outbox(本地事务写业务表与 outbox,再由 CDC 发消息);
- 方案C(Kafka):Idempotent Producer + 事务(EOS),将“下游写 + offset 提交”放进同一事务。
一、创建型(解决“对象怎么创建更合理”)
- Singleton|全局唯一、懒/饿汉、线程安全 例:配置中心、连接池、ID 生成器。
- Factory Method / Abstract Factory|把“创建细节”交给工厂,或一组相关产品同源创建 例:JDBC 驱动、序列化器(JSON/Avro)切换、多云存储适配。
- Builder|复杂对象分步构建,必填/可选参数清晰 例:HTTP 请求构建、对象有很多可选字段(Lombok builder)。
- Prototype|用“克隆”代替新建,拷贝后微调 例:表单/报表模板复制、规则模板实例化。
二、结构型(解决“对象如何组合更灵活”)
- Adapter|老接口 vs 新接口不匹配 → 转接 例:把第三方 SMS/支付 SDK 适配成统一接口。
- Facade|给复杂子系统包一层“门面” 例:下单流程一键调用:校验→库存→支付→通知。
- Decorator|在不改类的前提下“动态叠加能力” 例:IO 流过滤链、为服务加监控/限流/缓存。
- Proxy|控制访问:远程、权限、缓存、AOP 例:MyBatis/Feign 动态代理、Spring AOP。
- Composite|树形结构“部分-整体”一致处理 例:菜单/评论树、文件系统。
- Flyweight|共享小而多的不可变对象,省内存 例:字体字形、地图瓦片、颜色/图标缓存。
- Bridge|抽象与实现解耦,二维扩展不爆炸 例:消息类型(短信/邮件)× 通道(AWS/阿里云)组合。
三、行为型(解决“对象之间如何协作更优雅”)
- Strategy|可替换算法,运行时切换 例:优惠计算(满减/折扣)、排序/路由策略。
- Template Method|定义流程骨架,步骤可覆写 例:爬虫流程、订单审核通用模板。
- Observer / Pub-Sub|事件发布订阅,解耦通知 例:Spring ApplicationEvent、订单成功→发券/埋点。
- Chain of Responsibility|责任链逐个尝试/过滤 例:Web 过滤器/拦截器、风控规则链。
- Command|把操作封装成命令,可排队/撤销/重做 例:任务队列、后台批处理、编辑器撤销。
- State|对象随状态切换行为 例:订单(待支付/已支付/已取消)、工作流节点。
- Iterator|统一遍历集合的方式 例:集合/游标遍历器。
- Mediator|用中介者管理“多对象多关系” 例:聊天室、UI 控件联动。
- Visitor|在不改数据结构前提下添加操作 例:AST 语法树多种遍历逻辑、报表导出。
- Memento|快照/撤销恢复 例:编辑器历史、配置回滚。
代理模式(Proxy):在不改变目标对象的前提下,放一个“代理对象”在前面,控制访问或附加通用能力(鉴权、缓存、限流、事务、日志、远程调用等)。
Client ──> Proxy ──(前后织入能力)──> RealSubject
能解决什么问题
- 访问控制:鉴权、权限校验(Protection Proxy)
- 远程调用:把本地方法调用“伪装”为 RPC(Remote Proxy)
- 懒加载/延迟创建:大对象按需加载(Virtual Proxy)
- 性能/稳定性:缓存、限流、降级、重试、熔断
- 运维可观测:日志、埋点、链路追踪
- 事务/资源管理:方法前后开启/提交/回滚
#
30 秒速答
在地址栏输入 URL 回车后:(可能先命中缓存/Service Worker)→ DNS 解析 → 选协议与建连(TCP/TLS 或 QUIC)→ 发 HTTP 请求 → 经 CDN/反向代理到应用 → 服务器生成响应 → 浏览器收流并渲染(解析 HTML/CSS/JS、布局绘制、请求子资源)→ 连接复用与缓存落盘。期间涉及 HSTS/HTTPS 升级、证书校验、Cookie、缓存协商、CORS、压缩与分片传输 等。
分步说明(简洁版)
- 输入与导航判定
- 浏览器判断是搜索词还是 URL;规范化 URL,若命中 HSTS 规则直接升为
https://。 - 先查 浏览器缓存 / Service Worker:若命中,直接返回或走 SW 的
fetch逻辑(可离线)。
- 浏览器判断是搜索词还是 URL;规范化 URL,若命中 HSTS 规则直接升为
- DNS 解析
- 走浏览器/系统/hosts/本地缓存 → 递归解析器 → 权威 DNS,得到 IP 与端口;HTTP/2/3 会用 ALPN 协商协议。
- 有代理则按代理解析;也可能用 DoH/DoT。
- 建连与安全
- HTTP/1.1/2:TCP 三次握手 → TLS 握手(SNI 指定域名、证书链校验、OCSP stapling、ALPN 协商 h2/h1)。
- HTTP/3:直接 QUIC(UDP)+TLS1.3,可 0-RTT。
- 建立后进入连接池,可复用/多路复用。
- 发起请求
- 组装请求行与请求头:
Host/Accept/Accept-Encoding(User-Agent)/Cookie/Referer/Origin等;必要时先做 CORS 预检。 - 可能带 ETag/If-None-Match / If-Modified-Since 做缓存协商。
- 组装请求行与请求头:
- 中间层转发
- 流量通常先到 CDN/WAF/负载均衡 → 反向代理(Nginx)→ 应用服务。
- CDN 命中直接回源;未命中向源站拉取。
- 服务器处理
- 依据
Host/SNI 选虚拟主机,路由到应用;读缓存/DB/下游服务;生成响应与 状态码、头(Cache-Control/Set-Cookie/CSP)及实体;按需 gzip/brotli、分块传输。
- 依据
- 浏览器接收与渲染
- 收到响应:若
304用本地缓存;否则写入磁盘/内存缓存并交给渲染进程。 - 解析 HTML → DOM,并行拉取子资源;解析 CSS → CSSOM(阻塞渲染);JS 执行可能阻塞解析(
defer/async优化)。 - 生成 Render Tree → 布局 → 绘制 → 合成;后续交互走事件循环。
- 收到响应:若
- 后续与优化
- 连接 keep-alive/HTTP2 多路复用;预解析/预连接/预加载(
dns-prefetch/preconnect/preload);将来访问走缓存/复用连接。
- 连接 keep-alive/HTTP2 多路复用;预解析/预连接/预加载(
Kafka 的索引结构(按分区)
每个 分区 被切成多个 段(segment),每段是一组并列文件:
00000000000000000000.log # 真实数据(顺序追加)
00000000000000000000.index # 偏移索引:offset -> 物理位置pos(稀疏)
00000000000000000000.timeindex # 时间索引:timestamp -> offset(稀疏)
00000000000000000000.txnindex # 事务索引:记录中止事务范围(仅事务主题)
- 稀疏索引 + 内存映射(mmap):并不是每条消息都建索引,而是每隔若干字节(
log.index.interval.bytes)采样一条。- offset 索引项≈
relativeOffset(int32) + position(int32)→ 8 字节/条 - time 索引项≈
timestamp(int64) + relativeOffset(int32)→ 12 字节/条
- offset 索引项≈
- 查 offset:对
.index二分 → 得到最近的不大于目标 offset 的位置 → 跳到.log顺序扫几条就命中。 - 查时间:对
.timeindex二分得到一个 offset → 再走上一步查 offset。 - 重建容易:索引只是加速结构,崩溃可从
.log扫描重建。 - 高吞吐原因:数据文件顺序写,查找靠稀疏索引 + OS 页缓存,内存占用极小、定位 O(logN) 后顺序读。
常见消息队列索引是不是都一样?
不一样。各家根据目标侧重点,索引设计差异很大:
| 系统 | 存储与索引核心 | 典型能力/取舍 |
|---|---|---|
| Kafka | 分区→段;offset 索引 + 时间索引(稀疏,mmap) | 擅长按 offset/时间 顺序消费、回溯;极致顺序写与吞吐 |
| RocketMQ | CommitLog(顺序写) + ConsumeQueue(每条20B:物理偏移、大小、tag hash) + 可选 IndexFile(hash 倒排,key→offset 列表) | 额外支持按 消息键/Tag 快速检索;多一层队列与键索引 |
| Pulsar | 基于 BookKeeper ledger 的“托管日志”(Managed Ledger);位点是 (ledgerId, entryId);游标(cursor)是持久化检查点 | 把索引更多交给 ledger/cursor 管理;天然分布式复制,按 ledger 边界滚动 |
| RabbitMQ | 每队列自己的存储/段文件 + 内存索引;重在队列语义(确认、路由、镜像/法定队列) | 面向队列与路由键,偏消息生命周期管理;无 Kafka 那种时间索引 |
| Redis Streams | 基于 radix tree + listpack 的有序流;消费者组维护 pending 列表 | 内存/持久化一体,按流 ID 顺序;索引即数据结构本身 |
I/O 多路复用就是:让一个/少量线程同时盯住很多网络连接,只有“谁就绪”才被内核叫醒处理。 epoll是 Linux 下最常用、最高效的实现;比老的 select/poll 好在只返回就绪的那几个,不需要每次把所有 FD都扫一遍,能轻松扛十万连接(Nginx、Redis、Netty 都在用)。
类比版(面试官容易记住)
- 你开了 1 万家店(1 万个连接)。
- select/poll:每隔一会儿你要挨个打电话问“有客人吗?”→ 全量轮询、越多越慢。
- epoll:先让店里装门铃并在“总控台”登记(
epoll_ctl);平时你睡觉,哪家门铃响(就绪)总控台才叫醒你(epoll_wait)→ 只处理就绪少数。 - LT(电平触发)*像*门铃一直响,直到你把人都接待完;ET(边沿触发)*像*只响一下,必须一口气把门口的客人“接待到没人”(读到
EAGAIN)才不漏单。
一分钟展开(关键术语)
- 就绪模型:把“兴趣的 FD”注册给内核(
epoll_ctl),阻塞等事件(epoll_wait),醒来处理回调。 - 复杂度:select/poll 近似 O(连接数);epoll 近似 O(就绪数),还用 mmap 降低拷贝。
- 平台差异:Linux 用 epoll;BSD/macOS 用 kqueue;Windows 是IOCP(完成型,不是就绪型)。
- 典型栈:Nginx、Redis、Kafka、Netty/Java NIO、Go runtime 都基于 epoll/kqueue/IOCP 做事件驱动。
常见追问 & 金句
- 为什么 epoll 更快? “兴趣集长期在内核,无需每次拷贝和全量扫描;只把就绪 FD 返出来,醒来就干活。”
- ET 必须注意什么?
“非阻塞套接字,并且每次循环读/写到
EAGAIN;否则会漏事件。” - 怎么避免阻塞事件循环? “回调里别做重活;重活扔线程池/协程,主循环只收发和分发。”
1) Java 的线程怎么运行?
一句话:Java Thread 是对操作系统线程的 1:1 封装;调用 start() 后由 JVM 创建原生线程,交给 OS 调度,执行你覆写的 run()。
- 生命周期:
NEW → RUNNABLE(运行/就绪/内核等待) → BLOCKED/WAITING/TIMED_WAITING → TERMINATED - 关键点
start()≠run():start()创建并启动新线程,run()只是普通方法调用。- 内存可见性靠 JMM:
synchronized/volatile/Lock、start/join都提供 happens-before 保障。 - 阻塞来源:I/O、锁竞争、
sleep/wait/join、锁膨胀/上下文切换。 - GC/安全点会“停一下世界”(或并发配合),这是 JVM 层的调度协作。
2) Java 线程池底层原理(ThreadPoolExecutor)
一句话:先用现有线程,再排队,排满后再扩容到最大;还不行就拒绝。
- 核心参数:
corePoolSize、maximumPoolSize、keepAliveTime、workQueue、threadFactory、RejectedExecutionHandler - 提交流程(面试画这三步就行)
- 运行线程数
< core→ 新建线程执行任务; - 否则尝试入队到
workQueue; - 队满且线程数
< max→ 再新建线程; - 还不行 → 拒绝策略(
Abort/CallerRuns/DiscardOldest/Discard)。
- 运行线程数
- 工作线程:
Worker.runWorker()从BlockingQueue循环取任务;空闲超过keepAliveTime的非核心线程会被回收(可配置核心也回收)。 - 状态机:原子整型打包线程数+运行状态(
RUNNING/SHUTDOWN/STOP/TIDYING/TERMINATED),保证并发安全。 - 常用队列:
LinkedBlockingQueue(无界,易堆积)、ArrayBlockingQueue(有界,稳态好)、SynchronousQueue(直交接,高并发短任务)、DelayedWorkQueue(定时任务)。
面试金句:“先线程、后排队、再扩容,最后拒绝;任务循环在
runWorker,状态用一个原子 ctl 管。”
3) MyBatis 有什么作用?
一句话:MyBatis 是“以 SQL 为中心”的持久层框架,把 SQL 与 Java 对象映射起来,简化 JDBC。
- 你写 SQL(XML/注解),MyBatis 负责:
- 入参绑定(
#{}占位)、结果映射到 POJO; - 动态 SQL(
<if> <where> <foreach>); - 类型转换(TypeHandler);
- 事务/连接托管(常与 Spring 集成);
- 插件拦截器(
Executor/Statement/ResultSet/Parameter)做审计、分库分表; - 缓存(一级/二级)。
- 入参绑定(
- 定位:比 JPA 更可控(你掌控 SQL),比原生 JDBC 更省力。
4) MyBatis 的缓存架构(面试高频)
一级缓存(本地缓存)
- 作用域:
SqlSession级别(同一个会话内)。 - 默认开启:相同查询(SQL+参数+分页)命中缓存,不再打库。
- 失效时机:同会话内执行
INSERT/UPDATE/DELETE、commit/rollback/close、flushCache=true、localCacheScope=STATEMENT。 - 特点:线程不共享、成本低、事务内提升命中。
二级缓存(Mapper/命名空间级)
- 作用域:同 Mapper(
namespace)下、跨 SqlSession 共享。 - 启用:在 mapper XML 声明
<cache/>(可配size、flushInterval、eviction=LRU/FIFO、readOnly等),SELECT useCache=true(默认)。 - 写入时机:在事务提交时把结果放入二级缓存;
INSERT/UPDATE/DELETE默认触发清空本 namespace 的缓存。 - 实现:装饰器叠加链——
PerpetualCache(基础存储) +LruCache/FifoCache/ScheduledCache/LoggingCache/SynchronizedCache等;也可自定义对接 Redis/Ehcache(实现Cache接口)。 - Key 组成:
MappedStatementId + SQL + 参数值 + RowBounds + 环境。 - 注意:跨表/跨命名空间更新容易脏读;强一致业务慎用或手动精细失效。
面试金句:“一级会话级、二级命名空间级;二级在 commit 才写入,任何写操作会清掉命名空间缓存。MyBatis 的缓存是‘可用但要敬畏’,强一致别硬开。”
常见进程间通信(IPC)方式
本机内核中转(拷贝型)
- 匿名管道 pipe:父子进程单机、一端写一端读;
ls | grep就是管道。简单、流式、无消息边界。 - 命名管道 FIFO:有路径名,非亲缘进程可用;本机一对一/多对一。
- 消息队列(SysV/POSIX):有消息边界、可优先级;适合“消息化”通信,容量受限。
- Unix Domain Socket(UDS):本机“套接字”,支持流/报文、权限控制、高吞吐(常比 TCP 快);Nginx、Docker 都用。
- 信号 signal:轻通知(如
SIGTERM),载荷极小,做“打断/唤醒”,不传大数据。
共享内存(零拷贝,需自管同步)
- 共享内存(SysV shm / POSIX
shm_open+mmap):最快;配合信号量/互斥量/futex/eventfd做同步。 - 内存映射文件
mmap:把文件映射到多进程地址空间,既通信又可持久化(日志/环形缓冲)。
跨机器/通用
- TCP/UDP Socket:网络通信基础。进程内也可用 TCP,但本机优先 UDS。
- RPC 框架:gRPC/Thrift/HTTP+JSON,本质走 socket,解决序列化、超时、重试等。
一、为什么分库?(库=实例/集群维度的水平拆分)
要解决:吞吐、容量、隔离与可用性
- 扩吞吐:把请求摊到多台库(多主或分片主库),提升并发 QPS。
- 扩容量:单机磁盘/内存顶不住;多库把数据按规则切开,单库只放一部分。
- 资源隔离:把“高峰/重写”的业务拆到独立实例,避免互相拖垮(IO/锁/缓冲池)。
- 高可用/容灾:多库多副本、跨可用区/跨地域,缩小故障域。
- 团队拆分:按业务域(用户/订单/账务)独立演进与发布。
记忆点:分库=横向扩展系统能力 + 降低故障半径。
二、为什么分表?(表=单实例内的数据粒度拆分/分区)
要解决:单表过大导致的性能与运维问题
- 查询/索引更快:单表行数小、索引 B+Tree 更浅,缓存命中更好。
- 减少锁竞争:热点行/页被切散,降低写冲突。
- DDL/运维友好:大表加字段、改索引、备份/恢复都更可控。
- 冷热/时间分层:按月/日分表,老表归档,常查新表。
常见策略:
- 按范围(时间):
order_2025_09(配合保留策略/冷热分层) - 按哈希/取模:
order_{uid % 64}(均衡写入、抗热点) - 二级维度:时间 + 哈希(既便于归档,又分散热点)
记忆点:分表=让“单表可管理、可快跑”;注意它不等同于 MySQL 的 partition table(后者仍在一库一引擎文件里)。
三、分库 vs 分表(一句话对比)
- 分库侧重系统层面的扩吞吐与可用性;
- 分表侧重单库内部的查询/索引/运维效率。
- 大型系统常“先分表,后分库”,或两者配合。
\
TCP 的可靠性靠这几件事一起完成:
- 序号 + ACK(累计确认):每个字节有序号,收到了就发 ACK,没被确认的都在重传队列里。
- 重传机制:
- 超时重传(RTO 基于 RTT 估计,自适应回退);
- 快速重传(收到 ≥3 个重复 ACK 立即重传);
- SACK 选项精确告知“哪些块到了”,减少无谓重传。
- 滑动窗口:按对端通告的窗口发送,保证不丢/不淹接收端(流量控制)。
- 乱序重排/去重:接收端按序号缓存乱序段,去重后按序交付上层。
- 校验和:每段带 TCP 校验和(含伪首部),发现比特错误就丢弃并促使重传。
- 连接管理:三次握手生成随机初始序号、四次挥手关闭,避免老包混入新连接。 (拥塞控制不直接“修正数据”,但通过慢启动/拥塞避免/快恢复,降低丢包→间接提升可靠性。)
1)dupACK 是谁的 ACK?
dupACK(重复 ACK)是:接收端(正在收你数据的那一方)发回来的 “累计确认号相同的 ACK”。
- TCP 的 ACK 是累计确认:ACK=N 表示“0..N-1 都收到了,下一字节从 N 开始我还没见到”。
2)为什么多次收到 dupACK ⇒ “后续数据到了,唯独某段丢了”?
因为 TCP 规范要求:当接收端收到乱序报文段(也就是缺口之后的更高序号的段)时,必须立刻回一个 ACK,且 ACK 号仍是“缺口起点”,以此催促发送端把缺的那段重传;每来一段新的乱序数据,就再回一次同样的 ACK —— 于是发送端看到一连串相同 ACK 号,这就是 dupACK。
令牌桶是什么(10 秒版)
- 令牌桶(Token Bucket)用两参数表示:
(r, B)- 以速率 r(令牌/秒)往桶里放令牌,桶容量 B。
- 每个请求要消费 1 个令牌;有就立刻放行,没有就等待/丢弃(看你是整形器 shaper 还是警察 policer)。
- 因为令牌会累积,刚开始可能一次性放行最多 B 个请求 → 这就是短暂峰值(突发)。
为啥会有“暂时峰值”
当系统空闲了一会儿,桶里攒满了 B 个令牌;新流量一来,可以瞬间以线路速率把这 B 个请求立刻放走,平均速率仍受 r 限制,但瞬时有突发。
不想要峰值,怎么改?(四种常用思路)
按“从容易到严格”排列,你面试时说 2~3 个即可:
-
把桶变小/关掉突发
- 做法:把 B 调小(甚至设 B=1)。
- 结果:最多只允许1 个立即通过,其余按 r 速度放行或等待/丢弃。
- 代价:对抖动不友好,容易造成不必要的等待/丢弃。
-
改成“漏桶”(Leaky Bucket)——固定速率出水
- 思想:排队 + 匀速出队(严格按 r 发出),没有“攒满再一下子放”。
- 结果:完全消峰、输出平滑;
- 代价:需要队列(时延↑),队满就丢。
-
用“节拍/配速”的令牌桶(GCRA/虚拟时钟)
-
思想:不靠“积攒 B 个令牌”,而是为下一次允许通过计算一个时间点,到点才放行。
-
伪代码:
next = 0 on request: now = monotonic_now() next = max(next + 1/r, now) sleep_until(next) // 或忙等/排队 allow -
效果:请求按间隔 1/r 均匀通过,几乎没有突发;
-
这个就是电信标准里的 GCRA(令牌桶的严谨等价实现)。
-
-
把“允许就立即放行”改为“等待拿令牌”(阻塞式)
- 许多语言自带的限速器既支持
Allow()(可能突发),也支持Wait()(配速通过)。 - 例如 Go:
rate.NewLimiter(rate.Every(1/r), burst=1)并用Wait(ctx),基本无突发。
- 许多语言自带的限速器既支持
设计一个日志查看的系统,要求可以根据关键词检索该条日志,并确定是哪个机器上的日志,设计一下几个模块
整体架构(模块分层)
采集层(Agent/Shipper)
- 部署在每台机器/容器:Fluent Bit / Filebeat / 自研 Agent。
- 功能:采集→解析→打标签→压缩→批量发送。
- 每条日志都带上host_id、hostname、ip、env、app、pod、node等机器标识。
入口层(Collector API / Edge Gateway)
- HTTP/gRPC 入口(支持流式与批量)。
- 接收后立即落入消息队列(Kafka/Pulsar),返回202 + ingestion_id(异步解耦,避免超时)。
- 限流/鉴权/配额,支持GZIP、NDJSON、multipart。
管道层(Parse & Enrich)
- 从 MQ 消费,做解析(grok/正则/JSON)、时间校准、字段标准化、主机画像补全(从“主机注册表/CMDB”取别名、业务线、机房)。
索引与存储层
- 热数据:ES/OpenSearch/ClickHouse(倒排索引 + 列存),按时间分区(daily/rollover),模板与分片合理规划。
- 冷数据:对象存储(S3/OSS)存原始日志(Parquet/压缩),供回溯/大查询。
- 索引策略:消息体
message用分词器(中文可用 ik/smartcn);机器相关字段keyword 类型(可聚合过滤)。
查询服务(Search API)
- 提供关键词 + 时间范围 + 维度过滤(app/env/host_id)。
- 结果返回高亮片段 + 机器信息;支持按机器聚合(terms agg)与跳转“该机器日志详情”。
界面(Log Viewer)
- 支持搜索/过滤/排序;按机器分组展示;点击条目可显示host/pod/node、
trace_id/request_id。 - 支持实时 tail(WebSocket)、保存查询、告警联动。
运维与治理
- ILM冷热分层/自动归档、索引模板、分片数随规模自动化。
- 多租户与 RBAC;脱敏/敏感字段掩码。
1) goroutine
- 创建:
go func(){...}();初始栈 ~2KB,按需扩容/收缩。 - 阻塞点(I/O、channel、锁、
time.Sleep、runtime.Gosched())会让出执行;运行时可抢占,避免长时间独占。 GOMAXPROCS控制并行核数(默认=CPU 核心数)。
2) 调度器 G-P-M
- G=goroutine,M=内核线程,P=可运行上下文(持有本地运行队列)。
- 工作窃取:空闲 P 会从别的 P 窃取 G,提升负载均衡。
- netpoller:网络 I/O 统一托管(epoll/kqueue/IOCP),I/O 完成唤醒对应 G → 阻塞不占线程。
- syscall/cgo:真正阻塞的系统调用会占用 M;运行时会补充新 M 保持并行度。
3) channel(语义与用法)
- 无缓冲:发送/接收必须同到场,同步交接(强同步屏障)。
- 有缓冲:容量内异步,满/空时阻塞;可做队列/信号量。
- 关闭:只能由发送方关闭;接收端读到零值且
ok=false。向已关闭的 channel 发送会 panic。 - select:多路复用;
default可做非阻塞尝试;nil channel 可以“屏蔽”某个分支。
4) 同步与内存模型(JMM for Go)
- happens-before:
- send → receive;close → receive(
ok=false) Unlock→ 之后的Lock;WaitGroup.Done→Wait返回atomic读写提供有序可见性
- send → receive;close → receive(
- 读写共享数据要用 channel/锁/atomic;并发写
map会 panic。
二、底层怎么做到(三套“工具箱”)
1)锁 + 两段锁(2PL)
- S 共享锁:读;X 排他锁:写;范围/谓词锁:锁住“查询条件对应的键范围”,防止别人插入“新符合条件的行”(幻读的根因)。
- Repeatable Read:读的 S 锁一直持有到事务结束;写持有 X 锁到结束。
- Serializable:在 RR 基础上再加范围/谓词锁(SQL Server 的 Key-Range Lock、MySQL 的 Next-Key Lock)。
2)MVCC(多版本并发控制)
- 每行有版本(创建/删除事务号),读到的是快照版本。
- RC:语句级快照(每条 SELECT 拍一张照片)。
- RR:事务级快照(事务开始时拍一张照片,之后一致)。
- 写操作用 X 锁;读通常不加锁(“一致性读”)。是否能挡幻读要看有没有范围锁配合(如下)。
3)SSI(Serializable Snapshot Isolation,可串行化快照隔离)
- 仍用 快照读(不阻塞),但运行时跟踪读写依赖图,发现会形成不可串行化的“危险结构”就强制回滚其中一个事务(PostgreSQL 的实现,靠 SIREAD/谓词锁 + 冲突检测)。
三、按级别逐个说“怎么实现的”
Read Uncommitted
- 实现:读不走快照,也不拿 S 锁,直接看最新值(哪怕对方未提交)。写仍需 X 锁。
- 现象:可能脏读;现代引擎很少用,部分引擎实际把 RU 当 RC 处理。
Read Committed
- 实现 A(锁式):每条读语句临时加 S 锁,语句结束就释放;写用 X 锁。
- 实现 B(MVCC):语句级快照;无 S 锁,读到的是该语句开始时“已提交版本”。
- 结果:无脏读;但同一事务下一次再查,可能看到别人已经提交的新版本 → 不可重复读/幻读仍可能发生。
Repeatable Read
- 实现 A(锁式):读时加 S 锁并持有到提交;写用 X 锁到提交。
- 实现 B(MVCC):事务级快照;所有一致性读都看同一张“开事务时的照片”。
- 防幻读:
- 纯快照能“看不见后来插入的行”(对一致性读来说表象上无幻读);
- 若是加锁读(
FOR UPDATE/SHARE),需要范围锁/Next-Key把“间隙”也锁住,阻止别人插入满足条件的新行。
Serializable
- 实现 A(严格 2PL):对读加 谓词/范围锁,对写加 X 锁,全部到提交才释放 → 完全串行化。
- 实现 B(MVCC + 强化):把普通
SELECT也当锁式读处理(如 InnoDB 把 SELECT 变成共享范围锁)。 - 实现 C(SSI):快照读不阻塞,但检测冲突,一旦会破坏串行化就回滚其中一方(PostgreSQL)。
Redis 的有序集合(ZSET)底层有两种实现,按规模自动切换:
- 紧凑编码(小集合):一段连续内存里的紧凑表
- 现在用 listpack(早期版本叫 ziplist)。
- 适用于元素个数、成员长度都很小的 ZSET,节省内存。
- 到了阈值会自动“升格”。
- 跳表(skiplist)+ 字典(hash/dict)的组合(大集合):
- dict:
member -> score,O(1) 查找/更新分数。 - skiplist:按
(score, member)有序,用于范围查询/排名,O(log N) 插入、删除、按分数区间遍历。 - 两份结构同时维护:新增/改分数时先在 dict 查,再在 skiplist 插/删,保持一致。
- dict:
为什么要“跳表 + 字典”两把刷子?
- 字典让“按成员查分数/改分数”是 O(1)。
- 跳表让“按分数区间/排名遍历”是 O(log N) + 顺序前进,非常适合排行榜/区间检索。\
Java 面向对象三大特性 的理解:封装、继承、多态
🔨 在 Java 里:
class Animal { void sound() { System.out.println("animal sound"); } }
class Dog extends Animal { void sound() { System.out.println("wang!"); } }
class Cat extends Animal { void sound() { System.out.println("miao~"); } }
Animal a1 = new Dog();
Animal a2 = new Cat();
a1.sound(); // wang!
a2.sound(); // miao~
👉 同一个方法 sound(),运行时表现不同,这就是多态。
客户端突然掉电 / 崩溃(没发 FIN/ACK)
- 此时 TCP 连接还保持着,服务端“暂时不知道”。
- 服务端只有在尝试发送数据时,收不到 ACK,才会发现异常:
- 内核会重传几次数据。
- 如果多次重传失败,服务端会报
ETIMEDOUT(连接超时)。
- 另一种机制是 TCP keepalive:
- 如果开启了 keepalive,服务端会定期发送探测包。
- 探测失败一定次数后,服务端才会判定客户端已断开。
著名的 死锁产生的四个必要条件(互斥、占有并等待、不可剥夺、循环等待):
- 互斥条件 资源一次只能被一个线程/进程占用。 例如:打印机只能同时被一个进程使用。
- 占有并等待 一个进程已经持有了某些资源,同时又在等待其它资源。 例如:线程 A 占有锁 L1,等待 L2。
- 不可剥夺 资源不能被强制夺走,只能由持有它的进程主动释放。
- 循环等待
存在一个进程循环等待链。
例如:
- A 等待 B 占有的锁,
- B 等待 C 占有的锁,
G1 如何处理大对象
- 直接分配到 Humongous Region
- 如果对象大小 ≥ 半个 Region,就不放在 Eden 里,而是放在 一连串连续的 Region 中。
- 例如:Region 大小 4MB,一个 10MB 的对象会占用 3 个连续的 Region。
- 回收方式
- Humongous Region 会被当作 老年代的一部分 来管理。
- 回收时主要依赖 全局并发标记周期(Mixed GC) 来识别垃圾。
- 如果整个大对象没被引用,整片 Region 会被回收。
- 碎片问题
- 大对象需要连续的 Region,如果堆被切得比较碎(空 Region 不连续),可能会触发 Full GC 来做压缩整理。
三、为什么要这样设计
- 大对象如果放在年轻代,容易频繁复制,开销很大。
- G1 直接把它们放到专门的 Humongous Region,避免反复拷贝。
- 但缺点是:
- 占用空间大,难以移动;
- 需要连续 Region;
- 回收依赖全局标记周期,速度相对慢。
一、线程池的作用
简单来说,线程池就是提前创建好一批可复用的线程,放在池子里统一管理。 当有任务时,直接从池里取空闲线程执行;任务完成后,线程回到池中等待下一次使用。
👉 作用:
- 复用线程,避免频繁创建/销毁线程的开销
- 集中管理线程,控制并发数量
- 任务排队调度,提供统一的 API(如 Java Executor 框架)
二、线程池的好处
1. 性能提升
- 线程创建和销毁代价高(涉及内核资源申请/释放)。
- 线程池预先创建好线程,任务来时直接复用,大大减少开销。
2. 控制并发数量
- 避免无限制创建线程导致 CPU 切换频繁 / OOM。
- 可以限制最大线程数,保护系统。
3. 任务管理能力
- 可以设置队列,支持任务排队。
- 可以配置拒绝策略(任务太多时如何处理)。
- 可以做定时/周期性任务(如 ScheduledThreadPool)。
4. 统一调度和监控
- 线程池可以统一监控:线程数、任务数、队列长度。
- 可以统一异常处理、日志收集。
- 方便做系统的稳定性控制(如熔断/限流)。
Redis 的 有序集合(ZSet) 底层用 两种数据结构组合:
- 哈希表(dict)
- key = member
- value = score
- 用来快速根据元素找到分数,复杂度
O(1)。
- 跳表(skiplist)
- 按 score 有序存储所有元素。
- 用来实现区间查找、排序、排名,复杂度
O(logN)。 - 跳表节点里既存 score,也存 member。
👉 这两者组合在一起:
- 哈希表负责 快速定位某个元素的分数;
- 跳表负责 按分数有序存储、范围查询。
扩容过程(Redis Cluster 为例)
当要扩容(新增节点)时,流程大致是:
-
加入新节点
-
启动 Redis 实例,执行:
redis-cli --cluster add-node <new_ip>:<port> <existing_ip>:<port> -
新节点会加入集群,但暂时没有槽(slot)。
-
-
重新分配槽(slot rebalancing)
-
集群总共 16384 个槽,扩容时需要把一部分槽迁移给新节点:
redis-cli --cluster reshard <any_cluster_node_ip>:<port> -
输入要迁移的槽数、新节点 id、来源节点。
-
槽的迁移是在线进行的,业务不中断。
-
-
迁移数据(slot migration)
- Redis 会逐个 key 从源节点搬到目标节点。
- 客户端在迁移时可能遇到
MOVED或ASK重定向,客户端驱动会自动重试。 - 迁移完成后,新节点就开始对外提供服务。
1)准备阶段:给源/目标节点打标
对要迁移的每个槽 s,工具(redis-cli --cluster reshard 或其它运维工具)会先:
-
在源节点标记:
CLUSTER SETSLOT s MIGRATING <targetNodeId> -
在目标节点标记:
CLUSTER SETSLOT s IMPORTING <sourceNodeId>
含义:
- 源上 MIGRATING:这个槽里的 key 未来要走;如果有针对这些 key 的写操作打过来,源会用 ASK 临时重定向到目标。
- 目标上 IMPORTING:允许临时接收来自该源的写(需要客户端先发
ASKING)。
2)批量搬钥匙:GETKEYSINSLOT + MIGRATE
真正的数据迁移是按 key 批量搬的(不停机靠的就是“以 key 为粒度”的细粒度迁移 + ASK 临时重定向):
-
工具在源上反复取一批 key(比如 10~1000 条):
CLUSTER GETKEYSINSLOT s <count> # 例如 100 -
把这批 key 用 单次
MIGRATE发送到目标(同一条命令里可以带多 key):MIGRATE <target_ip> <target_port> "" 0 <timeout_ms> KEYS k1 k2 ... kn REPLACEMIGRATE会把每个 key(含类型、值、TTL)原子导入到目标;成功后在源删除(不带COPY时)。REPLACE表示目标已存在同名 key 时覆盖(正常不会冲突,保险起见加上)。- 这一步对单个 key 是阻塞/原子的,避免出现“半搬迁”的中间态。
循环执行直到该槽里已无 key。
小贴士:生产里一般控制 batch 大小和超时,避免长时间占用 IO 造成抖动。
3)收尾:宣布槽位归属变更(MOVED 生效)
当槽 s 的 key 都迁走了,运维工具会在集群所有节点上广播最终归属:
CLUSTER SETSLOT s NODE <targetNodeId>
至此,槽位的“权威映射”完成;之后客户端命中槽 s 会收到永久重定向 MOVED(而不是临时的 ASK)。
Redis 的性能瓶颈在哪里
虽然单线程很快,但在特定场景下还是有瓶颈:
- CPU 瓶颈
- 当请求量非常大、操作逻辑复杂(如 Lua 脚本、大量聚合操作、慢查询
SORT/ZRANGE)时,单线程 CPU 可能打满。
- 当请求量非常大、操作逻辑复杂(如 Lua 脚本、大量聚合操作、慢查询
- 内存带宽
- Redis 数据全在内存,受限于内存大小和带宽。
- 大量数据迁移、持久化(RDB/AOF rewrite)会占用带宽。
- 网络 I/O
- Redis 的吞吐在高并发场景下会受到网络带宽限制(尤其是 10Gbps 网卡上)。
- 持久化开销
- AOF 重写、RDB 快照时,会占用磁盘 I/O 和 CPU,可能导致主线程阻塞或延迟。
- 大 key/大对象
- 如果一个 key 特别大(比如几 MB 的 Hash 或 List),单次读写会阻塞主线程,造成卡顿。
什么是间隙锁(Gap Lock)
- 定义:锁定“索引记录之间的间隙”,防止其他事务在这个范围内插入数据。
- 特点:
- 不锁已有的行,而是锁住“空隙”。
- 主要用于防止 幻读(Phantom Read)。
例如:表里有索引值 10 和 20,如果某个事务对 10 加了间隙锁,那么 (10,20) 之间就不能插入新记录。
为什么需要间隙锁
- 在 RR(Repeatable Read,可重复读) 隔离级别下,MySQL 要解决 幻读。
- 如果没有间隙锁:
- 事务 A 查询
age between 10 and 20→ 结果空 - 事务 B 插入
age=15 - 事务 A 再查 → 发现“凭空多了一条” → 幻读
- 事务 A 查询
- 有了间隙锁,事务 B 在事务 A 提交前不能插入
15,避免幻读。
如果某行被加了“间隙锁”,能不能 UPDATE 这行?
- 可以的 ✅
因为 间隙锁本身不锁定已存在的行,只锁住空隙,防止别人插入新行。
- 例如:索引有
10和20,事务 A 对(10,20)加了间隙锁。 - 这时事务 B 仍然可以更新 id=10 或 id=20 的行,因为这两个行是记录锁的范围,不在间隙锁的限制里。
- 例如:索引有
ThreadLocal 的作用
- 让每个线程都能有一份 独立的变量副本,互不干扰。
- 常用于:数据库连接、用户会话、事务上下文等“线程私有”的数据。
👉 它不是用来解决多线程共享问题,而是避免共享。
二、ThreadLocal 的核心原理
- 每个线程(Thread 对象)里都维护一个
ThreadLocalMap- 这个
ThreadLocalMap是一个哈希表,key 是ThreadLocal对象,value 是实际存的值。 - 所以变量数据不是存在
ThreadLocal里,而是存在线程自己的 Map 里。
- 这个
- ThreadLocal 提供访问入口
- 调用
threadLocal.set(value)时:- 获取当前线程(
Thread.currentThread())。 - 从线程里拿到
ThreadLocalMap。 - 把
(threadLocal, value)存进去。
- 获取当前线程(
- 调用
threadLocal.get()时:- 从当前线程的
ThreadLocalMap里查到自己对应的 value。
- 从当前线程的
- 调用
- 隔离性原理
- 因为每个线程有自己独立的
ThreadLocalMap,所以即使是同一个ThreadLocal对象,不同线程读到的值也不同。
- 因为每个线程有自己独立的
ZAB 协议是什么
- 全称 ZooKeeper Atomic Broadcast,是 ZooKeeper 自己实现的一致性协议。
- 设计目标:保证事务的顺序一致性。
- 与 Paxos 类似,但更贴合 ZooKeeper 的 “一主多从”广播日志”模型。
三、ZAB 协议的两个阶段
1. 崩溃恢复阶段(Leader 选举)
- 当 Leader 崩溃或新节点加入时,进入恢复模式。
- 选出一个新的 Leader,保证:
- 新 Leader 具有最新的事务日志(数据不丢)。
- 所有 Follower 与 Leader 状态同步。
- ZooKeeper 常用 Fast Leader Election 算法来快速选主。
2. 消息广播阶段(正常工作)
- Leader 接收客户端写请求 → 生成事务 Proposal(提案)。
- Leader 把 Proposal 广播给所有 Follower。
- 超过一半 Follower ACK 确认后,Leader 提交事务 → 广播 Commit。
- 所有节点按相同顺序应用事务 → 数据一致。
👉 这就是 原子广播:要么所有节点都应用,要么都不应用。
第三次握手能不能传输数据?
👉 可以,但一般不这么做。
原因:
- TCP 允许 在第三次握手的 ACK 报文中携带数据(TCP 报文头里的 ACK 可以和数据一起发)。
- 但操作系统协议栈实现时,大多数内核不会在第三次握手的 ACK 里带应用层数据。
- 原因是:
- 第三次握手如果丢了,服务端还没确认连接,带的数据可能被浪费(需要重发)。
- 安全性考虑:避免被利用做 SYN Flood 攻击的载体。
- 一般业务逻辑上,应用层数据要等连接建立后才会发。
网关限流 + 熔断设计速查表(适用于 Nginx+OpenResty/Kong、Envoy/Istio、Spring Cloud Gateway+Resilience4j 等)。
目标与分层
- 入口网关:保护下游、隔离恶意流量、做配额/公平。
- 服务侧(或 Sidecar):本地保护、细粒度熔断、退避重试。
- 数据面:Redis/内存/RLS(Rate Limit Service)存计数与策略。
- 维度:按 租户/用户/API Key/IP/端点/HTTP 方法 分桶;支持优先级与套餐配额。
限流(Rate Limiting)
选算法(建议)
- 令牌桶(Token Bucket):允许突发(burst),平滑速率 —— 推荐。
- 滑动窗口计数 / 滑动日志:窗口更精确,成本更高。
- 并发数限制(Concurrency Limit):限制同时在途请求,抑制排队放大。
- 配额(Daily/Hourly):长周期配额由网关或后端批作业结算。
策略层次
- 全局:保护整体(例如 50k RPS,突发 100k)。
- 每租户/每 API Key:如 100 RPS,突发 200;套餐区分。
- 每端点:重型接口更严(比如
/search20 RPS)。 - 每 IP:防刷(如 10 RPS),白名单/灰名单可绕过或降档。
分布式实现
- 单节点网关:本地桶(内存 LRU)+ 周期同步即可。
- 多副本网关:用 Redis + Lua 或 Envoy Rate Limit Service 做中心计数,保证原子性。
- 哈希标签:将同一 key 的限流落在同一 Redis 分片,避免抖动。
- 时钟/一致性:窗口使用 TTL + 原子自增,避免跨节点计数撕裂。
熔断(Circuit Breaking)
状态机
- Closed:正常统计错误率/超时。
- Open:达到阈值后短路一段时间(如 30s),立即快速失败。
- Half-Open:放少量“探测流量”(如 5 个)验证恢复,成功则闭合。
触发信号(任取其一或组合)
- 错误率阈值:最近窗口(例如 10s/20–50 次调用)错误率 > 50%。
- 连续失败 N 次:如 N=10。
- 高延迟:P95/P99 超过阈值(比如 > 800ms)。
- 主动过载:本地队列长度/CPU/线程池拥塞(自适应并发)。
半开与恢复
- 半开允许 5 个探测;全部成功或成功率>80% → 关闭;否则重新打开,Backoff 叠加(30s→60s→120s…)。
配套策略
- 超时:网关到服务
connect 200ms / read 1s;服务内依职责不同 200ms–800ms。 - 重试:仅幂等读、不同副本/区域优先;设置重试预算(≤ 总量 10%)。
- 降级:静态兜底/缓存副本(stale-if-error)/回退到次优服务。
- 舱壁(Bulkhead):每后端独立连接池/线程池,互不拖累。
- 负载均衡+异常剔除:对异常实例做 outlier detection(Envoy)临时剔除。
实战参数(默认模板)
- 统计窗口:10s / 最少 20 次请求。
- 触发:错误率 ≥ 50% 或 连续失败 ≥ 10 或 P95 > 800ms。
- 打开时长:30s,指数退避。
- 半开探测:5 个请求,成功率阈值 80%。
监控与运营
- 指标:限流命中率、熔断打开次数、请求成功率、P95/99、在途并发、队列长度、重试命中率、下游实例 eject 率。
- 日志/追踪:在 429/502/504/熔断事件里打上 租户/端点/限流桶 key;TraceID 贯穿。
- 看板与告警:按租户/端点透视;错误预算(SLO)驱动阈值调整。
快速起步的“默认策略”
- 每租户:100 RPS(突发 200),并发 ≤ 50;超限 429。
- 重型端点:20 RPS;查询统一超时 800ms。
- 熔断:10s 窗口错误率 ≥ 50% 或 连续失败 ≥ 10 → 打开 30s;半开 5 探测,≥80% 关闭。
- 重试:GET/HEAD 最多 2 次,指数退避 + 抖动;POST 默认不重试。
- 降级:读用缓存兜底(stale-if-error≤30s),写回“已受理,稍后可查”。
Redis 的 Lua 脚本执行失败时,不会回滚已执行的部分。
详细解释
- Redis Lua 脚本的执行模型
EVAL或EVALSHA提交的脚本,会在 Redis 单线程事件循环中一次性执行完毕。- Redis 把脚本看作“一个整体的命令”,在执行过程中,不会被其他命令打断。
1. 队头阻塞(Head-of-Line Blocking, HoL)
定义:在一个按顺序传输的数据通道里,如果前面的数据包没到,后面的数据包就算已经到了也不能被处理 → 就像“高速路口前面一辆车堵住了,后面的全得停下”。
在 TCP/HTTP 里的表现:
- TCP 层:TCP 保证字节流的顺序性。如果某个包丢了,后面到的包要“排队”,直到丢失的包重传成功才能继续交付给应用层。
- HTTP/2 层:虽然 HTTP/2 支持多路复用,但它们还是跑在一个 TCP 流里 → 只要底层 TCP 出现丢包,所有流都被拖住。
后果:弱网下,单个丢包就能卡住多个请求/响应,延迟被放大。
2. 慢建连(Slow Connection Establishment)
定义:建立一个安全可靠的连接,需要经历多次“握手”,每次握手至少消耗一个 RTT(Round Trip Time,往返时延)。
在传统 TCP + TLS 下的流程:
- TCP 三次握手:客户端发 SYN → 服务端回 SYN+ACK → 客户端回 ACK
- TLS 握手:为了加密安全,再走 1–2 个 RTT(TLS 1.3 最少 1 RTT,TLS 1.2 需要 2 RTT)
结果:从零到可以传输应用数据,至少 2~3 个 RTT 才能开始,弱网/跨洋时延更明显。
例子:假设 RTT = 200ms(移动网络很常见),那 TCP+TLS 建连可能要 400–600ms 才能发第一个字节。
HTTP/3 = HTTP/2 语义 + QUIC 传输(基于 UDP),目的是绕开 TCP 的一些固有限制(尤其是队头阻塞和慢建连),在弱网/移动网络里显著更稳更快。它“不是不用网络层的传输协议”,而是不用 TCP,改用 QUIC(UDP)。
为什么很多场景想“别用 TCP”
- TCP 队头阻塞(HoL):HTTP/2 虽然多路复用,但底层还是一个 TCP 流;丢一个包时,所有流都要等。
- 建连慢:TCP 3 次握手 + TLS(1–2 个 RTT)。
- 连接迁移差:4 元组变了(切 Wi-Fi/蜂窝、NAT rebinding)就得重连,代价大。
HTTP/3/QUIC 怎么解决
- 应用层多路复用 + 无跨流 HoL:每个流独立重传,丢包只影响该流。
- 更快握手:TLS 1.3 集成进 QUIC,1-RTT 完成;支持 0-RTT 恢复(注意重放风险)。
- 连接迁移:用 Connection ID,IP/端口变更可以无感迁移,移动端体验更稳。
- 拥塞/丢包控制可进化:QUIC 在用户态实现,协议/算法迭代快(但也带来 CPU 开销更高的现实成本)。
- 可选不可靠通道:有 QUIC DATAGRAM 能发“尽力而为”的消息(游戏/实时信令),但 HTTP/3 主流程仍是可靠语义。
代价与坑
- CPU & 资源:加密、用户态栈、包处理比 TCP 栈更吃 CPU。
- 可观测性与运维:全程加密,L4/L7 可见度下降;需要更新链路监控与采样。
- 中间件/负载均衡支持:需要 QUIC/HTTP3-aware 组件;七层代理和防火墙策略要相应调整。
- 0-RTT 重放:启用 0-RTT 时,非幂等写操作要谨慎(可要求重放检测/禁用 0-RTT)。
- UDP 环境:极少数企业网络会限 UDP;要有 TCP/HTTP/2 回退路径(ALG、策略、端口开放)。
QUIC(Quick UDP Internet Connections) 是 Google 最初提出、后来 IETF 标准化的一种 基于 UDP 的传输层协议。 它的目标是:替代 TCP + TLS + HTTP/2 的组合,把传输层和安全层整合在一起,同时解决 TCP 的一些固有问题。
换句话说:
- TCP:传统传输层(保证可靠、有序)
- TLS:保证加密和安全
- HTTP/2:多路复用应用协议
- QUIC:把这三者打包进一个新协议里(在 UDP 上实现)
QUIC 的核心特性
- 跑在 UDP 之上
- 使用 UDP 作为“载体”,这样操作系统和网络设备都不会把它当作 TCP 处理。
- 在 UDP 上重新实现了可靠传输、流量控制、拥塞控制。
- 集成 TLS 1.3
- QUIC 自带加密,所有数据都必须加密。
- 握手更快(1-RTT,甚至 0-RTT),比 TCP+TLS 少一个往返延迟。
- 多路复用无队头阻塞
- 在 TCP 中,一个包丢了会卡住所有流。
- 在 QUIC 中,每个流独立排序和重传,一个流丢包不会影响别的流。
- 连接迁移
- QUIC 用 Connection ID 来标识连接,而不是传统的“四元组(IP+端口)”。
- 这样即使设备从 Wi-Fi 切到 4G,连接也能保持,不必重新建立。
- 可进化性强
- TCP 协议在内核中实现,要更新协议需要操作系统支持。
- QUIC 在用户态实现,迭代速度更快(Google Chrome、Cloudflare 都在频繁更新)。
分类角度(RFC 标准常用分类)
HTTP Header 大体分成四类:
- 通用首部(General Header)
- 请求和响应都会用到的字段
- 示例:
Date:报文生成的时间Connection:连接管理(如keep-alive、close)Cache-Control:缓存策略(如no-cache,max-age=3600)
- 请求首部(Request Header)
- 由客户端发送,描述客户端环境、期望的响应格式等
- 示例:
Host:请求的主机名和端口User-Agent:客户端类型(浏览器/设备信息)Accept:客户端能处理的响应类型(如text/html,application/json)Authorization:认证信息(如Bearer token)
- 响应首部(Response Header)
- 由服务器返回,描述服务器信息、响应相关的元数据
- 示例:
Server:服务器软件信息(如nginx/1.20)Set-Cookie:设置 Cookie 给客户端Location:重定向目标地址(常见于 302/301)
- 实体首部(Entity Header / Representation Header)
- 用来描述报文 body 的内容
- 示例:
Content-Type:内容类型(如application/json; charset=UTF-8)Content-Length:响应体的字节长度Content-Encoding:编码方式(如gzip,br)Last-Modified:资源最后修改时间
0. 先判断“谁”在烧 CPU
机器层面
top看占用最高的进程 PID(假设是12345)。top -H -p 12345看具体线程(LWP)的 CPU 占用;记下最高的那个 TID(十进制)。
把 LWP 映射到 Java 线程
- 把 TID 转 16 进制:
printf "%x\n" <tid>,得到如0x1a2b。 jstack 12345 | less搜索nid=0x1a2b,就能找到对应 Java 线程的堆栈(谁在干活一目了然)。
备选:
jcmd 12345 Thread.print也能打出线程堆栈(更快)。
1. 快速判断“CPU 高的类型”
A. 用户态计算忙(真算力) 堆栈多在你的业务代码里(计算、JSON/序列化、正则、解压、加密、数学循环、排序、流处理等)。
B. 自旋 / 锁竞争 / 阻塞切换频繁
堆栈显示 Unsafe.park、ReentrantLock.lock、synchronized 竞争、ForkJoinPool 繁忙、自旋重试等。
C. GC 导致的 CPU 高
GC Thread、G1 Conc、VM Thread 明显;jstat -gcutil 12345 1s 10 看到频繁 GC、CPU 飙升。
D. JIT/类加载抖动
刚启动或热点切换时,C2 CompilerThread 忙;可暂时属于“暖机期”。
E. JNI/Netty/IO 相关
栈里出现 JNI 调用、压缩/解压库、Netty NioEventLoop 紧张等。
2. 一分钟“止血”手段(不重启)
- 限流/降级:在网关或服务限流,先把系统拉回可控区间。
- 调大线程池不是万能:盲目加线程常让 CPU 更爆;先看堆栈与队列。
- 拉一段可视化采样:
jcmd 12345 JFR.start name=highcpu settings=profile filename=/tmp/hicpu.jfr duration=60s- 结束后:
jcmd 12345 JFR.stop name=highcpu,把/tmp/hicpu.jfr拿下来看(可用 JMC)。
- 轻量采样(Linux):
async-profiler./profiler.sh -d 30 -e cpu -f /tmp/cpu.svg 12345(火焰图直接看热点函数)
JFR/async-profiler 不会像
jstack那样只给瞬时状态,而是采样分布,更靠谱。
3. 系统性定位步骤
- 确认是否 GC 过高
jstat -gcutil 12345 1s 20看 YGC/FGC 频率、堆使用;- 开 GC 日志(若未开):
-Xlog:gc*:file=/var/log/gc.log:tags,uptime,time - 现象:FGC 频繁或 G1 并发标记长、升温即回收 → CPU 被 GC 吃掉。
- 线程热点
top -H -p+jstack找到几个最热 TID 的堆栈,通常就能锁定业务热点(某循环/正则/流/缓存穿透)。
- 锁竞争
jstack里大量BLOCKED/WAITING,或看到at java.util.concurrent.locks.ReentrantLock.lock/synchronized热点。- 用
jcmd 12345 Thread.print -l看拥有者和等待者,定位哪个锁把大家卡住。
- IO/网络
iostat -x 1/pidstat -d -p 12345 1看 IO;- 如果 CPU 高但堆栈多在 IO 等待,不是 CPU 真高,是上下文切换多或线程过量。
- 内核/系统层
mpstat -P ALL 1看是否单核打满;perf top看是否大量系统调用(epoll、socket、copy 等)。
4. 常见“元凶 → 修复方案”
- 无界循环/错误条件退出
- 修:加正确的终止条件与
sleep/backoff;避免忙等(busy wait)。
- 修:加正确的终止条件与
- 正则/JSON/序列化开销巨大
- 修:预编译 Pattern;简化正则;换更快的 JSON 库(如 Jackson/fastjson2 调优);对象复用、缓冲。
- 集合/算法劣化(N^2)
- 修:换数据结构(HashMap/Long2ObjectMap 等)、批量处理、分段排序、写入合并。
- 锁竞争 / 临界区过大
- 修:缩小临界区;读写锁拆分;用无锁/分段锁;减少共享状态;避免在锁内做 IO/重活。
ConcurrentHashMap分段/原子操作、LongAdder替换AtomicLong。
- 线程池配置不当
- 计算密集:
nThreads ≈ CPU核数(或核数 ± 一点) - IO 密集:
nThreads ≈ CPU核数 × (1 + IO等待时间/计算时间) - 队列要有界,拒绝策略要明确,避免雪崩。
- 核心/最大线程差太大也会导致频繁创建/切换。
- 计算密集:
- 缓存穿透/击穿
- 修:布隆过滤器、空值缓存、热点 Key 互斥/单飞、请求合并。
- GC 频繁
- 修:减少短命对象;复用 Buffer;开启/优化 G1(服务端默认);
- 合理设置堆大小、Pause 目标(
-XX:MaxGCPauseMillis=); - 观察 Survivor、晋升失败、Full GC 触发原因;必要时调大堆或调对象生命周期。
- Netty 事件循环被阻塞
- 修:在 EventLoop 线程里绝不做重活(IO 外任务 offload 到业务线程池)。
- JIT 抖动(启动期)
- 修:充分预热;或者 JIT 配置(通常无需改);观察是否是持续热点变化引发反复编译。
聊聊你对 MCP 的理解
核心定义
- MCP (Model Context Protocol) 是一种 标准化协议,用来规范 模型 ↔ 客户端 ↔ 工具/数据源 的交互。
- 背景类似当年 Language Server Protocol (LSP) 统一了 IDE 与语言服务的交互,MCP 则统一了 模型与外部工具 的交互。
为什么需要 MCP
- 现在不同模型(OpenAI、Anthropic、Ollama…)和 IDE/Agent 框架(Cursor、Claude Desktop、Copilot…)各有一套“调用工具”的方式,不统一,开发者接入成本高。
- MCP 让模型可以通过一套协议发现、描述并调用工具,而不需要每次都写一堆 glue code。
1. 为什么需要意向锁?
数据库里既有 行锁(细粒度,锁某一行数据)也有 表锁(粗粒度,锁整张表)。 问题是:
- 如果某个事务要对表加一个表锁(如全表共享锁/排他锁),数据库必须确保没有行锁与它冲突。
- 如果没有“标记机制”,数据库就得扫描整张表检查是否有行锁,代价太大。
所以引入 意向锁:在表级别放一个“意图标记”,表示“我这个事务在表的某些行上加了行锁”。这样数据库在需要加表锁时,只要检查意向锁,不需要逐行扫描。
2. 意向锁是什么?
- IS(Intention Shared):事务打算在表里某些行加共享锁(S)。
- IX(Intention Exclusive):事务打算在表里某些行加排他锁(X)。
- SIX:事务在表上有共享锁,同时打算对部分行加排他锁。
这些意向锁都是加在表上的,本身不会阻止别的事务读写具体的行,但会让表锁的获取受到约束。
3. 解决的问题
- 快速冲突检测:加表级锁时,只需检查表上的意向锁,而不用逐行检查。
- 兼容多粒度锁:支持行锁和表锁同时存在,保证正确性。
- 提高效率:减少锁管理的复杂度,避免频繁扫描全表。
4. 举例
事务 A:想更新一行 → 给这行加 X 锁,同时在表上放一个 IX 锁。 事务 B:想给整张表加 S 表锁(全表读)。
- 数据库看到表上已有 IX 锁,知道里面可能有行级排他锁 → B 的表级 S 锁和 IX 冲突 → B 需要等待。
如果没有意向锁,B 就得扫全表,确认有没有行被 X 锁住,非常低效。
✅ 总结一句话面试用: 意向锁是表级锁,用来表明事务在表中某些行上加了行锁。它的作用是解决表锁与行锁之间的兼容检测问题,使数据库能快速判断是否能安全地加表锁。
- filesort:当查询需要排序,但无法完全依赖索引顺序时,MySQL 会把需要排序的数据取出来,在内存或磁盘上做一次额外排序,这个过程就叫 filesort。
- 注意:这里的“file”是历史遗留说法,很多时候排序是在内存里完成的,不一定真的写磁盘。
2. 出现场景
order by的字段没有用到索引顺序。- 有多个排序字段,但现有索引不完全覆盖顺序。
- 索引能过滤数据,但排序字段不在索引里。
order by与where条件的索引冲突(无法同时利用)。
超卖的根因
- 可见性问题:第一个请求的订单还没提交事务,第二个请求看不到 → 都认为“还没下单”。
- 原子性问题:库存扣减与订单写入不是一个不可分割的原子操作 → 可能并发冲突。
2. 常见解决方案
方案一:数据库层唯一约束
- 在
order表上加唯一索引(user_id, product_id)。 - 即便两个请求同时进来,最终只会有一个成功插入,另一个会 违反唯一约束报错。
- 应用层捕获异常,返回“已下单”。 👉 这是最简单、可靠的兜底手段。
方案二:加锁(串行化用户操作)
- 悲观锁:查询库存时
select ... for update,保证同一商品的扣减是串行的。 - 分布式锁:对
user_id或product_id加 Redis 分布式锁(如SETNX),保证同一用户/商品只有一个请求在处理。 - 缺点:锁竞争可能成为瓶颈。
方案三:乐观锁(版本号/库存扣减)
-
在
inventory表里加stock和version字段:update inventory set stock = stock - 1, version = version + 1 where product_id = ? and version = ?; -
如果两条请求同时来,只有一个能成功更新,另一个会更新失败(返回0行),应用层重试/报错。
-
缺点:高并发下冲突较多,需要重试。
方案四:异步订单队列
- 下单请求先写入一个 消息队列(Kafka、RabbitMQ)。
- 消费者服务按顺序消费消息,逐个生成订单并扣减库存。
- 优点:彻底串行化,避免超卖;系统能削峰填谷。
- 缺点:增加延迟和架构复杂度。
方案五:缓存/限流保护
- 在 Redis 设置标志位
SETNX user_id:product_id,保证用户一次只下单一件。 - 请求成功下单后,写回数据库并删除标志。
- 可以和唯一索引双重保证。
3. 最佳实践组合
- 强约束:订单表唯一索引
(user_id, product_id)(防止同一用户重复下单)。 - 库存扣减:用乐观锁
update ... where stock > 0保证不会扣成负数。 - 高并发优化:前置 Redis 分布式锁或消息队列限流,减少数据库压力。
总体策略(设计之初就要埋好钩子)
- 默认只做“可加不减”的演进
- 字段只“新增可选(optional)”,不删除、不改语义、不改类型/含义。
- 避免必填字段后加(会破坏旧客户端)。
- 版本化(Versioning)
- 资源/接口版本:
/v1/...或Content-Type: application/vnd.foo.v1+json;gRPC/Proto 用包名/服务名标版本。 - 数据模型版本:响应里携带
schema_version或api_version便于客户端容错。 - 渐进弃用(Deprecation):加
Deprecation/Sunsetheader 或文档标注,给出迁移窗口与替代方案。
- 资源/接口版本:
- 宽进严出(Tolerant Reader)
- 客户端要 忽略未知字段;服务端返回时保证字段稳定、类型不变、默认值清晰。
- 错误码与枚举允许新增值,客户端用“unknown/OTHER”兜底。
- 幂等与可重放
- 写操作支持 幂等键(
Idempotency-Key)或业务唯一键;分页用游标(cursor)而非 pageNo,保证变更后结果稳定。
- 写操作支持 幂等键(
- 能力协商(Feature negotiation)
- 用 capabilities/flags 让客户端声明已支持的扩展,服务端据此开启新字段或行为(前向兼容)。
- 稳定排序与字段默认
- 序列/列表输出顺序要么声明“不保证”,要么明确排序规则,避免升级后顺序抖动。
- 对新增的可选字段给清晰默认值与空值语义(
nullvs 缺省)。
1. 什么是 API?
- API (Application Programming Interface)
- 面向应用开发者暴露的接口。
- 由框架/库提供,开发者直接调用。
- 设计目标:易用性、稳定性。
- 调用关系:应用 → 调用 → API。
例子
- JDBC 的
Connection#prepareStatement() - Spring 的
ApplicationContext.getBean() - Redis 客户端的
set(key, val)
2. 什么是 SPI?
- SPI (Service Provider Interface)
- 面向框架扩展/第三方实现者暴露的接口。
- 定义一组规范/约定,让别人来实现,再由框架在运行时动态加载。
- 设计目标:可扩展性、可插拔。
- 调用关系:框架 → 调用 → 开发者提供的 SPI 实现。
例子
- JDBC 驱动:
- API:
java.sql.DriverManager.getConnection() - SPI:
java.sql.Driver(由 MySQL / Oracle / PostgreSQL 驱动实现,JDK 通过 SPI 机制发现并加载)。
- API:
- 日志框架:
- API:
Logger.info("xxx")(由应用调用) - SPI:
org.slf4j.Logger(不同日志实现:logback/log4j2 来实现,SLF4J 运行时加载)。
- API:
3. 区别总结
| 对比项 | API | SPI |
|---|---|---|
| 面向对象 | 应用开发者 | 框架扩展/第三方实现者 |
| 调用方向 | 应用 → 框架/库 | 框架/库 → 应用提供的实现 |
| 目标 | 稳定、易用 | 扩展、可插拔 |
| 生命周期 | 编译期直接依赖,运行时直接调用 | 编译期依赖接口,运行时发现/加载实现 |
| 使用方式 | 直接 import 调用 | 在 META-INF/services/ 下注册实现类 |
| 场景举例 | SDK 提供的函数、类库调用 | 插件机制、驱动发现、日志实现、序列化框架 |
- SSL (Secure Sockets Layer)
- 最早由 Netscape 在 1990s 提出的一套安全通信协议。
- 主要目标:在 TCP 之上提供加密、身份认证和数据完整性保障。
- 主要版本:SSL 2.0、SSL 3.0(现在都被废弃)。
- TLS (Transport Layer Security)
- IETF 在 SSL 3.0 基础上标准化的后续版本。
- 是 SSL 的升级和标准化,语义兼容。
- 当前常用版本:TLS 1.2、TLS 1.3。
- 严格来说,现在我们用的“HTTPS”走的就是 TLS,不是 SSL。
1. 加密目标
无论 SSL 还是 TLS,本质都想在 TCP 之上提供:
- 机密性:数据被窃听也看不懂(对称加密)
- 完整性:数据未被篡改(MAC/哈希校验)
- 身份认证:确认对方身份(数字证书、CA)
2. 核心思路
- 公钥加密/密钥交换:在握手阶段安全地交换对称密钥。
- 对称加密:会话期间用同一个会话密钥加密所有数据(快)。
- MAC/哈希:对每个报文做完整性校验。
3. SSL(以 SSL 3.0 为例)的握手加密流程
-
客户端 Hello
- 告诉服务端自己支持的 SSL 版本、加密套件、随机数。
-
服务端 Hello
- 确认协议版本和加密套件,返回证书(含公钥)、随机数。
-
客户端生成 Premaster Secret
- 用服务端证书的公钥加密后发给服务端。
-
双方计算 Master Secret
-
客户端和服务端用:
MasterSecret = PRF(Premaster, ClientRandom, ServerRandom)
-
-
生成对称密钥
- 从 Master Secret 推导出对称加密和 MAC 的密钥。
-
交换“Finished”消息
- 表示后续都用对称加密。
👉 SSL 3.0 主要问题:依赖弱算法(RC4、MD5)、消息认证用的 MAC 容易被攻击。
4. TLS(以 TLS 1.2 为例)的握手加密流程
TLS 基本继承 SSL,但做了增强:
- ClientHello:支持 TLS 版本、加密算法列表、随机数。
- ServerHello:确认算法,返回证书、公钥、随机数。
- 密钥交换(两种方式):
- RSA:客户端生成 Premaster,用服务端公钥加密发给服务端。
- DH/ECDHE:双方协商临时密钥,支持 Perfect Forward Secrecy (前向保密)。
- 双方计算 Session Key(会话对称密钥)。
- Finished:验证握手完整性,进入对称加密通信。
👉 TLS 的改进:
- 引入更强的哈希(SHA-2),弃用 MD5、SHA1。
- 支持 Diffie-Hellman/ECDHE,提供前向保密。
- MAC 计算方式比 SSL 更安全。
5. TLS 1.3 的优化(现行主流)
- 握手简化:从原来的 2-RTT 缩短到 1-RTT,性能更好。
- 只保留强加密算法:AES-GCM、ChaCha20-Poly1305;不再支持 RSA key exchange。
- 前向保密强制化:必须用 ECDHE。
- 0-RTT 模式:可以复用之前的会话,首次请求时就能发送加密数据(但要注意重放攻击)。
1. Client Hello = Alice 打招呼
Alice 给 Bob 说:
“我支持这些加密方法(算法菜单),我还给你一个随机数(ClientRandom)。”
👉 就像她把“自己喜欢的暗号方式清单”和一张小卡片交给 Bob。
2. Server Hello = Bob 回应
Bob 回信:
“好,我选其中一种加密方法(协商算法),再给你我的随机数(ServerRandom)。另外,这是我的身份证(数字证书,里面有我的公钥)。”
👉 就像 Bob 出示护照,证明“我真的是 Bob,不是冒充的”。
3. Client Key Exchange = Alice 做暗号本
Alice:
“好,那我生成一份秘密的初始暗号(Premaster Secret),用 Bob 的公钥把它锁在箱子里,只有 Bob 能用私钥打开。”
👉 这一步确保:就算快递被截获,小偷没有 Bob 的私钥,也打不开。
4. 双方算出 Master Secret = 一起生成暗号本
Bob 用自己的私钥打开箱子,拿到 Premaster Secret。 然后:
MasterSecret = 函数(Premaster, ClientRandom, ServerRandom)
👉 相当于 Alice 和 Bob 各自用三样材料(前两张卡片 + 秘密暗号)一起搓出了同一本“暗号本”。
5. 生成对称密钥 = 得到共同语言
从 Master Secret 派生出多把钥匙:
- 用于加密消息的钥匙
- 用于校验消息完整性的钥匙
👉 相当于他们现在有一本“字典”,里面记了“苹果 = 19,香蕉 = 42”,之后都靠它交流。
6. Finished = 互相确认
Alice 和 Bob 各发一条测试消息:“好啦,暗号准备好了!” 如果对方能正确解读,就证明暗号一致,以后就可以直接用它交流。
RocketMQ vs Kafka 的区别
RocketMQ 和 Kafka 都是高性能消息队列,但定位和设计略不同:
| 对比点 | RocketMQ (RMQ) | Kafka |
|---|---|---|
| 起源 | 阿里巴巴,电商/金融场景 | LinkedIn,日志/大数据场景 |
| 存储结构 | CommitLog + ConsumeQueue,支持任意 Topic/Queue 数量 | Partition 顺序文件,Topic/Partition 数量过多性能下降 |
| 消息模型 | Queue/Topic 灵活,支持广播、顺序、延时 | 主要是发布订阅,顺序依赖 Partition |
| 消费语义 | 至少一次,支持顺序消费、事务消息 | 至少一次(默认),支持幂等但无事务消息(Kafka 0.11+ 加了幂等 producer 和事务 producer) |
| 事务支持 | 原生支持分布式事务消息(回查机制) | 有事务 API,但主要是保证生产端的原子性,不是强分布式事务 |
| 延时消息 | 支持(定时任务、延时队列) | 无内置延时消息机制,需要外部实现 |
| 高可用 | Master-Slave + DLedger(Raft 协议) | Partition 副本(ISR 机制) |
| 应用场景 | 金融、电商、订单、支付(强一致/事务需求) | 日志采集、流处理、大数据管道(吞吐优先) |
Netty 是什么?
- Netty 是基于 Java NIO 封装的 异步事件驱动网络通信框架。
- 它屏蔽了 Java 原生 NIO API 的复杂性(
Selector、Channel、ByteBuffer),提供了一套简洁易用的 API。 - 广泛用于高性能网络应用,比如 RPC 框架(Dubbo)、消息中间件(RocketMQ)、游戏服务器。
2. 核心特性
- 异步非阻塞:基于 Reactor 模型,能同时处理成千上万连接。
- 事件驱动:用事件和回调处理 IO,解耦读写逻辑。
- 高性能:零拷贝(ByteBuf)、内存池、批量处理。
- 跨平台:支持多种传输实现(NIO, Epoll, KQueue)。
- 可扩展:基于 Pipeline 的责任链模式,用户可以灵活定制协议、编解码、业务逻辑。
3. 核心组件
- Channel:网络连接的抽象(客户端 SocketChannel / 服务端 ServerSocketChannel)。
- EventLoop:事件循环,绑定一组 Channel,负责监听事件并回调。
- ChannelHandler:处理逻辑的处理器,比如解码器、业务逻辑。
- Pipeline:责任链模式,把多个 Handler 串起来。
- Future/Promise:异步结果的封装。
4. Netty 的工作原理(简化)
- BossGroup(EventLoopGroup):接收客户端连接。
- WorkerGroup:处理具体的读写事件。
- ChannelPipeline:消息经过一串 Handler(解码、业务处理、编码)。
- 异步执行:IO 操作返回 Future,真正完成后触发回调。
👉 这就是典型的 Reactor 多线程模型。
5. 应用场景
- RPC 框架:Dubbo、gRPC-Java 底层通信。
- 消息中间件:RocketMQ、Kafka(部分模块)网络层。
- 长连接/即时通讯:IM、推送系统、游戏服务器。
- HTTP 网关/代理:Spring Cloud Gateway、服务网关。
什么是 MySQL 主从延迟
- 在 主从复制(主库写、从库读)架构中,主库事务提交成功,但 从库还没来得及执行,这段时间的差异就是 主从延迟。
- 体现为:
- 在主库写入一条数据,马上去从库查可能查不到。
- 延迟可能是毫秒级,也可能在高负载时达到几秒甚至几十秒。
2. 为什么会有延迟(常见原因)
- 复制原理本身有异步
- 主库写 binlog → 传输到从库 → 从库应用到 relay log → 执行 SQL。
- 这中间有天然的“队列”和网络传输延迟。
- 从库性能差/压力大
- 从库机器配置低于主库,或者从库读请求太多,导致执行速度跟不上。
- SQL 本身耗时
- 大事务、复杂查询(慢 SQL)、DDL 操作。
- 单线程复制(MySQL 5.6 之前)
- 从库只能单线程按顺序执行 binlog,遇到慢事务就会拖慢整体。
- 虽然 MySQL 5.7+ 支持多线程复制,但在表/库分配上仍有限制。
3. 怎么解决主从延迟
(1)架构层面
- 业务区分读写:对强一致性要求高的请求走主库;允许弱一致性走从库。
- 缓存前置:热点读尽量从缓存(Redis)拿,降低从库压力。
- 延迟可见性:监控延迟指标(
Seconds_Behind_Master),超过阈值则自动切主。
(2)MySQL 参数与机制
- 半同步复制(Semi-Sync Replication)
- 主库提交事务时,至少要等一个从库确认收到 binlog 才返回成功。
- 保证“写成功 ≈ 至少一个从库有数据”,延迟减少但吞吐下降。
- 并行复制(MySQL 5.7+ 支持,基于库/表/事务组)
- 从库用多线程并行应用 relay log,显著降低延迟。
- 优化 binlog 格式
- 用 ROW 格式 替代 STATEMENT,减少重放时的复杂逻辑。
(3)SQL 优化
- 拆分大事务(避免一次写入几万行)。
- 避免从库执行耗时 DDL。
- 主库 binlog 刷盘策略优化(
sync_binlog=1确保安全但要考虑性能)。
1. 写扩散(Fanout-on-Write)
- 机制:消息一旦写入,就被“扩散”到所有接收用户的收件箱(inbox / queue)。
- 特点:
- 实时性强:消息写的时候就推送到目标用户的消息存储结构里,用户一查就能看到。
- 写放大:如果群里有 10 万人,就要写 10 万份索引或队列记录。
- 典型场景:朋友圈、DM、群人数较少的聊天。
2. 读扩散(Fanout-on-Read)
- 机制:消息只写一份(在会话 / Topic 的 timeline 里),用户读取时再根据自己的游标拉取。
- 特点:
- 写入轻:只写一份,写吞吐高。
- 读时聚合:用户读消息时,需要“从公共消息流里 + 自己的游标/过滤”算出来。
- 实时性取决于推还是拉:
- 如果系统支持 推送(长连接、通知),读扩散也可以实时收到。
- 如果只能 轮询拉取,就会像你说的那样,用户可能有延迟。
- 典型场景:大群聊、微博 timeline、推特 timeline。
读扩散 + 推送通知的区别
在 读扩散 体系里,消息只存一份(群 timeline)。
- 推送通知(例如 APNs/FCM/长连接信令)只是告诉用户: 👉「你有新消息,快来拉取」。
- 这一步并不把完整消息写入每个用户的 inbox,而是:
- 客户端拿到通知后,去拉取 timeline 上自己没读过的消息(通过游标/offset)。
- 这样流量压力大部分在客户端拉取环节,由用户分散消化,而不是在写入时放大。
- 每个线程对象(Thread)内部有一个
ThreadLocalMap,相当于一个特殊的Map<ThreadLocal, Object>。 - 当调用
threadLocal.set(val)时:- 先取当前线程的
ThreadLocalMap; - 如果没有就创建一个;
- 把
(当前ThreadLocal实例, 值)存进去。
- 先取当前线程的
👉 所以,ThreadLocal 的本质是:在每个线程自己维护一份独立的变量副本。
为什么 ThreadLocalMap 的 key 用 弱引用(WeakReference)
-
ThreadLocalMap 的 Entry 设计是这样的:
static class Entry extends WeakReference<ThreadLocal<?>> { Object value; } -
即 key 是
ThreadLocal的弱引用,value 是强引用。
原因:防止内存泄漏
- 如果 key 是强引用:
- 即使程序不再用这个
ThreadLocal对象了,只要线程还在运行,ThreadLocalMap里会一直有个强引用指向它 → 不能被 GC 回收 → 内存泄漏。
- 即使程序不再用这个
- 如果 key 是弱引用:
- 一旦没有外部强引用指向
ThreadLocal,这个 key 就会被 GC 回收。 - 虽然此时
value还在,但 key 变成了null,ThreadLocalMap 的机制会在下一次访问时把它清理掉(即 “stale entry”)。
- 一旦没有外部强引用指向
举个例子:
- 你写了一个
ThreadLocal<User>,用完后没调用remove()。 - 如果 key 是强引用:
User对象会一直挂在线程里,线程不死就不会释放。 - 如果 key 是弱引用:GC 能回收掉
ThreadLocal对象,value 虽然暂时还在,但下一次 ThreadLocalMap 操作时会清理它,减少泄漏风险。
微服务之间调用超时了你会怎么处理
从 超时处理 → 重试机制 → 降级策略 → 工程化保障 这四个方面来回答。
1. 超时处理(第一道防线)
- 调用必须设置超时时间:不能无限等待。
- RPC 框架(gRPC、Dubbo、Spring Cloud)通常有
timeout参数。 - 建议配合 整体请求超时预算(deadline)透传,避免级联超时。
- RPC 框架(gRPC、Dubbo、Spring Cloud)通常有
- 及时 fail fast:避免线程阻塞耗尽,保护调用方。
2. 重试机制
- 条件:
- 只对 幂等接口 才能安全重试(比如 GET、查询类请求)。
- POST/扣款/下单这类非幂等请求要加幂等键才能安全重试。
- 策略:
- 有限次数(如最多重试 3 次)。
- 指数退避 + 抖动(避免重试风暴)。
- 熔断器配合:若下游持续超时,熔断后不再重试。
3. 降级策略(第二道防线)
当超时/失败率过高时,调用方要有备用方案,保证核心流程可用。常见手段:
- 缓存降级
- 从本地缓存 / Redis 返回上一次结果或兜底数据。
- 适合配置查询、推荐、商品列表等对实时性要求没那么高的场景。
- 静态兜底
- 直接返回默认值 / 友好提示。
- 例如:“推荐服务不可用,先看看热榜”。
- 业务降级
- 非核心链路关闭或弱化。
- 例如秒杀流量过大,临时关闭个性化推荐,只保留下单服务。
- 异步补偿
- 请求先写消息队列,异步慢慢处理,前端给用户“稍后到账”的提示。
- 常见于支付、积分、消息通知等场景。
4. 工程化保障
- 熔断(Circuit Breaker):
- 下游持续超时或失败率超过阈值,快速失败,避免雪崩。
- 定期尝试半开恢复。
- 限流(Rate Limiting):
- 给下游打保护伞,避免瞬时洪峰压垮服务。
- 监控与报警:
- QPS、RT(响应时间)、错误率。
- 异常流量触发告警,人工介入。
这是消息队列设计的必考点。要点是 怎么重试、怎么避免重复和雪崩、怎么保证业务正确性。我分几层讲:
1. 为什么会消费失败?
- 下游服务不可用(超时、宕机)
- 消息本身有问题(数据不合法、幂等校验失败)
- 临时网络抖动、依赖接口超时
所以要区分 可重试错误 和 不可重试错误。
2. 重试机制设计
(1)立即重试(同步)
- 消费端捕获异常,马上重试 1–3 次。
- 优点:快速恢复短暂问题。
- 缺点:如果下游挂了,容易阻塞消费线程,拖慢整体。
(2)延迟重试(推荐)
- 把失败的消息丢到 延迟队列/重试队列,隔一段时间再拉取消费。
- 常用方式:
- Kafka:利用 死信队列(DLQ) + 延时机制
- RocketMQ:内置 重试 Topic,支持指数退避
- RabbitMQ:利用 TTL + DLX 实现延迟重试
- 好处:避免短时间内疯狂打失败下游(避免雪崩)。
(3)最大重试次数
- 每条消息设置 最大重试次数(例如 3 次、5 次)。
- 超过次数仍失败 → 进入 死信队列(DLQ),供人工介入或离线修复。
3. 需要注意的问题
- 幂等性
- 消息可能被多次投递 → 消费者必须支持幂等。
- 常见做法:唯一业务 key(如 orderId)+ 数据库唯一约束 / Redis
SETNX。
- 重试间隔
- 不能一失败就疯狂重试,会造成 雪崩。
- 一般用 指数退避 + 随机抖动:如 1s → 2s → 4s → 8s…
- 消息顺序性
- 如果消息有顺序要求,重试时要特别注意可能打乱顺序。
- 常见做法:按分区/Key 投递,或用串行消费。
- 死信队列 (DLQ)
- 重试超过阈值的消息必须有去处。
- 避免丢消息,同时给排查入口。
- 可观测性
- 失败率、重试次数、DLQ 堆积量要有监控和报警。
4. 设计举例(RocketMQ)
- 消费失败 → 自动重试(默认最多 16 次,指数退避)。
- 超过次数 → 投递到 死信队列
%DLQ%consumer_group。 - 运维人员或补偿服务可以订阅 DLQ,人工修复。
1. JVM 调优常关注的参数
主要分成 堆内存、垃圾回收、线程栈、诊断工具几类:
(1)堆内存相关
-Xms:JVM 初始堆大小-Xmx:JVM 最大堆大小(调优时通常和-Xms设成一样,避免扩容抖动)-Xmn:新生代大小(影响 Minor GC 频率)-XX:NewRatio:老年代和新生代的比例-XX:SurvivorRatio:Eden 和 Survivor 的比例
(2)GC 相关
-XX:+UseG1GC/-XX:+UseParallelGC/-XX:+UseZGC:选择合适 GC 算法-XX:MaxGCPauseMillis:期望最大停顿时间-XX:ParallelGCThreads:GC 并行线程数-XX:ConcGCThreads:并发 GC 线程数
(3)线程与方法区
-Xss:每个线程的栈大小-XX:MetaspaceSize/-XX:MaxMetaspaceSize:元空间大小(JDK8 以后替代 PermGen)
(4)诊断和观测
-XX:+PrintGCDetails、-XX:+PrintGCDateStamps、-Xlog:gc*:GC 日志-XX:+HeapDumpOnOutOfMemoryError:OOM 时自动生成 heap dump
2. 如何判断是不是内存泄漏
内存泄漏的本质:对象已经没有业务用途,但仍然被引用着,无法被 GC 回收。
判断方法:
- 监控内存曲线(JVM 监控工具如 JConsole、VisualVM、Prometheus + Grafana)
- 正常情况:堆使用量 锯齿状上升又下降(GC 后能回落)。
- 内存泄漏:堆使用量 持续上升,Full GC 后也不能明显下降。
- 观察 GC 日志
- 正常:Full GC 能释放大部分老年代对象。
- 泄漏:Full GC 之后老年代占用依然很高,越来越接近
-Xmx。
- Heap Dump 分析
- 用
jmap -dump:live,format=b,file=heap.hprof <pid>导出堆。 - 用 MAT(Memory Analyzer Tool)或 VisualVM 分析:
- 查看 占用最多的对象;
- 检查 GC Roots 路径(是谁引用着它不放)。
- 如果发现无用对象仍被静态集合、缓存、线程本地变量(ThreadLocal)持有,就是泄漏。
- 用
- 常见泄漏场景
static Map/List不断往里加元素,不清理ThreadLocal忘记 remove()- 连接/句柄(JDBC、IO、Netty buffer)未关闭
- Listener/回调注册后未释放
1. 切片的底层实现
在 Go 里,slice 不是数组本身,而是一个描述符(slice header),包含三个字段:
type slice struct {
ptr *T // 指向底层数组的指针
len int // 当前切片的长度
cap int // 底层数组的容量
}
ptr:指向底层数组的起始位置len:可访问的元素个数cap:从 ptr 开始到底层数组末尾的最大可用空间
👉 所以,slice 是一个“引用类型”,但它本质上是一个小结构体,存的只是指针和元信息。
2. 函数传递时的表现
- 传参是值传递:当你把
slice传给函数时,复制的是这个slice header(ptr、len、cap),不是整个数组。 - 但是:因为 header 里的
ptr指向同一个底层数组,所以在函数里修改slice[i]的值,会反映到原切片上。
channel 的基本结构
在 Go 的 runtime 里,channel 是一个结构体(hchan),核心字段包括:
- buf:环形队列的数组指针(存放元素)
- sendx:下一个写入元素的位置(环形索引)
- recvx:下一个读出元素的位置(环形索引)
- qcount:当前缓冲区中的元素个数
- dataqsiz:缓冲区大小
- sendq / recvq:等待发送和接收的 goroutine 队列(双向链表,存放
sudog) - lock:互斥锁,用于保证并发安全
Go 的垃圾回收分为 标记(mark) 和 清除(sweep) 两个阶段,属于 标记-清除(Mark-Sweep)算法。流程如下:
- STW(Stop The World)开始
- 暂停用户程序(mutator),收集 GC roots(栈、全局变量、寄存器等可达对象)。
- 这是短暂的 STW,用来启动一次 GC 循环。
- 标记阶段(Mark)
- 使用 三色标记法:对象会经历 白色 → 灰色 → 黑色 的转换。
- 从 root 开始,把直接可达的对象染成灰色,进入标记队列。
- 运行 GC worker goroutines 与 mutator 并发执行:
- worker 从灰色队列取对象,把它们的子对象染灰,并将自己染黑。
- 最后灰色队列为空,黑色对象即“存活”,白色对象即“不可达”。
- 写屏障(write barrier): 在标记阶段,mutator 仍可能修改指针,导致“丢失可达性”。Go 在写操作时插入屏障逻辑,保证新引用的对象也能被染灰。
- STW(End)
- 再次短暂停顿,清理剩余的标记队列,确保没有遗漏。
- 清除阶段(Sweep)
- 扫描堆内存,把未标记(白色)的对象回收。
- 清除是分批次执行的,避免长时间阻塞。
2. 三色标记法(核心思想)
- 白色:未被访问到的对象,可能是垃圾。
- 灰色:可达,但其子对象还没扫描。
- 黑色:可达,且子对象都已扫描。
GC 目标:结束时没有灰色对象,白色对象就是垃圾,黑色对象是存活的。
所谓 主从同步策略,本质上是指主库写入 binlog 后,从库如何接收并应用日志。常见的几种策略主要围绕 同步时机 / 等待程度 / 数据一致性 来划分。
1. 异步复制(Asynchronous Replication)
- 机制:主库写事务 → 记录 binlog → 提交事务成功并返回给客户端 → 异步把 binlog 发给从库。
- 优点:主库性能最好,写延迟最小。
- 缺点:主从延迟大时,可能出现数据丢失(主库挂了,从库没收到最新日志)。
- 适用:对一致性要求不高、追求吞吐量的场景。
2. 半同步复制(Semi-Synchronous Replication)
- 机制:
- 主库写事务 → 写 binlog → 至少等待 一个从库确认收到 binlog 才返回成功。
- 如果超时没等到确认,会退化为异步模式。
- 优点:保证至少一个从库有最新数据,减少数据丢失概率。
- 缺点:增加写延迟,吞吐量下降。
- 适用:对数据可靠性要求较高的场景(金融、电商核心交易)。
3. 强同步复制(Synchronous Replication)
- 机制:主库提交事务时,需要等待 所有从库确认 日志写入成功,才返回客户端。
- 优点:保证所有节点数据一致。
- 缺点:延迟非常高,只要一个从库慢,就拖累全局;可用性差。
- 适用:极少使用,只有对强一致性要求极端高的系统才会考虑。
4. 并行复制(Parallel Replication)
- 问题:传统复制是单线程,从库要顺序执行 relay log,容易落后。
- 优化:MySQL 5.7+ 支持基于库 / 基于组提交的并行复制,从库多线程应用 binlog。
- 效果:减少主从延迟,提高从库追赶速度。
Docker 底层实现
Docker 不是重新造 OS,而是利用 Linux 内核特性:
- Namespace(命名空间隔离)
- PID、NET、IPC、MNT、UTS、USER
- 保证容器进程、网络、文件系统互不干扰。
- Cgroups(控制组)
- 限制 CPU、内存、IO、网络等资源使用,防止容器抢占宿主机。
- UnionFS / OverlayFS
- 镜像分层存储,支持快速构建、共享只读层。
- 容器引擎
- 通过
runc等调用 Linux 内核接口来创建/运行容器。
- 通过
👉 简单说:Namespace 负责“看起来独立”,Cgroups 负责“资源可控”,UnionFS 负责“镜像可复用”。
Lua 脚本在 Redis 里的运行方式
- Redis 提供了
EVAL/EVALSHA命令来执行 Lua 脚本。 - 当你执行
EVAL时,Redis 会:- 把整个 Lua 脚本作为一个命令提交给 Redis;
- Redis 把脚本加载进内置的 Lua 解释器;
- 在单线程里一次性执行完脚本,然后再去处理下一个命令。
3. 为什么是原子性的
- 因为脚本执行过程是不可中断的:
- 在 Lua 脚本执行期间,Redis 不会去处理别的客户端请求。
- 所以脚本里的多个 Redis 命令(GET、SET、DEL…)会作为一个整体串行执行。
- 这就像事务里的“原子性”,保证脚本内部操作要么全部完成,要么(执行出错时)全部不生效。
4. 注意事项
- 原子性 ≠ 事务
- Lua 脚本执行期间,其他客户端无法插入命令,但 Lua 脚本本身不会自动回滚。
- 如果中途报错,前面执行的 Redis 命令已经生效。
- 所以要自己在脚本里处理错误场景。
- 长时间脚本会阻塞
- 因为 Redis 单线程,脚本如果写得很慢(比如循环 10 万次),会导致整个实例“卡死”,所有请求都要等它跑完。
- 所以官方建议 Lua 脚本应保持 快速、轻量。
- 结合 CAS(Check And Set)
- 常见用法:实现分布式锁、计数器、自定义原子操作。
- Lua 脚本能把“检查条件 + 修改操作”放在一个原子单元里执行。
1. Redis Lua 脚本的“原子性”
- 含义:执行脚本时不会被其他命令打断,脚本里的多条 Redis 命令视为一个整体,外界看不到中间状态。
- 失败时的表现:
- 如果脚本执行中途报错(比如语法错误、访问了不存在的 key 的字段),Redis 会中止脚本。
- 但是 前面已经执行过的命令不会回滚,它们依然修改了数据。
- 所以这里的“原子性”更接近于 不可分割性(要么完整执行,要么出错中止),但不具备事务回滚能力。
2. 数据库事务的原子性
- 数据库事务(MySQL、Postgres 等)依赖 日志(undo log、redo log) 和 事务管理器 来保证:
- 如果事务执行过程中失败或主动回滚,数据库会撤销之前已经执行的修改。
- 最终保证事务中的操作要么全部成功,要么全部失败(回滚到执行前状态)。
👉 这才是 ACID 原子性 的严格含义。
ChatGPT said:
这个问题挺常见的,考察对 TCP 连接建立过程 和 系统调用差异的理解。我们来分情况:
1. IP 地址写错的情况
(1)IP 地址无效(比如 256.0.0.1)
- 表现:
connect()调用立即返回错误(EINVAL/EADDRNOTAVAIL)。 - 原因:操作系统在本地就能判断 IP 不合法或没有对应路由,不会发起真正的网络请求。
(2)IP 地址合法,但没有主机(比如 192.168.1.250,但局域网里没人用)
- 表现:
connect()会阻塞一段时间,最后报错ETIMEDOUT。 - 原因:内核会发起 ARP 请求 / SYN 包,但对方不存在,收不到响应,直到超时。
2. 端口号写错的情况
- 场景:IP 地址正确,对应主机存在,但端口没有进程在监听。
- 表现:
connect()很快返回ECONNREFUSED(连接被拒绝)。 - 原因:目标主机的 TCP 栈收到了 SYN 包,发现端口没人监听,会立即回一个 RST 包,告诉客户端“拒绝连接”。
1. 僵尸进程的定义
- 在 Linux/Unix 里,僵尸进程(Zombie Process)指的是:
- 子进程已经 结束运行(exit),
- 但它的 退出状态信息还保留在内核进程表中,
- 等待父进程调用
wait()或waitpid()来收集(reap)。
👉 通俗点说:身体死了,但“户口”还在,所以叫僵尸。
2. 为什么会有僵尸进程
- 父进程 fork 出子进程。
- 子进程执行完毕,调用
exit()退出。 - 内核会把子进程的退出码、资源使用情况保留在进程表里。
- 父进程必须调用
wait()系列函数来读取这个状态 → 之后内核才能真正删除进程表项。 - 如果父进程没调用
wait(),这个条目就一直残留 → 僵尸进程。
3. 僵尸进程的危害
- 占用 进程号(PID):系统的 PID 是有限的(一般 32768 / 4194304)。
- 如果大量僵尸进程积累,可能导致新进程无法创建。
- 本身不占 CPU 和内存(因为已经退出,只剩内核表项)。
4. 如何避免 / 解决僵尸进程
- 父进程调用
wait()- 正确编写程序:在父进程里调用
wait()或waitpid()等待子进程回收。
- 正确编写程序:在父进程里调用
- 捕捉 SIGCHLD 信号
- 父进程注册
SIGCHLD信号处理函数,在子进程退出时自动调用wait()。
- 父进程注册
- 让子进程被 init 进程收养
- 如果父进程提前退出,子进程会被 1 号进程(init/systemd)接管,init 会自动回收。
- 强制手段
- 杀掉父进程 → 子进程会被 init 收养 → 自动回收僵尸。
1. 普通场景下的 ThreadLocal
- ThreadLocal 原理:每个线程内部有一个
ThreadLocalMap,存放ThreadLocal → 值。 - 特点:变量和线程绑定,线程结束时变量自然随之消失。
- 好处:避免多线程共享变量导致的并发问题。
2. 在线程池中的特殊情况
- 线程池会复用线程:
- 当一个任务执行完,线程不会销毁,而是放回池子里等待下一个任务。
- ThreadLocal 存的数据不会自动清理:
- 如果任务里设置了
ThreadLocal.set(x),执行完没调用remove(),这个值仍然存在于线程的ThreadLocalMap中。 - 下一个复用同一线程的任务,可能“继承”上一次遗留的数据 → 造成 脏数据泄露。
- 如果任务里设置了
Spring 是如何实现事务的
Spring 事务本质上是对 数据库本地事务(JDBC、JTA 等) 的封装,通过 AOP + 事务管理器 实现:
- 事务管理器(PlatformTransactionManager)
- 负责开启、提交、回滚事务。
- 常见实现:
DataSourceTransactionManager(JDBC,本地事务)JpaTransactionManager(JPA)JtaTransactionManager(分布式事务)
- 事务拦截器(TransactionInterceptor)
- Spring AOP 拦截
@Transactional方法调用。 - 进入方法前:调用事务管理器
getTransaction()→ 开启事务。 - 方法执行成功:提交事务。
- 方法抛异常:根据规则(运行时异常回滚,受检异常默认不回滚)执行回滚。
- Spring AOP 拦截
👉 所以:Spring 并没有重新实现事务,而是通过 AOP + 事务管理器,把数据库事务接管了。
Spring 如何“管理事务”
事务管理包含两个重要方面:传播机制 和 统一管理。
(1)传播机制(Propagation)
通过 @Transactional(propagation = Propagation.XXX) 定义方法在已有事务中的行为:
REQUIRED(默认):如果有事务就加入,没有就新建。REQUIRES_NEW:挂起当前事务,开启新事务。NESTED:在当前事务中再开子事务(依赖 savepoint)。SUPPORTS:有事务就用,没有就不用。MANDATORY:必须在事务中,否则抛异常。NOT_SUPPORTED:挂起事务,以非事务运行。NEVER:禁止在事务中运行。
(2)统一管理
- 开发者只需声明
@Transactional,不用自己写conn.setAutoCommit(false)。 - Spring 通过
TransactionManager统一管理提交/回滚,保证一致性。
Spring 如何实现“隔离事务”
- Spring 的隔离性依赖数据库本身的 事务隔离级别。
Nacos 的实现架构
Nacos Server 本质是一个分布式系统,由 服务注册中心 + 配置中心 两大核心组成:
(1)服务注册与发现
- 数据存储:服务实例信息存储在内存 + 持久化存储(MySQL)。
- 注册过程:
- 服务启动 → 向 Nacos 注册(IP、端口、权重等)。
- Nacos 维护心跳,定期检测实例健康。
- 消费者订阅服务 → Nacos 推送变更(长轮询/推送)。
- 集群模式:多台 Nacos 节点互相同步,前面一般会有一个 Nginx/SLB 负载均衡。
(2)配置中心
- 配置存储:默认存 MySQL(支持集群共享)。
- 推送机制:客户端 SDK 与 Nacos Server 保持长轮询或长连接,一旦配置变更就推送。
2. 一致性保证机制
(1)协议层面
- Nacos 1.x:使用 Distro 协议(阿里自研,类似 AP,保证最终一致性)。
- 每个节点保存完整的数据,定期通过增量同步/心跳保证一致性。
- 更偏向 可用性(Availability),满足 CAP 里的 AP。
- Nacos 2.x:支持 Raft 协议(CP 模式)。
- 通过 Raft 选主 + 日志复制,保证强一致性。
- 适合对一致性要求高的配置中心场景。
👉 Nacos 同时支持 AP 模式(服务发现) 和 CP 模式(配置管理)。
- 服务注册发现一般选择 AP(可用性优先,短暂不一致能容忍)。
- 配置中心一般选择 CP(不能让不同实例拿到不同配置)。
3. 举例解释
- 服务发现(AP 模式)
- 某个节点宕机,其他节点仍能提供服务。
- 可能短时间看到的数据不一致(比如某实例已下线但还没同步到所有节点),最终会通过心跳同步一致。
- 配置管理(CP 模式)
- 发布新配置时,必须过半节点写入成功才能对外生效。
- 即便牺牲一点可用性,也要确保所有客户端看到的配置是一致的。
Spring IOC 的底层原理
Spring IOC 的核心是 BeanFactory / ApplicationContext 容器,负责创建、管理和装配对象。其底层实现步骤大致是:
- 配置元数据加载
- XML、注解、Java Config。
- Spring 先解析这些配置,得到 Bean 的定义信息(类名、作用域、依赖关系等)。
- 反射创建对象
- 容器启动时,根据 Bean 定义,用 反射 或 CGLIB 动态代理 创建对象实例。
- 依赖注入(DI, Dependency Injection)
- 根据配置或注解(
@Autowired、@Resource、@Inject)注入依赖。 - 可以是构造器注入、setter 注入、字段注入。
- 根据配置或注解(
- Bean 生命周期管理
- 支持初始化方法(
@PostConstruct)、销毁方法(@PreDestroy)。 - 可以加上 AOP 代理(事务、日志、权限等)。
- 支持初始化方法(
- 上下文管理
- Bean 都交由 Spring 容器统一管理,应用代码只需要通过
@Autowired获取,而不需要手动new。
- Bean 都交由 Spring 容器统一管理,应用代码只需要通过
👉 本质:Spring IOC 就是 一个大工厂模式 + 反射机制 + 配置驱动。
2. Spring IOC 的优点
- 解耦合
- 对象不再由代码里
new出来,而是由容器统一创建和注入。 - 依赖关系在配置/注解里声明,降低模块间耦合度。
- 对象不再由代码里
- 更易扩展 & 测试
- 需要替换某个实现(比如把 MySQLDao 换成 RedisDao),只需改配置,不用改业务代码。
- 测试时可以轻松注入 Mock 对象。
- 统一生命周期管理
- 容器负责实例化、初始化、销毁,开发者不用关心资源释放等细节。
- 结合 AOP 提供横切能力
- IOC 和 AOP 结合,可以在不改业务代码的情况下加事务、日志、安全等。
- 方便配置管理
- 支持 XML、注解、Java Config,灵活选择。
TCP/IP 的分层模型(四层结构)
相比 OSI 七层模型,TCP/IP 模型更简单实用:
| 层级 | 主要功能 | 常见协议 |
|---|---|---|
| 应用层 | 定义应用数据格式和交互方式 | HTTP、HTTPS、DNS、FTP、SMTP、Telnet |
| 传输层 | 端到端通信,提供可靠或不可靠的传输 | TCP(可靠)、UDP(不可靠,快) |
| 网络层 | 负责寻址和路由,把数据包从源主机送到目标主机 | IP(IPv4/IPv6)、ICMP、ARP |
| 网络接口层 | 与底层硬件打交道,封装成帧在链路上传输 | Ethernet、Wi-Fi、PPP |
死锁产生的条件
死锁需要同时满足四个条件:
- 互斥:锁不能共享;
- 占有且等待:事务持有一个锁还想申请另一个;
- 不可抢占:锁不能强行夺走;
- 循环等待:事务之间形成锁等待环。
如何避免转账死锁
- 固定加锁顺序
- 例如:永远按照账户 ID 从小到大排序,再加锁。
- 保证所有事务锁顺序一致,就不会出现循环等待。
- 缩小锁的范围
- 尽量减少事务持锁时间,避免长事务。
- 合理重试机制
- 捕获死锁异常,重新发起事务。
1. 为什么需要事务消息
在分布式场景下,比如 下单成功 → 发送扣库存消息,需要保证业务操作和消息发送的一致性。
- 如果本地事务成功,但消息没发出去 → 数据丢失。
- 如果消息发出去了,但本地事务失败 → 数据不一致。
RocketMQ 的事务消息就是为了解决 本地事务与消息发送的最终一致性问题。
2. RocketMQ 事务消息的原理流程
RocketMQ 提供了一种 两阶段提交 + 回查 的事务消息机制:
阶段一:发送半消息(Half Message)
- Producer 先发送一条 半消息 到 Broker。
- 半消息对 Consumer 不可见,Broker 会先持久化下来,但状态是“待确认”。
阶段二:执行本地事务
- Producer 执行本地事务(比如下单、扣库存)。
- 执行完成后,Producer 会向 Broker 提交事务状态:
- Commit:本地事务成功 → Broker 把消息标记为可投递,Consumer 可以消费。
- Rollback:本地事务失败 → Broker 删除消息,丢弃不投递。
- Unknown:Producer 不确定 → Broker 需要回查。
3. 事务回查机制
为什么需要回查?
- 可能 Producer 在本地事务执行后宕机,来不及发送 Commit/Rollback;
- 或者网络异常导致状态消息丢失。
回查流程
- Broker 定期扫描状态为 Half 且超时未确认的消息。
- Broker 向 Producer 发起 事务回查请求。
- Producer 收到后,执行 事务回查接口,检查本地事务执行结果:
- 如果确认事务成功 → 返回 Commit。
- 如果确认事务失败 → 返回 Rollback。
- 如果仍然不确定 → Broker 会稍后继续回查(直到超过回查次数丢弃)。
👉 这样保证消息和本地事务最终一致,要么都成功,要么都失败。
4. 场景与注意事项
- 典型应用:金融转账、订单-库存扣减、支付通知等。
- 注意点:
- 本地事务逻辑要能通过回查接口 幂等地查询状态;
- 回查接口必须能确定事务结果(不要永远 Unknown);
- 事务消息是 最终一致性,不是强一致性,允许短暂延迟。
什么是大 key
- 大 key:某个 key 对应的 value 过大(比如一个 Hash 里有几百万 field,或者一个 String 占几十 MB)。
- 危害:
- 单次操作耗时长,阻塞 Redis 主线程。
- 大 key 删除/迁移时可能造成瞬时延迟抖动。
- 内存分布不均。
解决大 key 的方法
- 拆分大 key
- 大的 Hash / List 拆成多个小的(例如
user:1:friends:0,user:1:friends:1分片)。 - 或者用多 key 代替一个超大 key。
- 大的 Hash / List 拆成多个小的(例如
- 避免一次性操作整个大 key
- 用
SCAN代替KEYS/HGETALL,避免全量阻塞。 - 批量删除大 key 时分批删除,或用
UNLINK(异步删除)。
- 用
- 监控大 key
- 开启 Redis
--bigkeys或者用redis-cli --hotkeys定期检查。
- 开启 Redis
2. 什么是数据倾斜
- 在分布式缓存 / 分布式计算(比如 Redis Cluster、Kafka、Hadoop、Spark)中,某些 key 或任务分配到个别节点,导致负载不均。
- 表现:
- 部分节点 CPU/内存/网络占用过高;
- 部分分片存储量特别大,成为系统瓶颈。
解决数据倾斜的方法
(1)在 Redis Cluster 场景下
- 优化 key 的 hash tag
- Redis Cluster 根据 slot 映射分布 key,合理设计 key 的 hash tag(
{}中的部分)让数据均匀落到不同 slot。
- Redis Cluster 根据 slot 映射分布 key,合理设计 key 的 hash tag(
- 热点 key 分片
- 如果某个热点 key 请求量过大,可以人为拆分为多个 key,例如:
- 原 key:
item:123 - 拆分为:
item:123:0,item:123:1… - 访问时通过随机或一致性 hash 选择一个。
- 原 key:
- 如果某个热点 key 请求量过大,可以人为拆分为多个 key,例如:
- 加本地缓存 / 多级缓存
- 对热点 key 结果做本地缓存,降低对 Redis 的压力。
(2)在大数据计算(MapReduce/Spark)场景下
- 加随机前缀或后缀打散
- 把某个大 key 拆成多个小 key,再在 Reduce 阶段合并。
- 倾斜 key 单独处理
- 对大 key 的任务单独调度,避免和其他任务抢资源。
- 动态负载均衡
- 调整分片规则,让数据尽量均匀。
这是个挺典型的“不动原有代码,但能加新业务逻辑”的场景。设计模式里有几个特别契合的思路:
1. 装饰器模式(Decorator Pattern)
- 核心思想:在不修改原有类的情况下,动态地给对象添加新的职责。
- 应用场景:
- 你有一个服务
UserService,后来想加上日志记录、权限校验、性能统计等逻辑。 - 不需要改
UserService本身,而是写LoggingUserServiceDecorator、AuthUserServiceDecorator来包裹它。
- 你有一个服务
- 优点:随时叠加、灵活组合。
2. 代理模式(Proxy Pattern)
- 核心思想:通过代理类控制对目标对象的访问。
- 应用场景:
- 你想在调用某个微服务接口时,加一层缓存、限流、熔断,不用改原服务逻辑,直接在代理里做。
- 区别于装饰器:代理更偏向控制访问,而装饰器偏向功能增强。
3. 责任链模式(Chain of Responsibility Pattern)
- 核心思想:将一系列处理逻辑串起来,逐个执行,可以灵活插拔。
- 应用场景:
- 业务逻辑经常有“先校验 → 再鉴权 → 再处理 → 再通知”的链式过程。
- 新增需求(比如多一步数据清洗),只需要加一个 handler 到链条上,不用动原有代码。
4. 策略模式(Strategy Pattern)
- 核心思想:将可变的业务逻辑抽象成策略接口,运行时动态替换。
- 应用场景:
- 你有“订单优惠计算”,最开始只有满减,现在要加折扣、积分抵扣。
- 只需新增策略类实现接口,而不用修改原来的逻辑。
5. AOP(面向切面编程,框架支持)
- 如果你在用 Spring / Micronaut / NestJS 这种框架,可以直接用 AOP。
- 优势:横切逻辑(日志、监控、鉴权、事务)可以完全在切面里实现,不需要碰业务代码。
2. JDK 1.7 的实现 —— Segment 分段锁
- 整个
ConcurrentHashMap被分成若干个 Segment(类似小的Hashtable)。 - 每个
Segment有一把ReentrantLock。 - 线程操作时,只锁对应的 Segment,而不是整个 Map。
- 例如:put(k, v) 时,先定位到 Segment,再加锁。
- 这样可以并发地操作不同 Segment,提升性能。
缺点:Segment 数量固定,扩容和锁粒度相对大。
3. JDK 1.8 的实现 —— CAS + synchronized + Node 链表 / 红黑树
JDK 1.8 做了大改进,取消了 Segment,改为数组 + 链表/红黑树结构:
- 底层结构
- 类似
HashMap:数组 + 链表/红黑树。 - 当链表长度超过阈值时,转成红黑树,减少查询退化。
- 类似
- 写操作(put/remove)
- 定位桶(bin)时,用 CAS(compareAndSwap) 插入节点,如果 CAS 失败(说明有竞争),再用
synchronized锁定桶头节点。 - 因为锁的是“桶(bin)”而不是全表,粒度更细。
- 定位桶(bin)时,用 CAS(compareAndSwap) 插入节点,如果 CAS 失败(说明有竞争),再用
- 读操作(get)
get不加锁,直接通过数组索引、链表/红黑树查找。- 因为
Node的val和next都是volatile,保证了可见性。
- 扩容(resize)
- 使用 多线程协助扩容。线程在写入时如果发现需要扩容,会分配一部分桶给当前线程迁移,提高扩容效率。
Future是 Java 并发包(java.util.concurrent) 提供的一个接口。- 它表示一个异步计算的结果 —— 你提交一个任务,它会立刻返回一个
Future对象,你可以在之后获取执行结果。
2. 能做什么
主要功能有:
-
提交异步任务,立刻返回
Future。ExecutorService executor = Executors.newFixedThreadPool(2); Future<Integer> future = executor.submit(() -> { Thread.sleep(1000); return 42; });此时任务在后台执行,你拿到的只是
Future<Integer>。 -
获取结果(可能阻塞)。
Integer result = future.get(); // 阻塞直到任务完成如果任务没执行完,
get()会阻塞。 -
取消任务。
future.cancel(true); // 试图取消任务,true 表示允许中断正在执行的线程 -
检查任务状态。
future.isDone():是否完成(包括正常完成、异常、取消)。future.isCancelled():是否已取消。
3. 效果
- 异步编程:把耗时任务丢给线程池,主线程不用等,继续做其他事。
- 结果回收:需要结果时再调用
get(),避免阻塞太久。 - 可取消:任务如果不需要了,可以取消。
4. 缺点
get()是阻塞的,如果任务执行很久,调用线程会被卡住。- 不能主动回调通知,必须自己
get()或轮询。
5. 升级版
-
FutureTask:Future的一个实现类,可包装Callable或Runnable。 -
CompletableFuture(Java 8+):支持链式调用和回调,真正解决了Future的“只能阻塞取结果”的问题。CompletableFuture.supplyAsync(() -> "Hello") .thenApply(s -> s + " World") .thenAccept(System.out::println); // 输出: Hello World
✅ 一句话总结:
Future 用来 表示异步任务的结果,能异步执行、获取结果、取消任务,但结果获取是阻塞的;在实际开发中,更推荐用 CompletableFuture 来写非阻塞异步逻辑。
JVM 介绍
1. 什么是 JVM
- JVM(Java Virtual Machine,Java 虚拟机)是 Java 程序的运行环境。
- 它屏蔽了操作系统差异,让 Java 具备 一次编译,到处运行 的能力。
2. JVM 的主要组成
- 类加载子系统(ClassLoader Subsystem)
- 把
.class文件加载进内存,分为加载 → 链接(验证、准备、解析)→ 初始化。 - 双亲委派模型:先交给父加载器加载,保证核心类唯一性(比如
java.lang.String)。
- 把
- 运行时数据区(Runtime Data Area)
- 堆(Heap):存放对象实例,GC 的主要区域。
- 方法区(Metaspace,JDK8+):存放类元数据、常量池。
- 虚拟机栈:每个线程独有,存放局部变量表、操作数栈。
- 本地方法栈:支持 Native 方法。
- 程序计数器(PC 寄存器):指向当前执行的字节码行号。
- 执行引擎(Execution Engine)
- 解释执行:逐条解释字节码。
- JIT(Just-In-Time 编译器):热点代码直接编译成本地机器码,加速执行。
- 垃圾回收器(GC)
- 自动回收不再使用的对象,减少内存泄漏风险。
三、常见 JVM 调优手段
1. 内存参数调优
-Xms:初始堆大小-Xmx:最大堆大小(一般设置成和 Xms 一样,避免动态扩容)-Xmn:新生代大小(会影响 Minor GC 频率)-XX:MetaspaceSize:元空间大小-Xss:单线程栈大小(防止 StackOverflowError)
2. GC 算法选择
常见收集器:
- Serial GC:单线程,适合小内存(
-XX:+UseSerialGC)。 - Parallel GC:多线程,高吞吐量(默认,
-XX:+UseParallelGC)。 - CMS(Concurrent Mark Sweep):低停顿,JDK9 弃用(
-XX:+UseConcMarkSweepGC)。 - G1(Garbage First):JDK9+ 默认收集器,低延迟,分区化管理(
-XX:+UseG1GC)。 - ZGC / Shenandoah(JDK11+):超低延迟,适合大内存场景。
3. GC 调优参数
-XX:+PrintGCDetails:打印 GC 日志。-XX:+PrintGCDateStamps:带时间戳。-Xloggc:gc.log:输出到日志文件。-XX:+UseGCLogFileRotation:日志滚动。-XX:MaxGCPauseMillis=200:设定最大停顿时间目标。-XX:ParallelGCThreads=N:GC 并行线程数。
4. 典型调优思路
- 先监控
- 工具:
jstat,jmap,jstack,jconsole,VisualVM,GCViewer,Arthas。 - 重点关注:GC 次数、停顿时间、堆使用率、对象晋升速度。
- 工具:
- 发现问题
- Minor GC 过于频繁 → 新生代太小。
- Full GC 频繁 → 老年代太小 / 内存泄漏。
- Metaspace OOM → 类加载过多。
- 调整参数
- 扩大堆 / 新生代 / 老年代大小。
- 换用合适的 GC 算法。
- 调整 GC 线程数和暂停目标。
- 压测验证
- 调优是迭代过程,需要压测验证效果。
1. ZGC 的优势
- 停顿时间极低:< 10ms,基本和堆大小无关。
- 支持超大堆:可以管理 TB 级内存。
- 几乎所有阶段并发:减少了“Stop-The-World”事件。
- 实时性强:非常适合延迟敏感的业务(金融交易、实时竞价等)。
2. ZGC 的不足
- 相对较新:虽然在 JDK 15 之后转为正式,但在生产上大规模使用的案例相对 G1 少。
- 吞吐量可能略低于 G1:因为 ZGC 的“并发 GC”会占用更多 CPU 资源,对 CPU 吞吐量敏感的应用,可能反而不如 G1。
- 调优和诊断工具生态:G1 已经发展多年,生态和经验更丰富;ZGC 虽然逐渐完善,但资料和调优经验相对少。
- 小堆场景不一定划算:如果堆很小(比如 4GB ~ 8GB),用 G1 就足够,ZGC 带来的好处有限,还可能增加开销。
3. G1 的优势
- 成熟稳定:JDK 9+ 默认 GC,应用最广。
- 吞吐量更优:CPU 占用率比 ZGC 低,适合计算密集型应用。
- 经验丰富:调优参数和生产经验成熟,问题更容易定位。
- 适合中大内存(GB~百GB):大多数互联网业务场景够用了。
4. 场景对比
| 对比维度 | G1 | ZGC |
|---|---|---|
| 停顿时间 | 10ms ~ 200ms | <10ms |
| 吞吐量 | 高 | 略低(部分场景) |
| 支持内存 | GB ~ 百 GB | 百 GB ~ TB |
| 成熟度 | 高,经验丰富 | 新,案例相对少 |
| 适用场景 | 通用服务端应用 | 超大堆、实时性极强应用 |
Metaspace 在 Full GC 后空间未释放,在生产环境里很常见。它本质上是“类元数据空间”,和堆不同,释放逻辑有一些特殊性。下面我分几个角度帮你分析:
1. 类还在被引用
- Metaspace 里存的是 类的元信息(类名、方法、字段、常量池等)。
- 如果类的 ClassLoader 没有被回收,那么这个类也不会卸载,对应的 Metaspace 空间也不会释放。
- 常见原因:
- 使用了自定义
ClassLoader,但没有关闭或被 GC。 - Web 容器(Tomcat、Jetty)热部署时,新
ClassLoader创建,旧的没有释放 → Metaspace OOM 常见来源。
- 使用了自定义
2. 类卸载(Class Unloading)没有触发
- Full GC 并不保证一定触发类卸载。
- 类卸载需要
-XX:+ClassUnloading参数支持(JDK8 默认开启,但某些环境可能关掉)。 - 即使 Full GC 执行了,如果 JVM 判定没有足够的压力,也可能不做类卸载。
3. 元空间碎片化
- Metaspace 内存分配单位是 chunk,类卸载后会释放 chunk,但未必能立刻合并成大块空间。
- 结果就是 看起来没减少,但实际上有零散空闲空间,只是无法复用大对象。
- 类似堆里的 内存碎片问题。
4. Metaspace 大小设置过大
- 如果
-XX:MaxMetaspaceSize设置过大,JVM 可能不急于回收和归还内存给 OS。 - 即使类卸载,JVM 可能把空间保留在 Metaspace 内部池子里,而不是释放给操作系统 → 表现为 RSS 没下降。
下面把“分布式 MySQL 怎么同步”和“分布式 Redis 怎么同步”分开讲,再给你对比和落地建议。
MySQL 的数据同步
经典复制链路(异步为主):
- 主库写入事务 → binlog 落盘(STATEMENT/ROW/MIXED,生产一般用 ROW)。
- 从库 I/O 线程 连接主库,按位点或 GTID 拉取 binlog → 写到 relay log。
- 从库 SQL 线程/并行复制器 重放 relay log,应用到 InnoDB。
同步模式:
- 异步复制(默认):主库提交就返回;从库滞后有丢失风险。
- 半同步复制:主库提交后需 ≥1 从库“收到(写入 relay log)” 才返回(不是应用完成),降低丢失窗口。
- 组复制 / InnoDB Cluster(MGR):基于 Paxos/XCom + 写集认证 的多主/单主强一致方案;事务提交需多数派确认,冲突靠写集(writeset)检测。
并行复制与延迟:
- 基于库/表或 writeset 并行 应用,缓解重放瓶颈。
- 监控
Seconds_Behind_Master、Replica_parallel_applier_*等判断延迟。
DDL 同步:
- Online DDL 或迁移工具(gh-ost/pt-osc)减少复制卡顿。
容灾拓扑:
- 级联复制、跨机房复制;配合 GTID 做无痛切换与断点续传。
Redis 的数据同步
主从复制(Redis OSS):
- 初始全量复制:从节点
PSYNC→ 主节点生成 RDB 快照 发送;同时主节点把新写入命令放进 复制积压/输出缓冲。RDB 传完,再把积压里的命令增量发给从节点,使其追平。 - 部分重同步:断线后,靠 runid + offset 和 repl backlog 做差量续传,不必每次全量。
- 命令传播:之后主库将写命令实时异步发送给从库重放。
关键细节:
- 异步本质:默认主库不等从库应用完成就返回。可用
WAIT <replicas> <ms>等待若干副本收到(仍不等于持久化),或设置min-replicas-to-write/min-replicas-max-lag防止孤岛写入。 - 持久化与复制关系:AOF(
everysec/always)与 RDB 是磁盘持久化;复制只保证内存级副本一致,磁盘落盘策略独立。 - 无盘复制:diskless replication 直接通过 socket 推流 RDB,减少磁盘 IO。
- 高可用切换:
- Sentinel:监控/自动主从切换(最终一致)。
- Redis Cluster:16384 槽分片 + 每分片主从复制 + Gossip + 选主,写在对应分片主节点,仍是异步复制。
想要 Redis“强一致”的方案:用 RedisRaft 模块(Raft 共识)或商用 Redis Enterprise Active-Active(CRDT)。开源标准 Cluster + Sentinel 本质是“高可用 + 最终一致”,不是强一致。
1. STATEMENT / ROW / MIXED
这是 binlog(binary log,二进制日志)记录格式的三种模式:
-
STATEMENT(基于语句的复制,SBR)
-
binlog 里记录的就是执行的 SQL 语句,例如:
UPDATE users SET balance = balance - 100 WHERE id = 1; -
优点:日志量小(只记录语句),节省网络和磁盘。
-
缺点:有些语句在不同副本上可能执行结果不一致(如
NOW()、UUID()、LIMIT等依赖环境的语句)。
-
-
ROW(基于行的复制,RBR)
-
binlog 里记录的是 具体哪一行被改成了什么值,而不是语句。例如:
users.id=1, balance=1000 → balance=900 -
优点:完全无二义性,结果确定,强一致。
-
缺点:日志量大(尤其批量更新时,每行都会被记录)。
-
-
MIXED(混合复制)
- MySQL 自动选择:大多数情况用 STATEMENT,遇到可能有歧义的语句就切换成 ROW。
- 优点:兼顾性能与安全性。
- 缺点:行为不如纯 ROW 那么直观,排查问题时复杂。
👉 生产环境大多推荐 ROW,因为保证了数据一致性(尤其跨机房、异步复制时)。
按位点或 GTID 拉取 binlog → 写到 relay log
这是描述 MySQL 从库如何复制主库的日志:
-
位点(File + Position)
-
binlog 是分文件存储的,比如:
mysql-bin.000001 mysql-bin.000002 -
每个文件里有事件(event),每个事件有 position(偏移量)。
-
从库的 I/O 线程会告诉主库:“我上次同步到
mysql-bin.000001文件的 pos=12345 了,请从这里往后发给我。”
-
-
GTID(Global Transaction ID,全局事务 ID)
- 是 MySQL 5.6+ 提供的更先进的复制标识方式。
- 每个事务都有一个全局唯一的 ID(
server_uuid:transaction_id)。 - 从库同步时直接说:“我已经执行过哪些 GTID,你把我缺的发过来。”
- 不用再手动指定文件名+位置,主从切换、故障恢复更方便。
-
Relay log
- 从库把拉下来的 binlog 临时写在本地,叫 relay log。
- 然后从库的 SQL 线程会读取 relay log,重放里面的事件,把数据应用到自己的数据表。
1. AQS 是什么?
AQS 是 Java 并发包(java.util.concurrent)里锁和同步器的底层框架。像 ReentrantLock、Semaphore、CountDownLatch 这些常用工具,底层都是基于 AQS 来实现的。 它的核心思想是:用一个队列来管理获取资源的线程,用一个状态变量表示资源是否可用。
2. 通俗类比
你可以把 AQS 想象成一个银行柜台排队系统:
- state(状态值):就像柜台是否空闲。如果是 0,说明没人占用,可以办理业务;如果是 1,说明已经有人在柜台了,别人只能排队。
- 队列(CLH 队列):就像银行门口的一条队伍,先进先出。所有没抢到柜台的顾客(线程),都乖乖排在这条队伍里等待。
- 排队机制:当有顾客离开柜台(线程释放锁),队伍里的第一个人(队头线程)会被唤醒,然后走到柜台继续办理业务。
3. 工作流程(以 ReentrantLock 为例)
- 线程尝试加锁
- 如果柜台没人(state = 0),线程直接上去办理业务(获取锁成功)。
- 如果柜台有人(state = 1),线程加入队伍,进入等待状态。
- 线程释放锁
- 线程办完事,把 state 改回 0。
- 同时唤醒队伍里的下一个人(唤醒队列头结点)。
- 可重入(Reentrant)
- 如果你正在柜台办业务,又想继续办另一笔业务,你不需要重新排队(同一个线程可以多次获得锁,state++)。
1. 存储结构上的差异
- MySQL / RDS
- 存储:行存储(Row-Oriented),数据以行的形式紧密存放。
- 优势:适合 OLTP(联机事务处理),比如高并发的单行插入、更新、点查。
- 劣势:当要做大规模分析时,需要把大量行读出来,效率低。
- Hive (基于 HDFS + 列存格式 ORC/Parquet)
- 存储:数据存放在 HDFS 分布式文件系统,天然具备高吞吐和扩展性。
- 支持 列存储格式(ORC、Parquet),只读取分析所需的列,大大减少 I/O。
- 适合 OLAP(联机分析处理),例如统计、聚合、批处理。
2. 计算方式上的差异
- MySQL
- 实时执行:SQL 会由优化器生成执行计划,直接在单机引擎上运行。
- 受限于单机资源:CPU、内存、磁盘成为瓶颈,无法轻松扩展到 TB、PB 级数据。
- Hive
- 延迟执行:Hive SQL 不直接执行,而是翻译成 MapReduce / Tez / Spark 作业,在集群上分布式并行运行。
- 分布式计算:将数据分片存储,任务分发到多台机器并行处理,计算能力随集群节点数线性扩展。
- 适合批量分析:比如日志分析、全量统计,哪怕是几十亿行也能分布式跑完。
3. 应用场景上的差异
- MySQL
- OLTP 系统:电商订单、银行转账、用户表单操作。
- 数据量:一般百万 ~ 亿级数据,单表大了要分库分表。
- 查询模式:点查、多条件过滤、小范围聚合。
- Hive
- OLAP 系统:日志分析、用户画像、广告点击率统计。
- 数据量:天然支持 TB、PB 级。
- 查询模式:批量扫描、大范围聚合、关联计算。
- 优化:分区表、桶表、列存压缩,使得查询能在大数据规模下依旧高效。
4. 总结一句话
Hive 适合大数据的根本原因在于:
- 存储在 HDFS,具备分布式存储能力;
- 执行引擎基于分布式计算框架(MR/Tez/Spark),能并行处理海量数据;
- 数据格式支持列存储和压缩,减少 I/O 和存储开销。
而 MySQL 本质上是 单机事务型数据库,面向实时、小规模数据的增删改查,不适合大数据的全量分析。
你提到的 DeepSeek 这类推理模型(比如 DeepSeek-R1,或者其他“推理模型”/“Reasoning Models”),和传统的大模型(LLM)在架构上相似,但在 训练目标和推理方式 上有一些特别的设计。我给你分层解释一下:
1. 基础:和普通大模型类似
- 底层还是 Transformer 架构(Decoder-only,如 GPT 系列)。
- 输入是自然语言(prompt),输出是生成的 token 序列。
- 训练方式同样基于大规模语料,先做 预训练(语言建模),再做 微调(对齐任务)。
2. 关键不同点:引入推理过程
传统大模型往往是 直接输出答案,但推理模型强调:
- Chain-of-Thought (CoT)
- 模型被鼓励在回答前,先生成 中间推理步骤(像人写草稿)。
- 比如:解数学题时,先写思路,再写答案。
- 过程监督(Process Supervision)
- 不仅奖励最终答案对错,还奖励中间推理过程的合理性。
- 例如:如果一步中用了正确的逻辑,就给正反馈,即便最后答案错了。
- 这和传统的 “Outcome Supervision”(只看最终答案对错)不同。
- 训练方式升级
- 使用 RLHF(人类反馈强化学习) 或 RLAIF(AI反馈强化学习),但奖励信号不仅基于“答案对不对”,还包括“推理过程是否合理”。
- 有些模型会用 Verifiers(判别器)来检查推理链条是否合理。
3. 技术实现
- 数据:
- 收集包含 推理链条 的数据集(数学、逻辑题、编程题、推理问答)。
- 部分数据由人类或强模型(如 GPT-4)生成,再经人工或模型筛选。
- 训练方式:
- SFT(监督微调):教模型学会“先推理,再回答”。
- 奖励模型(Reward Model):对推理链条打分。
- RLHF / PPO / DPO 等强化学习:让模型逐渐偏好“有条理的推理过程”。
- Self-Consistency:在测试时,让模型多次推理,投票选择一致的答案。
- 推理时(Inference):
- 模型会生成一个 隐式/显式的中间链条(可能在最终输出隐藏掉,只保留答案)。
- 或者像 DeepSeek 这种,会在输出中直接展示 reasoning steps,让用户看到它的逻辑。
4. 为什么这样更强?
- 增强可靠性:有了推理过程,答案更透明,便于检查逻辑是否合理。
- 提升数学/逻辑/编程能力:这些任务需要逐步推理,模型的性能大幅提升。
- 与人类思维贴近:模仿人类“写草稿”的方式,减少“一步到位”时的错误率。
5. 和普通 LLM 的差别总结
| 对比点 | 普通 LLM | 推理模型 (DeepSeek, OpenAI o1 等) |
|---|---|---|
| 架构 | Transformer | Transformer |
| 输出方式 | 直接答案 | 先推理(CoT)再给答案 |
| 监督方式 | 只看最终答案 | 过程 + 答案双监督 |
| 强化学习 | 奖励=答案对错 | 奖励=推理合理性 + 答案正确 |
| 优势 | 流畅生成,泛用性强 | 数学/逻辑/代码等需要推理的任务表现更强 |
1. 存储结构上的差异
- MySQL / RDS
- 存储:行存储(Row-Oriented),数据以行的形式紧密存放。
- 优势:适合 OLTP(联机事务处理),比如高并发的单行插入、更新、点查。
- 劣势:当要做大规模分析时,需要把大量行读出来,效率低。
- Hive (基于 HDFS + 列存格式 ORC/Parquet)
- 存储:数据存放在 HDFS 分布式文件系统,天然具备高吞吐和扩展性。
- 支持 列存储格式(ORC、Parquet),只读取分析所需的列,大大减少 I/O。
- 适合 OLAP(联机分析处理),例如统计、聚合、批处理。
2. 计算方式上的差异
- MySQL
- 实时执行:SQL 会由优化器生成执行计划,直接在单机引擎上运行。
- 受限于单机资源:CPU、内存、磁盘成为瓶颈,无法轻松扩展到 TB、PB 级数据。
- Hive
- 延迟执行:Hive SQL 不直接执行,而是翻译成 MapReduce / Tez / Spark 作业,在集群上分布式并行运行。
- 分布式计算:将数据分片存储,任务分发到多台机器并行处理,计算能力随集群节点数线性扩展。
- 适合批量分析:比如日志分析、全量统计,哪怕是几十亿行也能分布式跑完。
3. 应用场景上的差异
- MySQL
- OLTP 系统:电商订单、银行转账、用户表单操作。
- 数据量:一般百万 ~ 亿级数据,单表大了要分库分表。
- 查询模式:点查、多条件过滤、小范围聚合。
- Hive
- OLAP 系统:日志分析、用户画像、广告点击率统计。
- 数据量:天然支持 TB、PB 级。
- 查询模式:批量扫描、大范围聚合、关联计算。
- 优化:分区表、桶表、列存压缩,使得查询能在大数据规模下依旧高效。
4. 总结一句话
Hive 适合大数据的根本原因在于:
- 存储在 HDFS,具备分布式存储能力;
- 执行引擎基于分布式计算框架(MR/Tez/Spark),能并行处理海量数据;
- 数据格式支持列存储和压缩,减少 I/O 和存储开销。
而 MySQL 本质上是 单机事务型数据库,面向实时、小规模数据的增删改查,不适合大数据的全量分析。
你提到的 DeepSeek 这类推理模型(比如 DeepSeek-R1,或者其他“推理模型”/“Reasoning Models”),和传统的大模型(LLM)在架构上相似,但在 训练目标和推理方式 上有一些特别的设计。我给你分层解释一下:
1. 基础:和普通大模型类似
- 底层还是 Transformer 架构(Decoder-only,如 GPT 系列)。
- 输入是自然语言(prompt),输出是生成的 token 序列。
- 训练方式同样基于大规模语料,先做 预训练(语言建模),再做 微调(对齐任务)。
2. 关键不同点:引入推理过程
传统大模型往往是 直接输出答案,但推理模型强调:
- Chain-of-Thought (CoT)
- 模型被鼓励在回答前,先生成 中间推理步骤(像人写草稿)。
- 比如:解数学题时,先写思路,再写答案。
- 过程监督(Process Supervision)
- 不仅奖励最终答案对错,还奖励中间推理过程的合理性。
- 例如:如果一步中用了正确的逻辑,就给正反馈,即便最后答案错了。
- 这和传统的 “Outcome Supervision”(只看最终答案对错)不同。
- 训练方式升级
- 使用 RLHF(人类反馈强化学习) 或 RLAIF(AI反馈强化学习),但奖励信号不仅基于“答案对不对”,还包括“推理过程是否合理”。
- 有些模型会用 Verifiers(判别器)来检查推理链条是否合理。
3. 技术实现
- 数据:
- 收集包含 推理链条 的数据集(数学、逻辑题、编程题、推理问答)。
- 部分数据由人类或强模型(如 GPT-4)生成,再经人工或模型筛选。
- 训练方式:
- SFT(监督微调):教模型学会“先推理,再回答”。
- 奖励模型(Reward Model):对推理链条打分。
- RLHF / PPO / DPO 等强化学习:让模型逐渐偏好“有条理的推理过程”。
- Self-Consistency:在测试时,让模型多次推理,投票选择一致的答案。
- 推理时(Inference):
- 模型会生成一个 隐式/显式的中间链条(可能在最终输出隐藏掉,只保留答案)。
- 或者像 DeepSeek 这种,会在输出中直接展示 reasoning steps,让用户看到它的逻辑。
4. 为什么这样更强?
- 增强可靠性:有了推理过程,答案更透明,便于检查逻辑是否合理。
- 提升数学/逻辑/编程能力:这些任务需要逐步推理,模型的性能大幅提升。
- 与人类思维贴近:模仿人类“写草稿”的方式,减少“一步到位”时的错误率。
5. 和普通 LLM 的差别总结
| 对比点 | 普通 LLM | 推理模型 (DeepSeek, OpenAI o1 等) |
|---|---|---|
| 架构 | Transformer | Transformer |
| 输出方式 | 直接答案 | 先推理(CoT)再给答案 |
| 监督方式 | 只看最终答案 | 过程 + 答案双监督 |
| 强化学习 | 奖励=答案对错 | 奖励=推理合理性 + 答案正确 |
| 优势 | 流畅生成,泛用性强 | 数学/逻辑/代码等需要推理的任务表现更强 |
虚拟内存(Virtual Memory)和地址空间(Address Space) 的问题,我们来分步骤梳理一下:
1. 地址空间大小由位数决定
-
在 32 位系统 下,CPU 的虚拟地址宽度是 32 位。
-
可寻址空间理论上就是:
232=4 GB2^{32} = 4\,GB232=4GB
-
所以 单个进程的虚拟地址空间最大就是 4GB。
2. 操作系统的划分
- 一般的 32 位操作系统会把这 4GB 地址空间 划分成:
- 用户空间(User space):通常是 3GB(0x00000000 ~ 0xBFFFFFFF)
- 内核空间(Kernel space):通常是 1GB(0xC0000000 ~ 0xFFFFFFFF)
- 也有的系统可以设置成 2GB/2GB 或 4GB/4GB(需特殊配置,如 Windows 的 /3GB 开关、Linux 的内核参数)。
3. 物理内存 vs 虚拟内存
- 物理内存:你机器上真实安装的内存(这里是 2GB)。
- 虚拟内存:由操作系统管理,通过页表把虚拟地址映射到物理内存 + 磁盘交换空间(swap/pagefile)。
- 虚拟内存大小不受物理内存直接限制,可以比物理内存大,只要操作系统和磁盘空间支持。
4. 实际能开的虚拟内存多大?
- 单进程上限: 在 32 位系统上,单进程最多 4GB 虚拟内存,但通常用户进程只有 2–3GB 可用。
- 系统级总虚拟内存:
= 物理内存 + 磁盘上的交换区大小。
- 例如:2GB 物理内存 + 4GB swap → 系统虚拟内存大约 6GB。
- 但单个进程依然受 32 位寻址限制,最多 2–3GB。
✅ 总结:
- 在 32 位系统中,虚拟地址空间上限是 4GB/进程(通常 2–3GB 用户可用)。
- 系统级虚拟内存大小 = 物理内存 + 磁盘交换区。
- 你有 2GB 物理内存,理论上你可以设置 swap 让虚拟内存开到 6GB、8GB 甚至更大,但单个进程依然只能用到 2–3GB 左右。
Kafka 出现 消息堆积、乱序、丢失,一般是由架构、配置、消费逻辑等多方面因素引起的。下面我分三部分说明原因和对应解决方法:
1. 消息堆积
可能原因:
- 生产快于消费:Producer 写入速率过高,Consumer 消费能力不足。
- Consumer Group 不均衡:分区数 < 消费者数,导致部分消费者空闲,其他负载过重。
- 消费逻辑慢:Consumer 内部做了复杂逻辑(写数据库、远程调用)影响速度。
- Kafka 磁盘/网络瓶颈:Broker IO 或网络带宽不足。
解决方法:
- 增加 Consumer 数量,保证 Partition 数 ≥ Consumer 数。
- 使用 批量消费 + 多线程处理,减少单条处理开销。
- 对耗时操作(DB 写入、调用外部接口)用 异步/队列/缓存 进行削峰。
- Kafka 层面:增加分区数、提升 Broker 磁盘性能、优化 batch.size/linger.ms 提高吞吐。
2. 消息乱序
可能原因:
- Kafka 只能保证 单分区有序,多分区天然乱序。
- Producer 使用了 轮询分区策略,同一 key 的消息被分配到不同分区。
- Consumer 多线程处理时没有保证同一 key 的消息顺序。
解决方法:
- 对需要顺序的消息,指定相同的 key,保证落在同一分区。
- 必要时限制某些 Topic 仅单分区,确保全局有序(但牺牲吞吐)。
- Consumer 端:
- 同一分区内只用一个线程处理,保持消费顺序。
- 如果要多线程处理,按 key 进行 分片路由(Hash 分配到固定线程)。
3. 消息丢失
可能原因:
- 生产端未开启 acks=all,仅 acks=0/1 时 broker 异常可能丢失。
- 未开启幂等性/事务,导致网络抖动时丢消息或重复消息。
- Broker 未设置 min.insync.replicas,主挂掉副本未同步时丢失。
- Consumer 自动提交 offset(enable.auto.commit=true),业务逻辑未完成就提交 offset。
- Consumer 手动提交 offset 出错,提交过早或遗漏。
解决方法:
- Producer:
- 设置 acks=all,确保消息写入多数副本。
- 开启 enable.idempotence=true 避免重复消息。
- 需要事务场景可用 事务 API 保证端到端一致性。
- Broker:
- 设置 min.insync.replicas ≥2,配合 acks=all 保证副本可靠。
- Consumer:
- 关闭自动提交,改为 手动提交 offset(在业务逻辑完成后提交)。
- 使用 幂等消费逻辑(如根据唯一 key 去重)。
✅ 总结:
- 堆积:消费能力不足 → 扩容/优化消费逻辑。
- 乱序:分区策略问题 → 按 key 固定分区 + 分区内单线程。
- 丢失:可靠性配置不当 → acks=all + 幂等/事务 + 正确 offset 提交。
Redis 更适合 轻量级、低延迟、对数据量要求不大的场景,而 Kafka 更偏向 高吞吐、持久化、分布式日志流平台。
下面分几种 Redis 实现方式说明:
1. 基于 List (RPUSH + LPOP / BRPOP)
原理:
- Producer 使用
RPUSH把消息写入队列。 - Consumer 使用
LPOP或阻塞式BRPOP取消息。
优点:
- 简单直接,支持阻塞消费。
- 实现快速。
缺点:
- 无确认机制:消息被
LPOP后就没了,Consumer 异常可能丢失消息。 - 无法保证多消费者的可靠分发(可能一个消费者取完,其他就没机会了)。
2. 基于 Pub/Sub
原理:
- Producer 使用
PUBLISH,所有订阅了 Channel 的消费者都会收到消息。
优点:
- 广播模式实现简单。
- 实时性好。
缺点:
- 消息不持久化,消费者不在线就会丢消息。
- 没有重试/确认机制。
3. 基于 Stream (推荐)
原理:
- Redis 5.0 引入的
XADD、XREAD、XREADGROUP等命令。 - 类似 Kafka,支持消息持久化、消费组、ack 确认。
优点:
- 支持 消费组(Consumer Group),多个消费者可分摊处理。
- 持久化(RDB/AOF)保证消息不会轻易丢失。
- ack 机制:消费者读到消息后必须
XACK确认,未确认的消息可以重试。 - 可以查看消息历史,类似日志流。
缺点:
- 仍然是单机为主,集群扩展性不如 Kafka。
- 吞吐能力有限,百万级以上消息/秒时可能遇到瓶颈。
1. AI大模型表现不佳时可以调的参数
在调用 LLM(比如 GPT、LLaMA、Claude 等)时,常见的可调参数包括:
- temperature
- 控制随机性,范围一般 0~2。
- 值小(如 0~0.3):更稳定、更确定,更适合问答、代码。
- 值大(如 0.7~1.2):更有创造性,更适合写作、头脑风暴。
- top_p (nucleus sampling)
- 控制从概率分布中采样的“覆盖率”。
- 小(如 0.8):只考虑概率较高的词,减少乱答。
- 大(接近 1.0):更发散,创造力强。
- 通常 调 temperature 或 top_p 其中一个,不要同时调。
- max_tokens
- 控制输出字数(或字 token 数)。
- 太小 → 截断;太大 → 成本高,可能跑偏。
- frequency_penalty / presence_penalty(OpenAI/Anthropic类模型特有)
- frequency_penalty:惩罚重复词,值大时会让输出更少重复。
- presence_penalty:鼓励引入新主题,避免一直黏着某个话题。
- stop sequences
- 设置停止符号(比如
"\n\n"),避免模型跑飞。
- 设置停止符号(比如
2. 提示词(Prompt)怎么调
提示词调优(prompt engineering)常见思路:
- 角色设定: “你是一个专业的数据库管理员/算法老师/面试官……” → 帮助模型进入上下文。
- 明确任务目标: “请给我一份分步骤的解决方案,而不是最终答案。” → 避免模型走偏。
- 提供上下文/示例(few-shot) 给出输入 + 正确输出示例,模型会模仿风格和结构。
- 约束输出格式: “请用 Markdown 表格输出结果。” → 限制输出形态。
- 逐步推理: 用 “让我们一步步思考” 或 “先分析,再总结” 来引导。
- 分解任务: 把一个复杂请求拆成小步骤,分多次对话完成。
3. 上下文记忆:模型的记忆 vs. 传过去的上下文
- 传过去的上下文(Context Window)
- 模型是“无状态”的,没真正长期记忆。
- 每次对话时,把之前的对话历史拼接到 prompt 里,一起送进模型。
- 模型只是在这一次推理里“看到”上下文。
- 受限于 上下文窗口大小(比如 8K、32K、100K tokens)。
- 模型的记忆(长期记忆)
- 需要外部系统支持,比如数据库/记忆模块。
- 常见做法:
- 用向量数据库(FAISS、Milvus)保存历史对话,按相似度检索,拼接进 prompt。
- 或者应用层维护 profile(用户资料、长期偏好),在调用时注入。
- 模型本身不“记得”你,但应用可以模拟出“有记忆”。
2. 语言特性差异
🟢 JDK8
- Lambda 表达式 + Stream API
- 接口默认方法
- 新的日期时间 API (
java.time) - Optional
- CompletableFuture
👉 让 Java 具备了函数式编程和更现代的异步能力。
🟡 JDK17(包含 9~16 的变化)
- 模块化系统 (Jigsaw, JDK9)
- var 关键字 (JDK10)
- 局部变量类型推断
- switch 表达式 (JDK12, JDK14)
- 文本块
"""(JDK13) - Records (JDK16):简化数据类
- sealed classes (JDK17):受限继承
- instanceof 模式匹配 (JDK16/17)
👉 更简洁的语法、模块化,更好的封装与类型安全。
🔵 JDK21(包含 18~21 的变化)
- 虚拟线程(Project Loom, JDK21 正式发布)
- 极大提升并发性能,轻量级线程,百万级并发不再依赖复杂的线程池。
- 模式匹配增强(switch 模式匹配)
- Record patterns(解构匹配)
- 字符串模板(类似 JavaScript 模板字符串)
- 序列集合 API(SequencedCollection)
- Structured Concurrency(结构化并发,预览)
👉 语法更现代,性能更强,并发处理方式发生革命性变化。
3. 性能 & JVM 改进
- JDK8 → JDK17/21:
- G1 GC 更加成熟,ZGC 和 Shenandoah GC 新增(低停顿)。
- JIT/AOT 编译改进。
- Class Data Sharing (CDS) 改善启动速度。
- 内存占用优化。
- JDK17 → JDK21:
- 虚拟线程:大幅减少线程上下文切换成本。
- GC 进一步优化(ZGC 支持 TB 级堆,低延迟)。
- Vector API、Foreign Function & Memory API (替代 JNI,访问 C 库更高效)。
Redis Cluster 是把所有 key 映射到 16384 个槽位(hash slots)里,每个 key 会通过 CRC16 算法取模映射到一个 slot,然后 slot 再分布到不同节点。
{} 的作用
- 正常情况:整个 key(例如
"user:1001:profile")会被用来计算哈希槽。 - 带
{}时:只有{}内的内容会参与哈希槽计算。比如:- key
"user:{1001}:profile" - key
"order:{1001}:info"→ 两个 key 的 hash 只计算"1001"这一部分,所以它们一定落在同一个 slot 上。
- key
为什么需要这样?
- 在 Redis Cluster 里,只有同一个槽位的 key 才能做多 key 操作(如
MGET、MSET、EVAL),否则会报错。 - 利用
{},你可以把相关的数据(比如同一个用户的资料、订单)强行绑定到同一个槽位上。
小结
{}内的内容叫 hash tag- 它 决定 key 的哈希槽分布,而不是整个 key
- 用来让相关 key 落在同一个 slot,从而支持跨 key 操作
©著作权归作者所有