微服务实战:从单体到分布式架构的演进之路

上一篇文章梳理了 Spring Cloud 各个组件的作用和学习路径。这次想聊聊更实际的问题:当你真正要把一个系统拆成微服务时,具体该怎么做?

我参考了github各个开源老师的 Spring Cloud 实战仓库结合其中的完整项目案例,整理了一套从服务拆分、数据库设计到分布式事务的实战思路。


一、服务拆分:怎么拆才合理

1.1 拆分的核心原则

微服务拆分没有标准答案,但有几个基本原则可以参考:

按业务边界拆分

每个服务对应一个明确的业务领域,比如用户服务、订单服务、商品服务。这样服务之间的职责清晰,团队可以独立开发和部署。

高内聚、低耦合

服务内部的功能紧密相关,服务之间的依赖尽量少。如果两个服务频繁互相调用,可能说明拆分得不够合理。

数据独立性

每个服务应该有自己的数据库,避免多个服务直接操作同一张表。这是微服务和传统 SOA 的重要区别。

1.2 实战项目的服务拆分案例

参考仓库中的房产项目(fangjia-fsh),它的拆分方式如下:

服务 职责 对应模块
API 网关 统一入口、路由转发、鉴权 fangjia-fsh-api
用户服务 用户注册、登录、个人信息 fangjia-fsh-user-service
房产服务 房源信息、搜索、展示 fangjia-fsh-house-service
替换服务 房源替换逻辑 fangjia-fsh-substitution-service
认证服务 服务间调用鉴权、Token 颁发 fangjia-auth-service
注册中心 服务发现与注册 fangjia-eureka
监控服务 服务健康状态监控 fangjia-boot-admin

拆分要点

  • 用户服务和房产服务是核心业务,独立拆分
  • 认证逻辑抽离成独立服务,避免每个服务重复实现
  • API 网关作为统一入口,对外暴露 REST 接口,对内路由到具体服务

二、数据库设计:每个服务一个数据库

2.1 数据库拆分的必要性

微服务架构中,每个服务应该拥有独立的数据库。这样做的好处:

  • 服务之间解耦,一个服务的数据库变更不会影响其他服务
  • 可以根据服务特点选择不同的数据库类型(MySQL、MongoDB、Redis 等)
  • 便于独立扩展,热点服务可以单独做读写分离或分库分表

2.2 读写分离实战

当读请求远多于写请求时,读写分离是常用的优化手段。仓库中的 fangjia-sjdbc-read-write 模块展示了基于 ShardingJDBC 的实现:

yaml 复制代码
spring:
  shardingsphere:
    datasource:
      names: master, slave
      master:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/master_db
        username: root
        password: 123456
      slave:
        type: com.zaxxer.hikari.HikariDataSource
        driver-class-name: com.mysql.cj.jdbc.Driver
        jdbc-url: jdbc:mysql://localhost:3306/slave_db
        username: root
        password: 123456
    masterslave:
      name: ms
      master-data-source-name: master
      slave-data-source-names: slave

关键点

  • 写操作路由到主库(master)
  • 读操作路由到从库(slave)
  • 主从同步由 MySQL 自身机制保证

2.3 分库分表实战

当单表数据量过大(通常超过千万级)时,需要考虑分库分表。仓库提供了两个示例:

分表(sharding-table):同一数据库内,按某个字段将数据分散到多张表

yaml 复制代码
spring:
  shardingsphere:
    sharding:
      tables:
        t_order:
          actual-data-nodes: ds0.t_order_${0..9}
          table-strategy:
            inline:
              sharding-column: order_id
              algorithm-expression: t_order_${order_id % 10}

分库分表(sharding-db-table):数据分散到多个数据库的多张表

yaml 复制代码
spring:
  shardingsphere:
    sharding:
      default-database-strategy:
        inline:
          sharding-column: user_id
          algorithm-expression: ds${user_id % 2}
      tables:
        t_order:
          actual-data-nodes: ds${0..1}.t_order_${0..9}

分片策略选择

策略 适用场景 优点 缺点
取模分片 数据分布均匀的场景 负载均衡 扩容时需要迁移数据
范围分片 按时间、ID 范围查询的场景 扩容方便 可能存在热点
哈希分片 需要均匀分布的场景 避免热点 范围查询效率低

三、分布式事务:怎么保证数据一致性

3.1 为什么需要分布式事务

微服务架构下,一个业务操作可能涉及多个服务。比如下单操作:

  1. 订单服务创建订单
  2. 库存服务扣减库存
  3. 用户服务扣减余额

这三个操作必须同时成功或同时失败,否则会出现数据不一致。

3.2 分布式事务解决方案对比

方案 原理 优点 缺点 适用场景
2PC 两阶段提交,协调者统一管理 强一致性 性能差、协调者单点故障 对一致性要求极高的金融场景
TCC Try-Confirm-Cancel,业务层实现补偿 性能较好 业务侵入性强,开发成本高 电商、支付等高并发场景
** Saga** 长事务拆分,失败时执行补偿操作 性能好 最终一致性,补偿逻辑复杂 业务流程长、需要异步处理的场景
可靠消息 通过消息队列保证最终一致性 解耦、性能好 实现复杂,需要处理幂等 大多数异步场景

3.3 实战:基于可靠消息的分布式事务

仓库中的 transaction-mq-service 模块实现了一套可靠消息服务,核心思路:

scss 复制代码
┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│  业务服务    │────▶│  消息服务    │────▶│  消息队列    │
│ (发起方)    │     │ (可靠消息)   │     │ (RocketMQ)  │
└─────────────┘     └─────────────┘     └──────┬──────┘
                                                │
                                                ▼
                                         ┌─────────────┐
                                         │  消费服务    │
                                         │ (接收方)    │
                                         └─────────────┘

核心流程

  1. 业务服务 执行业务操作,同时向消息服务发送预消息
  2. 消息服务将消息状态设为"待确认",返回给业务服务
  3. 业务服务执行业务逻辑,成功后调用消息服务确认消息
  4. 消息服务将消息投递到 MQ
  5. 消费服务消费消息,执行业务操作

消息表设计

sql 复制代码
CREATE TABLE transaction_message (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    message_id VARCHAR(64) NOT NULL UNIQUE,
    message_body TEXT NOT NULL,
    destination VARCHAR(255) NOT NULL,
    status TINYINT NOT NULL DEFAULT 0,  -- 0:待发送 1:已发送 2:消费成功 3:消费失败
    send_times INT DEFAULT 0,
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
    update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_status_time (status, create_time)
);

定时补偿机制

java 复制代码
@Component
public class MessageRetryTask {
    
    @Scheduled(fixedRate = 60000) // 每分钟执行
    public void retry() {
        // 查询待发送和发送失败的消息
        List<TransactionMessage> messages = messageService
            .findByStatusAndTime(MessageStatus.PENDING, 5);
        
        for (TransactionMessage msg : messages) {
            if (msg.getSendTimes() > 3) {
                // 超过重试次数,人工介入
                msg.setStatus(MessageStatus.FAILED);
                messageService.update(msg);
                continue;
            }
            
            // 重新发送
            rocketMQTemplate.asyncSend(msg.getDestination(), 
                msg.getMessageBody(), new SendCallback() {
                    @Override
                    public void onSuccess(SendResult sendResult) {
                        msg.setStatus(MessageStatus.SENT);
                        messageService.update(msg);
                    }
                    @Override
                    public void onException(Throwable e) {
                        msg.setSendTimes(msg.getSendTimes() + 1);
                        messageService.update(msg);
                    }
                });
        }
    }
}

关键点

  • 消息先落库再发送,保证消息不丢失
  • 定时任务补偿,处理发送失败的消息
  • 消费端幂等处理,避免重复消费
  • 超过重试次数的消息转入死信队列,人工处理

四、服务间调用安全:怎么防止接口被乱调

4.1 问题背景

微服务内部互相调用时,如果不对调用方进行校验,任何一个服务被攻破,都可能直接调用其他服务的内部接口。

4.2 解决方案:内部认证服务

仓库中的 fangjia-auth-service 实现了服务间调用的认证机制:

java 复制代码
@FeignClient(name = "auth-service")
public interface AuthClient {
    @PostMapping("/auth/token")
    AuthResponse getToken(@RequestBody AuthRequest request);
    
    @PostMapping("/auth/verify")
    boolean verifyToken(@RequestHeader("Authorization") String token);
}

调用流程

  1. 服务 A 调用服务 B 前,先向认证服务申请 Token
  2. 服务 A 在请求头中携带 Token 调用服务 B
  3. 服务 B 通过拦截器验证 Token 的有效性
  4. 验证通过才执行业务逻辑
java 复制代码
@Component
public class AuthInterceptor implements HandlerInterceptor {
    
    @Autowired
    private AuthClient authClient;
    
    @Override
    public boolean preHandle(HttpServletRequest request, 
                           HttpServletResponse response, 
                           Object handler) {
        String token = request.getHeader("Authorization");
        if (StringUtils.isEmpty(token)) {
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            return false;
        }
        
        boolean valid = authClient.verifyToken(token);
        if (!valid) {
            response.setStatus(HttpStatus.FORBIDDEN.value());
            return false;
        }
        
        return true;
    }
}

五、项目结构组织:怎么放代码才清晰

5.1 多模块项目结构

参考仓库的组织方式,一个完整的微服务项目可以这样分层:

bash 复制代码
spring-cloud-project/
├── api-gateway/              # API 网关
├── service-common/           # 公共模块(工具类、常量、异常定义)
├── service-api/              # Feign 客户端定义(API SDK)
├── service-auth/             # 认证服务
├── service-user/             # 用户服务
├── service-order/            # 订单服务
├── service-message/          # 消息服务(可靠消息)
├── eureka-server/            # 注册中心
├── config-server/            # 配置中心
└── pom.xml                   # 父 POM,统一管理依赖版本

5.2 单个服务的内部结构

bash 复制代码
service-user/
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com/example/user/
│   │   │       ├── UserApplication.java
│   │   │       ├── controller/       # 对外接口层
│   │   │       ├── service/          # 业务逻辑层
│   │   │       ├── mapper/           # 数据访问层
│   │   │       ├── entity/           # 实体类
│   │   │       ├── dto/              # 数据传输对象
│   │   │       ├── config/           # 配置类
│   │   │       └── feign/            # Feign 客户端(调用其他服务)
│   │   └── resources/
│   │       ├── application.yml
│   │       ├── application-dev.yml
│   │       └── mapper/               # MyBatis XML
│   └── test/
└── pom.xml

六、写在最后

微服务架构的落地不是一蹴而就的。从单体到微服务,需要考虑:

  1. 服务拆分是否合理 --- 拆得太细会增加调用复杂度,拆得太粗失去微服务的意义
  2. 数据库设计 --- 独立数据库是原则,但数据一致性成为新的挑战
  3. 分布式事务 --- 没有银弹,根据业务特点选择合适的方案
  4. 服务安全 --- 内部调用也需要认证,不能假设内网就是安全的
  5. 监控和治理 --- 服务多了之后,链路追踪、熔断降级、限流等能力必不可少

微服务不是目的,而是手段。不要为了拆分而拆分,根据团队规模和业务复杂度选择合适的架构方案,才是正确的做法。


  • 第一版书籍:《Spring Cloud微服务-全栈技术与案例解析》
  • 第二版书籍:《Spring Cloud微服务 入门 实战与进阶》
相关推荐
蝎子莱莱爱打怪5 天前
XZLL-IM干货系列 04|Netty 长连接实战:Pipeline 怎么排、心跳怎么跳、连接怎么管
后端·微服务·面试
SamDeepThinking6 天前
Java微服务练习方式
java·后端·微服务
米丘9 天前
微前端之 Web Components 完全指南
微服务·html
霸道流氓气质12 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
霸道流氓气质12 天前
Spring Boot 微服务性能优化完全指南
spring boot·微服务·性能优化
地瓜伯伯12 天前
从MESI缓存一致性协议讲透synchronized的底层
java·spring boot·spring·spring cloud·微服务·springcloud
Devin~Y12 天前
大厂 Java 面试实录:从音视频内容社区到 AI RAG 的全链路技术设计
java·spring boot·redis·spring cloud·微服务·kafka·音视频
递归尽头是星辰12 天前
AI 访问数据仓库:从直连到微服务化
数据仓库·人工智能·微服务·dataagent·ai数据治理
就改了13 天前
Windows 环境 SkyWalking 完整实操教程
windows·微服务·skywalking
至乐活着13 天前
Docker Compose多服务编排实战:从零搭建Node.js+MySQL+Redis全栈应用
docker·微服务·devops·容器编排·compose