每日一问
2026.08.25#
raft中多数派为什么是n / 2 + 1#
为了保证系统的强一致性,必须确保读操作能读到最新的写操作,这就要求读集合(R)和写集合(W)必须有交集,即满足 |W| + |R| > N(N 为总节点数)。 在满足该公式的前提下,系统的设计需要在可用性和可靠性之间做权衡: 如果要求全部节点写入成功(W=N, R=1),读取极快,但任何一个节点宕机都会导致系统不可写,可用性极差。
如果只需一个节点写入(W=1, R=N),写入极快,但每次读取必须全量查询所有节点,容错性同样极差。
为了在这两者之间取一个最佳的“中间值”,Raft 和 Paxos 等算法通常将 N 设为奇数,并要求写入和读取都达到多数派(W = R = N/2 + 1)。这样不仅完美满足了 |W| + |R| > N,保证了读写集合的交集,同时也能容忍少数派((N-1)/2 个)节点的宕机,实现了可用性和可靠性的最优解。
额外练习: 以下代码有死锁的风险吗
package main
import (
"sync"
"time"
)
// Service 抽象的服务,管理共享数据和事件分发
type Service struct {
mu sync.Mutex
data int
eventCh chan int //带缓冲的channel
}
// PublishEvent 模拟服务端内部逻辑:更新数据并广播事件
func (s *Service) PublishEvent() {
s.mu.Lock()
defer s.mu.Unlock()
s.data++
s.eventCh <- s.data
}
// RPC_UpdateData 模拟暴露给客户端的同步调用接口
func (s *Service) RPC_UpdateData(val int) {
s.mu.Lock()
defer s.mu.Unlock()
s.data = val
}
/*
// 客户端按照以下方式使用Service
for {
select {
case val, ok := <-cmdCh:
if !ok {
return
}
svc.RPC_UpdateData(val)
case _, ok := <-svc.eventCh:
if !ok {
return
}
// 处理服务端推送的事件
}
}
*/
}答案:有,PublishEvent在向channel发送数据时加锁可能会导致死锁
2026.08.09#
线上某个 Redis Key 每分钟被访问数百万次,Redis CPU 持续升高,但内存和网络带宽尚未打满。你会如何判断它是不是热点 Key?为什么热点 Key 会打高 CPU?如何分别治理读热点和写热点?有哪些一致性与可用性代价?#
先对比 Redis 各节点的 CPU、QPS、延迟和网络流量,确认是否存在单节点负载倾斜;再通过客户端或代理层访问统计定位具体 Key。使用 LFU 淘汰策略时,也可以通过 redis-cli –hotkeys 采样热点 Key。 Redis Cluster 使用 CRC16(key) mod 16384 计算槽位。同一个 Key 固定落到同一个槽和主节点,因此大量请求会集中消耗该节点的命令执行、协议解析、字典查找、网络读写和响应生成能力,最终导致单节点 CPU 成为瓶颈。 读热点可以采用: 业务进程本地缓存,并设置较短 TTL 和随机抖动; 使用 singleflight 合并并发回源请求; 将数据复制为多个不同 Key,分散到不同槽位; 在允许短暂不一致时读取副本。 代价是缓存失效复杂、存在短暂不一致,并可能发生击穿。 写热点不能直接无条件拆分: 对计数、点赞等可交换、可合并操作,可以拆成多个桶,之后异步聚合; 对普通状态更新,可以通过消息队列串行化、削峰并合并连续写; 需要保证消息顺序、幂等和失败补偿。 写热点治理通常会引入最终一致性、写入延迟、聚合复杂度以及故障恢复成本。
2026.08.07#
pprof日常怎么排查线上问题?CPU高、内存泄漏、goroutine泄漏分别用什么指标定位?#
Go的pprof分两种,一种是runtime/pprof,一种是net/http/pprof,线上一般用后者,导入_ “net/http/pprof"之后,访问/debug/pprof/这个路由就能看到各类采样数据。采样类型主要有五个:cpu、heap、goroutine、mutex、block,分别对应CPU占用、内存分配、goroutine数量、锁竞争、阻塞操作。
日常排查线上问题,一般分三步走:首先通过监控系统比如Prometheus+Grafana看概览指标,发现异常,然后通过pprof拉取对应的采样数据,最后用go tool pprof分析定位到具体代码行。
接下来分三种典型场景来说。
第一个是CPU高。当发现CPU使用率持续飙高,可以采样30秒的CPU profile,curl /debug/pprof/cpu?seconds=30 > cpu.pprof,然后用go tool pprof -top cpu.pprof看最耗CPU的函数,或者用list 函数名精确定位到哪一行代码。CPU高通常有两种情况,一种是业务逻辑本身的计算量大,需要优化算法或加缓存;另一种是频繁的GC导致大量CPU花在垃圾回收上,这时候看/debug/pprof/heap?gc=1触发一次GC看内存分配情况,同时用go tool pprof -alloc_space看分配最多的对象是哪些,针对性做对象复用或减少分配。
第二个是内存泄漏。内存泄漏的特征是内存使用只涨不跌,或者周期性涨跌但整体趋势向上。Heap profile用/debug/pprof/heap采样,分析的时候用go tool pprof -inuse_space看当前占用的内存,用-alloc_space看累计分配量。inuse_space能看出当前内存被谁占着,alloc_space能看出谁分配得最频繁。内存泄漏常见的原因有三种:第一种是全局的map或slice只增不减,比如缓存没有淘汰策略;第二种是goroutine泄漏导致goroutine的栈内存无法释放,需要结合goroutine profile一起看;第三种是CGO或者第三方库申请的内存没释放,这种情况用go tool pprof -inuse_space是看不出来的,需要结合系统内存监控和cgo内存分析的额外手段。
第三个是goroutine泄漏。特征是goroutine数量持续上涨不回落。 用/debug/pprof/goroutine拉取,分析时用go tool pprof -traces查看所有goroutine的调用栈,或者访问/debug/pprof/goroutine?debug=2看文本格式的详细栈信息。重点看goroutine的状态,如果大量卡在chan send、chan receive、select,说明是channel导致的阻塞;如果卡在sync.WaitGroup.Wait,说明是WaitGroup的Done没调用;如果卡在sync.RWMutex.Lock,说明是锁持有时间过长或死锁。定位到具体阻塞位置后,检查为什么条件永远不满足。
2026.08.05#
Golang 中数组和切片的区别?#
数组: 数组固定长度。数组长度是数组类型的一部分,所以 [3]int 和 [4]int 是两种不同的数组类型。数组需要指定大小,不指定也会根据初始化,自动推算出大小,大小不可改变。数组是通过值传递的。 切片: 切片可以改变长度。切片是轻量级的数据结构,三个属性:指针、长度、容量;不需要指定大小。切片在传参时按值传递的是 slice header(包含底层数组指针、长度、容量)的副本,但因为副本中的指针指向同一底层数组,所以函数内对底层数组元素的修改对调用方可见。切片可以通过数组来初始化,也可以通过内置函数 make() 来初始化,初始化的时候 len=cap,然后进行扩容。
分析: slice 的底层数据其实也是数组,slice 是对数组的封装,它描述一个数组的片段。slice 实际上是一个结构体,包含三个字段: 长度、容量、底层数组 。
// runtime/slice.go type slice struct { array unsafe.Pointer // 元素指针 len int // 长度 cap int // 容量 }
2026.08.04#
索引失效的场景#
对索引使用左或者左右模糊匹配 当我们使用左或者左右模糊匹配的时候,也就是 like %xx 或者 like %xx% 这两种方式都会造成索引失效。 因为索引 B+ 树是按照「索引值」有序排列存储的,只能根据前缀进行比较。
对索引字段使用函数 如果查询条件中对索引字段使用函数,就会导致索引失效。 因为索引保存的是索引字段的原始值,而不是经过函数计算后的值,自然就没办法走索引了。
对索引字段进行表达式计算 在查询条件中对索引进行表达式计算,也是无法走索引的。 原因跟对索引使用函数差不多,因为索引保存的是索引字段的原始值,所以无法走索引。只能通过把索引字段的取值都取出来,然后依次进行表达式的计算来进行条件判断,因此采用的就是全表扫描的方式。
对索引字段进行隐式类型转换 如果索引字段是字符串类型,但是在条件查询中,输入的参数是整型的话,你会在执行计划的结果发现这条语句会走全表扫描。 但是如果索引字段是整型类型,查询条件中的输入参数即使字符串,是不会导致索引失效,还是可以走索引扫描。 这是因为,MySQL 在遇到字符串和数字比较的时候,会自动把字符串转为数字,然后再进行比较。而这个转换操作,底层是调用了CAST函数,而前面我们也说了,对索引使用函数是会导致索引失效的。
联合索引非最左匹配 多个普通字段组合在一起创建的索引就叫做联合索引,也叫组合索引。联合索引要能正确使用需要遵循最左匹配原则,也就是按照最左优先的方式进行索引的匹配。 比如,如果创建了一个 (a, b, c) 联合索引,如果查询条件是以下这几种,就可以匹配上联合索引: where a=1; where a=1 and b=2 and c=3; where a=1 and b=2; 但是,如果查询条件是以下这几种,因为不符合最左匹配原则,所以就无法匹配上联合索引,联合索引就会失效: where b=2; where c=3; where b=2 and c=3;
原因是,在联合索引的情况下,数据是按照索引第一列排序,第一列数据相同时才会按照第二列排序
2026.08.03#
如何评测一个 Agent 做得好不好?#
评测维度分结果和过程两类,自动化靠程序校验、模型裁判和回归测试集。
评什么——两个维度:
- 结果:任务成功率(有没有做成,要能客观判定)。
- 过程:工具调用对不对、用了多少步/多少 token、有没有绕路死循环。
怎么自动化——三招:
- 能用代码验证的用代码验证(比如跑测试、查数据库最终状态)。
- 不能的用强模型当裁判打分(LLM-as-Judge,配上明确评分标准)。
- 线上 bad case 沉淀成固定测试集,每次改动都回归跑一遍。
加分点:Agent 输出不稳定,同一任务要跑多次看全过率(pass^k),不能跑一次过了就算数。
2026.08.02#
Kafka中Topic分区数设置过多会导致哪些生产问题?#
分区过多会放大Broker端文件句柄和内存消耗,引发频繁Full GC甚至OOM;同时Controller在选举或扩分区时需同步大量元数据,集群抖动加剧。客户端也会因线程和缓存膨胀出现性能下降,最终拖慢整体吞吐。
2026.07.08#
美股实时行情跨中美传输遇到丢包,怎么设计?#
评级:场景题(量化公司原题)
美股实时行情跨太平洋传输到国内服务器,核心挑战是高 RTT、随机丢包、网络抖动、路由绕行、带宽波动、乱序、跨境链路拥塞和运营商 QoS 差异。行情数据又有强时效性,业务通常更关注“尽快拿到最新状态”,同时关键成交、盘口变更也需要完整性保障。
原生 TCP 的主要问题是重传和拥塞控制会放大延迟。中美跨洋 RTT 常见在 120ms 以上,丢包后触发超时重传、快速重传、拥塞窗口收缩,容易产生明显尾延迟;高频小包行情还会受到滑动窗口、Nagle、队头阻塞和拥塞避让影响,吞吐与延迟都会抖动。
原生 UDP 的优势是低延迟和低协议开销,问题是缺少确认、重传、有序交付和拥塞控制。跨境随机丢包会直接丢失成交、盘口更新等关键数据,业务层需要自行识别缺口并修复。
更推荐 UDP + 应用层可靠机制,或使用 QUIC / 自研可靠传输协议。常见做法是每条行情带递增序号、时间戳和频道 ID,接收端检测乱序与缺口;对关键增量数据做 NACK 重传,对可容忍轻微损失的高频价格流使用 FEC 前向纠错、冗余发送和最新状态覆盖。
分层解决思路:
- 网络层:选择专线、金融低延迟线路、SD-WAN、多运营商链路、海外中转 POP,做链路质量探测和动态路由切换。
- 传输层:TCP 场景开启 BBR、SACK、窗口调优和连接复用;UDP 场景增加序号、ACK/NACK、FEC、乱序缓冲、重传队列和速率控制。
- 应用层:采用“快照 + 增量”模型,增量丢失时快速请求最新快照;按 symbol 或 channel 分片传输,避免单通道阻塞;消费端按序号做幂等、去重、缺口检测和延迟监控。
- 运维层:持续监控 RTT、丢包率、乱序率、重传率、端到端延迟和各运营商路径质量,按行情时段动态调整链路和冗余策略。
面试回答可以落在取舍上:原生 TCP 可靠性强但尾延迟高;原生 UDP 低延迟但完整性由业务承担。实时行情更适合基于 UDP 构建轻量可靠层,把关键数据可靠性、最新状态优先级和延迟预算放到业务协议中控制。
2026.07.07#
多进程机器频繁缺页中断,CPU 占用不高但请求延迟极高,怎么分析?#
评级:后端面试场景题
根因通常是物理内存无法容纳所有进程的工作集,系统频繁发生换页和 major page fault。缺页需要等待磁盘 IO 把页面换入内存,进程进入阻塞等待,CPU 看起来空闲,业务请求却被 IO 等待拖慢,最终表现为延迟飙升。
常见诱因:
- 整机物理内存不足,多进程内存占用叠加超过可用内存,产生内存颠簸。
- 程序存在内存泄漏、无限制大缓存、一次性加载大批量数据等行为,占用过量内存。
- 并发进程数量过多,多个进程同时争抢内存。
swappiness参数偏高,内核更积极地把匿名页换出到 swap。- swap 位于机械硬盘或低速磁盘,缺页等待时间被进一步放大。
排查时可以看 vmstat 的 si/so、sar -B 的缺页指标、top 的 wa、free -h 的 swap 使用量、pidstat -r 的进程内存变化,以及业务进程的 RSS、VSZ、major fault 数量。
优化方案:
- 增加物理内存,或降低单机承载的进程数量,把工作集控制在物理内存范围内。
- 修复内存泄漏,限制本地缓存大小,避免一次性加载超大数据,改成分页、流式或分批处理。
- 给进程设置内存上限和并发上限,通过 cgroup、容器 limits 或进程池控制资源争抢。
- 调低
vm.swappiness,减少主动换页;对低延迟核心服务可以关闭 swap 或只保留应急 swap。 - 把 swap 放到 SSD,并监控 major fault、swap in/out 和 IO wait,及时扩容或迁移实例。
2026.07.06#
进程、线程、协程三者区别#
评级:基础知识
进程是操作系统资源分配的基本单位。每个进程拥有独立虚拟地址空间、文件句柄、堆、信号、内核对象等资源,进程之间通过管道、Socket、共享内存、消息队列等 IPC 机制通信。
线程是 CPU 调度执行的基本单位,依附于进程存在。同一进程内的线程共享堆、全局变量、文件描述符和地址空间;每个线程拥有自己的栈、寄存器上下文和线程局部数据。线程切换需要内核参与,成本低于进程切换。
协程是用户态轻量级执行单元,由语言运行时或协程库调度。协程切换通常只需要保存和恢复少量上下文,成本低;多个协程可以运行在一个或多个线程上,遇到 IO、定时器或显式让出时由调度器切换执行。
Go 的 goroutine 可以看作运行时管理的轻量级协程。goroutine 初始栈很小并可按需增长,Go runtime 通过 GMP 模型把大量 goroutine 调度到少量系统线程上运行。
面试回答可以按“资源归属、调度者、切换成本、通信方式”四个维度讲:进程资源隔离最强、切换成本最高;线程共享进程资源、由内核调度;协程由用户态调度,适合高并发 IO 密集型任务。
2026.07.04#
虚拟地址、虚拟地址空间、页表三者有什么联系?#
评级:OS 基础
虚拟地址空间是进程逻辑上可使用的全部地址范围。每个进程通常拥有独立的虚拟地址空间,看起来像一片连续、私有的逻辑内存。
虚拟地址是程序访问内存时使用的逻辑地址,属于虚拟地址空间中的某一个位置。分页机制下,虚拟地址通常可以拆成“虚拟页号 + 页内偏移”。
页表是虚拟地址到物理地址的映射表,核心内容是“虚拟页号 -> 物理页框”的对应关系,同时还会记录权限位、有效位、脏位等控制信息。
CPU 访问内存时,会先拿到程序给出的虚拟地址,再通过页表找到对应物理页框,最后加上页内偏移得到真实物理地址。
三者关系可以概括为:虚拟地址空间提供进程视角下的逻辑内存范围,虚拟地址是这个空间里的具体位置,页表负责把这个位置映射到物理内存。
2026.07.03#
RabbitMQ 中为什么需要 Exchange?#
评级:RabbitMQ 常见
Exchange 的核心作用是根据交换机类型、Routing Key 和 Binding 规则,把 Producer 发送的消息路由到一个或多个 Queue。它让生产者只关心“把消息发给哪个交换机”,让队列绑定关系由 RabbitMQ 配置管理。
如果 Producer 直接发送到 Queue,会有几个问题:
- Producer 必须知道每个 Queue 的名字,生产者和具体队列强绑定。
- 新增、删除或调整 Queue 时,Producer 代码和配置都可能跟着改。
- 一个消息要投递给多个 Queue 时,Producer 需要自己发送多次,业务代码会混入路由逻辑。
有了 Exchange 之后:
- Producer 只需要把消息发送给 Exchange,并携带合适的
Routing Key。 - Exchange 根据绑定关系自动把消息路由到一个或多个 Queue。
- 新增消费者、新增 Queue 或调整消息分发规则时,通常只需要修改绑定关系,Producer 逻辑保持稳定。
- Direct、Topic、Fanout、Headers 等交换机类型可以覆盖精确路由、通配符路由、广播和按消息头路由等场景。
面试回答可以收口到一句:Exchange 把“消息生产”和“消息路由”解耦,Producer 负责发送业务事件,Exchange 负责按规则分发到队列。
2026.07.02#
TCP 主动关闭方为什么要等待 TIME_WAIT 2MSL?#
评级:TCP 高频
先确认角色:TIME_WAIT 是主动关闭方的状态。四次挥手过程通常是主动关闭方发送 FIN,被动关闭方返回 ACK;被动关闭方处理完剩余数据后发送 FIN;主动关闭方返回最后一个 ACK,进入 TIME_WAIT,等待 2MSL 后释放连接。
处理最后一个
ACK丢失的场景。主动关闭方发送最后一个ACK后,如果这个 ACK 在网络中丢失,被动关闭方会重传FIN。主动关闭方处于TIME_WAIT时仍能收到这个重传FIN,并再次发送ACK,帮助对端正常进入关闭状态。等待旧连接的滞留报文自然过期。网络中可能存在延迟到达、乱序到达的旧数据包。等待
2MSL可以让本次连接相关报文在网络中消失,降低旧报文影响后续相同四元组连接的风险。MSL是 Maximum Segment Lifetime,表示 TCP 报文在网络中的最大存活时间。2MSL覆盖两个方向上的最大残留时间,给最后一次ACK和可能重传的FIN留出完整处理窗口。
面试回答可以这样收口:TIME_WAIT 的核心价值是保证最后一个 ACK 可补发,并让旧连接报文充分过期;它属于主动关闭方。
2026.06.30#
Kafka 如何保证消息不丢失?#
评级:Kafka 高频
Kafka 保证消息不丢失需要 Producer、Broker、Consumer 三端协同配置。面试里可以按“生产确认、服务端副本、消费提交”三段回答。
Producer 端设置
acks=all,让消息写入 Leader 并同步到足够多的 ISR 副本后再返回成功;同时开启重试,合理配置retries、delivery.timeout.ms、request.timeout.ms和enable.idempotence=true,避免网络抖动、请求超时和重试带来的重复写入问题。Broker 端配置足够的副本数,比如
replication.factor >= 3,并设置合理的min.insync.replicas,保证至少有多个同步副本写入成功。生产环境通常配合unclean.leader.election.enable=false,避免落后副本成为 Leader 后造成已确认消息被截断。Consumer 端关闭自动提交 offset,使用手动提交。业务逻辑处理成功后再提交 offset,可以避免消费者拉到消息后进程崩溃导致“假消费”。批量消费时要在批处理成功后提交本批次最大 offset,并做好幂等处理。
可靠性还要配合监控和运维。重点关注 ISR 收缩、Under Replicated Partitions、生产失败率、重试次数、Consumer lag、磁盘故障和 Broker 重启。发现 ISR 频繁波动时,要检查磁盘、网络、Broker 负载和副本同步延迟。
工程取舍上,
acks=all、多副本、手动提交 offset 会提升可靠性,也会增加延迟和资源成本。核心交易、订单、支付、审计日志更适合可靠性优先;日志采集、埋点等场景可以按业务价值降低配置强度。
可能的追问点#
acks=all和acks=1在生产环境中的取舍依据是什么?- ISR 列表的变更对消息写入和数据完整性有什么影响?
- Consumer 手动提交 offset 时如何平衡性能和数据不丢?
2026.06.29#
Agent 多轮调用过程怎么排查?#
评级:拉完了
Agent 的一次请求通常包含多轮 LLM 调用、Tool 调用、循环决策、上下文裁剪和中间状态更新。排查重点是把对话过程还原成一棵可观察的 trace 树,看到每一步输入、输出、耗时、错误和父子关系。
hook 采集是入口。可以做一个 plugin,在每轮 LLM 调用、Tool 调用、循环决策前后都记录事件,带上
run_id、trace_id、span_id、parent_span_id、用户请求、模型参数、工具名、耗时、状态码、错误信息和 token 成本。结构化 Tracing 是核心。把一次 agent 执行视为一次分布式 trace,把每次 LLM call、Tool call、memory read/write、planner decision 都建成 span。这样可以从树形结构中看出调用顺序、分支、重试、并发子任务和异常节点。
关键 hook 点包括:LLM 调用前、LLM 调用后、Tool 调用前、Tool 调用后、上下文压缩前后、检索前后、循环/决策点、任务结束点。每个点记录输入摘要、输出摘要、上下文长度、token 数、耗时、错误、重试次数和 stop reason。
排查流程可以按四步走:先通过
run_id找到问题请求;再看 trace timeline 定位慢节点或失败节点;接着展开对应 span 检查 prompt、tool args、tool result、上下文快照;最后和成功样本 trace 对比,确认问题来自提示词、工具返回、上下文丢失、权限、模型输出偏差或外部依赖。落地上可以直接接 Langfuse。它是面向 LLM agent 的开源 tracing 平台,支持 trace/span UI、prompt 记录、token 成本、延迟统计、错误追踪和会话级分析,适合快速把日志流升级成可视化调用链。
可能的追问点#
- trace、span、log、metric 的区别是什么?
- Agent 的 trace 里哪些字段必须脱敏?
- 如何用 trace 复现一次失败的 agent 调用?
2026.06.26#
Go 使用协程要注意什么?#
评级:Go 并发常见
防泄漏:每个 goroutine 都要有明确退出路径,常用
context、关闭 channel、done channel 或任务队列收口。main函数退出时进程结束,其他 goroutine 会随进程结束;WaitGroup.Add要在启动 goroutine 前调用;高并发任务要用 worker pool、信号量或限流器控制 goroutine 数量。并发安全:多个 goroutine 共享读写同一份数据时,要用
sync.Mutex、sync.RWMutex、atomic、channel 或并发安全容器保护状态。开发和测试阶段配合go test -race检测数据竞争;全局单例初始化用sync.Once;复杂共享状态优先通过 channel 明确数据归属。闭包与 panic:循环里启动 goroutine 时显式拷贝循环变量,避免闭包引用同一个变量导致结果异常。子 goroutine 的 panic 要在自身内部用
defer+recover兜底,服务端常在 goroutine 入口、HTTP/RPC middleware、worker 边界统一处理 panic。Channel 规范:nil channel 的读写会永久阻塞;读取已关闭 channel 会立即返回零值和
ok=false;向已关闭 channel 写入会 panic;关闭已关闭 channel 会 panic。for range遍历 channel 依赖发送端关闭 channel 才能退出,工程上通常由发送方负责关闭。调度性能:纯 CPU 死循环要提供退出条件,必要时用
runtime.Gosched()或拆分任务让出调度机会;IO、锁等待、系统调用等阻塞点通常会触发调度器切换。循环里的select定时逻辑优先复用time.Timer或time.Ticker,减少反复time.After带来的定时器对象积累。工程规范:跨 goroutine 传递超时、取消和请求级信息统一使用
context;单个 goroutine 职责保持清晰;goroutine 之间通过明确的输入输出协作;日志、指标和 trace 要带上请求 ID 或任务 ID,方便排查泄漏、阻塞和异常退出。
可能的追问点#
- 循环里启动 goroutine,闭包捕获循环变量会出什么问题?为什么?怎么修复?
- 为什么父协程的
recover捕获不到子协程的panic?会造成什么后果? - 对 nil channel、已关闭的 channel 分别执行读、写、close 操作,各是什么结果?
2026.06.25#
MySQL 中如何排查慢 SQL?#
评级:MySQL 常见
排查慢 SQL 一般按“定位 SQL、分析执行计划、确认瓶颈、优化验证”四步走。
先开启慢查询日志
slow_query_log,设置合理的long_query_time,定位执行耗时高、扫描行数多、出现频率高的 SQL。线上也可以结合数据库监控、APM、performance_schema和业务链路 trace 找到具体接口与 SQL。用
EXPLAIN或EXPLAIN ANALYZE查看执行计划,重点看type、key、rows、filtered、Extra。比如type越接近const/ref/range通常越好;key表示实际选择的索引;rows表示预估扫描行数;Extra中重点关注Using filesort、Using temporary、Using index condition、Using where等信息。结合
SHOW PROFILE、optimizer_trace、performance_schema、InnoDB 状态和系统监控判断瓶颈类型。常见方向包括 CPU 计算、磁盘 IO、锁等待、回表过多、排序临时表、扫描行数过大、统计信息偏差、连接查询驱动表选择异常。优化时优先从索引和 SQL 写法入手。常见手段包括新建或调整联合索引,按
WHERE、JOIN、ORDER BY、GROUP BY设计索引顺序;减少SELECT *和返回数据量;优化深分页;拆分复杂关联查询;让条件表达式保持索引列原样;更新统计信息。优化后在测试环境和灰度环境验证执行计划、扫描行数、响应耗时和业务结果。慢 SQL 优化要同时关注读性能、写入成本、索引维护成本和线上回滚方案。
可能的追问点#
- 如果让你创建联合索引,你会怎么做?
type、key、rows、Extra等字段具体怎么看?- 索引失效的场景有哪些?参考 2026.06.24 每日一问。
2026.06.24#
索引失效的情况有哪些?#
评级:MySQL 常见
- 对索引列使用函数,索引列参与函数计算后,优化器通常难以直接利用原始索引有序性。
SELECT * FROM orders WHERE DATE(create_time) = '2026-06-24';- 对索引列使用表达式计算,查询条件中的索引列被运算包裹后,B+ 树索引上的原始值难以直接匹配。
SELECT * FROM orders WHERE score + 10 = 100;- 存在索引隐式类型转换,字段类型和查询值类型差异会触发转换,转换发生在索引列一侧时会影响索引使用。
SELECT * FROM orders WHERE user_id = 123456;- 联合索引违反最左前缀原则。假设联合索引是
(user_id, create_time, status),查询跳过最左列或范围条件后继续使用后续列,都可能导致部分索引利用受限。
-- 缺少最左列 user_id
SELECT * FROM orders WHERE status = 1;
-- create_time 是范围条件,status 的索引利用可能受限
SELECT * FROM orders
WHERE user_id = 1
AND create_time > '2026-06-01'
AND status = 1;可能的追问点#
- 联合索引的最左前缀原则,
ORDER BY和GROUP BY也遵守吗? IN和OR会导致索引失效吗?
2026.06.23#
Claude Code 与 Codex 的技术区别#
评级:群友八股(网易一面、二面)
- 子智能体架构
Claude Code 的子智能体设计遵循上下文隔离原则。每个子智能体运行在独立上下文窗口中,支持工具约束、配置复用和成本控制。其核心价值在于隔离性、可控性和任务边界清晰。
Codex 采用并行专用智能体模式,通过 specialized agents 执行子任务工作流,最终汇总结果。二者的工程侧重点可以这样概括:Claude Code 重视隔离性,Codex 强调并行吞吐。
- 上下文管理机制
Claude Code 通过自动压缩历史对话,将有效信息密度提升到更高水平,在有限 token 窗口内维持任务连贯性。
Codex 结合云端异步智能体的优势,利用服务端算力进行更激进的上下文重组。
从实测体验看:
- Claude Code:压缩策略保守,优先保证信息完整性;适合代码重构、大型 PR 审查等精确性要求高的场景。
- Codex:压缩策略激进,优先保证窗口利用效率;适合多文件并行修改、批量重构等吞吐量优先的场景。
- MCP 协议与工具生态
模型上下文协议(MCP)是 Claude Code 先发的重要基础设施。它定义了一套标准化的工具调用接口,允许第三方服务以插件形式接入。
Codex 后续跟进 MCP 能力,同时工具生态更依赖 OpenAI 自有体系。
- 技术栈差异
- Claude Code:命令行作为核心入口,通过 hooks、skills、MCP 插件向外扩展,开发者工作流深度优先。
- Codex:命令行、IDE、桌面 App、移动端和云端任务共同覆盖,访问覆盖面优先。
可能的追问点#
- Claude Code 的“自动压缩”是语义压缩还是统计压缩?
- 子智能体的故障隔离边界是什么?
2026.06.22#
从用户输入一个地址到浏览器显示画面,中间经历了什么?#
评级:常见八股(字节一面、腾讯一面、TME 一面、快手一面真题)
当用户在浏览器输入地址(URL)并回车后,浏览器会解析 URL 得到协议、域名等信息,先查本地 DNS 缓存和 Hosts 文件,未命中则向本地 DNS 服务器发起递归查询,最终从权威 DNS 拿到目标服务器 IP。
浏览器会根据协议判断连接方式。HTTP 通常在 TCP 三次握手后发送请求;HTTPS 会在 TCP 三次握手后继续进行 TLS/SSL 握手,完成身份认证、加密算法协商和会话密钥生成。
连接建立后,浏览器按 HTTP/HTTPS 协议组装请求报文发送给服务器。服务器处理请求,可能会查询数据库、访问缓存、调用内部接口,然后返回 HTML、状态码、响应头和响应体。
浏览器收到 HTML 后开始解析文档,构建 DOM 树;解析 CSS 构建 CSSOM 树;二者合成渲染树。遇到 JavaScript 脚本时,根据脚本加载方式执行对应逻辑,脚本可能修改 DOM、CSSOM 或继续发起网络请求。
渲染阶段会经历样式计算、布局(Reflow)、绘制(Paint)和合成(Composite),最终把页面像素显示到屏幕上。页面中的图片、CSS、JS、字体等资源会继续按依赖关系拉取,异步请求也可能继续更新页面内容。
可能的追问点#
- DNS 协议属于哪个层,具体流程是?
- HTTPS 和 HTTP 的区别?
- TLS/SSL 的过程?
2026.06.18#
在 RAG 中,常见的分块策略有哪些?区别是什么?#
评级:AI 基础知识
常见的分块策略有六种:自然结构分块、固定大小分块、滑动窗口分块、递归分块、语义分块和混合分块。
- 自然结构分块:按文档原有格式拆分,遇到标题、空行、章节编号、句号这些天然分隔符就切分。Markdown 文档可以按
###切,普通文档可以按段落空行切。 - 固定大小分块:按固定字符数、词数或 token 数均匀切分。比如每 500 tokens 切一块,实现简单,适合快速处理大量非结构化文本。
- 滑动窗口分块:在固定大小基础上加入重叠机制。相邻两个块之间保留 10% 到 20% 的重叠内容,避免关键信息刚好卡在分界线上被截断。
- 递归分块:分层拆分,先按大结构粗分,超长的部分再递归细化。比如先按章节切,章节太长就按段落切,段落还长就按句子切,直到块大小达标。
- 语义分块:用 NLP 模型判断语义边界,确保每个块是完整的语义单元。它会识别一段话是否讲完、下一段是否进入新话题。
- 混合分块:组合使用多种策略。常见做法是先按结构或固定大小快速处理,再对关键部分做语义优化,形成粗筛加精修的流程。
2026.06.17#
Go map 新的底层实现(Swiss Table)#
评级:Go 新版本知识
完整内容参考:飞书文档
- 面试中可以把旧版
hmap和新版 Swiss Table 放在一起讲。旧版hmap的核心是hmap + bucket + overflow bucket;Go 1.24 的新实现基于 Swiss Tables,核心是开放寻址、8 槽 group、control word 和探测序列。 - 旧版
hmap的一个 bucket 通常保存 8 个 key/value,并通过tophash快速筛候选;冲突较多时挂 overflow bucket。查找可能沿着 overflow 链逐个扫描,链式访问容易带来更多 cache miss。 - Swiss Table 使用开放寻址,key/value 存在 table 的槽位里。发生冲突后,根据 probe sequence 寻找后续 group 和空槽。它把哈希分成 H1/H2:H1 用于定位 table 和 group,H2 存在 control byte 里做快速候选匹配。
- 新实现的关键优化是 group + control word。一个 group 有 8 个槽和 8 字节控制字,控制字记录每个槽的状态和 H2。查找时先用位运算在控制字里批量筛出候选槽位,再对少量 key 做精确比较,把逐槽扫描变成批量候选筛选。
- 旧版
hmap在冲突严重时依赖 overflow bucket,内存局部性和扫描路径较差;Swiss Table 把数据组织在连续 group 中,减少指针追踪,提升缓存友好性。 - 内存和扩容方面,旧版
hmap在扩容阶段维护buckets、oldbuckets和迁移状态,冲突多时还会产生额外溢出桶成本;Swiss Table 去掉 overflow bucket 链,通过更高装载率和紧凑元数据提升空间利用率。经典 Swiss Table 的目标负载率接近 7/8。 - Go 的新 map 为了保持渐进扩容,把一个 map 拆成多个独立 table,每个 table 覆盖一段 key 空间,单个 table 最多 1024 个 entry;上层用 hash 的高位选择 table。这样既能引入 Swiss Table 的查找优势,又能控制单次插入的扩容工作量。
- 面试回答时,把差异落在四点:冲突处理从 overflow 链转向开放寻址和探测序列;候选匹配从逐项扫描转向 control word 批量筛选;内存布局从链式溢出桶转向连续 group;扩容组织从单组 bucket 迁移转向多 table 下的渐进增长。
2026.06.16#
协程、线程、进程的区别#
评级:基础知识
- 进程是程序运行时的资源容器,拥有独立地址空间、文件句柄、虚拟内存和权限上下文。进程之间通过管道、Socket、共享内存、消息队列等 IPC 方式通信。
- 进程隔离性强,稳定性和安全性更高。一个进程崩溃通常影响自身范围;代价是创建、销毁和上下文切换成本更高,切换时涉及页表、寄存器、内核资源等状态。
- 线程是进程内的执行单元,也是 CPU 调度的基本单位。多个线程共享所属进程的地址空间、堆、全局变量和文件句柄,每个线程保留自己的栈、寄存器和线程局部数据。
- 线程通信成本低,共享数据访问方便;共享内存也带来数据竞争风险,工程上需要互斥锁、读写锁、条件变量、原子操作等机制保证并发安全。
- 协程是用户态轻量级执行单元,由语言运行时或协程库调度。协程通常拥有更小的初始栈,创建成本低,切换时主要保存和恢复少量上下文,调度路径更短。
- Go 的 goroutine 可以理解为 Go runtime 管理的轻量级协程。goroutine 初始栈很小,并按需增长;运行时通过 GMP 模型把大量 goroutine 调度到少量系统线程上执行。
- 协程之间共享变量依然需要并发控制。Go 中常见做法是用 channel 传递数据,用
sync.Mutex、sync.RWMutex、atomic等工具保护共享状态。 - 面试回答时按资源归属和调度层级讲清楚:进程是资源隔离单位,线程是内核调度单位,协程是用户态调度单位;从进程到线程再到协程,隔离性逐步降低,创建和切换成本也逐步降低。
2026.06.15#
RocketMQ 如何实现延迟消息,不同版本之间有什么区别?#
评级:中级高频
- 延迟消息指生产者把消息发送到 Broker 后,由 Broker 持有一段时间,在指定延迟或时间点到达后再投递给消费者。常见场景有订单超时关闭、支付状态检查、定时提醒、失败任务退避重试。
- RocketMQ 4.x 通过固定延迟等级实现延迟消息。生产者发送消息时设置
delayTimeLevel,比如某个等级代表 30 分钟;Broker 收到延迟消息后,会保存原始 Topic、QueueId 等属性,并把消息改写到内部 TopicSCHEDULE_TOPIC_XXXX。 SCHEDULE_TOPIC_XXXX是 4.x 暂存延迟消息的内部 Topic。每个延迟等级对应这个 Topic 下的一个队列,比如 1s、5s、10s、30s、1m、5m、10m、30m、1h、2h 等等级会落到对应队列中。ScheduleMessageService是 Broker 内部负责延迟消息调度的服务。它会周期性扫描SCHEDULE_TOPIC_XXXX的各个队列,根据消息写入时间和延迟等级计算投递时间,发现到期消息后恢复原始 Topic 和 QueueId。- 到期消息需要重新写入 CommitLog。RocketMQ 的消费依赖业务 Topic 下的 ConsumeQueue 索引;延迟阶段的消息属于
SCHEDULE_TOPIC_XXXX,到期后恢复成真实 Topic 并重新写入 CommitLog,才能生成业务 Topic 的 ConsumeQueue 索引,让消费者拉取到消息。 - 4.x 的特点是实现简单、稳定,和原有存储模型结合紧密;能力边界是固定延迟等级。业务需要 37 分钟后执行或明天上午 10 点投递时,通常要调整等级配置或在业务层增加调度能力。
- RocketMQ 5.x 增强为更完整的定时/延迟消息能力,支持更灵活的投递时间。5.x 引入 Timer Message 相关机制,用 TimerLog、TimerWheel 等思路管理定时消息,更适合大规模、细粒度、指定时间点投递的场景。
- 延迟消息适合理解为到期后尽快投递。实际投递时间会受到 Broker 扫描调度、系统负载、存储性能、消息堆积和消费者处理能力影响,适合秒级或最终执行类场景。
- 电商订单超时关闭是典型场景。订单创建成功后发送一条 30 分钟延迟消息;消费者收到后查询订单状态,状态仍为待支付时关闭订单并释放库存,状态已支付时结束处理。消费者要按订单状态做幂等判断,保证重复投递时结果稳定。
2026.06.12#
分布式系统中的 CAP 理论和 BASE 理论#
评级:基础知识
- CAP 分别是 Consistency(一致性)、Availability(可用性)和 Partition Tolerance(分区容错性)。一致性强调所有节点读到同一份最新数据;可用性强调每个请求都能收到响应;分区容错性强调网络分区发生时系统仍能继续运行。
- CAP 描述的是分布式系统在网络分区场景下的取舍。网络分区发生后,节点之间通信异常;系统要么等待数据同步后再响应,偏向一致性;要么先响应请求并接受短暂状态差异,偏向可用性。
- 真实分布式系统里网络故障、机房抖动、跨地域链路异常都属于常态风险,所以 P 一般必须保留。发生分区时,核心选择集中在 CP 和 AP。
- CP 系统优先保证一致性,适合银行转账、支付扣款、库存扣减、分布式锁、配置中心等场景。发生分区时,部分请求可能等待、失败或降级,以保证关键数据准确。
- AP 系统优先保证可用性,适合点赞数、浏览量、商品详情缓存、推荐特征、评论计数等场景。系统可以接受短暂状态差异,并通过异步同步、补偿任务和对账机制达到最终一致。
- BASE 是 Basically Available、Soft State、Eventually Consistent,分别对应基本可用、软状态和最终一致性。它是 AP 思路在工程上的落地方式,强调高并发下先保证核心服务可用,再通过异步链路收敛数据。
- ACID 追求事务内的原子性、一致性、隔离性和持久性,适合强一致事务;BASE 接受短暂软状态,适合高并发、跨服务、跨存储的分布式业务。
- 电商系统中,商品详情页库存展示可以偏 AP,通过缓存刷新、消息队列同步和定时校准保持最终一致;下单扣库存和支付扣款偏 CP,要保证扣减唯一、余额准确和订单状态一致。
2026.06.11#
OSI 七层模型和 TCP/IP 四层模型#
评级:基础知识
- OSI 是国际标准化组织提出的七层理论参考模型,自下而上分别是物理层、数据链路层、网络层、传输层、会话层、表示层和应用层。
- TCP/IP 是互联网实际使用的四层模型,自下而上分别是网络接口层、网络层、传输层和应用层。
- 物理层负责比特流在网线、光纤、无线电等介质上传输;数据链路层负责同一链路内的帧传输、MAC 地址、差错检测和交换机转发。
- 网络层负责跨网络寻址和路由,典型协议是 IP、ICMP;传输层负责端到端通信,典型协议是 TCP 和 UDP。
- 会话层负责会话建立、维护和释放;表示层负责数据格式、编码、压缩和加密;应用层面向具体应用协议,比如 HTTP、DNS、SMTP、FTP。
- 映射关系上,TCP/IP 的网络接口层对应 OSI 的物理层和数据链路层;TCP/IP 的网络层、传输层分别对应 OSI 的网络层、传输层;TCP/IP 的应用层对应 OSI 的会话层、表示层和应用层。
- 面试回答时可以先背层级,再讲职责和协议例子:OSI 更适合理论分层记忆,TCP/IP 更贴近真实网络协议栈。
2026.06.09#
LRU 和 LFU 是什么?适用于什么场景,底层的实现原理是什么?#
评级:LRU 手撕高频,LFU 可能会被提及
- LRU 是 Least Recently Used,按最近访问时间淘汰数据。核心思想是时间局部性:最近访问过的数据,短期内再次访问概率更高。缓存容量满时,优先淘汰最久没有被访问的数据。
- LFU 是 Least Frequently Used,按访问频次淘汰数据。核心思想是稳定热点:访问次数越高的数据,未来继续被访问的概率更高。缓存容量满时,优先淘汰访问次数最低的数据。
- LRU 适合热点变化快、最近访问更能代表未来访问的场景,比如页面缓存、本地查询结果缓存、操作系统页缓存、InnoDB Buffer Pool 类缓存。
- LFU 适合长期热点明显、访问频次稳定、希望降低偶发访问干扰的场景,比如 Redis 热点 Key、CDN 静态资源缓存、商品详情缓存、推荐特征缓存。
- LRU 的底层实现通常是哈希表 + 双向链表。哈希表负责通过 key 在 O(1) 时间定位节点;双向链表负责维护访问顺序,链表头部放最近访问的数据,链表尾部放最久访问的数据。
Get命中后把节点移动到头部,Put命中后更新值并移动到头部,容量满时删除尾部节点。 - LFU 的底层实现通常是
key -> node哈希表 +freq -> 双向链表哈希表 +minFreq。每个节点记录 key、value、freq;同一访问频次的节点放在同一条双向链表里,链表内部再按 LRU 顺序排列。Get命中后频次加一,把节点从旧频次链表移动到新频次链表;Put容量满时,从minFreq对应链表的尾部淘汰一个节点。 - 工程取舍:LRU 实现简单、维护成本低,对最近访问敏感;LFU 更能保留长期高频热点,但频次统计有额外维护成本,热点迁移时会受历史频次影响,生产实现常加入频次衰减或近似统计。
- 面试收口:LRU 看最近一次访问时间,核心结构是
map + 双向链表;LFU 看访问次数,核心结构是map + freqMap + minFreq + 双向链表。手写重点是保证Get、Put、移动节点和淘汰节点都是 O(1)。
LRU 示例代码#
package main
import "fmt"
type lruNode struct {
key, value int
prev, next *lruNode
}
type LRUCache struct {
capacity int
items map[int]*lruNode
head *lruNode
tail *lruNode
}
func NewLRUCache(capacity int) *LRUCache {
head, tail := &lruNode{}, &lruNode{}
head.next = tail
tail.prev = head
return &LRUCache{
capacity: capacity,
items: make(map[int]*lruNode),
head: head,
tail: tail,
}
}
func (c *LRUCache) Get(key int) int {
node, ok := c.items[key]
if !ok {
return -1
}
c.moveToHead(node)
return node.value
}
func (c *LRUCache) Put(key, value int) {
if c.capacity <= 0 {
return
}
if node, ok := c.items[key]; ok {
node.value = value
c.moveToHead(node)
return
}
node := &lruNode{key: key, value: value}
c.items[key] = node
c.addToHead(node)
if len(c.items) > c.capacity {
removed := c.removeTail()
delete(c.items, removed.key)
}
}
func (c *LRUCache) moveToHead(node *lruNode) {
c.remove(node)
c.addToHead(node)
}
func (c *LRUCache) addToHead(node *lruNode) {
node.prev = c.head
node.next = c.head.next
c.head.next.prev = node
c.head.next = node
}
func (c *LRUCache) remove(node *lruNode) {
node.prev.next = node.next
node.next.prev = node.prev
}
func (c *LRUCache) removeTail() *lruNode {
node := c.tail.prev
c.remove(node)
return node
}
func main() {
cache := NewLRUCache(2)
cache.Put(1, 1)
cache.Put(2, 2)
fmt.Println(cache.Get(1)) // 1
cache.Put(3, 3)
fmt.Println(cache.Get(2)) // -1,key 2 被淘汰
}LFU 示例代码#
package main
import (
"container/list"
"fmt"
)
type lfuEntry struct {
key, value int
freq int
}
type LFUCache struct {
capacity int
size int
minFreq int
items map[int]*list.Element
freqs map[int]*list.List
}
func NewLFUCache(capacity int) *LFUCache {
return &LFUCache{
capacity: capacity,
items: make(map[int]*list.Element),
freqs: make(map[int]*list.List),
}
}
func (c *LFUCache) Get(key int) int {
element, ok := c.items[key]
if !ok {
return -1
}
entry := element.Value.(*lfuEntry)
c.increaseFreq(element)
return entry.value
}
func (c *LFUCache) Put(key, value int) {
if c.capacity <= 0 {
return
}
if element, ok := c.items[key]; ok {
entry := element.Value.(*lfuEntry)
entry.value = value
c.increaseFreq(element)
return
}
if c.size == c.capacity {
listByFreq := c.freqs[c.minFreq]
back := listByFreq.Back()
entry := back.Value.(*lfuEntry)
delete(c.items, entry.key)
listByFreq.Remove(back)
c.size--
}
entry := &lfuEntry{key: key, value: value, freq: 1}
listByFreq := c.getList(1)
c.items[key] = listByFreq.PushFront(entry)
c.minFreq = 1
c.size++
}
func (c *LFUCache) increaseFreq(element *list.Element) {
entry := element.Value.(*lfuEntry)
oldFreq := entry.freq
oldList := c.freqs[oldFreq]
oldList.Remove(element)
if oldFreq == c.minFreq && oldList.Len() == 0 {
c.minFreq++
}
entry.freq++
newList := c.getList(entry.freq)
c.items[entry.key] = newList.PushFront(entry)
}
func (c *LFUCache) getList(freq int) *list.List {
if c.freqs[freq] == nil {
c.freqs[freq] = list.New()
}
return c.freqs[freq]
}
func main() {
cache := NewLFUCache(2)
cache.Put(1, 1)
cache.Put(2, 2)
fmt.Println(cache.Get(1)) // 1,key 1 的频次变成 2
cache.Put(3, 3)
fmt.Println(cache.Get(2)) // -1,key 2 频次最低,被淘汰
fmt.Println(cache.Get(3)) // 3
}2026.06.08#
短视频平台点赞系统高并发设计#
评级:goclub 原创题、场景题
场景:某条视频突然爆火,5 分钟内收到 1000 万次点赞请求,峰值 20 万 QPS。用户点完赞后,需要立刻看到“已点赞”状态,视频详情页的点赞数也要尽快变化,同时系统还要更新作者通知、排行榜、推荐特征。
- 核心问题有五类。第一类是写入压力,数据库直接承接 20 万 QPS 的点赞记录和计数更新会被打满。第二类是一致性压力,用户点赞状态、视频点赞数、数据库记录、缓存计数在短时间内会出现状态差异。第三类是热点压力,单条爆款视频会形成热点 key、热点行和热点消息分区。第四类是下游放大,通知、排行榜、推荐特征等任务会被点赞流量同步放大。第五类是重复请求,用户连点、网络重试、客户端补偿都会产生重复点赞。
- 请求链路先保证强体验数据。点赞接口同步完成鉴权、限流、幂等判断和用户点赞状态写入,让用户立刻看到“已点赞”。用户维度可以用
user_id + video_id做幂等键,落到 Redis Set、Hash 或独立状态 key,也可以用 MySQL 唯一索引做最终兜底。 - 视频点赞数走缓存计数和异步落库。接口阶段优先更新 Redis 计数,比如对
video_like_count:{video_id}做原子增量,详情页读取 Redis 展示最新计数;数据库点赞记录和视频计数字段通过 MQ 异步批量写入,减少单次请求的数据库写放大。 - 下游任务用消息队列拆分。点赞主事件进入 MQ 后,消费者分别处理点赞记录落库、计数汇总、作者通知、排行榜、推荐特征。核心链路优先处理“状态和计数”,通知、推荐特征、排行榜可以按优先级消费,积压时允许延迟、采样、合并或临时降级。
- 幂等和顺序要单独设计。幂等键使用
user_id + video_id + action,重复点赞直接返回当前状态。点赞和取消点赞可以带版本号、时间戳或操作序列,消费者按最新操作生效,避免乱序消息把用户状态回滚。 - 热点数据要削峰。入口层做用户维度和视频维度限流,Redis 侧对热点 key 做本地缓存、分片计数或批量聚合,消费者侧把多次点赞合并成批量 SQL 或定时 flush。排行榜可以按时间窗口聚合,例如分钟级 ZSet,再周期性合并到更长窗口。
- 积压要可观测、可降级、可补偿。监控 MQ 堆积量、消费延迟、Redis 失败率、数据库批量写耗时和点赞状态差异。消费速度低于生产速度时扩容消费者、扩大批量、关闭低优先级任务、对通知做聚合提醒。后续通过对账任务扫描 Redis、MQ、DB,补齐漏写和修正计数。
- 面试收口:这个场景的核心设计是“同步保证用户体验,异步承接高吞吐,下游解耦削峰,幂等保证重复请求安全,监控补偿保证最终一致”。用户自己的点赞状态优先强保证,视频点赞数秒级变化,通知、排行榜、推荐特征按优先级最终完成。
2026.06.05#
Redis 大 Key 会造成什么影响,如何解决大 Key 问题?#
评级:中高级八股、中频
定义:大 Key 指单个 Key 对应的 value 很大,或集合类 Key 的成员数量很多、成员内容很大。
例子:5MB 的 String、10 万成员的 List/ZSet、总大小几十 MB 的 Hash。
- 产生原因:业务数据集中堆到一个 Key,Hash、List、ZSet 持续膨胀,热点业务数据天然集中。
- 主要影响:大 value 读写占用 Redis 主线程,序列化、网络传输、后续请求排队时间变长。
- 系统风险:内存压力、网络带宽压力、复制延迟、主从切换风险、同步删除卡顿都会上升。
fork与 AOF 风险:BGSAVE、AOF rewrite、全量同步会触发fork;大 Key 修改会带来 COW 内存上涨;AOF 写入和fsync延迟容易抖动。- 发现方式:低峰期用
redis-cli --bigkeys;用SCAN + MEMORY USAGE定时巡检;集合类用HLEN、LLEN、SCARD、ZCARD统计规模。 - 治理思路:把一个大 Key 拆成多个小 Key,按用户 ID、业务 ID、时间片或哈希分片存储;集合类设置最大成员数、分页存储、定期归档。
- 线上迁移:新 Key 分片、双写、分批迁移、灰度切读、异步清理、监控回滚。
例子:user:info:all 是大 Hash,可以改成 user:info:{shardId},按 hash(uid) % 100 分片;应用先双写,后台用 HSCAN 分批迁移,配合 pipeline 和 sleep 控制压力;切读后用 UNLINK 或 HSCAN + HDEL 清理老 Key。
面试收口:大 Key 治理的核心是把单次大操作拆成多次小操作,把集中风险拆成可灰度、可监控、可回滚的迁移流程,同时关注主线程阻塞、内存、网络、复制、fork、COW、AOF 和 fsync。
2026.06.04#
RAG 完整链路#
评级:中难度、中频
链路:文档切块 -> embedding -> 向量库 -> topK 检索 -> prompt 拼接 -> LLM 回答。
- 文档入库:PDF、网页、Markdown、数据库内容先清洗,去掉无效格式、噪声文本和重复内容,再按段落、标题层级或语义边界切成 chunk。
- 向量化:每个 chunk 通过 Embedding 模型转成向量,连同原文、标题、来源、权限、时间等元数据一起写入 Milvus、FAISS、Qdrant、pgvector 等向量库。
- 用户提问:用户问题也通过同一个或兼容的 Embedding 模型转成向量,用余弦相似度、内积或 L2 距离检索相关 chunk。
- 召回与重排:先从向量库粗召回 TopK,再用 Reranker 对问题和候选片段做相关性打分,选出最相关、信息密度最高的片段进入上下文。
- 生成答案:把用户问题、检索片段、回答规则、引用要求和安全边界拼成 Prompt,交给 LLM 生成最终回答。
- 工程优化:常见优化包括 chunk 大小和 overlap 调整、混合检索、元数据过滤、权限过滤、rerank、上下文压缩、引用溯源、缓存和效果评估。
- 面试收口:RAG 的核心是“先检索,再生成”。链路重点看文档质量、切块策略、向量召回、重排质量、Prompt 组织和答案可追溯性。
2026.06.03#
I/O 多路复用解决什么问题,常用 I/O 多路复用技术对比#
评级:中高难度、中频
- I/O 多路复用主要解决用少量线程同时管理大量 I/O 对象的事件等待问题,常用于网络连接、终端输入、磁盘读写和高并发 I/O 服务。
select的优点是跨平台、接口简单、历史最久;特点是使用 fd 位图表示监听集合。它受FD_SETSIZE限制,每次调用都需要传入 fd 集合,内核和用户态之间存在拷贝成本,返回后还要线性扫描找就绪 fd。poll用结构体数组替代位图,突破了select固定 fd 位图的限制。它仍然需要每次传入全量 fd 数组,返回后也要线性扫描,所以连接数很大时效率会下降。epoll的基本流程是通过epoll_create创建实例,通过epoll_ctl注册或修改 fd,通过epoll_wait等待就绪事件。它把 fd 集合长期维护在内核里,返回的是就绪事件列表,适合连接很多、活跃连接较少的高并发网络场景。epoll支持 LT 和 ET 两种触发模式。LT 是水平触发,只要 fd 仍可读或可写,就会持续通知;ET 是边缘触发,只在状态变化时通知一次,通常需要配合非阻塞 I/O 循环读写直到EAGAIN。- 面试收口:
select胜在通用简单,poll解决固定 fd 位图限制,epoll通过内核维护监听集合和返回就绪列表提升高并发场景效率,Linux 下 Nginx、Redis 这类服务通常依赖这种模型。
2026.06.02#
HTTP 状态码表示的意思#
评级:基础知识、中低频
- HTTP 状态码是服务端返回给客户端的三位数字,用来表示本次请求的处理结果。第一位数字表示状态大类,后两位表示具体含义。
1xx表示提示信息,代表请求已经收到,客户端可以继续后续操作,常见如100 Continue。2xx表示请求已被成功处理,常见如200 OK表示请求成功,201 Created表示资源创建成功,204 No Content表示请求成功且响应体为空。3xx表示重定向或缓存相关结果,常见如301 Moved Permanently表示永久重定向,302 Found表示临时重定向,304 Not Modified表示客户端可以继续使用本地缓存。4xx表示客户端侧请求条件存在问题,常见如400 Bad Request表示请求格式或参数有问题,401 Unauthorized表示需要身份认证,403 Forbidden表示权限受限,404 Not Found表示资源路径缺失或地址错误。5xx表示服务端处理链路出现故障,常见如500 Internal Server Error表示服务端内部处理异常,502 Bad Gateway表示网关收到上游异常响应,503 Service Unavailable表示服务暂时过载或维护,504 Gateway Timeout表示网关等待上游响应超时。- 面试收口:HTTP 状态码可以按首位数字快速判断请求结果,
2xx看成功处理,3xx看跳转和缓存,4xx看客户端请求条件,5xx看服务端处理链路。
2026.06.01#
TCP 和 UDP 的对比#
评级:基础知识八股、中频
- TCP 是面向连接的可靠字节流协议,通信前需要建立连接,通信过程中通过确认应答、超时重传、序列号和校验机制保证数据可靠到达。
- UDP 是无连接的数据报协议,发送前无需建立连接,头部更小,交付路径更短,适合对时延敏感或由应用层自定义可靠性的场景。
- TCP 提供有序交付、流量控制和拥塞控制,可以把数据按字节流稳定交给上层应用;UDP 以数据报为单位交付,每个报文保留边界,应用层需要自行处理丢包、乱序和重传策略。
- TCP 常用于文件传输、网页访问、数据库连接、RPC 等可靠性优先的场景;UDP 常用于实时音视频、在线游戏、DNS、QUIC 等更关注时延和灵活控制的场景。
- 面试收口:按连接、可靠性、顺序、控制和应用场景五个维度对比即可,核心是 TCP 追求可靠有序传输,UDP 追求低开销和低时延。
2026.05.29#
Skill 的工作流程,如何写自己的 Skill?#
评级:AI Agent 工程、中低频
- Skill 是给 AI Agent 使用的专项能力包,核心作用是把某类任务的触发条件、执行流程、领域知识、规则约束、工具脚本和素材模板沉淀下来,让 Agent 遇到同类任务时可以按稳定流程处理。
- 一个 Skill 的工作流程通常分为四层:触发条件决定什么时候启用;工作流程规定先做什么、后做什么;知识与规则提供领域判断标准和输出约束;工具与素材提供脚本、模板、参考资料、图片、字体等可复用资源。
- 写自己的 Skill 前,先想清楚它解决什么问题、面向哪些请求、输入是什么、输出是什么、成功标准是什么。适合固化的内容包括重复出现的步骤、固定格式的交付物、容易遗漏的检查项、领域术语、接口规范、脚本命令和模板文件。
- 一个常见 Skill 目录包含
SKILL.md,以及可选的scripts/、references/、assets/。SKILL.md写触发描述和核心流程;scripts/放稳定可执行的脚本;references/放按需读取的长文档;assets/放最终产物要复用的素材。 SKILL.md的重点是简洁、可执行、可触发。description要覆盖用户可能的说法,因为 Agent 会先通过名称和描述判断是否加载 Skill;正文保留关键步骤和判断规则,详细资料放到引用文件里按需读取。- 可以按下面的模板起草:
# Skill 名称
## 1. 适用场景
这个 Skill 用于:[描述它解决的任务类型]
典型触发请求:
- [用户会怎么说 1]
- [用户会怎么说 2]
- [用户会怎么说 3]
## 2. 目标
使用这个 Skill 后,应该产出:[最终交付物]
成功标准:
- [标准 1]
- [标准 2]
- [标准 3]
## 3. 输入
用户可能提供:
- [输入类型 1:文本 / 文件 / 链接 / 数据 / 图片]
- [输入类型 2]
- [输入类型 3]
需要主动确认的信息:
- [关键信息 1]
- [关键信息 2]
## 4. 输出
默认输出格式:
- 类型:[Markdown / JSON / 表格 / 代码 / 报告 / 设计稿]
- 结构:[章节、字段或模块]
- 粒度:[简版 / 标准版 / 详细版]
输出模板:
[在这里放固定输出结构]- 面试收口:Skill 的本质是把可复用的任务经验产品化,先定义触发条件和输入输出,再沉淀流程、规则、工具和素材,让 Agent 在同类任务上稳定地产出高质量结果。
2026.05.28#
什么是 Function Calling?原理是什么?#
评级:中级八股、中频
- Function Calling 是大模型根据用户输入生成结构化函数调用请求的能力。它让模型可以把自然语言意图转成可执行的工具调用,比如查数据库、调接口、搜索文档、执行计算或操作业务系统。
- 核心输入包括用户问题和工具定义。工具定义通常用 JSON Schema 描述函数名、函数说明、参数字段、参数类型、必填项和约束条件,模型会根据上下文判断需要调用哪个工具以及传入什么参数。
- 执行流程是:应用把用户问题和工具列表传给模型;模型输出
tool_calls,其中包含函数名和结构化参数;应用侧解析tool_calls并真正执行函数;函数执行结果再返回给模型;模型基于工具结果生成最终回复。 - Function Calling 的关键点是模型负责“选择工具和生成参数”,应用负责“执行工具和控制权限”。生产环境需要做好参数校验、权限控制、超时控制、重试、审计日志和异常兜底。
- 面试收口:Function Calling 本质是让大模型从自然语言生成结构化工具调用请求,通过外部函数补齐模型自身缺少的实时数据、业务能力和执行能力。
2026.05.27#
谈谈你对 RAG 的理解?#
评级:中级八股、中频
- RAG 是 Retrieval-Augmented Generation,中文常叫检索增强生成。核心目标是让大模型在回答前先检索外部知识,再结合检索结果生成答案,从而提升知识时效性和答案准确性。
- 典型流程是:先把文档清洗、切分成 chunk,再通过 embedding 模型转成向量并写入向量数据库;用户提问时也会被 embedding 成向量;系统根据向量相似度召回相关文档片段;最后把问题和召回内容一起放进 prompt 交给模型回答。
- RAG 常用于企业知识库、客服问答、内部文档助手、法律合同检索、技术文档问答等场景,尤其适合知识经常更新、答案需要引用依据、私有数据保留在外部知识库中统一管理的业务。
- RAG 的效果重点看文档质量、切分策略、embedding 模型、召回策略、重排效果和 prompt 组织。工程上常见优化包括混合检索、rerank、元数据过滤、引用溯源和权限过滤。
- 面试收口:RAG 的本质是“先检索,再生成”,用外部知识库给大模型补充上下文,让回答更贴近业务数据和最新资料。
2026.05.26#
如何查看网络性能指标?#
评级:高级八股、低频
- 先看基础连通性和时延。常用
ping查看延迟、丢包和抖动;traceroute或mtr查看链路路径和每一跳耗时;curl -w可以拆分 DNS、TCP 建连、TLS 握手、首字节和总耗时。 - 看网卡层指标。
ip -s link、ifconfig、ethtool -S eth0可以查看收发包量、错误包、丢包、重传、队列丢弃、网卡速率和双工模式;sar -n DEV 1可以持续观察各网卡吞吐。 - 看 TCP 层指标。
ss -s查看连接整体状态;ss -antp查看具体连接、状态和进程;netstat -s或nstat查看 TCP 重传、连接失败、超时、reset 等统计;sar -n TCP,ETCP 1可以看主动连接、被动连接、重传和错误。 - 看带宽、流量和进程维度。
iftop、nload、vnstat看实时流量;nethogs看进程流量;iperf3做端到端带宽压测;tcpdump抓包分析三次握手、重传、reset、DNS 和应用协议问题。 - 线上排查建议按层次推进:应用耗时拆分、主机网卡指标、TCP 状态与重传、链路丢包与抖动、抓包验证。核心关注指标是延迟、丢包、吞吐、连接数、重传率、错误包、队列丢弃和带宽利用率。
2026.05.25#
map 扩容机制?#
评级:中级八股、中频
- Go 的
map底层是哈希表结构,核心对象是hmap。数据存放在一组 bucket 中,每个 bucket 通常保存 8 个 key/value,并配有tophash用于快速判断候选项;哈希冲突较多时会挂接 overflow bucket。 map查找时会先对 key 计算 hash,再根据低位定位 bucket,在 bucket 内结合tophash和 key 比较找到目标元素。插入时如果目标 bucket 放满,会使用 overflow bucket 承接冲突数据。- 扩容触发主要有两类:元素数量增长导致负载因子过高;overflow bucket 过多导致哈希表局部冲突严重。前者通常触发翻倍扩容,后者可能触发等量扩容,用于整理 bucket 和 overflow bucket。
- Go 的
map扩容采用渐进式搬迁。扩容开始后会分配新的 bucket 数组,旧 bucket 保存在oldbuckets中;后续每次读写 map 时顺带搬迁一部分旧 bucket,逐步把数据迁到新 bucket,降低一次性搬迁带来的停顿。 - 扩容期间访问 map 时,需要同时考虑新旧 bucket。查找、插入和删除会根据 bucket 的迁移状态决定访问旧表还是新表,并在操作过程中推进迁移。
- 面试收口:Go
map通过 bucket + overflow bucket 解决哈希冲突,通过负载因子和 overflow 数量触发扩容,通过渐进式搬迁平滑扩容成本。
2026.05.22#
defer 什么时候执行?多个 defer 的执行顺序,defer 的应用场景?#
评级:中级八股、中低频
defer会在当前函数返回前执行。函数执行到return、正常走到函数末尾,或者因为panic退出当前函数时,已经注册的defer都会按规则执行。- 多个
defer的执行顺序是后进先出,也就是最后注册的defer最先执行。 defer的参数会在注册时立即求值。比如defer fmt.Println(i)会保存当时的i值;闭包形式defer func(){ fmt.Println(i) }()会在真正执行时读取外层变量。defer常用于资源收尾,比如关闭文件、关闭连接、释放锁、提交或回滚事务、捕获 panic、统计耗时和统一记录日志。- 面试收口:
defer是函数级收尾机制,执行时机在函数退出阶段,顺序是 LIFO,参数求值发生在注册阶段。
2026.05.21#
MySQL 如何优雅分页?#
评级:中级八股、中频
- 普通分页通常写成
limit size offset n。MySQL 需要先扫描并取出offset + size条记录,再丢弃前面的offset条,页码越深扫描成本越高,深分页性能会明显变差。 - 游标分页基于主键或索引字段向后翻页,例如
where id > last_id order by id limit size。这种方式可以利用索引直接定位,性能稳定,适合信息流、列表滚动、时间线等连续翻页场景。 - 延迟关联适合需要返回完整行的深分页。先通过覆盖索引查出目标主键集合,再用主键回表查询完整数据,减少大量无效回表。
- 设计分页时要保证排序稳定。常见做法是使用唯一字段兜底,比如
order by create_time desc, id desc,游标也保存create_time和id,避免相同时间的数据顺序抖动。 - 面试收口:普通分页实现简单,深分页优先考虑基于索引的游标分页;需要跳页或兼容后台列表时,可以用延迟关联和覆盖索引降低扫描与回表成本。
2026.05.20#
RabbitMQ 的交换机类型以及适用场景#
评级:中级八股、中低频
- RabbitMQ 常见 Exchange 类型有
direct、fanout、topic和headers。 direct按 routing key 精确匹配,把消息路由到绑定 key 完全一致的队列,适合点对点分发、明确业务类型路由和任务队列。fanout会把消息广播到所有绑定到该交换机的队列,routing key 在路由时被忽略,适合系统通知、广播消息、日志分发和多消费者同时接收同一事件。topic按 routing key 的通配符模式匹配,*匹配一个单词,#匹配零个或多个单词,适合复杂主题路由,比如order.created、order.paid、user.*这类事件分发。headers根据消息头中的键值对匹配,适合按多个属性组合筛选的场景。实际业务里使用频率较低,因为配置和维护成本更高。- 面试收口:业务里最常用的是
direct和topic;简单精确路由用direct,广播用fanout,复杂主题匹配用topic,多属性头部匹配用headers。
2026.05.19#
Redis 宕机如何处理?#
评级:中级八股、中频
重点:快速恢复服务、确认数据是否丢失、排查原因、后续优化。
- 先确认 Redis 是否真的宕机,可以从应用报错、监控告警、连接测试、端口状态、进程状态和实例健康检查入手。单机实例优先快速重启恢复服务;主从、哨兵或 Cluster 环境先查看节点状态、主从复制状态、故障转移结果和客户端路由情况,再确定重启、切主、扩容或摘除异常节点。
- 恢复后重点确认数据是否完整。Redis 主要依靠 RDB 和 AOF 做持久化恢复:RDB 是某个时间点的快照,恢复速度快,但会丢失快照之后的写入;AOF 记录写命令,数据更完整,恢复时会重放日志。实例拉起后要检查加载日志、key 数量、核心业务数据、主从一致性和应用读写结果。
- 查看日志和监控排查宕机原因。常见方向包括内存打满、
maxmemory配置异常、磁盘空间不足、AOF rewrite 或fsync抖动、RDB fork 卡顿、慢查询、大 Key、网络抖动、机器 OOM、容器资源限制、主从复制异常和版本缺陷。定位原因后按问题类型处理,比如清理大 Key、调整内存策略、扩容、优化持久化参数或修复部署环境。 - 生产环境要做高可用和兜底。推荐使用主从 + Sentinel 或 Redis Cluster,开启合适的 RDB/AOF 持久化策略,配置监控告警,关注内存、连接数、慢日志、复制延迟、
fork耗时和磁盘 I/O;业务侧准备本地缓存、限流、降级、熔断和数据回源策略。 - 面试收口:Redis 宕机处理顺序是先恢复服务,再确认数据,再排查原因,最后补齐高可用、持久化、监控和降级能力。
2026.05.18#
HTTPS 的 S 是什么?为什么要有 TLS?TLS 的过程?#
评级:中高级八股、中频
- 讲出 HTTPS 的含义:HTTPS 里的 S 是 Secure,工程含义是 HTTP over TLS,也就是 HTTP 运行在 TLS 安全层之上。TLS 给 HTTP 增加加密、身份认证和完整性保护。
- 讲出为什么要有 TLS:HTTP 明文传输,内容容易被窃听、篡改和伪造;TLS 提供机密性、身份认证和完整性。机密性保证传输内容被加密;身份认证通过证书确认服务端身份;完整性保证数据传输过程中被改动时能被发现。
- 讲出 TLS 握手过程:客户端先发送
ClientHello,携带支持的 TLS 版本、加密套件、随机数、SNI、ALPN,以及 TLS 1.3 里的 key share;服务端返回ServerHello,选择 TLS 版本和加密套件,并返回证书链和自己的密钥协商材料;客户端校验证书链、域名和有效期,再通过 ECDHE 等算法和服务端协商共享密钥;双方基于共享密钥生成会话密钥。 - 讲出通信阶段:握手完成后,HTTP 数据使用对称加密传输,比如 AES-GCM 或 ChaCha20-Poly1305,同时结合认证标签完成完整性校验。整体思路是握手阶段完成身份认证和密钥协商,通信阶段使用对称加密传输数据。
- 面试收口:HTTPS 的 S 是 Secure,本质是 HTTP 运行在 TLS 之上;TLS 握手负责认证服务端身份、协商会话密钥并建立安全通道,后续 HTTP 数据使用对称加密传输。
2026.05.15#
sync.Map 和 map 的区别,sync.Map 原理#
评级:中级八股、中频
- 讲出普通
map的特点:map是 Go 的内置哈希表,性能高、类型安全,适合普通场景;多个 goroutine 共享读写时,通常配合sync.Mutex或sync.RWMutex做并发控制。 - 讲出
sync.Map的特点:sync.Map是标准库提供的并发安全 map,适合读多写少、key 相对稳定、缓存类场景,比如 key 写入一次后多次读取,或者多个 goroutine 主要读写不同 key。 - 讲出工程取舍:
sync.Map通过读写分离降低读路径开销,同时 API 基于any,使用时需要类型断言;写多、需要复杂复合操作、需要强类型约束的场景,普通map + Mutex/RWMutex更常用。 - 讲出核心原理:
sync.Map内部维护read和dirty两份数据。read是只读区,读操作先查read,命中时通过原子操作读取;dirty是写区,写操作和读穿透会进入加锁路径并访问dirty。 - 讲出晋升机制:
misses用来统计读read失败后访问dirty的次数;当穿透次数达到阈值时,dirty会晋升为新的read,后续读请求继续走低开销路径。entry保存真实 value,并通过原子指针管理 value 的状态。 - 面试收口:普通
map更适合大多数明确受控的场景;并发共享、读多写少、key 变化少的缓存类场景可以选择sync.Map。
2026.05.14#
MySQL 中常见的引擎,InnoDB 和 MyISAM 区别,应用的场景?#
评级:中高级八股、中低频
- 讲出常见存储引擎:MySQL 常见引擎有 InnoDB、MyISAM、Memory、CSV 等,线上业务最常用 InnoDB,MySQL 5.5 之后默认引擎是 InnoDB。
- 讲出核心区别:InnoDB 支持事务、行级锁、外键、崩溃恢复和 MVCC,适合高并发 OLTP;MyISAM 结构简单,使用表级锁,读多写少场景表现轻量。
- 讲出适用场景:InnoDB 适合大多数线上业务表,尤其是订单、支付、账户、库存等核心业务表;MyISAM 适合历史遗留系统、低并发读多写少、对事务一致性要求很低的场景。
- 面试收口:实际生产中核心业务表通常优先选择 InnoDB,因为它在事务、并发控制、崩溃恢复和数据一致性上更完整。
2026.05.13#
什么是 Buffer Pool?为什么要有?它的结构是什么,以及如何管理?和 Redo Log 的关系?#
评级:中高级八股、中低频
- 讲出 Buffer Pool 的概念:Buffer Pool 是 InnoDB 的内存缓存区,缓存数据页、索引页、undo 页等。查询一行数据时,InnoDB 会先定位这行数据所在的数据页,然后把整个页加载到 Buffer Pool 中;后续再次访问同一页时,可以直接从内存读取。
- 讲出为什么要有 Buffer Pool:核心目标是减少磁盘读取、提升更新性能、合并随机写、提高整体吞吐。读请求命中缓存后减少磁盘 I/O;写请求先改内存页,后续由后台线程按策略刷盘。
- 讲出 Buffer Pool 的结构:Buffer Pool 由多个缓存页 Frame 组成,每个缓存页都有对应的控制块 Control Block,控制块记录页号、表空间、脏页标记、访问状态等元信息;常见链表包括 Free List、Flush List、LRU List。
- 讲出如何管理:Free List 管理空闲页;LRU List 管理页的命中、冷热分层和淘汰;Flush List 管理脏页刷盘。管理重点是页的加载、命中、淘汰、刷盘。
- 讲出和 Redo Log 的关系:Buffer Pool 提升性能,Redo Log 保证崩溃恢复。事务更新数据时先修改 Buffer Pool 中的数据页并生成 redo 记录,提交时 redo 按策略落盘;即使脏页还在内存中,宕机后也能通过 Redo Log 重放恢复。
- 面试收口:Buffer Pool 让 InnoDB 把随机磁盘读写转成内存访问和后台批量刷盘,Redo Log 配合 WAL 机制保证已提交事务的持久性。
2026.05.11#
如何保证消息队列消息可靠投递以及重复消费的幂等性?#
评级:中级八股、中频
- 生产端:常见风险是业务数据已提交但消息发送失败、网络超时导致发送结果未知、重试产生重复消息。解决方案是开启生产者确认机制,比如 RabbitMQ 的 publisher confirm、Kafka 的 ack 机制;发送失败要重试,消息携带全局唯一
message_id或业务幂等键;关键业务可用本地消息表或 Outbox 模式,在同一个本地事务里写业务数据和消息记录,再由后台任务扫描补偿投递,保证业务数据和消息发送最终一致。 - 服务端:常见风险是 Broker 收到消息后宕机、消息只在内存中、队列或分区副本异常。解决方案是开启消息持久化、队列或 Topic 持久化、落盘后确认、多副本或镜像队列;Kafka 场景可以配置
acks=all、合理的min.insync.replicas和副本数,并配合监控告警、重试队列、死信队列处理异常消息。 - 消费端:常见风险是业务处理完成前自动 ACK、业务处理成功后 ACK 失败、消费过程中宕机。解决方案是使用手动 ACK,在业务处理成功且状态落库后再确认;处理失败时重试,超过次数后进入死信队列;消费逻辑保留幂等能力,因为 ACK 丢失、重平衡、重试都会带来重复投递。
- 幂等性的概念:同一条消息被消费一次或多次,对业务产生的最终结果一致。比如扣库存、发券、创建订单这类操作,重复消费时要识别同一个业务动作,并让它只产生一次有效业务结果。
- 保证幂等性的方法:常见做法是“唯一 ID + 去重表”,消费前用
message_id或业务唯一键插入去重表,并给该字段加唯一索引;插入成功后执行业务逻辑,插入冲突说明这条消息已经处理过,直接返回成功。也可以用业务唯一索引、状态机流转、RedisSETNX短期去重、乐观锁版本号等方式,核心是让重复消息命中同一条业务记录或同一次状态流转。 - 面试可直接说:生产端靠确认、重试、本地消息表保证消息发出;服务端靠持久化、副本和确认机制保证消息存住;消费端靠手动 ACK、失败重试、死信队列保证消息被处理;重复消费靠全局唯一 ID、去重表、唯一索引和状态机做幂等。
2026.05.08#
TCP 三次握手四次挥手#
评级:基础八股、中频
- 讲出三次握手:客户端发送
SYN请求建立连接;服务端收到后返回SYN + ACK;客户端再发送ACK,双方进入ESTABLISHED状态。这个过程完成双方收发能力确认和初始序列号同步。 - 讲出四次挥手:主动关闭方发送
FIN表示自己发送方向关闭;被动关闭方返回ACK;被动关闭方处理完剩余数据后发送FIN;主动关闭方返回ACK,等待TIME_WAIT后连接释放。 - 讲出挥手比握手多一次的原因:TCP 是全双工协议,两个方向的数据发送能力要分别关闭。收到对方
FIN后,只代表对方发送方向关闭,本端可能还有数据要发,所以先回ACK,等本端数据发送完成后再发FIN。 - 讲出两次握手的问题:两次握手只能让服务端确认客户端的发送能力和服务端的接收能力,客户端对服务端
SYN + ACK的接收确认没有回到服务端;历史SYN报文如果延迟到达,服务端可能误建立连接并占用资源。
2026.05.07#
如何建立合适的索引?#
评级:基础八股、中频
- 先看查询语句和业务访问路径,再决定索引服务哪些 SQL。
- 选择适合建索引的字段,优先考虑高频过滤列、关联列、排序分组列,以及区分度高的字段。
- 设计联合索引时结合最左前缀原则,把高频等值查询列放在前面,再结合范围查询、排序、分组和覆盖索引需求安排顺序。
- 评估索引代价,索引会占用额外空间,也会增加写入、更新和删除时的维护成本,需要在查询收益和写入成本之间找到平衡点。
- 用
EXPLAIN查看执行计划,重点关注type、key、rows、Extra,判断索引是否生效。
2026.05.06#
Redis 五种数据结构及其原理#
评级:基础八股、中频
- 讲出 Redis 五种基础数据结构:String、Hash、List、Set、ZSet。
- 讲出各个结构的底层实现:String 基于 SDS;Hash 小数据量常用 listpack,数据变大后转 hashtable;List 基于 quicklist;Set 小整数集合常用 intset,复杂集合用 hashtable;ZSet 小数据量常用 listpack,大数据量用 skiplist + dict。
- 讲出跳表的实现:跳表是多层有序链表,节点通过随机层数建立多级索引,查询时从最高层向右、向下逐步定位,平均查询、插入、删除复杂度是 O(logN),ZSet 借助跳表支持范围查询和有序遍历。
2026.04.30#
Redis 缓存穿透、击穿、雪崩是什么,如何避免?#
评级:基础八股、中高频
- 讲出三个概念:缓存穿透是查询无效数据,请求穿过缓存后继续打到数据库;缓存击穿是热点 key 过期或重建时,大量并发请求集中打到数据库;缓存雪崩是大量 key 集中过期或缓存服务故障,请求大面积打到数据库。
- 讲出发生场景:穿透常见于恶意请求、参数异常、业务数据被删除;击穿常见于爆款商品、热点用户、热点配置等高频 key 失效;雪崩常见于批量设置相同 TTL、缓存集群故障、缓存预热缺失。
- 讲出避免方案:穿透用参数校验、布隆过滤器、空值缓存短 TTL;击穿用互斥锁、singleflight、逻辑过期、后台异步刷新;雪崩用 TTL 随机化、分层缓存、限流降级、集群高可用和缓存预热。
2026.04.29#
RabbitMQ、Kafka、RocketMQ 对比#
评级:八股外知识、低频
- 讲出 RabbitMQ 的优势和适合场景:RabbitMQ 基于 AMQP,Exchange 路由能力强,支持 Direct、Topic、Fanout 等模型,生态成熟、使用简单,适合业务系统解耦、任务队列、后台异步处理、复杂路由和失败重试。
- 讲出 Kafka 的优势和适合场景:Kafka 吞吐量高,分区并行、顺序追加写、消费位点管理能力强,适合日志采集、埋点上报、实时计算、数据管道和事件流平台。
- 讲出 RocketMQ 的优势和适合场景:RocketMQ 的事务消息、顺序消息、延时消息、消息轨迹等业务能力完善,适合电商订单、金融支付、交易链路和分布式事务最终一致性。
2026.04.28#
Go 中的 Channel#
评级:基础八股、中频
- 讲出 CSP 思想:CSP(Communicating Sequential Processes)强调通过通信组织并发,Go 里用 goroutine 执行任务,用 channel 传递消息,让数据流动带动协作,减少共享可变状态竞争。
- 讲出 channel 的两种类型以及适用场景:无缓冲 channel 的发送和接收同步配对,适合强同步、任务交接、信号通知;有缓冲 channel 带队列能力,适合削峰、限流、生产消费解耦。
- 讲出关闭 channel 后发送和接收的行为:向已关闭 channel 发送会 panic;接收会先读完缓冲区剩余数据,缓冲区读尽后继续接收会得到元素零值,
ok=false。
2026.04.27#
MySQL 中 InnoDB 引擎三种日志及作用#
评级:基础八股、中高频
- 讲出三种日志的作用和使用场景:undo log 记录数据旧版本,支持事务回滚和 MVCC;redo log 记录数据页的物理修改,支持崩溃恢复和 WAL;binlog 记录数据库逻辑变更,支持主从复制、增量备份和基于时间点恢复。
- 讲出 redo log 和 binlog 的区别:redo log 属于 InnoDB 层物理日志,循环写,核心目标是 crash recovery;binlog 属于 MySQL Server 层逻辑日志,追加写,核心目标是复制和归档恢复。
- 讲出 redo log 的归属:redo log 是 InnoDB 引擎日志,和 Buffer Pool、checkpoint、脏页刷盘一起保障事务持久性。
2026.04.24#
输入一条 URL 会发生什么?#
评级:基础八股、中高频
- 讲出 IP 和域名的关系:域名是给人看的地址,IP 是网络通信真正使用的地址,DNS 负责把域名解析成 IP。
- 讲出 DNS 的原理:浏览器和操作系统会先查本地缓存,缓存里没有再向本地域名服务器发起查询,本地域名服务器再递归或迭代访问根服务器、顶级域服务器和权威 DNS 服务器,最终拿到目标 IP。
- 讲出完整链路:浏览器解析 URL 后先做 DNS 解析,再建立 TCP 连接;如果是 HTTPS 还要进行 TLS 握手;随后发送 HTTP 请求,服务器处理请求并返回响应,浏览器接收 HTML、CSS、JS 等资源,完成解析、渲染和页面展示。
2026.04.23#
内存分页的好处以及缺页中断是什么?#
评级:中级八股、中低频
- 讲出内存分页的概念:把虚拟内存和物理内存都按固定大小划分为一页一页,进程访问地址时通过页表把虚拟页映射到物理页。
- 讲出分页的好处:固定大小分配让内存管理更简单,也能减少外部内存碎片,提高内存利用率。
- 讲出缺页中断的概念及发生情况:进程访问的页面当前没有在物理内存中时,会触发缺页中断,由操作系统把目标页从磁盘调入内存,更新页表后再继续执行指令。
2026.04.22#
慢查询的排查与优化#
评级:中高级八股、中低频
- 先开启慢查询日志定位最慢的 SQL,重点关注执行时间、扫描行数、返回行数和调用频率,优先处理耗时高且出现频繁的语句。
- 用
EXPLAIN分析执行计划,重点看type、key、rows、Extra,判断是否出现全表扫描、回表过多、临时表或文件排序。 - 根据查询条件、排序和分组优化索引,优先考虑高频过滤列、联合索引和覆盖索引,同时清理重复索引和低价值索引。
- 调整 SQL 写法,减少
SELECT *、深分页、索引列函数计算和隐式类型转换,让查询条件更容易命中索引。
2026.04.21#
Redis 缓存与数据库如何保证最终一致性?#
评级:中级八股、项目中缓存相关问题出现概率较高
- 讲清三种方案:更新数据库后删除缓存、删除缓存后更新数据库、延迟双删;其中主流写法是 Cache Aside 里的“更新数据库后删除缓存”。
- 讲出三种方案的并发风险:更新数据库后删除缓存会遇到删缓存失败和旧值回填;删除缓存后更新数据库会遇到数据库更新窗口里的旧值回填;延迟双删通过第二次删除清理并发窗口里的脏缓存,效果取决于延迟时间设置。
- 讲出优化手段:删除缓存失败重试、MQ 或 binlog 订阅做异步补偿删除、给缓存加 TTL 兜底、热点 key 回填时加互斥锁或 singleflight、一致性要求高的读请求走主库。
2026.04.20#
死锁的四个必要条件#
评级:基础八股、中频八股
- 讲出死锁的概念:多个线程或进程在运行过程中,因为争夺资源而相互等待,最终都无法继续执行。
- 讲出死锁的四个必要条件:互斥、请求并保持、不可剥夺、循环等待。
- 讲出如何避免死锁:核心是破坏其中一个条件,比如一次性申请资源、申请失败后释放已有资源、按照固定顺序加锁。
2026.04.16#
虚拟内存是什么?为什么要有?#
评级:中等八股、中频八股
- 讲出虚拟内存的概念与和物理内存(RAM)的对比:虚拟内存是进程看到的逻辑地址空间,需要通过映射才会落到物理内存。
- 讲出虚拟内存是分页管理的:按页划分内存,依赖页表完成虚拟地址到物理地址的转换,缺页时触发缺页中断并换入页面。
- 讲出虚拟内存的好处与缺陷:好处是进程隔离、按需加载、提升内存利用率、支持更大地址空间;缺陷是地址转换和页表维护有开销,缺页与频繁换页会导致性能下降(抖动)。
2026.04.15#
Redis 过期删除与内存淘汰#
评级:中等八股、中频八股
- 讲出过期删除与内存淘汰的区别及触发场景。
- 讲出过期删除策略及其优缺点对比。
- 讲出内存淘汰策略及其原理(主要是 LRU 与 LFU)。
2026.04.14#
为什么 MySQL 存储引擎普遍采用 B+ 树?#
评级:基础八股、高频八股
- 讲出 B+ 树的结构。
- 讲出 B+ 树相较于 B 树的优势。
- 重点讲出树查询时间复杂度主要取决于树的高度。
- 讲出 B+ 树相较于哈希的优势。
- 讲出更适合数据库页结构。
2026.04.13#
HTTP/1.1、HTTP/2、HTTP/3 的区别#
- 讲出 HTTP/1.1 队头阻塞。
- 讲出 HTTP/2 多路复用、二进制分帧、头部压缩。
- 讲出 HTTP/2 的缺陷以及 HTTP/3 是如何解决的(UDP + QUIC)。
2026.04.10#
GMP 是什么?核心作用是什么?#
- 在早期 GM 模型中,如果直接给每个 M 分配本地队列和上下文资源,是否也能缓解全局锁冲突?为什么仍需要在 G 和 M 之间引入 P 这个抽象层?
- M 是否可以直接进行任务窃取?为什么调度设计中仍需要 P?
- 如果 M 阻塞掉,P 会怎么处理?
- 怎么动态知道 M 会阻塞,并提前退回 P?
- M 被解绑后,P 的归属会如何变化?新的 M 如何接手该 P?
- 如果所有 M 都陷入系统调用,程序是否会停滞?调度器如何避免这种情况?
2026.04.09#
MySQL 索引失效的场景?#
- 不满足最左匹配原则。
- 对索引列做了函数操作。
- 对索引列做了运算。
SELECT *查询。LIKE '%xxx'前置通配符。OR条件使用不当。- 索引列和查询条件类型不一致,发生隐式转换。
- 联合索引中,范围查询后面的列失效。
2026.04.08#
进程的 5 个状态是什么?状态在什么情况下会转变?#
- 讲出五种状态。
- 讲出状态之间如何转换。
2026.04.07#
如何保证消息队列的可靠性?#
- 从链路作答:生产者发送消息 -> Broker 存储消息 -> 消费者消费消息。
- 讲出生产者和消费者分别有哪些确认机制。
- 考虑消息重复消费。
2026.04.02#
Redis 的持久化机制?#
- 讲出 RDB 和 AOF 是什么。
- 讲出 RDB 和 AOF 优缺点对比。
- 讲出两者的底层原理。
- 讲出混合持久化。
结论:RDB 适合做容灾备份,AOF 适合保证数据一致性,而生产环境通常开启混合持久化。
2026.04.01#
MySQL 的事务是如何实现的?#
基础:答出 ACID。
深入:讲出每个特性怎么保障的。原子性靠 undo log,持久性靠 InnoDB 的 redo log,隔离性靠锁和 MVCC,一致性由约束、事务机制和业务逻辑共同保证。
2026.03.31#
AI 相关基础概念科普#
讲出你对以下概念的理解:
- token
- llm
- Context
- Prompt
- RAG
- Function call
- MCP
- Skills
- Agent
- Harness
2026.03.30#
自旋锁和互斥锁的区别#
自旋锁在获取锁失败后会持续循环尝试,不主动让出 CPU;互斥锁在获取锁失败后会阻塞线程并让出 CPU,等锁可用时再被唤醒。
- 自旋锁避免了线程阻塞和唤醒的上下文切换开销,但等待期间会持续消耗 CPU。
- 互斥锁等待时不占用 CPU,但阻塞和唤醒需要进行线程调度,存在上下文切换开销。
- 自旋锁适合临界区很短、锁竞争较轻且运行在多核 CPU 上的场景;互斥锁适合临界区较长或竞争较激烈的场景。
- 如果持锁时间不可控,使用自旋锁可能浪费大量 CPU,甚至降低系统整体吞吐,因此通常优先使用互斥锁,只在能够证明临界区足够短时考虑自旋锁。
结论:等待时间短时,自旋锁可以用 CPU 时间换取更低的切换延迟;等待时间较长时,互斥锁通过阻塞线程减少 CPU 浪费。