八股文 / 腾讯后台

腾讯后台开发八股文

持续更新中。来源:2026 暑期实习真实面经。


Go 语言

Q1:Go 的 GMP 调度模型是什么?

来源:26暑期实习-后台AI开发一面

GMP 是 Go runtime 的协程调度模型,由三个核心角色组成:

角色 全称 说明
G Goroutine 用户态轻量级线程,创建成本极低(初始栈仅 2KB)
M Machine 操作系统线程,是实际执行 G 的载体
P Processor 逻辑处理器,持有本地运行队列和运行 G 所需的资源

调度流程

  1. 每个 P 维护一个本地队列(最多 256 个 G);还有一个全局队列作为溢出缓冲
  2. M 必须绑定一个 P 才能执行 G:M ← P ← G
  3. M 从绑定的 P 的本地队列取 G 执行;本地队列空了就去全局队列或其他 P 那里(work stealing)
  4. 当 G 发生系统调用(如文件 IO)阻塞了 M 时,P 会与这个 M 解绑(hand off),找另一个空闲 M 或新建 M 继续执行队列中的 G

核心设计思想:P 的引入将”可运行的协程队列”和”操作系统线程”解耦——M 阻塞不影响其他 G 的调度,实现了 M:N 调度(M 个 OS 线程调度 N 个协程)。


Q2:M 和 P 的数量关系是怎样的?

来源:26暑期实习-后台AI开发一面

  • P 的数量决定了最大并行度:同一时刻最多有 GOMAXPROCS 个 G 在真正并行执行
  • M 的数量 $\geq$ P 的数量:因为当某个 M 被系统调用阻塞时,它绑定的 P 会转交给另一个 M,所以活跃 M 数可能超过 P 数
  • M 的数量也可以远大于 P:大量 G 同时执行阻塞式系统调用时,每个阻塞的 G 都占一个 M

一句话总结:P 决定并行度上限,M 是按需创建的——阻塞 IO 多时 M 多,纯计算负载时 M ≈ P。


Q3:P 的数量怎么设定?M 创建有没有上限?

来源:26暑期实习-后台AI开发一面

P 的数量

  • 默认值 = CPU 逻辑核心数(runtime.NumCPU()
  • 可通过 runtime.GOMAXPROCS(n) 或环境变量 GOMAXPROCS=n 调整
  • 设太小:CPU 核心闲置;设太大:上下文切换开销增加
  • 经验法则:CPU 密集型保持默认,IO 密集型可适当调大

M 的上限

  • 默认上限 10000(通过 runtime/debug.SetMaxThreads() 可调)
  • 超过上限程序直接 panic:thread exhaustion
  • 实际中如果 M 数接近上限,说明大量协程在做阻塞式系统调用,应该改用非阻塞 IO 或减少并发

Q4:Go 的内存逃逸是什么?

来源:26暑期实习-后台AI开发一面

Go 编译器通过逃逸分析(escape analysis)决定变量分配在栈上还是堆上。如果编译器判断变量的生命周期超出当前函数作用域,就会将其”逃逸”到堆上分配。

常见逃逸场景

场景 示例 原因
返回局部变量的指针 return &x 函数返回后栈帧销毁,必须放堆上
闭包引用外部变量 func() { use(x) } 闭包的生命周期可能长于外部函数
赋值给接口类型 var i interface{} = x 接口的底层实现需要堆分配
发送到 channel ch <- &x 接收方在另一个协程,生命周期不确定
slice/map 动态扩容 append(s, x) 触发扩容 新底层数组无法在栈上分配
栈空间不足 超大数组或深递归 编译器判断栈放不下

为什么关注逃逸:堆分配 = GC 压力。高频调用的热路径中避免不必要的逃逸,可以显著减少 GC 停顿。

如何检查go build -gcflags="-m" 会输出逃逸分析报告。

优化手段

  • 尽量用值传递代替指针传递(小结构体)
  • 预分配 slice 容量避免扩容:make([]int, 0, n)
  • 避免在热路径中使用 interface{}

Redis

Q5:Redis 为什么性能强?Redis 单线程怎么理解?

来源:26暑期实习-后台AI开发一面

Redis 性能强的原因

因素 说明
纯内存操作 数据都在内存中,读写在纳秒级,比磁盘快 $10^5$ 倍
IO 多路复用 使用 epoll/kqueue 单线程监听上万个连接,不阻塞
高效数据结构 SDS(动态字符串)、跳表、压缩列表、整数集合等专门优化的底层结构
单线程无锁 避免了锁竞争、死锁、上下文切换的开销
请求/响应协议简单 RESP 协议解析极快,命令执行通常是 $O(1)$ 或 $O(\log n)$

“单线程”的准确含义

Redis 的”单线程”指的是命令执行(核心事件循环)是单线程的,即读取请求 → 解析命令 → 执行 → 返回结果这条主路径是单线程串行的。但 Redis 并不是只有一个线程:

  • 6.0 之前:网络 IO(read/write)和命令执行都在主线程
  • 6.0+ 引入多线程 IO:网络读写(解包/发包)可以用多个 IO 线程并行处理,但命令执行仍然是单线程的——这保证了不需要加锁
  • 后台线程:持久化(RDB/AOF)、异步删除(UNLINK)、关闭文件描述符等任务由后台线程完成

为什么单线程就够了:Redis 的瓶颈不在 CPU 而在网络 IO 和内存带宽。内存中执行一条命令只需微秒级,单线程每秒可以处理 10w+ 请求。


消息队列

Q6:为什么用 RocketMQ?有没有了解过别的消息队列?怎么做的选型?

来源:26暑期实习-后台AI开发一面

消息队列的选型维度

维度 RabbitMQ RocketMQ Kafka
开发语言 Erlang Java Scala/Java
单机吞吐 万级 十万级 百万级
延迟消息 插件支持 原生支持(18个等级) 不支持
事务消息 不支持 支持(半消息机制) 支持(Exactly Once 语义)
消息过滤 Routing Key Tag + SQL92 仅 Topic 级别
消息回溯 不支持 支持(按时间戳) 支持(按 offset)
顺序消息 不保证 支持分区有序 支持分区有序
社区生态 成熟但偏小众 阿里开源,中文文档好 最活跃,大数据生态完善
适用场景 中小规模、复杂路由 业务消息(电商/金融) 日志采集、流处理、大数据

选型结论

  • 选 RocketMQ:需要延迟消息(如订单超时取消)、事务消息(如分布式事务最终一致性)、消息过滤、金融级可靠性
  • 选 Kafka:需要超高吞吐、日志采集、流计算(Flink/Spark Streaming 集成好)
  • 选 RabbitMQ:消息量不大但路由规则复杂、需要多协议支持(AMQP/STOMP/MQTT)

Q7:RocketMQ 和 Kafka 的区别?

来源:26暑期实习-后台AI开发一面

对比维度 RocketMQ Kafka
架构 NameServer(无状态,轻量) ZooKeeper/KRaft(有状态,较重)
存储模型 CommitLog + ConsumeQueue(物理上所有 Topic 共享一个日志文件) 每个 Partition 独立日志文件
写入性能 顺序写 CommitLog,十万级 TPS 顺序写 Partition,百万级 TPS
消费模型 Push + Pull 都支持 Pull 为主
延迟消息 原生 18 个等级 + 5.0 支持任意时间 不支持
事务消息 半消息 → 本地事务 → 确认/回滚,自带回查机制 0.11+ 支持 Exactly Once,但实现更复杂
消息过滤 Broker 端按 Tag/SQL 过滤,减少网络传输 只能 Consumer 端自己过滤
消息保序 同一个 MessageQueue 严格有序 同一个 Partition 严格有序
堆积能力 十亿级(CommitLog 顺序写) 十亿级(Partition 顺序写)
典型场景 电商下单(事务消息)、延迟通知、订单状态流转 日志聚合、用户行为追踪、实时流处理

一句话总结:Kafka 是”高速公路”(吞吐至上),RocketMQ 是”智能物流”(功能丰富、业务友好)。


面试算法题

寻找重复数(LeetCode 287)

来源:26暑期实习-后台AI开发一面

题意:给定一个包含 $n + 1$ 个整数的数组 nums,每个整数在 $[1, n]$ 范围内,只有一个重复的整数,找出这个重复数。要求不修改数组,空间 $O(1)$。

思路 — Floyd 快慢指针判环

把数组看作一个隐式链表:下标 $i$ 的”下一个节点”是 $\text{nums}[i]$。因为有 $n+1$ 个数但值域只有 $[1, n]$,至少有两个不同下标指向同一个值——这意味着链表中存在环,而环的入口就是重复数

和链表判环问题完全一样,分两步:

  1. 快慢指针找相遇点:慢指针每次走一步 slow = nums[slow],快指针每次走两步 fast = nums[nums[fast]],在环内相遇
  2. 找环入口:将其中一个指针重置到起点(下标 0),两个指针各走一步,再次相遇的位置就是环入口 = 重复数
def find_duplicate(nums):
    slow = fast = 0
    # 第一阶段:快慢指针找环内相遇点
    while True:
        slow = nums[slow]
        fast = nums[nums[fast]]
        if slow == fast:
            break
    # 第二阶段:找环入口
    slow = 0
    while slow != fast:
        slow = nums[slow]
        fast = nums[fast]
    return slow

# ACM 模式
n = int(input())
nums = list(map(int, input().split()))
print(find_duplicate(nums))

时间复杂度:$O(n)$。空间复杂度:$O(1)$。

为什么有效:环入口一定是重复数,因为只有重复数对应的下标会被多个前驱指向(两个不同的位置 $i, j$ 满足 $\text{nums}[i] = \text{nums}[j]$),形成环的入口。


面经记录

日期 岗位 面次 来源 考察内容
2026-04 后台AI开发 一面 牛客 项目 + Go(GMP/逃逸) + Redis + RocketMQ + 算法(LC287)