Appearance
微服务架构详解
一、为什么需要微服务?
1.1 单体架构的困境
在互联网早期,大多数应用采用单体架构(Monolithic Architecture):所有功能模块打包在一个 war/jar 包中,部署在同一台服务器上。这种架构在项目初期开发效率高、部署简单,但随着业务增长,问题逐渐暴露:
+----------------------------------------------------------+
| 单体应用 (Monolith) |
| +----------------+ +----------------+ +----------------+ |
| | 用户模块 | | 订单模块 | | 商品模块 | |
| +----------------+ +----------------+ +----------------+ |
| | 支付模块 | | 物流模块 | | 库存模块 | |
| +----------------+ +----------------+ +----------------+ |
| 单一数据库 |
+----------------------------------------------------------+单体架构的痛点:
| 痛点 | 描述 |
|---|---|
| 部署耦合 | 改一行代码需要整个应用重新部署,部署窗口大,风险高 |
| 扩展困难 | 无法针对热点模块单独扩容,只能整体扩容,资源浪费 |
| 技术锁定 | 整个应用必须使用同一种技术栈,无法灵活选择 |
| 团队协作冲突 | 多人修改同一代码库,合并冲突频繁 |
| 可靠性差 | 一个模块的 bug(如内存泄漏)可能拖垮整个应用 |
| 认知负担 | 代码量膨胀后,新人难以理解全貌 |
1.2 微服务的核心思想
微服务架构(Microservices Architecture)由 Martin Fowler 和 James Lewis 于 2014 年正式定义:将单一应用划分为一组小服务,每个服务运行在独立进程中,服务间通过轻量级通信机制协作,每个服务围绕业务能力构建,可独立部署。
+----------+ +----------+ +----------+ +----------+
| 用户服务 | | 订单服务 | | 商品服务 | | 支付服务 |
| (独立DB) | | (独立DB) | | (独立DB) | | (独立DB) |
+----------+ +----------+ +----------+ +----------+
| | | |
+----------------+---------------+---------------+
服务注册中心 / API 网关微服务 vs 单体架构对比:
| 维度 | 单体架构 | 微服务架构 |
|---|---|---|
| 部署 | 整体打包部署 | 每个服务独立部署 |
| 扩展 | 整体水平扩展 | 按需对特定服务扩展 |
| 技术栈 | 单一技术栈 | 每个服务可选择不同技术栈 |
| 团队组织 | 按技术分层(前端/后端/DBA) | 按业务领域(跨职能团队) |
| 数据管理 | 统一数据库 | 每个服务有自己的数据库 |
| 故障隔离 | 一处故障可能全局崩溃 | 故障隔离在单个服务内 |
| 测试复杂度 | 低 | 高(需要集成测试) |
| 运维复杂度 | 低 | 高(需要容器化、编排、监控) |
1.3 微服务的核心价值
| 价值 | 说明 |
|---|---|
| 独立部署 | 单个服务修改后无需部署整个应用,部署频率从月级提升到日级甚至小时级 |
| 技术多样性 | 不同服务可以根据场景选择最适合的技术(如推荐系统用 Python,核心交易用 Java) |
| 团队自治 | 每个小团队(2-pizza team)独立负责一个或多个服务,减少跨团队协调 |
| 故障隔离 | 服务 A 宕机不应影响服务 B,通过熔断、降级等机制保护系统 |
| 可扩展性 | 针对热点服务(如秒杀订单服务)独立扩容,资源利用率更高 |
| 可维护性 | 代码库更小,新人上手更快,理解成本更低 |
二、微服务拆分原则
2.1 DDD 限界上下文
DDD(Domain-Driven Design,领域驱动设计)中的**限界上下文(Bounded Context)**是微服务拆分的核心指导思想。一个限界上下文对应一个微服务,上下文内部有统一的业务模型和语言(Ubiquitous Language)。
电商系统限界上下文划分示例:
+------------------+ +------------------+ +------------------+
| 用户上下文 | | 订单上下文 | | 商品上下文 |
| (User Service) | | (Order Service) | | (Product Service) |
| | | | | |
| 用户、地址、积分 | | 订单、订单项、 | | 商品、SKU、分类、 |
| | | 订单状态 | | 库存 |
+------------------+ +------------------+ +------------------+
| | |
v v v
+------------------+ +------------------+ +------------------+
| 支付上下文 | | 物流上下文 | | 营销上下文 |
| (Payment Service) | | (Logistics Svc) | | (Marketing Svc) |
| | | | | |
| 支付单、退款、 | | 运单、物流轨迹、 | | 优惠券、活动、 |
| 对账 | | 仓库 | | 秒杀 |
+------------------+ +------------------+ +------------------+2.2 拆分原则
| 原则 | 说明 | 反例 |
|---|---|---|
| 高内聚低耦合 | 相关行为聚集在一起,服务间依赖最小化 | 把订单和支付拆成同一个服务(耦合过高) |
| 按业务能力拆分 | 围绕业务领域而非技术层次拆分 | 按 Controller/Service/Dao 分层拆分微服务 |
| 单一职责 | 每个服务只负责一个业务领域 | 一个服务既管用户又管订单又管物流 |
| 数据自治 | 每个服务拥有自己的数据库,通过 API 访问数据 | 多个服务直接连接同一个数据库 |
| 渐进式拆分 | 从边缘业务开始拆分,逐步演进 | 一次性全面拆分,风险极高 |
2.3 拆分时机
不要过早拆分! 微服务不是银弹,它带来了分布式系统的复杂性。以下信号表明可以考虑拆分:
- 团队规模超过 20 人,代码合并冲突频繁
- 不同模块的变更速率差异大
- 某些模块需要独立扩缩容
- 不同模块需要不同的技术栈
- 模块边界清晰,业务理解成熟
三、服务间通信
3.1 通信方式对比
| 维度 | 同步通信(REST/gRPC) | 异步通信(消息队列) |
|---|---|---|
| 耦合度 | 高(调用方等待响应) | 低(生产者和消费者解耦) |
| 实时性 | 高(即时响应) | 较低(最终一致) |
| 可靠性 | 低(依赖被调用方可用) | 高(消息持久化,可重试) |
| 适用场景 | 查询、实时操作 | 事件通知、数据同步、削峰 |
| 代表技术 | REST、gRPC、Dubbo | Kafka、RocketMQ、RabbitMQ |
3.2 RESTful API(同步)
最常用的同步通信方式,基于 HTTP 协议,简单通用。
java
// 订单服务调用用户服务获取用户信息
@RestController
public class OrderController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("/orders/{orderId}")
public OrderDetailVO getOrderDetail(@PathVariable Long orderId) {
// 1. 查询订单
Order order = orderService.getById(orderId);
// 2. 通过 REST 调用用户服务获取用户信息
String userServiceUrl = "http://user-service/users/{userId}";
UserVO user = restTemplate.getForObject(userServiceUrl, UserVO.class, order.getUserId());
// 3. 通过 REST 调用商品服务获取商品信息
String productServiceUrl = "http://product-service/products/{productId}";
ProductVO product = restTemplate.getForObject(productServiceUrl, ProductVO.class, order.getProductId());
return OrderDetailVO.builder()
.order(order)
.user(user)
.product(product)
.build();
}
}3.3 gRPC(同步)
基于 HTTP/2 + Protobuf,高性能,适合内部服务间通信。
java
// gRPC 服务端定义 (proto 文件)
// service OrderService {
// rpc GetOrder(GetOrderRequest) returns (GetOrderResponse);
// }
// 服务端实现
@GrpcService
public class OrderGrpcService extends OrderServiceGrpc.OrderServiceImplBase {
@Override
public void getOrder(GetOrderRequest request,
StreamObserver<GetOrderResponse> responseObserver) {
Order order = orderService.getById(request.getOrderId());
GetOrderResponse response = GetOrderResponse.newBuilder()
.setOrderId(order.getId())
.setAmount(order.getAmount().doubleValue())
.setStatus(order.getStatus())
.build();
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
// 客户端调用
@GrpcClient("order-service")
private OrderServiceGrpc.OrderServiceBlockingStub orderStub;
public void queryOrder() {
GetOrderResponse response = orderStub.getOrder(
GetOrderRequest.newBuilder().setOrderId(1001L).build()
);
}3.4 消息队列(异步)
java
// 订单服务:创建订单后发送消息
@Service
public class OrderService {
@Autowired
private RocketMQTemplate rocketMQTemplate;
@Transactional
public Order createOrder(CreateOrderRequest request) {
// 1. 保存订单
Order order = orderDao.save(buildOrder(request));
// 2. 发送订单创建事件(异步通知其他服务)
OrderCreatedEvent event = new OrderCreatedEvent();
event.setOrderId(order.getId());
event.setUserId(order.getUserId());
event.setAmount(order.getAmount());
rocketMQTemplate.convertAndSend("order-created-topic", event);
return order;
}
}
// 库存服务:消费订单创建事件,扣减库存
@Component
@RocketMQMessageListener(topic = "order-created-topic", consumerGroup = "inventory-consumer")
public class OrderCreatedConsumer implements RocketMQListener<OrderCreatedEvent> {
@Override
public void onMessage(OrderCreatedEvent event) {
// 扣减库存
inventoryService.deductStock(event.getProductId(), event.getQuantity());
}
}四、微服务核心挑战与解决方案
4.1 服务发现
问题: 服务实例动态变化(扩缩容、重启),调用方如何知道目标服务在哪里?
解决方案: 服务注册中心(Service Registry)
+----------+ 注册 +----------------+ 发现 +----------+
| 服务提供者 | ---------> | 注册中心 | ---------> | 服务消费者 |
| (Provider) | <--------- | (Nacos/Eureka) | <--------- | (Consumer) |
+----------+ 心跳 +----------------+ 订阅 +----------+Spring Cloud + Nacos 实现:
java
// 服务提供者:注册到 Nacos
@SpringBootApplication
@EnableDiscoveryClient
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
// application.yml
spring:
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
application:
name: order-service
// 服务消费者:通过服务名调用
@Service
public class OrderService {
@Autowired
private RestTemplate restTemplate;
@LoadBalanced // 客户端负载均衡
public UserVO getUser(Long userId) {
// 直接使用服务名,Ribbon/LoadBalancer 自动负载均衡
return restTemplate.getForObject(
"http://user-service/users/" + userId, UserVO.class
);
}
}4.2 配置管理
问题: 微服务数量多,配置分散,修改配置需要重启服务。
解决方案: 配置中心(Nacos Config / Apollo)
java
// Spring Cloud + Nacos 配置中心
@RestController
@RefreshScope // 支持配置热更新
public class ConfigController {
@Value("${order.timeout:30}")
private int orderTimeout; // 可在 Nacos 控制台动态修改,无需重启
@Value("${feature.new-checkout:false}")
private boolean newCheckoutEnabled; // 功能开关(动态灰度)
@GetMapping("/config")
public Map<String, Object> getConfig() {
return Map.of("orderTimeout", orderTimeout,
"newCheckout", newCheckoutEnabled);
}
}
// bootstrap.yml
spring:
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
file-extension: yaml
shared-configs:
- data-id: common-config.yaml
group: DEFAULT_GROUP
refresh: true4.3 服务容错
问题: 一个服务故障可能级联导致整个系统崩溃(雪崩效应)。
解决方案: Sentinel / Resilience4j 实现熔断、降级、限流
java
// Sentinel 熔断降级
@Service
public class OrderService {
@Autowired
private RestTemplate restTemplate;
// 使用 Sentinel 注解实现熔断
@SentinelResource(
value = "getUserInfo",
fallback = "getUserFallback", // 降级方法
blockHandler = "getUserBlockHandler" // 限流处理
)
public UserVO getUserInfo(Long userId) {
return restTemplate.getForObject(
"http://user-service/users/" + userId, UserVO.class
);
}
// 降级方法:当用户服务不可用时返回默认值
public UserVO getUserFallback(Long userId, Throwable e) {
log.error("获取用户信息失败,userId={}", userId, e);
return UserVO.builder()
.userId(userId)
.nickname("匿名用户")
.build();
}
// 限流处理方法
public UserVO getUserBlockHandler(Long userId, BlockException e) {
log.warn("获取用户信息被限流,userId={}", userId);
return UserVO.builder()
.userId(userId)
.nickname("系统繁忙,请稍后重试")
.build();
}
}三种容错模式:
| 模式 | 描述 | 比喻 |
|---|---|---|
| 熔断(Circuit Breaker) | 故障率达到阈值后自动切断调用,快速失败 | 电闸跳闸 |
| 降级(Fallback) | 服务不可用时返回兜底数据 | 备用方案 |
| 限流(Rate Limiting) | 控制请求速率,保护服务不被冲垮 | 排队限流 |
4.4 分布式事务
问题: 一个业务操作跨多个服务,如何保证数据一致性?
解决方案: 最终一致性 > 强一致性,常用 Seata(AT/TCC/Saga 模式)
java
// Seata AT 模式:对业务无侵入,自动回滚
@Service
public class OrderService {
@Autowired
private AccountServiceClient accountClient;
@Autowired
private InventoryServiceClient inventoryClient;
@GlobalTransactional // 一键开启分布式事务
@Transactional
public void createOrder(CreateOrderRequest request) {
// 1. 创建订单(本地事务)
orderDao.save(buildOrder(request));
// 2. 扣减库存(远程调用)
inventoryClient.deduct(request.getProductId(), request.getQuantity());
// 3. 扣减余额(远程调用)
accountClient.debit(request.getUserId(), request.getAmount());
// 任何一步失败,Seata 自动回滚所有操作
}
}4.5 API 网关
问题: 客户端需要知道所有服务地址,认证、限流等逻辑散落在各服务中。
解决方案: Spring Cloud Gateway
java
// Spring Cloud Gateway 路由配置
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/api/users/**
filters:
- StripPrefix=1
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 100
redis-rate-limiter.burstCapacity: 200
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/orders/**
filters:
- StripPrefix=1
// 自定义全局过滤器:认证鉴权
@Component
public class AuthFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
if (token == null || !validateToken(token)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
}4.6 可观测性
问题: 一个请求跨多个服务,如何追踪调用链、定位问题?
解决方案: 日志收集(ELK)+ 链路追踪(SkyWalking / Zipkin)+ 指标监控(Prometheus + Grafana)
+--------+ +--------+ +--------+
| 服务 A | --> | 服务 B | --> | 服务 C |
+--------+ +--------+ +--------+
| | |
+-------------+--------------+
|
v
+------------------+
| SkyWalking 链路追踪 |
| 全链路拓扑 + 性能分析 |
+------------------+java
// 使用 Micrometer + Prometheus 暴露指标
@RestController
public class MetricsController {
private final MeterRegistry meterRegistry;
private final Counter orderCounter;
public MetricsController(MeterRegistry meterRegistry) {
this.meterRegistry = meterRegistry;
// 自定义业务指标
this.orderCounter = Counter.builder("orders.created.total")
.description("Total number of orders created")
.register(meterRegistry);
}
@PostMapping("/orders")
public Order createOrder(@RequestBody CreateOrderRequest request) {
Order order = orderService.create(request);
orderCounter.increment(); // 打点
return order;
}
}五、Spring Cloud 生态全景
| 组件 | 功能 | 推荐方案 | 替代方案 |
|---|---|---|---|
| 服务注册与发现 | 服务实例管理 | Nacos | Eureka, Consul, Zookeeper |
| 配置中心 | 动态配置管理 | Nacos | Apollo, Spring Cloud Config |
| API 网关 | 统一入口,路由转发 | Spring Cloud Gateway | Zuul, Kong |
| 服务调用 | RPC 通信 | OpenFeign + LoadBalancer | Dubbo, gRPC |
| 熔断降级 | 服务容错 | Sentinel | Resilience4j, Hystrix(已停更) |
| 分布式事务 | 跨服务事务 | Seata | 基于 MQ 的最终一致性 |
| 链路追踪 | 调用链分析 | SkyWalking | Zipkin, Jaeger |
| 消息驱动 | 异步通信 | Spring Cloud Stream | 直接使用 RocketMQ / Kafka |
六、CAP 定理在微服务中的应用
CAP 定理:一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)、分区容错性(Partition Tolerance)中的两个。
C (一致性)
/\
/ \
/ \
/ CA \
/--------\
/ \
/ CP \ AP
/--------------\----------
P A
(分区容错性) (可用性)在微服务中,P(分区容错)是必须选择的(网络分区不可避免),所以实际是在 CP 和 AP 之间选择:
| 选择 | 含义 | 典型场景 | 技术选型 |
|---|---|---|---|
| CP | 发生分区时,优先保证一致性,牺牲可用性 | 金融交易、账户余额 | Zookeeper, Etcd |
| AP | 发生分区时,优先保证可用性,牺牲强一致性 | 社交动态、商品浏览 | Eureka, Nacos |
微服务中的实践: 大多数业务场景选择 AP(最终一致性),通过消息队列、补偿机制保证数据最终一致。对于核心金融场景,使用 CP 方案或 TCC 分布式事务。
七、微服务代码示例:完整电商微服务
7.1 项目结构
ecommerce-platform/
├── ecommerce-gateway/ # API 网关
├── ecommerce-common/ # 公共模块
├── user-service/ # 用户服务
│ ├── user-service-api/ # Feign 接口定义
│ └── user-service-impl/ # 接口实现
├── order-service/ # 订单服务
├── product-service/ # 商品服务
└── inventory-service/ # 库存服务7.2 OpenFeign 声明式调用
java
// 定义 Feign 接口(放在 user-service-api 模块中,供其他服务引用)
@FeignClient(name = "user-service", path = "/users",
fallbackFactory = UserClientFallbackFactory.class)
public interface UserClient {
@GetMapping("/{userId}")
Result<UserVO> getUserById(@PathVariable("userId") Long userId);
@GetMapping("/batch")
Result<List<UserVO>> getUsersByIds(@RequestParam("ids") List<Long> ids);
}
// 降级工厂:获取异常信息
@Component
public class UserClientFallbackFactory implements FallbackFactory<UserClient> {
@Override
public UserClient create(Throwable cause) {
log.error("UserClient 调用失败", cause);
return new UserClient() {
@Override
public Result<UserVO> getUserById(Long userId) {
return Result.fail("用户服务不可用");
}
@Override
public Result<List<UserVO>> getUsersByIds(List<Long> ids) {
return Result.fail("用户服务不可用");
}
};
}
}
// 订单服务中使用 Feign 调用
@Service
public class OrderService {
@Autowired
private UserClient userClient; // 像调用本地方法一样调用远程服务
public OrderDetailVO getOrderDetail(Long orderId) {
Order order = orderDao.getById(orderId);
Result<UserVO> userResult = userClient.getUserById(order.getUserId());
return OrderDetailVO.builder()
.order(order)
.user(userResult.getData())
.build();
}
}八、面试高频问题
Q1: 微服务和单体架构各有什么优缺点?什么时候应该选择微服务?
- 单体:开发简单、部署简单、调试方便,适合小型项目和早期阶段
- 微服务:独立部署、技术异构、故障隔离,适合大型项目和多团队协作
- 选择标准:团队规模 > 20 人,业务复杂度高,有独立部署需求时考虑微服务
Q2: 如何确定微服务的拆分粒度?
- 以 DDD 限界上下文为参考,一个上下文一个服务
- 遵循"两个人原则":一个服务足够小,可以让两个开发人员完全理解
- 从粗粒度开始,逐步细化,避免过度拆分
Q3: 微服务间如何保证数据一致性?
- 尽量避免分布式事务,通过业务设计规避
- 对于必须跨服务的数据一致性,使用最终一致性方案:
- 消息队列 + 本地消息表
- Seata(AT/TCC/Saga 模式)
- 补偿机制(对账、定时任务修复)
Q4: 如何解决微服务调用链过长的问题?
- 减少服务嵌套,合理划分服务粒度
- 使用异步消息解耦,减少同步调用链
- 数据冗余(如订单服务冗余用户昵称、商品名称)
- 服务合并(过度拆分的服务合并回去)
Q5: 服务注册中心的 CAP 如何选择?
- Nacos:默认 AP,支持切换到 CP(通过 Distro 协议和 Raft 协议)
- Eureka:AP,优先保证可用性
- Zookeeper:CP,Leader 选举期间不可用
- 推荐:微服务注册中心选 AP(可用性优先),配置中心选 CP(一致性优先)
Q6: 如何实现服务平滑上下线?
- 上线:先注册但不接收流量 → 预热 → 逐步放开流量
- 下线:先从注册中心摘除 → 等待 in-flight 请求处理完 → 优雅关闭(Spring Boot Graceful Shutdown)
- 配置示例:
server.shutdown=graceful+spring.lifecycle.timeout-per-shutdown-phase=30s
Q7: 分布式链路追踪的原理是什么?
一个请求进入系统后,生成全局唯一的 TraceId。每个服务处理时生成自己的 SpanId,并将 TraceId 透传到下游服务。所有 Span 上报到追踪系统,形成完整的调用链。关键组件:TraceId(全局跟踪)、SpanId(单个服务调用)、ParentSpanId(父子关系)。
