一、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 知识截止和幻觉问题的核心架构:
- 索引阶段:文档 → 分块(Chunking)→ 嵌入(Embedding)→ 存入向量数据库
- 检索阶段:用户查询 → 嵌入 → 向量相似度搜索 → Top-K 相关文档
- 生成阶段:将检索结果 + 用户问题 → 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 | 轻量、嵌入式、开发友好 | 原型验证、小数据量 |
| pgvector | PostgreSQL 扩展、事务支持 | 已有 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 // 互斥锁
}发送流程:
- 如果 recvq 有等待者 → 直接拷贝数据给接收者,唤醒
- 如果缓冲区有空间 → 放入缓冲区
- 否则 → 当前 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调度策略
- 本地队列优先:P 先从自己的本地队列取 G(无锁操作)
- 全局队列:本地队列空 → 从全局队列批量取(需加锁)
- Work Stealing:全局也空 → 随机从其他 P 的本地队列偷一半
- 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/goroutine2.3 GC 垃圾回收
三色标记法
- 白色:未被访问的对象(GC 结束后回收)
- 灰色:已被访问但其引用的对象未扫描
- 黑色:已被访问且其引用的对象已扫描
过程:从根对象出发 → 标灰 → 扫描灰色对象的引用并标灰 → 自己变黑 → 直到无灰色对象 → 回收白色
写屏障(Write Barrier)
并发标记期间,应用程序可能修改指针导致漏标问题。写屏障在指针修改时通知 GC:
- 插入屏障(Dijkstra):新引用的对象标灰 — 防止白色对象被黑色引用后漏标
- 删除屏障(Yuasa):被删除引用的对象标灰 — 保守策略,延迟回收
- 混合写屏障(Go 1.8+):结合两者,栈上对象无需重新扫描,减少 STW
GC 触发时机
- 堆内存增长:堆大小达到上次 GC 后的 GOGC% 增长(默认 100%,即翻倍触发)
- 定时触发:超过 2 分钟没有 GC
- 手动触发:
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 ballastSTW 阶段分析
Go GC 的 STW 极短(通常 < 1ms),只有两个短暂阶段:
- Mark Setup:开启写屏障,扫描栈根(~几十μs)
- 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(冷数据)两个区域:
- 新读取的页先放入 old 区域头部
- 如果页在 old 区域停留超过 1 秒后再次被访问 → 移到 young 区域头部
- 这样全表扫描读到的大量页不会进入 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 = '张三';索引失效场景
- 函数操作:
WHERE YEAR(create_time) = 2024 - 隐式类型转换:
WHERE phone = 13800000000(phone 是 varchar) - 左模糊:
WHERE name LIKE '%三' - OR 条件:OR 两侧字段没有分别建索引
- 违反最左前缀:联合索引 (a,b,c),查询条件跳过 a
- NOT IN / NOT EXISTS:通常导致全表扫描
EXPLAIN 关键字段
| 字段 | 关注点 |
|---|---|
| type | const > eq_ref > ref > range > index > ALL(全表扫描要优化) |
| key | 实际使用的索引,NULL 表示没走索引 |
| rows | 预估扫描行数,越小越好 |
| Extra | Using 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:最后修改该行的事务 IDroll_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 的一致性:
- Prepare 阶段:redo log 写入并标记为 prepare 状态
- 写 Binlog:将事务写入 binlog
- Commit 阶段:redo log 标记为 commit 状态
崩溃恢复:redo log 是 prepare 且 binlog 完整 → 提交;binlog 不完整 → 回滚
Binlog 三种格式
| 格式 | 特点 | 优缺点 |
|---|---|---|
| Statement | 记录 SQL 语句 | 日志小,但非确定性函数可能主从不一致 |
| Row | 记录行数据变化 | 一致性好,但日志量大 |
| Mixed | 自动选择 | 折中方案,推荐使用 Row |
3.6 SQL 优化
慢查询优化流程
- 开启慢查询日志:
slow_query_log = 1, long_query_time = 1 EXPLAIN分析执行计划- 检查索引使用情况
- 优化 SQL 写法
- 考虑业务层优化(缓存、分页策略)
分页优化
-- 慢:深分页,需要扫描 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(或正在 BGSAVE 时 > 5)→ 触发扩容
- 分配 ht[1](大小为 ht[0] 已用空间的 2 倍以上的最小 2^n)
- 渐进迁移:每次 CRUD 操作迁移 1 个桶,100ms 定时迁移 100 个桶
- 迁移完成后释放 ht[0],ht[1] 变为 ht[0]
Redis 对象编码
| 类型 | 编码 | 条件 |
|---|---|---|
| String | int / embstr / raw | 整数 / ≤44字节 / 更大 |
| List | listpack / quicklist | 元素少且小 / 其他(Redis 7.0+) |
| Hash | listpack / hashtable | ≤128个 & 值≤64字节 / 其他 |
| Set | intset / hashtable | 全整数且≤512个 / 其他 |
| ZSet | listpack / 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 高可用
主从复制
- 全量同步:从节点首次连接 → 主节点 BGSAVE 生成 RDB → 发送给从节点 → 从节点加载
- 增量同步:使用 replication buffer + repl_backlog 实现断点续传
- 主节点 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 算法
- 向 N 个独立 Redis 节点(通常 5 个)依次请求加锁
- 超过半数节点加锁成功 且 总耗时 < 锁过期时间 → 加锁成功
- 锁的有效时间 = 设置的过期时间 - 加锁耗时
| 方案 | 性能 | 可靠性 | 复杂度 |
|---|---|---|---|
| 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 预扣库存 → 消息队列异步 → 数据库落库
流量削峰策略
- 前端:按钮防重、验证码、倒计时、请求加密签名
- 网关层:Nginx 限流(令牌桶)、用户级限频
- 服务层:本地缓存标记已售罄、请求排队
库存扣减方案
// 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.1 | HTTP/2 | HTTP/3 |
|---|---|---|---|
| 连接 | Keep-Alive | 多路复用 | 多路复用 |
| 队头阻塞 | 有 | TCP 层有 | 无(QUIC/UDP) |
| 头部 | 文本 | HPACK 压缩 | QPACK 压缩 |
| 推送 | 无 | Server Push | Server Push |
| 协议 | TCP | TCP | QUIC(UDP) |
HTTPS 握手(TLS 1.2)
- Client Hello:支持的加密套件、随机数 R1
- Server Hello:选定加密套件、随机数 R2、服务器证书
- Client:验证证书链 → 生成 Pre-Master Secret → 用证书公钥加密发送
- 双方用 R1 + R2 + Pre-Master 生成会话密钥(对称加密)
- 后续通信使用对称加密
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"]
# 最终镜像仅包含二进制和运行时依赖,约 20MB7.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: 10HPA 自动扩缩容
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 设备管控
核心技术挑战
- 50w 设备并发连接:5 个网关实例,每个 10w 连接,基于 epoll 的非阻塞 I/O
- 指令下发可靠性:离线设备指令持久化(Redis + MySQL),上线后推送,ACK 确认机制
- OTA 更新:Bsdiff 差分算法,灰度发布策略,失败自动回滚
面试回答模板
"在设备管控系统中,最大的挑战是保证 50 万台设备的实时在线管理。我的解决方案是..."
9.3 作业帮商城秒杀
优化路径
- 初版:直接查数据库 → 500 QPS(瓶颈在 MySQL)
- 加 Redis 缓存 → 5,000 QPS
- 库存预扣到 Redis + Lua 原子操作 → 20,000 QPS
- 多级缓存 + 本地内存标记 + 异步落库 → 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):结果和量化数据
示例:最有挑战的项目
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 定义和落地技术产品的技术负责人。"