【微服务】(2) 环境和工程搭建
先安装好 JDK 和 MySQL。
一、案例介绍
以电商平台为例,有很多业务:用户、订单、秒杀、搜索……该服务巨大,微服务是更好的选择。
1、服务拆分原则
并不是越细越好,越细带来的管理困难也就越高。
- 单一职责,效率更高。
- 服务自治,互相影响更小。每个服务都能独立开发、测试、构建、部署、运行。
- 单向依赖,禁止循环、双向依赖。无法避免时,可用其他技术如消息队列解耦。
2、服务拆分示例
对于订单列表:
① 单一职责:拆分为订单服务、商品服务(实际项目划分更细)。
② 服务自治:每个服务创建独立数据库。
订单表:
-- 建库
create database if not exists cloud_order charset utf8mb4;
-- 订单表
DROP TABLE IF EXISTS order_detail;
CREATE TABLE order_detail (`id` INT NOT NULL AUTO_INCREMENT COMMENT '订单id',`user_id` BIGINT ( 20 ) NOT NULL COMMENT '⽤⼾ID',`product_id` BIGINT ( 20 ) NULL COMMENT '产品id',`num` INT ( 10 ) NULL DEFAULT 0 COMMENT '下单数量',`price` BIGINT ( 20 ) NOT NULL COMMENT '实付款',`delete_flag` TINYINT ( 4 ) NULL DEFAULT 0,`create_time` DATETIME DEFAULT now(),`update_time` DATETIME DEFAULT now(),
PRIMARY KEY ( id )) ENGINE = INNODB DEFAULT CHARACTER
SET = utf8mb4 COMMENT = '订单表';
-- 数据初始化
insert into order_detail (user_id,product_id,num,price)
values
(2001, 1001,1,99), (2002, 1002,1,30), (2001, 1003,1,40),
(2003, 1004,3,58), (2004, 1005,7,85), (2005, 1006,7,94);
商品表:
create database if not exists cloud_product charset utf8mb4;
-- 产品表
DROP TABLE IF EXISTS product_detail;
CREATE TABLE product_detail (`id` INT NOT NULL AUTO_INCREMENT COMMENT '产品id',`product_name` varchar ( 128 ) NULL COMMENT '产品名称',`product_price` BIGINT ( 20 ) NOT NULL COMMENT '产品价格',`state` TINYINT ( 4 ) NULL DEFAULT 0 COMMENT '产品状态 0-有效 1-下架',`create_time` DATETIME DEFAULT now(),`update_time` DATETIME DEFAULT now(),
PRIMARY KEY ( id )) ENGINE = INNODB DEFAULT CHARACTER
SET = utf8mb4 COMMENT = '产品表';
-- 数据初始化
insert into product_detail (id, product_name,product_price,state)
values
(1001,"T恤", 101, 0), (1002, "短袖",30, 0), (1003, "短裤",44, 0),
(1004, "卫⾐",58, 0), (1005, "⻢甲",98, 0),(1006,"⽻绒服", 101, 0),
(1007, "冲锋⾐",30, 0), (1008, "袜⼦",44, 0), (1009, "鞋⼦",58, 0),
(10010, "⽑⾐",98, 0)
可能会出现错误:
这是客户端编码跟表编码不一致的锅,在建库前加一句:
SET NAMES utf8mb4;
二、工程搭建
1、构建父子工程
(1)构建父项目
- 父工程负责管理子模块依赖,不负责编译生成可执行文件,因此只需保留 pom.xml 文件。
创建空 maven 项目,删除所有源代码,只保留 pom.xml 文件。
完善 pom 文件:
- 父工程的打包方式是 pom 而不是 jar,因此需要手动声明以 pom 方式打包。
- 父工程依赖 Sring Boot 父工程,需要 <parent> 声明父工程。
- <properties> 管理依赖的版本号,注意 spring cloud 版本与 spring boot 版本匹配。
- <dependencies> jar 包直接放到项目中,子项目直接继承。
- <dependencyManagement> 声明依赖,没有引入 jar 包,子项目想引入 jar 需要显示声明。若子项目指定版本号,按指定版本引入;若子项目没有指定版本,按父工程管理的版本号来。
<!-- 声明当前工程包含哪些子工程,创建子工程时会自动添加到当前工程中--><modules><module>order-service</module><module>product-service</module></modules><!-- 声明以 pom 方式打包 --><packaging>pom</packaging><!-- 继承官方的 Spring Boot 父工程,引入 Spring Boot 常用依赖无需手动指定版本号--><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.1.6</version><relativePath/></parent><!-- 声明管理依赖的版本号--><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><java.version>17</java.version><mybatis.version>3.0.3</mybatis.version><mysql.version>8.0.33</mysql.version><spring-cloud.version>2022.0.3</spring-cloud.version></properties><!-- 引入依赖,子工程继承父工程的依赖,无需重复引入--><dependencies><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency></dependencies><!-- 声明依赖,子工程引入依赖时,可无需指定版本号,由父工程统一管理--><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>${mybatis.version}</version></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId><version>${mysql.version}</version></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter-test</artifactId><version>${mybatis.version}</version><scope>test</scope></dependency></dependencies></dependencyManagement>
因为<dependencyManagement> 只是声明,没有真正引入 jar 包,所以爆红了很正常,不用管。上面的依赖没有爆红,是因为我之前下载到本地仓库过:
(2)构建子项目:订单、商品服务
创建新模块:
引入项目依赖和项目构建插件,这里就不用指定版本号,直接继承父工程的依赖版本号管理:
<dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId></dependency><!--mybatis--><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
2、完善子项目服务
- 启动类
package com.pygymi.order;import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplication
public class OrderServiceApplication {public static void main(String[] args) {SpringApplication.run(OrderServiceApplication.class, args);}
}
- 配置文件
#应用端口号
server:port: 8080
# 数据库连接配置
spring:datasource:url: jdbc:mysql://127.0.0.1:3306/cloud_order?characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=trueusername: rootpassword: "123456"driver-class-name: com.mysql.jdbc.Driver
# mybatis配置
mybatis:configuration:log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印日志map-underscore-to-camel-case: true # 配置驼峰⾃动转换
- 完善订单服务业务代码:
实体类:
@Data
public class OrderInfo {private Integer id;private Integer userId;private Integer productId;private Integer num;private Integer price;private Integer deleteFlag;private Date createTime;private Date updateTime;
}
controller:
@RestController
@RequestMapping("/order")
public class OrderController {@Resource(name = "orderServiceImpl")private OrderService orderService;@GetMapping("/{orderId}")public OrderInfo getOrder(@PathVariable("orderId") String orderId) {return orderService.getOrder(orderId);}
}
service:
public class OrderServiceImpl implements OrderService {@Resourceprivate OrderMapper orderMapper;@Overridepublic OrderInfo getOrder(String orderId) {OrderInfo orderInfo = orderMapper.selectOrderById(orderId);return orderInfo;}
}
mapper:
public interface OrderMapper {@Select("SELECT * FROM order_info WHERE id = #{orderId}")OrderInfo selectOrderById(String orderId);
}
- 完善商品服务业务代码:同理。
- 接口测试:
订单查询:127.0.0.1:8080/order/1
商品查询:127.0.0.1:9090/product/1001
3、远程调用
查询订单信息时,需要查询商品的详细信息,解决办法是:订单服务通过 HTTP 访问商品服务,获取到商品详情放到订单返回结果中。
实现方式使用 Spring 提供的 RestTemplate。
定义 RestTemplate,因为五大注入 bean 的注解修饰的是类,RestTemplate 是别人写的类我们无法修改,所以使用方法注入 @Bean:
@Configuration
public class BeanConfig {@Beanpublic RestTemplate restTemplate() {return new RestTemplate();}
}
在订单服务中,通过 HTTP 访问商品服务:
@Service
public class OrderServiceImpl implements OrderService {@Resourceprivate OrderMapper orderMapper;@Resourceprivate RestTemplate restTemplate;@Overridepublic OrderInfo getOrder(String orderId) {OrderInfo orderInfo = orderMapper.selectOrderById(orderId);// 构造访问商品服务的 urlString url = "http://127.0.0.1:9090/product/" + orderInfo.getProductId();// 使用 restTemplate 访问商品服务ProductInfo productInfo = restTemplate.getForObject(url, ProductInfo.class);// 把商品详情设置到 orderInfo 中orderInfo.setProductInfo(productInfo);return orderInfo;}
}
测试:127.0.0.1:8080/order/1
三、RestTemplate
1、REST
REST(表现层资源状态转移)是一种设计风格,指网络资源以某种表现形式进行状态转移。
- 资源:网络上的所有数据,如图像、视频、文本等。
- 表现层:资源的表现形式,如 jpg、txt、json 等。
- 状态转移:通过网络访问资源,对资源进行修改(增删改查),都会引起资源状态转移。
2、RESTful API
满足 REST 设计风格的接口都叫做 RESTful API。其特性:
- 统一接口:对资源的操作,对应 HTTP 协议提供的 GET、POST、PUT、DELETE。
如:GET /blog/{blogId}:查询博客
使用 REST 风格实现的接口,我们只能从 URL 定位其资源,并不能直接知晓其是什么操作,想要知道还需要用抓包工具查看请求,得知 GET、POST……
3、RestTemplate
就是 Spring 实现的,强制使用 RESTsul 风格的 HTTP 调用模板。我们只需要提供资源地址和参数类型。
4、RESTful 风格缺点
- 想知道该接口对资源执行了什么操作,还要先抓包,不方便团队理解和交流。
- 一些旧浏览器对 GET、POST 以外的请求支持不太友好。
- 复杂的业务强行使用 RESTful 风格反而增加开发难度,因为不能仅仅使用增删改查实现。
四、项目存在的问题
- 远程调用时,被调用方的 IP 是写死的,IP 容易写错,也容易在源代码基础上修改。我们希望不依赖提供方的 IP。
- 所有服务都可以调用该接口,具有风险。
- 多机部署,没有实现压力分摊。
…… 后续继续学习 spring cloud 的各种组件,来解决这些问题。