快手 Java 一面:线程池隔离、发券幂等与分片扩容
- 轮次
- 一面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- 实习中有哪些印象深、值得展开的需求或技术工作?
- N+1 调用是什么,与串行调用有什么关系?
- 分池并发聚合具体解决什么问题,为什么要分成三个线程池?
- 不同场景的线程池参数分别怎样确定,依据是什么?
- 分池只是为了提高并发度吗,与在一个池里增加核心线程数有什么区别?
- 所有任务放进一个池会出现哪些排查困难,分池能否避免线程不足?
- 批量接口下移具体指什么?
- 三个下游分别耗时 100、200、800 毫秒,串行和并行聚合各需要多久?
- 800 毫秒的下游超时后,聚合接口应该怎样返回?
- 是否使用 CompletableFuture,三个线程池的结果怎样汇总?
- 并发调用多个服务有什么风险?流量增长 100 倍或 1000 倍时,怎样保护上下游?
- 排查生产环境 JVM 或内存问题,前十分钟会做哪些事?保服务与保留现场怎样取舍?
- MDC 在线程间传递的原理是什么,是否了解 OpenTelemetry?
- 长期持有约 810 MB 内存的原因是什么,最后定位到了什么参数?
- 意图识别拦截了 60% 的请求,但被拦截部分有 15% 分类错误,这项优化应怎样评价?
- Spring 的 publishEvent 默认在哪个线程执行,监听器异常会产生什么影响?
- 从双重全量加载改成流式读取后,内存为什么仍可能随着文件大小增长?
- 100 万张券面对 10 秒内 1000 万次请求,每人限领一张、不能超发,允许少量失败,链路怎样设计?
- Redis Lua 原子扣减库存后服务大量宕机,会出现什么问题,订单怎样恢复?
- 按用户 ID 分片,从 8 张表扩到 16 张表,直接修改取模参数会发生什么?正确扩容应怎样做?
- 怎样保证 RocketMQ 重复投递不会重复发券,幂等检查应该放在哪一层?
- 设计表索引有哪些原则,哪些情况会使预期索引没有发挥作用?
- 如何实现支持 O(1) 查询、更新和淘汰的 LRU 缓存?
《参考解析》
分池的价值要落到故障隔离上
三个互不依赖的调用,忽略调度和汇总开销,串行约为 1100 毫秒,并行约为 800 毫秒。并行缩短了单次等待,却没有减少下游处理的工作量。慢服务如果占满共享池,其他任务也会排队;独立的并发额度能限制影响范围,但不能让已经超载的下游凭空多出容量。
参数应结合到达速率、耗时分布、连接池与下游容量确定,并看排队时间和尾延迟。超时后是否接受部分结果,由接口约定决定;任务超时也不等于底层请求已经停止,仍要考虑资源释放。
发券成功要有可恢复的事实
Redis 中扣减成功,只能说明那段脚本执行成功,不能证明订单已经持久化。可以先把请求变成有唯一业务号的持久化事件,再由消费者完成库存条件更新和领券记录写入。库存变更与领券记录尽量放在同一个数据库事务内,并用活动与用户的唯一约束拦住重复领取。
消息重复投递时,消费者返回已有结果;失败时根据持久化事件重试或补偿。若 Redis 负责预扣库存,还必须能识别哪些预扣最终没有形成订单。仅在进程内记一张已处理列表,宕机后就失去依据。
改模会改变旧数据的地址
例如用户编号 9,模 8 落在 1 号表,模 16 则落在 9 号表。直接改路由后,旧记录仍在原表,查询却去了新表。扩容需要明确迁移范围、切换时点及迁移期间的写入归属,核对数据后再切换路由。虚拟分片能减小以后迁移的范围,但不能省去这一次的数据搬迁。
MDC 与流式读取都有存活范围
MDC 通常依靠线程本地存储,线程池复用线程时不会自动继承每次提交者的上下文。可在提交时复制、执行时设置,并在结束时恢复或清理,防止日志串到下一个请求。
流式读取只限制读取阶段的缓冲。如果后面仍把所有解析对象放入列表、缓存或未完成任务队列,内存仍然随输入增长。要看完整链路中对象被谁引用、何时释放,不能仅凭换成流式 API 判断问题已经解决。
LRU 用两种结构各做一件事
哈希表负责按键找到节点,双向链表负责维护最近访问顺序。命中或更新时将节点移到头部,容量超限时删除尾节点,同时删除哈希表条目。边界包括容量为零、更新已有键、删除最后一个节点;并发使用时还要让两个结构的变化保持一致。