为什么需要微服务拆分?
当你的单体应用代码量超过 10 万行,每次部署都需要 30 分钟,某个小功能的修改可能导致整个系统崩溃时,你就该考虑微服务拆分了。微服务架构通过将系统拆分为多个独立部署的小服务,提升了系统的可维护性、可扩展性和团队自治能力。但拆分不当会带来分布式系统的复杂性,如数据一致性、服务治理等问题。本文将介绍一套经过实践检验的拆分策略,并给出可运行的代码示例。
一、拆分前的准备:领域驱动设计(DDD)
拆分微服务的第一步不是写代码,而是理解业务。推荐使用领域驱动设计(DDD)中的限界上下文(Bounded Context)作为拆分依据。
1.1 识别核心域与子域
以一个电商系统为例,我们可以识别出:
- 核心域:订单、支付、商品
- 支撑子域:用户、库存、物流
- 通用子域:通知、日志、认证
1.2 划分限界上下文
每个限界上下文对应一个微服务。例如:
- 订单上下文:负责订单创建、状态流转
- 支付上下文:负责支付交易、退款
- 商品上下文:负责商品信息、分类
1.3 定义上下文映射
服务之间通过 API 或消息队列通信。例如订单服务需要调用商品服务获取商品价格。
二、拆分策略:按业务能力 vs 按子域
实践中,我更推荐按子域拆分,因为它更贴近业务,且能更好地实现高内聚低耦合。
2.1 拆分原则
- 单一职责:每个服务只负责一个业务功能
- 数据独立:每个服务拥有自己的数据库
- 接口清晰:服务间通过 REST 或 gRPC 通信
- 避免循环依赖:服务调用不能形成环
2.2 一个反例
曾经有个项目将“订单服务”和“订单详情服务”拆分为两个微服务,但两者频繁互相调用,导致延迟激增。正确的做法是将订单详情作为订单服务的一部分。
三、实战:从单体到微服务拆分
假设我们有一个简单的电商单体应用,包含订单、支付、商品三个模块。我们将其拆分为三个微服务。
3.1 技术栈
- Spring Boot 2.7 + Spring Cloud 2021.0.5
- Eureka 作为注册中心
- OpenFeign 服务间调用
- MySQL + Redis
3.2 项目结构
ecommerce/
├── order-service/
├── payment-service/
├── product-service/
└── eureka-server/
3.3 实现步骤
步骤1:创建 Eureka Server
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
application.yml:
server:
port: 8761
eureka:
client:
register-with-eureka: false
fetch-registry: false
步骤2:创建 Product Service
@RestController
@RequestMapping("/products")
public class ProductController {
@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
// 模拟返回商品
return new Product(id, "iPhone 15", 6999.0);
}
}
application.yml:
server:
port: 8081
spring:
application:
name: product-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
步骤3:创建 Order Service(调用 Product Service)
添加 OpenFeign 依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
启用 Feign 客户端:
@SpringBootApplication
@EnableFeignClients
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class, args);
}
}
定义 Feign 接口:
@FeignClient(name = "product-service")
public interface ProductClient {
@GetMapping("/products/{id}")
Product getProduct(@PathVariable("id") Long id);
}
创建订单服务:
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private ProductClient productClient;
@PostMapping
public Order createOrder(@RequestBody OrderRequest request) {
// 调用商品服务获取商品信息
Product product = productClient.getProduct(request.getProductId());
// 创建订单逻辑
Order order = new Order();
order.setProductId(product.getId());
order.setAmount(product.getPrice());
return order;
}
}
application.yml:
server:
port: 8082
spring:
application:
name: order-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
步骤4:处理分布式事务(可选)
在真实场景中,创建订单可能需要扣减库存。这涉及跨服务事务。可以使用最终一致性方案,例如使用消息队列(RabbitMQ)实现可靠事件模式。
// 订单服务发送事件
@Autowired
private RabbitTemplate rabbitTemplate;
public void createOrder(Order order) {
// 保存订单状态为“待确认”
orderRepository.save(order);
// 发送库存扣减事件
rabbitTemplate.convertAndSend("order.exchange", "stock.deduct", order);
}
库存服务监听事件:
@RabbitListener(queues = "stock.queue")
public void handleStockDeduct(Order order) {
// 扣减库存
stockRepository.deduct(order.getProductId(), order.getQuantity());
// 更新订单状态为“已确认”
orderService.confirmOrder(order.getId());
}
> 注意:如果库存扣减失败,需要实现补偿机制(如发送回滚消息)。
四、常见坑与最佳实践
4.1 数据一致性
不要使用分布式事务(如 XA),除非业务对一致性要求极高。优先使用最终一致性和Saga 模式。
4.2 服务间通信
- 同步调用(Feign):适用于低延迟、实时性要求高的场景
- 异步消息(RabbitMQ/Kafka):适用于解耦、削峰填谷
- gRPC:高性能,适合内部服务间调用
4.3 服务治理
- 使用熔断器(Hystrix/Sentinel)防止雪崩
- 使用链路追踪(Sleuth + Zipkin)定位问题
- 使用配置中心(Nacos/Spring Cloud Config)统一管理配置
4.4 数据库拆分
- 每个服务独立数据库,禁止跨服务 join
- 使用API 组合或CQRS处理跨服务查询
五、总结
微服务拆分没有银弹,但遵循 DDD 限界上下文、单一职责和数据独立原则可以大大降低风险。本文从理论到实践,给出了一个完整的分步骤指南,并提供了可运行的代码示例。记住:拆分是手段,不是目的。对于小型项目,单体架构可能更合适。
延伸阅读:
- 《领域驱动设计:软件核心复杂性应对之道》
- 《微服务设计》
- Spring Cloud 官方文档
如果你对文中示例有任何疑问,欢迎留言讨论!