快手外包一面复盘:算法、Java 锁、线程池、Redis 与 MySQL
这篇文章整理一次快手外包 Java 一面的面试记录。整体难度不算偏,但覆盖面比较典型:两道基础算法题,加上一串 Java 后端八股,包括锁、线程池、Redis 持久化、MySQL 索引和 explain。
如果是准备 Java 后端一面,这份题单很适合当作一次查漏补缺。
面试题目
算法
- 无重复字符的最长子串
- 反转链表
Java 八股
- Java 里的锁有哪些?区别是什么?
ReentrantLock的加锁过程是什么?- synchronized 锁升级过程
- 线程池核心参数有哪些?拒绝策略有哪些?
- Redis 备份策略
- AOF 的三个刷盘级别
- MySQL 索引类型、索引失效、
explain - 最左匹配原则
算法一:无重复字符的最长子串
题目要求:给定一个字符串,找出其中不含重复字符的最长子串长度。
典型做法是滑动窗口。用两个指针维护一个窗口 [left, right],窗口内保证没有重复字符。遍历右指针,如果当前字符上一次出现的位置在窗口内,就把左指针移动到重复字符的后一位。
1 | import java.util.HashMap; |
面试时可以重点说:
- 时间复杂度是
O(n),每个字符最多被访问常数次。 - 空间复杂度和字符集大小有关,通常可认为是
O(k)。 - 左指针只能向右移动,不能回退,否则会破坏窗口的线性复杂度。
算法二:反转链表
反转链表是链表基础题。最常见的是迭代法,用 prev 保存前一个节点,用 cur 遍历当前节点,每次把 cur.next 指向 prev。
1 | class ListNode { |
回答时可以强调三个指针的作用:
prev:已经反转好的链表头。cur:当前正在处理的节点。next:临时保存下一个节点,避免断链。
Java 里的锁
Java 里常见的锁可以从不同维度分类。
synchronized 和 ReentrantLock
synchronized 是 JVM 层面的内置锁,使用简单,进入同步块自动加锁,退出同步块自动释放锁。发生异常时也会自动释放,不容易忘记解锁。
ReentrantLock 是 JUC 包里的显式锁,需要手动 lock() 和 unlock(),通常要把 unlock() 放到 finally 里。它提供了更丰富的能力,比如可中断锁、尝试加锁、公平锁、多个条件队列。
1 | ReentrantLock lock = new ReentrantLock(); |
可以这样总结:
| 对比项 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM 内置 | JDK 代码实现,基于 AQS |
| 释放锁 | 自动释放 | 手动释放 |
| 可中断 | 不支持等待中断 | 支持 lockInterruptibly() |
| 尝试加锁 | 不支持 | 支持 tryLock() |
| 公平锁 | 不支持显式配置 | 支持公平锁和非公平锁 |
| 条件队列 | 一个对象一个等待队列 | 可创建多个 Condition |
公平锁和非公平锁
公平锁会尽量按照线程等待顺序获取锁,避免饥饿,但吞吐量通常低一些。
非公平锁允许线程插队竞争锁,吞吐量更高,但极端情况下可能让某些线程等待更久。
ReentrantLock 默认是非公平锁:
1 | ReentrantLock nonFairLock = new ReentrantLock(); |
可重入锁
可重入指同一个线程拿到锁以后,可以再次获取同一把锁,不会被自己阻塞。
synchronized 和 ReentrantLock 都是可重入锁。可重入主要解决递归调用、同步方法之间互相调用时的死锁问题。
ReentrantLock 加锁过程
ReentrantLock 底层主要依赖 AQS。AQS 用一个 state 表示锁状态,用一个 CLH 变体队列管理等待线程。
以非公平锁为例,加锁过程大致是:
- 线程调用
lock()。 - 先尝试通过 CAS 把
state从0改成1。 - 如果成功,说明当前线程获得锁,并记录独占线程。
- 如果失败,判断当前持锁线程是不是自己。
- 如果是自己,说明发生重入,
state + 1。 - 如果不是自己,线程会进入 AQS 等待队列。
- 等前驱节点释放锁后,当前节点再尝试获取锁。
释放锁时:
- 当前线程调用
unlock()。 - AQS 把
state - 1。 - 如果
state还不为0,说明只是释放了一层重入。 - 如果
state == 0,清空独占线程,并唤醒等待队列里的后继节点。
面试里可以抓住几个关键词:AQS、state、CAS、可重入、等待队列、LockSupport.park/unpark。
synchronized 锁升级过程
synchronized 在 HotSpot 里经历过锁优化,常见说法是:无锁、偏向锁、轻量级锁、重量级锁。
不过需要注意:偏向锁在 JDK 15 之后被默认禁用,并且后续版本已经逐步移除相关机制。所以如果面试官问锁升级,一般还是按经典流程回答,同时补一句“新版本 JDK 对偏向锁有调整”会更稳。
经典锁升级过程:
- 无锁:对象刚创建时,没有线程进入同步块。
- 偏向锁:如果总是同一个线程获取锁,就把对象头 Mark Word 偏向这个线程,减少 CAS 操作。
- 轻量级锁:当出现其他线程竞争时,偏向锁撤销,线程通过 CAS 尝试获取锁,失败时会自旋。
- 重量级锁:如果竞争激烈或自旋失败,锁膨胀为重量级锁,线程阻塞,依赖操作系统互斥量。
一句话总结:锁升级是 JVM 为了降低加锁成本做的优化,从低竞争场景到高竞争场景逐步升级,升级后通常不会降级。
线程池参数和拒绝策略
Java 线程池最核心的类是 ThreadPoolExecutor。
构造参数如下:
1 | public ThreadPoolExecutor( |
核心参数解释:
| 参数 | 含义 |
|---|---|
corePoolSize |
核心线程数 |
maximumPoolSize |
最大线程数 |
keepAliveTime |
非核心线程空闲存活时间 |
unit |
时间单位 |
workQueue |
任务队列 |
threadFactory |
线程工厂,用于命名线程等 |
handler |
拒绝策略 |
线程池执行流程:
- 当前线程数小于核心线程数,直接创建核心线程执行任务。
- 核心线程已满,把任务放入阻塞队列。
- 队列也满了,且线程数小于最大线程数,创建非核心线程执行任务。
- 线程数达到最大线程数,执行拒绝策略。
常见拒绝策略:
| 策略 | 说明 |
|---|---|
AbortPolicy |
默认策略,直接抛出 RejectedExecutionException |
CallerRunsPolicy |
由提交任务的线程自己执行 |
DiscardPolicy |
直接丢弃任务,不抛异常 |
DiscardOldestPolicy |
丢弃队列中最旧的任务,再尝试提交新任务 |
实际开发里一般不建议静默丢弃任务。更常见的是自定义拒绝策略,记录日志、打点监控、保存任务或做降级处理。
Redis 备份策略
Redis 常见持久化方式有两种:RDB 和 AOF。
RDB
RDB 是快照持久化,会在某个时间点生成 Redis 数据的二进制快照文件。
优点:
- 文件紧凑,适合备份和灾难恢复。
- 恢复速度通常较快。
- 对主线程影响相对可控,通常由子进程执行持久化。
缺点:
- 两次快照之间的数据可能丢失。
fork子进程时可能带来短暂性能抖动。
适合场景:定期全量备份、冷备、数据恢复。
AOF
AOF 会把写命令追加到日志文件中,Redis 重启时通过重放 AOF 恢复数据。
优点:
- 数据安全性更高。
- 可读性比 RDB 好,日志是 Redis 协议格式。
- 支持 AOF rewrite,避免文件无限增长。
缺点:
- 文件通常比 RDB 大。
- 恢复速度可能比 RDB 慢。
- 刷盘频率越高,对性能影响越明显。
混合持久化
Redis 4.0 之后支持 RDB + AOF 混合持久化。AOF rewrite 后的新文件前半部分是 RDB 快照,后半部分是增量 AOF 命令。
它结合了两者优点:恢复速度接近 RDB,数据完整性接近 AOF。
AOF 的三个级别
AOF 的刷盘策略由 appendfsync 控制,常见有三个级别。
| 策略 | 含义 | 优点 | 缺点 |
|---|---|---|---|
always |
每次写命令都刷盘 | 数据安全性最高 | 性能最差 |
everysec |
每秒刷盘一次 | 性能和可靠性均衡 | 最多丢失约 1 秒数据 |
no |
交给操作系统决定刷盘 | 性能最好 | 数据丢失风险最大 |
生产环境最常见的是 everysec。如果业务对数据安全要求极高,可以考虑 always,但要接受性能下降。
MySQL 索引类型
MySQL 索引可以从不同维度分类。
按数据结构分:
B+Tree索引:InnoDB 默认索引结构,适合范围查询、排序、等值查询。- Hash 索引:Memory 引擎支持,InnoDB 的自适应 Hash 索引由数据库内部维护。
- FullText 全文索引:适合文本检索。
- R-Tree 空间索引:适合 GIS 空间数据。
按逻辑功能分:
- 主键索引:
PRIMARY KEY,唯一且非空。 - 唯一索引:
UNIQUE,保证字段值唯一。 - 普通索引:普通加速查询。
- 联合索引:多个字段组成一个索引。
- 覆盖索引:查询字段都能从索引中拿到,不需要回表。
InnoDB 中还要区分聚簇索引和二级索引:
- 聚簇索引:叶子节点存放整行数据,一张表只能有一个,通常是主键。
- 二级索引:叶子节点存放主键值,查询到主键后可能需要回表。
索引失效场景
常见索引失效情况:
- 对索引列使用函数,例如
where date(create_time) = '2026-05-07'。 - 对索引列做计算,例如
where age + 1 = 20。 - 字符串类型不加引号,发生隐式类型转换。
- 使用前置模糊查询,例如
like '%abc'。 - 联合索引不满足最左匹配原则。
or两边不是都能走索引。- 范围查询后面的联合索引字段无法继续用于精确匹配。
- 数据量太小,或优化器判断全表扫描成本更低。
- 查询条件选择性太差,例如性别字段单独建索引。
这里要注意:索引“失效”不是说索引文件不存在,而是优化器没有使用它,或者只使用了联合索引的一部分。
explain 怎么看
explain 用来查看 SQL 的执行计划。面试里重点关注这几个字段。
| 字段 | 关注点 |
|---|---|
id |
查询执行顺序,值越大优先级越高 |
select_type |
查询类型,比如 SIMPLE、PRIMARY、SUBQUERY |
table |
当前访问的表 |
type |
访问类型,判断查询效率的重要字段 |
possible_keys |
可能使用的索引 |
key |
实际使用的索引 |
key_len |
使用索引的长度,可判断联合索引用到了几列 |
rows |
预估扫描行数 |
filtered |
条件过滤比例 |
Extra |
额外信息,比如 Using index、Using filesort |
type 的常见顺序大致是:
1 | system > const > eq_ref > ref > range > index > ALL |
通常来说,至少要尽量达到 range 或 ref,如果出现 ALL,就要重点关注是否全表扫描。
Extra 中常见信息:
Using index:使用覆盖索引,通常是好事。Using where:存储引擎返回数据后,Server 层还要过滤。Using filesort:需要额外排序,可能需要优化索引。Using temporary:使用临时表,常见于复杂排序、分组。
最左匹配原则
最左匹配原则主要针对联合索引。
假设有联合索引:
1 | KEY idx_user_age_city (user_name, age, city) |
能较好使用索引的查询:
1 | select * from user where user_name = 'Tom'; |
不能完整使用联合索引的查询:
1 | select * from user where age = 18; |
因为它们没有从联合索引的最左列 user_name 开始匹配。
再看范围查询:
1 | select * from user |
这里通常能用到 user_name 和 age,但 city 很可能无法继续用于索引过滤。可以简单记成:联合索引从左到右匹配,遇到范围查询后,后面的字段使用能力会明显下降。
设计联合索引时,一般把区分度高、经常用于等值查询、排序或分组的字段放在前面。但最终还是要结合 SQL 场景和 explain 结果判断。
面试回答思路
这次面试题的特点是“基础但宽”。每个点不用讲到源码级别,但要能讲清楚主线。
算法题重点是:
- 先说思路,再写代码。
- 写完主动说复杂度。
- 链表题注意不要断链。
Java 锁重点是:
synchronized和ReentrantLock的区别。ReentrantLock要提 AQS、CAS、state、等待队列。- 锁升级要讲清楚从低竞争到高竞争的变化。
线程池重点是:
- 七个核心参数。
- 任务提交流程。
- 四种拒绝策略。
- 生产环境避免使用无界队列和静默丢弃。
Redis 重点是:
- RDB 适合快照备份。
- AOF 数据安全性更高。
appendfsync everysec是常见折中。- 混合持久化兼顾恢复速度和数据完整性。
MySQL 重点是:
- InnoDB 默认 B+Tree 索引。
- 聚簇索引和二级索引的区别。
- 索引失效场景。
explain重点看type、key、rows、Extra。- 联合索引遵循最左匹配原则。
总结
快手外包一面的题目比较标准,主要考察候选人对 Java 后端基础的熟练程度。算法题是 LeetCode 高频基础题,八股部分集中在并发、缓存、数据库三个方向。
如果准备类似面试,建议把每个知识点都整理成“定义 + 原理 + 使用场景 + 常见坑”的结构。这样面试官追问时,回答不会只停留在背概念,而是能顺着一个清晰的技术主线往下讲。

