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

上一篇文章梳理了 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微服务 入门 实战与进阶》
相关推荐
Demon1_Coder2 小时前
Day4-微服务-Seata
微服务·云原生·架构
huipeng9262 小时前
企业级微服务开发实战(三):公共模块设计与统一规范封装
java·spring boot·spring cloud·微服务·架构·系统架构·php
金融支付架构实战指南2 小时前
微服务DDD落地规范:内部抛异常、RPC外层Result封装
微服务·rpc·架构·错误码
SilentSamsara18 小时前
Python 微服务全链路:gRPC + 链路追踪 + 服务网格接入
开发语言·分布式·python·微服务·架构
小小龙学IT19 小时前
Go 后端开发实战:从单机千QPS到十万级微服务架构的演进之路
微服务·架构·golang
小江的记录本1 天前
【Spring全家桶】Spring Cloud 2023.0.x:微服务核心理论、CAP/BASE定理(附《思维导图》+《面试高频考点清单》)
java·spring boot·后端·spring·spring cloud·微服务·面试
Demon1_Coder1 天前
Day4-微服务-Seata默认事务
java·数据库·微服务
huipeng9261 天前
企业级微服务开发实战(二):微服务基础设施搭建与中间件部署
java·redis·mysql·spring cloud·微服务·nacos·rabbitmq
Jabes.yang1 天前
Java电商订单系统面试全流程解析:接口设计、数据库、微服务与分布式事务实战
java·微服务·mybatis·分布式事务·电商·订单系统·接口设计