美团 Java 一面 面经深度解析
来源:牛客网 热爱生活的黑眼圈顶呱呱 岗位:美团 Java 后端开发 特点:25 题覆盖缓存、Redis、MySQL、Java 并发、MQ、微服务六大板块,追问链路长,注重实战经验。
一、缓存相关
Q1:缓存穿透怎么解决?(布隆过滤器;缓存空结果;参数校验)
考点:缓存穿透 → 查询不存在的数据,缓存和数据库都没有,请求直接打到 DB。
答案:
|
方案 |
原理 |
优缺点 |
|---|---|---|
|
布隆过滤器 |
用 bitmap + 多个 hash 函数预先标记所有存在的 key。请求先过布隆过滤器,不存在的 key 直接拦截 |
✅ 内存占用极小(亿级 key 只需几十 MB);❌ 有误判率(判存在的不一定存在),不支持删除 |
|
缓存空值 |
数据库查不到的 key,也缓存一份 null/空对象,设置较短过期时间(1-5 分钟) |
✅ 实现简单;❌ 恶意攻击会创建大量空 key 占内存 |
|
参数校验 |
接口层校验参数合法性(如 ID 不能为负、格式校验) |
✅ 第一道防线;❌ 只能拦截格式非法请求 |
布隆过滤器原理:
插入 key:
key → hash1() → bit1 = 1
key → hash2() → bit2 = 1
key → hash3() → bit3 = 1
查询 key:
key → hash1() = 0? → 一定不存在 ✅
hash1/hash2/hash3 全是 1? → 可能存在(有误判风险)
┌──────────────────────────────────────┐
│ bitmap: [0,1,0,1,0,1,1,0,1,0,...] │
└──────────────────────────────────────┘
生产环境组合方案:布隆过滤器(第一道拦截) + 缓存空值(兜底) + 参数校验(防非法请求)。
Q2:缓存一致性怎么保证?(延迟双删;基于 binlog 监听;设置合理缓存过期时间兜底)
考点:数据库更新后,缓存和 DB 的数据保持一致。
方案一:延迟双删
写操作流程:
1. 删除缓存
2. 更新数据库
3. sleep(几百ms) ← "延迟"的核心
4. 再次删除缓存 ← "双删"的第二次删除
为什么要双删?
- 第一次删:防止其他线程在更新 DB 前读到旧缓存
- 第二次删:防止「读请求在更新 DB 期间把旧值写入缓存」
时间线示例:
T1: 写线程删缓存
T2: 读线程查缓存 miss → 查 DB(旧值)→ sleep
T3: 写线程更新 DB → sleep
T4: 读线程把旧值写回缓存 ← 缓存被污染!
T5: 写线程第二次删缓存 ← 修正污染
方案二:基于 binlog 监听(Canal + MQ)
MySQL → Canal 监听 binlog → MQ → 消费者更新/删除缓存
原理:
1. MySQL 开启 binlog(记录所有数据变更)
2. Canal 伪装成 MySQL Slave,读取 binlog
3. 解析 binlog 得到变更的「表名 + 主键 + 变更类型」
4. 投递到 MQ(RocketMQ/Kafka)
5. 缓存服务消费 MQ 消息,删除/更新对应缓存
优点:异步解耦、最终一致性、不影响业务代码
方案三:设置合理过期时间兜底
- 所有缓存设置过期时间,即使前面方案失败,过期后自动重新加载正确数据
- 过期时间根据业务容忍度设置(如 1 分钟 ~ 1 小时)
生产环境组合:延迟双删(核心)+ binlog 监听(最终一致性)+ 缓存过期(兜底)。
Q3:Redis 持久化有哪些方式?
|
方式 |
原理 |
优点 |
缺点 |
|---|---|---|---|
|
RDB(快照) |
定期(如 save 900 1)将内存数据全量写入 dump.rdb 文件 |
文件紧凑,恢复快,适合灾备 |
两次快照之间的数据会丢失 |
|
AOF(追加文件) |
每条写命令追加到 appendonly.aof 文件,重写机制压缩文件 |
数据安全性高,最多丢 1 秒数据 |
文件大,恢复慢 |
|
混合持久化(Redis 4.0+) |
RDB 做全量 + AOF 做增量,结合两者优点 |
恢复快 + 数据安全 |
文件格式复杂 |
AOF 同步策略 appendfsync:
|
策略 |
行为 |
性能 |
安全性 |
|---|---|---|---|
|
|
每条命令 fsync |
低 |
最高 |
|
|
每秒 fsync一次(默认) |
中 |
高 |
|
|
交给 OS 决定 |
高 |
低 |
Q4:RDB 和 AOF 哪个恢复更快?
答案:RDB 恢复更快。
|
原因 |
说明 |
|---|---|
|
RDB 是二进制快照 |
直接加载到内存即可,O(n) 数据量 |
|
AOF 是命令日志 |
需要逐条重放所有命令,O(n) 命令数 |
|
RDB 文件更小 |
压缩存储;AOF 记录每条写命令,体积更大 |
|
Redis 重启流程 |
优先加载 AOF( |
Q5:Redis 为什么快?(单线程 + I/O 多路复用)
┌──────────────────────────────────────────┐
│ Redis 主线程 │
│ ┌────────────────────────────────────┐ │
│ │ I/O 多路复用(epoll / kqueue) │ │
│ │ 一个线程监听多个 socket 就绪事件 │ │
│ └────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────┐ │
│ │ 命令处理(单线程) │ │
│ │ 内存操作、无锁竞争、无上下文切换 │ │
│ └────────────────────────────────────┘ │
└──────────────────────────────────────────┘
|
原因 |
说明 |
|---|---|
|
纯内存操作 |
数据在内存中,读写纳秒级 |
|
单线程处理命令 |
无锁竞争、无上下文切换开销,保证命令原子性 |
|
I/O 多路复用(epoll) |
单线程高效管理成千上万并发连接,仅在 socket 可读/可写时处理 |
|
高效数据结构 |
动态字符串(SDS)、跳跃表、压缩列表、quicklist 等精心优化 |
|
非阻塞 I/O |
网络 I/O 不阻塞主线程 |
注意:Redis 6.0+ 引入多线程 I/O(仅网络读写用多线程,命令执行仍是单线程)。
二、MySQL 相关
Q6:事务的四大隔离级别?
|
隔离级别 |
脏读 |
不可重复读 |
幻读 |
实现方式 |
|---|---|---|---|---|
|
READ UNCOMMITTED(读未提交) |
✅ 可能 |
✅ 可能 |
✅ 可能 |
不加锁 |
|
READ COMMITTED(读已提交) |
❌ |
✅ 可能 |
✅ 可能 |
每次读生成新 ReadView(MVCC) |
|
REPEATABLE READ(可重复读) |
❌ |
❌ |
✅ 可能(InnoDB 用间隙锁减少) |
事务开始时生成 ReadView(MVCC) + 间隙锁 |
|
SERIALIZABLE(串行化) |
❌ |
❌ |
❌ |
读加共享锁,写加排他锁 |
三种读问题定义:
脏读(Dirty Read):读到其他事务未提交的数据
不可重复读(Non-Repeatable Read):同一事务内两次读同一条记录,结果不同(被其他事务 update 并提交)
幻读(Phantom Read):同一事务内两次范围查询,结果集不同(被其他事务 insert 并提交)
Q7:MySQL 默认隔离级别?它解决了什么问题?
答案:REPEATABLE READ(可重复读)。
解决的三大问题:
|
解决的问题 |
实现机制 |
|---|---|
|
脏读 |
MVCC:ReadView 机制保证只能读到已提交的数据 |
|
不可重复读 |
MVCC:事务开始时创建 ReadView,同一事务内的普通查询始终读到相同快照 |
|
幻读(部分解决) |
MVCC 快照读解决大部分幻读;Next-Key Lock(临键锁 = 记录锁 + 间隙锁) 阻止其他事务在查询范围内插入,解决当前读的幻读 |
为什么只是部分解决?
-- 事务 A
SELECT * FROM t WHERE id > 10; -- 快照读,返回 3 条
-- 事务 B 插入 id=15 并提交
SELECT * FROM t WHERE id > 10; -- 快照读,还是 3 条(MVCC 解决)
UPDATE t SET name='x' WHERE id > 10; -- 当前读,更新了 4 条(包括新插入的)← 幻读!
Q8:MySQL 索引默认使用什么数据结构?
答案:B+ 树。
InnoDB 存储引擎的默认索引结构,聚簇索引(主键索引)和非聚簇索引(二级索引)都用 B+ 树。
Q9:为什么用 B+ 树而不用红黑树?【高频】
|
对比维度 |
红黑树 |
B+ 树 |
|---|---|---|
|
本质 |
二叉搜索树(每个节点 1 个 key + 2 个分支) |
多路搜索树(每个节点 N 个 key + N+1 个分支) |
|
树高 |
高(数据量越大越高) |
矮胖(一个节点存大量 key,层数极少) |
|
磁盘 IO |
每次比较可能需要一次磁盘 IO |
一次 IO 读整个节点(通常 16KB),包含大量 key |
|
范围查询 |
需要中序遍历(多次 IO) |
叶子节点双向链表,找到起点后顺序遍历即可 |
|
节点利用率 |
低(每个节点 1 个 key) |
高(通常填充率 60-70%),空间利用率高 |
|
存储引擎适配 |
不适用于磁盘存储 |
为磁盘 IO 优化(节点大小 = 磁盘页大小) |
核心原因:数据库索引存储在磁盘上,磁盘 IO 次数 = 树的高度。B+ 树极矮(3 层可存 2000 万行),IO 次数极少;红黑树高达 20-30 层,IO 次数无法接受。
Q10:什么是回表?为什么会损耗性能?
考点:聚簇索引 vs 二级索引,最经典的 MySQL 性能问题。
二级索引查询流程:
┌──────────────┐
│ 二级索引 B+树 │ ← 叶子节点存:索引列 + 主键值
│ (name 索引) │
└──────┬───────┘
│ 找到主键 id
▼
┌──────────────┐
│ 聚簇索引 B+树 │ ← 叶子节点存:完整行数据
│ (主键索引) │
└──────┬───────┘
│ 通过主键找到完整行
▼
返回完整记录
回表定义:使用二级索引查询时,索引中不包含所需字段,需根据二级索引存储的主键值回到聚簇索引中查找完整行数据。
性能损耗:多了一次磁盘 IO(从二级索引树跳到聚簇索引树再查一次)。
如何避免回表?覆盖索引(Covering Index):
-- 假设有联合索引 (name, age)
-- ❌ 需要回表(email 不在索引中)
SELECT * FROM t WHERE name = 'Tom';
-- ✅ 覆盖索引(查询列都在索引中,无需回表)
SELECT name, age FROM t WHERE name = 'Tom';
-- Using index 出现在 Extra 中
Q11:一条 SQL 的完整执行流程?
查询流程:
客户端发送 SQL
→ 连接器(验证身份、权限)
→ 查询缓存(MySQL 8.0 已移除)
→ 分析器(词法分析:识别关键字 → 语法分析:检查语法)
→ 优化器(选择索引、决定 JOIN 顺序、生成执行计划)
→ 执行器(调用存储引擎 API,逐行读取/校验权限)
→ 返回结果集
更新流程(比查询多了日志系统):
1. 连接器 → 分析器 → 优化器
2. 执行器:找到目标行
3. InnoDB:将行读入内存(buffer pool)
4. 写 undo log(用于回滚 + MVCC)
5. 修改内存中的数据
6. 写 redo log (prepare 状态) ─┐
7. 写 binlog ├── 两阶段提交
8. redo log 改为 commit 状态 ─┘
9. 返回客户端 "OK"
Q12:更新时涉及哪些日志?两阶段提交是什么?
三种日志:
|
日志 |
所属层 |
格式 |
作用 |
|---|---|---|---|
|
undo log |
InnoDB(引擎层) |
逻辑日志 |
回滚事务 + MVCC(记录修改前的旧值) |
|
redo log |
InnoDB(引擎层) |
物理日志 |
崩溃恢复(记录"数据页做了什么修改"),WAL(Write-Ahead Logging) |
|
binlog |
Server 层 |
逻辑日志 |
数据恢复、主从复制(记录 SQL 语句或行变更) |
两阶段提交:
为什么需要两阶段提交?
问题:redo log 和 binlog 必须一致。如果写完一个系统崩溃,另一个没写 → 主从数据不一致。
两阶段提交流程:
┌─────────────┐ ┌─────────────┐ ┌──────────────┐
│ Prepare 阶段 │ ──→ │ 写 binlog │ ──→ │ Commit 阶段 │
│ redo log 写 │ │ │ │ redo log 标记 │
│ prepare 状态 │ │ │ │ commit 完成 │
└─────────────┘ └─────────────┘ └──────────────┘
崩溃恢复:
- binlog 有记录且 redo log 是 prepare → 自动 commit
- binlog 无记录 → 回滚
三、Java 并发
Q13:Java 创建线程有哪些方式?
|
方式 |
代码示例 |
推荐度 |
|---|---|---|
|
继承 Thread |
|
⭐⭐ 简单场景可用 |
|
实现 Runnable |
|
⭐⭐⭐⭐ 推荐 |
|
实现 Callable |
|
⭐⭐⭐⭐ 有返回值 |
|
线程池 |
|
⭐⭐⭐⭐⭐ 生产必须用 |
// Callable + Future 获取返回结果
Callable<String> task = () -> { Thread.sleep(1000); return "done"; };
FutureTask<String> future = new FutureTask<>(task);
new Thread(future).start();
String result = future.get(); // 阻塞等待结果
// 线程池提交 Callable
ExecutorService pool = Executors.newFixedThreadPool(5);
Future<String> f = pool.submit(() -> "result");
Q14:线程池的 7 大核心参数?
public ThreadPoolExecutor(
int corePoolSize, // 1. 核心线程数(常驻线程)
int maximumPoolSize, // 2. 最大线程数
long keepAliveTime, // 3. 空闲线程存活时间
TimeUnit unit, // 4. 时间单位
BlockingQueue<Runnable> workQueue, // 5. 工作队列
ThreadFactory threadFactory, // 6. 线程工厂(命名/优先级)
RejectedExecutionHandler handler // 7. 拒绝策略
)
任务提交流程:
提交任务 → 核心线程有空闲?
├─ 是 → 核心线程执行
└─ 否 → 队列已满?
├─ 否 → 入队等待
└─ 是 → 线程数 < max?
├─ 是 → 创建新线程
└─ 否 → 拒绝策略
四种拒绝策略:
|
策略 |
行为 |
|---|---|
|
|
抛 |
|
|
由提交任务的线程自己执行 |
|
|
直接丢弃 |
|
|
丢弃队列最旧的任务 |
Q15:子线程异常/结果如何汇总给主线程?
|
方式 |
原理 |
适用场景 |
|---|---|---|
|
CountDownLatch |
主线程 |
等待多个线程都完成后汇总 |
|
CyclicBarrier |
所有线程到达 barrier 点后,最后一个到达的线程触发回调汇总结果 |
多个线程分阶段执行后汇总 |
|
线程池 + Future |
主线程遍历 |
线程池管理,有返回值 |
// CountDownLatch 方式
List<String> results = Collections.synchronizedList(new ArrayList<>());
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
results.add(doWork());
latch.countDown();
}).start();
}
latch.await(); // 等待所有子线程完成
// 线程池 + Future 方式
List<Future<String>> futures = pool.invokeAll(tasks);
for (Future<String> f : futures) {
String result = f.get(); // 阻塞获取结果
}
Q16:start() 和 run() 的区别?
|
|
|
|
|---|---|---|
|
本质 |
启动一个新线程 |
普通方法调用 |
|
调用线程 |
由 JVM 创建的新线程执行 |
当前线程直接执行方法体 |
|
能否多次调用 |
❌ 只能一次(第二次抛 |
✅ 可多次调用 |
|
并发 |
多线程并发 |
顺序执行,不并发 |
Q17:HashMap 的实现原理?
JDK 8:数组 (Node[] table) + 链表 + 红黑树。
put 流程:
1. 计算 hash: (h = key.hashCode()) ^ (h >>> 16)
2. 定位桶: (n - 1) & hash
3. 桶为空 → 直接插入
4. 桶非空 → 遍历链表/树 → key 相同则替换
5. 链表长度 >= 8 且 table >= 64 → 转为红黑树
6. size > threshold → 扩容(2 倍)
Q18:HashMap 是线程安全的吗?
答案:不是。
多线程下的典型问题:
|
问题 |
说明 |
|---|---|
|
数据覆盖 |
两个线程同时 put,A 线程的 value 被 B 覆盖 |
|
扩容死循环 |
JDK 7 头插法扩容可能形成环形链表,CPU 100%(JDK 8 已修复为尾插法) |
|
size 不准确 |
size++ 非原子操作,多线程下统计偏差 |
线程安全替代:ConcurrentHashMap(推荐) 或 Collections.synchronizedMap()。
Q19:JDK 8 ConcurrentHashMap 如何保证线程安全?
JDK 7 → JDK 8 的演进:
|
版本 |
实现 |
并发度 |
锁粒度 |
|---|---|---|---|
|
JDK 7 |
Segment 分段锁(继承 ReentrantLock) |
Segment 数组长度(默认 16) |
段级别 |
|
JDK 8 |
CAS + synchronized + volatile |
无固定并发度限制 |
桶级别 |
JDK 8 实现细节:
// 1. put 时,桶为空 → CAS 尝试插入,无锁
if (table[i] == null) {
casTabAt(table, i, null, new Node<>(hash, key, value));
}
// 2. 桶非空 → synchronized 锁住桶的第一个节点 f
synchronized (f) {
// 遍历链表/红黑树,插入或更新
}
// 3. 扩容迁移:多线程协作,每个线程负责一段槽位的迁移
// 4. Node[] table 用 volatile 修饰,保证可见性
JDK 8 的优点:
- 锁粒度更细:锁单个桶(链表头节点),而不是整个段
- 无锁读取:get 方法无锁(table 是 volatile,Node 的 val 和 next 是 volatile)
- 并发扩容:多线程协同扩容迁移数据
四、消息队列
Q20:如何保证 MQ 消息可靠性?
三方协同保障:
生产者 ──────────→ Broker ──────────→ 消费者
|
环节 |
保证机制 |
实现方式 |
|---|---|---|
|
生产者可靠投递 |
确认机制 + 重试 |
同步发送 + 失败重试、事务消息、Confirm 机制 |
|
Broker 持久化 |
落盘 + 主从 + 同步刷盘 |
消息写入磁盘、主从同步复制、Raft 选举 |
|
消费者可靠消费 |
手动 ACK + 重试 |
业务处理成功后才提交 offset/ack,失败则重试/投死信 |
// RabbitMQ 可靠消费示例
channel.basicConsume(queue, false, (consumerTag, delivery) -> {
try {
process(delivery.getBody()); // 业务处理
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false); // 手动 ACK
} catch (Exception e) {
channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, true); // 重试
}
}, consumerTag -> {});
Q21:消费者怎么保证幂等性?
幂等:同一消息被消费多次,结果与消费一次相同。
|
方案 |
实现 |
适用场景 |
|---|---|---|
|
唯一约束 |
DB 唯一索引( |
有数据库落地的场景 |
|
Redis 记录 |
|
无 DB 落地的场景 |
|
版本号/状态机 |
消息带版本号,消费时判断当前版本是否已更新 |
有业务状态流转的场景 |
|
业务唯一键 |
利用业务天然唯一键(订单号、流水号)去重 |
有业务主键的场景 |
// Redis 幂等处理示例
boolean processed = redis.setnx("msg:" + msgId, String.valueOf(System.currentTimeMillis()));
if (!processed) {
return; // 已处理过,跳过
}
try {
doBusiness(message);
} catch (Exception e) {
redis.del("msg:" + msgId); // 处理失败,删除标记以便重试
throw e;
}
五、微服务
Q22:用过什么 Java 框架?
考点:考察项目经验广度,根据使用过的框架展开追问。
|
方向 |
常用框架 |
|---|---|
|
Web/微服务 |
Spring Boot, Spring Cloud, Spring Cloud Alibaba, Dubbo |
|
ORM |
MyBatis, MyBatis-Plus, JPA, Hibernate |
|
中间件 |
Redis, RocketMQ, RabbitMQ, Kafka, ElasticSearch |
|
数据库 |
MySQL, 连接池(Druid/HikariCP) |
|
网关 |
Spring Cloud Gateway, Nginx |
|
任务调度 |
XXL-JOB, Elastic-Job |
|
容器化 |
Docker, Kubernetes |
Q23:微服务之间怎么相互调用?
|
方式 |
通信协议 |
说明 |
适用 |
|---|---|---|---|
|
HTTP REST |
HTTP/JSON |
通过 RestTemplate / Feign / WebClient 发起 HTTP 调用 |
跨语言、网关层 |
|
RPC(Dubbo/gRPC) |
自定义协议(TCP 长连接) |
高性能二进制协议,服务注册发现 + 负载均衡 |
纯 Java 微服务内部 |
|
消息队列 |
MQ(异步) |
通过 RabbitMQ / RocketMQ / Kafka 异步投递消息 |
异步解耦、削峰 |
Q24:RPC 怎么从 A 服务调用 B 服务?
RPC 完整调用链路:
┌──────┐ ┌──────────────┐ ┌──────┐
│ 服务A │ │ 注册中心(Nacos)│ │ 服务B │
│Consumer│ │ │ │Provider│
└──┬───┘ └──────┬───────┘ └──┬───┘
│ │ │
│ 1. 注册自己 │ 2. 注册自己 │
│────────────────────→│←───────────────────│
│ │ │
│ 3. 订阅服务B列表 │ │
│────────────────────→│ │
│ 4. 返回B的地址列表 │ │
│←────────────────────│ │
│ │ │
│ 5. 选择一台B(负载均衡)┐ │
│ 6. TCP 长连接 → 序列化请求 → 发送 │
│──────────────────────────────────────────→│
│ │ │
│ 7. 接收响应 → 反序列化 → 返回给业务代码 │
│←──────────────────────────────────────────│
关键步骤:
- 服务注册:服务 B 启动时注册到注册中心(Nacos/Zookeeper/Consul)
- 服务发现:服务 A 从注册中心订阅服务 B 的地址列表
- 负载均衡:从地址列表中选择一台(轮询/随机/一致性哈希)
- 序列化:将请求对象序列化为二进制(Protobuf/Hessian/JSON)
- 网络传输:通过 TCP 连接发送(Netty)
- 反序列化 + 反射调用:服务 B 反序列化请求,反射调用目标方法
- 返回响应:结果序列化 → 网络传输 → A 反序列化拿到结果
动态代理的核心作用:
// 服务 A 中调用 B 的代码
@DubboReference
private UserService userService;
User user = userService.getById(1); // 像调本地方法一样
// 实际 userService 是 Dubbo 生成的代理对象
// 代理对象拦截 getById(1) 调用 → 转为 RPC 请求 → 发送到服务 B
Q25:RabbitMQ 怎么调用其他服务?【事件驱动架构】
问题本质:MQ 不直接"调用"服务,而是通过发布-订阅实现服务间异步协作。
┌──────┐ ┌──────────┐ ┌──────┐ ┌──────┐
│ 服务A │ ───→│ RabbitMQ │ ───→│ 服务B │ │ 服务C │
│Producer│ │ Exchange │ │Consumer│ │Consumer│
└──────┘ │ │ └──────┘ └──────┘
│ Queue │ ───→ 服务A 的响应可以由
└──────────┘ 另一个回调队列返回
模式一:异步处理
// 服务 A:发布消息
rabbitTemplate.convertAndSend("order.exchange", "order.created", order);
// 服务 B:消费消息执行业务(如发短信、减库存)
@RabbitListener(queues = "sms.queue")
public void handleOrderCreated(Order order) {
smsService.sendNotification(order.getUserId());
}
模式二:RPC 回调(Request-Reply Pattern)
// 服务 A 发送请求,等待回复
Message response = rabbitTemplate.sendAndReceive(
"rpc.exchange", "rpc.request", requestMessage
);
// RabbitMQ 自动创建临时回调队列,服务 B 处理完后回复到该队列
核心区别:RPC 是同步请求-响应(等结果),MQ 是异步发布-订阅(发完即走)。
面经总结
考点分布
|
领域 |
题数 |
难度 |
追问特点 |
|---|---|---|---|
|
缓存 |
3 |
⭐⭐⭐ |
穿透方案组合、一致性多方案对比、持久化原理层次 |
|
Redis |
3 |
⭐⭐ |
单线程模型深度理解 |
|
MySQL |
7 |
⭐⭐⭐⭐ |
从隔离级别→默认级别→解决的问题→索引结构→为什么 B+ 树→回表→SQL 执行流程→日志→两阶段提交,层层递进 |
|
Java 并发 |
7 |
⭐⭐⭐ |
HashMap→线程安全→ConcurrentHashMap 演进,追问清晰 |
|
MQ |
2 |
⭐⭐⭐ |
可靠性三方协同、幂等性工程设计 |
|
微服务 |
3 |
⭐⭐⭐ |
框架熟悉度→RPC 原理→MQ 异步调用模式 |
美团面试特点
- 追问链路极长:缓存穿透→三种方案对比→每种方案的优缺点;MySQL 隔离级别→默认级别→解决了什么→怎么解决→为什么用 B+ 树→回表是什么,连续追问 7 题
- 注重对比分析:RDB vs AOF、B+ 树 vs 红黑树、start vs run、JDK7 vs JDK8 ConcurrentHashMap
- 从原理到实战:不仅问"是什么",还要问"怎么保证"和"什么场景下用"
- 广度大:一个面试覆盖缓存、Redis、MySQL、JUC、MQ、微服务六大方向