字节后端实习一面:恶意充值 Agent 与线程池拆分的连环追问
- 轮次
- 一面
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
实习部分:
- 恶意充值 Skill 怎么实现的?
- 封号的规则怎么写的?
- 为什么是恶意充值?为什么不直接修改逻辑,而是让你排查恶意用户?
- 为什么用 Agent 解决?代码直接判断并封号不就行了?
- 有没有对误封有过措施?
- 拆分线程池怎么做的?介绍一下。
- 线程池工作的任务是什么传过来的?HTTP?RPC?用 RPC 了为什么要用线程池,RPC 框架本身是不是就有线程池?
- 动态拆分,「动态」指的是?分片后会有线程池的释放吗?
- 充值送 token 怎么保证幂等?一致性、原子性?
- JEV 判题怎么优化的?和大模型的输入有什么区别?流程什么样?要挂知识库吗?调的是官方的、自己部署的,还是第三方的?官方收费怎么收费的?
八股部分:
- JVM 内存分区。
- 垃圾回收机制。
- MySQL 索引类型有哪些?
- 哪些情况会导致索引失效?
- 怎么排查慢 SQL?
- 加什么样的索引?
算法:
- 打家劫舍。
《参考解析》
用 Agent 做风控排查:要能回答「为什么不直接改代码」
这是整场面试最核心的追问,原帖作者的复盘写得很好,可以直接吸收成答题模板。被问「为什么用 Agent 而不是直接改逻辑」时,正确的答法分三层。第一层是承认工程现实:活动刚上线、规则还在探索期,直接改扣费逻辑意味着动核心链路、需要完整回归与灰度,周期长且风险高。第二层说明 Agent 的附加价值:用 Agent 排查不只是「解决封号」这一个动作,它同时产出了判定所需的数据(哪些特征像恶意充值、误判率多少、人怎么复核),这些数据是后续把规则固化下来的前提。第三层给出收束路径:数据稳定之后就把它改写成定时任务或确定性规则,把 Agent 收敛到「探索期工具」的定位。这样答既不显得在给不合理方案找借口,也体现了你会考虑「临时方案怎么收敛成长期方案」。
关于「封号规则怎么写的」,要能讲出规则形态与依据:规则通常由特征组合构成(短时间高频充值、金额与行为不匹配、设备与账号关联、退款率、支付渠道异常),每条规则要能说清来源(数据分析得出的阈值还是业务经验)、以及如何权衡误伤。被问「有没有对误封有过措施」时,坦诚的回答(当时主要靠人工申诉)加上改进方向(分级处置——先限制后封禁、增加人工复核队列、记录判定依据以便申诉时回溯、用误判样本回流调阈值)比硬说「有完备机制」更可信。
线程池拆分与「RPC 框架本身就有线程池」
面试官的追问链条很典型:先说拆分线程池(按任务类型隔离,避免慢任务占满所有线程、避免不同类型任务互相影响),接着问任务从哪来(HTTP 请求、RPC 调用、消息消费、定时任务),再抛出一个尖锐问题——「用 RPC 了为什么还要用线程池,RPC 框架本身是不是就有线程池?」回答要分清层次:RPC 框架的线程池是「服务端处理请求」的池(每个进来的请求占一个线程),它是 IO 与协议层的调度,不代表业务层不需要自己的池。业务侧需要独立线程池的理由有三个:一是把「请求处理线程」与「业务执行线程」解耦,避免慢业务把 RPC 的请求线程占满导致无法接收新请求(这正是舱壁隔离的意义);二是可以按业务类型分别控制并发度、队列与拒绝策略,保护不同的下游;三是业务侧可以做批量与并行编排(一个请求内部并发调用多个下游,用固定大小的池控制下游压力)。要顺带指出反面风险:嵌套线程池会放大线程数(请求线程等待业务线程),所以要限制池的层数与总线程数,并给等待超时。
「动态拆分」的追问要点是:动态指什么(按上游流量或下游压力动态调整分片数与线程数,而不是启动时写死)、依据什么信号(队列长度、任务耗时、下游错误率)、以及调整时的收敛(怎么保证调整过程中不丢任务、不重复处理)。「分片后会有线程池的释放吗」要答清生命周期:空闲线程按 keepAliveTime 回收(只回收超过核心线程数的部分)、核心线程默认不回收(除非 allowCoreThreadTimeOut(true))、线程池本身在缩容决策下可以被关闭(shutdown 优雅停止:不再接新任务、处理完队列再退出,shutdownNow 则中断并丢弃队列),关闭时要确保任务不丢(先停止上游投递、再排空队列、最后关闭)。
充值送 token 的幂等与原子性
这类「送权益」的动作必须幂等,否则重试会造成多发。方案分两层:一是幂等键,用「用户 + 活动 + 订单号」构成的业务唯一键,在数据库上建唯一索引,重复插入直接冲突即视为已处理(这是最终兜底,任何其他层都不能替代它);二是状态机与条件更新,把发放记录设计成状态流转(待发放 → 已发放),用 update ... where status = '待发放' 这样的条件更新抢占,影响行数为 0 说明已被处理。原子性上要区分「同一事务内」与「跨服务」:库存或额度扣减与发放记录写入如果在同一个库,用本地事务即可;如果跨服务,用 TCC(Try 冻结额度、Confirm 确认发放、Cancel 释放)或本地消息表加补偿。缓存层的去重(Redis SETNX)只能作为前置的快速拦截,不能作为唯一保证——因为 Redis 可能丢数据、也可能过期。答题时把「数据库唯一索引是最后一道防线」说出来,基本就拿到了这题的分数。
JEV 判题优化与模型部署
这题要区分两件事:判题(业务逻辑)与模型调用(能力来源)。判题的优化点通常是:把重复的评测任务去重与缓存(同一份提交内容直接复用结果)、把同步判题改成异步流水线(提交后入队、结果回调)、把大输入切分与流式处理、限制单次运行的资源与时长(沙箱、超时、内存上限)、以及并发控制(判题机资源固定时用队列与优先级)。与「大模型输入」的区别在于:判题需要确定性、可复现、结果可比对,而模型输出是概率性的——所以判题链路里模型只应出现在「辅助理解题目、生成测试用例、给出提示」这类环节,最终判定必须由确定性代码完成;这也回答了「要不要挂知识库」——如果只是判题不需要,如果要做「按题解解释为什么错」这类能力,挂一份题库与题解知识库是有价值的。部署方式要如实区分(官方 API、第三方中转、自己部署开源模型),并给出选型理由:官方 API 的效果与服务稳定但按量收费、成本随调用量线性增长;自部署前期有 GPU 与运维投入,但长期单价低且数据不出内网;第三方介于两者之间但要多考虑合规与稳定性。收费口径要能讲清(按输入输出 token 分档计价、缓存命中的折扣、批处理的折扣),这也是被追问成本时最实在的答法。
八股:JVM 内存分区、GC、MySQL 索引与慢 SQL
JVM 运行时数据区要分区说清(程序计数器、虚拟机栈、本地方法栈、堆、方法区/元空间 + 直接内存),并说明线程私有与共享、各区域可能抛的异常。垃圾回收要讲清判定(可达性分析 + GC Roots 有哪些)与算法(标记清除、标记复制、标记整理、分代收集),以及常见收集器的特点(Serial、Parallel、CMS 的并发标记与碎片问题、G1 的分区与可预测停顿、ZGC 的低延迟),再补一句「GC 日志怎么看」——先看是哪种 OOM 与 Young/Full 的频率,再定位是分配速率问题还是泄漏。
索引类型按数据结构(B+ 树、哈希、全文)与功能(普通、唯一、主键/聚簇、联合、前缀、覆盖)来讲。索引失效的常见情况要报全:对索引列做函数或运算、隐式类型转换(字符串列传数字)、like '%x' 前导通配、or 连接非索引列、不满足联合索引最左前缀、!=/not in 在大比例数据上导致优化器改选全表(严格说是「可能失效」而非必然,因为优化器按代价决定)、以及数据区分度太低时全表扫更便宜。慢 SQL 的排查按「定位 → 分析 → 优化 → 验证」讲:开慢查询日志、用 EXPLAIN 看 type/key/rows/Extra,关注是否全表扫、有没有 Using filesort、Using temporary;优化手段是补或用对索引、减少返回列、改写 SQL(子查询改 join、避免 or)、处理深分页、必要时做结构优化(归档、分区、汇总表)。「加什么样的索引」的判据是:高频出现在 where/join/order by 的列、区分度高的列、能构成覆盖索引的组合,并按「等值列在前、范围列在后」排联合索引的列序,同时控制索引数量以免拖慢写入。
算法与 ACM 模式
打家劫舍是线性 DP 的入门题:dp[i] = max(dp[i-1], dp[i-2] + nums[i]),滚动变量可优化到 O(1) 空间;要覆盖空数组、单元素、相邻两元素的边界。更值得复盘的是原帖提到的「跟力扣写代码不一样,好像是 ACM 模式」——这是很多实习笔试与面试的常态,必须提前练熟:手写读入(先读 n,再读一行 n 个整数,注意可能跨行或有多余空格)、处理多组测试数据(读到 EOF)、以及按题目要求输出(不要多打印调试信息)。平时的练法是把常刷的题在本地用 ACM 模板重写一遍,形成肌肉记忆;面试时如果一时写不出完整输入输出,也要先把核心逻辑写出来并口述复杂度,避免因为环境不熟而丢掉全部分数。