概述
服务拆分最容易犯的错,是把单体时代的写法原封不动搬进微服务:查订单时顺手去查用户表,于是每个服务都写了一遍别人的业务逻辑。这篇用 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字段,是下一篇远程调用要填的坑。