跳转到内容

美团 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

策略

行为

性能

安全性

always

每条命令 fsync

最高

everysec

每秒 fsync一次(默认)

no

交给 OS 决定


Q4:RDB 和 AOF 哪个恢复更快?

答案:RDB 恢复更快。

原因

说明

RDB 是二进制快照

直接加载到内存即可,O(n) 数据量

AOF 是命令日志

需要逐条重放所有命令,O(n) 命令数

RDB 文件更小

压缩存储;AOF 记录每条写命令,体积更大

Redis 重启流程

优先加载 AOF(appendonly yes 时),因为数据更完整


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

class MyThread extends Thread { @Override run() {} }

⭐⭐ 简单场景可用

实现 Runnable

class MyRun implements Runnable { @Override run() {} }

⭐⭐⭐⭐ 推荐

实现 Callable

class MyCall implements Callable<String> { @Override call() {} }

⭐⭐⭐⭐ 有返回值

线程池

Executors.newFixedThreadPool(10)new ThreadPoolExecutor(...)

⭐⭐⭐⭐⭐ 生产必须用

// 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?
                                  ├─ 是 → 创建新线程
                                  └─ 否 → 拒绝策略

四种拒绝策略

策略

行为

AbortPolicy(默认)

RejectedExecutionException

CallerRunsPolicy

由提交任务的线程自己执行

DiscardPolicy

直接丢弃

DiscardOldestPolicy

丢弃队列最旧的任务


Q15:子线程异常/结果如何汇总给主线程?

方式

原理

适用场景

CountDownLatch

主线程 await() 阻塞等待计数器归零;子线程完成任务后 countDown(),将结果存入线程安全的共享集合(如 ConcurrentLinkedQueue

等待多个线程都完成后汇总

CyclicBarrier

所有线程到达 barrier 点后,最后一个到达的线程触发回调汇总结果

多个线程分阶段执行后汇总

线程池 + Future

主线程遍历 List&lt;Future&lt;T&gt;&gt;,逐个调用 future.get() 阻塞获取结果

线程池管理,有返回值

// 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() 的区别?

start()

run()

本质

启动一个新线程

普通方法调用

调用线程

由 JVM 创建的新线程执行 run()

当前线程直接执行方法体

能否多次调用

❌ 只能一次(第二次抛 IllegalThreadStateException

✅ 可多次调用

并发

多线程并发

顺序执行,不并发


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 唯一索引(UNIQUE INDEX ON msg_id),重复插入直接忽略

有数据库落地的场景

Redis 记录

SETNX msg_id {timestamp},消费前先判断是否已处理

无 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. 接收响应 → 反序列化 → 返回给业务代码     │
   │←──────────────────────────────────────────│

关键步骤

  1. 服务注册:服务 B 启动时注册到注册中心(Nacos/Zookeeper/Consul)
  2. 服务发现:服务 A 从注册中心订阅服务 B 的地址列表
  3. 负载均衡:从地址列表中选择一台(轮询/随机/一致性哈希)
  4. 序列化:将请求对象序列化为二进制(Protobuf/Hessian/JSON)
  5. 网络传输:通过 TCP 连接发送(Netty)
  6. 反序列化 + 反射调用:服务 B 反序列化请求,反射调用目标方法
  7. 返回响应:结果序列化 → 网络传输 → 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 异步调用模式

美团面试特点

  1. 追问链路极长:缓存穿透→三种方案对比→每种方案的优缺点;MySQL 隔离级别→默认级别→解决了什么→怎么解决→为什么用 B+ 树→回表是什么,连续追问 7 题
  2. 注重对比分析:RDB vs AOF、B+ 树 vs 红黑树、start vs run、JDK7 vs JDK8 ConcurrentHashMap
  3. 从原理到实战:不仅问"是什么",还要问"怎么保证"和"什么场景下用"
  4. 广度大:一个面试覆盖缓存、Redis、MySQL、JUC、MQ、微服务六大方向