别再背组件名了,你只需要知道“拆开了之后,怎么让它们好好聊天、不出乱子”
写在前面
微服务是Java面试里**最“唬人”**的一块。什么服务注册、服务发现、负载均衡、熔断降级、分布式事务、分布式ID……名词一大堆,感觉很高大上。
但微服务的本质就一句话:把一个系统拆成多个小系统,各自独立部署。
为什么要拆?因为一个系统太大,100个人改一个代码仓库,天天冲突;一台服务器扛不住所有流量,一个功能挂了整个系统都瘫了。
拆开之后问题就来了——拆开的服务之间怎么通信?怎么知道对方在哪?对方挂了怎么办?怎么保证数据一致性?
Spring Cloud 就是解决这些问题的“工具箱”。
一、注册中心:本质上还是个Map
你前面的理解完全正确:A调用B,A怎么知道B在哪?注册中心就是解决这个问题的。
1.1 注册中心干了什么?
- 服务注册:B启动时,把自己的IP、端口、服务名上报给注册中心(比如Nacos、Eureka)。
- 服务发现:A要调B时,从注册中心拉取B的地址列表。
- 健康检查:B定期发心跳给注册中心,告诉“我还活着”。如果B挂了,注册中心把它踢掉。
底层还是那个Map(key=服务名,value=IP列表)。
1.2 细节:不是每次都去查注册中心(面试常问)
如果每次调用B都去注册中心查,注册中心会被高并发打爆。
真实流程:
- A启动时,从注册中心拉取全量服务列表,缓存到本地内存。
- A调用B时,直接从本地缓存拿地址。
- A和注册中心保持一个长连接(心跳),注册中心发现B有变动(比如B多了一个节点),会主动Push(推送)给A,A更新本地缓存。
所以注册中心不是每次调用时的“中介”,而是“启动时的信息同步器 + 变更时的通知器”。
1.3 Nacos vs Eureka(选型理由)
| 对比 | Nacos | Eureka |
|---|---|---|
| 健康检查 | 支持主动心跳 + 服务端主动探测 | 只支持客户端心跳 |
| 保护模式 | 支持 | 支持 |
| 配置中心 | 自带 | 没有(要配合Spring Cloud Config) |
| 状态 | 活跃开发(Spring Cloud Alibaba) | 已进入维护状态(Netflix宣布停更) |
现在国内大厂基本不用Eureka了,Nacos是标配(因为阿里的技术栈渗透率太高了)。
二、负载均衡:从多个节点里选一个
B部署了多个节点(比如B1、B2、B3),A调用B时,到底调哪一个?
2.1 Ribbon / Spring Cloud LoadBalancer
负载均衡器从本地缓存的地址列表里,按照某种策略选一个出来。
默认策略:轮询(Round Robin)——轮流来,第一个请求B1,第二个B2,第三个B3,第四个又B1。
其他策略:
- 随机(Random):随机挑一个(很少用)。
- 加权响应时间:响应时间短的节点多分配请求(复杂但精准)。
- 最小并发数:当前处理请求最少的节点优先(BestAvailableRule,适合长连接或处理时长不均的场景)。
2.2 负载均衡发生在哪?
客户端负载均衡:Ribbon/LoadBalancer是嵌入在A服务里的,A自己选一个节点发起HTTP请求。没有独立的负载均衡器机器。
这和Nginx(服务端负载均衡)不同:Nginx是独立的代理服务器,请求先到Nginx,Nginx转发到后端。
Spring Cloud用的是客户端负载均衡,因为在微服务架构里,每个服务都是调用方也是被调用方,不需要单点代理层。
三、服务调用:封装HTTP请求(OpenFeign)
你前面说的“服务调用说白了就是封装了HTTP调用流程”,完全正确。
3.1 Feign干了什么?
你写:
@FeignClient(name="stock-service")publicinterfaceStockClient{@GetMapping("/stock/{productId}")StockInfogetStock(@PathVariable("productId")LongproductId);}你直接调stockClient.getStock(123L),Feign底层帮你做了:
- 从注册中心拿到
stock-service的IP列表。 - 负载均衡选一个节点(比如
192.168.1.100:8080)。 - 拼URL:``。
- 发起HTTP请求(用OKHttp或HttpClient)。
- 把返回的JSON转成
StockInfo对象。
你不需要手动拼接URL、处理HTTP连接、解析JSON,一个接口搞定。
3.2 Feign和Ribbon/LoadBalancer的关系
Feign内置了负载均衡器。你调stock-service时,它先去本地缓存里查stock-service的IP列表,再经过负载均衡选一个,然后发起HTTP请求。Feign本身也集成了服务发现能力。
四、熔断 vs 限流(你之前搞混了,这里彻底讲清)
你之前在“熔断”那块说的“1000并发来了10000请求,只处理1000个”,这其实是限流,不是熔断。面试里这两个概念经常被捆绑问,但它们的保护对象完全不同。
4.1 限流(Rate Limiting)——保护“自己”
限流:保护的是服务提供方(B)。B只能处理1000个并发,来了10000个,我就只处理1000个,剩下9000个直接返回“系统繁忙”或者排队。目的:别把我自己打死了。
常用算法:
- 令牌桶(Token Bucket):每秒放N个令牌,拿到令牌的才能处理请求。允许突发流量(令牌可以积累)。
- 漏桶(Leaky Bucket):请求像水一样流进桶里,桶以固定速率漏出去。削平流量,不允许突发。
- 计数器:滑动窗口计数(比如Sentinel的QPS限流)。
4.2 熔断(Circuit Breaker)——保护“别人”
熔断:保护的是服务调用方(A)。B响应越来越慢(比如从50ms变成了5秒),A的线程池里所有线程都被B的慢请求卡住了,A自己的内存和CPU资源耗尽,整个A服务跟着挂掉。
熔断器的逻辑(以Resilience4j为例):
- 关闭状态(正常):请求正常发往B。
- 开启状态(跳闸):错误率达到阈值(比如50%),熔断器打开。接下来N秒内,A根本不发起HTTP请求,直接返回降级结果(比如“商品服务繁忙,请稍后再试”)。给B喘息的时间。
- 半开状态(试探):N秒后,熔断器允许少量请求通过(比如1个),试试B好了没。如果成功,熔断器关闭;如果失败,熔断器重新开启,再等N秒。
熔断和限流的本质区别(面试关键):
- 限流:在服务提供方(B)的门口限流——防止自己被冲垮(防“内伤”)。
- 熔断:在服务调用方(A)内部跳闸——防止自己被慢依赖拖死(防“传染”)。
4.3 Sentinel vs Hystrix
| 对比 | Sentinel | Hystrix(已停更) |
|---|---|---|
| 状态 | 活跃(阿里出品) | 停更(Netflix) |
| 功能 | 限流、熔断、热点参数限流、系统自适应保护 | 主要就熔断和线程隔离 |
| 控制台 | 自带Dashboard | 需要自己搭建 |
| 埋点方式 | 多种适配(Web、Dubbo、gRPC) | 需用@HystrixCommand |
现在线上新项目选Sentinel,Hystrix已经不建议用在新的生产环境了。
五、网关(Gateway):统一小区大门
你前面的理解“分发用户请求到不同的模块”基本对,但网关不只是转发。
网关做的事情:
- 路由转发:用户请求
/order/**转到订单服务,/user/**转到用户服务。 - 统一鉴权:所有请求先过网关,网关验证Token是否有效,放行才转发。不需要每个服务自己写鉴权拦截器。
- 限流:在网关层对总入口做限流(比如全系统总QPS不超过10万)。
- 日志/监控:记录所有请求的入参、出参、耗时(统一埋点)。
Spring Cloud Gateway:
- 基于WebFlux(响应式编程),非阻塞I/O,性能远超Zuul 1.x(同步阻塞)。
- 配置方式:
routes+filters(路由+过滤器链)。
六、分布式事务(重点中的重点)
面试官只要问微服务,分布式事务跑不掉。你已经理解了核心概念,这里把技术选型和本质彻底说透。
场景:A(订单)扣款,B(库存)扣库存。如何在分布式环境下保证一致性?
6.1 2PC/3PC(基本不用)
- 2PC(两阶段提交):准备阶段 + 提交阶段。有一个事务协调者。所有参与者都准备好,协调者才发提交命令。问题:协调者单点故障;准备阶段锁资源时间长;性能差。
- 3PC:加了个超时机制,但本质没解决性能问题。
- 互联网高并发系统很少用2PC/3PC,因为锁资源和同步阻塞太影响性能。
6.2 TCC(Try-Confirm-Cancel)——代码侵入大,但强一致
你之前说“TCC侵入性大,一般不常用”有点绝对了。在资金交易、金融转账等强一致性场景,TCC是主流方案之一。
TCC三阶段:
- Try:尝试执行。比如“冻结账户里的100元”(不实际扣,只是冻结)。
- Confirm:确认执行。Try全部成功后,执行Confirm(“把冻结的100元正式划走”)。
- Cancel:取消回滚。任何一个Try失败,执行所有Cancel(“解冻这100元”)。
为什么侵入性大?
业务系统要实现Try、Confirm、Cancel三个接口,每个接口都要写对应的业务逻辑(冻结、解冻、正式扣减)。而且这三个接口必须幂等(Confirm和Cancel可能被重复调用)。
适合场景:支付、转账、机票锁座。对资金数据零容忍。
6.3 事务消息(RocketMQ)——最终一致性,代码侵入小
你之前已经理解得很好了,但再来回顾一下本质:
核心三步骤:
- 发送半消息(对消费者不可见)。
- 执行本地事务(A扣款),跟本地事务绑定。
- 根据本地事务结果发Commit/Rollback(二次确认)。Commit后消息可见,B扣库存。
反查兜底:如果A发Commit时宕机,RocketMQ会定时回查A:“到底成功了没?”A查数据库告诉MQ是Commit还是Rollback。
本质:这是一种最终一致性方案,允许短暂(毫秒级)的不一致。如果B扣库存失败,不会回滚A的扣款(因为A已经成功了),而是重试 + 人工补偿。适合:下单扣库存、积分增加、日志记录。
6.4 Seata AT模式——对代码0侵入,但性能一般
Seata AT(Auto-Transaction)的原理是:
- 拦截SQL,记录数据修改前后的快照(
before image和after image)到undo_log表。 - 如果事务回滚,根据
undo_log自动生成反向SQL(类似数据库的MVCC)。 - 基于改良版2PC,对代码零侵入(只要加
@GlobalTransactional)。
缺点:全局锁机制在热点数据上有性能瓶颈,且依赖数据库的undo_log表,对数据库有一定的资源消耗。适合并发要求不高的场景。
选型总结(面试核心):
- 强一致 + 资金类→ TCC
- 最终一致 + 高并发(库存、积分)→ 事务消息(RocketMQ)
- 老系统改造/非核心业务→ Seata AT(0侵入)
七、分布式ID(雪花算法)
数据库分库分表后,不能再用自增ID了(多个表自增会冲突)。需要一个全局唯一ID生成器。
常见方案:
- UUID:32位16进制字符串,生成简单,但无序、太长,不适合做数据库主键(B+树插入性能差)。
- Redis自增:
INCR key,但依赖Redis,网络开销大。 - 雪花算法(Snowflake):Twitter开源,最常用。
雪花算法的结构(64位Long,面试能画出结构图就赢了一半)
| 1 bit 符号位 | 41 bits 时间戳 | 10 bits 机器ID | 12 bits 序列号 |解读:
- 1bit:最高位固定为0(保证ID为正数)。
- 41bit:时间戳(毫秒级),能支撑约69年(2^41 / 1000 / 3600 / 24 / 365 ≈ 69年)。
- 10bit:机器ID(5位数据中心 + 5位机器),最多支持1024个节点。
- 12bit:序列号,同一毫秒内最多生成4096个ID(如果不满足,则自旋等待下一毫秒)。
优点:生成快、趋势递增(时间戳在前,方便数据库插入)、不依赖第三方。
八、分布式链路追踪(SkyWalking / Zipkin)
问题:用户一个请求进来,经过了网关→订单→库存→积分,如果某个环节慢了,你根本不知道慢在哪。
链路追踪:给每个请求生成一个全局唯一的Trace ID,在各个服务间传递(放在HTTP Header里)。调用链上的每个Span(跨度)都记录开始时间、结束时间、调用关系。
SkyWalking会把整个调用链画成一张图,让你一眼看到哪个环节耗时最长。
在实际开发中:一般公司会要求日志里带上TraceId,方便排查问题。如果没单独部署SkyWalking,至少要把TraceId打在日志里,方便基于日志平台检索。
九、面试官真正想听什么
场景1:“你们项目怎么拆微服务的?”
加分回答:
“我们是按业务域拆的:订单域、用户域、商品域、库存域。每个域独立数据库,通过接口调用或MQ通信。拆分后遇到了分布式事务问题,下单场景我们用了RocketMQ事务消息保证最终一致性。网关用Spring Cloud Gateway做统一鉴权和路由。注册中心用Nacos,因为它同时支持服务注册和配置管理。”
场景2:“熔断和限流你们怎么做的?”
加分回答(纠正你之前的认知,然后给出实战):
“限流和熔断是两个不同的保护机制。限流我们在网关层和Sentinel配合,对总QPS做限制,超过阈值直接返回‘繁忙’(保护服务方不被冲垮)。熔断我们在调用外部第三方接口时配置了Resilience4j,设置错误率超过50%就跳闸5秒,走降级逻辑(返回兜底数据),防止外部接口慢把我们自己的服务拖死。”
场景3:“分布式事务你们用的哪种方案?”
加分回答(体现架构思考):
“分场景。核心资金流水我们用的TCC,虽然代码侵入大,但数据强一致性有保障。普通的订单扣库存用的RocketMQ事务消息,因为我们允许库存有短暂的延迟,最终一致即可,而且消息方案不会长时间锁表,性能更好。Seata AT我们也评估过,但觉得全局锁在热点商品上可能会成为性能瓶颈,就没用。”
场景4:“雪花算法生成的ID有重复过吗?”
加分回答:
“我们用的开源实现(美团Leaf),时钟回拨是个隐患。如果服务器时间被人为调回去,可能生成重复ID。我们的解决策略是:NTP时间同步做了特殊配置(缓慢调整不回调),并在代码里加了时钟回拨检测,回拨超过5ms就抛异常报警,人工介入。”
小结
| 核心概念 | 大白话 | 面试考点 |
|---|---|---|
| 注册中心 | 存服务地址的Map | 本地缓存+心跳推送 |
| 负载均衡 | 从多个节点里选一个 | 轮询/加权等策略 |
| Feign | 封装HTTP调用 | 注解声明式接口 |
| 限流 | 人多不接了(保护自己) | 令牌桶、漏桶 |
| 熔断 | 对方病了不等了(保护调用方) | 状态机(关→开→半开) |
| 网关 | 统一小区大门 | 路由+鉴权+限流 |
| 分布式事务 | 跨服务保一致性 | TCC / 事务消息 / Seata |
| 分布式ID | 雪花算法 | 64位结构 |
| 链路追踪 | 看请求卡在哪一环 | TraceID透传 |
记住一句话:微服务就是把大系统拆成小系统,然后解决“拆开后怎么通信、怎么治理、怎么保数据一致”的问题。所有组件都是为这三个目标服务的。
下一篇我们讲Docker & K8s——容器到底比虚拟机轻在哪?K8s到底在编排什么?把这个“集装箱调度”彻底讲透。下期见。