面灵AI

快手 Java 一面:线程池隔离、发券幂等与分片扩容

轮次
一面
时间
2026-09
来源
牛客网

《面试题目》

  1. 实习中有哪些印象深、值得展开的需求或技术工作?
  2. N+1 调用是什么,与串行调用有什么关系?
  3. 分池并发聚合具体解决什么问题,为什么要分成三个线程池?
  4. 不同场景的线程池参数分别怎样确定,依据是什么?
  5. 分池只是为了提高并发度吗,与在一个池里增加核心线程数有什么区别?
  6. 所有任务放进一个池会出现哪些排查困难,分池能否避免线程不足?
  7. 批量接口下移具体指什么?
  8. 三个下游分别耗时 100、200、800 毫秒,串行和并行聚合各需要多久?
  9. 800 毫秒的下游超时后,聚合接口应该怎样返回?
  10. 是否使用 CompletableFuture,三个线程池的结果怎样汇总?
  11. 并发调用多个服务有什么风险?流量增长 100 倍或 1000 倍时,怎样保护上下游?
  12. 排查生产环境 JVM 或内存问题,前十分钟会做哪些事?保服务与保留现场怎样取舍?
  13. MDC 在线程间传递的原理是什么,是否了解 OpenTelemetry?
  14. 长期持有约 810 MB 内存的原因是什么,最后定位到了什么参数?
  15. 意图识别拦截了 60% 的请求,但被拦截部分有 15% 分类错误,这项优化应怎样评价?
  16. Spring 的 publishEvent 默认在哪个线程执行,监听器异常会产生什么影响?
  17. 从双重全量加载改成流式读取后,内存为什么仍可能随着文件大小增长?
  18. 100 万张券面对 10 秒内 1000 万次请求,每人限领一张、不能超发,允许少量失败,链路怎样设计?
  19. Redis Lua 原子扣减库存后服务大量宕机,会出现什么问题,订单怎样恢复?
  20. 按用户 ID 分片,从 8 张表扩到 16 张表,直接修改取模参数会发生什么?正确扩容应怎样做?
  21. 怎样保证 RocketMQ 重复投递不会重复发券,幂等检查应该放在哪一层?
  22. 设计表索引有哪些原则,哪些情况会使预期索引没有发挥作用?
  23. 如何实现支持 O(1) 查询、更新和淘汰的 LRU 缓存?

《参考解析》

分池的价值要落到故障隔离上

三个互不依赖的调用,忽略调度和汇总开销,串行约为 1100 毫秒,并行约为 800 毫秒。并行缩短了单次等待,却没有减少下游处理的工作量。慢服务如果占满共享池,其他任务也会排队;独立的并发额度能限制影响范围,但不能让已经超载的下游凭空多出容量。

参数应结合到达速率、耗时分布、连接池与下游容量确定,并看排队时间和尾延迟。超时后是否接受部分结果,由接口约定决定;任务超时也不等于底层请求已经停止,仍要考虑资源释放。

发券成功要有可恢复的事实

Redis 中扣减成功,只能说明那段脚本执行成功,不能证明订单已经持久化。可以先把请求变成有唯一业务号的持久化事件,再由消费者完成库存条件更新和领券记录写入。库存变更与领券记录尽量放在同一个数据库事务内,并用活动与用户的唯一约束拦住重复领取。

消息重复投递时,消费者返回已有结果;失败时根据持久化事件重试或补偿。若 Redis 负责预扣库存,还必须能识别哪些预扣最终没有形成订单。仅在进程内记一张已处理列表,宕机后就失去依据。

改模会改变旧数据的地址

例如用户编号 9,模 8 落在 1 号表,模 16 则落在 9 号表。直接改路由后,旧记录仍在原表,查询却去了新表。扩容需要明确迁移范围、切换时点及迁移期间的写入归属,核对数据后再切换路由。虚拟分片能减小以后迁移的范围,但不能省去这一次的数据搬迁。

MDC 与流式读取都有存活范围

MDC 通常依靠线程本地存储,线程池复用线程时不会自动继承每次提交者的上下文。可在提交时复制、执行时设置,并在结束时恢复或清理,防止日志串到下一个请求。

流式读取只限制读取阶段的缓冲。如果后面仍把所有解析对象放入列表、缓存或未完成任务队列,内存仍然随输入增长。要看完整链路中对象被谁引用、何时释放,不能仅凭换成流式 API 判断问题已经解决。

LRU 用两种结构各做一件事

哈希表负责按键找到节点,双向链表负责维护最近访问顺序。命中或更新时将节点移到头部,容量超限时删除尾节点,同时删除哈希表条目。边界包括容量为零、更新已有键、删除最后一个节点;并发使用时还要让两个结构的变化保持一致。