文章目录
之前我们说过,微服务拆分时粒度要小,这其实是拆分的目标。具体可以从两个角度来分析:
- 高内聚:每个微服务的职责要尽量单一,包含的业务相互关联度高、完整度高。
- 低耦合:每个微服务的功能要相对独立,尽量减少对其它微服务的依赖,或者依赖接口的稳定性要强。
高内聚首先是单一职责,但不能说一个微服务就一个接口,而是要保证微服务内部业务的完整性为前提。目标是当我们要修改某个业务时,最好就只修改当前微服务,这样变更的成本更低。
一旦微服务做到了高内聚,那么服务之间的耦合度自然就降低了。
当然,微服务之间不可避免的会有或多或少的业务交互,比如下单时需要查询商品数据。这个时候我们不能在订单服务直接查询商品数据库,否则就导致了数据耦合。而应该由商品服务对应暴露接口,并且一定要保证微服务对外接口的稳定性(即:尽量保证接口外观不变)。虽然出现了服务间调用,但此时无论你如何在商品服务做内部修改,都不会影响到订单微服务,服务间的耦合度就降低了。
明确了拆分目标,接下来就是拆分方式了。我们在做服务拆分时一般有两种方式:
- 纵向拆分
- 横向拆分
所谓纵向拆分,就是按照项目的功能模块来拆分。例如黑马商城中,就有用户管理功能、订单管理功能、购物车功能、商品管理功能、支付功能等。那么按照功能模块将他们拆分为一个个服务,就属于纵向拆分。这种拆分模式可以尽可能提高服务的内聚性。
而横向拆分,是看各个功能模块之间有没有公共的业务部分,如果有将其抽取出来作为通用服务。例如用户登录是需要发送消息通知,记录风控数据,下单时也要发送短信,记录风控数据。因此消息发送、风控数据记录就是通用的业务功能,因此可以将他们分别抽取为公共服务:消息中心服务、风控管理服务。这样可以提高业务的复用性,避免重复开发。同时通用业务一般接口稳定性较强,也不会使服务之间过分耦合。
当然,由于黑马商城并不是一个完整的项目,其中的短信发送、风控管理并没有实现,这里就不再考虑了。而其它的业务按照纵向拆分,可以分为以下几个微服务:
- 用户服务
- 商品服务
- 订单服务
- 购物车服务
- 支付服务
创建新模块
拷贝yml配置文件
修改配置文件的信息
application.yaml
application-dev.yaml
application-local.yaml
因为我的mysql是部署在docker里面的, 所以dev环境与虚拟机里部署的mysql相挂载
修改pom.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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>hmall</artifactId>
<groupId>com.heima</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>user-service</artifactId>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<!--common-->
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-common</artifactId>
<version>1.0.0</version>
</dependency>
<!--feign模块-->
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-api</artifactId>
<version>1.0.0</version>
</dependency>
<!--web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--数据库-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--openFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--负载均衡器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!--OK http 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>9</source>
<target>9</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
记得修改SpringBoot
的版面本号为2.xxx
启动入口
package com.hmall.cart;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
public class CartApplication {
public static void main(String[] args) {
SpringApplication.run(CartApplication.class, args);
}
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
} // 这个是远程调用需要的工具 后面就不用这个了
}
记得加上mapper扫包, 如果需要远程调用别的模块的方法, 在此加上RestTemplate
在这里启动服务, 而不是跑到每个启动入口去启动, 这里还要修改启动端口, 具体如下
拷贝相关文件
拷贝顺序最好是: domain -> mapper -> service -> impl -> controller 这样报错就会少一些
接口文档配置
knife4j:
enable: true
openapi:
title: 黑马商城购物车管理接口文档
description: "黑马商城购物车管理接口文档"
email: zhanghuyi@itcast.cn
concat: 虎哥
url: https://www.itcast.cn
version: v1.0.0
group:
default:
group-name: default
api-rule: package
api-rule-resources:
- com.hmall.cart.controller // 这里扫包路径记得更改!!!!!!
记得改扫描路径
配置启动项
这是一个引导, 后面介绍的Nacos更方便操作远程调用
先将RestTemplate注册为一个Bean:
package com.hmall.cart.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class RemoteCallConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
接下来,我们修改cart-service
中的com.hmall.cart.service.impl.CartServiceImpl
的handleCartItems
方法,发送http请求到item-service
:
注册中心原理
在微服务远程调用的过程中,包括两个角色:
- 服务提供者:提供接口供其它微服务访问,比如item-service
- 服务消费者:调用其它微服务提供的接口,比如cart-service
流程如下:
- 服务启动时就会注册自己的服务信息(服务名、IP、端口)到注册中心
- 调用者可以从注册中心订阅想要的服务,获取服务对应的实例列表(1个服务可能多实例部署)
- 调用者自己对实例列表负载均衡,挑选一个实例
- 调用者向该实例发起远程调用
当服务提供者的实例宕机或者启动新实例时,调用者如何得知呢?
- 服务提供者会定期向注册中心发送请求,报告自己的健康状态(心跳请求)
- 当注册中心长时间收不到提供者的心跳时,会认为该实例宕机,将其从服务的实例列表中剔除
- 当服务有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表
- 当注册中心服务列表变更时,会主动通知微服务,更新本地服务列表
Nacos注册中心
拉取nacos-server镜像
docker pull nacos/nacos-server:v2.1.0-slim
创建nacos数据库存储数据
新建一个名为nacos的数据库, 然后导入下面的nacos.sql文件即可
我们基于Docker来部署Nacos的注册中心,首先我们要准备MySQL数据库表,用来存储Nacos的数据。由于是Docker部署,将SQL文件导入到Docker中的MySQL容器中:
部署nacos在docker容器中
请自行创建一个nacos文件夹, 里面再创建一个custom.env文件,内容如下:
其中的nacos/custom.env文件中,有一个MYSQL_SERVICE_HOST也就是mysql地址,
需要修改为你自己的虚拟机IP地址:
PREFER_HOST_MODE=hostname
MODE=standalone
SPRING_DATASOURCE_PLATFORM=mysql
MYSQL_SERVICE_HOST=8.134.57.218
MYSQL_SERVICE_DB_NAME=nacos
MYSQL_SERVICE_PORT=3306
MYSQL_SERVICE_USER=root
MYSQL_SERVICE_PASSWORD=123456
MYSQL_SERVICE_DB_PARAM=characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/Shanghai
然后,将nacos目录上传至虚拟机的/root目录。
进入root目录,然后执行下面的docker命令:
docker run -d \
--name nacos \
--env-file ./nacos/custom.env \
-p 8848:8848 \
-p 9848:9848 \
-p 9849:9849 \
--restart=always \
nacos/nacos-server:v2.1.0-slim
mkdir -p /data/nacos/{conf,logs}
chmod -R 777 /data/nacos/{conf,logs}
docker cp nacos:/home/nacos/conf/application.properties /data/nacos/conf/
docker run -d \
--name nacos \
--env-file ./nacos/custom.env \
-p 8848:8848 \
-p 9848:9848 \
-p 9849:9849 \
-v /data/nacos/logs:/home/nacos/logs \
-v /data/nacos/conf/application.properties:/home/nacos/conf/application.properties \
-v /data/nacos/data:/home/nacos/data \
--restart=always \
nacos/nacos-server:v2.1.0-slim
=---=
docker run -d \
-e MODE=standalone \
--privileged=true \
-e SPRING_DATASOURCE_PLATFORM=mysql \
-e MYSQL_SERVICE_HOST=192.168.100.100 \
-e MYSQL_SERVICE_PORT=3306 \
-e MYSQL_SERVICE_USER=root \
-e MYSQL_SERVICE_PASSWORD=ovoAdmin123 \
-e MYSQL_SERVICE_DB_NAME=nacos \
-e TIME_ZONE='Asia/Shanghai' \
-v /data/nacos/logs:/home/nacos/logs \
-v /data/nacos/data:/home/nacos/data \
-v /data/nacos/conf:/home/nacos/conf \
-p 8848:8848 -p 9848:9848 -p 9849:9849 \
--name nacos --restart=always nacos/nacos-server:v2.1.0-slim
- docker run -d: 这部分启动一个后台(detached)容器,使容器在后台运行。
- –name nacos: 指定容器的名称为 “nacos”。
- –env-file ./nacos/custom.env: 从指定的环境变量文件 ./nacos/custom.env 中读取环境变量。这可以包含 Nacos 服务的配置信息。
- -p 8848:8848 -p 9848:9848 -p 9849:9849: 将容器内部的端口映射到主机上。这里将 Nacos 默认的三个端口 8848、9848、9849 映射到主机的对应端口。
- –restart=always: 在容器退出时,总是重新启动容器。这样可以确保 Nacos 服务在意外退出后自动重新启动。
- nacos/nacos-server:v2.1.0-slim: 指定要运行的 Docker 镜像的名称和版本。这里使用的是 Nacos 的官方镜像 nacos/nacos-server 的 v2.1.0-slim 版本。
启动完成后,访问下面地址:http://192.168.164.128:8848/nacos/,注意将192.168.164.128替换为你自己的虚拟机IP地址。首次访问会跳转到登录页,账号密码都是nacos
服务注册
- 引入依赖
- 配置Nacos地址
- 启动 !!!
引入依赖
如果父工程配有springcloud的版本的话, 这里就不用带version了
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2021.0.4.0</version>
</dependency>
配置Nacos地址
在item-service的application.yml中添加nacos地址配置:
spring:
application:
name: item-service # 服务名称
cloud:
nacos:
server-addr: 192.168.164.128:8848 # nacos地址
启动
可以copy多一份启动两份服务实例
重启item-service的两个实例:
访问Nacos控制台, 发现启动成功:
点击详情,可以查看到item-service服务的两个实例信息:
服务发现(调用)
这里也只是引导, 后面用OpenFeign代码更简洁
- 引入依赖
- 配置Nacos地址
- 发现并调用服务
引入依赖
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
配置nacos地址
spring:
cloud:
nacos:
server-addr: 192.168.164.128:8848
发现并调用服务
接下来,服务调用者cart-service就可以去订阅item-service服务了。不过item-service有多个实例,而真正发起调用时只需要知道一个实例的地址。
因此,服务调用者必须利用负载均衡的算法,从多个实例中挑选一个去访问。常见的负载均衡算法有:
- 随机
- 轮询
- IP的hash
- 最近最少访问
- …
这里我们可以选择最简单的随机负载均衡。
另外,服务发现需要用到一个工具,
DiscoveryClient,SpringCloud已经帮我们自动装配,我们可以直接注入使用:
接下来,我们就可以对原来的远程调用做修改了,之前调用时我们需要写死服务提供者的IP和端口:
现在不需要了,我们通过DiscoveryClient发现服务实例列表,通过负载均衡算法,选择一个实例去调用:
private void handleCartItems(List<CartVO> vos) {
// 1.获取商品id
Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toS
// 2.查询商品
//List<ItemDTO> items = itemService.queryItemByIds(itemIds);
// 2.1 根据服务名称获取服务实例列表
List<ServiceInstance> instances = discoveryClient.getInstances("item-service")
if(CollUtil.isEmpty(instances)){
// 服务不存在
return;
}
// 2.2 手写负载均衡, 从实例列表中挑选一个实例
ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size()
// 2.3 利用RestTemplate发送http请求,得到http相应(远程调用)
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
instance.getUri() + "/items?ids={ids}",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<ItemDTO>>() {
},
Map.of("ids", CollUtil.join(itemIds, ","))
);
// 2.2 解析响应
if(!response.getStatusCode().is2xxSuccessful()){
// 查询失败, 直接结束
return;
}
到这里nacos的基本注册和使用就结束啦~
快速入门
引入依赖
<!--openFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--负载均衡器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
启动OpenFeign
我们在cart-service的CartApplication启动类上添加注解,启动OpenFeign功能:
编写OpenFeign客户端Client
这里可以直接参考最佳实践~client统一写在一个package里
在cart-service中,定义一个新的接口,编写Feign客户端:
package com.hmall.cart.client;
import com.hmall.cart.domain.dto.ItemDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@FeignClient("item-service")
public interface ItemClient {
@GetMapping("/items")
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}
这里只需要声明接口,无需实现方法。接口中的几个关键信息:
- @FeignClient(“item-service”) :声明服务名称
- @GetMapping :声明请求方式
- @GetMapping(“/items”) :声明请求路径
- @RequestParam(“ids”) Collection ids :声明请求参数
- List:返回值类型
有了上述信息,OpenFeign就可以利用动态代理帮我们实现这个方法,并且向http://item-service/items发送一个GET请求,携带ids为请求参数,并自动将返回值处理为List。
我们只需要直接调用这个方法,即可实现远程调用了。
使用FeignClient
我们在cart-service对应的实现层中改造代码,直接调用ItemClient的方法:
feign替我们完成了服务拉取、负载均衡、发送http请求的所有工作,是不是看起来优雅多了。
而且,这里我们不再需要RestTemplate了,还省去了RestTemplate的注册
连接池(提高响应速度)
- Apache HttpClient :支持连接池
- OKHttp:支持连接池
引入依赖
在cart-service的pom.xml中引入依赖:
<!--OK http 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
开启连接池
feign:
okhttp:
enabled: true # 开启OKHttp功能
最佳实践
- 思路1:抽取到微服务之外的公共module
- 思路2:每个微服务自己抽取一个module
方案1抽取更加简单,工程结构也比较清晰,但缺点是整个项目耦合度偏高。
方案2抽取相对麻烦,工程结构相对更复杂,但服务之间耦合度降低。
我们最终采取方案一, 代码较少~
抽取Feign客户端
在hmall下定义一个新的module,命名为hm-api
记得在父工程pom.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>hmall</artifactId>
<groupId>com.heima</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>hm-api</artifactId>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<!--open feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- load balancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- swagger 注解依赖 -->
<dependency>
<groupId>io.swagger</groupId>
<artifactId>swagger-annotations</artifactId>
<version>1.6.6</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
然后把ItemDTO和ItemClient都拷贝过来,最终结构如下:
扫描包
任何微服务要调用item-service中的接口,只需引hm-api模块依赖即可,无需自己编写Feign客户端了。
<!--feign模块-->
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-api</artifactId>
<version>1.0.0</version>
</dependency>
- 方式1:声明扫描包:
- 方式2:声明要用的FeignClient
这里比较推荐使用方式1, 直接扫描整个client包, 小孩才做选择, 我全都要~
日志配置
OpenFeign只会在FeignClient所在包的日志级别为DEBUG时,才会输出日志。而且其日志级别有4级:
- NONE:不记录任何日志信息,这是默认值。
- BASIC:仅记录请求的方法,URL以及响应状态码和执行时间
- HEADERS:在BASIC的基础上,额外记录了请求和响应的头信息
- FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据。
Feign默认的日志级别就是NONE,所以默认我们看不到请求日志。
定义日志级别
在hm-api模块下新建一个配置类,定义Feign的日志级别:
后面的保存用户登录信息在各微服务使用也是在这个配置里写Bean, 划重点~
package com.hmall.api.config;
import feign.Logger;
import org.springframework.context.annotation.Bean;
public class DefaultFeignConfig {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.Basic; // 推荐使用Basic
}
}
配置生效
要让日志级别生效,还需要配置这个类。有两种方式:
- 局部生效:在某个FeignClient中配置,只对当前FeignClient生效
@FeignClient(value = "item-service", configuration = DefaultFeignConfig.class)
- 全局生效:在@EnableFeignClients中配置,针对所有FeignClient生效。
@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)
总结
如何利用OpenFeign实现远程调用
- 引入OpenFeign和SpringCloudLoadBalancer依赖
- 利用@EnableFeignClients注解开启OpenFeign功能
- 编写FeignClient
如何配置OpenFeign连接池
- 引入http客户端依赖, 例如OKhttp, HttpClient
- 配置yml文件, 打开OpenFeign连接池开关
OpenFeign使用的最佳实践方式是什么
- 由服务提供者编写独立module, 将FeignClient及DTO抽取
如何配置OpenFeign输出日志的级别
- 声明类型为Logger].Level的Bean
- 在@FeignClient或@EnableFeignClient注解上使用