一、AI Agent & Prompt Engineering

1.1 Agent 架构设计

核心概念:感知-决策-执行循环

AI Agent 的本质是一个自主决策系统,遵循 Perception → Reasoning → Action 循环:

  • 感知(Perception):接收用户输入、环境信息、工具返回结果
  • 推理(Reasoning):LLM 作为"大脑",分析当前状态,制定行动计划
  • 行动(Action):调用工具、生成回复、修改环境状态
  • 观察(Observation):接收行动结果,进入下一轮循环

Agent 类型

ReAct(Reasoning + Acting):交替进行推理和行动,每一步都先"思考"再"执行"。

思考: 用户问的是北京天气,我需要调用天气API
行动: call weather_api(city="北京")
观察: 返回 {"temp": 25, "condition": "晴"}
思考: 已获取天气数据,可以回复用户
回答: 北京今天25°C,天气晴朗。

Plan-and-Execute:先制定完整计划,再逐步执行。适合复杂多步任务。

Multi-Agent:多个 Agent 协作,各司其职。例如:一个 Agent 负责搜索,一个负责分析,一个负责总结。

Tool Use / Function Calling

让 LLM 输出结构化的工具调用指令,而非直接回答:

// LLM 输出
{
  "tool": "search_database",
  "arguments": {
    "query": "2024年Q3销售额",
    "table": "sales"
  }
}

// 系统执行工具,将结果注入下一轮对话
💡 面试常见追问

Q: Function Calling 和 ReAct 有什么区别?
A: Function Calling 是单次工具调用的机制;ReAct 是多轮推理-行动的框架,可以包含多次 Function Calling。

Memory 系统设计

Agent 的记忆分为三层:

类型实现用途示例
短期记忆对话上下文窗口当前对话的上下文最近10轮对话
长期记忆向量数据库跨会话的知识存储用户偏好、历史交互
工作记忆结构化状态当前任务的中间状态多步任务的进度

RAG(检索增强生成)

解决 LLM 知识截止和幻觉问题的核心架构:

  1. 索引阶段:文档 → 分块(Chunking)→ 嵌入(Embedding)→ 存入向量数据库
  2. 检索阶段:用户查询 → 嵌入 → 向量相似度搜索 → Top-K 相关文档
  3. 生成阶段:将检索结果 + 用户问题 → LLM → 生成带引用的回答
✅ 实战要点

Chunk 大小通常 512-1024 tokens,重叠 10-20%。Embedding 模型推荐 OpenAI text-embedding-3-small 或开源 bge-large。

1.2 Prompt Engineering

核心原则

  • 清晰性:明确任务目标,避免歧义
  • 具体性:提供约束条件、输出格式、示例
  • 结构化:使用 XML/Markdown 标签组织复杂 Prompt

核心技巧

Few-shot Learning:提供 2-5 个输入-输出示例,引导模型理解任务模式。

Chain-of-Thought(CoT):要求模型"一步步思考",显著提升推理准确率。

问:一个商店有 23 个苹果,卖了 17 个,又进了 12 个,现在有多少?
Let's think step by step:
1. 初始: 23 个
2. 卖出: 23 - 17 = 6 个
3. 进货: 6 + 12 = 18 个
答: 18 个

Self-Consistency:多次采样推理路径,取多数投票结果,提升复杂推理准确度。

System Prompt 设计模式

你是一个资深 Go 开发工程师。

## 角色
- 精通 Go 语言底层原理和最佳实践
- 善于代码审查和性能优化

## 约束
- 所有代码使用 Go 1.21+ 特性
- 遵循 Effective Go 规范
- 给出建议时附带原因

## 输出格式
- 代码块使用 Go 语法高亮
- 重要概念用**粗体**标注

Prompt 注入防御

  • 输入验证与清洗
  • 使用分隔符隔离用户输入:<user_input>...</user_input>
  • System Prompt 中明确声明不可被用户指令覆盖
  • 输出过滤:检测异常输出模式

1.3 AI Memory 系统(简历项目深挖)

为什么需要 AI Memory?

Oncall 场景痛点:

  • 值班工程师轮换,知识断层严重
  • 同类问题反复出现,每次重新排查
  • RunBook 文档更新滞后,实际操作经验未沉淀

架构设计

知识采集 → Oncall ticket、Slack 对话、Post-mortem 文档
处理管线 → 文本清洗 → 实体提取 → 分块 → Embedding → 存储
检索服务 → 语义搜索 + 关键词搜索混合排序
生成回答 → 检索结果 + 问题 → LLM → 带引用的解决方案

向量数据库选型

方案特点适用场景
Pinecone全托管、低延迟、自动扩缩快速上线、小团队
Milvus开源、高性能、GPU加速大规模、自建基础设施
Chroma轻量、嵌入式、开发友好原型验证、小数据量
pgvectorPostgreSQL 扩展、事务支持已有 PG 基础设施

效果评估

  • 检索准确率:Top-5 命中率、MRR(Mean Reciprocal Rank)
  • 回答质量:人工评分、与 Ground Truth 对比
  • 业务指标:平均故障恢复时间(MTTR)降低、重复 Ticket 减少率
💡 面试回答策略

被问到 "AI Memory 系统"时,从业务痛点出发 → 技术方案 → 量化效果,展示完整的技术决策思路。

二、Golang 深度

2.1 语言核心

Map 底层实现

Go 的 map 底层是哈希表,核心结构 hmap

type hmap struct {
    count     int    // 元素数量
    flags     uint8  // 状态标志(是否正在写入)
    B         uint8  // bucket 数量 = 2^B
    noverflow uint16 // 溢出桶数量
    hash0     uint32 // 哈希种子
    buckets    unsafe.Pointer // 桶数组
    oldbuckets unsafe.Pointer // 扩容时的旧桶
    nevacuate  uintptr        // 扩容进度
}

type bmap struct {
    tophash [8]uint8 // 每个 key 哈希值的高8位
    // 后面跟着 8个key 和 8个value(编译期计算布局)
    // 最后是 overflow 指针
}

查找过程:hash(key) → 低B位定位桶 → 高8位在 tophash 中比对 → 找到后比较完整 key

扩容机制

  • 翻倍扩容:负载因子 > 6.5(元素数/桶数)→ 桶数翻倍
  • 等量扩容:溢出桶过多 → 重新整理,不增加桶数
  • 渐进式迁移:每次访问 map 时迁移 1-2 个旧桶,避免一次性 STW
⚠️ 并发安全

map 不是并发安全的!并发读写会触发 fatal error。解决方案:sync.RWMutex 包装、sync.Map、或分片锁。

Slice 底层

type slice struct {
    array unsafe.Pointer // 底层数组指针
    len   int           // 当前长度
    cap   int           // 容量
}

// 扩容策略 (Go 1.18+)
// oldCap < 256: newCap = oldCap * 2
// oldCap >= 256: newCap = oldCap + oldCap/4 + 192 (约1.25倍增长)

内存泄漏陷阱

// 危险:大切片的小引用会阻止 GC 回收整个底层数组
func getFirstThree(data []byte) []byte {
    return data[:3] // 底层数组不会被 GC
}

// 安全:复制所需数据
func getFirstThree(data []byte) []byte {
    result := make([]byte, 3)
    copy(result, data[:3])
    return result
}

Channel 原理

type hchan struct {
    qcount   uint   // 队列中的数据数量
    dataqsiz uint   // 环形缓冲区大小
    buf      unsafe.Pointer // 环形缓冲区指针
    elemsize uint16 // 元素大小
    closed   uint32 // 是否已关闭
    sendx    uint   // 发送索引
    recvx    uint   // 接收索引
    recvq    waitq  // 等待接收的 goroutine 队列
    sendq    waitq  // 等待发送的 goroutine 队列
    lock     mutex  // 互斥锁
}

发送流程

  1. 如果 recvq 有等待者 → 直接拷贝数据给接收者,唤醒
  2. 如果缓冲区有空间 → 放入缓冲区
  3. 否则 → 当前 goroutine 挂起,加入 sendq

Interface 底层

// 包含方法的 interface
type iface struct {
    tab  *itab          // 类型信息 + 方法表
    data unsafe.Pointer // 指向实际数据
}

// 空 interface
type eface struct {
    _type *_type         // 类型信息
    data  unsafe.Pointer // 指向实际数据
}

// nil interface 陷阱
var e error          // e == nil (iface 的 tab 和 data 都为 nil)
var p *MyError = nil
e = p                // e != nil! (iface 的 tab 不为 nil)

Struct 内存对齐

// 差的布局:占 24 字节
type Bad struct {
    a bool    // 1 + 7 padding
    b int64   // 8
    c bool    // 1 + 7 padding
}

// 好的布局:占 16 字节
type Good struct {
    b int64   // 8
    a bool    // 1
    c bool    // 1 + 6 padding
}
// 规则:按字段对齐大小降序排列

2.2 GMP 调度模型

三大组件

  • G(Goroutine):轻量级协程,初始栈 2KB(可自动扩缩至 1GB)
  • M(Machine):OS 线程,执行 G 的载体。默认最多 10000 个
  • P(Processor):逻辑处理器,数量 = GOMAXPROCS。持有本地运行队列

调度循环:M 必须绑定 P 才能执行 G。M 从 P 的本地队列取 G → 执行 → G 完成/阻塞 → 取下一个 G。

G 的状态流转

_Gidle → _Grunnable → _Grunning → _Gwaiting → _Grunnable → ...
                          ↓
                      _Gsyscall → _Grunnable
                          ↓
                       _Gdead

调度策略

  1. 本地队列优先:P 先从自己的本地队列取 G(无锁操作)
  2. 全局队列:本地队列空 → 从全局队列批量取(需加锁)
  3. Work Stealing:全局也空 → 随机从其他 P 的本地队列偷一半
  4. Network Poller:检查网络 I/O 就绪的 G

抢占式调度

Go 1.14 之前:协作式抢占 — 仅在函数调用时检查抢占标志(死循环无法抢占)

Go 1.14+:基于信号的异步抢占 — sysmon 线程检测到 G 运行超过 10ms → 向 M 发送 SIGURG 信号 → 中断执行,保存现场 → G 回到队列

Goroutine 泄漏排查

// 常见泄漏模式
func leak() {
    ch := make(chan int)
    go func() {
        val := <-ch // 永远阻塞,因为没人发送
        fmt.Println(val)
    }()
    // ch 没有 close,goroutine 永远不会退出
}

// 排查工具
// 1. runtime.NumGoroutine() 监控数量趋势
// 2. pprof goroutine profile
// 3. go tool pprof http://localhost:6060/debug/pprof/goroutine

2.3 GC 垃圾回收

三色标记法

  • 白色:未被访问的对象(GC 结束后回收)
  • 灰色:已被访问但其引用的对象未扫描
  • 黑色:已被访问且其引用的对象已扫描

过程:从根对象出发 → 标灰 → 扫描灰色对象的引用并标灰 → 自己变黑 → 直到无灰色对象 → 回收白色

写屏障(Write Barrier)

并发标记期间,应用程序可能修改指针导致漏标问题。写屏障在指针修改时通知 GC:

  • 插入屏障(Dijkstra):新引用的对象标灰 — 防止白色对象被黑色引用后漏标
  • 删除屏障(Yuasa):被删除引用的对象标灰 — 保守策略,延迟回收
  • 混合写屏障(Go 1.8+):结合两者,栈上对象无需重新扫描,减少 STW

GC 触发时机

  1. 堆内存增长:堆大小达到上次 GC 后的 GOGC% 增长(默认 100%,即翻倍触发)
  2. 定时触发:超过 2 分钟没有 GC
  3. 手动触发runtime.GC()

GC 调优

// GOGC:控制 GC 触发频率
// GOGC=100 (默认):堆内存翻倍时触发
// GOGC=200:堆增长到 3x 时触发(减少 GC 频率,增加内存占用)
// GOGC=50:堆增长到 1.5x 时触发(更频繁 GC,减少内存占用)

// Go 1.19+ GOMEMLIMIT:设置内存软上限
// GOMEMLIMIT=4GiB  →  GC 会更积极地在接近 4GB 时回收
// 配合 GOGC=off 使用:纯粹靠内存限制驱动 GC

// Ballast 技巧(Go 1.19 之前)
// 分配大块不用的内存,降低 GC 频率
var ballast = make([]byte, 1<<30) // 1GB ballast

STW 阶段分析

Go GC 的 STW 极短(通常 < 1ms),只有两个短暂阶段:

  1. Mark Setup:开启写屏障,扫描栈根(~几十μs)
  2. Mark Termination:关闭写屏障,清理状态(~几十μs)

中间的并发标记和清扫阶段与应用并行执行。

2.4 并发编程

sync.Mutex 实现原理

两种模式:

  • 正常模式:新来的 goroutine 与被唤醒的 goroutine 竞争锁。新来的有优势(正在 CPU 上运行),性能更好
  • 饥饿模式:等待超过 1ms → 锁直接交给等待队列头部的 goroutine,保证公平性

sync.Map 适用场景

// sync.Map 内部使用 read-only map + dirty map 双存储
// 适用于:读多写少、key 稳定不频繁增删
// 不适用于:写密集、需要遍历、需要 Len()

// 读操作流程:
// 1. 先查 read map(无锁 atomic)→ 命中直接返回
// 2. miss → 加锁查 dirty map → 提升 dirty 为 read

// 替代方案:分片锁 map
type ShardedMap struct {
    shards [256]struct {
        sync.RWMutex
        m map[string]interface{}
    }
}
func (s *ShardedMap) getShard(key string) int {
    h := fnv.New32a()
    h.Write([]byte(key))
    return int(h.Sum32()) & 255
}

Context 最佳实践

// 1. 超时控制
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()

// 2. 取消传播
func worker(ctx context.Context) error {
    select {
    case <-ctx.Done():
        return ctx.Err() // context.Canceled 或 context.DeadlineExceeded
    case result := <-doWork():
        return process(result)
    }
}

// 3. 不要在 struct 中存储 context(应作为函数第一个参数)
// 4. 不要传递 nil context,用 context.TODO()
// 5. context.Value 仅用于请求级元数据(trace-id),不用于业务参数

errgroup 并发控制

import "golang.org/x/sync/errgroup"

g, ctx := errgroup.WithContext(context.Background())
g.SetLimit(10) // 限制并发数

for _, url := range urls {
    url := url
    g.Go(func() error {
        return fetch(ctx, url)
    })
}

if err := g.Wait(); err != nil {
    // 任一 goroutine 返回 error,ctx 被取消
    log.Fatal(err)
}

2.5 性能调优

pprof 分析

import _ "net/http/pprof"

func main() {
    go func() {
        http.ListenAndServe(":6060", nil)
    }()
    // ...
}

// 命令行分析
// go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30  # CPU
// go tool pprof http://localhost:6060/debug/pprof/heap                # 内存
// go tool pprof http://localhost:6060/debug/pprof/goroutine           # goroutine

逃逸分析

// go build -gcflags="-m" main.go

// 逃逸到堆的常见场景:
// 1. 返回局部变量的指针
func newUser() *User { return &User{} } // escapes to heap

// 2. 闭包引用外部变量
func closure() func() int {
    x := 0
    return func() int { x++; return x } // x escapes
}

// 3. interface 参数(编译器无法确定大小)
fmt.Println(x) // x escapes (参数是 interface{})

Benchmark 编写

func BenchmarkConcat(b *testing.B) {
    b.Run("Plus", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            s := ""
            for j := 0; j < 100; j++ {
                s += "hello"
            }
        }
    })
    b.Run("Builder", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            var sb strings.Builder
            for j := 0; j < 100; j++ {
                sb.WriteString("hello")
            }
            _ = sb.String()
        }
    })
}
// go test -bench=. -benchmem

三、MySQL 深度

3.1 InnoDB 存储引擎

核心架构

  • Buffer Pool:内存中缓存数据页和索引页,默认 128MB。采用改进的 LRU(young/old 区域 5:3)避免全表扫描污染缓存
  • Change Buffer:缓存对二级索引的写操作,后台异步合并到磁盘。适用于写多读少的场景
  • Log Buffer:缓存 redo log 写入,由 innodb_flush_log_at_trx_commit 控制刷盘策略
  • Adaptive Hash Index:InnoDB 自动为热点页建立哈希索引,O(1) 查找

Buffer Pool 管理

改进的 LRU 链表将缓存分为 young(热数据)和 old(冷数据)两个区域:

  1. 新读取的页先放入 old 区域头部
  2. 如果页在 old 区域停留超过 1 秒后再次被访问 → 移到 young 区域头部
  3. 这样全表扫描读到的大量页不会进入 young 区域,不会驱逐热点数据
💡 面试要点

innodb_flush_log_at_trx_commit:0=每秒刷盘 | 1=每次提交刷盘(最安全)| 2=每次提交写 OS 缓存,每秒刷盘

3.2 索引原理

B+ 树结构

InnoDB 使用 B+ 树作为索引结构的原因:

  • 非叶子节点只存 key,单节点容纳更多 key → 树更矮(通常 3-4 层)
  • 叶子节点通过双向链表连接 → 支持高效范围查询
  • 每个节点对应一个磁盘页(16KB)→ 最大化减少磁盘 I/O

聚簇索引 vs 二级索引

特性聚簇索引二级索引
叶子节点存储完整行数据存储主键值
数量每表仅一个可以有多个
查询直接拿到数据需要回表(通过主键查聚簇索引)
选择主键 > 第一个唯一索引 > 隐藏的 row_id用户定义

覆盖索引

-- 联合索引 (name, age)
-- 覆盖索引:查询的字段都在索引中,无需回表
SELECT name, age FROM users WHERE name = '张三'; -- Using index

-- 需要回表:email 不在索引中
SELECT name, age, email FROM users WHERE name = '张三';

索引失效场景

  1. 函数操作WHERE YEAR(create_time) = 2024
  2. 隐式类型转换WHERE phone = 13800000000(phone 是 varchar)
  3. 左模糊WHERE name LIKE '%三'
  4. OR 条件:OR 两侧字段没有分别建索引
  5. 违反最左前缀:联合索引 (a,b,c),查询条件跳过 a
  6. NOT IN / NOT EXISTS:通常导致全表扫描

EXPLAIN 关键字段

字段关注点
typeconst > eq_ref > ref > range > index > ALL(全表扫描要优化)
key实际使用的索引,NULL 表示没走索引
rows预估扫描行数,越小越好
ExtraUsing index(覆盖索引好)、Using filesort(需优化)、Using temporary(需优化)

索引下推(ICP, Index Condition Pushdown)

MySQL 5.6+ 特性。联合索引 (name, age),查询 WHERE name LIKE '张%' AND age = 25

  • 无 ICP:存储引擎返回所有 name 以"张"开头的记录 → Server 层过滤 age
  • 有 ICP:存储引擎直接在索引层面过滤 age → 减少回表次数

3.3 MVCC 机制

版本链

每行数据包含两个隐藏字段:

  • trx_id:最后修改该行的事务 ID
  • roll_pointer:指向 Undo Log 中该行的上一版本

多次修改形成版本链:当前版本 → 上一版本 → 更早版本 → ...

ReadView

ReadView 包含:
- m_ids: 创建 ReadView 时活跃(未提交)的事务 ID 列表
- min_trx_id: m_ids 中最小的事务 ID
- max_trx_id: 下一个将分配的事务 ID
- creator_trx_id: 创建该 ReadView 的事务 ID

可见性判断:
1. trx_id == creator_trx_id → 可见(自己修改的)
2. trx_id < min_trx_id → 可见(事务已提交)
3. trx_id >= max_trx_id → 不可见(ReadView 之后才开始的事务)
4. min_trx_id <= trx_id < max_trx_id:
   - trx_id 在 m_ids 中 → 不可见(事务未提交)
   - trx_id 不在 m_ids 中 → 可见(事务已提交)

隔离级别与 ReadView

  • READ COMMITTED:每次 SELECT 都创建新的 ReadView → 能读到其他事务已提交的数据
  • REPEATABLE READ:只在第一次 SELECT 时创建 ReadView,后续复用 → 同一事务内读到的数据一致

当前读 vs 快照读

  • 快照读:普通 SELECT → 走 MVCC,读取 ReadView 对应版本
  • 当前读:SELECT ... FOR UPDATE / LOCK IN SHARE MODE / INSERT / UPDATE / DELETE → 读最新版本,加锁

3.4 锁机制

行级锁类型

  • Record Lock:锁定索引记录本身
  • Gap Lock:锁定索引记录之间的间隙,防止幻读
  • Next-Key Lock:Record Lock + Gap Lock,锁定记录和前面的间隙。默认的行锁类型

加锁分析实战

-- 表结构:id(PK), age(普通索引), name
-- 数据:(1,10,'A'), (5,20,'B'), (10,30,'C'), (15,40,'D')

-- Case 1: 主键等值查询(存在)
SELECT * FROM t WHERE id = 5 FOR UPDATE;
-- 加锁:Record Lock on id=5

-- Case 2: 主键等值查询(不存在)
SELECT * FROM t WHERE id = 7 FOR UPDATE;
-- 加锁:Gap Lock on (5, 10)

-- Case 3: 普通索引等值查询
SELECT * FROM t WHERE age = 20 FOR UPDATE;
-- 加锁:Next-Key Lock on age (10,20] + Gap Lock (20,30)
--        + Record Lock on 主键 id=5

-- Case 4: 普通索引范围查询
SELECT * FROM t WHERE age >= 20 AND age < 40 FOR UPDATE;
-- 加锁:Next-Key Lock on age (10,20], (20,30], (30,40)

死锁检测与预防

  • InnoDB 使用等待图(wait-for graph)检测死锁,发现环路 → 回滚代价最小的事务
  • 预防策略:固定加锁顺序、减小事务范围、使用合理索引减少锁范围

3.5 事务与日志

Redo Log(重做日志)

WAL(Write-Ahead Logging):先写日志,再写数据页。保证崩溃恢复时数据不丢失。

  • 物理日志:记录"某页某偏移处修改为某值"
  • 循环写入:固定大小文件组,write pos 追 checkpoint
  • 当 redo log 写满 → 触发 checkpoint,将脏页刷盘

两阶段提交

保证 redo log 和 binlog 的一致性:

  1. Prepare 阶段:redo log 写入并标记为 prepare 状态
  2. 写 Binlog:将事务写入 binlog
  3. Commit 阶段:redo log 标记为 commit 状态

崩溃恢复:redo log 是 prepare 且 binlog 完整 → 提交;binlog 不完整 → 回滚

Binlog 三种格式

格式特点优缺点
Statement记录 SQL 语句日志小,但非确定性函数可能主从不一致
Row记录行数据变化一致性好,但日志量大
Mixed自动选择折中方案,推荐使用 Row

3.6 SQL 优化

慢查询优化流程

  1. 开启慢查询日志:slow_query_log = 1, long_query_time = 1
  2. EXPLAIN 分析执行计划
  3. 检查索引使用情况
  4. 优化 SQL 写法
  5. 考虑业务层优化(缓存、分页策略)

分页优化

-- 慢:深分页,需要扫描 10000+10 行
SELECT * FROM orders ORDER BY id LIMIT 10000, 10;

-- 优化1:延迟关联
SELECT o.* FROM orders o
INNER JOIN (SELECT id FROM orders ORDER BY id LIMIT 10000, 10) t
ON o.id = t.id;

-- 优化2:游标分页(需要上一页最后一条的 id)
SELECT * FROM orders WHERE id > 10000 ORDER BY id LIMIT 10;

JOIN 优化

  • NLJ(Nested Loop Join):被驱动表有索引时使用,效率 O(n×log m)
  • BNL(Block Nested Loop Join):被驱动表无索引,用 Join Buffer 批量比较
  • Hash Join(MySQL 8.0.18+):替代 BNL,小表建哈希表,大表探测
  • 优化原则:小表驱动大表,被驱动表 JOIN 字段加索引

四、Redis 深度

4.1 数据结构底层

SDS(Simple Dynamic String)

struct sdshdr {
    int len;    // 已使用长度
    int free;   // 剩余可用空间
    char buf[]; // 字节数组
};
// 优点:O(1)获取长度、二进制安全、空间预分配减少realloc、惰性释放

跳表(SkipList)

ZSet 的底层实现之一(元素数量 > 128 或元素长度 > 64 字节时使用):

  • 多层链表结构,平均 O(log n) 查找
  • 每个节点随机层高(概率 p=0.25,最高 32 层)
  • 比红黑树实现简单,范围查询更高效(链表遍历)
  • 支持按 score 排序 + 按 rank 查找

渐进式 Rehash

Redis dict 使用两个哈希表 ht[0] 和 ht[1]:

  1. 负载因子 > 1(或正在 BGSAVE 时 > 5)→ 触发扩容
  2. 分配 ht[1](大小为 ht[0] 已用空间的 2 倍以上的最小 2^n)
  3. 渐进迁移:每次 CRUD 操作迁移 1 个桶,100ms 定时迁移 100 个桶
  4. 迁移完成后释放 ht[0],ht[1] 变为 ht[0]

Redis 对象编码

类型编码条件
Stringint / embstr / raw整数 / ≤44字节 / 更大
Listlistpack / quicklist元素少且小 / 其他(Redis 7.0+)
Hashlistpack / hashtable≤128个 & 值≤64字节 / 其他
Setintset / hashtable全整数且≤512个 / 其他
ZSetlistpack / skiplist+dict≤128个 & 值≤64字节 / 其他

4.2 持久化

RDB(快照)

  • fork 子进程 + COW(Copy On Write)生成全量快照
  • 触发时机:save 规则(如 900 秒 1 次修改)、BGSAVE 命令、主从全量同步
  • 优点:文件紧凑、恢复速度快
  • 缺点:可能丢失最后一次快照后的数据、fork 大内存时可能阻塞

AOF(追加日志)

  • 记录每个写操作命令
  • 写入策略:always(每次写 fsync,最安全最慢)、everysec(默认,最多丢 1 秒)、no(OS 决定)
  • AOF 重写:fork 子进程,将内存数据重新生成精简的 AOF 文件(合并冗余命令)

混合持久化(Redis 4.0+)

AOF 重写时,先以 RDB 格式写入当前内存数据,然后追加重写期间的增量 AOF 命令。兼顾恢复速度和数据安全性。

4.3 高可用

主从复制

  1. 全量同步:从节点首次连接 → 主节点 BGSAVE 生成 RDB → 发送给从节点 → 从节点加载
  2. 增量同步:使用 replication buffer + repl_backlog 实现断点续传
  3. 主节点 repl_backlog 是环形缓冲区(默认 1MB),从节点断连后只要 offset 还在 backlog 中就能增量同步

Sentinel 哨兵

  • 监控主从节点健康状态
  • 主观下线:单个 Sentinel 认为节点不可达(PING 超时)
  • 客观下线:quorum 数量的 Sentinel 都认为主节点不可达 → 触发故障转移
  • 选主策略:优先级 → 复制偏移量最大 → runID 最小

Cluster 集群

  • 16384 个哈希槽(Hash Slot),CRC16(key) % 16384 决定 key 在哪个节点
  • Gossip 协议通信:PING/PONG 交换节点状态,每秒随机联系几个节点
  • 故障转移:从节点检测到主节点下线 → 发起选举 → 超过半数主节点同意 → 升级为新主
⚠️ 脑裂问题

网络分区可能导致两个主节点同时写入。防护措施:min-replicas-to-write 1 + min-replicas-max-lag 10,主节点至少有 1 个从节点在 10 秒内有回复才接受写入。

4.4 缓存实战

缓存穿透

查询不存在的数据,请求直达数据库。

  • 布隆过滤器:在缓存前加一层布隆过滤器,不存在的 key 直接拦截
  • 空值缓存:查到空结果也缓存(设短过期时间如 5 分钟)

缓存击穿

热点 key 过期瞬间,大量请求同时打到数据库。

  • 互斥锁:只有一个请求去查数据库,其他等待
  • 逻辑过期:缓存不设 TTL,数据中包含过期时间字段,异步更新

缓存雪崩

大量 key 同时过期 或 Redis 宕机。

  • 过期时间加随机偏移:TTL = base + random(0, 300s)
  • 多级缓存:本地缓存(Caffeine)→ Redis → 数据库
  • Redis 高可用部署(Sentinel / Cluster)

缓存一致性

延迟双删:删缓存 → 更新数据库 → 延迟 500ms → 再删缓存

Canal 监听 Binlog:数据库变更 → Canal 解析 Binlog → 异步删除/更新缓存。最终一致性方案。

4.5 分布式锁

基本实现

// 加锁
SET lock_key unique_value NX EX 30

// 解锁(Lua 脚本保证原子性)
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

看门狗续期(Redisson 方案)

后台线程定期(默认每 10 秒)检查锁是否仍被持有 → 是则续期至 30 秒。避免业务执行时间超过锁过期时间。

Redlock 算法

  1. 向 N 个独立 Redis 节点(通常 5 个)依次请求加锁
  2. 超过半数节点加锁成功 且 总耗时 < 锁过期时间 → 加锁成功
  3. 锁的有效时间 = 设置的过期时间 - 加锁耗时
💡 分布式锁对比
方案性能可靠性复杂度
Redis中(主从切换可能丢锁)
ZooKeeper高(CP 系统)
etcd高(Raft 一致性)

4.6 应用场景

排行榜(ZSet)

ZADD rank 100 user:1
ZADD rank 200 user:2
ZREVRANGE rank 0 9 WITHSCORES  # Top 10
ZRANK rank user:1              # 查排名

限流(滑动窗口)

// 用 ZSet 实现滑动窗口限流
func isAllowed(userID string, limit int, window time.Duration) bool {
    key := "ratelimit:" + userID
    now := time.Now().UnixMilli()
    pipe := rdb.Pipeline()
    pipe.ZRemRangeByScore(ctx, key, "0", strconv.FormatInt(now-window.Milliseconds(), 10))
    pipe.ZCard(ctx, key)
    pipe.ZAdd(ctx, key, redis.Z{Score: float64(now), Member: now})
    pipe.Expire(ctx, key, window)
    results, _ := pipe.Exec(ctx)
    count := results[1].(*redis.IntCmd).Val()
    return count < int64(limit)
}

五、系统设计

5.1 秒杀系统设计(简历深挖)

整体架构

前端限流 → CDN → 网关层(Nginx 限流)→ 服务层(Go 微服务)→ Redis 预扣库存 → 消息队列异步 → 数据库落库

流量削峰策略

  1. 前端:按钮防重、验证码、倒计时、请求加密签名
  2. 网关层:Nginx 限流(令牌桶)、用户级限频
  3. 服务层:本地缓存标记已售罄、请求排队

库存扣减方案

// Redis Lua 脚本原子扣减
local stock = tonumber(redis.call('get', KEYS[1]))
if stock <= 0 then
    return -1  -- 已售罄
end
redis.call('decr', KEYS[1])
return stock - 1

// 流程:
// 1. Redis 预扣库存(Lua 原子操作)
// 2. 扣减成功 → 发送 MQ 消息(订单创建)
// 3. 消费者异步落库(数据库扣减 + 创建订单)
// 4. 失败回滚:定时任务检查未支付订单,回补库存

50,000 QPS 如何达到

  • Redis 单机 10w+ QPS,多级缓存 + 本地内存标记已售罄
  • Nginx 层拦截 80% 无效请求
  • Go 服务多实例部署(8 个 Pod)
  • 数据库不在关键路径上(异步落库)

99.99% 成功率保障

  • 超时重试 + 幂等设计(订单号唯一索引)
  • MQ 消息可靠投递(事务消息)
  • 补偿机制:定时对账 Redis 与数据库库存
  • 降级策略:Redis 不可用时返回"排队中",异步处理

5.2 设备管控系统设计(简历深挖)

50w+ 设备在线架构

  • 接入层:多个网关节点,每个维护约 10 万长连接
  • 协议:MQTT / WebSocket,心跳保活(60 秒间隔)
  • 设备状态:Redis 存储在线状态 + 最后心跳时间
  • 指令下发:服务端通过消息队列 → 网关 → 设备

Bsdiff 增量更新

二进制差分算法,将新旧版本的差异压缩为 patch 文件:

  • 100MB 的固件,更新包仅 20-50MB(减少 50-80%)
  • 设备端使用 bspatch 应用更新
  • 校验:MD5/SHA256 校验 + 版本号验证

差错率 0.01% 保障

  • 更新前后双重校验(文件完整性 + 功能自检)
  • 灰度发布:1% → 10% → 50% → 100%,每阶段观察错误率
  • 失败自动回滚到上一版本
  • 异常设备自动上报 + 告警

5.3 通用系统设计

分布式 ID 生成(Snowflake)

| 1 bit 符号位 | 41 bit 时间戳 | 10 bit 机器ID | 12 bit 序列号 |
                 (69年)          (1024台)         (4096/ms)

// 单机每毫秒生成 4096 个不重复 ID
// 时钟回拨问题:拒绝生成 / 等待 / 备用ID段

限流算法对比

算法原理优缺点
固定窗口时间窗口内计数简单,但窗口边界有突刺
滑动窗口细分子窗口,平滑计数更平滑,实现复杂度适中
漏桶固定速率处理请求流量整形好,但无法应对突发
令牌桶固定速率放令牌,有令牌才能通过允许一定突发,最常用

分布式事务

方案一致性性能适用场景
2PC强一致数据库间事务
TCC最终一致资金交易
Saga最终一致长事务、微服务
本地消息表最终一致异步场景,推荐

RocketMQ 核心

  • 架构:NameServer(注册中心)+ Broker(存储)+ Producer + Consumer
  • 消息可靠性:同步刷盘 + 同步复制 = 不丢消息
  • 顺序消息:同一 MessageQueue + 单线程消费
  • 延迟消息:18 个延迟级别(1s/5s/10s/30s/.../2h)
  • 事务消息:Half Message + 本地事务 + 回查机制

六、网络协议

6.1 TCP 深度

三次握手

Client              Server
  |--- SYN(seq=x) --->|     Client: SYN_SENT
  |<-- SYN+ACK(seq=y, ack=x+1) ---|  Server: SYN_RCVD
  |--- ACK(ack=y+1) -->|    Client: ESTABLISHED
                             Server: ESTABLISHED

为什么三次?
- 两次不行:Server 无法确认 Client 收到了 SYN+ACK(可能建立已失效的连接)
- 四次多余:第三次已足够双方确认收发能力

四次挥手

Client              Server
  |--- FIN --->|    Client: FIN_WAIT_1
  |<-- ACK ----|    Client: FIN_WAIT_2, Server: CLOSE_WAIT
  |<-- FIN ----|    Server: LAST_ACK
  |--- ACK --->|    Client: TIME_WAIT (等待 2MSL)
                    Server: CLOSED

TIME_WAIT 为什么等 2MSL(60s)?
1. 确保最后一个 ACK 能到达 Server(如果丢了,Server 会重发 FIN)
2. 让本次连接的残余报文在网络中消失

拥塞控制

  • 慢启动:cwnd 从 1 开始指数增长,直到 ssthresh
  • 拥塞避免:cwnd 线性增长(每 RTT +1)
  • 快重传:收到 3 个重复 ACK → 立即重传(不等超时)
  • 快恢复:ssthresh = cwnd/2,cwnd = ssthresh + 3,继续拥塞避免

TCP 粘包/拆包

TCP 是字节流协议,没有消息边界。解决方案:

  • 固定长度消息
  • 分隔符(如 \r\n)
  • 消息头 + 消息体(头部标明长度)← 最常用

6.2 HTTP/HTTPS

HTTP 版本对比

特性HTTP/1.1HTTP/2HTTP/3
连接Keep-Alive多路复用多路复用
队头阻塞TCP 层有无(QUIC/UDP)
头部文本HPACK 压缩QPACK 压缩
推送Server PushServer Push
协议TCPTCPQUIC(UDP)

HTTPS 握手(TLS 1.2)

  1. Client Hello:支持的加密套件、随机数 R1
  2. Server Hello:选定加密套件、随机数 R2、服务器证书
  3. Client:验证证书链 → 生成 Pre-Master Secret → 用证书公钥加密发送
  4. 双方用 R1 + R2 + Pre-Master 生成会话密钥(对称加密)
  5. 后续通信使用对称加密

TLS 1.3 优化:1-RTT 握手(合并步骤)、0-RTT 恢复(PSK)、移除不安全算法

HTTP 缓存

  • 强缓存:Cache-Control: max-age=3600 / Expires → 命中直接用缓存,不请求服务器
  • 协商缓存:ETag/If-None-Match、Last-Modified/If-Modified-Since → 服务器返回 304 或新数据

6.3 其他协议

WebSocket

基于 HTTP 升级的全双工通信协议:

// 升级请求
GET /chat HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

// 响应
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

gRPC

  • 基于 HTTP/2 + Protocol Buffers
  • 支持四种模式:Unary、Server Streaming、Client Streaming、Bidirectional Streaming
  • 优点:强类型、高性能(二进制序列化)、跨语言、流式通信
  • 适用:微服务间通信、实时数据推送

七、Docker & Kubernetes

7.1 Docker

容器隔离原理

  • Namespace:隔离进程(PID)、网络(NET)、文件系统(MNT)、用户(USER)等
  • Cgroups:限制 CPU、内存、磁盘 I/O 等资源使用
  • Union FS:分层文件系统(OverlayFS),多层只读 + 一层可写

Dockerfile 最佳实践

# 多阶段构建
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download          # 利用缓存层
COPY . .
RUN CGO_ENABLED=0 go build -o server .

FROM alpine:3.19
RUN apk add --no-cache ca-certificates tzdata
COPY --from=builder /app/server /server
EXPOSE 8080
ENTRYPOINT ["/server"]

# 最终镜像仅包含二进制和运行时依赖,约 20MB

7.2 Kubernetes

核心架构

Master 节点

  • API Server:所有请求的入口,RESTful API
  • Scheduler:根据资源需求和约束调度 Pod 到 Node
  • Controller Manager:运行控制器(Deployment、ReplicaSet、Node 等)
  • etcd:分布式 KV 存储,保存集群所有状态

Worker 节点

  • kubelet:管理 Pod 生命周期
  • kube-proxy:维护网络规则(Service → Pod 映射)
  • Container Runtime:containerd / CRI-O

Deployment 滚动更新

apiVersion: apps/v1
kind: Deployment
spec:
  replicas: 3
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1        # 最多多创建1个Pod
      maxUnavailable: 0  # 不允许有不可用Pod
  template:
    spec:
      containers:
      - name: app
        image: myapp:v2
        readinessProbe:   # 就绪检查,通过才接收流量
          httpGet:
            path: /health
            port: 8080
          initialDelaySeconds: 5
          periodSeconds: 10

HPA 自动扩缩容

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: myapp
  minReplicas: 2
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70

八、数据结构与算法

8.1 高频数据结构

哈希表

  • 冲突解决:链地址法(Java HashMap)、开放寻址法(Go map)
  • 负载因子 = 元素数 / 桶数。Go map 阈值 6.5,Java HashMap 阈值 0.75
  • 链表过长 → 红黑树(Java 8+ HashMap,链表长度 > 8 且桶数 > 64)

B+ 树 vs 红黑树 vs 跳表

结构查找范围查询适用场景
B+ 树O(log n)高效(叶子链表)数据库索引
红黑树O(log n)中序遍历内存有序映射(TreeMap)
跳表O(log n)高效(链表遍历)Redis ZSet

堆 / Top-K 问题

// 求最大的 K 个元素 → 维护大小为 K 的最小堆
// 时间复杂度: O(n log K)
type MinHeap []int
func (h MinHeap) Len() int           { return len(h) }
func (h MinHeap) Less(i, j int) bool { return h[i] < h[j] }
func (h MinHeap) Swap(i, j int)      { h[i], h[j] = h[j], h[i] }
func (h *MinHeap) Push(x any)        { *h = append(*h, x.(int)) }
func (h *MinHeap) Pop() any {
    old := *h; n := len(old)
    x := old[n-1]; *h = old[:n-1]
    return x
}

8.2 高频算法模式

双指针 / 滑动窗口

// 无重复字符的最长子串 (LeetCode 3)
func lengthOfLongestSubstring(s string) int {
    window := make(map[byte]int)
    left, result := 0, 0
    for right := 0; right < len(s); right++ {
        c := s[right]
        window[c]++
        for window[c] > 1 {
            window[s[left]]--
            left++
        }
        if right-left+1 > result {
            result = right - left + 1
        }
    }
    return result
}

二分查找模板

// 查找左边界
func searchLeft(nums []int, target int) int {
    lo, hi := 0, len(nums)
    for lo < hi {
        mid := lo + (hi-lo)/2
        if nums[mid] < target {
            lo = mid + 1
        } else {
            hi = mid
        }
    }
    return lo // 第一个 >= target 的位置
}

排序算法复杂度

算法平均最坏空间稳定
快排O(n log n)O(n²)O(log n)
归并O(n log n)O(n log n)O(n)
堆排O(n log n)O(n log n)O(1)
计数排序O(n+k)O(n+k)O(k)

九、项目深挖准备

9.1 Amazon Oncall AI 优化

项目背景与动机

问题:Oncall 值班时,工程师面对大量重复告警和已知问题,排查效率低。新人上手 Oncall 成本高,平均需要 2-3 个月熟悉系统。

技术方案

  • AI Memory 系统:将历史 Oncall ticket、Post-mortem、RunBook 向量化存储
  • 智能匹配:新告警自动匹配历史相似问题,推荐解决方案
  • 知识沉淀:每次 Oncall 结束自动总结,更新知识库

面试常见问题准备

  • Q: 为什么不用传统搜索? A: 语义理解比关键词匹配更准确,工程师描述问题用自然语言
  • Q: 如何评估效果? A: MTTR 降低 30%,重复问题首次解决率提升至 85%
  • Q: 数据安全? A: 所有数据在 AWS 内网,不调用外部 API,模型部署在 SageMaker

9.2 作业帮 Pad 设备管控

核心技术挑战

  1. 50w 设备并发连接:5 个网关实例,每个 10w 连接,基于 epoll 的非阻塞 I/O
  2. 指令下发可靠性:离线设备指令持久化(Redis + MySQL),上线后推送,ACK 确认机制
  3. OTA 更新:Bsdiff 差分算法,灰度发布策略,失败自动回滚

面试回答模板

"在设备管控系统中,最大的挑战是保证 50 万台设备的实时在线管理。我的解决方案是..."

9.3 作业帮商城秒杀

优化路径

  1. 初版:直接查数据库 → 500 QPS(瓶颈在 MySQL)
  2. 加 Redis 缓存 → 5,000 QPS
  3. 库存预扣到 Redis + Lua 原子操作 → 20,000 QPS
  4. 多级缓存 + 本地内存标记 + 异步落库 → 50,000 QPS

线上事故

一次超卖事故:Redis 主从切换期间丢失了部分库存扣减记录。

复盘措施:引入库存对账定时任务(每 5 秒),Redis 和数据库库存不一致时告警并以数据库为准。

9.4 贝壳惠居私域

PMO 角色

协调 4 个团队(前端、后端、产品、运营),管理项目进度和资源分配。

鉴权系统设计

  • 统一登录:JWT + 刷新令牌机制
  • 权限模型:RBAC(角色-权限-用户)
  • 跨平台:微信小程序、App、Web 统一鉴权

面试要点

强调从0到1的经验、跨团队协调能力、技术决策过程。

十、行为面试(BQ)

10.1 STAR 法则

  • S(Situation):描述背景和情境
  • T(Task):你的具体任务/目标
  • A(Action):你采取了什么行动(重点!用"我"而非"我们")
  • R(Result):结果和量化数据

示例:最有挑战的项目

✅ STAR 示例

S: 在作业帮双11大促前2周,压测发现秒杀系统只能支撑 5,000 QPS,远低于预期的 50,000。
T: 我需要在2周内将系统性能提升 10 倍。
A: 我分析了性能瓶颈——90%的延迟在数据库层。我设计了 Redis 预扣库存 + Lua 原子操作方案,加入多级缓存和异步落库。同时与运维合作增加了 Go 服务实例。
R: 压测结果 52,000 QPS,双11 当天峰值 48,000 QPS,交易成功率 99.99%,零超卖。

10.2 常见问题准备

Tell me about yourself

"我是一名有8年经验的后端工程师,目前在 Amazon 担任 SDE II。我的核心技术栈是 Go,在高并发系统设计方面有深入实践。在作业帮期间,我主导了支撑 50,000 QPS 的秒杀系统和管理 50 万台设备的管控平台。近期我在 Amazon 的工作聚焦在用 AI 技术优化 Oncall 效率,搭建了团队的 AI Memory 知识库。我对 AI Agent 和系统架构的交叉领域很感兴趣。"

为什么离职?

保持正面:聚焦于"寻找更大的挑战"和"技术方向匹配",不抱怨前公司。

与同事的冲突

展示:先倾听理解 → 数据驱动讨论 → 找到共识 → 维护关系。

失败经历

展示:承认失败 → 分析原因 → 具体改进措施 → 后续避免。

职业规划

"短期希望在 AI + 系统工程的交叉领域深耕,长期目标是成为能够从 0 到 1 定义和落地技术产品的技术负责人。"