Redis 主要提供 RDB(快照持久化)AOF(日志持久化) 以防止数据丢失。

1️⃣ RDB(Redis Database Snapshot)

2️⃣ AOF(Append Only File)

跳表的操作


(1) 查找(O(log n))

假设要查找 20

  1. 从最高层开始,沿着索引层查找,直到找到最接近但不大于 20 的节点
  2. 降级到下一层,继续从该节点往后找。
  3. 重复步骤 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 时:

  1. 查找插入位置(与查找过程类似)。
  2. 随机决定 x 的层级(采用抛硬币策略)。
  3. 从底层到对应层级插入 x,并调整索引。

层数如何决定?

采用 随机概率,如果 p = 0.5,则:

这样保证跳表的结构接近平衡二叉树

(3) 删除(O(log n))

  1. 查找 x 的所有层级位置(与查找过程类似)。
  2. 从每层链表删除 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)

假设并发冲突是常态,因此在访问数据前,会强制加锁,防止其他事务修改数据,从而保证数据的安全性。

乐观锁(Optimistic Lock)

假设并发冲突很少,因此不加锁,而是通过版本号(Version) 或 CAS(Compare And Swap)机制来保证数据的正确性。

QUIC(Quick UDP Internet Connections) 是 Google 开发的一种基于 UDP 的传输协议,旨在提高网络传输效率,并解决 TCP 的一些性能问题,特别是在高延迟和丢包环境下

🔹 QUIC 是 “TCP + TLS + HTTP/2” 的结合体,但它不基于 TCP,而是基于 UDP,并在应用层实现了:

消息队列(Message Queue, MQ)详解


消息队列(MQ)是一种用于异步解耦流量削峰的架构模式,广泛应用于高并发系统、微服务架构、日志处理、事件驱动系统等


1. 为什么需要消息队列?


(1) 解耦(Decoupling)

在传统的同步调用中,服务 A 调用服务 B,两者强耦合

A ---> B

如果 B 发生故障,A 也会受影响。

使用消息队列后A 只需要发送消息到 MQ,B 异步消费:

A ---> [MQ] ---> B

AB 完全解耦,即使 B 挂掉,消息仍然保留在 MQ 中。


(2) 削峰(Traffic Shaping)

假设秒杀场景:

用户请求  --->  [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 **“不丢消息”? 💡 解决方案: ✅ 生产者保证

Broker(MQ)保证

消费者保证


(2) 消息重复消费

MQ 可能发生“消息重复消费”(如消费者超时未 ACK,被重新投递)。 💡 解决方案: ✅ 幂等性设计

生产者为每条消息生成全局唯一 messageId;消费者在处理前先去 Redis 占坑。 如果占坑成功=第一次见到 → 允许执行业务;占坑失败=重复 → 直接 ACK 丢弃。

Redis Key 设计


(3) 消息积压

如果消费者消费太慢,消息堆积在 MQ: 💡 解决方案: ✅ 增加消费者数量(提高消费速率)。 ✅ 消息分区(Kafka),让多个消费者并行消费。


6. MQ 在实际业务中的应用


(1) 秒杀系统

MQ 削峰限流,避免数据库崩溃:

用户请求  --->  [MQ]  --->  订单服务  --->  数据库

(2) 订单处理

MQ 解耦,订单创建后异步通知

下单  --->  [MQ]  --->  库存扣减
                --->  物流通知
                --->  发票系统

(3) 分布式事务

基于 RocketMQ 的事务消息

  1. 生产者发送事务消息(Pending)。
  2. 本地事务执行(扣库存)
  3. MQ 确认事务成功,消息才正式投递

7. MQ 面试常见问题


🔹 MQ 和数据库之间如何保证数据一致性?

方案 1本地事务 + MQ

方案 2MQ 先发,业务后执行


🔹 Kafka 为什么比 RabbitMQ 吞吐量高?

Kafka 采用 顺序写磁盘 + PageCache 读写比随机写快 10 倍)。 ✅ RabbitMQ 采用 传统 AMQP 协议,基于 Erlang,消息必须存入内存/磁盘,影响性能。


8. 总结

消息队列用于解耦、削峰、异步处理。 ✅ 常见 MQ:RabbitMQ(事务)、Kafka(高吞吐)、RocketMQ(事务+金融)。 ✅ 保证消息可靠性(幂等性、事务消息、ACK)。 ✅ 优化高并发场景(秒杀、订单、日志处理)。

💡 面试时重点关注 MQ 的应用、消息一致性、消息丢失、重复消费等问题! 🚀

InnoDB 的 B+ Tree 实现方式:

聚簇索引是一种特殊的索引,它决定了数据在表中的物理存储顺序。 换句话说:

这和普通索引(非聚簇索引)不同,普通索引的叶子节点存放的是“指向数据行的地址(指针/主键)”。

1. 所有数据都在叶子节点,非叶子节点只做索引

2. 叶子节点天然有序 + 双向链表

3. 更好的磁盘局部性(Cache Friendliness)

对比:

4. 查询效率稳定

B+树是一种多路搜索树(多叉平衡树),它的特点包括:

B+ 树在 插入或删除 时可能导致以下变化:

1. 插入

2. 删除

🔍 一、B 树 vs B+ 树 的核心区别

特性 B 树 ✅ B+ 树
数据存储位置 数据存储在所有节点(叶子 & 非叶子)中 数据只存在叶子节点
非叶子节点是否存数据 ✅ 是(key + value) ❌ 否(只存 key + 指针)
叶子节点是否链表连接 ❌ 否 ✅ 是(有序双向链表)
范围查询性能 ❌ 差(需中序遍历整棵树) ✅ 高效(直接顺序遍历叶子)
节点可存 key 数量 少(因为含有 value) 多(只有 key,能放更多 key)
树的高度 较高(I/O 多) 更矮(I/O 更少)
查找路径是否统一 ❌ 不统一(可能在中间节点命中) ✅ 统一(都走到叶子)

磁盘访问友好:每次 I/O 更值钱

数据库核心瓶颈是 磁盘 I/O(即页读写):

👉 每次磁盘访问能用得更“值”,性能高

查找路径更稳定统一

数据更容易批量读入内存

由于叶子节点是链表有序排列

HashMap 是 Java 中最常用的集合之一,其底层结构结合了 数组(Array)链表(LinkedList),从 Java 8 开始还引入了 红黑树(Red-Black Tree) 来优化性能。

计算 hash 值

int hash = hash(key);  // 通过扰动函数计算 hash

定位数组下标

index = (table.length - 1) & hash;  // 取模(效率高)

处理冲突

链表转红黑树的条件

✅ 1. 缓存穿透(Penetration)

📌 定义:

用户请求的数据 在缓存中查不到,数据库中也没有,请求不断打到数据库上。

方案 说明
缓存空值 把数据库查不到的 key 缓存为 null,并设置短过期时间
参数校验 比如 ID < 0 就直接拦截
布隆过滤器 使用布隆过滤器拦截不存在的 key(空间效率高)
限流器 限制频繁请求同一 key 的速率

布隆过滤器(Bloom Filter)是一种非常经典的概率型数据结构,常用于集合成员测试(判断一个元素是否在集合中)。


🌱 基本定义

换句话说: 👉 “不在”=100% 准确; 👉 “在”=有一定概率错误。


⚙️ 它是怎么工作的?

  1. 准备一个长度为 m 位的位数组(初始全是 0)。
  2. 定义 k 个哈希函数
  3. 当要插入元素时:
    • 用这 k 个哈希函数分别计算位置,把位数组相应位置标记为 1。
  4. 当要查询元素时:
    • 同样用这 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 是否已过期,如果过期了就立刻删除。

📌 特点:

✅ 2. 定期删除(Active Expire Cycle)

Redis 每隔一定时间(默认 100ms)随机抽取一批设置了过期时间的 key 进行检查和删除

📌 特点:

每个带过期时间的 key,会被存入一个“过期字典(expire dict)”中:

🔍 当 Redis 执行任何操作如 GET/SET,先检查 expire dict 决定是否已过期。

synchronizedJVM 层面的原生锁机制,简单但功能有限; LockJava 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)

✅ 场景:

✅ 原理:

✅ 优点:

❌ 缺点:


🟡 2. 轻量级锁(自旋锁)

✅ 场景:

✅ 原理:

✅ 优点:


🔴 3. 重量级锁(Monitor 锁)

✅ 场景:

✅ 原理:

❌ 缺点:

CAS 是一种无锁(lock-free)机制,用于多线程并发下的安全数据更新,核心逻辑是:

👉 “如果内存中的值等于预期值,那么就将其更新为新值;否则什么也不做。”

❓原文:

每个线程会在进入 synchronized 时尝试用 CAS 将对象头的锁信息改成指向自己的栈帧(Lock Record)

✅通俗解释:

👉 你想用会议室,就先试着把门上的牌子改成写你的名字(用 CAS)。 👉 这个“贴牌子”的过程是 原子操作:要么一瞬间贴上成功,要么完全失败。 👉 这个动作就是 CAS:如果门上的名字是空的,就让我贴;不是空的,我就失败。


❓原文:

如果 CAS 成功 → 获得锁

✅通俗解释:

👉 你成功把门上的牌子换成自己的名字,那就说明没人用,你进屋开会就行了(加锁成功)


❓原文:

如果 CAS 失败 → 说明有竞争,进入自旋阶段(尝试一段时间后升级为重量级锁)

✅通俗解释:

👉 如果你发现门上已经贴了别人的名字(锁被别人抢了),你不急着离开,而是先在门口等一等,看看他是不是很快就出来 👉 如果他很快出来了,你就马上贴上你的名字进去(自旋成功) 👉 如果等了几轮他还不出来,你就放弃等,去前台登记排号排队等通知(进入等待队列,升级为重量级锁

✅ 所以“需要等谁就让谁 join”

join() = “等你干完活我再干”

await() 会让线程 挂起等待,直到某个条件满足时,由其他线程通过“通知”唤醒它

类别 作用 唤醒方式
CountDownLatch.await() 等待计数器归零 调用 countDown(),直到计数为 0 → 唤醒所有 await 的线程
Condition.await() 等待某个条件 其他线程调用 condition.signal()signalAll()
CyclicBarrier.await() 所有线程都到达屏障 到达设定线程数后 → 自动唤醒全部
Future.get()(内部也使用 await() 等待任务完成 任务执行完,设置结果 → 唤醒调用者

String(字符串类型)

SET name "Alice"
GET name                 # "Alice"

INCR count              # 自增操作(数值)
APPEND name " Smith"    # 字符串追加

key        →       value
"name"     →       "Alice"
"count"    →       "100"
"token:uid:1" →    "abc123xyz"

List(列表)

LPUSH queue task1
RPUSH queue task2
LPOP queue       # 出队
RPOP queue       # 出栈

Set(集合)

SADD tags "java"
SADD tags "redis"
SISMEMBER tags "java"     # 判断是否存在
SMEMBERS tags             # 获取所有成员

Hash(哈希)

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(有序集合)

ZADD ranking 100 Tom
ZADD ranking 80 Alice
ZRANGE ranking 0 -1       # 按分数升序返回所有人
ZREVRANGE ranking 0 2

数据结构组成

Redis 设计得很巧妙: 👉 插入一个元素时,会同时写进 哈希表(方便查找)和 跳表(方便排序和范围操作)。 👉 删除时,也会在这两个结构里都删掉。

1. HashMap 扩容时链表转红黑树的阈值为什么是 8?退化为 6 的原因?

🔹3. G1 垃圾回收器如何预测停顿时间?

4. Region 大小如何设置?

🧠 一、什么是“可见性”?

在并发编程中,“可见性”指的是:

一个线程对共享变量的修改,能否被其他线程立即看见。

如果没有保证可见性,就可能出现:


❓二、volatile 能做什么?

java


CopyEdit
volatile int a;
情况 是否保证元素可见性 是否保证原子性 推荐
volatile int[] arr ❌ 否 ❌ 否 🚫 不推荐
AtomicIntegerArray ✅ 是 ✅ 是 ✅ 推荐
synchronized 控制访问 ✅ 是 ✅ 是 ✅ 推荐(更通用)

🔹6. ThreadLocal 内存泄漏的根本原因?JDK 改进方案?

6. ThreadLocal 内存泄漏的根本原因?JDK 改进方案?

⚠️ 四、ABA 问题

什么是 ABA?

一个线程看到 top 还是 A,以为没变,但实际上它已经变成 B 又变回 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 配合才持久化

✅ 背景问题:缓存与数据库如何保持一致?

我们常见的一致性策略是:

写库的时候,把缓存删了

比如你要更新一个商品价格:

  1. 更新数据库价格
  2. 删除 Redis 缓存中的这条数据(如 item:12345

这是典型的:更新数据库 + 删除缓存


❗但是,这种方式会在高并发下出问题!

❓为什么会出问题?——读写并发冲突

假设发生如下时序:

cssCopyEdit1. 请求 A:更新商品价格(update DB)
2. 请求 B:读取商品信息(从 Redis)
3. 请求 A:删除缓存

❗风险点:


✅ 怎么解决?——延迟双删机制!


📌 核心思路:

删两次缓存,中间隔一段时间(比如 1 秒)再删一次。


✅ 延迟双删流程如下:

markdownCopyEdit1. 更新数据库(保证源数据正确)
2. 删除缓存(第一次)
3. 等待一段时间(如 1 秒)
4. 再删一次缓存(第二次,防止并发脏读)

这样,即使中间有并发读操作把旧值写回缓存,第二次删除也会清掉它

属性 全称 含义(通俗解释)
C Consistency(一致性) 所有节点看到的数据是一致的(像单机一样)
A Availability(可用性) 每个请求都能在有限时间内获得响应(无论成功/失败)
P Partition tolerance(分区容忍性) 系统能容忍网络分区(节点/链路间网络断开)

为什么三者不能同时满足?

因为 一旦发生网络分区(P),你必须在 C 和 A 之间做出权衡

情况一:选择 CP(放弃 A)

情况二:选择 AP(放弃 C)

情况三:选择 CA(放弃 P)

隔离级别 中文名 能否读未提交 能否防止脏读 能否防止不可重复读 能否防止幻读
READ UNCOMMITTED 读未提交 ✅ 能 ❌ 否 ❌ 否 ❌ 否
READ COMMITTED 读已提交 ❌ 否 ✅ 是 ❌ 否 ❌ 否
REPEATABLE READ 可重复读(MySQL默认) ❌ 否 ✅ 是 ✅ 是 ❌(部分解决)
SERIALIZABLE 串行化 ❌ 否 ✅ 是 ✅ 是 ✅ 是

事务 = 数据库执行的原子性工作单元

隔离级别 决定事务内部读到的数据一致性 vs 最新性

“不可重复读”不是绝对坏事,取决于业务需求

问题类型 说明
脏读 读到了其他事务尚未提交的数据
不可重复读 两次读取同一数据,结果不同(因为其他事务修改了它)
幻读 两次相同条件查询,返回的记录数不同(因为其他事务新增或删除了满足条件的记录)

在 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() 的 高位信息下沉,避免只用低位导致分布不均。

你能做的“避免/减轻碰撞”的手段

扩容(resize)到底干了啥?(JDK 8 细节)

触发条件:size > threshold(阈值 = capacity * loadFactor

步骤:

  1. 新表容量 = 旧容量 * 2(到上限 1<<30)。
  2. 阈值同步翻倍(newThreshold = newCap * loadFactor)。
  3. 重新分布每个桶的节点,但不重算完整哈希
    • 关键位是 oldCap 这个二进制最高新增位。
    • 对桶内每个节点,看 (node.hash & oldCap)
      • 为 0留在原 index
      • 为 1去新位置 index + oldCap
    • 这样把一个桶 一分为二,且 保持相对顺序。树节点用 TreeNode.split 对应拆分。

复杂度:一次 resize 需要把每个桶走一遍,O(n);但均摊到多次插入,平均摊销仍近似 O(1)。

初始容量、装载因子、阈值

静态代码块的执行顺序规则

  1. 父类 → 子类(先执行父类的静态代码块,再执行子类的)
  2. 同一个类中:
    • 源码出现顺序执行: 静态变量显式赋值 和 static {} 是合并在一起、从上到下依次执行的
    • 不区分“变量赋值”和“静态块”优先级,谁在前谁先执行
  3. 执行完静态部分 → 再执行构造代码(构造代码块 & 构造方法)

JVM 加载一个类时,会按以下顺序执行:

  1. 加载(Loading) → 读 .class 文件进内存
  2. 验证 / 准备 / 解析(Linking) → 给静态变量分配内存并设默认值(不是赋初值)
  3. 初始化(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(略高)

什么是慢查询

方法 说明
建合适的索引 常用条件列、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)是一种创建型设计模式,它的目标是:

在整个程序运行期间,某个类只会被创建一个实例,并且提供一个全局访问点。

为什么要用单例模式

核心特点

  1. 构造方法私有化:外部不能直接 new
  2. 类内部持有一个静态实例
  3. 提供一个静态方法/属性获取实例
实现方式 懒加载 线程安全 实现难度 性能 优点 缺点
饿汉式 简单 实现简单,类加载即实例化,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.SECONDSMILLISECONDS
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 / unlockhappens-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
}
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(叶子外部的空节点)视为黑色:

  1. 每个节点非红即黑
  2. 根节点是黑色
  3. 所有叶子节点(NIL)是黑色
  4. 红色节点的子节点必须是黑色(不能有两个连续的红节点)
  5. 从任一节点到其所有后代叶子的路径上,黑色节点数量相同(黑高一致性)

这五条性质共同作用,防止树在操作后退化成链表。


3️⃣ 它如何保持平衡

红黑树通过局部调整 + 颜色翻转保持平衡,调整策略主要有:

插入和删除时会触发这两种操作的组合:

红黑树并不是“完全平衡”的,而是保证最长路径不超过最短路径的两倍,足以保证 O(log n) 的性能。

2️⃣ 程序运行 & 虚拟内存分配

  1. 加载器(Loader)
    • 程序运行时,操作系统把可执行文件的各个段(代码段、数据段、BSS 段等)映射到进程的虚拟地址空间
    • 给栈和堆分配虚拟地址区域。
  2. 变量地址
    • 你在代码里看到的“地址”其实是 虚拟地址(VA),进程自己看到的是连续的地址空间。

3️⃣ 虚拟地址到物理地址的转换(MMU & 页表)

  1. CPU 访问变量时,会把虚拟地址交给 内存管理单元(MMU)
  2. MMU 通过 页表(Page Table) 查找虚拟页号 → 物理页号的映射。
    • 页表存在内存中,但 页表缓存(TLB, Translation Lookaside Buffer)里可能已经有了映射。
  3. 如果 TLB 命中 → 直接得到物理地址。 如果 TLB 未命中 → 硬件/操作系统查页表;若该页不在内存,还会触发缺页中断,从磁盘加载。

4️⃣ 物理地址到 CPU 缓存/内存访问

  1. 拿到物理地址后,CPU 会先查 多级缓存(L1 → L2 → L3)
    • 如果缓存命中(cache hit) → 直接取值,延迟很低(L1 只有几个 CPU 周期)。
    • 如果缓存未命中(cache miss) → 去下一层缓存,最后可能访问主存(几十到几百个 CPU 周期)。
  2. 如果数据不在内存(比如被换出到磁盘的 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 分阶段等人 —— 多批人分多轮集合,每轮都要等人齐了才能开始下一轮,像接力赛分棒次同步。 可动态注册/注销线程 动态任务批次同步

原子类(AtomicIntegerAtomicLongAtomicReference 等)底层的核心是 CAS(Compare-And-Swap/Compare-And-Set) + volatile 保证可见性 + Unsafe 类直接操作内存。我帮你分成几个层面解释:


1. 原子类的组成原理

1.1 CAS(比较并交换)


1.2 volatile 保证可见性


1.3 Unsafe 类直接操作内存

Java GC(复制算法,Copying GC)里分两个区的原因、好处和缺点我帮你分开说一下,你就能完全理解了。


1️⃣ 为什么要分两个区

复制算法的核心思想是:

只在一块区域分配对象,回收时将存活对象复制到另一块空区域,然后一次性清空原区域。

在 HotSpot 的 新生代(Young Generation)里,这两个区就是 From 区To 区(有时候还会加一个 Eden 区,形成 Eden + From + To 的组合)。

原因:

  1. 避免内存碎片化
    • 复制算法直接把活对象压缩到另一块连续内存,剩下的整块空间一次性清掉,内存自然是连续的,没有碎片。
  2. 分配速度快
    • 复制后,下一次分配只要用指针碰撞(Bump-the-pointer),即从连续内存的头部往后分配,速度很快,不需要复杂的空闲链表。
  3. 实现简单
    • 不用维护空闲块列表,只需要知道From → To怎么切换。
  4. 适合新生代高回收率场景
    • 新生代对象朝生夕死,存活率低,大部分空间一次清空,复制成本低。

2️⃣ 为什么是两个区(而不是一个区就直接移动)

如果只有一块区域:


3️⃣ 缺点

  1. 浪费一半空间
    • 因为必须留出一块空的 To 区来接收对象,意味着同一时间只有一半的新生代可用。
    • 例如新生代 100MB,From + To 各 50MB,实际能用的只有 50MB。
  2. 存活对象多时,复制成本高
    • 如果存活对象很多(比如老年代迁入的对象、长生命周期对象),复制算法的效率就下降,因为需要挨个复制。
    • 这也是为什么它适合新生代而不适合老年代
  3. 内存利用率低
    • 在内存紧张的环境下,浪费 50% 会很明显,不如标记-整理(Mark-Compact)节省空间。
  4. 需要额外的写屏障处理跨区引用
    • 年轻代 GC 时,如果老年代对象引用了年轻代对象,需要做Card Table 记录,否则复制时会漏掉引用。

1️⃣ 栈(Stack)

2️⃣ 堆(Heap)

Redis 一般怎么用?在论坛里的落地清单

1) 缓存与反压

2) 计数与排行榜

3) 用户态功能

4) 搜索与推荐的辅助

5) 消息与异步

6) 防作弊/风控辅助

7) 分布式锁与任务协调

键设计

线程不安全

线程安全

线程不安全

线程不安全

线程安全

ConcurrentHashMap

1. Hashtable


2. ConcurrentHashMap(JDK 1.8 之后的实现)

红黑树是不是平衡二叉树?

这样保证了:

因此: 👉 红黑树是一种近似平衡的二叉搜索树,而不是严格意义上的平衡二叉树。

特点 红黑树 B+ 树(MySQL 索引)
结构 二叉搜索树(2 分叉) 多路平衡树(可有上百个分叉)
数据存放 所有节点存储数据 叶子节点存数据,非叶子节点存索引
高度 高,数据量大时树很深 低,节点分叉多,IO 次数少
适用场景 内存数据结构(如 TreeMap、Linux 调度器) 数据库索引(磁盘存储优化)
顺序遍历 需要中序遍历整个树 叶子节点链表即可高效顺序扫描
磁盘友好性 差(频繁磁盘 IO) 好(节点对齐磁盘页,减少 IO 次数)

总结

单例模式的核心思想是: 👉 保证一个类在整个程序运行过程中只有一个实例,并且提供一个全局的访问点。

换句话说:


📦 使用场景

单例常用在需要唯一全局对象的地方,比如:

1️⃣ 最基础的懒汉模式(非线程安全

public class Singleton {
    private static Singleton instance;   // 没有提前创建
    private Singleton() {}               // 私有构造函数

    public static Singleton getInstance() {
        if (instance == null) {          // 第一次调用时才创建
            instance = new Singleton();
        }
        return instance;
    }
}

2️⃣ 加锁版懒汉(线程安全,但性能差

public class Singleton {
    private static Singleton instance;
    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

4️⃣ 更优雅的懒汉式:静态内部类

public class Singleton {
    private Singleton() {}

    private static class Holder {
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }
}

1. 明确问题现象


2. 快速确认影响范围


3. 定位问题入口


4. 分层排查


5. 验证并复现


6. 应急处理


7. 复盘与预防


👉 一个口诀是:“先止血,再定位,最后复盘”

如果你要并发调用十几个下游,线程池参数别拍脑袋配。最靠谱的是用“隔离 + 估算 + 验证”的套路:

一、先做隔离(Bulkhead)

二、用数据估算池大小(核心公式)

拿到每个下游的 3 个数:目标 QPS、p95 延迟(或超时上限)、突发系数/冗余比例。 用 Little’s Law 估并发需求:

例:某下游 QPS=300,p95=80ms=0.08s C≈300×0.08=24,并发冗余 1.3 → 31

三、如何落到线程池参数

以 Java ThreadPoolExecutor 为例(阻塞式 I/O 场景):

  1. corePoolSize ≈ 上面算出的 C(向上取整)。
    • 业务稳定:取 C
    • 有突发:取 1.1~1.3×C
  2. maximumPoolSize = 1.5~2×core,但有上限
    • CPU 密集:≤ 2×CPU 核心数
    • I/O 阻塞:可以高一些(几十~上百),但要用监控盯上下文切换和 GC
  3. 工作队列(非常关键)
    • 对尾延迟敏感SynchronousQueue(0 队列)+ 合理的 max,失败快、保护下游
    • 需吞吐+吸收小突发LinkedBlockingQueue(cap),cap ≈ C ~ 2C
    • 队列太大 = 隐藏拥塞 + 放大尾延迟
  4. keepAliveTime 30~60s;流量波动大且想省线程,可 allowCoreThreadTimeOut(true)
  5. 拒绝策略
    • AbortPolicy:立刻失败,上报/打点(推荐保护下游)
    • CallerRunsPolicy:向上游反压,但要确保调用方线程能承受
    • 或自定义:记录下游名、池名、当前 in-flight,打点报警
  6. 超时(不属于线程池但必须配)
    • 下游调用超时要 小于 你对外 SLA
    • 读超时 ≈ p99 延迟 + 少量余量;连接超时单独设更小
  7. 并发上限(替代或补充线程池) 使用信号量隔离(如 Resilience4j Bulkhead)把“同时在途请求数”钳住到 C~1.5C,效果常比仅靠队列更稳定。

给你一份超精简版多机房数据同步攻略(够用且能落地):

1)先选一种模式(别全都上)

2)数据库怎么落地(MySQL 举例)

3)缓存 & 消息

4)最重要的 5 条铁律

  1. 幂等:所有跨机房写都带 request_id,DB 上加唯一索引/去重表。
  2. Outbox/CDC 代替双写:应用内先写本地 outbox,再异步发 Kafka,同步两地靠日志,不靠应用并行双写。
  3. 读写一致性:需要写后立读的请求,绑回同机房/同分片或带版本条件读。
  4. 演练切换:有回滚预案(能 5 分钟内切回),季度演练 RTO/RPO。
  5. 监控三件套:复制延迟(秒/位点)、Kafka lag、冲突/去重命中率。

5)一分钟落地清单

记忆法:“默认单主,能异步别同步;能幂等别重试;能日志别双写;能就近读别跨洋。”

线程与进程的区别

  1. 基本概念

    • 进程(Process):操作系统分配资源的最小单位。每个进程都有独立的内存空间(代码段、数据段、堆、栈),以及文件描述符、寄存器等。
    • 线程(Thread):CPU 调度的最小单位,是进程中的一个执行流。线程共享所属进程的资源(如内存、文件句柄),但有自己独立的栈和寄存器。
  2. 区别总结

    对比点 进程 线程
    资源 拥有独立的内存地址空间和资源 共享进程的内存和资源
    开销 创建、销毁、切换开销大 创建、销毁、切换开销小
    通信 进程间通信(IPC)需要特殊机制,如管道、消息队列、共享内存 线程间通信更容易,直接读写共享内存
    稳定性 一个进程崩溃不会直接影响其他进程 一个线程崩溃可能导致整个进程崩溃
    调度 由操作系统调度 同样由操作系统调度,但粒度更小

栈(Stack)

堆(Heap)

区别总结

对比点
管理方式 系统自动管理 程序员手动管理
存储内容 局部变量、函数调用信息 动态分配的对象、数组
空间大小 较小,连续存储 较大,分散存储
速度 相对慢
问题 可能溢出 可能内存泄漏、碎片化

Redis 中的 Sentinel 模式

如果你是问 Redis Sentinel,那它是 Redis 高可用方案

核心作用

架构组成

  1. Sentinel 进程:独立运行,专门负责监控 Redis 节点。
  2. Master 节点:主节点,负责写入和复制。
  3. Slave 节点:从节点,负责读和备份。

工作机制

  1. 监控:Sentinel 定期 ping Master 和 Slave,检查是否存活。
  2. 选举:如果 Master 宕机,多数 Sentinel 一致认为“主挂了”,会发起一次 投票选举新的 Master
  3. 故障转移:把某个 Slave 升级为 Master,并通知其他 Slave 去同步它。
  4. 通知:Sentinel 还会通过 API 通知客户端“新的 Master 地址”。

👉 打个比方:

ACID 四大特性

  1. Atomicity(原子性)
    • 定义:事务中的所有操作要么全部成功,要么全部失败,不会出现“只做了一半”的情况。
    • 例子:银行转账,A 给 B 转 100 块。
      • A 账户减 100
      • B 账户加 100
      • 原子性保证:这两步要么都成功,要么都不执行。不会出现 A 扣钱了但 B 没收到。

  1. Consistency(一致性)
    • 定义:事务执行前后,数据库必须保持一致性状态,不会违反数据完整性约束。
    • 例子:转账后,A 和 B 的余额总和应该保持不变(转账前后总额相等),即使发生异常也不能破坏规则。

  1. Isolation(隔离性)
    • 定义:多个事务并发执行时,每个事务都应该像自己独占数据库一样,不受其他事务影响。
    • 例子
      • 如果 A 正在查询商品库存,B 正在修改库存,那么 A 应该要么看到修改前的数据,要么看到修改后的数据,而不是一个“中间状态”。
    • 补充:数据库通过 隔离级别(读未提交、读已提交、可重复读、串行化) 来实现不同程度的隔离。

  1. Durability(持久性)
    • 定义:事务一旦提交,修改就会永久保存,即使数据库宕机、断电也不会丢失。
    • 实现:通常依赖 WAL(Write Ahead Log,预写日志)、Redo Log 等机制。
    • 例子:转账事务提交后,即使系统瞬间掉电,重启后也能从日志恢复数据。

泄露的内存什么时候会释放

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 字节)

  1. Version (4 bit) 表示 IP 协议版本号。常见是 4(IPv4),IPv6 中为 6。
  2. IHL (Internet Header Length, 4 bit) 头部长度,单位是 4 字节。最小值 5(即 20 字节),最大 15(即 60 字节)。若有 Options 字段,则 IHL > 5。
  3. DSCP/ECN (8 bit)
    • DSCP (Differentiated Services Code Point):服务质量(QoS),用于流量优先级。
    • ECN (Explicit Congestion Notification):显式拥塞通知,用于拥塞控制。
  4. Total Length (16 bit) 整个 IP 包(头 + 数据)的长度,单位字节,最大 65,535。
  5. Identification (16 bit) 分片标识号。分片时所有片的 ID 相同,接收方靠它把分片组装回去。
  6. Flags (3 bit) 控制分片:
    • Bit 0:保留(必须为 0)
    • DF (Don’t Fragment):1 表示禁止分片
    • MF (More Fragments):1 表示后面还有分片
  7. Fragment Offset (13 bit) 当前分片相对于原始数据的偏移量,单位 8 字节。
  8. TTL (Time To Live, 8 bit) 存活时间,每经过一个路由器减 1,减到 0 则丢弃,防止环路。
  9. Protocol (8 bit) 表示 IP 数据部分使用的上层协议:
    • 6 = TCP
    • 17 = UDP
    • 1 = ICMP
    • 2 = IGMP
  10. Header Checksum (16 bit) 只覆盖 IP 头部的校验和,用于检测头部是否损坏。
  11. Source IP Address (32 bit) 源主机 IP 地址。
  12. Destination IP Address (32 bit) 目标主机 IP 地址。
  13. Options (可选, 0–40 字节) 用于测试、路由记录、安全性要求等。现在很少用。
  14. Padding 用 0 填充,使头部长度为 4 字节的整数倍。

IPv6 头部字段及作用

(固定 40 字节,更简化)

  1. Version (4 bit) 固定为 6。
  2. Traffic Class (8 bit) 类似 IPv4 的 DSCP/ECN,用于 QoS、拥塞控制。
  3. Flow Label (20 bit) 表示数据流标识,路由器可识别同一流量并保证顺序或 QoS。
  4. Payload Length (16 bit) 负载长度(不包括头部),最大 65,535。若更大需扩展头部支持。
  5. Next Header (8 bit) 类似 IPv4 的 Protocol 字段,表示紧跟在 IPv6 头后面的协议(TCP/UDP/ICMPv6 等),或扩展头类型。
  6. Hop Limit (8 bit) 类似 IPv4 的 TTL,每跳减 1,归零丢包。
  7. Source Address (128 bit) 源 IPv6 地址。
  8. Destination Address (128 bit) 目的 IPv6 地址。

🔑 总结:

如何提升服务性能

我一般从几方面考虑:

  1. 算法与数据结构:降低复杂度,选合适容器。
  2. 并发与架构:用异步 I/O、线程池、减少锁争用。
  3. 缓存:本地缓存 + 分布式缓存,减少数据库压力。
  4. 数据库优化:建索引、读写分离、批量操作。
  5. 网络与系统调优:连接池、零拷贝、参数优化。
  6. 可观测性:先监控和定位,再有针对性优化。

两个 800GB 大文件找重复行 —— 思路分层讲

1. 明确限制

2. 常见方法

方法一:外部排序 + 归并

方法二:哈希分桶

3. 辅助优化

阻塞(Blocking)与非阻塞(Non-blocking)

阻塞 IO

非阻塞 IO

同步(Synchronous)与异步(Asynchronous)

同步 IO

异步 IO

1. 先想象一个场景:你点外卖


2. 换到 IO 上


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 请求,数据到达后由操作系统通知。

一个生活类比

新版 Redis 为何采用「RDB + AOF 混合」策略?

什么是缓存热点 key?电商秒杀场景下如何减少单 key 高频写入?

如何把热点 key 拆分或合并请求以降低压力?

MySQL 常见索引分为以下几类:

  1. 主键索引(Primary Key)
    • 每个表只能有一个,唯一且非空,通常基于 B+ 树。
    • InnoDB 中,主键索引的叶子节点直接存储整行数据(聚簇索引)。
  2. 唯一索引(Unique Key)
    • 保证列值唯一,但允许有一个 NULL。
    • 叶子节点存储的是索引列和主键值。
  3. 普通索引(Index / Key)
    • 最基本的索引,加快查询,没有唯一性约束。
  4. 联合索引(Composite Index)
    • 在多个列上建立的索引,遵循最左前缀原则
  5. 全文索引(Fulltext Index)
    • 用于大文本字段(如 CHAR、VARCHAR、TEXT),支持分词、模糊搜索。
  6. 空间索引(Spatial Index)
    • 基于 R-Tree,主要用于地理空间数据(GIS)。

B+ 树节点大小通常与操作系统页对齐,MySQL 默认页大小是多少?

如果让你设计,B+ 树非叶子节点大小应遵循什么原则?

要让“用户 → 正确分片”这件事稳定又可扩展,常见有 4 种做法。你可以任选其一或组合使用。

1) 应用层取模路由(最简单)

2) 一致性哈希(平滑扩容)

3) 代理/中间层路由(对应用透明)

4) 存储自带分片(以 Redis 为例)

最终一致:在分布式系统里,数据在不同副本之间不会立刻同步,但经过一段时间(秒/分钟)后,一定会收敛到一致的状态

Nginx(发音类似 engine-x)是一个高性能的Web服务器和反向代理服务器,同时也可以用作负载均衡器HTTP缓存。它最初由俄罗斯工程师 Igor Sysoev 在 2004 年开发,目标是解决当时 Apache 在高并发场景下的性能瓶颈。

主要特点

  1. 高并发性能
    • Nginx 使用事件驱动的异步非阻塞架构,在处理成千上万并发连接时,资源占用率远低于传统的多进程/多线程服务器(如 Apache)。
  2. 反向代理
    • 它可以作为客户端和后端服务器之间的中间层,接收请求并转发到后端应用服务器(如 Tomcat、Node.js、Flask 等)。
    • 常用于隐藏后端服务器 IP、做 SSL 终止、实现请求分流。
  3. 负载均衡
    • 支持多种策略(轮询、最少连接、IP hash 等),可将请求分配到多台服务器,从而提升系统的可用性和扩展性。
  4. 静态资源服务
    • 对静态文件(HTML、CSS、JS、图片、视频)的处理速度非常快,常用于 CDN 和静态资源服务器。
  5. 模块化设计
    • 可以扩展功能,例如支持缓存、限流、安全防护等。

你访问一个大型网站(比如电商平台),前端请求先到达 Nginx

什么时候不需要锁全表(绝大多数业务)

哪些情况可能需要“锁全表”或导致看起来像被锁住

这些一般是运维/DDL 级别,而不是普通业务写操作:

  1. 某些 DDL(表结构变更)
    • MySQL InnoDB 大多数变更支持 Online DDL(如 ALGORITHM=INPLACE/INSTANTLOCK=NONE),但并不是所有变更都无锁
      • 例如 变更主键、修改列类型(复杂改动)、变更存储引擎、DROP/ADD 一些约束 等,可能需要较强的表级锁。
      • 即便 LOCK=NONE,也会有元数据锁(MDL):在提交前阻止新的 DML/DDL 开始,若处理不当也会“全场卡住”的观感。
    • Postgres 中如 VACUUM FULLCLUSTERREINDEX CONCURRENTLY(较温和,但也有约束) 等会对并发有较强影响。
  2. 需要全局一致性的运维动作
    • 全量去重/修复唯一性(比如手机号唯一,需要先清洗脏数据再加唯一索引)——为确保一致性,可能短时间阻断写入或加较强锁。
    • 大范围脱敏/回填且必须“一次原子完成”的场景。通常更推荐分批 + 幂等,避免长事务/大锁。
  3. 没有合适索引的大批量更新/删除
    • 虽然不一定是“表锁”,但会产生大量行锁 + 间隙锁,把热点区段“锁死”,外界体验像“被锁表”。
    • 解决:先补索引、按主键范围分批、控制事务大小。
  4. 老旧或特殊引擎
    • MySQL MyISAM 是表级锁;SQLite 写入时会有较大粒度的锁。生产上一般用 InnoDB/PG 避免这类问题。

Kafka 分区的作用


场景类比:快递分拣中心


如果没有分区(只有 1 条传送带)


有了分区(比如 3 个分区 = 3 条传送带)

  1. 提升吞吐量
    • 同时有 3 个传送带运转,3 个快递员并行处理,速度快很多。
  2. 支持扩展
    • 如果订单太多,可以增加传送带数量(增加分区)和快递员数量(消费者)。
  3. 保证顺序
    • 同一客户的所有订单会始终进入同一条传送带(同一个分区),这样就能保证顺序不乱。
  4. 容错性
    • 每条传送带旁边都有备用通道(副本)。哪怕一个坏了,还有其他副本能继续处理,不会丢快递。

总结成一句话

Kafka 分区就像把快递分到多条传送带上:既能并行提高速度,又能保证同一条传送带上的顺序,还能方便扩容和容错。

1. 概念层面


2. 区别总结


3. 各自适合的问题

7. Redis 集群(Redis Cluster)

作用:把数据分片到多台节点,既扩容内存/吞吐,也提供故障转移(高可用)。

核心机制

优点

注意点 / 踩坑

何时用 Cluster 而非单机/哨兵


8. “三大缓存”与缓存一致性

这里常被问的是三种主流缓存模式(有时面试官叫“三大缓存”)+ 如何保证与数据库的一致性:

8.1 三种缓存模式(读/写路径)

  1. Cache-Aside(旁路/旁路缓存) – 业务最常用
  1. Read-Through – 由缓存层代替业务读 DB
  1. Write-Through / Write-Behind(回写/异步回写)

面试金句:读多写少 → Cache-Aside;延迟极致 & 接受最终一致 → Write-Behind;有统一的缓存代理/网关 → Read-Through。

8.2 缓存与数据库一致性(强/最终)

目标:避免读到过期数据或“回写覆盖”。

通用实践(按重要性)

  1. 写顺序:先 DB,后删缓存(而不是更新)
    • 解决并发下“旧值覆盖新值”的经典问题。
    • 失败重试:删除失败要有重试/补偿(重试队列、任务表)。
  2. 延时双删(Double-Delete)
    • 写 DB → 删缓存 → 延时再删一次(例如 200~500ms)。
    • 目的:覆盖并发读导致的“旧值回填”。
  3. Binlog 异步订阅修正(强烈推荐)
    • 监听 DB binlog(如 Canal/Debezium),把变更投递到 MQ,消费者精确失效/重建缓存
    • 优点:最终一致且可靠,对业务入侵小。

在「数据库 + 缓存(Redis)」架构里,常见的痛点是:


2. Binlog 异步订阅修正是什么?

比如:


3. 优点

  1. 最终一致且可靠
    • Binlog 是 MySQL 的事实来源(ground truth),不会漏。
    • 即使业务代码没管缓存,Binlog 订阅也能保证缓存和数据库最终一致。
  2. 对业务入侵小
    • 业务代码只管写数据库,不用到处加 del redis
    • 缓存修正由异步订阅系统负责,和业务解耦。
  3. 解耦 + 可扩展
    • 除了缓存修正,还能把 Binlog 投递给别的系统(搜索引擎、实时计算、数据同步等)。

4. 总结一句话

👉 Binlog 异步订阅修正 = 监听数据库 Binlog → 通过 Canal/Debezium 把变更投递到 MQ → 消费者更新/失效缓存。 好处是 最终一致、可靠、低业务入侵,是业界强烈推荐的「DB-Cache 一致性方案」。

  1. TTL + 逻辑过期
    • 设置合理 TTL 兜底;对热点可用逻辑过期(值+过期时间),过期后由单协程重建,其余读老值,避免击穿。
  2. 防并发写脏
    • 分布式锁/乐观锁(版本号、CAS)防止多写覆盖;
    • 幂等(按业务主键/请求 ID)。
  3. 读写隔离与一致性选择
    • 读多写少常选最终一致(性能优先);金融/下单等关键路径用“强一致”:
      • 写:DB 成功→删缓存成功才返回;
      • 读:强制读 DB 或携带版本戳校验。

8.3 典型故障与治理

JVM 内存主要分为 线程私有线程共享 两大类区域:

1. 指针


2. 切片 (slice) & 数组


3. map

扩容的时候发生了什么?

  1. 触发条件:装载因子太大(元素数 / bucket 数超过阈值,大约 6.5)。
  2. 扩容方式:哈希表会新建一个 桶数量翻倍 的数组。
    • 原来可能是 8 个 bucket,扩容后变成 16 个 bucket。
  3. 数据迁移:旧 bucket 里的 kv 会逐步搬到新 bucket 里(渐进式,避免一次性 STW 卡顿)。

举个例子

1. 溢出桶(overflow bucket)

结构大致像这样(伪图):

[主 bucket] -> [overflow bucket1] -> [overflow bucket2] -> ...

4. 字符串


5. 接口 (interface)


6. defer / panic / recover

1. goroutine

👉 考点:为什么 Go 能开成千上万个 goroutine,而 Java/C++ 线程不行? ➡️ 因为 goroutine 栈很小且可动态增长,调度在用户态完成,开销远小于内核线程。


2. channel

👉 考点:

1. 无缓冲 channel

特点

典型应用场景

  1. 任务同步/信号通知

    • 比如一个 goroutine 完成了任务,要通知另一个 goroutine:
    done := make(chan struct{})
    go func() {
        work()
        done <- struct{}{} // 通知完成
    }()
    <-done // 等待通知
    
  2. 生产者-消费者一对一传递

    • 适合实时、点对点的数据交付。
    • 确保“谁发的,谁收到了”,不会堆积。
  3. 控制并发节奏

    • 用无缓冲 channel 让 goroutine 按顺序执行,而不是乱跑。

👉 可以理解为“电话沟通”:必须两边同时在线才能传话。


2. 有缓冲 channel

特点

典型应用场景

  1. 生产者-消费者(异步)

    • 多个生产者 goroutine 往里写,消费者 goroutine 慢慢取:
    ch := make(chan int, 100)
    go producer(ch)
    go consumer(ch)
    
  2. 任务队列/工作池

    • 把待处理任务放到有缓冲 channel,工人 goroutine 从中取任务并处理。
    • 类似一个简易版的队列。
  3. 削峰填谷

    • 短时间内生产速度快于消费速度,用缓冲来“平滑”压力。
    • 比如日志收集、网络请求缓存。

👉 可以理解为“邮箱”:发信人可以先丢进去,收信人慢慢取。


3. 对比总结

类型 特点 应用场景
无缓冲 channel 发送接收必须同时就绪;同步 信号通知、顺序控制、即时任务交付
有缓冲 channel 缓冲区存储;异步 消息队列、工作池、削峰填谷

3. select

👉 常见场景:超时控制

select {
case v := <-ch:
    fmt.Println(v)
case <-time.After(1 * time.Second):
    fmt.Println("timeout")
}

4. context

👉 典型用法:

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

👉 使用场景:读多写少时 RWMutex 效果好;写多时反而可能更慢。

(2) WaitGroup

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


6. 原子操作

👉 面试常考点:用 atomic 实现自旋锁、计数器。


总结一张表

模块 核心点 考点
goroutine GMP 模型 为什么能开很多协程
channel CSP 模型 无缓冲 vs 有缓冲,close 行为
select 多路复用 随机公平,超时控制
context 跨协程控制 cancel, timeout, deadline
Mutex/RWMutex 锁机制 读写性能对比
WaitGroup 协程等待 AddDone 配合
Once 单例 保证只执行一次
Cond 条件变量 唤醒机制
Map 并发安全 map 读多写少
atomic 原子操作 CAS, 计数器

1. GMP 调度模型

关系

为什么高效?


2. 垃圾回收 (GC)

Go 的 GC 是 并发标记清除,核心是 三色标记法 + 写屏障

三色标记

流程:

  1. GC 起始时,根对象(全局变量、栈)标记为灰色。
  2. 遍历灰色对象,把它们引用的对象标记成灰色,然后自身变黑。
  3. 最后白色对象就是垃圾,被回收。

写屏障

4. 内存对齐

例子:

type A struct {
    a int8   // 1B
    b int64  // 8B
    c int8   // 1B
}
fmt.Println(unsafe.Sizeof(A{})) // 输出 24

解释:

👉 优化办法:把大的字段放前面,小的放后面,减少填充。

Go 的 net/http 库底层是如何和网络沟通的。面试里这一块也常会考。简单讲,它其实是对 TCP 套接字 (socket) 的一层封装。


1. 启动 HTTP 服务的入口

最常见的启动方法是:

http.ListenAndServe(":8080", handler)

它背后做了几件事:

  1. 创建监听器:调用 net.Listen("tcp", ":8080"),监听 TCP 端口。
  2. 循环 accept:不断 Accept() 等待新的客户端连接。
  3. 每个连接开一个 goroutine:并发处理请求。
  4. 读写数据:在 goroutine 内,对 socket 做 Read/Write,并解析 HTTP 协议。

2. 底层网络调用流程

Go 的 net 包其实就是对 系统调用(syscall) 的封装:


3. Go 的并发网络模型

👉 这就是为什么 Go 的 http server 可以轻松支撑成千上万并发连接 —— goroutine 是轻量的,I/O 调度靠事件驱动,而不是线程阻塞。


4. Handler 的调用链

当数据读到用户态,net/http 会:

  1. 解析 HTTP 请求行、Header、Body → 生成 http.Request 对象。
  2. 调用用户注册的 Handler.ServeHTTP(w, r)
  3. 用户写 w.Write() → 最终写回到 TCP 连接。

总结一句话: Go 的 net/http 底层是用 net 包的 TCP socket 实现的,连接管理靠 epoll/kqueue/IOCP,I/O 事件由 runtime 的 netpoller 分发给 goroutine,最终由用户定义的 Handler 来处理业务逻辑。

1. goroutine 泄漏场景与排查方法

👉 面试答法:goroutine 不会被 GC 自动回收,必须保证退出机制,比如用 context 控制生命周期。


2. channel 死锁的几种情况


3. interface 底层实现机制

👉 常考陷阱:


4. slice 扩容机制,append 触发条件


5. map 并发读写问题,如何解决


6. defer 的执行顺序,和 return 结合时的执行时机

例子:

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 随机性以及默认分支


9. Go 的 GC 与 C++/Java 的区别


10. Go 的零值机制,为什么避免了“野指针”问题

常见性能优化手段

🌰 情况一:goroutine 相互等待资源(两个人互相等钥匙)

想象一下:

结果呢? 👉 小明说:“你先给我钥匙 B,我再给你钥匙 A。” 👉 小红说:“你先给我钥匙 A,我再给你钥匙 B。”

谁也不松手,双方僵住了。门永远打不开。

这就是 两个 goroutine 拿锁顺序不一致 → 相互等待 → 死锁

避免方法:规定规则,比如“大家必须先拿钥匙 A,再拿钥匙 B”。这样就不会互相僵持。


🌰 情况二:channel 双向阻塞(两个人互相等对方说话)

想象一下:

于是两人都张着嘴巴,互相盯着,尴尬沉默,谁都不说话。

👉 这就是 无缓冲 channel 的读写双方都在等 → 死锁

解决方法

下面把“Redis 集群一致性”拆成 4 个层面:写一致性、读一致性、持久化一致性、迁移/故障一致性,并给出可落地做法。

1) 写一致性(主从复制)

2) 读一致性(主/从读与读己之)

3) 持久化一致性(崩溃后是否一致)

4) 迁移/故障切换一致性

5) 应用侧“补齐一致性”的通用做法

🔹 (1) 全量复制(初次同步 / 断线重连)

  1. 从节点发送 PSYNCSYNC 给主节点。
  2. 主节点执行 bgsave 生成 RDB 快照,并把快照文件发送给从节点。
  3. 在生成快照期间,主节点会把新的写操作写入 复制缓冲区 (replication backlog buffer)
  4. 从节点加载 RDB 文件,更新数据。
  5. 从节点再接收缓冲区里的增量命令,保证数据和主节点一致。

🔹 (2) 增量复制(部分同步)

3. 主从复制的实现细节

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 的?


2. 反射为啥会影响性能?


3. 线程安全的工作原理是啥?


4. 主内存和工作内存(JMM)

9. 网络编程里的 IO 模型

常见五种:

  1. 阻塞 IO(BIO):调用阻塞,直到数据就绪。
  2. 非阻塞 IO:调用立即返回,需轮询。
  3. IO 多路复用select/poll/epoll,一个线程管理多个连接。
  4. 信号驱动 IO:数据就绪时内核发信号通知应用。
  5. 异步 IO(AIO):应用提交请求后,内核完成后直接通知应用处理。

11. TCP 是怎么保证可靠传输的?

流量控制 (Flow Control)


拥塞控制 (Congestion Control)

✅ 窗口是不是固定的?

不是固定的。

发送窗口大小 = min(接收窗口, 拥塞窗口)

也就是说,既不能超过接收方的承受能力,也不能超过网络的拥堵程度。


✅ 窗口长度怎么算?

是的,你理解的没错,窗口长度 = 已发送未确认的数据 + 还能发送的数据

1. synchronized 的特点


2. ReentrantLock 的特点


3. 两者对比

特性 synchronized ReentrantLock
使用方式 关键字,自动释放 显式 lock/unlock
可重入 支持 支持
可中断 不支持 支持
超时获取 不支持 支持
公平性 不支持 支持
条件队列 wait/notify Condition 多路等待
性能 JDK1.6 后优化,常用场景足够 在高并发/复杂控制下更灵活

1. 什么是 AQS?


2. AQS 的核心组成

  1. state 变量
    • volatile int state;
    • 表示同步状态,比如锁是否被占用、剩余的许可数等。
    • 通过 CAS (compare-and-swap) 保证修改的原子性。
  2. CLH 队列
    • AQS 使用 CLH(双向链表队列) 来保存等待线程。
    • 每个线程排队时会封装成一个 Node,放入队尾。
    • headtail 指针维护队列。
  3. Node 节点
    • 保存线程本身(Thread)、等待状态(waitStatus)、前驱/后继引用。
    • 状态标记有:SIGNAL(等待唤醒)、CANCELLED(取消等待)、CONDITION(在条件队列中)等。

3. 获取锁(acquire)的过程(独占模式为例)

  1. 尝试获取资源
    • 调用 tryAcquire()(子类实现,如 ReentrantLock)。
    • 如果成功:直接返回。
    • 如果失败:进入等待队列。
  2. 入队
    • 将当前线程封装为 Node,CAS 插入到队尾。
  3. 自旋等待
    • 线程在队列中循环检查是否能获取到锁:
      • 如果前驱是 head 且资源可用,则 CAS 修改 state 成功并获取锁。
      • 否则阻塞(LockSupport.park())。
  4. 唤醒
    • 前驱节点释放锁时,会唤醒后继节点(LockSupport.unpark()),后继线程继续尝试获取锁。

4. 释放锁(release)的过程

  1. 修改 state
    • 调用 tryRelease()(子类实现)。
    • 如果 state == 0(完全释放),说明锁已可用。
  2. 唤醒后继节点
    • 唤醒队列中第一个有效的等待线程(head 的 next)。
    • 被唤醒的线程会重新尝试获取锁。

5. 共享模式(Semaphore、CountDownLatch)


6. 核心思想总结

也就是说,AQS 就是一个 可扩展的框架:它自己不定义获取/释放的具体逻辑,而是交给子类(ReentrantLockSemaphore 等)实现 tryAcquire/tryRelease,然后用 AQS 封装好的排队 + 阻塞 + 唤醒机制来管理并发。

双亲委派模型(Parent Delegation)

定义:类加载器在加载类时,先把请求交给父类加载器去尝试加载,只有父类加载器找不到时,才由当前加载器自己去加载。

主要目的:

  1. 避免重复加载:父类加载器加载过的类,子类加载器就不会重复加载。
  2. 保证核心类安全:比如 java.lang.Object 必须由最顶层的引导类加载器(Bootstrap ClassLoader)加载,防止被恶意替换。

典型流程(从下往上委派):

慢 SQL 常见原因

  1. 缺少合适索引:条件字段没有索引,导致全表扫描。
  2. 索引未命中:例如 like '%abc'、函数操作 where date(create_time)=...,会导致索引失效。
  3. 返回数据过多:一次查询几百万行,网络传输和应用层处理都慢。
  4. join 设计不当:没有合理索引的多表 join。
  5. 排序 / 分组开销大order bygroup bydistinct 没有索引辅助。
  6. 统计类函数count(*) 在大表上性能差(InnoDB 要全表扫描)。
  7. 锁/阻塞:事务未提交,SQL 被锁住。

3. 优化思路

  1. 加索引
    • 单列索引、联合索引(最左匹配原则)。
    • 覆盖索引(select id,name from user where id=...)。
  2. 改写 SQL
    • 避免 select *,只取需要的列。
    • 避免 !=、<>、or 等导致索引失效。
    • 尽量把计算放在等号右边,例如 where create_time >= '2024-01-01',而不是 date(create_time) = ...
  3. 分库分表 / 限流
    • 大表按时间、用户 ID 做分表。
    • 分页优化:limit 100000, 10 → 用子查询或 ID 范围。
  4. 缓存
    • 读多写少的场景,用 Redis/Memcached 缓存热点数据。
  5. 架构优化
    • 主从复制:读写分离。
    • 数据仓库:复杂统计查询放到 Hive/ClickHouse。

1. 联合索引的本质

定义:MySQL 在使用联合索引时,会从 最左的列开始匹配,能连续利用多少列,就用多少列。

一旦中间断了,就无法继续利用后面的索引列。

因为 B+Tree 的有序性

  1. redo log(重做日志,InnoDB 专有)
    • 物理日志,记录“数据页修改了什么”。
    • 作用:保证 崩溃恢复(crash-safe)。即使 MySQL 异常宕机,也能用 redo log 把已提交事务恢复出来。
  2. binlog(归档日志,Server 层)
    • 逻辑日志,记录“执行了什么 SQL/行操作”。
    • 作用:保证 主从复制数据恢复
    • 是 MySQL Server 层统一的日志,所有引擎都能用。

两阶段提交(保证两种日志一致)

问题:如果先写 redo log 再写 binlog,中途宕机,可能 redo log 有事务,binlog 没有,主从不一致。 解决:两阶段提交(2PC),保证 redo log 和 binlog 的一致性。

流程:

  1. 写入 redo log(prepare 阶段)
    • 把 redo log 写入磁盘,但标记为 “prepare”,表示事务还没提交。
  2. 写入 binlog
    • 把 binlog 写入磁盘,刷盘成功。
  3. 提交 redo log(commit 阶段)
    • 修改 redo log 标记为 “commit”。
    • 至此事务才算真正提交。

MySQL(InnoDB 引擎)主要通过 undo log + 两阶段提交 来实现原子性。

(1) Undo Log(回滚日志)

👉 这保证了事务失败时,可以回到“没执行之前”的样子。


(2) Redo Log + Binlog 的两阶段提交

👉 这保证了事务提交后,要么 redo/binlog 都有,要么都没有,不会出现“一半成功”。

redo log(重做日志,物理日志,InnoDB 专有) 记录“某个页上做了什么修改”,保证 崩溃恢复(crash-safe)

undo log(回滚日志,逻辑日志,InnoDB 专有) 记录“相反操作”,用于事务回滚,保证 原子性

binlog(归档日志,逻辑日志,Server 层) 记录“执行了什么 SQL/行操作”,用于 主从复制、数据恢复

(1)事务执行过程

  1. 写 undo log(记录老版本)
    • 更新前先写 undo log,以便失败时回滚。
  2. 修改内存数据(Buffer Pool)
    • 把数据页加载到内存并修改,此时还没写入磁盘。
  3. 写 redo log(prepare 阶段)
    • 把修改写入 redo log buffer,并刷盘(标记为 prepare)。
    • 保证即使宕机,也能用 redo log 重放修改。
  4. 写 binlog
    • 把 SQL/行修改写入 binlog cache,提交时统一刷盘。
  5. 提交 redo log(commit 阶段)
    • 修改 redo log 状态为 commit,事务正式提交。

Redis 本质上是一个 单线程事件驱动 的服务器,它的网络模型基于:

避免线程切换开销:单线程就没有锁竞争。

大部分操作在内存中完成:CPU 不是瓶颈,主要瓶颈在网络 I/O。

I/O 多路复用高效:epoll 能同时处理上万连接。

分布式系统里的 几个核心问题常见解决方案详细讲一下:


1. 数据一致性问题

问题:多个副本同时更新,可能出现不一致(读到旧数据、丢更新)。

解决方案

👉 一句话:一致性依靠 共识协议最终一致性模型来实现,取决于业务对一致性的要求。


2. 网络问题

问题:延迟、丢包、网络分区(部分节点之间无法通信)。

解决方案


3. 节点故障

问题:节点可能宕机,服务不能用。

解决方案


4. 数据分片与路由

问题:数据量太大,单机存不下。

解决方案


5. 分布式事务

问题:多个数据库 / 服务参与同一个事务时,难以保证 ACID。

解决方案

消息队列(MQ)的 推/拉模式 是个经典话题。我们可以从概念、流程、优缺点、应用场景四个角度来看:


1. 概念

很多 MQ(Kafka、RocketMQ、RabbitMQ)其实都 支持两种模式,或者底层是拉,但对外封装成推。


2. 流程

拉模式

  1. 消费者周期性向 Broker 发起拉取请求。
  2. Broker 返回一批消息,如果没有消息可能返回空。
  3. 消费者自己决定什么时候再拉。

推模式

  1. Broker 监听到有新消息。
  2. 主动把消息推送到消费者(通常通过长连接)。
  3. 消费者处理后返回确认(ACK)。

3. 优缺点

模式 优点 缺点
拉(Pull) - 消费者可控,自己决定拉取速率。 - 适合批量处理(一次拉多条)。 - 不容易压垮消费者。 - 可能出现空拉(拉不到消息,浪费资源)。 - 实时性差(取决于拉取频率)。
推(Push) - 实时性强,有消息就送。 - 消费者不用自己轮询。 - 如果消费者处理能力不足,可能被压垮。 - 需要流控(Flow Control)、ACK 机制。

分布式系统 + 消息队列 中,“消息幂等”是面试必考点。因为 MQ 天然可能出现:

(1) 唯一消息 ID + 去重表


(2) 利用业务唯一键


(3) 记录消息处理状态


(4) 幂等操作设计


(5) 分布式锁

常见 OOM 出现场景

不同内存区域都可能 OOM:

(1) Java Heap Space

(2) GC Overhead Limit Exceeded

(3) Metaspace / PermGen(JDK8 前是 PermGen,JDK8+ 是 Metaspace)

(4) Direct Buffer Memory

(5) Unable to create new native thread

(6) Out of swap space


3. 为什么会出现 OOM?

栈溢出后程序会怎样?

现在主流的 HotSpot JVM(包括 OpenJDK)中:

所以 Java 的 Thread对操作系统线程的封装

🔹 第一范式(1NF:原子性)

定义:字段必须是原子值,不可再分。

例子: ❌ 错误设计:

学号 姓名 电话号码
1 张三 123,456

电话有两个值,不满足 1NF。

✅ 正确设计:

学号 姓名 电话号码
1 张三 123
1 张三 456

🔹 第二范式(2NF:消除部分依赖)

定义:在 1NF 基础上,表中的非主属性必须完全依赖于主键,不能只依赖主键的一部分。

例子: ❌ 错误设计: 主键 = (学号, 课程号)

学号 课程号 成绩 姓名
       

问题:

✅ 正确设计: 拆成两张表:


🔹 第三范式(3NF:消除传递依赖)

定义:在 2NF 基础上,非主属性之间不能存在传递依赖

例子: ❌ 错误设计:

学号 姓名 系别 系主任
       

问题:

✅ 正确设计: 拆成两张表:


🔹 总结对照表

范式 要求 解决的问题
1NF 字段原子性 列不可再分,消除多值列
2NF 消除部分依赖 非主属性必须完全依赖于主键
3NF 消除传递依赖 非主属性不能依赖于其他非主属性

多个客户端的请求管理其实就是 并发请求的接收、分发和处理,同时要保证系统的 高效性、可靠性和可扩展性。我分几个层次来解释:


1. 请求接入层(入口管理)


2. 请求调度层(并发控制)


3. 请求处理层(业务逻辑)


4. 请求返回层(响应管理)


5. 日志与监控


总结一句话: 服务端管理多个客户端请求的核心就是:入口限流 → 并发调度 → 正确处理 → 高效返回 → 监控保障

Nginx 的核心功能

1. Web 服务器

2. 反向代理

3. 负载均衡

4. 高并发能力

Nginx 一般部署在哪里?

Nginx 常见的部署位置有:

  1. 前端服务器(入口层)
    • 部署在最靠近用户的一层(公网可访问的机器上)。
    • 功能:作为 网关反向代理,统一接收客户端请求,再转发到后端。
    • 示例:电商网站首页静态资源由 Nginx 提供,API 请求再反向代理到 Java/Python 后端服务。
  2. 应用前的负载均衡器
    • 部署在 多台应用服务器前,把流量均衡分配。
    • 功能:均衡请求,避免某一台机器过载。
    • 示例:Nginx → {Tomcat1, Tomcat2, Tomcat3}。
  3. 微服务网关的一部分
    • 在微服务架构中,Nginx 常与 K8s Ingress、API Gateway 配合。
    • 功能:做 动静分离、限流、路由分发,保护后端微服务。

🔹 什么是正向代理 (Forward Proxy)?

例子: 你在中国访问 google.com,由于无法直连,你先访问正向代理(比如一个国外代理服务器),由它帮你访问 Google,再把结果转发回来。


🔹 什么是反向代理 (Reverse Proxy)?

例子: 你访问 www.taobao.com,实际上请求先到阿里云的反向代理(比如 Nginx / SLB),再由它转发到真正的后端服务器集群。

索引建立的核心原则


1️⃣ 索引适合建立在「高频查询」的列上


2️⃣ 选择区分度高的列


3️⃣ 组合索引 > 单列索引


4️⃣ 避免在频繁更新的列上建索引


5️⃣ 控制索引数量


6️⃣ 使用前缀索引(针对长字符串列)


7️⃣ 覆盖索引(索引即结果)


8️⃣ 考虑排序和分组需求


9️⃣ 结合业务场景

redis分布式锁

1️⃣ 最基础实现:SETNX

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

👉 注意:unique_id 是客户端生成的唯一值(如 UUID),用来标识“是谁加的锁”。

🎯 想象一个现实场景 —— 公共厕所的隔间


1️⃣ 加锁:先到先得

小明要上厕所,他先看门上有没有挂“有人”的牌子。

👉 这就是 Redis 加锁:保证同一时间只有一个人(进程)能进。


2️⃣ 解锁:只能自己开

小明用完出来了,要把牌子摘掉。

👉 所以必须先确认“牌子上还是我的名字”(校验 uuid),才能删。 这就是 Lua 脚本解锁


3️⃣ 锁过期:占太久会被清理

假设小明忘记出来,超过 30 分钟了,厕所管理员会把“有人”牌子自动摘掉。

👉 这就是 锁过期 + 进程卡顿 带来的并发进入问题。


4️⃣ 看门狗机制:自动续期

为了避免小明被赶出来,他带了一只“看门狗”,每隔 10 分钟就帮他去刷新牌子上的时间(PEXPIRE)。

👉 这就是 锁自动续期(watchdog)


5️⃣ 主从复制延迟:双重预订

厕所有两个管理员(Redis 主从)。

👉 这就是 Redis 异步复制带来的双持有问题

解决方法:

在计算机中,幂等(Idempotency) 指的是: 一个操作,无论执行一次还是执行多次,产生的效果都是一样的,不会因为重复调用而产生副作用。


🔹 日常生活类比

👉 这就是幂等:重复执行不影响最终结果

想象你要和银行(网站)打电话(建立连接),电话线(网络)不安全,可能有人窃听。于是你们约定了一个安全流程:

  1. 打招呼(Client Hello): 你告诉银行:「我支持 AES、RSA、TLS1.3 等加密方式,还有一个随机数 random1。」
  2. 回应(Server Hello): 银行说:「我选用 AES256 这种加密方式,这是我的数字证书(证明我是合法银行),再给你一个随机数 random2。」
  3. 验证证书: 你拿到银行的证书,去权威机构(CA 公钥)验证,确认证书有效且没被篡改,确保对方不是骗子。
  4. 生成密钥(Key Exchange)
    • 你和银行用 random1 + random2 + (可能再加 random3) 生成一个「会话密钥」。
    • 这个过程可能用 RSA 或者 Diffie-Hellman(ECDHE) 算法,保证别人即使偷听,也无法推算出密钥。
  5. 握手确认(Finished): 双方用刚才生成的会话密钥互相加密发一句:「OK,我准备好了」。 收到能解开,说明密钥一致,握手成功。
  6. 安全通信开始: 后续所有数据(HTTP 请求、响应)都用会话密钥对称加密传输。

底层实现 & 迭代器有效性(为啥会“失效”)

不同容器的迭代器,本质上指向“元素位置”。位置可能是:

典型规则(C++):

Java 的 fail-fast 原理(简化):容器维护 modCount,每个迭代器保存创建时的 expectedModCount;遍历过程中发现不一致就抛错。它不修复,只是快速发现“你边遍历边改”的不安全行为。

1. A – 原子性 (Atomicity)

👉 事务中的操作要么全部成功,要么全部失败。


2. C – 一致性 (Consistency)

👉 事务执行前后,数据库要从一个一致状态变为另一个一致状态(比如满足约束条件)。


3. I – 隔离性 (Isolation)

👉 多个事务之间相互隔离,避免相互干扰。


4. D – 持久性 (Durability)

👉 事务一旦提交,就必须永久保存,不会因为宕机而丢失。

🌟 为什么需要 MVCC?

如果只有锁(读锁、写锁),会导致:

👉 MVCC 的目标:让读操作几乎不阻塞写,写操作也几乎不阻塞读


⚙️ MVCC 的实现核心

在 InnoDB 中,MVCC 主要依赖以下机制:

  1. 隐藏列
    • 每行数据除了用户定义的字段,还有两个隐含字段:
      • DB_TRX_ID:最近一次修改这行的事务 ID
      • DB_ROLL_PTR:回滚指针,指向 Undo Log(历史版本)
    • 有时还会有 DB_ROW_ID(唯一标识行)。
  2. Undo Log(回滚日志)
    • 当事务修改一行数据时,会把旧值保存到 Undo Log。
    • 这样就能“回溯”到某个历史版本的数据。
  3. ReadView(读视图)
    • 一个事务在 执行查询时 会生成一个“视图”,记录当前活跃事务 ID 的范围。
    • 之后的查询就根据这个视图来决定能看到哪个版本的数据

📌 不同隔离级别下的 MVCC 表现


🔎 举个例子

假设表中有一条数据 balance=100,事务 ID = 10。

  1. 事务 A(ID=20)开始查询 balance,此时生成 ReadView,只能看到 ID≤10 的数据。
  2. 事务 B(ID=30)更新 balance=200,并提交 → 新版本写入数据行,旧版本保存到 Undo Log。
  3. 事务 A 再次查询时,依旧用最初的 ReadView → 只能看到旧版本 balance=100,而不会受事务 B 的影响。
  4. 新事务 C(ID=40)查询时 → 它的 ReadView 已包含 B 的更新,因此能读到 balance=200。

👉 这样就实现了 读写互不阻塞,读者能“看到属于自己时空的历史版本”。

MySQL 性能监控一般要从 数据库本身运行状态 + 业务访问情况 + 系统资源使用 三个维度来看。常见指标可以分几类:


1. 连接与线程相关


2. 查询与事务相关


3. InnoDB 存储引擎指标


4. 延迟与执行情况


5. 复制与高可用(主从架构时)

找到慢 SQL 后,用以下方法分析:

🔍 (1) EXPLAIN 执行计划

EXPLAIN SELECT * FROM orders WHERE user_id = 123 AND status = 'paid';

重点关注字段:

🔍 (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 引擎里)一个重要的优化手段。

定义

当一个索引包含了 查询中需要的所有列 时,这个索引就叫做覆盖索引。 换句话说,查询只需要通过索引就能拿到结果,不必回表(再去主键索引或数据页中取完整行)。

优点

使用场景

Elasticsearch 是一个分布式搜索和分析引擎,核心价值:


📖 生活化类比

你可以把 ES 想象成一个“超级图书馆检索系统”


🔎 ES 能做什么?

  1. 全文搜索(Full-text Search)
    • 模糊匹配、分词、相关性排序。
    • 例如:电商搜索框里输入“红色运动鞋”,ES 能智能拆分关键词并排序。
  2. 结构化检索(Filtering)
    • 精确过滤(比如:价格 < 200,品牌=NIKE)。
    • 和全文搜索结合,用来做复杂的搜索场景。
  3. 实时分析(Aggregations)
    • 类似 SQL 里的 GROUP BYCOUNTAVG
    • 例如:统计“每个城市今天新增的订单数”。
  4. 日志/监控/指标存储
    • 常见场景:ELK/EFK 日志系统(Elasticsearch + Logstash/Fluentd + Kibana)。
    • 把大量日志写入 ES,再通过 Kibana 可视化检索和分析。

异常(Exception) 大体分为三类:

1. 异常分类

  1. Checked Exception(受检异常)
    • 编译时必须显式处理(try-catch 或 throws)。
    • 比如:IOException, SQLException, ClassNotFoundException
    • 特点:通常是外部因素导致,程序本身无法完全避免。
  2. Unchecked Exception(非受检异常 / Runtime Exception)
    • 不需要编译期强制捕获。
    • 比如:NullPointerException, ArrayIndexOutOfBoundsException
    • 特点:大多是由代码逻辑错误引起。
  3. Error
    • JVM 无法恢复的严重错误。
    • 比如:OutOfMemoryError, StackOverflowError

RuntimeException 常见子类

RuntimeException 及其子类常见的有:

自定义业务异常继承哪个类,要取决于你希望它在编译期还是运行期被强制处理:


1. 继承 Exception(Checked Exception,受检异常)

public class BusinessException extends Exception {
    public BusinessException(String message) {
        super(message);
    }
}

2. 继承 RuntimeException(Unchecked Exception,运行时异常)

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) 数据库自增 / 发号器

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(雪花算法)

1. Twitter Snowflake(经典雪花算法)

背景:Twitter 最早提出的分布式 ID 算法,用 64 位 long 型 ID 来保证唯一性和趋势有序。

结构(64 位)

1位符号位 | 41位时间戳 | 10位机器标识 | 12位序列号

优点

缺点


2. 百度 UidGenerator(雪花算法的改进版)

背景:百度在雪花算法基础上改进,推出了 UidGenerator,优化了位分配和时钟回拨问题。

主要改进点

  1. 位结构可配置化
    • Snowflake 是固定的 41-10-12 分配。
    • 百度改成 可配置的时间位 + 工作节点位 + 序列位,适应不同业务。
    • 例如电商高并发可增大序列位,低并发长周期业务可增大时间位。
  2. 时钟回拨处理
    • Snowflake 一旦发生时间回拨,常见做法是阻塞或报错。
    • UidGenerator 支持 环形队列缓存 ID,即便出现时间小范围回拨,也能继续发号。
  3. 工程化支持
    • 提供 Spring Boot Starter,易接入
    • 机器号可通过数据库或 ZK 自动分配

优点:灵活、容错能力强、工程化好。 缺点:需要引入额外的缓存/队列,稍复杂。


3. 美团 Leaf

背景:美团点评内部开源的分布式 ID 生成服务,解决大规模分布式系统中的 ID 需求。

两种模式

  1. Leaf-segment(号段模式)
    • 利用数据库中的号段表,每次批量取一段 ID(如 1000 个),缓存在本地发号。
    • 优点:性能高(QPS 可到数十万),DB 压力小(批量更新)。
    • 缺点:依赖 DB,高可用需主从架构;ID 不严格单调(跨服务时可能有跳号)。
  2. Leaf-snowflake(雪花模式)
    • 改造版 Snowflake,利用 Zookeeper 分配 workerId,避免冲突。
    • 优点:本地生成,低延迟,无需频繁访问 DB。
    • 缺点:依赖 ZK 的可用性;时钟回拨仍需处理。

工程化特性

Snowflake 遇到时钟回拨时,为什么常见的做法是阻塞或直接报错?

这里的根源在于 Snowflake 的 ID 设计强依赖本地系统时间👇


1. Snowflake ID 的组成回顾

[1位符号][41位时间戳][10位机器号][12位序列号]

2. 时钟回拨为什么危险

如果系统时钟因为 NTP 校准、人工改动、硬件问题等被拨回:

例如:


3. 为什么选择“阻塞或报错”

这两种做法虽然会影响可用性,但它们保证了 ID 的正确性(唯一性 + 趋势有序性)

你问的就是「环形队列缓存 ID」到底怎么做。给你一份工程化落地方案(接近百度 UidGenerator 的思路),含原理 + 关键数据结构 + 生成/续桶流程 + 代码骨架。

核心思路(一句话)

不是临时现算一个 ID 再返回,而是按时间片(毫秒/秒)预先批量生成一大把未来可用的 ID,放进一个环形队列(ring buffer)*里;业务线程只需从队列里 take()。当时间*小幅回拨时,队列里还有“未来时间片”生成的 ID 可用,从而不中断发号

🍱 号段模式(Leaf-segment)

想象一下食堂打饭:

👉 在这里:

好处:发号非常快(内存操作),坏处:每次搬一筐会“跳号”,比如 A 窗口发到 1000,B 窗口发的是 2001,中间的 1001~2000 就没用了。


🕰️ 雪花模式(Leaf-snowflake)

再想象一个工厂生产流水号的机器:

👉 在这里:

好处:本地自己就能造号,速度极快,不用找“仓库”;坏处:如果时钟倒退(比如手表拨慢了),可能会打出重复的号。


🌟 总结对比

1. 号段的本质


2. 跳号是怎么发生的

👉 这就是“跳号”的根源:号段是一次性预分配的,没用完也不能退回去

1. LIMIT

-- 取出前 10 行
SELECT * FROM news ORDER BY created_at DESC LIMIT 10;

👉 这里即使表里有 100 万条新闻,结果只会返回 10 条。


2. OFFSET

-- 跳过前 20 行,取接下来的 10 行
SELECT * FROM news ORDER BY created_at DESC LIMIT 10 OFFSET 20;

👉 这表示“第 3 页的数据”(假设每页 10 条):


3. 两者关系

例如:

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)

-- 子查询只用覆盖索引拿本页主键
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) 必须“跳页”时

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);

三、索引与查询设计原则

1. OFFSET 的原理

原本的分页方式是:

SELECT * FROM post ORDER BY id ASC LIMIT 20 OFFSET 100000;

数据库内部会先扫描 100000 行、丢掉,再取 20 行。 所以页数越深,扫描越多,性能越来越差。


2. 替换为 “游标分页(键集分页)”

我们观察分页本质:

于是写成:

-- 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. 什么是“排序的确定性”


2. 为什么要加唯一键

如果我们写成:

ORDER BY created_at DESC, id DESC

👉 这就避免了分页时“相同时间戳的数据前后飘”的问题。


3. 为什么“唯一键放在最后”

在联合索引里,MySQL 按最左前缀原则排序:


4. 举个例子

假设表里数据是:

id created_at
1 2025-09-01
2 2025-09-01
3 2025-09-02

这样分页时不会丢记录或重复。

1. 什么叫“回表”

在 MySQL InnoDB 里:

当你用二级索引做查询时:

  1. MySQL 先在 二级索引里找到满足条件的主键 id;
  2. 再根据这个主键去 聚簇索引(主键索引) 里取整行。 👉 这个过程就叫 回表(back to table lookup)

2. 为什么“回表”会慢

下面给出一套可落地的“海量数据分布式排序 + 防倾斜”方案(框架无关,MapReduce/Spark/自研 gRPC 都通用)。

一、怎么在分布式里排好序(单机放不下)

总流程(Sample → RangePartition → Shuffle → Local Merge → Concatenate):

  1. 本地外排序(External Sort)
    • 每个节点把数据分块读入内存(≤ 可用内存),块内排序后落盘成有序 run
    • k 路归并把多个 run 合并成更大的有序段(仍可落盘)。
  2. 抽样选分界(Splitters)
    • 每个节点随机/均匀抽样 s 个 key(可从已排序 run 里等距抽样更准),汇总到协调者排序;
    • 选出 p-1近似分位数作为分界(i/p 分位),广播给所有节点。
  3. 按范围分区(Range Partition)+ Shuffle
    • 依据分界把本机有序段切成 p 个子段(区间不重叠),发给对应“归并节点”。
  4. 节点侧最终归并(Final Merge)
    • 每个归并节点把收到的多路已排序子段做堆归并,写出一个全局有序分片
  5. 拼接即全局有序
    • 分片 #0, #1, … 顺序拼接(或作为分区化的有序输出)。

关键点:局部 run 有序 + 全局按范围路由,最终每个分片内有序、分片之间按范围递增 ⇒ 全局有序。

读阶段:乐观锁通常不加锁(普通 SELECT,依赖 MVCC/快照读)。

写阶段:不提前占锁;在提交那一刻用“条件更新/版本校验”原子地写入。数据库为保证原子性会短暂拿行锁/闩锁,但这不是你在业务层显式加的“先锁再干活”的悲观锁。

ToC(面向消费者、上亿级并发)做分布式锁,和 ToB(内部系统、相对可控流量)相比,需要额外关注一堆“在大流量+强对抗+多地域”场景才会暴露的问题。下面给你一份工程化清单(带做法与参数建议),直接当评审/上线前的 checklist 用。

0. ToC vs ToB 的根本差异

2) 热点 Key & 惊群(thundering herd)

ToC 秒杀类请求会把同一个 lock:sku123 打爆,导致锁服务先被打死

缓解手段

注意:给热点 key 加“盐”分片并不能保持互斥(会破坏语义)。盐值只用于吞吐型操作,互斥必须单 key,因此前置分流与排队更关键。

关键差异 & 额外要点(ToC 必做)

  1. 热点键与倾斜
  1. 惊群效应(锁释放瞬间风暴)
  1. 租约续期与进程暂停
  1. 一致性模型选择
  1. TTL 与任务时长不确定
  1. 多机房/跨区域
  1. 公平性与饿死
  1. 限流与降级
  1. 键/值体积与连接管理
  1. 监控与 SLO
  1. 安全与多租户隔离
  1. 成本与演练

Redis的慢查询可能是由什么原因导致的

为什么会慢(4 类原因 + 例子)

  1. 命令层:一次性干太多
    • 大 key/全量:KEYS *LRANGE 0 -1HGETALL、超大 Z* 运算
    • 阻塞脚本:长时间 Lua
    • 删除大 key:同步 DEL 一卡全卡
  2. 服务层:单线程被“堵”
    • 持久化抖动:AOF fsyncBGSAVE/AOF rewritefork+COW
    • 内存/OS:Swap、开启 THP、过期/淘汰“雪崩”
    • 慢客户端:输出缓冲爆了,拖住事件循环
  3. 客户端/网络:来回跑太多
    • 没用 pipeline、连接池太小、跨机房高 RTT、value 超大序列化
  4. Cluster 场景:分片与复制带来的额外成本
    • MOVED/ASK 重定向频繁、单槽热点、从库落后、WAIT 等强确认

怎么办(对号入座)

怎么判断是网络波动问题还是key设置不合理问题

结论口诀端到端慢但服务端快=网络;服务端也慢且集中在某些命令/某些键=键设计问题

第 1 步|比“端到端”与“服务器执行”

第 2 步|看是否“命令/键”集中

第 3 步|排除“慢客户端”与复制/持久化干扰

容器/消费者宕机时,靠“未确认不算消费、重放+幂等/事务”来保证不丢不乱: 消息系统负责持久化+复制投递确认(ack/offset/visibility timeout),业务侧负责幂等/事务,二者配合实现最终一致,必要时做到“准 Exactly-once”。


三层机制(通用思路)

  1. 存储层(不丢)
    • 持久化:落盘/AOF、日志;
    • 复制/多数派:主从/仲裁(如 Kafka ISR、RabbitMQ Quorum Queue);
    • 生产确认:publisher confirm / acks=all,失败重试。
  2. 投递层(能恢复)
    • 未确认=可重投
      • 拉模型offset 未提交就视为未消费(Kafka);
      • 推模型未 ack会重投(RabbitMQ);
      • 可见性超时:到时未删即重投(SQS 的 visibility timeout);
      • Streamspending listXACKXCLAIM(Redis Streams)。
    • 重试与死信:最多 N 次→DLQ,便于排障与补偿。
  3. 消费层(不重/不乱)
    • 幂等处理:业务幂等键/去重表/唯一约束;
    • 事务性消费:读→处理→写库/产生新消息 要么都成功要么都回滚
      • 方案A:消费后提交 offset(至少一次)+ 幂等;
      • 方案B:事务性 outbox(本地事务写业务表与 outbox,再由 CDC 发消息);
      • 方案C(Kafka):Idempotent Producer + 事务(EOS),将“下游写 + offset 提交”放进同一事务。

一、创建型(解决“对象怎么创建更合理”)

二、结构型(解决“对象如何组合更灵活”)

三、行为型(解决“对象之间如何协作更优雅”)

代理模式(Proxy):在不改变目标对象的前提下,放一个“代理对象”在前面,控制访问附加通用能力(鉴权、缓存、限流、事务、日志、远程调用等)。

Client ──> Proxy ──(前后织入能力)──> RealSubject

能解决什么问题

#

30 秒速答

在地址栏输入 URL 回车后:(可能先命中缓存/Service Worker)→ DNS 解析 → 选协议与建连(TCP/TLS 或 QUIC)→ 发 HTTP 请求 → 经 CDN/反向代理到应用 → 服务器生成响应 → 浏览器收流并渲染(解析 HTML/CSS/JS、布局绘制、请求子资源)→ 连接复用与缓存落盘。期间涉及 HSTS/HTTPS 升级、证书校验、Cookie、缓存协商、CORS、压缩与分片传输 等。


分步说明(简洁版)

  1. 输入与导航判定
    • 浏览器判断是搜索词还是 URL;规范化 URL,若命中 HSTS 规则直接升为 https://
    • 先查 浏览器缓存 / Service Worker:若命中,直接返回或走 SW 的 fetch 逻辑(可离线)。
  2. DNS 解析
    • 走浏览器/系统/hosts/本地缓存 → 递归解析器 → 权威 DNS,得到 IP 与端口;HTTP/2/3 会用 ALPN 协商协议。
    • 有代理则按代理解析;也可能用 DoH/DoT
  3. 建连与安全
    • HTTP/1.1/2:TCP 三次握手 → TLS 握手(SNI 指定域名、证书链校验、OCSP stapling、ALPN 协商 h2/h1)。
    • HTTP/3:直接 QUIC(UDP)+TLS1.3,可 0-RTT。
    • 建立后进入连接池,可复用/多路复用。
  4. 发起请求
    • 组装请求行与请求头:Host/Accept/Accept-Encoding(User-Agent)/Cookie/Referer/Origin 等;必要时先做 CORS 预检
    • 可能带 ETag/If-None-Match / If-Modified-Since 做缓存协商。
  5. 中间层转发
    • 流量通常先到 CDN/WAF/负载均衡 → 反向代理(Nginx)→ 应用服务。
    • CDN 命中直接回源;未命中向源站拉取。
  6. 服务器处理
    • 依据 Host/SNI 选虚拟主机,路由到应用;读缓存/DB/下游服务;生成响应与 状态码、头Cache-Control/Set-Cookie/CSP)及实体;按需 gzip/brotli分块传输
  7. 浏览器接收与渲染
    • 收到响应:若 304 用本地缓存;否则写入磁盘/内存缓存并交给渲染进程。
    • 解析 HTML → DOM,并行拉取子资源;解析 CSS → CSSOM(阻塞渲染);JS 执行可能阻塞解析(defer/async 优化)。
    • 生成 Render Tree → 布局 → 绘制 → 合成;后续交互走事件循环。
  8. 后续与优化
    • 连接 keep-alive/HTTP2 多路复用预解析/预连接/预加载dns-prefetch/preconnect/preload);将来访问走缓存/复用连接。

Kafka 的索引结构(按分区)

每个 分区 被切成多个 段(segment),每段是一组并列文件:

00000000000000000000.log         # 真实数据(顺序追加)
00000000000000000000.index       # 偏移索引:offset -> 物理位置pos(稀疏)
00000000000000000000.timeindex   # 时间索引:timestamp -> offset(稀疏)
00000000000000000000.txnindex    # 事务索引:记录中止事务范围(仅事务主题)

常见消息队列索引是不是都一样?

不一样。各家根据目标侧重点,索引设计差异很大:

系统 存储与索引核心 典型能力/取舍
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) Java 的线程怎么运行?

一句话:Java Thread对操作系统线程的 1:1 封装;调用 start() 后由 JVM 创建原生线程,交给 OS 调度,执行你覆写的 run()


2) Java 线程池底层原理(ThreadPoolExecutor

一句话先用现有线程,再排队,排满后再扩容到最大;还不行就拒绝

面试金句“先线程、后排队、再扩容,最后拒绝;任务循环在 runWorker,状态用一个原子 ctl 管。”


3) MyBatis 有什么作用?

一句话:MyBatis 是“以 SQL 为中心”的持久层框架,把 SQL 与 Java 对象映射起来,简化 JDBC。


4) MyBatis 的缓存架构(面试高频)

一级缓存(本地缓存)

二级缓存(Mapper/命名空间级)

面试金句“一级会话级、二级命名空间级;二级在 commit 才写入,任何写操作会清掉命名空间缓存。MyBatis 的缓存是‘可用但要敬畏’,强一致别硬开。”

常见进程间通信(IPC)方式

本机内核中转(拷贝型)

共享内存(零拷贝,需自管同步)

跨机器/通用

一、为什么分库?(库=实例/集群维度的水平拆分)

要解决:吞吐、容量、隔离与可用性

记忆点:分库=横向扩展系统能力 + 降低故障半径


二、为什么分表?(表=单实例内的数据粒度拆分/分区)

要解决:单表过大导致的性能与运维问题

常见策略:

记忆点:分表=让“单表可管理、可快跑”;注意它不等同于 MySQL 的 partition table(后者仍在一库一引擎文件里)。


三、分库 vs 分表(一句话对比)

\

TCP 的可靠性靠这几件事一起完成:

  1. 序号 + ACK(累计确认):每个字节有序号,收到了就发 ACK,没被确认的都在重传队列里。
  2. 重传机制
    • 超时重传(RTO 基于 RTT 估计,自适应回退);
    • 快速重传(收到 ≥3 个重复 ACK 立即重传);
    • SACK 选项精确告知“哪些块到了”,减少无谓重传。
  3. 滑动窗口:按对端通告的窗口发送,保证不丢/不淹接收端(流量控制)。
  4. 乱序重排/去重:接收端按序号缓存乱序段,去重后按序交付上层。
  5. 校验和:每段带 TCP 校验和(含伪首部),发现比特错误就丢弃并促使重传。
  6. 连接管理:三次握手生成随机初始序号、四次挥手关闭,避免老包混入新连接。 (拥塞控制不直接“修正数据”,但通过慢启动/拥塞避免/快恢复,降低丢包→间接提升可靠性。)

1)dupACK 是谁的 ACK?

dupACK(重复 ACK)是接收端(正在收你数据的那一方)发回来的 “累计确认号相同的 ACK”

2)为什么多次收到 dupACK ⇒ “后续数据到了,唯独某段丢了”?

因为 TCP 规范要求:当接收端收到乱序报文段(也就是缺口之后的更高序号的段)时,必须立刻回一个 ACK,且 ACK 号仍是“缺口起点”,以此催促发送端把缺的那段重传;每来一段新的乱序数据,就再回一次同样的 ACK —— 于是发送端看到一连串相同 ACK 号,这就是 dupACK

令牌桶是什么(10 秒版)


为啥会有“暂时峰值”

当系统空闲了一会儿,桶里攒满了 B 个令牌;新流量一来,可以瞬间以线路速率把这 B 个请求立刻放走,平均速率仍受 r 限制,但瞬时有突发


不想要峰值,怎么改?(四种常用思路)

按“从容易到严格”排列,你面试时说 2~3 个即可:

  1. 把桶变小/关掉突发

    • 做法:把 B 调小(甚至设 B=1)。
    • 结果:最多只允许1 个立即通过,其余按 r 速度放行或等待/丢弃。
    • 代价:对抖动不友好,容易造成不必要的等待/丢弃。
  2. 改成“漏桶”(Leaky Bucket)——固定速率出水

    • 思想:排队 + 匀速出队(严格按 r 发出),没有“攒满再一下子放”。
    • 结果:完全消峰、输出平滑;
    • 代价:需要队列(时延↑),队满就丢。
  3. 用“节拍/配速”的令牌桶(GCRA/虚拟时钟)

    • 思想:不靠“积攒 B 个令牌”,而是为下一次允许通过计算一个时间点,到点才放行。

    • 伪代码:

      next = 0
      on request:
        now = monotonic_now()
        next = max(next + 1/r, now)
        sleep_until(next)  // 或忙等/排队
        allow
      
    • 效果:请求按间隔 1/r 均匀通过,几乎没有突发;

    • 这个就是电信标准里的 GCRA(令牌桶的严谨等价实现)。

  4. 把“允许就立即放行”改为“等待拿令牌”(阻塞式)

    • 许多语言自带的限速器既支持 Allow()(可能突发),也支持 Wait()配速通过)。
    • 例如 Go:rate.NewLimiter(rate.Every(1/r), burst=1) 并用 Wait(ctx),基本无突发。

设计一个日志查看的系统,要求可以根据关键词检索该条日志,并确定是哪个机器上的日志,设计一下几个模块

整体架构(模块分层)

采集层(Agent/Shipper)

入口层(Collector API / Edge Gateway)

管道层(Parse & Enrich)

索引与存储层

查询服务(Search API)

界面(Log Viewer)

运维与治理

1) goroutine

2) 调度器 G-P-M

3) channel(语义与用法)

4) 同步与内存模型(JMM for Go)

二、底层怎么做到(三套“工具箱”)

1)锁 + 两段锁(2PL)

2)MVCC(多版本并发控制)

3)SSI(Serializable Snapshot Isolation,可串行化快照隔离)


三、按级别逐个说“怎么实现的”

Read Uncommitted

Read Committed

Repeatable Read

Serializable

Redis 的有序集合(ZSET)底层有两种实现,按规模自动切换:

  1. 紧凑编码(小集合):一段连续内存里的紧凑表
    • 现在用 listpack(早期版本叫 ziplist)。
    • 适用于元素个数、成员长度都很小的 ZSET,节省内存。
    • 到了阈值会自动“升格”。
  2. 跳表(skiplist)+ 字典(hash/dict)的组合(大集合)
    • dictmember -> score,O(1) 查找/更新分数。
    • skiplist:按 (score, member) 有序,用于范围查询/排名,O(log N) 插入、删除、按分数区间遍历。
    • 两份结构同时维护:新增/改分数时先在 dict 查,再在 skiplist 插/删,保持一致。

为什么要“跳表 + 字典”两把刷子?

  • 字典让“按成员查分数/改分数”是 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)

著名的 死锁产生的四个必要条件(互斥、占有并等待、不可剥夺、循环等待)

  1. 互斥条件 资源一次只能被一个线程/进程占用。 例如:打印机只能同时被一个进程使用。
  2. 占有并等待 一个进程已经持有了某些资源,同时又在等待其它资源。 例如:线程 A 占有锁 L1,等待 L2。
  3. 不可剥夺 资源不能被强制夺走,只能由持有它的进程主动释放。
  4. 循环等待 存在一个进程循环等待链。 例如:
    • A 等待 B 占有的锁,
    • B 等待 C 占有的锁,

G1 如何处理大对象

  1. 直接分配到 Humongous Region
    • 如果对象大小 ≥ 半个 Region,就不放在 Eden 里,而是放在 一连串连续的 Region 中。
    • 例如:Region 大小 4MB,一个 10MB 的对象会占用 3 个连续的 Region。
  2. 回收方式
    • Humongous Region 会被当作 老年代的一部分 来管理。
    • 回收时主要依赖 全局并发标记周期(Mixed GC) 来识别垃圾。
    • 如果整个大对象没被引用,整片 Region 会被回收。
  3. 碎片问题
    • 大对象需要连续的 Region,如果堆被切得比较碎(空 Region 不连续),可能会触发 Full GC 来做压缩整理。

三、为什么要这样设计

一、线程池的作用

简单来说,线程池就是提前创建好一批可复用的线程,放在池子里统一管理。 当有任务时,直接从池里取空闲线程执行;任务完成后,线程回到池中等待下一次使用。

👉 作用:


二、线程池的好处

1. 性能提升

2. 控制并发数量

3. 任务管理能力

4. 统一调度和监控

Redis 的 有序集合(ZSet) 底层用 两种数据结构组合

  1. 哈希表(dict)
    • key = member
    • value = score
    • 用来快速根据元素找到分数,复杂度 O(1)
  2. 跳表(skiplist)
    • 按 score 有序存储所有元素。
    • 用来实现区间查找、排序、排名,复杂度 O(logN)
    • 跳表节点里既存 score,也存 member。

👉 这两者组合在一起:

扩容过程(Redis Cluster 为例)

当要扩容(新增节点)时,流程大致是:

  1. 加入新节点

    • 启动 Redis 实例,执行:

      redis-cli --cluster add-node <new_ip>:<port> <existing_ip>:<port>
      
    • 新节点会加入集群,但暂时没有槽(slot)。

  2. 重新分配槽(slot rebalancing)

    • 集群总共 16384 个槽,扩容时需要把一部分槽迁移给新节点:

      redis-cli --cluster reshard <any_cluster_node_ip>:<port>
      
    • 输入要迁移的槽数、新节点 id、来源节点。

    • 槽的迁移是在线进行的,业务不中断。

  3. 迁移数据(slot migration)

    • Redis 会逐个 key 从源节点搬到目标节点。
    • 客户端在迁移时可能遇到 MOVEDASK 重定向,客户端驱动会自动重试。
    • 迁移完成后,新节点就开始对外提供服务。

1)准备阶段:给源/目标节点打标

对要迁移的每个槽 s,工具(redis-cli --cluster reshard 或其它运维工具)会先:

含义:

  • 源上 MIGRATING:这个槽里的 key 未来要走;如果有针对这些 key 的写操作打过来,源会用 ASK 临时重定向到目标。
  • 目标上 IMPORTING:允许临时接收来自该源的写(需要客户端先发 ASKING)。

2)批量搬钥匙:GETKEYSINSLOT + MIGRATE

真正的数据迁移是按 key 批量搬的(不停机靠的就是“以 key 为粒度”的细粒度迁移 + ASK 临时重定向):

循环执行直到该槽里已无 key。

小贴士:生产里一般控制 batch 大小和超时,避免长时间占用 IO 造成抖动。


3)收尾:宣布槽位归属变更(MOVED 生效)

当槽 s 的 key 都迁走了,运维工具会在集群所有节点上广播最终归属:

CLUSTER SETSLOT s NODE <targetNodeId>

至此,槽位的“权威映射”完成;之后客户端命中槽 s 会收到永久重定向 MOVED(而不是临时的 ASK)。

Redis 的性能瓶颈在哪里

虽然单线程很快,但在特定场景下还是有瓶颈:

  1. CPU 瓶颈
    • 当请求量非常大、操作逻辑复杂(如 Lua 脚本、大量聚合操作、慢查询 SORT/ZRANGE)时,单线程 CPU 可能打满。
  2. 内存带宽
    • Redis 数据全在内存,受限于内存大小和带宽。
    • 大量数据迁移、持久化(RDB/AOF rewrite)会占用带宽。
  3. 网络 I/O
    • Redis 的吞吐在高并发场景下会受到网络带宽限制(尤其是 10Gbps 网卡上)。
  4. 持久化开销
    • AOF 重写、RDB 快照时,会占用磁盘 I/O 和 CPU,可能导致主线程阻塞或延迟。
  5. 大 key/大对象
    • 如果一个 key 特别大(比如几 MB 的 Hash 或 List),单次读写会阻塞主线程,造成卡顿。

什么是间隙锁(Gap Lock)

例如:表里有索引值 1020,如果某个事务对 10 加了间隙锁,那么 (10,20) 之间就不能插入新记录。

为什么需要间隙锁

如果某行被加了“间隙锁”,能不能 UPDATE 这行?

ThreadLocal 的作用

👉 它不是用来解决多线程共享问题,而是避免共享


二、ThreadLocal 的核心原理

  1. 每个线程(Thread 对象)里都维护一个 ThreadLocalMap
    • 这个 ThreadLocalMap 是一个哈希表,key 是 ThreadLocal 对象,value 是实际存的值。
    • 所以变量数据不是存在 ThreadLocal 里,而是存在线程自己的 Map 里。
  2. ThreadLocal 提供访问入口
    • 调用 threadLocal.set(value) 时:
      • 获取当前线程(Thread.currentThread())。
      • 从线程里拿到 ThreadLocalMap
      • (threadLocal, value) 存进去。
    • 调用 threadLocal.get() 时:
      • 从当前线程的 ThreadLocalMap 里查到自己对应的 value。
  3. 隔离性原理
    • 因为每个线程有自己独立的 ThreadLocalMap,所以即使是同一个 ThreadLocal 对象,不同线程读到的值也不同。

ZAB 协议是什么


三、ZAB 协议的两个阶段

1. 崩溃恢复阶段(Leader 选举)

2. 消息广播阶段(正常工作)

👉 这就是 原子广播:要么所有节点都应用,要么都不应用。

第三次握手能不能传输数据?

👉 可以,但一般不这么做。

原因:

网关限流 + 熔断设计速查表(适用于 Nginx+OpenResty/Kong、Envoy/Istio、Spring Cloud Gateway+Resilience4j 等)。


目标与分层


限流(Rate Limiting)

选算法(建议)

策略层次

分布式实现

熔断(Circuit Breaking)

状态机

触发信号(任取其一或组合)

半开与恢复

配套策略

实战参数(默认模板)

监控与运营


快速起步的“默认策略”

Redis 的 Lua 脚本执行失败时,不会回滚已执行的部分


详细解释

  1. Redis Lua 脚本的执行模型
    • EVALEVALSHA 提交的脚本,会在 Redis 单线程事件循环中一次性执行完毕。
    • Redis 把脚本看作“一个整体的命令”,在执行过程中,不会被其他命令打断。

1. 队头阻塞(Head-of-Line Blocking, HoL)

定义:在一个按顺序传输的数据通道里,如果前面的数据包没到,后面的数据包就算已经到了也不能被处理 → 就像“高速路口前面一辆车堵住了,后面的全得停下”。

在 TCP/HTTP 里的表现

后果:弱网下,单个丢包就能卡住多个请求/响应,延迟被放大。


2. 慢建连(Slow Connection Establishment)

定义:建立一个安全可靠的连接,需要经历多次“握手”,每次握手至少消耗一个 RTT(Round Trip Time,往返时延)。

在传统 TCP + TLS 下的流程

  1. TCP 三次握手:客户端发 SYN → 服务端回 SYN+ACK → 客户端回 ACK
  2. 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”

HTTP/3/QUIC 怎么解决

代价与坑

QUIC(Quick UDP Internet Connections) 是 Google 最初提出、后来 IETF 标准化的一种 基于 UDP 的传输层协议。 它的目标是:替代 TCP + TLS + HTTP/2 的组合,把传输层和安全层整合在一起,同时解决 TCP 的一些固有问题。

换句话说:


QUIC 的核心特性

  1. 跑在 UDP 之上
    • 使用 UDP 作为“载体”,这样操作系统和网络设备都不会把它当作 TCP 处理。
    • 在 UDP 上重新实现了可靠传输、流量控制、拥塞控制。
  2. 集成 TLS 1.3
    • QUIC 自带加密,所有数据都必须加密。
    • 握手更快(1-RTT,甚至 0-RTT),比 TCP+TLS 少一个往返延迟。
  3. 多路复用无队头阻塞
    • 在 TCP 中,一个包丢了会卡住所有流。
    • 在 QUIC 中,每个流独立排序和重传,一个流丢包不会影响别的流。
  4. 连接迁移
    • QUIC 用 Connection ID 来标识连接,而不是传统的“四元组(IP+端口)”。
    • 这样即使设备从 Wi-Fi 切到 4G,连接也能保持,不必重新建立。
  5. 可进化性强
    • TCP 协议在内核中实现,要更新协议需要操作系统支持。
    • QUIC 在用户态实现,迭代速度更快(Google Chrome、Cloudflare 都在频繁更新)。

分类角度(RFC 标准常用分类)

HTTP Header 大体分成四类:

  1. 通用首部(General Header)
    • 请求和响应都会用到的字段
    • 示例:
      • Date:报文生成的时间
      • Connection:连接管理(如 keep-aliveclose
      • Cache-Control:缓存策略(如 no-cache, max-age=3600
  2. 请求首部(Request Header)
    • 由客户端发送,描述客户端环境、期望的响应格式等
    • 示例:
      • Host:请求的主机名和端口
      • User-Agent:客户端类型(浏览器/设备信息)
      • Accept:客户端能处理的响应类型(如 text/html, application/json
      • Authorization:认证信息(如 Bearer token
  3. 响应首部(Response Header)
    • 由服务器返回,描述服务器信息、响应相关的元数据
    • 示例:
      • Server:服务器软件信息(如 nginx/1.20
      • Set-Cookie:设置 Cookie 给客户端
      • Location:重定向目标地址(常见于 302/301)
  4. 实体首部(Entity Header / Representation Header)
    • 用来描述报文 body 的内容
    • 示例:
      • Content-Type:内容类型(如 application/json; charset=UTF-8
      • Content-Length:响应体的字节长度
      • Content-Encoding:编码方式(如 gzip, br)
      • Last-Modified:资源最后修改时间

0. 先判断“谁”在烧 CPU

机器层面

把 LWP 映射到 Java 线程

备选:jcmd 12345 Thread.print 也能打出线程堆栈(更快)。


1. 快速判断“CPU 高的类型”

A. 用户态计算忙(真算力) 堆栈多在你的业务代码里(计算、JSON/序列化、正则、解压、加密、数学循环、排序、流处理等)。

B. 自旋 / 锁竞争 / 阻塞切换频繁 堆栈显示 Unsafe.parkReentrantLock.locksynchronized 竞争、ForkJoinPool 繁忙、自旋重试等。

C. GC 导致的 CPU 高 GC ThreadG1 ConcVM Thread 明显;jstat -gcutil 12345 1s 10 看到频繁 GC、CPU 飙升。

D. JIT/类加载抖动 刚启动或热点切换时,C2 CompilerThread 忙;可暂时属于“暖机期”。

E. JNI/Netty/IO 相关 栈里出现 JNI 调用、压缩/解压库、Netty NioEventLoop 紧张等。


2. 一分钟“止血”手段(不重启)

JFR/async-profiler 不会像 jstack 那样只给瞬时状态,而是采样分布,更靠谱。


3. 系统性定位步骤

  1. 确认是否 GC 过高
  1. 线程热点
  1. 锁竞争
  1. IO/网络
  1. 内核/系统层

4. 常见“元凶 → 修复方案”

聊聊你对 MCP 的理解

核心定义

为什么需要 MCP

1. 为什么需要意向锁?

数据库里既有 行锁(细粒度,锁某一行数据)也有 表锁(粗粒度,锁整张表)。 问题是:

所以引入 意向锁:在表级别放一个“意图标记”,表示“我这个事务在表的某些行上加了行锁”。这样数据库在需要加表锁时,只要检查意向锁,不需要逐行扫描。


2. 意向锁是什么?

这些意向锁都是加在表上的,本身不会阻止别的事务读写具体的行,但会让表锁的获取受到约束。


3. 解决的问题


4. 举例

事务 A:想更新一行 → 给这行加 X 锁,同时在表上放一个 IX 锁。 事务 B:想给整张表加 S 表锁(全表读)。

如果没有意向锁,B 就得扫全表,确认有没有行被 X 锁住,非常低效。


总结一句话面试用意向锁是表级锁,用来表明事务在表中某些行上加了行锁。它的作用是解决表锁与行锁之间的兼容检测问题,使数据库能快速判断是否能安全地加表锁。


2. 出现场景

超卖的根因


2. 常见解决方案

方案一:数据库层唯一约束


方案二:加锁(串行化用户操作)


方案三:乐观锁(版本号/库存扣减)


方案四:异步订单队列


方案五:缓存/限流保护


3. 最佳实践组合

总体策略(设计之初就要埋好钩子)

  1. 默认只做“可加不减”的演进
    • 字段只“新增可选(optional)”,不删除、不改语义、不改类型/含义。
    • 避免必填字段后加(会破坏旧客户端)。
  2. 版本化(Versioning)
    • 资源/接口版本/v1/...Content-Type: application/vnd.foo.v1+json;gRPC/Proto 用包名/服务名标版本。
    • 数据模型版本:响应里携带 schema_versionapi_version 便于客户端容错。
    • 渐进弃用(Deprecation):加 Deprecation/ Sunset header 或文档标注,给出迁移窗口与替代方案。
  3. 宽进严出(Tolerant Reader)
    • 客户端忽略未知字段服务端返回时保证字段稳定、类型不变、默认值清晰。
    • 错误码与枚举允许新增值,客户端用“unknown/OTHER”兜底。
  4. 幂等与可重放
    • 写操作支持 幂等键Idempotency-Key)或业务唯一键;分页用游标(cursor)而非 pageNo,保证变更后结果稳定。
  5. 能力协商(Feature negotiation)
    • capabilities/flags 让客户端声明已支持的扩展,服务端据此开启新字段或行为(前向兼容)。
  6. 稳定排序与字段默认
    • 序列/列表输出顺序要么声明“不保证”,要么明确排序规则,避免升级后顺序抖动。
    • 对新增的可选字段给清晰默认值空值语义null vs 缺省)。

1. 什么是 API?

例子


2. 什么是 SPI?

例子


3. 区别总结

对比项 API SPI
面向对象 应用开发者 框架扩展/第三方实现者
调用方向 应用 → 框架/库 框架/库 → 应用提供的实现
目标 稳定、易用 扩展、可插拔
生命周期 编译期直接依赖,运行时直接调用 编译期依赖接口,运行时发现/加载实现
使用方式 直接 import 调用 META-INF/services/ 下注册实现类
场景举例 SDK 提供的函数、类库调用 插件机制、驱动发现、日志实现、序列化框架

1. 加密目标

无论 SSL 还是 TLS,本质都想在 TCP 之上提供:


2. 核心思路

  1. 公钥加密/密钥交换:在握手阶段安全地交换对称密钥。
  2. 对称加密:会话期间用同一个会话密钥加密所有数据(快)。
  3. MAC/哈希:对每个报文做完整性校验。

3. SSL(以 SSL 3.0 为例)的握手加密流程

  1. 客户端 Hello

    • 告诉服务端自己支持的 SSL 版本、加密套件、随机数。
  2. 服务端 Hello

    • 确认协议版本和加密套件,返回证书(含公钥)、随机数。
  3. 客户端生成 Premaster Secret

    • 用服务端证书的公钥加密后发给服务端。
  4. 双方计算 Master Secret

    • 客户端和服务端用:

      MasterSecret = PRF(Premaster, ClientRandom, ServerRandom)
      
  5. 生成对称密钥

    • 从 Master Secret 推导出对称加密和 MAC 的密钥。
  6. 交换“Finished”消息

    • 表示后续都用对称加密。

👉 SSL 3.0 主要问题:依赖弱算法(RC4、MD5)、消息认证用的 MAC 容易被攻击。


4. TLS(以 TLS 1.2 为例)的握手加密流程

TLS 基本继承 SSL,但做了增强:

  1. ClientHello:支持 TLS 版本、加密算法列表、随机数。
  2. ServerHello:确认算法,返回证书、公钥、随机数。
  3. 密钥交换(两种方式):
    • RSA:客户端生成 Premaster,用服务端公钥加密发给服务端。
    • DH/ECDHE:双方协商临时密钥,支持 Perfect Forward Secrecy (前向保密)。
  4. 双方计算 Session Key(会话对称密钥)。
  5. Finished:验证握手完整性,进入对称加密通信。

👉 TLS 的改进:


5. TLS 1.3 的优化(现行主流)

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 是什么?


2. 核心特性

  1. 异步非阻塞:基于 Reactor 模型,能同时处理成千上万连接。
  2. 事件驱动:用事件和回调处理 IO,解耦读写逻辑。
  3. 高性能:零拷贝(ByteBuf)、内存池、批量处理。
  4. 跨平台:支持多种传输实现(NIO, Epoll, KQueue)。
  5. 可扩展:基于 Pipeline 的责任链模式,用户可以灵活定制协议、编解码、业务逻辑。

3. 核心组件


4. Netty 的工作原理(简化)

  1. BossGroup(EventLoopGroup):接收客户端连接。
  2. WorkerGroup:处理具体的读写事件。
  3. ChannelPipeline:消息经过一串 Handler(解码、业务处理、编码)。
  4. 异步执行:IO 操作返回 Future,真正完成后触发回调。

👉 这就是典型的 Reactor 多线程模型


5. 应用场景

什么是 MySQL 主从延迟


2. 为什么会有延迟(常见原因)

  1. 复制原理本身有异步
    • 主库写 binlog → 传输到从库 → 从库应用到 relay log → 执行 SQL。
    • 这中间有天然的“队列”和网络传输延迟。
  2. 从库性能差/压力大
    • 从库机器配置低于主库,或者从库读请求太多,导致执行速度跟不上。
  3. SQL 本身耗时
    • 大事务、复杂查询(慢 SQL)、DDL 操作。
  4. 单线程复制(MySQL 5.6 之前)
    • 从库只能单线程按顺序执行 binlog,遇到慢事务就会拖慢整体。
    • 虽然 MySQL 5.7+ 支持多线程复制,但在表/库分配上仍有限制。

3. 怎么解决主从延迟

(1)架构层面

(2)MySQL 参数与机制

(3)SQL 优化

1. 写扩散(Fanout-on-Write)


2. 读扩散(Fanout-on-Read)

读扩散 + 推送通知的区别

读扩散 体系里,消息只存一份(群 timeline)。

👉 所以,ThreadLocal 的本质是:在每个线程自己维护一份独立的变量副本

为什么 ThreadLocalMap 的 key 用 弱引用(WeakReference)

原因:防止内存泄漏

举个例子:

微服务之间调用超时了你会怎么处理

超时处理 → 重试机制 → 降级策略 → 工程化保障 这四个方面来回答。


1. 超时处理(第一道防线)


2. 重试机制


3. 降级策略(第二道防线)

当超时/失败率过高时,调用方要有备用方案,保证核心流程可用。常见手段:

  1. 缓存降级
    • 从本地缓存 / Redis 返回上一次结果或兜底数据。
    • 适合配置查询、推荐、商品列表等对实时性要求没那么高的场景。
  2. 静态兜底
    • 直接返回默认值 / 友好提示。
    • 例如:“推荐服务不可用,先看看热榜”。
  3. 业务降级
    • 非核心链路关闭或弱化。
    • 例如秒杀流量过大,临时关闭个性化推荐,只保留下单服务。
  4. 异步补偿
    • 请求先写消息队列,异步慢慢处理,前端给用户“稍后到账”的提示。
    • 常见于支付、积分、消息通知等场景。

4. 工程化保障

这是消息队列设计的必考点。要点是 怎么重试、怎么避免重复和雪崩、怎么保证业务正确性。我分几层讲:


1. 为什么会消费失败?

所以要区分 可重试错误不可重试错误


2. 重试机制设计

(1)立即重试(同步)

(2)延迟重试(推荐)

(3)最大重试次数


3. 需要注意的问题

  1. 幂等性
    • 消息可能被多次投递 → 消费者必须支持幂等。
    • 常见做法:唯一业务 key(如 orderId)+ 数据库唯一约束 / Redis SETNX
  2. 重试间隔
    • 不能一失败就疯狂重试,会造成 雪崩
    • 一般用 指数退避 + 随机抖动:如 1s → 2s → 4s → 8s…
  3. 消息顺序性
    • 如果消息有顺序要求,重试时要特别注意可能打乱顺序。
    • 常见做法:按分区/Key 投递,或用串行消费。
  4. 死信队列 (DLQ)
    • 重试超过阈值的消息必须有去处。
    • 避免丢消息,同时给排查入口。
  5. 可观测性
    • 失败率、重试次数、DLQ 堆积量要有监控和报警。

4. 设计举例(RocketMQ)

1. JVM 调优常关注的参数

主要分成 堆内存、垃圾回收、线程栈、诊断工具几类:

(1)堆内存相关

(2)GC 相关

(3)线程与方法区

(4)诊断和观测


2. 如何判断是不是内存泄漏

内存泄漏的本质:对象已经没有业务用途,但仍然被引用着,无法被 GC 回收。

判断方法:

  1. 监控内存曲线(JVM 监控工具如 JConsole、VisualVM、Prometheus + Grafana)
    • 正常情况:堆使用量 锯齿状上升又下降(GC 后能回落)。
    • 内存泄漏:堆使用量 持续上升,Full GC 后也不能明显下降。
  2. 观察 GC 日志
    • 正常:Full GC 能释放大部分老年代对象。
    • 泄漏:Full GC 之后老年代占用依然很高,越来越接近 -Xmx
  3. Heap Dump 分析
    • jmap -dump:live,format=b,file=heap.hprof <pid> 导出堆。
    • 用 MAT(Memory Analyzer Tool)或 VisualVM 分析:
      • 查看 占用最多的对象
      • 检查 GC Roots 路径(是谁引用着它不放)。
    • 如果发现无用对象仍被静态集合、缓存、线程本地变量(ThreadLocal)持有,就是泄漏。
  4. 常见泄漏场景
    • static Map/List 不断往里加元素,不清理
    • ThreadLocal 忘记 remove()
    • 连接/句柄(JDBC、IO、Netty buffer)未关闭
    • Listener/回调注册后未释放

1. 切片的底层实现

在 Go 里,slice 不是数组本身,而是一个描述符(slice header),包含三个字段:

type slice struct {
    ptr *T   // 指向底层数组的指针
    len int  // 当前切片的长度
    cap int  // 底层数组的容量
}

👉 所以,slice 是一个“引用类型”,但它本质上是一个小结构体,存的只是指针和元信息。


2. 函数传递时的表现

channel 的基本结构

在 Go 的 runtime 里,channel 是一个结构体(hchan),核心字段包括:

Go 的垃圾回收分为 标记(mark)清除(sweep) 两个阶段,属于 标记-清除(Mark-Sweep)算法。流程如下:

  1. STW(Stop The World)开始
    • 暂停用户程序(mutator),收集 GC roots(栈、全局变量、寄存器等可达对象)。
    • 这是短暂的 STW,用来启动一次 GC 循环。
  2. 标记阶段(Mark)
    • 使用 三色标记法:对象会经历 白色 → 灰色 → 黑色 的转换。
    • 从 root 开始,把直接可达的对象染成灰色,进入标记队列。
    • 运行 GC worker goroutines 与 mutator 并发执行:
      • worker 从灰色队列取对象,把它们的子对象染灰,并将自己染黑。
      • 最后灰色队列为空,黑色对象即“存活”,白色对象即“不可达”。
    • 写屏障(write barrier): 在标记阶段,mutator 仍可能修改指针,导致“丢失可达性”。Go 在写操作时插入屏障逻辑,保证新引用的对象也能被染灰。
  3. STW(End)
    • 再次短暂停顿,清理剩余的标记队列,确保没有遗漏。
  4. 清除阶段(Sweep)
    • 扫描堆内存,把未标记(白色)的对象回收。
    • 清除是分批次执行的,避免长时间阻塞。

2. 三色标记法(核心思想)

GC 目标:结束时没有灰色对象,白色对象就是垃圾,黑色对象是存活的

所谓 主从同步策略,本质上是指主库写入 binlog 后,从库如何接收并应用日志。常见的几种策略主要围绕 同步时机 / 等待程度 / 数据一致性 来划分。


1. 异步复制(Asynchronous Replication)


2. 半同步复制(Semi-Synchronous Replication)


3. 强同步复制(Synchronous Replication)


4. 并行复制(Parallel Replication)

Docker 底层实现

Docker 不是重新造 OS,而是利用 Linux 内核特性:

  1. Namespace(命名空间隔离)
    • PID、NET、IPC、MNT、UTS、USER
    • 保证容器进程、网络、文件系统互不干扰。
  2. Cgroups(控制组)
    • 限制 CPU、内存、IO、网络等资源使用,防止容器抢占宿主机。
  3. UnionFS / OverlayFS
    • 镜像分层存储,支持快速构建、共享只读层。
  4. 容器引擎
    • 通过 runc 等调用 Linux 内核接口来创建/运行容器。

👉 简单说:Namespace 负责“看起来独立”,Cgroups 负责“资源可控”,UnionFS 负责“镜像可复用”。

Lua 脚本在 Redis 里的运行方式


3. 为什么是原子性的


4. 注意事项

  1. 原子性 ≠ 事务
    • Lua 脚本执行期间,其他客户端无法插入命令,但 Lua 脚本本身不会自动回滚。
    • 如果中途报错,前面执行的 Redis 命令已经生效。
    • 所以要自己在脚本里处理错误场景。
  2. 长时间脚本会阻塞
    • 因为 Redis 单线程,脚本如果写得很慢(比如循环 10 万次),会导致整个实例“卡死”,所有请求都要等它跑完。
    • 所以官方建议 Lua 脚本应保持 快速、轻量
  3. 结合 CAS(Check And Set)
    • 常见用法:实现分布式锁、计数器、自定义原子操作。
    • Lua 脚本能把“检查条件 + 修改操作”放在一个原子单元里执行。

1. Redis Lua 脚本的“原子性”


2. 数据库事务的原子性

👉 这才是 ACID 原子性 的严格含义。

ChatGPT said:

这个问题挺常见的,考察对 TCP 连接建立过程系统调用差异的理解。我们来分情况:


1. IP 地址写错的情况

(1)IP 地址无效(比如 256.0.0.1)

(2)IP 地址合法,但没有主机(比如 192.168.1.250,但局域网里没人用)


2. 端口号写错的情况

1. 僵尸进程的定义

👉 通俗点说:身体死了,但“户口”还在,所以叫僵尸。


2. 为什么会有僵尸进程


3. 僵尸进程的危害


4. 如何避免 / 解决僵尸进程

  1. 父进程调用 wait()
    • 正确编写程序:在父进程里调用 wait()waitpid() 等待子进程回收。
  2. 捕捉 SIGCHLD 信号
    • 父进程注册 SIGCHLD 信号处理函数,在子进程退出时自动调用 wait()
  3. 让子进程被 init 进程收养
    • 如果父进程提前退出,子进程会被 1 号进程(init/systemd)接管,init 会自动回收。
  4. 强制手段
    • 杀掉父进程 → 子进程会被 init 收养 → 自动回收僵尸。

1. 普通场景下的 ThreadLocal


2. 在线程池中的特殊情况

Spring 是如何实现事务的

Spring 事务本质上是对 数据库本地事务(JDBC、JTA 等) 的封装,通过 AOP + 事务管理器 实现:

  1. 事务管理器(PlatformTransactionManager)
    • 负责开启、提交、回滚事务。
    • 常见实现:
      • DataSourceTransactionManager(JDBC,本地事务)
      • JpaTransactionManager(JPA)
      • JtaTransactionManager(分布式事务)
  2. 事务拦截器(TransactionInterceptor)
    • Spring AOP 拦截 @Transactional 方法调用。
    • 进入方法前:调用事务管理器 getTransaction() → 开启事务。
    • 方法执行成功:提交事务。
    • 方法抛异常:根据规则(运行时异常回滚,受检异常默认不回滚)执行回滚。

👉 所以:Spring 并没有重新实现事务,而是通过 AOP + 事务管理器,把数据库事务接管了。

Spring 如何“管理事务”

事务管理包含两个重要方面:传播机制统一管理

(1)传播机制(Propagation)

通过 @Transactional(propagation = Propagation.XXX) 定义方法在已有事务中的行为:

(2)统一管理

Spring 如何实现“隔离事务”

Nacos 的实现架构

Nacos Server 本质是一个分布式系统,由 服务注册中心 + 配置中心 两大核心组成:

(1)服务注册与发现

(2)配置中心


2. 一致性保证机制

(1)协议层面

👉 Nacos 同时支持 AP 模式(服务发现)CP 模式(配置管理)


3. 举例解释

Spring IOC 的底层原理

Spring IOC 的核心是 BeanFactory / ApplicationContext 容器,负责创建、管理和装配对象。其底层实现步骤大致是:

  1. 配置元数据加载
    • XML、注解、Java Config。
    • Spring 先解析这些配置,得到 Bean 的定义信息(类名、作用域、依赖关系等)。
  2. 反射创建对象
    • 容器启动时,根据 Bean 定义,用 反射CGLIB 动态代理 创建对象实例。
  3. 依赖注入(DI, Dependency Injection)
    • 根据配置或注解(@Autowired@Resource@Inject)注入依赖。
    • 可以是构造器注入、setter 注入、字段注入。
  4. Bean 生命周期管理
    • 支持初始化方法(@PostConstruct)、销毁方法(@PreDestroy)。
    • 可以加上 AOP 代理(事务、日志、权限等)。
  5. 上下文管理
    • Bean 都交由 Spring 容器统一管理,应用代码只需要通过 @Autowired 获取,而不需要手动 new

👉 本质:Spring IOC 就是 一个大工厂模式 + 反射机制 + 配置驱动


2. Spring IOC 的优点

  1. 解耦合
    • 对象不再由代码里 new 出来,而是由容器统一创建和注入。
    • 依赖关系在配置/注解里声明,降低模块间耦合度。
  2. 更易扩展 & 测试
    • 需要替换某个实现(比如把 MySQLDao 换成 RedisDao),只需改配置,不用改业务代码。
    • 测试时可以轻松注入 Mock 对象。
  3. 统一生命周期管理
    • 容器负责实例化、初始化、销毁,开发者不用关心资源释放等细节。
  4. 结合 AOP 提供横切能力
    • IOC 和 AOP 结合,可以在不改业务代码的情况下加事务、日志、安全等。
  5. 方便配置管理
    • 支持 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

死锁产生的条件

死锁需要同时满足四个条件:

  1. 互斥:锁不能共享;
  2. 占有且等待:事务持有一个锁还想申请另一个;
  3. 不可抢占:锁不能强行夺走;
  4. 循环等待:事务之间形成锁等待环。

如何避免转账死锁

  1. 固定加锁顺序
    • 例如:永远按照账户 ID 从小到大排序,再加锁。
    • 保证所有事务锁顺序一致,就不会出现循环等待。
  2. 缩小锁的范围
    • 尽量减少事务持锁时间,避免长事务。
  3. 合理重试机制
    • 捕获死锁异常,重新发起事务。

1. 为什么需要事务消息

在分布式场景下,比如 下单成功 → 发送扣库存消息,需要保证业务操作和消息发送的一致性。

RocketMQ 的事务消息就是为了解决 本地事务与消息发送的最终一致性问题


2. RocketMQ 事务消息的原理流程

RocketMQ 提供了一种 两阶段提交 + 回查 的事务消息机制:

阶段一:发送半消息(Half Message)

阶段二:执行本地事务


3. 事务回查机制

为什么需要回查?

回查流程

  1. Broker 定期扫描状态为 Half 且超时未确认的消息。
  2. Broker 向 Producer 发起 事务回查请求
  3. Producer 收到后,执行 事务回查接口,检查本地事务执行结果:
    • 如果确认事务成功 → 返回 Commit。
    • 如果确认事务失败 → 返回 Rollback。
    • 如果仍然不确定 → Broker 会稍后继续回查(直到超过回查次数丢弃)。

👉 这样保证消息和本地事务最终一致,要么都成功,要么都失败。


4. 场景与注意事项

什么是大 key

解决大 key 的方法

  1. 拆分大 key
    • 大的 Hash / List 拆成多个小的(例如 user:1:friends:0user:1:friends:1 分片)。
    • 或者用多 key 代替一个超大 key。
  2. 避免一次性操作整个大 key
    • SCAN 代替 KEYS / HGETALL,避免全量阻塞。
    • 批量删除大 key 时分批删除,或用 UNLINK(异步删除)。
  3. 监控大 key
    • 开启 Redis --bigkeys 或者用 redis-cli --hotkeys 定期检查。

2. 什么是数据倾斜

解决数据倾斜的方法

(1)在 Redis Cluster 场景下

  1. 优化 key 的 hash tag
    • Redis Cluster 根据 slot 映射分布 key,合理设计 key 的 hash tag({} 中的部分)让数据均匀落到不同 slot。
  2. 热点 key 分片
    • 如果某个热点 key 请求量过大,可以人为拆分为多个 key,例如:
      • 原 key:item:123
      • 拆分为:item:123:0, item:123:1
      • 访问时通过随机或一致性 hash 选择一个。
  3. 加本地缓存 / 多级缓存
    • 对热点 key 结果做本地缓存,降低对 Redis 的压力。

(2)在大数据计算(MapReduce/Spark)场景下

  1. 加随机前缀或后缀打散
    • 把某个大 key 拆成多个小 key,再在 Reduce 阶段合并。
  2. 倾斜 key 单独处理
    • 对大 key 的任务单独调度,避免和其他任务抢资源。
  3. 动态负载均衡
    • 调整分片规则,让数据尽量均匀。

这是个挺典型的“不动原有代码,但能加新业务逻辑”的场景。设计模式里有几个特别契合的思路:


1. 装饰器模式(Decorator Pattern)


2. 代理模式(Proxy Pattern)


3. 责任链模式(Chain of Responsibility Pattern)


4. 策略模式(Strategy Pattern)


5. AOP(面向切面编程,框架支持)

2. JDK 1.7 的实现 —— Segment 分段锁

缺点:Segment 数量固定,扩容和锁粒度相对大。


3. JDK 1.8 的实现 —— CAS + synchronized + Node 链表 / 红黑树

JDK 1.8 做了大改进,取消了 Segment,改为数组 + 链表/红黑树结构:

  1. 底层结构
    • 类似 HashMap:数组 + 链表/红黑树。
    • 当链表长度超过阈值时,转成红黑树,减少查询退化。
  2. 写操作(put/remove)
    • 定位桶(bin)时,用 CAS(compareAndSwap) 插入节点,如果 CAS 失败(说明有竞争),再用 synchronized 锁定桶头节点。
    • 因为锁的是“桶(bin)”而不是全表,粒度更细。
  3. 读操作(get)
    • get 不加锁,直接通过数组索引、链表/红黑树查找。
    • 因为 Nodevalnext 都是 volatile,保证了可见性。
  4. 扩容(resize)
    • 使用 多线程协助扩容。线程在写入时如果发现需要扩容,会分配一部分桶给当前线程迁移,提高扩容效率。

2. 能做什么

主要功能有:

  1. 提交异步任务,立刻返回 Future

    ExecutorService executor = Executors.newFixedThreadPool(2);
    Future<Integer> future = executor.submit(() -> {
        Thread.sleep(1000);
        return 42;
    });
    

    此时任务在后台执行,你拿到的只是 Future<Integer>

  2. 获取结果(可能阻塞)。

    Integer result = future.get();  // 阻塞直到任务完成
    

    如果任务没执行完,get() 会阻塞。

  3. 取消任务

    future.cancel(true);  // 试图取消任务,true 表示允许中断正在执行的线程
    
  4. 检查任务状态

    • future.isDone():是否完成(包括正常完成、异常、取消)。
    • future.isCancelled():是否已取消。

3. 效果


4. 缺点


5. 升级版


一句话总结Future 用来 表示异步任务的结果,能异步执行、获取结果、取消任务,但结果获取是阻塞的;在实际开发中,更推荐用 CompletableFuture 来写非阻塞异步逻辑。

JVM 介绍

1. 什么是 JVM

2. JVM 的主要组成

  1. 类加载子系统(ClassLoader Subsystem)
    • .class 文件加载进内存,分为加载 → 链接(验证、准备、解析)→ 初始化。
    • 双亲委派模型:先交给父加载器加载,保证核心类唯一性(比如 java.lang.String)。
  2. 运行时数据区(Runtime Data Area)
    • 堆(Heap):存放对象实例,GC 的主要区域。
    • 方法区(Metaspace,JDK8+):存放类元数据、常量池。
    • 虚拟机栈:每个线程独有,存放局部变量表、操作数栈。
    • 本地方法栈:支持 Native 方法。
    • 程序计数器(PC 寄存器):指向当前执行的字节码行号。
  3. 执行引擎(Execution Engine)
    • 解释执行:逐条解释字节码。
    • JIT(Just-In-Time 编译器):热点代码直接编译成本地机器码,加速执行。
  4. 垃圾回收器(GC)
    • 自动回收不再使用的对象,减少内存泄漏风险。

三、常见 JVM 调优手段

1. 内存参数调优

2. GC 算法选择

常见收集器:


3. GC 调优参数


4. 典型调优思路

  1. 先监控
    • 工具:jstat, jmap, jstack, jconsole, VisualVM, GCViewer, Arthas
    • 重点关注:GC 次数、停顿时间、堆使用率、对象晋升速度。
  2. 发现问题
    • Minor GC 过于频繁 → 新生代太小。
    • Full GC 频繁 → 老年代太小 / 内存泄漏。
    • Metaspace OOM → 类加载过多。
  3. 调整参数
    • 扩大堆 / 新生代 / 老年代大小。
    • 换用合适的 GC 算法。
    • 调整 GC 线程数和暂停目标。
  4. 压测验证
    • 调优是迭代过程,需要压测验证效果。

1. ZGC 的优势


2. ZGC 的不足


3. G1 的优势


4. 场景对比

对比维度 G1 ZGC
停顿时间 10ms ~ 200ms <10ms
吞吐量 略低(部分场景)
支持内存 GB ~ 百 GB 百 GB ~ TB
成熟度 高,经验丰富 新,案例相对少
适用场景 通用服务端应用 超大堆、实时性极强应用

Metaspace 在 Full GC 后空间未释放,在生产环境里很常见。它本质上是“类元数据空间”,和堆不同,释放逻辑有一些特殊性。下面我分几个角度帮你分析:


1. 类还在被引用


2. 类卸载(Class Unloading)没有触发


3. 元空间碎片化


4. Metaspace 大小设置过大

下面把“分布式 MySQL 怎么同步”和“分布式 Redis 怎么同步”分开讲,再给你对比和落地建议。

MySQL 的数据同步

经典复制链路(异步为主):

  1. 主库写入事务 → binlog 落盘(STATEMENT/ROW/MIXED,生产一般用 ROW)。
  2. 从库 I/O 线程 连接主库,按位点或 GTID 拉取 binlog → 写到 relay log
  3. 从库 SQL 线程/并行复制器 重放 relay log,应用到 InnoDB。

同步模式:

并行复制与延迟:

DDL 同步:

容灾拓扑:

Redis 的数据同步

主从复制(Redis OSS):

关键细节:

想要 Redis“强一致”的方案:用 RedisRaft 模块(Raft 共识)或商用 Redis Enterprise Active-Active(CRDT)。开源标准 Cluster + Sentinel 本质是“高可用 + 最终一致”,不是强一致。

1. STATEMENT / ROW / MIXED

这是 binlog(binary log,二进制日志)记录格式的三种模式:

👉 生产环境大多推荐 ROW,因为保证了数据一致性(尤其跨机房、异步复制时)。

按位点或 GTID 拉取 binlog → 写到 relay log

这是描述 MySQL 从库如何复制主库的日志

1. AQS 是什么?

AQS 是 Java 并发包(java.util.concurrent)里锁和同步器的底层框架。像 ReentrantLock、Semaphore、CountDownLatch 这些常用工具,底层都是基于 AQS 来实现的。 它的核心思想是:用一个队列来管理获取资源的线程,用一个状态变量表示资源是否可用。


2. 通俗类比

你可以把 AQS 想象成一个银行柜台排队系统


3. 工作流程(以 ReentrantLock 为例)

  1. 线程尝试加锁
    • 如果柜台没人(state = 0),线程直接上去办理业务(获取锁成功)。
    • 如果柜台有人(state = 1),线程加入队伍,进入等待状态。
  2. 线程释放锁
    • 线程办完事,把 state 改回 0。
    • 同时唤醒队伍里的下一个人(唤醒队列头结点)。
  3. 可重入(Reentrant)
    • 如果你正在柜台办业务,又想继续办另一笔业务,你不需要重新排队(同一个线程可以多次获得锁,state++)。

1. 存储结构上的差异


2. 计算方式上的差异


3. 应用场景上的差异


4. 总结一句话

Hive 适合大数据的根本原因在于:

  1. 存储在 HDFS,具备分布式存储能力
  2. 执行引擎基于分布式计算框架(MR/Tez/Spark),能并行处理海量数据;
  3. 数据格式支持列存储和压缩,减少 I/O 和存储开销。

而 MySQL 本质上是 单机事务型数据库,面向实时、小规模数据的增删改查,不适合大数据的全量分析。

你提到的 DeepSeek 这类推理模型(比如 DeepSeek-R1,或者其他“推理模型”/“Reasoning Models”),和传统的大模型(LLM)在架构上相似,但在 训练目标和推理方式 上有一些特别的设计。我给你分层解释一下:


1. 基础:和普通大模型类似


2. 关键不同点:引入推理过程

传统大模型往往是 直接输出答案,但推理模型强调:

  1. Chain-of-Thought (CoT)
    • 模型被鼓励在回答前,先生成 中间推理步骤(像人写草稿)。
    • 比如:解数学题时,先写思路,再写答案。
  2. 过程监督(Process Supervision)
    • 不仅奖励最终答案对错,还奖励中间推理过程的合理性。
    • 例如:如果一步中用了正确的逻辑,就给正反馈,即便最后答案错了。
    • 这和传统的 “Outcome Supervision”(只看最终答案对错)不同。
  3. 训练方式升级
    • 使用 RLHF(人类反馈强化学习)RLAIF(AI反馈强化学习),但奖励信号不仅基于“答案对不对”,还包括“推理过程是否合理”。
    • 有些模型会用 Verifiers(判别器)来检查推理链条是否合理。

3. 技术实现


4. 为什么这样更强?


5. 和普通 LLM 的差别总结

对比点 普通 LLM 推理模型 (DeepSeek, OpenAI o1 等)
架构 Transformer Transformer
输出方式 直接答案 先推理(CoT)再给答案
监督方式 只看最终答案 过程 + 答案双监督
强化学习 奖励=答案对错 奖励=推理合理性 + 答案正确
优势 流畅生成,泛用性强 数学/逻辑/代码等需要推理的任务表现更强

1. 存储结构上的差异


2. 计算方式上的差异


3. 应用场景上的差异


4. 总结一句话

Hive 适合大数据的根本原因在于:

  1. 存储在 HDFS,具备分布式存储能力
  2. 执行引擎基于分布式计算框架(MR/Tez/Spark),能并行处理海量数据;
  3. 数据格式支持列存储和压缩,减少 I/O 和存储开销。

而 MySQL 本质上是 单机事务型数据库,面向实时、小规模数据的增删改查,不适合大数据的全量分析。

你提到的 DeepSeek 这类推理模型(比如 DeepSeek-R1,或者其他“推理模型”/“Reasoning Models”),和传统的大模型(LLM)在架构上相似,但在 训练目标和推理方式 上有一些特别的设计。我给你分层解释一下:


1. 基础:和普通大模型类似


2. 关键不同点:引入推理过程

传统大模型往往是 直接输出答案,但推理模型强调:

  1. Chain-of-Thought (CoT)
    • 模型被鼓励在回答前,先生成 中间推理步骤(像人写草稿)。
    • 比如:解数学题时,先写思路,再写答案。
  2. 过程监督(Process Supervision)
    • 不仅奖励最终答案对错,还奖励中间推理过程的合理性。
    • 例如:如果一步中用了正确的逻辑,就给正反馈,即便最后答案错了。
    • 这和传统的 “Outcome Supervision”(只看最终答案对错)不同。
  3. 训练方式升级
    • 使用 RLHF(人类反馈强化学习)RLAIF(AI反馈强化学习),但奖励信号不仅基于“答案对不对”,还包括“推理过程是否合理”。
    • 有些模型会用 Verifiers(判别器)来检查推理链条是否合理。

3. 技术实现


4. 为什么这样更强?


5. 和普通 LLM 的差别总结

对比点 普通 LLM 推理模型 (DeepSeek, OpenAI o1 等)
架构 Transformer Transformer
输出方式 直接答案 先推理(CoT)再给答案
监督方式 只看最终答案 过程 + 答案双监督
强化学习 奖励=答案对错 奖励=推理合理性 + 答案正确
优势 流畅生成,泛用性强 数学/逻辑/代码等需要推理的任务表现更强

虚拟内存(Virtual Memory)和地址空间(Address Space) 的问题,我们来分步骤梳理一下:


1. 地址空间大小由位数决定


2. 操作系统的划分


3. 物理内存 vs 虚拟内存


4. 实际能开的虚拟内存多大?


总结

Kafka 出现 消息堆积、乱序、丢失,一般是由架构、配置、消费逻辑等多方面因素引起的。下面我分三部分说明原因和对应解决方法:


1. 消息堆积

可能原因:

解决方法:


2. 消息乱序

可能原因:

解决方法:


3. 消息丢失

可能原因:

解决方法:


总结:

Redis 更适合 轻量级、低延迟、对数据量要求不大的场景,而 Kafka 更偏向 高吞吐、持久化、分布式日志流平台

下面分几种 Redis 实现方式说明:


1. 基于 List (RPUSH + LPOP / BRPOP)

原理

优点

缺点


2. 基于 Pub/Sub

原理

优点

缺点


3. 基于 Stream (推荐)

原理

优点

缺点

1. AI大模型表现不佳时可以调的参数

在调用 LLM(比如 GPT、LLaMA、Claude 等)时,常见的可调参数包括:


2. 提示词(Prompt)怎么调

提示词调优(prompt engineering)常见思路:


3. 上下文记忆:模型的记忆 vs. 传过去的上下文

2. 语言特性差异

🟢 JDK8

👉 让 Java 具备了函数式编程和更现代的异步能力。


🟡 JDK17(包含 9~16 的变化)

👉 更简洁的语法、模块化,更好的封装与类型安全。


🔵 JDK21(包含 18~21 的变化)

👉 语法更现代,性能更强,并发处理方式发生革命性变化。


3. 性能 & JVM 改进

Redis Cluster 是把所有 key 映射到 16384 个槽位(hash slots)里,每个 key 会通过 CRC16 算法取模映射到一个 slot,然后 slot 再分布到不同节点。

{} 的作用

为什么需要这样?

小结




©著作权归作者所有