Java框架 SpringCloud 快速入门: 服务拆分案例 Demo

概述

服务拆分最容易犯的错,是把单体时代的写法原封不动搬进微服务:查订单时顺手去查用户表,于是每个服务都写了一遍别人的业务逻辑。这篇用 cloud-demo 工程把拆分原则和落地的目录、SQL、配置、代码一次讲清。

纲要

  • 拆分的反例:单体思维下"订单模块里查用户表"为什么会变成重复开发
  • 三条原则
    • 单一职责:不同微服务不要重复开发相同业务
    • 数据独立:不要访问其它微服务的数据库
    • 面向服务:把自己的业务暴露为 Restful 接口供其它服务调用
  • 工程结构:cloud-demo 父工程(packaging=pom)+ order-service(8080)+ user-service(8081)
  • 数据层:cloud_user / cloud_order 两个独立 database,tb_user / tb_order 建表与初始数据
  • 代码:User、Order 实体,UserController、OrderController、Service、Mapper、启动类
  • 配置:两个 application.yml 的端口与数据源差异
  • 启动验证:浏览器 / curl 访问两个接口的实际返回
  • 踩坑清单:共用库、端口冲突、实体类各写一份、type-aliases-package 抄串包

从单体到微服务,最容易带过来的坏习惯

需求很常见:查询订单时,把订单关联的用户信息、商品信息一起查出来。

单体架构里没人觉得这有问题,一个方法里全干完:

java 复制代码
package cn.itcast.order.service;

import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import cn.itcast.order.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * 反例:这是单体时代的写法,不是微服务写法。
 * 订单服务直接查了用户表,等于把用户模块的业务又实现了一遍。
 */
@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private UserMapper userMapper;

    public Order queryOrderById(Long orderId) {
        // 1.查询订单
        Order order = orderMapper.findById(orderId);
        // 2.拿着订单里的 userId 直接查用户表 ------ 问题就出在这一步
        User user = userMapper.findById(order.getUserId());
        order.setUser(user);
        // 3.返回
        return order;
    }
}

逻辑上跑得通,但它同时违反了微服务拆分的两条底线:

问题 表现 后果
业务重复开发 用户模块有查询用户的逻辑,订单模块又写了一遍 同一段逻辑散落在多处,改一次要改 N 个服务
数据不独立 订单服务直接读 tb_user 用户表结构一改,订单服务跟着崩;用户库迁走,订单服务直接瘫痪

微服务拆分的目的就是单一职责:一个服务只做与自己相关的事。订单模块凭什么实现用户查询?如果每个服务都是"用到什么就查什么",那拆分的唯一结果就是多了一堆网络调用,耦合一点没少。

判断标准很简单:如果在拆分过程中出现了重复业务,说明边界划错了,要么服务切分不合理,要么该下沉的能力没有下沉。

拆分的三条原则

原则 要求 落地手段
单一职责 不同微服务不要重复开发相同业务 按业务模块切分,一个服务只对自己的领域负责
数据独立 不要访问其它微服务的数据库 每个服务拥有独立 database,物理上就断掉直连的念想
面向服务 将业务暴露为接口供其它微服务调用 提供 Restful 接口,别人发 HTTP 请求来拿数据

数据独立这条最有意思,它是用物理隔离来强制逻辑解耦。用户库里只有用户信息,订单库里只有订单信息,订单业务想查用户?库里没有这张表,从根源上就杜绝了耦合。

那订单确实需要用户信息怎么办?用户服务把查询能力暴露成接口,订单服务发 HTTP 请求去拿。这就是下一篇要讲的远程调用。

拆分前后对比

#mermaid-svg-KTpCb0rRmdho7lYN{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-KTpCb0rRmdho7lYN .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-KTpCb0rRmdho7lYN .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-KTpCb0rRmdho7lYN .error-icon{fill:#552222;}#mermaid-svg-KTpCb0rRmdho7lYN .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-KTpCb0rRmdho7lYN .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-KTpCb0rRmdho7lYN .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-KTpCb0rRmdho7lYN .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-KTpCb0rRmdho7lYN .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-KTpCb0rRmdho7lYN .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-KTpCb0rRmdho7lYN .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-KTpCb0rRmdho7lYN .marker{fill:#333333;stroke:#333333;}#mermaid-svg-KTpCb0rRmdho7lYN .marker.cross{stroke:#333333;}#mermaid-svg-KTpCb0rRmdho7lYN svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-KTpCb0rRmdho7lYN p{margin:0;}#mermaid-svg-KTpCb0rRmdho7lYN .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-KTpCb0rRmdho7lYN .cluster-label text{fill:#333;}#mermaid-svg-KTpCb0rRmdho7lYN .cluster-label span{color:#333;}#mermaid-svg-KTpCb0rRmdho7lYN .cluster-label span p{background-color:transparent;}#mermaid-svg-KTpCb0rRmdho7lYN .label text,#mermaid-svg-KTpCb0rRmdho7lYN span{fill:#333;color:#333;}#mermaid-svg-KTpCb0rRmdho7lYN .node rect,#mermaid-svg-KTpCb0rRmdho7lYN .node circle,#mermaid-svg-KTpCb0rRmdho7lYN .node ellipse,#mermaid-svg-KTpCb0rRmdho7lYN .node polygon,#mermaid-svg-KTpCb0rRmdho7lYN .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-KTpCb0rRmdho7lYN .rough-node .label text,#mermaid-svg-KTpCb0rRmdho7lYN .node .label text,#mermaid-svg-KTpCb0rRmdho7lYN .image-shape .label,#mermaid-svg-KTpCb0rRmdho7lYN .icon-shape .label{text-anchor:middle;}#mermaid-svg-KTpCb0rRmdho7lYN .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-KTpCb0rRmdho7lYN .rough-node .label,#mermaid-svg-KTpCb0rRmdho7lYN .node .label,#mermaid-svg-KTpCb0rRmdho7lYN .image-shape .label,#mermaid-svg-KTpCb0rRmdho7lYN .icon-shape .label{text-align:center;}#mermaid-svg-KTpCb0rRmdho7lYN .node.clickable{cursor:pointer;}#mermaid-svg-KTpCb0rRmdho7lYN .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-KTpCb0rRmdho7lYN .arrowheadPath{fill:#333333;}#mermaid-svg-KTpCb0rRmdho7lYN .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-KTpCb0rRmdho7lYN .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-KTpCb0rRmdho7lYN .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KTpCb0rRmdho7lYN .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-KTpCb0rRmdho7lYN .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KTpCb0rRmdho7lYN .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-KTpCb0rRmdho7lYN .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-KTpCb0rRmdho7lYN .cluster text{fill:#333;}#mermaid-svg-KTpCb0rRmdho7lYN .cluster span{color:#333;}#mermaid-svg-KTpCb0rRmdho7lYN div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-KTpCb0rRmdho7lYN .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-KTpCb0rRmdho7lYN rect.text{fill:none;stroke-width:0;}#mermaid-svg-KTpCb0rRmdho7lYN .icon-shape,#mermaid-svg-KTpCb0rRmdho7lYN .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KTpCb0rRmdho7lYN .icon-shape p,#mermaid-svg-KTpCb0rRmdho7lYN .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-KTpCb0rRmdho7lYN .icon-shape .label rect,#mermaid-svg-KTpCb0rRmdho7lYN .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KTpCb0rRmdho7lYN .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-KTpCb0rRmdho7lYN .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-KTpCb0rRmdho7lYN :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 拆分前 单体应用
订单模块
单一数据库
用户模块
商品模块

单体时代所有模块共享一个库,跨模块查数据就是一条 JOIN,方便但也意味着边界完全不存在。
#mermaid-svg-EB0z3f0j6qxbNrO7{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-EB0z3f0j6qxbNrO7 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-EB0z3f0j6qxbNrO7 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-EB0z3f0j6qxbNrO7 .error-icon{fill:#552222;}#mermaid-svg-EB0z3f0j6qxbNrO7 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-EB0z3f0j6qxbNrO7 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-EB0z3f0j6qxbNrO7 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-EB0z3f0j6qxbNrO7 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-EB0z3f0j6qxbNrO7 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-EB0z3f0j6qxbNrO7 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-EB0z3f0j6qxbNrO7 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-EB0z3f0j6qxbNrO7 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-EB0z3f0j6qxbNrO7 .marker.cross{stroke:#333333;}#mermaid-svg-EB0z3f0j6qxbNrO7 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-EB0z3f0j6qxbNrO7 p{margin:0;}#mermaid-svg-EB0z3f0j6qxbNrO7 .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-EB0z3f0j6qxbNrO7 .cluster-label text{fill:#333;}#mermaid-svg-EB0z3f0j6qxbNrO7 .cluster-label span{color:#333;}#mermaid-svg-EB0z3f0j6qxbNrO7 .cluster-label span p{background-color:transparent;}#mermaid-svg-EB0z3f0j6qxbNrO7 .label text,#mermaid-svg-EB0z3f0j6qxbNrO7 span{fill:#333;color:#333;}#mermaid-svg-EB0z3f0j6qxbNrO7 .node rect,#mermaid-svg-EB0z3f0j6qxbNrO7 .node circle,#mermaid-svg-EB0z3f0j6qxbNrO7 .node ellipse,#mermaid-svg-EB0z3f0j6qxbNrO7 .node polygon,#mermaid-svg-EB0z3f0j6qxbNrO7 .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-EB0z3f0j6qxbNrO7 .rough-node .label text,#mermaid-svg-EB0z3f0j6qxbNrO7 .node .label text,#mermaid-svg-EB0z3f0j6qxbNrO7 .image-shape .label,#mermaid-svg-EB0z3f0j6qxbNrO7 .icon-shape .label{text-anchor:middle;}#mermaid-svg-EB0z3f0j6qxbNrO7 .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-EB0z3f0j6qxbNrO7 .rough-node .label,#mermaid-svg-EB0z3f0j6qxbNrO7 .node .label,#mermaid-svg-EB0z3f0j6qxbNrO7 .image-shape .label,#mermaid-svg-EB0z3f0j6qxbNrO7 .icon-shape .label{text-align:center;}#mermaid-svg-EB0z3f0j6qxbNrO7 .node.clickable{cursor:pointer;}#mermaid-svg-EB0z3f0j6qxbNrO7 .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-EB0z3f0j6qxbNrO7 .arrowheadPath{fill:#333333;}#mermaid-svg-EB0z3f0j6qxbNrO7 .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-EB0z3f0j6qxbNrO7 .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-EB0z3f0j6qxbNrO7 .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EB0z3f0j6qxbNrO7 .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-EB0z3f0j6qxbNrO7 .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EB0z3f0j6qxbNrO7 .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-EB0z3f0j6qxbNrO7 .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-EB0z3f0j6qxbNrO7 .cluster text{fill:#333;}#mermaid-svg-EB0z3f0j6qxbNrO7 .cluster span{color:#333;}#mermaid-svg-EB0z3f0j6qxbNrO7 div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-EB0z3f0j6qxbNrO7 .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-EB0z3f0j6qxbNrO7 rect.text{fill:none;stroke-width:0;}#mermaid-svg-EB0z3f0j6qxbNrO7 .icon-shape,#mermaid-svg-EB0z3f0j6qxbNrO7 .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-EB0z3f0j6qxbNrO7 .icon-shape p,#mermaid-svg-EB0z3f0j6qxbNrO7 .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-EB0z3f0j6qxbNrO7 .icon-shape .label rect,#mermaid-svg-EB0z3f0j6qxbNrO7 .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-EB0z3f0j6qxbNrO7 .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-EB0z3f0j6qxbNrO7 .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-EB0z3f0j6qxbNrO7 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 拆分后 微服务
HTTP GET /user/id
order-service 8080
cloud_order
user-service 8081
cloud_user

拆完之后,订单服务只连 cloud_order,用户服务只连 cloud_user。订单里的 user 字段不再来自 SQL,而是来自一次对 user-service 的 HTTP 调用。

工程结构

cloud-demo 是父工程,只负责依赖版本管理,不写业务代码;两个子模块各自是一个可以独立启动的 SpringBoot 应用。

text 复制代码
cloud-demo/                          # 父工程,packaging = pom,管理依赖版本
├── pom.xml
├── order-service/                   # 订单微服务,端口 8080,库 cloud_order
│   ├── pom.xml
│   └── src/main/
│       ├── java/cn/itcast/order/
│       │   ├── OrderApplication.java       # 启动类 + @MapperScan
│       │   ├── web/OrderController.java    # GET /order/{orderId}
│       │   ├── service/OrderService.java
│       │   ├── mapper/OrderMapper.java
│       │   └── pojo/
│       │       ├── Order.java
│       │       └── User.java               # 为远程调用预留,本篇尚未填充
│       └── resources/application.yml
└── user-service/                    # 用户微服务,端口 8081,库 cloud_user
    ├── pom.xml
    └── src/main/
        ├── java/cn/itcast/user/
        │   ├── UserApplication.java
        │   ├── web/UserController.java     # GET /user/{id}
        │   ├── service/UserService.java
        │   ├── mapper/UserMapper.java
        │   └── pojo/User.java
        └── resources/application.yml

两个服务的业务已经解耦:订单服务只做订单,用户服务只做用户。注意 order-service 里也有一份 User------这是为了下一步接收远程调用结果预留的,它也正是"重复代码"的引子。

父工程 pom.xml

父工程继承 spring-boot-starter-parent,用 <dependencyManagement> 锁版本,子模块引依赖时不用写版本号。

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.itcast.demo</groupId>
    <artifactId>cloud-demo</artifactId>
    <version>1.0</version>

    <!-- 父工程聚合两个微服务 -->
    <modules>
        <module>user-service</module>
        <module>order-service</module>
    </modules>

    <!-- 关键:父工程只做依赖管理,不产 jar -->
    <packaging>pom</packaging>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.9.RELEASE</version>
        <relativePath/>
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Hoxton.SR10</spring-cloud.version>
        <mysql.version>5.1.47</mysql.version>
        <mybatis.version>2.1.1</mybatis.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <!-- springCloud -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
            <!-- mysql驱动 -->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>${mysql.version}</version>
            </dependency>
            <!-- mybatis -->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>${mybatis.version}</version>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>
</project>

版本组合是 SpringBoot 2.3.9.RELEASE + SpringCloud Hoxton.SR10。这两个必须配套,SpringBoot 2.3.x 对应的就是 Hoxton,随手把 SpringCloud 升到 2020.x 会直接起不来。

子模块的 pom 只声明 parent 和自己的 artifactId,依赖全靠父工程管理:

xml 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud-demo</artifactId>
        <groupId>cn.itcast.demo</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>user-service</artifactId>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
    </dependencies>

    <build>
        <finalName>app</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

数据库:两个独立的 database

生产上两个服务会部署在不同的数据库实例上。本地演示没有那么多实例,就用两个 database 模拟------效果一样,跨库依然没法做关联查询。

cloud-user.sql:

sql 复制代码
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '收件人',
  `address` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '地址',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `username`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 109 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

INSERT INTO `tb_user` VALUES (1, '柳岩', '湖南省衡阳市');
INSERT INTO `tb_user` VALUES (2, '文二狗', '陕西省西安市');
INSERT INTO `tb_user` VALUES (3, '华沉鱼', '湖北省十堰市');
INSERT INTO `tb_user` VALUES (4, '张必沉', '天津市');
INSERT INTO `tb_user` VALUES (5, '郑爽爽', '辽宁省沈阳市大东区');
INSERT INTO `tb_user` VALUES (6, '范兵兵', '山东省青岛市');

SET FOREIGN_KEY_CHECKS = 1;

cloud-order.sql:

sql 复制代码
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

DROP TABLE IF EXISTS `tb_order`;
CREATE TABLE `tb_order`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单id',
  `user_id` bigint(20) NOT NULL COMMENT '用户id',
  `name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '商品名称',
  `price` bigint(20) NOT NULL COMMENT '商品价格',
  `num` int(10) NULL DEFAULT 0 COMMENT '商品数量',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `username`(`name`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 109 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

INSERT INTO `tb_order` VALUES (101, 1, 'Apple 苹果 iPhone 12 ', 699900, 1);
INSERT INTO `tb_order` VALUES (102, 2, '雅迪 yadea 新国标电动车', 209900, 1);
INSERT INTO `tb_order` VALUES (103, 3, '骆驼(CAMEL)休闲运动鞋女', 43900, 1);
INSERT INTO `tb_order` VALUES (104, 4, '小米10 双模5G 骁龙865', 359900, 1);
INSERT INTO `tb_order` VALUES (105, 5, 'OPPO Reno3 Pro 双模5G 视频双防抖', 299900, 1);
INSERT INTO `tb_order` VALUES (106, 6, '美的(Midea) 新能效 冷静星II ', 544900, 1);
INSERT INTO `tb_order` VALUES (107, 2, '西昊/SIHOO 人体工学电脑椅子', 79900, 1);
INSERT INTO `tb_order` VALUES (108, 3, '梵班(FAMDBANN)休闲男鞋', 31900, 1);

SET FOREIGN_KEY_CHECKS = 1;

订单表靠 user_id 与用户表产生业务关联,但两张表在两个库里,没有任何外键,也做不了 JOIN。这不是疏忽,正是数据独立要的结果。

导入:

bash 复制代码
mysql -uroot -p cloud_user < cloud-user.sql
mysql -uroot -p cloud_order < cloud-order.sql

用 Navicat 的话,先建 cloud_user、cloud_order 两个库,各自选中库后右键运行 SQL 文件即可。

数据源与端口配置

两个服务各有一份 application.yml,差异只在端口和库名。

user-service/src/main/resources/application.yml:

yaml 复制代码
server:
  port: 8081
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/cloud_user?useSSL=false
    username: root
    password: 123
    driver-class-name: com.mysql.jdbc.Driver
mybatis:
  type-aliases-package: cn.itcast.user.pojo
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    cn.itcast: debug
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS

order-service/src/main/resources/application.yml:

yaml 复制代码
server:
  port: 8080
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
    username: root
    password: 123
    driver-class-name: com.mysql.jdbc.Driver
mybatis:
  type-aliases-package: cn.itcast.order.pojo
  configuration:
    map-underscore-to-camel-case: true
logging:
  level:
    cn.itcast: debug
  pattern:
    dateformat: MM-dd HH:mm:ss:SSS

map-underscore-to-camel-case: true 是必须的,否则 user_id 映射不到 Order.userId,字段会是 null。

driver-class-name: com.mysql.jdbc.Driver 是 MySQL 5.x 驱动的老写法,配 8.x 驱动要换成 com.mysql.cj.jdbc.Driver,同时 URL 加 serverTimezone。

用户服务代码

启动类:

java 复制代码
package cn.itcast.user;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@MapperScan("cn.itcast.user.mapper")
@SpringBootApplication
public class UserApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserApplication.class, args);
    }
}

实体类:

java 复制代码
package cn.itcast.user.pojo;

import lombok.Data;

@Data
public class User {
    private Long id;
    private String username;
    private String address;
}

Controller,暴露 GET /user/{id}:

java 复制代码
package cn.itcast.user.web;

import cn.itcast.user.pojo.User;
import cn.itcast.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * 路径: /user/110
     *
     * @param id 用户id
     * @return 用户
     */
    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id) {
        return userService.queryById(id);
    }
}

Service 与 Mapper:

java 复制代码
package cn.itcast.user.service;

import cn.itcast.user.mapper.UserMapper;
import cn.itcast.user.pojo.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    public User queryById(Long id) {
        return userMapper.findById(id);
    }
}
java 复制代码
package cn.itcast.user.mapper;

import cn.itcast.user.pojo.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;

public interface UserMapper {

    @Select("select * from tb_user where id = #{id}")
    User findById(@Param("id") Long id);
}

订单服务代码

启动类:

java 复制代码
package cn.itcast.order;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

Order 实体里的 user 字段是为下一步远程调用准备的,本篇还没有数据来源,查询结果是 null:

java 复制代码
package cn.itcast.order.pojo;

import lombok.Data;

@Data
public class Order {
    private Long id;
    private Long price;
    private String name;
    private Integer num;
    private Long userId;
    private User user;
}

Controller,暴露 GET /order/{orderId}:

java 复制代码
package cn.itcast.order.web;

import cn.itcast.order.pojo.Order;
import cn.itcast.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("order")
public class OrderController {

    @Autowired
    private OrderService orderService;

    @GetMapping("{orderId}")
    public Order queryOrderByUserId(@PathVariable("orderId") Long orderId) {
        // 根据id查询订单并返回
        return orderService.queryOrderById(orderId);
    }
}

Service 与 Mapper:

java 复制代码
package cn.itcast.order.service;

import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    public Order queryOrderById(Long orderId) {
        // 1.查询订单(用户信息的填充留给远程调用那一节)
        Order order = orderMapper.findById(orderId);
        // 2.返回
        return order;
    }
}
java 复制代码
package cn.itcast.order.mapper;

import cn.itcast.order.pojo.Order;
import org.apache.ibatis.annotations.Select;

public interface OrderMapper {

    @Select("select * from tb_order where id = #{id}")
    Order findById(Long id);
}

启动与验证

先启动 UserApplication(8081),再启动 OrderApplication(8080)。

bash 复制代码
curl http://localhost:8081/user/1
# {"id":1,"username":"柳岩","address":"湖南省衡阳市"}

curl http://localhost:8081/user/2
# {"id":2,"username":"文二狗","address":"陕西省西安市"}

curl http://localhost:8080/order/101
# {"id":101,"price":699900,"name":"Apple 苹果 iPhone 12 ","num":1,"userId":1,"user":null}

订单 id 从 101 开始,用户 id 从 1 开始。第三步的 user 是 null------订单服务此刻还不知道怎么去拿用户信息,这正是下一篇远程调用要补上的空缺。

API 速览

服务 端口 方法 路径 返回 说明
user-service 8081 GET /user/{id} User 根据 id 查用户,查 cloud_user.tb_user
order-service 8080 GET /order/{orderId} Order 根据 id 查订单,查 cloud_order.tb_order,user 字段暂为 null

几个真实的坑

两个服务连同一个库,拆分就白做了。 最常见也最隐蔽的退步:嫌建库麻烦,两个服务的 spring.datasource.url 都指向 cloud_order,表全塞一起。跑得起来,但订单服务随时能 select * from tb_user,数据独立这条原则形同虚设。判断方法很简单------把另一个服务的表删掉,你的服务还能不能启动。能,说明独立;不能,说明还在耦合。

端口冲突。 两个 SpringBoot 应用默认都是 8080,第二个启动时报 Web server failed to start. Port 8080 was already in use.。本地测试要么像 demo 这样写死 8080/8081,要么用 --server.port=8082 覆盖。上了容器之后这个坑会变样子,容器内的端口可以和宿主机不一样。

实体类各写一份。 User 在 user-service 和 order-service 里各有一份,字段一模一样。短期看是"重复代码",但别急着抽公共模块------一旦抽成 common-pojo 让两个服务依赖,改一个字段就得同时发版两个服务,又回到了紧耦合。这种冗余在微服务里是合理的代价。

type-aliases-package 抄串包。 demo 里 order-service 的 application.yml 原文写的是 cn.itcast.user.pojo,从用户服务复制过来忘了改。因为 Mapper 用的是 @Select 注解、返回类型从方法签名推断,所以这个错误不会立刻暴露;一旦改用 XML 里写 resultType="Order",就会报类型找不到。

跨库不能 JOIN。 习惯了 select * from tb_order o join tb_user u on o.user_id = u.id 的同学到这里会卡住。拆分后必须改成两次查询:一次查订单拿到 userId,一次调用用户服务接口拿到用户信息,在应用层组装。这是拆分换来的代价,也是远程调用存在的理由。

官方文档

总结

  • 拆分不是把代码复制成两份,而是让每个服务只负责自己的领域。出现重复业务,说明边界没划对。
  • 数据独立是硬约束:两个服务两个库,cloud_user 和 cloud_order 之间没有任何 JOIN 的可能。
  • 拿不到数据不是死路,把业务暴露成 Restful 接口让别人来调------GET /user/{id} 就是用户服务对外提供的能力。
  • order-service 里那个为 null 的 user 字段,是下一篇远程调用要填的坑。
相关推荐
小小龙学IT1 小时前
Python SQLAlchemy 2.0 深度解析:从 Core 到 ORM 的数据访问双引擎
开发语言·数据库·python
2601_962885721 小时前
如何用 Python 计算 ROC 变动率指标做动量分析?
开发语言·python·算法
DongQiShanRen1 小时前
裁决台账双向互校(上):名册与实物的第一道对账
java·linux·运维·数据库·人工智能·自然语言处理·数据挖掘
潼心1412o2 小时前
C++初阶(长期更新)第9讲:string(上)
开发语言·c++
朝朝辞暮i2 小时前
C++ 第 39 章: 阶段性总复习——类、对象、构造、继承、this、指针与智能指针
开发语言·c++·算法·ros2
cpolar技术支持2 小时前
Spring Boot 接口本地正常,异地前端却报跨域?用 cpolar 跑通 CORS 预检与白名单
java·springboot·cpolar·前后端分离·cors
wuminyu2 小时前
Virtual Thread重投递至ForkJoinPool任务队列过程解析
java·linux·c语言·jvm·c++
小蒜学长2 小时前
在线保险服务与管理平台的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端·服务平台·在线保险
掉进电商坑三年没爬出来的东叔2 小时前
青龙面板进阶:一个面板聚合多平台签到,所有积分自动领
java·开发语言