字节跳动 GMPT 后端日常实习一面
- 轮次
- 一面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- 实习经历深挖(面试基本全部在问实习)。
- Redis 分布式锁怎么实现?
- 程序退出前怎么回收所有 goroutine?
- goroutine 为什么比内核线程快?
- TCP 的流量控制、拥塞控制。
- 数据库隔离级别、MVCC。
- 手撕:实现字符串转浮点,例如
"123.456"输出123.456,不需要考虑浮点精度缺失。
《参考解析》
Redis 分布式锁:加锁必须是原子的 SET key value NX PX 30000——NX 保证互斥、PX 过期时间防死锁、value 放本次请求的唯一标识(UUID/线程 id)以便安全释放。释放必须用 Lua 脚本「先比对 value 再删除」,不能先 GET 再 DEL(两步之间锁可能已过期并被别人持有,会误删他人锁)。如果业务执行时间可能超过过期时间,要有看门狗续期(如 Redisson 的 tryLock + 后台续期线程),或者干脆把过期时间设得远大于超时上限并在业务里做幂等。集群场景下主从异步复制会导致锁丢失(主节点写入后未同步即宕机,从节点升主后另一客户端也能拿到锁),Redlock 算法通过多数派加锁来缓解,但它在时钟漂移与 GC 停顿下仍被质疑;务实的方案是:优先做业务幂等(唯一索引/状态机),把分布式锁当成减少竞争的优化而不是正确性的唯一保障。另外要提一句锁粒度与等待策略(自旋 vs 阻塞订阅),以及为什么不用 SETNX + EXPIRE 两条命令。
程序退出前回收所有 goroutine:Go 没有「杀死 goroutine」的能力,只能靠通知 + 等待。标准套路是:① 用 context.WithCancel(或监听 signal.Notify 收到 SIGTERM/SIGINT)作为全局退出信号,所有长期运行的 goroutine 在 select 里监听 ctx.Done(),收到就清理并 return;② 用 sync.WaitGroup(或 errgroup.Group)登记所有后台 goroutine,主流程在收到退出信号后 wg.Wait() 等它们退出,再关闭资源;③ 关闭顺序要自底向上:先停止接收新请求(关监听、把健康检查置为不健康、从服务发现摘除并留出排空窗口),再等正在处理的请求结束,然后停止消费端与定时任务,最后关数据库连接池、消息队列、日志与链路上报,让缓冲日志落盘;④ 给等待设置超时上限(例如 30 秒),超时就强制退出,避免优雅关闭变成卡死;⑤ 泄漏排查用 runtime.NumGoroutine() 或 pprof 的 goroutine profile 对比退出前后的数量,常见泄漏源是无人接收的 channel 发送、time.Ticker 忘记 Stop、HTTP 响应体未关闭。
goroutine 为什么比内核线程快:① 调度在用户态:Go 运行时的 GMP 调度器把 goroutine(G)复用到少量 OS 线程(M)上,由处理器(P)持有本地运行队列,调度切换不需要陷入内核、不涉及系统调用,开销在百纳秒量级;② 栈小且可增长:goroutine 初始栈只有 2KB 起(内核线程栈通常是 MB 级),按需增长/收缩,所以同样内存能起几十万个;③ M:N 模型与工作窃取:一个 goroutine 阻塞在系统调用时,M 会与 P 解绑,P 可以带其他 G 交给新的 M 继续跑(handoff),不会浪费一个 P;空闲 P 会从其他 P 的队列或全局队列窃取任务,负载均衡;④ 抢占式调度(基于 sysmon 的协作 + 异步抢占信号)避免单个死循环 G 饿死其他任务;⑤ 代价:goroutine 的阻塞式写法依赖运行时对网络轮询器(netpoller)的封装,纯 CPU 密集任务不会因为 goroutine 而变快,也不适合把阻塞式 C 调用当成普通调用(会占用 M)。
TCP 流量控制与拥塞控制:流量控制是端到端保护接收方的机制:接收方在 ACK 里带 rwnd(接收窗口,等于接收缓冲剩余空间),发送方保证「已发送未确认的数据 ≤ min(cwnd, rwnd)」;窗口为 0 时发送方停发并启动零窗口探测(避免死等,因为窗口更新报文可能丢失)。拥塞控制是保护网络的机制:慢启动(cwnd 指数增长到 ssthresh)→ 拥塞避免(线性增长)→ 超时重传(ssthresh 减半、cwnd 回 1,重新慢启动)→ 快速重传/快速恢复(3 个重复 ACK 触发立即重传,cwnd 减半后继续拥塞避免);现代内核还有 CUBIC(以丢包为信号,窗口按三次函数增长)和 BBR(基于带宽与最小 RTT 建模,不以丢包为唯一信号)。答题时的加分点:说明两者的判定依据不同(rwnd 由接收方通告,cwnd 由发送方按拥塞信号维护)、生效条件是取小,以及 Nagle 算法与延迟 ACK 叠加会引入额外延迟(实时场景常用 TCP_NODELAY 关闭)。
数据库隔离级别与 MVCC:SQL 标准四个级别对应三类读异常——读未提交(脏读)、读已提交(不可重复读)、可重复读(幻读)、串行化(全部避免)。MySQL InnoDB 默认是可重复读,并且通过 MVCC + 间隙锁在很大程度上避免了幻读。MVCC 的实现:每行记录有隐藏列 DB_TRX_ID(最近修改它的事务 id)与 DB_ROLL_PTR(指向 undo log 中的旧版本),读操作生成一个 Read View(活跃事务 id 列表 m_ids、最小活跃事务 id、下一个待分配事务 id),按可见性规则沿着 undo 版本链找到对当前事务可见的版本——在可重复读下 Read View 在第一次快照读时创建并复用整个事务,在读已提交下每条语句都重新创建,这就是两者语义差异的根源。要点:快照读(普通 SELECT)走 MVCC 不加锁,当前读(SELECT ... FOR UPDATE、UPDATE、DELETE)读最新版本并加锁(记录锁/间隙锁/临键锁);RC 级别下 binlog 必须用 row 格式以避免主从不一致;长事务会撑大 undo 版本链、阻碍 purge,从而引发性能问题。
手撕:字符串转浮点:不要用 strconv.ParseFloat,面试要的是手写解析。步骤:① 去掉首尾空白,处理符号位(记录 sign);② 跳过前导 0,整数部分 integer = integer*10 + digit,注意用整型累加避免反复乘浮点带来的误差;③ 遇到 . 后进入小数部分,用 frac = frac*10 + digit,同时记录 div *= 10;④ 遇到 e/E 解析指数,再乘以 10 的幂(正指数乘、负指数除);⑤ 结果 sign * (integer + frac/div),最后做范围校验(超出 float64 返回 ±Inf 或报错,按面试官要求)。边界要主动覆盖:空串/只有符号、".5"、"5."、"+0.0"、"-0"、超长数字、非法字符(遇到非数字非 . 非 e 直接返回错误或停止解析,先问清语义)、以及 "1e400" 溢出。如果要求更高精度,可以只对整数部分累加成整型、小数部分拼成整数字符串再一次性除以 10 的幂,减少多次浮点运算的误差累积。
面试复盘:整场 1 小时以上、无反问环节,10 分钟后约了二面,面试重心明显在实习经历而不是八股——这类实习面试的通用准备方式是把实习项目拆成「背景约束 → 我的方案 → 数据结果 → 踩过的坑」四段,每个技术选型都准备一句「为什么不用另一个」。原帖作者在末尾问了 GMPT 是做什么的部门,可在面试中直接反问业务方向,这类信息比薪资更能帮助判断是否值得去。