快手 Java 面试复盘分享
面试流程
- 自我介绍:按照常规流程介绍了自己的背景、实习项目。
- 项目深挖:面试官让我详细讲解实习项目中的架构设计和技术选型,过程中会不断追问一些实现细节。
技术问题汇总与思考
1. Kafka 消息如何保证不丢失?
- 生产端:开启
acks=all,确保消息被所有 ISR 副本确认;配合retries重试。 - Broker:开启
replication.factor>=3,避免单点失败;开启min.insync.replicas保证至少两个副本成功写入。 - 消费端:先消费再手动提交 offset(避免先提交 offset 再消费导致消息丢失)。
2. LRU 缓存的实现
- 基于 双向链表 + HashMap。
- HashMap 存储 key -> 节点指针;链表维护访问顺序,最近使用的放在头部,最久未使用的在尾部,淘汰时删除尾部节点。
- Java 中
LinkedHashMap自带 LRU 实现。
3. Kafka 一致性检查
- 依靠副本同步机制,ISR(In-Sync Replicas)保证只有和 leader 保持同步的副本才能参与写入确认。
- 事务场景下,可以利用 Kafka 的 幂等生产者(idempotent producer) 和 事务生产者(transactional producer) 保证 exactly-once 语义。
4. 分布式存储 & CDN 加速
- 分布式存储:通过副本、多副本一致性协议(如 Raft、Paxos)保证高可用。
- CDN 加速:就近访问,边缘节点缓存;源站更新后可通过 TTL 或主动刷新保证一致性。
5. ES 与 MySQL 数据一致性
- 使用 双写 + 异步同步机制:业务写 MySQL 后,通过 binlog(如 Canal)实时同步到 ES。
- 问题:可能出现短暂的不一致;解决方法包括幂等更新、定期全量校验。
6. Kafka 异步处理导致延迟
- 核心逻辑放主线程处理,非核心逻辑异步化。
- 比如:下单请求 → 主线程写库 + 返回成功;日志/埋点数据异步写 Kafka。
7. Redis 分布式锁
SET key value NX EX seconds实现加锁,value 通常是唯一请求 ID。- 解锁时校验 value,避免误删他人锁。
- 更高级方案:Redisson 提供自动续期和安全解锁机制。
8. 锁未释放问题
-
若线程崩溃导致锁未释放,可:
- 设置过期时间(EX seconds);
- 用 Redisson 的看门狗机制自动续期,保证锁不会过早过期,同时防止死锁。
9. ThreadLocal 弊端
- ThreadLocal 适合单线程上下文存储,但跨线程(如 RPC 调用)会丢失。
- 缺陷:内存泄漏(线程池复用场景),以及上下文透传问题。
- 解决:使用 显式参数传递 或者 TransmittableThreadLocal。
10. MySQL 自增算法的弊端
- 自增 ID 在分布式场景下可能出现热点写入,且主从复制中 binlog 可能导致冲突。
- 迁移/合并多库时容易出现 ID 冲突。
- 解决:雪花算法(Snowflake)、UUID、数据库号段模式。
11. 乐观锁 vs 悲观锁
- 乐观锁:通过版本号/时间戳,适合冲突少的场景。
- 悲观锁:通过
for update加锁,适合冲突多的场景。 - 结合使用:先乐观尝试,失败时回退到悲观锁,兼顾性能和安全。
12. Spring 事务隔离级别
- 四种隔离级别:读未提交、读已提交、可重复读、串行化。
- 默认:MySQL InnoDB 使用 可重复读(RR) 。
- 可重复读实现:依赖 MVCC + Next-Key Lock,避免幻读。
13. SQL 慢查询分析
- 开启慢查询日志,定位耗时 SQL。
- 使用
EXPLAIN分析执行计划。 - 关注是否命中索引、是否出现回表、是否有全表扫描。
14. 复杂 SQL 索引走法
SELECT * FROM t WHERE a = ? AND b > ? ORDER BY c;
- 索引
(a,b,c)可以全用上。 a=?精确匹配,b>?范围查询,c仍可利用索引进行排序(归并排序),比无索引快。
15. 算法题:二叉树层序遍历
- BFS 思路,用队列实现。
public List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> res = new ArrayList<>();
if (root == null) return res;
Queue<TreeNode> q = new LinkedList<>();
q.add(root);
while (!q.isEmpty()) {
int size = q.size();
List<Integer> level = new ArrayList<>();
for (int i = 0; i < size; i++) {
TreeNode node = q.poll();
level.add(node.val);
if (node.left != null) q.add(node.left);
if (node.right != null) q.add(node.right);
}
res.add(level);
}
return res;
}
总结
面试官不仅耐心,还会引导我思考。通过这次复盘,我意识到:
- 项目要多准备细节,可能会被深挖。
- 分布式、缓存、数据库、消息队列这些高频问题要形成体系化理解。
- 算法要多刷,现场实现更熟练。
面试失败不可怕,重要的是积累与总结。