前后端分离开发
- 前后端分离开发后,前后端代码不在混合在同一个maven工程中,而是分为前端工程和后端工程。此时前后端代码并行开发,可以加快项目的开发进度
- 在前后端代码分离后,此时后端工程会打包部署到Tomcat上,前端工程会打包部署到Nginx上
前后端分离开发流程
-
前后端分离开发后有一个问题:前端人员和后端人员如何进行配合来共同开发一个项目。如果前后端人员的开发习惯或规范不一样就会导致后期联调对接不上,出很多问题,所以为了后期方便开发,就需要先定制接口来规范开发内容,让前后端人员均按照此规范来进行开发,这样就避免了后期前后端联调的麻烦问题,此时开发流程如下图所示:
-
请求(API接口):就是一个http请求地址,主要是去定义:请求路径、请求方式、请求参数、响应数据等内容。示例图如下
-
前后端人员自测
- 前端
- 使用mock:产生模拟数据,帮助前端人员进行测试
- 后端
- YApi:可用来提前定义接口文档以及请求测试等
- 可自行进入官网注册并登录后进行操作
- Swagger:用来生成接口文档
- PostMan:请求测试
- YApi:可用来提前定义接口文档以及请求测试等
- 前端
微服务背景引入
单体架构
- 单体架构:将业务的所有功能集中在一个项目中开发,打成一个包部署。
- 优点:架构简单且部署成本低
- 缺点:耦合度高(维护困难、升级困难)
分布式架构
- 分布式架构:根据业务功能对系统进行拆分,每个业务模块作为独立项目开发,称为一个服务
- 优点:降低服务耦合度,有利于服务升级拓展
- 缺点:服务调用关系错综复杂
- 微服务就是分布式架构的一种
微服务
-
微服务是一种经过良好架构设计的分布式架构方案,特征为:
-
单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责
-
面向服务:服务提供统一标准的接口,与语言和技术无关
-
自治:团队独立、技术独立、数据独立,独立部署和交付
-
隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题
①优点:拆分粒度更小、服务更独立、耦合度更低
②缺点:架构非常复杂,运维、监控、部署难度提高
-
-
微服务技术最知名的为:SpringCloud和阿里巴巴的Dubbo,Dubbo后期在SpringCloud的基础上优化产生了SpringCloudAlibaba
- 国内使用最广泛的为SpringCloud
-
微服务技术对比
-
企业使用微服务组合种类
SpringCloud
-
SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验。
版本兼容
-
SpringCloud底层是依赖于SpringBoot的,所以它俩之间有严格的版本兼容关系,看而详见SpringCloud官网,目前最新的版本兼容关系如图所示
注意:
由于博主的SpringBoot版本为3.4.0,所以会使用的SpringCloud版本为
2024.0.x aka Moorgate
推荐使用Spring Cloud 2021.0.x以及Spring Boot 2.7.x版本
-
推荐使用的版本如下
注意:由于SpringCloudAlibaba最新版为2023,所以若使用SpringAlibaba时,SpringBoot以及SpringCloud只能用2024之前的版本,具体的版本对应关系可详见SpringAlibaba的GItHub
SpringBoot SpringCloud SpringAlibaba 最低支持的JDK版本 2.3.12.RELEASE Hoxton.SR12 2.2.8.RELEASE JDK1.8 2.3.9.RELEASE Hoxton.SR10 2.2.5.RELEASE JDK1.8 3.0.7 2022.0.3 2022.0.0.0-RC2 JDK17 3.2.x 2023.0.x 2023.x JDK17
-
微服务拆分
-
微服务拆分原则/注意事项
- 不同微服务,不能重复开发相同业务
- 若出现重复相同业务,则就不能称之为微服务
- 微服务数据独立,不能访问其它微服务的数据库
- 微服务可以将自己的业务暴露为接口,供其它微服务调用
- 不同微服务,不能重复开发相同业务
-
微服务拆分后的工程结构有两种
-
方式一:将每个业务都拆分为独立的Project
-
方式二:将每个业务都拆分为Module
-
在快速入门中以第二种方式为例
-
快速入门
环境准备
后端环境准备,该后端测试项目已上传至Gitee,可自行下载
-
Step1: 创建网络
hm-net
:docker network create hm-net
-
Step2: 在Linux中利用Docker部署MySQL容器,并将该容器添加到hm-net网络中,代码如下:
具体过程可详见Docker部分内容
docker run -id \ --name mysql \ -p 3307:3306 \ -v /root/mysql/conf:/etc/mysql/conf.d \ -v /root/mysql/logs:/logs \ -v /root/mysql/data:/var/lib/mysql \ -v /root/mysql/init:/docker-entrypoint-initdb.d \ -e TZ=Asia/Shanghai \ -e MYSQL_ROOT_PASSWORD=123456 \ --network hm-net \ mysql
-
Step3: 利用
vim /root/mysql/conf/hm.cnf
在挂载的配置目录下创建该MySQL容器配置文件,该文件代码如下:[client] default_character_set=utf8mb4 [mysql] default_character_set=utf8mb4 [mysqld] character_set_server=utf8mb4 collation_server=utf8mb4_unicode_ci init_connect='SET NAMES utf8mb4'
-
Step4: 利用
vim /root/mysql/init/hmall.sql
在脚本目录下创建脚本文件hmall.sql,文件代码略 -
Step5: 将项目
hmall
导入到idea中,并使idea与MySQL容器建立连接,如下: -
Step6: 在该项目中添加SpringBoot的启动项
上述点击之后,则会出现如下界面:
点击对应按钮,即可实现运行或DEBUG运行。但是在运行之前,再做个简单配置:在
HMallApplication
上点击鼠标右键,会弹出窗口,然后选择Edit Configuration
:在弹出窗口中配置SpringBoot的启动环境为
local
: -
Step8: 在yaml配置文件中修改数据库的ip地址为自己的虚拟机地址以及端口号
-
Step9: 测试项目是否能够启动成功
-
注意:若数据库连接超时,如图所示,则说明挂起之后无法连接MySQL容器,则需要在Linux中进行解决,解决步骤如下:
-
Step1: 关闭防火墙:
systemctl stop firewalld
-
Step2: 禁止开机启动防火墙:
systemctl disable firewalld
-
Step3: 修改ipv4转发状态
vi /usr/lib/sysctl.d/00-system.conf
,- 添加代码
net.ipv4.ip_forward = 1
-
Step4: 保存退出后,重启网络服务
systemctl restart network
-
将docker的网络接口设置为不被NetworkManager管理
-
vi /etc/NetworkManager/conf.d/10-unmanage-docker-interfaces.conf
-
添加代码:
[keyfile] unmanaged-devices=interface-name:docker*;interface-name:veth*;interface-name:br-*;interface-name:vmnet*;interface-name:vboxnet*
-
-
Step5: 重启网络
systemctl restart NetworkManager
-
Step6: 重启docker
systemctl restart docker
-
Step7: 启动相应的容器:
docker restart mysql
-
商品服务拆分
需求:将hm-service中与商品管理相关的工程拆分到一个微服务module中,命名为item-service
-
Step1: 在hmall中创建模块且模块名为:item-service
-
Step2: 在item-service模块的pom.xml文件中添加坐标依赖
-
将hm-service的pom.xml文件中与商品管理相关的依赖复制到item-service模块的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"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.heima</groupId> <artifactId>hmall</artifactId> <version>1.0.0</version> </parent> <artifactId>item-service</artifactId> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--common--> <dependency> <groupId>com.heima</groupId> <artifactId>hm-common</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> <!--单元测试--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> </dependencies> <build> <finalName>${project.artifactId}</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
-
-
Step3: 创建启动引导类
item-service\src\main\java\com\hmall\item\ItemApplication.java
代码如下:package com.hmall.item; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @MapperScan("com.hmall.item.mapper") @SpringBootApplication public class ItemApplication { public static void main(String[] args) { SpringApplication.run(ItemApplication.class, args); } }
-
Step4: 复制
hm-service
中的application.yml、application-local.yml、application-dev.yml
配置文件到 item-service 模块的resources
目录如下:-
application.yml
文件修改后如下:server.port
的端口号由8080改为8081datasource.url
的数据库由hmall改为hm-itemopenapi.title
、openapi.description
参数由 黑马商城接口文档 改为 商品服务接口文档api-rule-resources
属性值由com.hmall.controller
改为com.hmall.item.controller
server: port: 8081 spring: application: name: item-service profiles: active: dev datasource: url: jdbc:mysql://${hm.db.host}:3307/hm-item?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver username: root password: ${hm.db.pw} mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler global-config: db-config: update-strategy: not_null id-type: auto logging: level: com.hmall: debug pattern: dateformat: HH:mm:ss:SSS file: path: "logs/${spring.application.name}" knife4j: enable: true openapi: title: 商品服务接口文档 description: "商品服务接口文档" email: itheima@itcast.cn concat: itheima url: https://www.itcast.cn version: v1.0.0 group: default: group-name: default api-rule: package api-rule-resources: - com.hmall.item.controller
-
-
Step5: 复制
hm-service
中与商品有关的代码到item-service
模块中,最终该模块中的代码如下注意:将代码复制过来后要修改类中import的包的路径,因为在该模块中,比原始项目多了一层包item
这里有一个地方的代码需要改动,就是
ItemServiceImpl
中的deductStock
方法:因为ItemMapper的所在包发生了变化,因此这里代码必须修改包路径
-
Step6: 创建hm-item数据库
-
Step6-1: 将本地创建hm-item数据库的sql文件导入运行
-
Step6-2: 将idea与hm-item数据库建立连接
-
最终,会在数据库创建一个名为
hm-item
的database,将来的每一个微服务都会有自己的一个database:
-
-
Step7: 复制一个SpringBoot的启动项并修改,步骤如图所示
修改完后如下
-
Step8: 启动
item-service
的引导类ItemApplication
,测试是否该模块是否能够运行成功
购物车服务拆分
需求:将hm-service中与购物车相关的工程拆分到一个微服务module中,命名为cart-service
注意:该拆分过程与商品服务拆分过程一致,所以此处不在进行图片示例演示
-
Step1: 在hmall中创建模块且模块名为:cart-service
-
Step2: 在cart-service模块的pom.xml文件中添加坐标依赖
-
将hm-service的pom.xml文件中与购物车服务相关的依赖复制到cart-service模块的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"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>com.heima</groupId> <artifactId>hmall</artifactId> <version>1.0.0</version> </parent> <artifactId>cart-service</artifactId> <properties> <maven.compiler.source>11</maven.compiler.source> <maven.compiler.target>11</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--common--> <dependency> <groupId>com.heima</groupId> <artifactId>hm-common</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> <!--单元测试--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> </dependency> </dependencies> <build> <finalName>${project.artifactId}</finalName> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
-
-
Step3: 创建启动引导类
item-service\src\main\java\com\hmall\item\CartApplication.java
代码如下:package com.hmall.cart; import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @MapperScan("com.hmall.cart.mapper") @SpringBootApplication public class CartApplication { public static void main(String[] args) { SpringApplication.run(ItemApplication.class, args); } }
-
Step4: 复制
hm-service
中的application.yml、application-local.yml、application-dev.yml
配置文件到cart-service模块的resources
目录如下:-
application.yml
文件修改后如下:server.port
的端口号由8080改为8082datasource.url
的数据库由hmall改为hm-cartopenapi.title
、openapi.description
参数由 黑马商城接口文档 改为 购物车服务接口文档api-rule-resources
属性值由com.hmall.controller
改为com.hmall.cart.controller
server: port: 8082 spring: application: name: cart-service profiles: active: dev datasource: url: jdbc:mysql://${hm.db.host}:3307/hm-cart?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai driver-class-name: com.mysql.cj.jdbc.Driver username: root password: ${hm.db.pw} mybatis-plus: configuration: default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler global-config: db-config: update-strategy: not_null id-type: auto logging: level: com.hmall: debug pattern: dateformat: HH:mm:ss:SSS file: path: "logs/${spring.application.name}" knife4j: enable: true openapi: title: 购物车服务接口文档 description: "购物车服务接口文档" email: itheima@itcast.cn concat: itheima url: https://www.itcast.cn version: v1.0.0 group: default: group-name: default api-rule: package api-rule-resources: - com.hmall.cart.controller
-
-
Step5: 复制
hm-service
中与商品有关的代码到item-service
模块 中,最终该模块中的代码如下注意:将代码复制过来后要修改类中import的包的路径,因为在该模块中,比原始项目多了一层包cart
这里有一个地方的代码需要改动,就是
CartServiceImpl
:- 需要获取登录用户信息,但登录校验功能目前没有复制过来,先写死固定用户id
- 查询购物车时需要查询商品信息,而商品信息不在当前服务,需要先将这部分代码注释
package com.hmall.cart.service.impl; import cn.hutool.core.util.StrUtil; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.hmall.cart.domain.dto.CartFormDTO; import com.hmall.cart.domain.po.Cart; import com.hmall.cart.domain.vo.CartVO; import com.hmall.cart.mapper.CartMapper; import com.hmall.cart.service.ICartService; import com.hmall.common.exception.BizIllegalException; import com.hmall.common.utils.BeanUtils; import com.hmall.common.utils.CollUtils; import com.hmall.common.utils.UserContext; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; import java.util.function.Function; import java.util.stream.Collectors; /** * <p> * 订单详情表 服务实现类 * </p> * * @author itheima * @since 2023-05-05 */ @Service @RequiredArgsConstructor public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService { //private final IItemService itemService; @Override public void addItem2Cart(CartFormDTO cartFormDTO) { // 1.获取登录用户 Long userId = UserContext.getUser(); // 2.判断是否已经存在 if(checkItemExists(cartFormDTO.getItemId(), userId)){ // 2.1.存在,则更新数量 baseMapper.updateNum(cartFormDTO.getItemId(), userId); return; } // 2.2.不存在,判断是否超过购物车数量 checkCartsFull(userId); // 3.新增购物车条目 // 3.1.转换PO Cart cart = BeanUtils.copyBean(cartFormDTO, Cart.class); // 3.2.保存当前用户 cart.setUserId(userId); // 3.3.保存到数据库 save(cart); } @Override public List<CartVO> queryMyCarts() { // 1.查询我的购物车列表 //List<Cart> carts = lambdaQuery().eq(Cart::getUserId, UserContext.getUser()).list(); // TODO 先将用户的id写死为 1 List<Cart> carts = lambdaQuery().eq(Cart::getUserId, 1L).list(); if (CollUtils.isEmpty(carts)) { return CollUtils.emptyList(); } // 2.转换VO List<CartVO> vos = BeanUtils.copyList(carts, CartVO.class); // 3.处理VO中的商品信息 handleCartItems(vos); // 4.返回 return vos; } private void handleCartItems(List<CartVO> vos) { // 1.获取商品id // TODO 暂时无法调用商品服务,先注释 /* Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet()); // 2.查询商品 List<ItemDTO> items = itemService.queryItemByIds(itemIds); if (CollUtils.isEmpty(items)) { return; } // 3.转为 id 到 item的map Map<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity())); // 4.写入vo for (CartVO v : vos) { ItemDTO item = itemMap.get(v.getItemId()); if (item == null) { continue; } v.setNewPrice(item.getPrice()); v.setStatus(item.getStatus()); v.setStock(item.getStock()); } */ } @Override public void removeByItemIds(Collection<Long> itemIds) { // 1.构建删除条件,userId和itemId QueryWrapper<Cart> queryWrapper = new QueryWrapper<Cart>(); queryWrapper.lambda() .eq(Cart::getUserId, UserContext.getUser()) .in(Cart::getItemId, itemIds); // 2.删除 remove(queryWrapper); } private void checkCartsFull(Long userId) { int count = lambdaQuery().eq(Cart::getUserId, userId).count(); if (count >= 10) { throw new BizIllegalException(StrUtil.format("用户购物车课程不能超过{}", 10)); } } private boolean checkItemExists(Long itemId, Long userId) { int count = lambdaQuery() .eq(Cart::getUserId, userId) .eq(Cart::getItemId, itemId) .count(); return count > 0; } }
-
Step6: 创建hm-cart数据库
-
Step6-1: 将本地创建hm-cart数据库的sql文件导入运行
-
Step6-2: 将idea与hm-item数据库建立连接
-
最终,会在数据库创建一个名为
hm-item
的database,将来的每一个微服务都会有自己的一个database
-
-
Step7: 复制一个SpringBoot的启动项并修改为cart-service模块的引导类
CartApplication
-
Step8: 启动
cart-service
的引导类CartApplication
,测试是否该模块是否能够运行成功
微服务远程调用
本示例的初始项目cloud-demo具体搭建过程可详见SpringCloud项目快速搭建部分内容
且本项目已上传至Gitee,可自行下载
背景说明
-
该项目一共有两个模块,分别为订单模块和用户模块,每个模块各实现了一个功能,该羡项目的完整结构及表现层代码如图所示
-
对应的实体类代码有两个:User、Order
-
-
运行该项目下两个模块的启动类后测试结果如下
-
order-service模块
-
user-service模块
-
从该项目的order-service模块的运行结果可看出
虽然可以正常运行,但是user为null,这是由于此时order-service模块并未远程调用user-service模块
因此我们就需要从一个微服务来远程调用另一个微服务,即在order-service模块中来远程调用user-service模块,从而获取数据
-
-
解决办法: 可以在微服务中来模拟浏览器发送http请求到另一个微服务,从而获取到数据
- 即在order-service模块中发送http请求到user-service模块来获取数据
- Spring给我们提供了一个
RestTemplate
的API,可以方便的实现Http的各种请求的发送。
RestTemplate
-
RestTemplate
类是Spring提供的一个API,它继承自抽象类InterceptingHttpAccessor
,且实现了RestOperations
接口 -
利用RestTemplate发送http请求与前端ajax发送请求非常相似,都包含四部分信息:
- ① 请求方式
- ② 请求路径
- ③ 响应数据类型
- ④ 请求参数
-
RestOperations
类中提供了大量的方法,部分方法如图所示有图可知,对于常见的Get、Post、Put、Delete请求都支持,如果请求参数比较复杂,还可以使用
exchange
方法来构造请求。 -
RestOperations
类常用方法注意:以下方法在发生异常时均会抛出
RestClientException
异常GET方法 解释 public <T> T getForObject(URI url, Class<T> responseType)
发送Get请求并返回指定类型对象 public <T> T getForObject(String url, Class<T> responseType, Object... uriVariables)
发送Get请求并返回指定类型对象 public <T> T getForObject(String url, Class<T> responseType, Map<String, ?> uriVariables)
发送Get请求并返回指定类型对象 POST方法 解释 public <T> T postForObject(URI url, @Nullable Object request, Class<T> responseType) throws RestClientException
发送Post请求并返回指定类型对象 public <T> T postForObject(String url, @Nullable Object request, Class<T> responseType, Object... uriVariables) throws RestClientException
发送Post请求并返回指定类型对象 public <T> T postForObject(String url, @Nullable Object request, Class<T> responseType, Map<String, ?> uriVariables) throws RestClientException
发送Post请求并返回指定类型对象 PUT方法 解释 public void put(URI url, @Nullable Object request)
发送PUT请求 public void put(String url, @Nullable Object request, Object... uriVariables)
发送PUT请求 public void put(String url, @Nullable Object request, Map<String, ?> uriVariables)
发送PUT请求 DELETE方法 解释 public void delete(URI url)
发送Delete请求 public void delete(String url, Object... uriVariables)
发送Delete请求 public void delete(String url, Map<String, ?> uriVariables)
发送Delete请求 EXCHANGE方法 解释 public <T> ResponseEntity<T> exchange(String url, HttpMethod method, @Nullable HttpEntity<?> requestEntity, Class<T> responseType, Map<String, ?> uriVariables)
发送任意类型请求,返回ResponseEntity public <T> ResponseEntity<T> exchange(URI url, HttpMethod method, @Nullable HttpEntity<?> requestEntity, Class<T> responseType)
发送任意类型请求,返回ResponseEntity public <T> ResponseEntity<T> exchange(String url, HttpMethod method, @Nullable HttpEntity<?> requestEntity, Class<T> responseType, Object... uriVariables)
发送任意类型请求,返回ResponseEntity - 以上方法在遇到时会进行详细说明,此处省略
快速入门
-
Step1: 在order-service模块中创建一个与三层架构包同级的config包,并在该包下创建一个配置类
RemoteCallConfig
,代码如下注意:该配置类必须用
@Configuration
注解来标记为配置类package at.guigu.config; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; @Slf4j @Configuration public class RemoteCallConfig { @Bean public RestTemplate restTemplate() { log.info("restTemplate创建成功,微服务远程调用开启"); return new RestTemplate(); } }
-
注意:
-
order-service模块中的启动类
OrderApplication
本身也是一个配置类,所以我们也可以直接在该启动类中进行微服务远程调用的配置。 -
若在启动类中进行配置,则不需要创建配置类
RemoteCallConfig
,此时启动类OrderApplication
代码如下package at.guigu; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @Slf4j @SpringBootApplication public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); log.info("OrderApplication Running"); } @Bean public RestTemplate restTemplate() { log.info("restTemplate创建成功,微服务远程调用开启"); return new RestTemplate(); } }
-
-
-
Step2: 修改业务层代码
-
Step2-1: 在
IOrderService
接口中添加方法queryOrderById
,代码如下:注意:接口中的方法默认就是
public abstract
,此处写出来只是为了演示,实际项目中可详略package at.guigu.service; import at.guigu.po.Order; import com.baomidou.mybatisplus.extension.service.IService; public interface IOrderService extends IService<Order> { // 返回包含用户信息的订单信息 public abstract Order queryOrderById(Long orderId); }
-
Step2-2: 在
IOrderService
接口中添加方法queryOrderById
,代码如下:package at.guigu.service.impl; import at.guigu.mapper.OrderMapper; import at.guigu.po.Order; import at.guigu.po.User; import at.guigu.service.IOrderService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @Service public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService { private final RestTemplate restTemplate; public OrderServiceImpl(RestTemplate restTemplate) { this.restTemplate = restTemplate; } /** * 获取包含用户信息的订单信息 * @param orderId * @return Order */ @Override public Order queryOrderById(Long orderId) { // 1 查询订单信息 Order order = this.getById(orderId); // 2 利用RestTemplate发起Http请求来获取用户数据,实现远程调用 // 2.1 设置url路径 String url = "http://localhost:8081/user/" + order.getUserId(); // 2.2 发送Http请求,实现远程调用 User user = restTemplate.getForObject(url, User.class); // 3 封装用户数据存储到订单信息中 order.setUser(user); return order; } }
-
-
Step3: 表现层
OrderController
类中的queryOrderById
方法代码更改如下package at.guigu.controller; import at.guigu.po.Order; import at.guigu.service.IOrderService; import lombok.RequiredArgsConstructor; 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") @RequiredArgsConstructor public class OrderController { private final IOrderService orderService; @GetMapping("{orderId}") public Order queryOrderById(@PathVariable("orderId") Long orderId) { // 根据id查询订单并返回 return orderService.queryOrderById(orderId); } }
-
运行启动类测试
提供者与消费者
-
服务提供者:一次业务中,被其它微服务调用的服务
- 即提供接口给其它微服务的微服务
-
服务消费者:一次业务中,调用其它微服务调用的服务
- 即调用其它微服务提供的接口
-
在微服务远程调用快速入门的示例中order-srevice为服务消费者,order-service为服务提供者
-
注意
- 一个服务既可以是提供者,又可以是消费者
- 服务的具体身份根据具体业务分析,可能只是其中一个身份,但也可能同时拥有两种身份
服务注册与发现
在微服务远程调用的示例中,从
利用RestTemplate发起Http请求来获取用户数据从而实现远程调用
的那一部分代码中可以看出我们是直接将url硬编码到代码中(如上图所示),这样就产生了问题: 1.实际项目中,可能会由于开发、测试、生产环境的切换,导致url地址频繁发生变化
2.实际项目中,为了应对多并发环境,user-service服务可能会部署成一个集群(如下图所示),这就导致user-service服务存在不同的url
所以就不能采用硬编码的方式,但是不采用硬编码的方式,那服务消费者该如何获取服务提供者的地址信息呢?若有多个服务提供者,那服务消费者又要如何选择呢?且如何判断服务提供者的健康状态呢?
这就引出了服务注册中心
-
目前开源的注册中心框架有很多,国内比较常见的有:
- Eureka:Netflix公司出品,目前被集成在SpringCloud当中,一般用于Java应用
- Nacos:Alibaba公司出品,目前被集成在SpringCloudAlibaba中,一般用于Java应用
- Consul:HashiCorp公司出品,目前集成在SPringCloud中,不限制微服务语言
以上几种注册中心都遵循SpringCloud中的API规范,因此在业务开发使用上没有太大差异。由于Nacos是国内产品,中文文档比较丰富,而且同时具备配置管理功能,因此在国内使用较多
在本章内容中主要讲解Eureka以及Nacos,两者选用一个即可,实际项目推荐Nacos
Eureka注册中心
Eureka代码演示已上传至Gitee,可自行下载
-
Eureka中主要有两部分:
- 服务端:eureka-server注册中心
- 客户端:eureka-client
- 客户端主要包括两部分:服务提供者和服务消费者
-
Eureka原理
- 服务提供者
- Eureka客户端中的服务提供者在服务启动时会创建服务实例信息(包括服务提供者的服务名称、ip地址、端口号等),然后将自己的服务实例信息注册到Eureka服务端(即eureka-server注册中心),并且服务提供者会定期(默认30s/次)向注册中心发送请求,报告自己的健康状态(心跳请求)
- 当注册中心长时间收不到服务提供者的心跳时,会认为该实例宕机,然后将其从服务的实例列表中剔除
- 当服务提供者有新实例启动时,会发送注册服务请求,其信息会被记录在注册中心的服务实例列表
- 当注册中心服务列表变更时,也会主动通知服务提供者,更新本地服务列表
- 服务消费者
- 假设此时有个服务消费者要进行消费,就会从Eureka服务端(即eureka-server注册中心)来 拉取/订阅/发现 相关服务提供者的服务实例信息,,然后服务消费者会结合负载均衡算法来挑选一个服务实例进行远程调用
- Eureka客户端
- 为减少Eureka服务端(即eureka-server注册中心)的压力,eureka-client本地会维护一份服务注册表的缓存并定期更新(默认30s/次),服务调用时会优先从本地缓存中读取服务实例信息
- 服务提供者
-
相关问题
Eureka服务搭建注册中心入门
主要分为三大步骤,如图所示
-
Step1: 创建一个新的微服务
注意:由于Eureka自己也是一个微服务,所以此处需要创建一个新的微服务
-
创建一个新的Maven项目eureka-server
-
创建完成后的项目结构如下
-
-
Step2: 在eureka-server模块的pom.xml文件中引入相关依赖
-
启动类相关依赖:spring-boot-starter-web
注意:由于启动类相关依赖已在聚合工程(即父工程)中配置了,所以此处就未进行配置
-
Eureka依赖:spring-cloud-starter-netflix-eureka-server
<?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> <parent> <groupId>cn.itcast.demo</groupId> <artifactId>cloud-demo</artifactId> <version>1.0</version> </parent> <artifactId>eureka-server</artifactId> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!--Eureka服务端依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId> </dependency> </dependencies> </project>
-
-
Step3: 在java包下创建at.guigu包,并在该包下创建启动类
EurekaApplication
,代码如下注意:
@EnableEurekaServer
注解是开启Eureka服务的必备注解,不可缺失package at.guigu; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @Slf4j @EnableEurekaServer @SpringBootApplication public class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class, args); log.info("Eureka-Server服务端启动成功"); } }
-
Step4: 右键源代码配置文件目录(即资源文件
resources
)→New
→File
,创建配置文件application.yml,代码如下:- Eureka服务配置
server: port: 10086 spring: application: # 指定eureka服务端的名称 name: eurekaserver eureka: client: service-url: # 配置eureka服务端的默认地址信息,同时将Eureka服务自身注册到Eureka-server注册中心 defaultZone: http://127.0.0.1:10086/eureka
-
Step5: 运行eureka-server服务的启动类报错:
'url' attribute is not specified and no embedded datasource could be configured
-
原因:未配置数据源相关的url
-
注意:Eureka服务本身是不需要配置数据源的,但是 Spring Boot 自动配置机制 默认需要一个数据源配置,所以解决方式如下:
-
在Eureka服务的启动类
EurekaApplication
中,给@SpringBootApplication
注解添加exclude
属性且属性值为DataSourceAutoConfiguration.class
,以此来排除数据源自动配置package at.guigu; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer; @Slf4j @EnableEurekaServer @SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) public class EurekaApplication { public static void main(String[] args) { SpringApplication.run(EurekaApplication.class, args); log.info("Eureka-Server Running"); } }
-
-
-
Step6: 再次运行eureka-server服务的启动类
-
eureka-server服务的启动类运行成功后,浏览器输入url:
http://localhost:10086/
后会跳转到Eureka的管理页面
Eureka服务注册快速入门
-
Step1: 在服务提供者(即user-service模块)的pom文件中引入相关依赖
- Eureka客户端依赖:spring-cloud-starter-netflix-eureka-client
<?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> <!--Eureka客户端依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> </dependencies> </project>
-
Step2: 在服务提供者(即user-service模块)的配置文件中编写Eureka配置
- 配置服务提供者或服务消费者的名称
- 由于此时user-service模块充当的是服务提供者,所以name属性值即为服务提供者名
- 配置Eureka客户端的默认地址信息
- 目的:将服务提供者(即user-service模块)的实例信息注册到Eureka服务端(即eureka-server注册中心)
server: port: 8081 spring: application: # 配置服务提供者或服务消费者的名称 # 由于此时user-service模块属于服务提供者,所以该名称即为服务提供者名 name: userservice # 配置数据库连接信息以及数据源信息 datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/cloud_user?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true username: root password: 123456 mybatis-plus: configuration: # 配置MyBatisPlus的运行日志 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 是否开启下划线和驼峰映射, 默认为true map-underscore-to-camel-case: true # 实体类的别名扫描包---即设置别名 type-aliases-package: at.guigu.po global-config: db-config: # 全局id类型---即id生成策略 # id-type: auto # 更新策略,默认为not_null # update-strategy: not_null # 设置数据库表名前缀 table-prefix: tb_ eureka: client: service-url: # 配置eureka服务端的默认地址信息 # 目的:将服务提供者(即user-service模块)的实例信息注册到Eureka服务端(即eureka-server注册中心) defaultZone: http://127.0.0.1:10086/eureka register-with-eureka: true # 是否将该服务的实例信息注册到Eureka(默认为true) fetch-registry: true # 是否从注册中心获取服务实例信息(默认为true) logging: level: # 配置指定包及其子包下的所有类的日志级别为debug at.guigu: debug # 配置日志输出的时间戳格式 pattern: dateformat: MM-dd HH:mm:ss:SSS
- 配置服务提供者或服务消费者的名称
-
运行服务提供者(即user-service模块)以及Eureka服务的启动类后输入
http://localhost:10086/
打开Eureka的管理页面,由该页面可看到,服务提供者(即user-service模块)的实例信息已注册成功-
在Eureka服务端(即eureka-server注册中心)可以看到,只有一个服务提供者(即user-service模块)的实例信息,若想要有多个该服务的实例信息来模拟多实例部署的话,则步骤如下:
以上操作成功后,服务列表就会新增一个服务提供者(即user-service模块)的启动类
UserApplication2
运行该启动类,然后刷新Eureka的管理页面,由该页面可看到,成功注册两个服务提供者(即user-service模块)的实例信息
-
Eureka服务拉取/订阅/发现快速入门
-
Step1: 在服务消费者(即order-service模块)的pom文件中引入相关依赖
- Eureka客户端依赖:spring-cloud-starter-netflix-eureka-client
<?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>order-service</artifactId> <!--配置依赖--> <dependencies> <!--Eureka客户端依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> </dependencies> </project>
-
Step2: 在服务消费者(即order-service模块)的配置文件中编写Eureka配置
- 配置服务提供者或服务消费者的名称
- 由于此时order-service模块充当的是服务消费者,所以name属性值即为服务消费者名
- 配置Eureka客户端的默认地址信息
server: port: 8080 spring: application: # 指定服务提供者/服务消费的名称 name: orderservice # 配置数据库连接信息以及数据源信息 datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/cloud_order?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true username: root password: 123456 mybatis-plus: configuration: # 配置MyBatisPlus的运行日志 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 是否开启下划线和驼峰映射, 默认为true map-underscore-to-camel-case: true # 实体类的别名扫描包---即设置别名 type-aliases-package: at.guigu.po global-config: db-config: # 全局id类型---即id生成策略 # id-type: auto # 更新策略,默认为not_null # update-strategy: not_null # 设置数据库表名前缀 table-prefix: tb_ eureka: client: service-url: # 配置eureka服务端的默认地址信息 # 目的:将服务提供者(即user-service模块)的实例信息注册到Eureka服务端(即eureka-server注册中心) defaultZone: http://127.0.0.1:10086/eureka register-with-eureka: false # 是否将该服务的实例信息注册到Eureka(默认为true) fetch-registry: true # 是否从注册中心获取服务实例信息(默认为true) logging: level: # 配置指定包及其子包下的所有类的日志级别为debug at.guigu: debug # 配置日志输出的时间戳格式 pattern: dateformat: MM-dd HH:mm:ss:SSS
- 配置服务提供者或服务消费者的名称
-
Step3: 更改服务消费者(即order-service模块)的业务层
OrderServiceImpl
类中queryOrderById
方法的代码- 修改访问的url路径:用服务名代替ip以及端口号
package at.guigu.service.impl; import at.guigu.mapper.OrderMapper; import at.guigu.po.Order; import at.guigu.po.User; import at.guigu.service.IOrderService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @Service public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService { private final RestTemplate restTemplate; public OrderServiceImpl(RestTemplate restTemplate) { this.restTemplate = restTemplate; } /** * 获取包含用户信息的订单信息 * @param orderId * @return Order */ @Override public Order queryOrderById(Long orderId) { // 1 查询订单信息 Order order = this.getById(orderId); // 2 利用RestTemplate发起Http请求来获取用户数据 /* // 2.1 设置url路径(存在url硬编码问题,不推荐) String url = "http://localhost:8081/user/" + order.getUserId(); */ // 2.1 设置url路径:用服务提供者的服务名代替ip以及端口号------解决url硬编码问题(推荐) String url = "http://userservice/user/" + order.getUserId(); // 2.2 发送Http请求,实现远程调用 User user = restTemplate.getForObject(url, User.class); // 3 封装用户数据存储到订单信息中 order.setUser(user); return order; } }
-
Step4: 给创建
RestTemplate
对应的Bean的方法添加@LoadBalanced
注解(该注解为负载均衡注解)注意:负载均衡注解
@LoadBalanced
作用是标记RestTemplate
发起的请求要被Ribbon负载均衡来拦截并处理,从而实现通过负载均衡来调用服务实例。(具体可详见负载均衡原理部分内容)-
若在服务消费者(即order-service模块)的config包下创建配置类
RemoteCallConfig
来获取的RestTemplate
对应的Bean,则在该配置类对应的方法上添加负载均衡注解@LoadBalanced
注意:
@LoadBalanced
注解存在于Ribbon客户端依赖中 若使用的是Nacos而不是Eureka时,则需要额外导入loadBalancer依赖才能使用
@LoadBalanced
注解package at.guigu.config; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; @Slf4j @Configuration public class RemoteCallConfig { // 负载均衡注解实现服务消费者对Eureka服务拉取/订阅/发现 @LoadBalanced @Bean public RestTemplate restTemplate() { log.info("restTemplate创建成功,微服务远程调用开启"); return new RestTemplate(); } }
-
若是通过服务消费者(即order-service模块)的启动类中来获取到的
RestTemplate
所对应的Bean,则在启动类中的对应方法上添加负载均衡注解@LoadBalanced
package at.guigu; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @Slf4j @SpringBootApplication public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); log.info("OrderApplication Running"); } // 微服务远程调用 // @LoadBalanced:负载均衡注解实现服务消费者对Eureka服务拉取/订阅/发现 @Bean @LoadBalanced public RestTemplate restTemplate() { log.info("restTemplate创建成功,微服务远程调用开启"); return new RestTemplate(); } }
-
-
Step5: 重启服务消费者(即order-service模块)的启动类进行测试发现:服务消费者会自动利用负载均衡来挑选一个服务实例进行远程调用,如下两图所示
注意:负载均衡规则默认为轮询规则(即依次调用服务实例) ,所以在测试访问时,8081以及8082端口均能访问到
Eureka配置文件常用相关配置
eureka:
client:
# 是否启用Eureka客户端(默认为true---启用Eureka)
enabled: true
service-url:
# 配置eureka服务端的默认地址信息
# 目的:将服务提供者(即user-service模块)的实例信息注册到Eureka服务端(即eureka-server注册中心)
defaultZone: http://127.0.0.1:10086/eureka
register-with-eureka: false # 是否将该服务的实例信息注册到Eureka(默认为true)
fetch-registry: true # 是否从注册中心获取服务实例信息(默认为true)
Nacos注册中心
注意:Nacos演示推送到了分支nacos上,可自行在Gitee上下载
- Nacos与Eureka一样,都是开源的注册中心框架,Nacos是国内产品,中文文档比较丰富,而且同时具备配置管理功能,因此在国内使用较多
Nacos下载安装与配置
Windows版本下载安装与配置
-
Step1: 通过Nacos的官网进入到GitHub,下载当前稳定版本,下载完成后将其解压到对应的目录下即可安装成功
-
Step2: 进入到nacos的安装目录下的conf目录找到applications.properties配置文件,可根据自身需求更改端口号
注意:默认端口号为8848,若该端口号已被占用,则可自行修改端口号
-
Step3: 启动nacos
注意:
1.此处只演示单击启动,后续会演示集群启动
2.方式一启动属于Nacos集群启动;方式二属于Nacos单点启动
- 方式一:进入到nacos的安装目录下的bin目录,双击startup.cmd后即可 单机 启动
-
方式二:也可在startup.cmd所在目录下进入命令行窗口输入
startup.cmd -m standalone
命令来 单机 启动Nacos -
启动成功后复制如图所示url到浏览器测试是否启动成功
-
启动成功后会自动进入到Nacos管理界面,若想要启动成功后输入用户名及密码进行登录,则:打开配置文件application.properties,修改配置文件中的用户名、密码即可
- 假设我的用户名和密码均为nacos,则此时Nacos启动成功后会先进入登录页面,如下所示
Linux版本下载安装与配置
注意:Linux版本直接从Docker官方仓库拉取对应的nacos镜像即可,步骤略,可详见Docker部分内容
-
运行并创建配置Nacos的Docker容器的代码如下:
docker run -d \ --name nacos \ --env-file ./nacos/custom.env \ -p 8848:8848 \ -p 9848:9848 \ -p 9849:9849 \ --network webb \ --restart=always \ nacos/nacos-server:v2.4.3
Nacos快速入门
-
Step1: 启动Nacos
具体可详见 Nacos下载安装与配置 部分内容
-
Step2: 在聚合工程(即父工程)cloud-demo的pom文件中添加依赖
-
SpringCloudAlibaba依赖
- 该依赖在创建聚合工程时博主已添加,若要查看SpringBoot、SpringCloud、SpringAlibaba的版本对应关系,可详见 版本兼容 部分内容
-
LoadBalancer依赖
Nacos 自2021版本开始已经没有自带ribbon的整合,所以无法通过修改Ribbon负载均衡的模式来实现nacos提供的负载均衡模式,所以就需要引入另一个支持的jar包loadbalancer来实现对
@LoadBalanced
注解以及LoadBalancer负载均衡的支持<!--LoadBalancer依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency>
-
-
Step3: 在服务提供者(即user-service模块)以及服务消费者(即order-service模块)的pom文件中添加Nacos的客户端依赖(即Nacos服务发现依赖)
<!--nacos客户端服务管理依赖(即Nacos服务发现依赖)--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency>
注意:若服务提供者(即user-service模块)以及服务消费者(即order-service模块)的pom文件中即存在Eureka依赖,又存在Nacos依赖,则必须注释掉其中一种依赖,否则运行出错,具体可详见 LoadBalancer快速入门部分内容
-
Step4: 在服务提供者(即user-service模块)的配置文件中编写Nacos配置
- 配置服务提供者或服务消费者的名称
- 由于此时user-service模块充当的是服务提供者,所以name属性值即为服务提供者名
- 配置Nacos的默认地址信息
- 目的:将服务提供者(即user-service模块)的实例信息注册到Nacos
注意:若存在Eureka地址配置,则必须注释掉
server: port: 8081 spring: application: # 指定服务提供者的名称 name: userservice # 配置数据库连接信息以及数据源信息 datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/cloud_user?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true username: root password: 123456 cloud: nacos: discovery: # 是否启用Nacos的服务注册与发现功能(默认为rue---启用Nacos) enabled: true # 设置Nacos服务端地址 server-addr: localhost:8848 mybatis-plus: configuration: # 配置MyBatisPlus的运行日志 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 是否开启下划线和驼峰映射, 默认为true map-underscore-to-camel-case: true # 实体类的别名扫描包---即设置别名 type-aliases-package: at.guigu.po global-config: db-config: # 全局id类型---即id生成策略 # id-type: auto # 更新策略,默认为not_null # update-strategy: not_null # 设置数据库表名前缀 table-prefix: tb_ logging: level: # 配置指定包及其子包下的所有类的日志级别为debug at.guigu: debug # 配置日志输出的时间戳格式 pattern: dateformat: MM-dd HH:mm:ss:SSS
- 配置服务提供者或服务消费者的名称
-
Step5: 在服务消费者(即order-service模块)的配置文件中编写Nacos配置
- 配置服务提供者或服务消费者的名称
- 由于此时order-service模块充当的是服务消费者,所以name属性值即为服务消费者名
- 配置Nacos的默认地址信息
注意:若存在Eureka地址配置,则必须注释掉
server: port: 8080 spring: application: # 指定服务消费者的名称 name: orderservice # 配置数据库连接信息以及数据源信息 datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/cloud_order?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true username: root password: 123456 cloud: nacos: discovery: # 是否启用Nacos的服务注册与发现功能(默认为rue---启用Nacos) enabled: true # 设置Nacos服务地址 server-addr: localhost:8848 mybatis-plus: configuration: # 配置MyBatisPlus的运行日志 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 是否开启下划线和驼峰映射, 默认为true map-underscore-to-camel-case: true # 实体类的别名扫描包---即设置别名 type-aliases-package: at.guigu.po global-config: db-config: # 全局id类型---即id生成策略 # id-type: auto # 更新策略,默认为not_null # update-strategy: not_null # 设置数据库表名前缀 table-prefix: tb_ logging: level: # 配置指定包及其子包下的所有类的日志级别为debug at.guigu: debug # 配置日志输出的时间戳格式 pattern: dateformat: MM-dd HH:mm:ss:SSS
- 配置服务提供者或服务消费者的名称
-
Step6: 更改服务消费者(即order-service模块)的业务层
OrderServiceImpl
类中queryOrderById
方法的代码- 修改访问的url路径:用服务名代替ip以及端口号
package at.guigu.service.impl; import at.guigu.mapper.OrderMapper; import at.guigu.po.Order; import at.guigu.po.User; import at.guigu.service.IOrderService; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; @Service public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService { private final RestTemplate restTemplate; public OrderServiceImpl(RestTemplate restTemplate) { this.restTemplate = restTemplate; } /** * 获取包含用户信息的订单信息 * @param orderId * @return Order */ @Override public Order queryOrderById(Long orderId) { // 1 查询订单信息 Order order = this.getById(orderId); // 2 利用RestTemplate发起Http请求来获取用户数据 /* // 2.1 设置url路径(存在url硬编码问题,不推荐) String url = "http://localhost:8081/user/" + order.getUserId(); */ // 2.1 设置url路径:用服务提供者的服务名代替ip以及端口号------解决url硬编码问题(推荐) String url = "http://userservice/user/" + order.getUserId(); // 2.2 发送Http请求,实现远程调用 User user = restTemplate.getForObject(url, User.class); // 3 封装用户数据存储到订单信息中 order.setUser(user); return order; } }
-
Step8: 给创建
RestTemplate
对应的Bean的方法添加@LoadBalanced
注解(该注解为负载均衡注解)注意:负载均衡注解
@LoadBalanced
作用是标记RestTemplate
发起的请求要被Nacos负载均衡来拦截并处理,从而实现通过负载均衡来调用服务实例。(具体可详见负载均衡原理部分内容)-
若在服务消费者(即order-service模块)的config包下创建配置类
RemoteCallConfig
来获取的RestTemplate
对应的Bean,则在该配置类对应的方法上添加负载均衡注解@LoadBalanced
注意:
@LoadBalanced
注解并不存在于Nacos依赖中,所在需要在服务消费者(即order-service模块)的pom文件中导入LoadBalancer
依赖,否则运行报错package at.guigu.config; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate; @Slf4j @Configuration public class RemoteCallConfig { // 负载均衡注解实现服务消费者对Nacos服务拉取/订阅/发现 @LoadBalanced @Bean public RestTemplate restTemplate() { log.info("restTemplate创建成功,微服务远程调用开启"); return new RestTemplate(); } }
-
若是通过服务消费者(即order-service模块)的启动类中来获取到的
RestTemplate
所对应的Bean,则在启动类中的对应方法上添加负载均衡注解@LoadBalanced
package at.guigu; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @Slf4j @SpringBootApplication public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); log.info("OrderApplication Running"); } // 微服务远程调用 // @LoadBalanced:负载均衡注解实现服务消费者对Nacos服务拉取/订阅/发现 @Bean @LoadBalanced public RestTemplate restTemplate() { log.info("restTemplate创建成功,微服务远程调用开启"); return new RestTemplate(); } }
-
-
Step9: 启动服务提供者(即user-service模块)以及服务消费者(即order-service模块)的启动类
-
刷新Nacos的管理页面,由该页面可看到服务注册成功
注意:此时服务消费者也会将自己的服务实例注册到Nacos中,也就是说此时,服务消费者也是服务提供者
-
-
Step10: PostMan测试,服务消费者会自动利用负载均衡来挑选一个服务实例进行远程调用,如下两图所示
注意:负载均衡规则默认为轮询规则(即依次调用服务实例) ,所以在测试访问时,8081以及8082端口均能访问到
-
注意
- 使用Nacos进行注册时,不论服务提供者还是服务消费者,它们都会被注册到Nacos,也就是说此时,它们既是服务提供者也是服务消费者
Nacos服务分级存储与集群优先
一个服务可以有多个实例,例如我们的user-service模块,可以有以下6个服务实例
- 127.0.0.1:8081
- 127.0.0.1:8082
- 127.0.0.1:8083
- 127.0.0.1:8084
- 127.0.0.1:8085
- 127.0.0.1:8086
假设以上这些服务实例分布于全国各地不同的机房中
- 127.0.0.1:8081------北京
- 127.0.0.1:8082------北京
- 127.0.0.1:8083------上海
- 127.0.0.1:8084------上海
- 127.0.0.1:8085------杭州
- 127.0.0.1:8086------杭州、
此时Nacos就会将处于同一机房或同一地区的服务实例划分为一个集群。
也就是说,user-service是服务,一个服务可以包含多个集群,如杭州、上海,每个集群下可以有多个实例,形成分级模型,如下图所示
微服务互相访问时,应该尽可能访问同集群实例,因为本地访问速度更快。当本集群内不可用时,才访问其它集群。如下图所示,杭州机房内的order-service应该优先访问同机房的user-service,若同机房的不可用才会访问上海的user-service
-
Nacos服务分级存储模型有三级
- 一级是服务
- 二级是集群
- 三级是实例
-
集群配置
-
为了更好的演示集群,博主将在创建一个服务提供者(即user-service模块)的实例信息(具体创建过程可详见 Eureka服务注册快速入门 部分内容),端口号为8083,如图所示
-
在服务提供者(即user-service模块)的配置文件中进行配置
注意:
spring.cloud.nacos.discovery
下用来设置Nacos服务注册与发现的相关配置
spring.cloud.nacos.config
下用来设置Nacos配置中心相关配置(可详见Nacos配置中心部分的内容,此处只提一下)spring: application: # 指定服务提供者的名称 name: userservice cloud: nacos: # 设置Nacos服务注册与发现相关配置 discovery: # 是否启用Nacos的服务注册与发现功能(默认为rue---启用Nacos) enabled: true # 设置Nacos服务端地址 server-addr: localhost:8848 # 配置集群名称 cluster-name: HZ
-
重新运行服务提供者(即user-service模块)的启动类,刷新Nacos的管理页面,单击对应服务的详情进入到服务详细页面,即可看user-service服务的三个实例均属于同一个集群
-
假设现在想要更改8083的集群为SH,则在服务提供者(即user-service模块)的配置文件中进行配置
spring: application: # 指定服务提供者的名称 name: userservice cloud: nacos: # 设置Nacos服务注册与发现相关配置 discovery: # 是否启用Nacos的服务注册与发现功能(默认为rue---启用Nacos) enabled: true # 设置Nacos服务端地址 server-addr: localhost:8848 # 配置集群名称 cluster-name: SH
-
然后仅仅重启端口号为8083的启动类 ,再次刷新Nacos的管理页面,单击对应服务的详情进入到服务详细页面,即可看user-service服务的两个实例分别属于不同的集群
-
为了让服务消费者(即order-service模块)远程调用服务提供者(即user-service模块)时优先选择本地集群,所以也需要为服务消费者(即order-service模块)也配置一个集群属性,步骤如下
-
在服务消费者(即order-service模块)的配置文件中进行配置
注意:此处假设服务消费者(即order-service模块)处于HZ的集群中
server: port: 8080 spring: application: # 指定服务消费者的名称 name: orderservice cloud: nacos: # 设置Nacos服务注册与发现相关配置 discovery: # 是否启用Nacos的服务注册与发现功能(默认为rue---启用Nacos) enabled: true # 设置Nacos服务地址 server-addr: localhost:8848 cluster-name: HZ
-
重新启动服务消费者(即order-service模块)的启动类,然后刷新Nacos的管理页面发现服务消费者(即order-service模块)成功加入到HZ集群
此时虽然服务消费者(即order-service模块)成功加入到HZ集群,但是还不能优先选择在同一集群中的服务消费者(即user-service模块)实例,因为负载均衡规则默认为
ZoneAvoidanceRule
,该规则并不能实现根据同集群优先来实现负载均衡;而Nacos中提供了一个NacosRule
规则的实现,可以优先从同集群中挑选实例,步骤如下 -
-
在服务消费者(即order-service模块)的配置文件中修改负载均衡规则为
NacosRule
# 自定义配置某个服务提供者的负载均衡策略,这里是userservice服务 userservice: # 指定服务提供者的服务名 ribbon: # 自定义负载均衡策略为NacosRule NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule
完整配置文件代码如下
server: port: 8080 spring: application: # 指定服务消费者的名称 name: orderservice # 配置数据库连接信息以及数据源信息 datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/cloud_order?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true username: root password: 123456 cloud: nacos: # 设置Nacos服务注册与发现相关配置 discovery: # 是否启用Nacos的服务注册与发现功能(默认为rue---启用Nacos) enabled: true # 设置Nacos服务地址 server-addr: localhost:8848 cluster-name: HZ mybatis-plus: configuration: # 配置MyBatisPlus的运行日志 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 是否开启下划线和驼峰映射, 默认为true map-underscore-to-camel-case: true # 实体类的别名扫描包---即设置别名 type-aliases-package: at.guigu.po global-config: db-config: # 全局id类型---即id生成策略 # id-type: auto # 更新策略,默认为not_null # update-strategy: not_null # 设置数据库表名前缀 table-prefix: tb_ # 自定义配置某个服务提供者的负载均衡策略,这里是userservice服务 userservice: # 指定服务提供者的服务名 ribbon: # # 自定义负载均衡策略为NacosRule NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule logging: level: # 配置指定包及其子包下的所有类的日志级别为debug at.guigu: debug # 配置日志输出的时间戳格式 pattern: dateformat: MM-dd HH:mm:ss:SSS
注意:SpringCloud2020版本之前可用Ribbon来修改负载均衡规则为
NacosRule
,若使用的是SpringCloud2020以及更新的版本,则必须利用LoadBalancer来修改负载均衡规则为NacosRule
此步骤只演示了SpringCloud2020版本之前用Ribbon来修改负载均衡规则为
NacosRule
的步骤;若使用的是SpringCloud2020以及更新的版本可详见 LoadBalancer自定义负载均衡策略 部分的内容 -
重新启动服务消费者(即order-service模块)的启动类,然后利用PostMan进行测试
-
博主进行了6次测试,由结果表明服务消费者(即order-service模块)同集群优先调用,如下两图所示
-
服务消费者(即order-service模块)并未调用处不同集群的8083端口,如下图所示
-
-
假设此时与服务消费者(即order-service模块)处于统一集群的所有服务提供者均不可用,则这个时候它才会去使用处于不同集群的服务提供者的服务实例
-
博主为了演示该步骤,将关闭8081以及8082端口对应的服务,只开启8083对应的服务,运行结果如下图所示
-
Nacos服务实例权重配置
配置完Nacos服务分级存储以及集群优先后,服务消费者会优先同集群挑选。但在实际项目中可能会由于服务器的设备性能差异,导致部分实例所在机器性能较好,另一部分较差。此时如果处于同集群的服务性能比较差,我们想要使用处于同集群中性能较好的服务时就用到了Nacos服务实例权重
-
Nacos服务实例权重可通过Nacos管理页面来设置,如下所示
-
修改完成后截图如下
如上图所示,将端口号为8081的权重设置为了0.1,此时由于处于同集群的8082权重为1,相差10倍,所以8081被访问的概率会大大降低,而8082被访问的概率会大大增加
-
利用PostMan进行6次请求测试,结果如下
-
-
注意
- 服务的默认权重大小均为1
- 若服务权重设置为0,则该服务实例永远不会被访问
- 可用于服务版本的平滑升级,用户是感知不到的
Nacos环境隔离
Nacos不仅是一个注册中心,还是一个数据中心。所以在Nacos中为了方便的做数据与服务的管理,就引入了环境隔离
-
Nacos的服务存储与数据存储的最外层都是一个名为namespace的东西,nacos中可以有多个namespace,用来彼此间的相互隔离(即用来做最外层隔离)。
- namespace内可以有一个group属性------用来将不同服务或数据进行分组
- group内可以有一个Service/Data属性------用来服务存储或数据存储
- 服务在往下就是集群,集群在往下就是实例(即服务实例)
-
默认情况下,所有的service、data、group都在同一个名为public的命名空间(即namespace)中
具体步骤如下:
-
Step1: 按如图步骤创建命名空间(即namespace)
-
Step2: 回到服务列表,单击新创建的命名空间后会自动跳转到该命名空间的页面,有图可知此时为空
-
Step3: 在服务消费者(即order-serice模块)的yml配置文件中配置命名空间
spring: application: # 指定服务消费者的名称 name: orderservice cloud: nacos: # 设置Nacos服务注册与发现相关配置 discovery: # 是否禁用Nacos的服务注册与发现功能(默认为rue) # enabled: true # 设置Nacos服务地址 server-addr: localhost:8848 cluster-name: HZ # 配置命名空间,值为命名空间的ID namespace: 90ae0c6d-aeaf-44a8-833d-40575ac14a6b
-
Step4: 重启服务消费者(即order-serice模块)的启动类后刷新Nacos的管理页面,由下图可知,此时务消费者(即order-serice模块已处于名为dev的命名空间中
-
此时由于未将服务消费者(即user-service模块)也配置到名为dev的命名空间中(如图一所示),所以利用PostMan进行测试时会报错(如图二所示),因为处于不同的命名空间时会相互隔离,无法访问
Eureka与Nacos的区别
注意:不论是使用Eureka还是Nacos,都要先实现微服务远程调用中的步骤 ,在Eureka与Nacos的入门中均省略了该部分内容的步骤,具体可详见 微服务远程调用 的内容
Nacos的服务实例分为两种l类型:
-
临时实例:如果实例宕机超过一定时间,会从服务列表剔除,默认的类型。
-
非临时实例:如果实例宕机,不会从服务列表剔除,也可以叫永久实例。
配置一个服务实例为永久实例:
spring:
cloud:
nacos:
# 设置Nacos服务注册与发现相关配置
discovery:
ephemeral: false # 设置为非临时实例
Nacos和Eureka整体结构类似,服务注册、服务拉取、心跳等待,但是也存在一些差异:
-
Nacos与eureka的共同点
- 都支持服务注册、服务周期性拉取
- 都支持服务提供者心跳方式做健康检测
-
Nacos与Eureka的区别
- Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
- 临时实例心跳不正常会被剔除,非临时实例则不会被剔除
- Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
- Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式
Nacos配置中心
Nacos除了可以做注册中心,同样可以做配置管理来使用
在Nacos配置中心的所有示例中,服务提供者和服务消费者都在命名空间dev下来演示。如何将其均移到开发环境(即dev命名空间)下可详见 Nacos环境隔离 部分的内容
本部分演示已上传至Gitee,可自行下载
统一配置管理
当在Nacos注册中心部署的微服务实例越来越多,达到数十、数百时,逐个修改微服务配置就会很容易出错,就算不出错也要在配置完成后来进行逐个重启,这样就会很麻烦。所以就引入了一种统一配置管理方案,可以集中管理所有实例的配置,同时在配置发生变更时,能够及时通知微服务,从而实现配置的热更新(即不用重启服务,配置会自动生效)
在Nacos配置中心中添加配置信息的步骤如下
-
Step1: 单击配置列表,然后按图示步骤操作
-
Step2: 单击创建配置,然后Nacos会创建一个在指定命名空间下的配置文件,然后在该配置文件中进行相关配置后单击发布即可
注意:
博主使用的是yml配置文件,而yml是yaml的缩写,所以在配置文件id的后缀名为yaml,且配置格式为yaml
Nacos配置中心的配置格式目前支持yaml以及properties
创建完成之后,Nacos配置中心的配置管理页面对应的命名空间下即出现相关配置文件,如下图所示
-
Step3: 删除服务消费者(即user-service模块)的application.yml配置文件中已配置到Nacos配置中心的配置文件中的相关热更新需求的配置代码
微服务拉取配置
在Nacos配置中心完成含有配置信息的配置文件的添加后,微服务要从Nacos管理中心拉取相关配置配件中的配置然后与本地的application.yml配置文件合并之后才能完成项目的启动
为了完成项目的启动spring引入了一种新的配置文件(即bootstrap.yaml文件),该文件会在本地的application.yml配置文件之前被读取,流程如下图所示
-
Step1: 在服务提供者(即user-service模块)的pom文件中引入nacos-config的客户端依赖
<!--nacos客户端配置管理依赖(即Nacos配置发现依赖)--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
此时服务提供者(即user-service模块)的pom文件完整代码如下
<?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> <!--nacos客户端服务管理依赖(即Nacos服务发现依赖)--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--nacos客户端配置管理依赖(即Nacos配置发现依赖)--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> </dependencies> </project>
注意:引入该依赖后必须存在bootstrap.yaml文件,否则服务无法启动成功,报错如下图所示,此时有两种解决方式:
方式一:创建bootstrap.yaml文件,并写入相关配置,具体可详见第二步
方式二:在application.yml配置文件中设置
spring.cloud.nacos.config.import-check.enabled=false
来禁用配置文件检查 -
Step2: 在服务提供者(即user-service模块)的源代码配置文件目录(即资源文件
resources
)下创建配置文件bootstrap.yml,代码如下:注意:由于博主使用的是yml格式,所以创建yml格式的bootstrap配置文件。在实际项目中可根据使用的配置文件格式来创建一样格式的bootstrap配置文件
spring: application: name: userservice # 服务名称(即Nacos配置文件id格式的服务名) profiles: active: dev #开发环境(即Nacos配置文件id格式的profile),这里是dev cloud: nacos: # 设置Nacos配置中心相关配置 config: file-extension: yaml # Nacos配置文件id格式的后缀名(即文件后缀名) server-addr: localhost:8848 # Nacos地址
注意:该配置文件的目的就是去读取Nacos配置中心的配置文件userservice-dev.yaml
-
由于Nacos配置中心的配置文件是在dev的命名空间下,所以需要在bootstrap.yaml配置文件中添加命名空间,最终该配置文件代码如下:
spring: application: name: userservice # 服务名称(即Nacos配置文件id格式的服务名) profiles: active: dev #开发环境(即Nacos配置文件id格式的profile),这里是dev cloud: nacos: # 设置Nacos配置中心相关配置 config: file-extension: yaml # Nacos配置文件id格式的后缀名(即文件后缀名) server-addr: localhost:8848 namespace: 90ae0c6d-aeaf-44a8-833d-40575ac14a6b
-
-
Step3: 删除服务提供者(即user-service模块)的application.yml配置文件中与Nacos中心配置的对应配置文件中的配置信息以及bootstrap.yaml配置文件相同部分的代码
此时服务提供者(即user-service模块)的application.yml配置文件代码如下
server: port: 8081 spring: # application: # # 指定服务提供者的名称 # name: userservice # 配置数据库连接信息以及数据源信息 datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/cloud_user?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true username: root password: 123456 cloud: nacos: # 设置Nacos服务注册与发现相关配置 discovery: # 是否禁用Nacos的服务注册与发现功能(默认为rue) enabled: true # 设置Nacos服务端地址 server-addr: localhost:8848 cluster-name: HZ namespace: 90ae0c6d-aeaf-44a8-833d-40575ac14a6b mybatis-plus: configuration: # 配置MyBatisPlus的运行日志 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 是否开启下划线和驼峰映射, 默认为true map-underscore-to-camel-case: true # 实体类的别名扫描包---即设置别名 type-aliases-package: at.guigu.po global-config: db-config: # 全局id类型---即id生成策略 # id-type: auto # 更新策略,默认为not_null # update-strategy: not_null # 设置数据库表名前缀 table-prefix: tb_ logging: level: # 配置指定包及其子包下的所有类的日志级别为debug at.guigu: debug # 配置日志输出的时间戳格式 # pattern: # dateformat: MM-dd HH:mm:ss:SSS
-
Step4: 在服务提供者(即user-service模块)的UserController方法中添加业务逻辑来测试是否能够正常读取Nacos配置中心的对应配置文件中的pattern.dateformat配置,完整代码如下:
package at.guigu.controller; import at.guigu.po.User; import at.guigu.service.IUserService; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; 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; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @RestController @RequiredArgsConstructor @RequestMapping("/user") public class UserController { private final IUserService userService; @Value("${pattern.dateformat}") private String dateformat; @GetMapping("now") public String now(){ return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat)); } @GetMapping("/{id}") public User queryUserById(@PathVariable("id") Long id) { return userService.getById(id); } }
-
重新启动服务提供者(即user-service模块)的启动类,报如下图错误
-
原因是:在SpringCloud2020版本及该版本之后已不在提供对bootstrap的支持,所以应该在服务消费者(即user-service模块)的pom文件中引入bootstrap依赖,此时完整的pom文件代码如下
<?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> <!--nacos客户端依赖(即Nacos服务发现依赖)--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> </dependency> <!--nacos配置客户端依赖(即Nacos配置发现依赖)--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency> <!--bootstrap依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bootstrap</artifactId> <version>3.1.5</version> </dependency> </dependencies> </project>
-
重新运行启动类后若仍报上图所示错误,则需要明确指定出在Nacos配置中心要使用的配置文件名称及后缀
-
解决方式:在bootstrap.yaml文件中添加代码:
spring.config.import=optional:nacos:${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
,此时bootstrap.yaml配置文件完整代码如下spring: application: name: userservice # 服务名称(即Nacos配置文件id格式的服务名) profiles: active: dev #开发环境(即Nacos配置文件id格式的profile),这里是dev cloud: nacos: # 设置Nacos配置中心相关配置 config: file-extension: yaml # Nacos配置文件id格式的后缀名(即文件后缀名) server-addr: localhost:8848 namespace: 90ae0c6d-aeaf-44a8-833d-40575ac14a6b config: import: - optional:nacos:${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
-
若仍有问题则检查bootstrap.yml配置文件中是否配置了命名空间、组等内容,若配置了就检查其与Nacos配置中心的对应配置文件是否在同一个命名空间的分组下即可
-
-
运行端口号为8081的服务消费者(即user-service模块)的启动类进行测试
- 由运行结果可知,此时已可正常读取Nacos配置中心的对应配置文件中的相关配置信息
配置热更新
在微服务拉取配置中,虽然已经实现了从Nacos配置中心来拉取对应配置文件中的配置信息,但是若Nacos配置中心的对应配置文件中的配置信息更改后,微服务是无法做到实时更新的,除非重启微服务,这在实际项目中就会造成很大的问题,为解决该问题就引入了配置热更新功能
- 配置热更新:修改nacos中的配置后,微服务中无需重启即可让配置生效,也就是配置热更新。
实现配置热更新方式一
-
Step1: 在使用
@Value
注解来注入Nacos配置中心对应的配置文件中的配置信息的类上添加@RefreshScope
,此时UserController类的完整代码如下package at.guigu.controller; import at.guigu.po.User; import at.guigu.service.IUserService; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.context.config.annotation.RefreshScope; 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; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @RestController @RequiredArgsConstructor // 实现Nacos配置中心的配置热更新 @RefreshScope @RequestMapping("/user") public class UserController { private final IUserService userService; @Value("${pattern.dateformat}") private String dateformat; @GetMapping("/now") public String now(){ return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat)); } @GetMapping("/{id}") public User queryUserById(@PathVariable("id") Long id) { return userService.getById(id); } }
-
Step2: 重启服务提供者(即user-service模块)端口为8081的启动类,然后利用PostMan进行测试
-
未更改Nacos配置中心对应的配置文件中的相关配置信息时,运行结果如下
-
现在在保证微服务不重启的情况下,更改Nacos配置中心对应的配置文件中的相关配置信息后(如图一所示),再次用PostMan进行测试,由运行结果可知(如图一所示),实现了配置的热更新
-
实现配置热更新方式二
-
Step1: 在服务提供者(即user-service模块)中创建一个config包,然后再该包下创建一个获取Nacos配置中心对应配置文件的相关配置信息的配置类
NacosConProperties
,代码如下-
使用@Component注解将该类添加到Spring容器中
-
使用@Data注解实现getter、setter方法
-
使用
@ConfigurationProperties
来指定要获取的属性的前缀名 -
在该方法内部定义要获取的属性的变量名
注意:只要
前缀名.变量名
拼接后是相关配置信息就能获取到对应的值
package at.guigu.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component @Data @ConfigurationProperties(prefix = "pattern") public class NacosConProperties { private String dateformat; }
-
-
Step2: 在服务提供者(即user-service模块)的业务逻辑相关的类UserController中依赖注入该类(即
NacosConProperties
的bean),然后利用该类来获取Nacos配置中心对应配置文件中的配置信息,代码如下package at.guigu.controller; import at.guigu.config.NacosConProperties; import at.guigu.po.User; import at.guigu.service.IUserService; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.context.config.annotation.RefreshScope; 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; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @RestController @RequiredArgsConstructor // 实现Nacos配置中心的配置热更新 @RefreshScope @RequestMapping("/user") public class UserController { private final IUserService userService; private final NacosConProperties nacosConProperties; @GetMapping("/now") public String now(){ return LocalDateTime.now().format(DateTimeFormatter.ofPattern(nacosConProperties.getDateformat())); } @GetMapping("/{id}") public User queryUserById(@PathVariable("id") Long id) { return userService.getById(id); } }
-
Step3: 重启服务提供者(即user-service模块)端口为8081的启动类,然后利用PostMan进行测试
-
未更改Nacos配置中心对应配置文件的相关配置信息时,运行结果如下
-
现在在保证微服务不重启的情况下,修改Nacos配置中心对应配置文件中的相关配置信息后(如图一所示),再次用PostMan进行测试,由运行结果可知(如图一所示),实现了配置的热更新
-
多环境配置共享
假设现在微服务有个配置信息在开发测试生产三个环境下的值是一样的,此时若在不同环境的配置文件中都进行相关配置就会造成高耦合的缺点,为避免该问题就引入了多环境配置共享
-
在实现了拉取配置以及热更新后,微服务在启动时会自动从Nacos配置中心读取多个不同格式配置文件:
- 格式一 :
${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
,比如:userservice-dev.yml、userservice-test.yml等 - 格式二 :
${spring.application.name}.${spring.cloud.nacos.config.file-extension}
,比如:userservice.yml - 不论Nacos配置中心中格式一对应的配置文件如何变化,与格式二相关的配置文件一定会被加载。因此,可以在与格式二相关的配置文件中写入多环境共享的配置信息
- 格式一 :
-
配置文件优先级
- Nacos配置中心的格式一的配置文件优先级高于格式二的配置文件优先级
- Nacos配置中心的配置文件优先级高于服务本地的配置文件优先级
实现步骤如下:
-
Step1: 在Nacos配置中心中新建一个配置文件userservice.yaml文件
-
创建完成之后Nacos配置中心的dev命名空间下就有了两种配置格式的文件
-
-
Step2: 在服务提供者(即user-service模块)获取Nacos配置中心对应配置文件的相关配置信息的配置类
NacosConProperties
中添加新的属性来获取多环境共享配置文件中的属性,代码如下package at.guigu.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; @Component @Data @ConfigurationProperties(prefix = "pattern") public class NacosConProperties { // userservice-dev.yaml文件中的属性 private String dateformat; // userservice.yaml文件中的属性 private String envSharedValue; }
-
Step3: 在服务提供者(即user-service模块)的业务逻辑相关的类UserController中依赖注入该类(即
NacosConProperties
的bean),然后利用该类来获取Nacos配置中心对应两个配置文件中的配置信息,代码如下package at.guigu.controller; import at.guigu.config.NacosConProperties; import at.guigu.po.User; import at.guigu.service.IUserService; import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.cloud.context.config.annotation.RefreshScope; 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; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; @RestController @RequiredArgsConstructor // 实现Nacos配置中心的配置热更新 @RefreshScope @RequestMapping("/user") public class UserController { private final IUserService userService; private final NacosConProperties nacosConProperties; @GetMapping("/now") public String now(){ return LocalDateTime.now().format(DateTimeFormatter.ofPattern(nacosConProperties.getDateformat())); } // 直接返回NacosConProperties类的对象给前端页面,前端会自动将其转为JSON数据 @GetMapping("nacosConProp") public NacosConProperties nacosConProperties() { return nacosConProperties; } @GetMapping("/{id}") public User queryUserById(@PathVariable("id") Long id) { return userService.getById(id); } }
-
Step4: 为了更好的演示多环境配置共享,博主将启动服务提供者(即user-service模块)端口号为8081和8082两个端口的启动类,且8081为开发环境,8082为测试环境
-
设置8081为开发环境
由于已在bootstrap.yml配置文件中设置了环境为dev开发环境,所以8081和8082启动后会默认在开发环境下,所以此处不用设置8081为开发环境,只需要更改8082为测试环境即可
-
设置8082为测试环境步骤如图所示
注意:设置8082为测试环境之后,启动8082端口的启动类后,该服务是无法读取到userservice-dev.yaml配置文件的,因为环境不同。但是它能读取到多环境共享的配置文件userservice.yaml
-
-
Step5: 启动端口号为8081和8082服务的启动类,然后利用PostMan分别测试两个端口对应的服务
-
8081读取到了Nacos配置中心的两个配置文件(即userservice-dev.yaml和userservice.yaml)中的配置信息
-
8082只读取到了Nacos配置中心的配置文件userservice.yaml中的配置信息
-
Nacos集群搭建
注意:
在之前关于Nacos的演示中使用的均为单点Nacos部署。而在实际项目中使用的基本均为Nacos集群搭建,因为这样具有高可用性
集群搭建示例已上传至nacosCon分支,可自行下载
- 原理
- 用户请求到后端后,首先会通过负载均衡器来将请求分发到不同的Nacos节点来形成一个集群结构,然后多个Nacos都访问同一个MySQL集群从而实现数据共享
- 负载均衡可通过Nginx来实现。因为Nginx不仅可以实现负载均衡,还可以实现反向代代理
为了模拟集群搭建,博主将模拟三个Nacos节点,不同Nacos节点的端口分别为:8845、8846、8847。环境模拟步骤如下
-
Step1: 将Nacos压缩包解压到任意非中文目录下
-
Step2: 将conf目录下的配置文件
cluster.conf.example
重命名为cluster.conf
,然后添加每个Nacos集群中每个Nacos节点的ip地址和端口号注意:由于博主是在本地来进行集群搭建演示的,所以ip地址均为本地ip
127.0.0.1:8845 127.0.0.1:8847 127.0.0.1:8849
-
Step3: 修改conf目录下的配置文件
application.properties
:增加支持MySQL数据源配置,添加MySQL数据源的url、用户名和密码-
模板代码如下(具体可详见官网中的集群模式部署)
注意:
1.Nacos默认有一个内嵌数据库来实现数据的存储,该内嵌数据库存在弊端(即不方便观察数据存储的基本情况),为了解决该不断就引入了内嵌数据库的功能
2.该步骤的目的是使Nacos使用外嵌数据库MySQL,方便管擦和数据存储的情况
3.不论使用的是Nacos单点模式还是集群模式,都可以使用外嵌数据库MySQL
# 指定使用mysql数据源配置(注意:在老版本中使用spring.datasource.platform=mysql) spring.sql.init.platform=mysql # 指定集群中MySQL的数量 db.num=1 # 配置数据库url以及用户密码 db.url.0=jdbc:mysql://${mysql_host}:${mysql_port}/${nacos_database}?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true db.user.0=${mysql_user} db.password.0=${mysql_password}
注意:在实际项目中可配置MySQL集群,在配置文件中用
db.url.0
表示第一个数据库;db.url.1
表示第二个数据库,依次类推。在本示例中只配置了一个外嵌数据库
-
-
Step4: 将nacos目录复制三分,分别命名为:nacos1、nacos2、nacos3
-
Step5: 分别进入到三个nacos目录下的conf目录下的配置文件application.properties,将它们的端口号分别修改为8845、8847、8849,即
server.port=端口号
-
Step6: 分别进入到nacos1、nacos2、nacos3目录下的bin目录,然后cmd打开命令行窗口,直接输入命令
startup.cmd
来启动nacos1、nacos2、nacos3注意:Linux、Windows、Ubuntu的启动方式都是不一样的,具体可详见官网中的集群模式部署
- 在启动时若报错
Address already in use: bind
,则说明端口号已被其它进程占用,可结束占用该端口的进程或修改端口号
- 在启动时若报错
-
注意:若Nacos集群启动时,本地内存不够则解决方法:利用文本编辑器打开bin目录下的
startup.cmd
,然后将Nacos集群启动时所用的内存改小即可,如图所示
集群搭建步骤如下
-
Step1: 搭建Nacos数据库,初始化数据库表结构
注意:
官网已准备了数据库表的初始化的SQL文件,可官网自行下载
本步骤详细过程省略,创建完Naocs数据库及表后,结构如下图所示
-
Step2: 配置Nacos(可详见模拟集群搭建的步骤)
-
Step3: 启动Nacos集群(可详见模拟集群搭建的步骤)
-
Step4: Nginx反向代理
-
Step4-1: 去Nginx官网下载Nginx压缩包
-
Step4-2: 将Nginx解压到自己想要安装的目录下,然后找到Nginx中的conf目录,打开nginx.conf文件
-
Step4-3: 在nginx.conf文件中配置以下代码
注意:以下代码必须粘贴到 http代码块 内部
upstream nacos-cluster { server 127.0.0.1:8845; server 127.0.0.1:8847; server 127.0.0.1:8849; } server { listen 80; server_name localhost; location /nacos { proxy_pass http://nacos-cluster; } }
-
Step4-4: 在nginx.conf文件中配置grpc通讯端口
注意:
1.以下代码与 http代码块 同级
2.在springboot3.x版本中的gRPC通讯端口存在1000的偏移量,所以需要配置一下rpc通讯端口;而在springboot2.x版本中的gRPC通讯端口不存在偏移量,所以就不需要该4-4步骤
3.该4-4步骤可先省略,若最后运行失败再添加该步骤
stream { upstream nacos-grpc{ server 127.0.0.1:9845; server 127.0.0.1:9847; server 127.0.0.1:9849; } server { listen 1080; proxy_pass nacos-grpc; } }
-
Step4-5: 然后到Nginx目录下,打开cmd命令行窗口,然后输入命令
start nginx.exe
,来启动Nginx注意:若自己已有Nginx,且已启动,则在Nginx目录下执行
nginx.exe -s reload
命令重启生效即可
-
-
Step5: 打开浏览器输入
localhost/nacos
进行测试注意:Nginx会通过负载均衡自动从Nacos集群中挑选一个Nacos节点进行访问
-
Step6: 将微服务的所有配置文件中Nacos的地址更改为
localhost:80
-
Step7: 运行服务提供者以及服务消费者的启动类,然后刷新Nacos
-
博主使用的是SpringBoot3.x版本的,假设我未配置gRPC偏移量,则运行服务提供者或服务消费者的启动类时,会报如图所示gRPC客户端错误
原因:SpringBoot3.x版本存在gRPC偏移量,且偏移量为1000。若不在Nginx的配置文件中配置gRPC偏移量的话,Nginx默认的80端口就无法映射到1080端口,导致服务提供者或服务消费者无法通过Nginx的负载均衡来添加到Nacos注册中心或配置中心上
负载均衡
负载均衡实例均已Eureka为基础进行,并未使用Nacos,不过它们的原理是一样的
-
主流的负载方案主要有两种
负载方案 解释 集中式负载均衡(服务端负载均衡) 在服务提供者和服务消费者之间使用独立的代理方式进行负载, 客户端负载均衡 客户端根据自己的请求情况自己做负载均衡,比如Ribbon - 集中式负载均衡(服务端负载均衡)的中间层可以通过硬件或软件来实现
- 若通过硬件实现,则硬件是直接跟交换机进行连接,比如:F5
- 因为任何请求进来都是先进入到交换机
- 性能高但成本也高(一线大厂用)
- 若通过软件实现,则需要先安装一个负载均衡的软件,比如:Nginx
- 性能低,但成本也低
- 若通过硬件实现,则硬件是直接跟交换机进行连接,比如:F5
- 集中式负载均衡(服务端负载均衡)的中间层可以通过硬件或软件来实现
-
常见的负载均衡算法
常见的负载均衡算法 解释 随机 通过随机选择服务进行执行,一般这种方式使用较少 轮询 负载均衡默认实现方式,请求来之后排队处理 加权轮询 通过对服务器性能的分型,给高配置,低负载的服务器分配更高的权重,均衡各个服务器的压力 地址Hash 通过客户端请求的地址的HASH值取模映射进行服务器调度。ip ->hash 最小链接数 即使请求均衡了,压力不一定会均衡,最小连接数法就是根据服务器的情况,比如请求积压数等参数,将请求分配到当前压力最小的服务器上。 最小活跃数 -
注意:
- 在设置负载均衡之前必须首先实现服务的注册与发现,具体步骤可详见 服务注册与发现 部分内容
Ribbon负载均衡
注意:Ribbon负载均衡演示在主分支上,可自行在Gitee上下载
- Spring Cloud Ribbon是基于Netflix Ribbon实现的一套客户端负载均衡工具,Ribbon客户端组件提供了一系列完善的配置,比如超时、重试等等。
- 通过Load Balancer获取到服务提供的所有机器实例后,Ribbon会自动根据某种负载均衡策略(比如:轮询、随机等)去调用这些服务
- Ribbon也可以实现我们自己的负载均衡算法
注意:Ribbon 负载均衡是由Spring Cloud Netflix 提供的客户端负载均衡器,已在新版 Spring Cloud 中被弃用,SpringCloud2020版本之前可用Ribbon负载均衡
若想要测试则对应的SpringBoot、SpringCloud、SpringCloudAlibaba的版本分别为:2.3.9.RELEASE、Hoxton.SR10、2.2.5.RELEASE
-
负载均衡流程简述
服务消费者通过url发起请求后,Ribbon负载均衡会通过负载均衡拦截器(
LoadBalancerInterceptor
)会拦截该请求,然后根据该请求中的服务名从Eureka服务端(即eureka-server注册中心)中来拉取该服务名对应的服务实例信息,最后Ribbon负载均衡会根据载均衡规则来调用服务实例负载均衡规则默认为
ZoneAvoidanceRule
,是一种轮询方案 -
负载均衡流程详述(SpringCloud2020版本之前)
服务消费者通过url发起请求后,Ribbon负载均衡会通过负载均衡拦截器(
LoadBalancerInterceptor
)会拦截该请求,然后将该请求传给Ribbon负载均衡器客户端(RibbonLoadBalancerClient
类),而Ribbon负载均衡器客户端(RibbonLoadBalancerClient
类)获取到url中的服务id(即服务名)后会将其传给动态服务列表负载均衡器(即DynamicServerListLoadBalance
)然后,动态服务列表负载均衡器(即
DynamicServerListLoadBalance
)就会根据服务名从Eureka服务端(即eureka-server注册中心)中来拉取该服务名对应的服务实例信息,并将其传给IRule
,IRule会利用内置的负载均衡规则来选取其中一个返回给负载均衡拦截器(LoadBalancerInterceptor
)负载均衡拦截器(
LoadBalancerInterceptor
)获取到真实的ip地址和端口号后就会将url替换为真实的url来访问服务提供者,从而获取到相应数据
负载均衡原理
-
负载均衡是通过
@LoadBalanced
注解实现的- 作用:标记
RestTemplate
发起的请求要被Ribbon负载均衡来拦截并处理,从而实现通过负载均衡来调用服务实例
- 作用:标记
-
拦截的动作是通过实现
ClientHttpRequestInterceptor
接口的实现类LoadBalancerInterceptor
来完成的-
ClientHttpRequestInterceptor
接口中只有一个方法ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException
,主要用来拦截RestTemplate
发起的请求 -
实现类
LoadBalancerInterceptor
中具体定义了该方法,代码如下public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException { URI originalUri = request.getURI(); String serviceName = originalUri.getHost(); Assert.state(serviceName != null, "Request URI does not contain a valid hostname: " + originalUri); return (ClientHttpResponse)this.loadBalancer.execute(serviceName, this.requestFactory.createRequest(request, body, execution)); }
-
源码跟踪
DUGEG测试:给
URI originalUri = request.getURI();
这句代码打上断点进行DEBUG测试
LoadBalancerIntercepor
-
DEBUG运行,PostMan发送请求后会自动跳转到断点处,可看出
LoadBalancerIntercepor
类(即负载均衡拦截器)拦截了用户的户的HttpRequest请求,然后在该方法内部做了如下几件事:request.getURI()
:获取请求uri,本例中就是 http://userservice/user/1originalUri.getHost()
:获取uri路径的主机名,其实就是服务id(即服务名userservice
)this.loadBalancer.execute()
:处理服务id,和用户请求。
-
运行到最后一行代码时,可看到使用的是
RibbonLoadBalancerClient
对象(Ribbon负载均衡客户端对象)的execute
方法来处理服务id和用户请求(如下图所示)注意:在Spring Cloud 2020.0版本之前使用的是
RibbonLoadBalancerClient
对象的execute
方法,但是在Spring Cloud 2020.0版本之后,RibbonLoadBalancerClient
就被弃用了,它被BlockingLoadBalancerClient
代替
LoadBalancerClient
接口的实现类RibbonLoadBalancerClient
-
进入到
execute
方法后 -
继续进入到该方法对应的重载方法execute中,然后debuge完如图所示代码后发现会获取一个动态服务列表负载均衡器对象(即
DynamicServerListLoadBalance
),该对象中包含了服务消费者实例所对应的所有端口号-
getLoadBalancer(serviceId)
:根据服务id获取ILoadBalancer
,而ILoadBalancer
会拿着服务id去eureka中获取服务列表并保存起来 -
getServer(loadBalancer)
:利用内置的负载均衡算法,从服务列表中选择一个。本例中,可以看到获取了8081端口的服务
-
-
既然获取到了端口号,那么下一行代码
Server server = getServer(loadBalancer, hint)
就是通过负载均衡来获取其中一个服务实例,所以进入到getServer
方法,发现该方法也是在RibbonLoadBalancerClient
类(Ribbon负载均衡器客户端)中,且该方法调用了chooseServer
方法
LoadBalancerClient
接口的实现类ZoneAwareLoadBalancer
-
进入到
chooseServer
方法中,进入后,发现它调用了其父类中的chooseServer
方法 -
进入到其父类的
chooseServer
方法后DEBUG到return rule.choose(key)
发现其调用rule对象中的choose方法来返回一个ZoneAvoidanceRule
对象(默认负载均衡规则对象)
IRule
接口
-
定位到
rule
发现rule
对象是IRule
接口的对象,然后查看该接口的实现类发现,其有四个实现类- 这四个实现类就是负载均衡已有的规则
- 这说明负载均衡规则由IRule接口来实现,若想要自定义负载均衡规则,则实现该接口并重写该接口中的方法即可
-
回到
Server server = getServer(loadBalancer, hint)
并DEBUG该行代码,此时会根据默认的负载均衡规则来获取到将要使用的端口号 -
然后即可使用真实的ip地址和端口号来代替url了
自定义默认负载均衡策略
-
Ribbon的负载均衡规则是通过
IRule
接口定义的,每个子接口就是一种规则,IRule
接口的关系图如下IRule
接口是所有负载均衡策略的父接口,该接口的核心方法为choose
方法,用来选择一个服务实例AbstractLoadBalancerRule
抽象类:该抽象类中定义了一个IloadBalancer
来辅助负责均衡策略选取合适的服务端实例
-
常见的负载均衡策略
默认的实现就是
ZoneAvoidanceRule
,是一种轮询方案可通过自定义负载均衡策略来指定要使用的内置负载均衡策略
内置负载均衡规则类 解释 ZoneAvoidanceRule
(默认规则)以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可以理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询。 RoundRobinRule
简单轮询服务列表来选择服务器。它是Ribbon默认的负载均衡规则。 RetryRule
重试机制的轮询选择逻辑 WeightedResponseTimeRule
为每一个服务器赋予一个权重值。服务器响应时间越长,这个服务器的权重就越小。这个规则会随机选择服务器,这个权重值会影响服务器的选择。 BestAvailableRule
忽略那些短路(即失效)的服务器,并选择并发数较低的服务器。 RandomRule
随机选择一个可用的服务器。 AvailabilityFilteringRule
对以下两种服务器进行忽略: (1)在默认情况下,这台服务器如果3次连接失败,这台服务器就会被设置为“短路”状态。短路状态将持续30秒,如果再次连接失败,短路的持续时间就会几何级地增加。 (2)并发数过高的服务器。如果一个服务器的并发连接数过高,配置了AvailabilityFilteringRule规则的客户端也会将其忽略。并发连接数的上限,可以由客户端的 <cllientName>.<clientConfigNameSpace>.ActiveConnectionsLimit
属性进行配置。RetryRule
是在RoundRobinRule
的基础上进行重试机制- 若通过
RoundRobinRule
轮询选择到的服务实例为null或已失效时,在客户端请求未超时 的情况下会重新通过RoundRobinRule
轮询选择服务
- 若通过
WeightedResponseTimeRule
- 若一个服务的平均响应时间越短则权重越大,则该服务实例被选中的概率也就越大
- 若想要手动重新配置权重策略,则需要通过nacos的
NacosRule
来设置
AvailabilityFilteringRule
:先过滤掉故障服务器,再选择并发数较低的服务器。
自定义默认负载均衡策略主要有两种方法
方法一(全局配置):在服务消费者(即order-service模块)中定义一个新的IRule接口的对象
-
方法一有两种解决方案:
-
方案一: 在config包下创建一个配置类
LoadBalancerConfig
,然后在该类中定义randomRule
方法,代码如下注意:方案一的配置类必须放在启动类所能扫描到的包下,否则全局配置就无法生效,如图所示
package at.guigu.config; import com.netflix.loadbalancer.IRule; import com.netflix.loadbalancer.RandomRule; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Slf4j @Configuration public class LoadBalancerConfig { @Bean public IRule randomRule() { log.info("成功自定义负载均衡规则"); // 返回自定义要使用的负载均衡策略的对象 return new RandomRule(); } }
-
方案二: 在服务消费者(即order-service模块)的启动类中定义
randomRule
方法,代码如下package at.guigu; import com.netflix.loadbalancer.IRule; import com.netflix.loadbalancer.RandomRule; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; @Slf4j @SpringBootApplication public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); log.info("OrderApplication Running"); } @Bean public IRule randomRule(){ // 自定义负载均衡规则 // 返回自定义要使用的负载均衡策略的对象 return new RandomRule(); } /* // 微服务远程调用 // 由于已在config包下创建相关配置类RemoteCallConfig,所以此处注掉,两者功能相同 // @LoadBalanced:负载均衡注解实现服务消费者对Eureka服务拉取/订阅/发现 @Bean @LoadBalanced public RestTemplate restTemplate() { log.info("restTemplate创建成功,微服务远程调用开启"); return new RestTemplate(); } */ }
-
方法二(局部配置)
-
方法二也有两种解决方案
-
方案一: 在全局配置的方案一中已经明确说明,如果Ribbon配置类
LoadBalancerConfig
并不在服务消费者(即user-service模块)的启动类能扫描到的包下的话,此时就变为了局部配置,如图所示-
Step1: 在启动类所能扫描到的包之外创建一个
ribbon
包,并在该包下创建一个配置类LoadBalancerConfig
,然后在该类中定义iRule
方法,代码如下注意:此时方法名必须为
iRule
,否则局部配置失效package at.guigu.config; import com.netflix.loadbalancer.IRule; import com.netflix.loadbalancer.RandomRule; import lombok.extern.slf4j.Slf4j; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Slf4j @Configuration public class LoadBalancerConfig { // 方法名必须为iRule,否则局部配置失效 @Bean public IRule iRule() { log.info("成功自定义负载均衡规则"); // 返回自定义要使用的负载均衡策略的对象 return new RandomRule(); } }
-
Step2: 给服务消费者(即order-service模块)的启动类
OrderApplication
添加@RibbonClients
并在该注解内使用@RibbonClient
注解来指定服务消费者(即user-service模块)的服务名及其对应的配置类package at.guigu; import at.ribbon.LoadBalancerConfig; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.annotation.Bean; import org.springframework.cloud.netflix.ribbon.RibbonClient; import org.springframework.cloud.netflix.ribbon.RibbonClients; @Slf4j @SpringBootApplication // 配置自定义负载均衡策略(局部配置) @RibbonClients(value = { @RibbonClient(name = "userservice", configuration = LoadBalancerConfig.class) }) public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); log.info("OrderApplication Running"); } /* // 微服务远程调用 // 由于已在config包下创建相关配置类RemoteCallConfig,所以此处注掉,两者功能相同 // @LoadBalanced:负载均衡注解实现服务消费者对Eureka服务拉取/订阅/发现 @Bean @LoadBalanced public RestTemplate restTemplate() { log.info("restTemplate创建成功,微服务远程调用开启"); return new RestTemplate(); } */ }
-
-
方案二: 在服务消费者(即order-service模块)的配置文件application.yml中配置自定义默认负载均衡规则
# 自定义配置某个服务提供者的负载均衡策略,这里是userservice服务 userservice: # 指定服务提供者的服务名 ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 自定义默认的负载均衡规则
服务消费者(即order-service模块)的配置文件application.yml完整代码如下:
server: port: 8080 spring: application: # 指定服务提供者/服务消费的名称 name: orderservice # 配置数据库连接信息以及数据源信息 datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/cloud_order?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true username: root password: 123456 mybatis-plus: configuration: # 配置MyBatisPlus的运行日志 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 是否开启下划线和驼峰映射, 默认为true map-underscore-to-camel-case: true # 实体类的别名扫描包---即设置别名 type-aliases-package: at.guigu.po global-config: db-config: # 全局id类型---即id生成策略 # id-type: auto # 更新策略,默认为not_null # update-strategy: not_null # 设置数据库表名前缀 table-prefix: tb_ eureka: client: service-url: # 配置eureka服务端的默认地址信息 # 目的:将服务提供者(即user-service模块)的实例信息注册到Eureka服务端(即eureka-server注册中心) defaultZone: http://127.0.0.1:10086/eureka register-with-eureka: false # 是否将该服务的实例信息注册到Eureka(默认为true) fetch-registry: true # 是否从注册中心获取服务实例信息(默认为true) # 自定义配置某个服务提供者的负载均衡策略,这里是userservice服务 userservice: # 指定服务提供者的服务名 ribbon: NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 自定义负载均衡策略为随机策略 logging: level: # 配置指定包及其子包下的所有类的日志级别为debug at.guigu: debug # 配置日志输出的时间戳格式 pattern: dateformat: MM-dd HH:mm:ss:SSS
-
-
注意
-
方法一是全局配置:在服务消费者(即order-service模块)中调用任何服务消费者都会使用该自定义的负载均衡策略
-
方法二是局部配置:在服务消费者(即order-service模块)中调用的任何服务提供者若不在进行自定义负载均衡策略配置,则会使用默认的负载均衡策略
-
一般使用默认的负载均衡策略即可,除非有特定要求时才做更改
-
在实际项目中,若通过代码方式自定义则需要重新打包发布;若通过配置文件方式则不需要重新打包发布,只是说无法做到全局配置而已
-
自定义负载均衡策略
在自定义默认负载均衡策略中使用的都是已经存在的负载均衡策略,本部分内容将自己定义新的负载均衡策略来使用
-
Step1: 在ribbon包下创建自定义负载均衡策略的类
RibbonRandomRuleConfig
- Step1-1: 继承实现了
IRule
接口的AbstractLoadBalancerRule
抽象类 - Step1-2: 重写
choose
方法来自定义负载均衡策略(该方法为核心方法,用来选根据负载均衡策略择一个服务实例)
package at.ribbon; import com.netflix.client.config.IClientConfig; import com.netflix.loadbalancer.AbstractLoadBalancerRule; import com.netflix.loadbalancer.ILoadBalancer; import com.netflix.loadbalancer.Server; import java.util.List; import java.util.concurrent.ThreadLocalRandom; public class RibbonRandomRuleConfig extends AbstractLoadBalancerRule { // 自定义负载均衡策略 @Override public Server choose(Object o) { // 获取负载均衡器对象(该对象中包含了服务实例列表) ILoadBalancer loadBalancer = this.getLoadBalancer(); // 获取当前请求的服务提供者的实例列表 List<Server> reachableServers = loadBalancer.getReachableServers(); Server server = null; do { // 自定义随机负载均衡策略 int random = ThreadLocalRandom.current().nextInt(reachableServers.size()); server = reachableServers.get(random); } while (!(server.isAlive())); return server; } // 初始化配置 @Override public void initWithNiwsConfig(IClientConfig iClientConfig) { } }
- Step1-1: 继承实现了
后续步骤按照 自定义默认负载均衡策略 的步骤即可,可将该自定义负载均衡策略配置为全局配置或局部配置,此处只进行局部配置的配置文件形式的演示
-
Step2: 在服务消费者(即order-service模块)的配置文件application.yml中配置自定义负载均衡规则
# 自定义配置某个服务提供者的负载均衡策略,这里是userservice服务 userservice: # 指定服务提供者的服务名 ribbon: NFLoadBalancerRuleClassName: at.guigu.ribbon.RibbonRandomRuleConfig # 自定义负载均衡规则
服务消费者(即order-service模块)的配置文件application.yml完整代码略
懒加载与饥饿加载
-
懒加载
- 定义:即延迟资源或对象的初始化 ,直到它被真正需要时才进行加载,而不是在系统启动或对象创建时立即加载
- Ribbon默认采用的就是懒加载 ,在第一次访问时才会去创建
LoadBalanceClient
,请求时间会比较长,但在后续访问时,请求时间就会缩短 - 优点
- 节省资源:延迟加载避免了不必要的资源占用,尤其在服务实例较多但使用不频繁的情况下。
- 提升启动性能:系统启动时无需加载所有服务和实例信息,从而加快启动速度。
- 按需加载:只有在需要时才加载资源或服务,可以更灵活地应对动态变化。
- 缺点
- 首次调用延迟:由于资源只有在第一次使用时才加载,可能会导致首次调用的延迟。
- 复杂性增加:实现懒加载需要额外的逻辑控制,可能增加代码复杂性。
- 潜在的异常处理:如果懒加载的资源加载失败,可能会导致调用失败,需要额外的异常处理机制。
-
饥饿加载
- 定义:在系统启动或对象创建时立即加载所有必要的资源或对象 ,不论这些资源是否会被马上使用。这种方式旨在提前完成资源准备,以减少后续的访问延迟。
- 优点
- 避免首次调用延迟访问:所有资源在启动阶段已加载,后续使用时无需等待。
- 逻辑简单:实现逻辑较为直接,不需要动态判断或控制加载时机。
- 一致性高:所有资源在启动时就已准备好,减少运行时的资源不可用风险
- 缺点
- 启动时间长:因为所有资源在系统启动时加载,会增加初始化的耗时。
- 资源浪费:如果某些资源从未使用过,提前加载可能导致资源浪费。
- 内存压力大:大量资源在启动时同时加载,可能会占用较多内存或计算资源。
-
将Ribbon改为饥饿加载
-
在服务消费者(即order-service模块)的配置文件application.yml中更改指定服务提供者的加载方式为饥饿加载
- Step1: 开启饥饿加载
- Step2: 指定饥饿加载的服务提供者的服务名称
ribbon: eureka: # 开启饥饿加载(默认为false,即未开启) enabled: true # 指定饥饿加载的服务提供者的服务名称 clients: userservice
若指定多个服务提供者,则方式如下
ribbon: eureka: # 开启饥饿加载 enabled: true # 指定饥饿加载的服务提供者的服务名称 clients: - userservice - xxxservice
-
LoadBalancer负载均衡
注意:LoadBalancer负载均衡推送到了分支master02上,可自行在Gitee上下载
Spring Cloud LoadBalancer是由Spring Cloud 提供的官方负载均衡实现,从 Spring Cloud 2020 开始推荐使用,用来替代Ribbon
-
Spring Cloud LoadBalancer 提供了两种负载均衡的客户端
- RestTemplate:RestTemplate是sping提供的用于访问Rest服务的客户端
- RestTemplate 提供了多种便捷访问远程Http服务的方法,能够大大提高客户端的编写效率,默认情况下,RestTemplate 默认依赖idK的HTTP连接工具。
- WebClient:WebClient是从sping webFlux5.0版本开始提供的一个非阻塞的基于响应式编程的进行Http请求的客户端工具
- 它的响应式编程是基于Reactor的。WebClient中提供了标准Http请求方式对应的get、post、put、delete等方法,可以用来发起相应的请求。
- RestTemplate:RestTemplate是sping提供的用于访问Rest服务的客户端
-
Spring Cloud LoadBalancer 仅支持两种负载均衡策略
- 轮询策略
RoundRobinLoadBalancer
------默认 - 随机选择策略
RandomLoadBalancer
- 轮询策略
快速入门
-
Step1: 若服务消费者(即order-service模块)的pom文件中存在nacos服务发现依赖:
spring-cloud-starter-alibaba-nacos-discovery
,则利用<exclusions>
标签排除Ribbon注意:若不存在该依赖,则跳过第一步即可
<!--nacos服务发现依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <exclusions> <!--排除Ribbon--> <exclusion> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </exclusion> </exclusions> </dependency>
-
Step2: 在服务消费者(即order-service模块)的pom文件中添加LoadBalancer依赖
<!--LoadBalancer依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency>
-
完整pom文件代码如下
<?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>order-service</artifactId> <!--配置依赖--> <dependencies> <!--Eureka客户端依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!--nacos服务注册的依赖--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <exclusions> <!--排除Ribbon--> <exclusion> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-ribbon</artifactId> </exclusion> </exclusions> </dependency> <!--LoadBalancer依赖--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-loadbalancer</artifactId> </dependency> </dependencies> </project>
-
-
Step3: 在服务消费者(即order-service模块)的配置文件application.yml中配置Ribbon为禁用状态
SpringCloud2020版本之后就不需要在配置文件application.yml中配置Ribbon为禁用状态,不过还是最好加上该步骤
spring: cloud: loadbalancer: ribbbon: # 禁用Ribbon enabled: false
-
完整配置文件代码如下
server: port: 8080 spring: application: # 指定服务提供者/服务消费的名称 name: orderservice # 配置数据库连接信息以及数据源信息 datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/cloud_order?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true username: root password: 123456 cloud: loadbalancer: ribbbon: # 禁用Ribbon enabled: false mybatis-plus: configuration: # 配置MyBatisPlus的运行日志 log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 是否开启下划线和驼峰映射, 默认为true map-underscore-to-camel-case: true # 实体类的别名扫描包---即设置别名 type-aliases-package: at.guigu.po global-config: db-config: # 全局id类型---即id生成策略 # id-type: auto # 更新策略,默认为not_null # update-strategy: not_null # 设置数据库表名前缀 table-prefix: tb_ eureka: client: service-url: # 配置eureka服务端的默认地址信息 # 目的:将服务提供者(即user-service模块)的实例信息注册到Eureka服务端(即eureka-server注册中心) defaultZone: http://127.0.0.1:10086/eureka register-with-eureka: false # 是否将该服务的实例信息注册到Eureka(默认为true) fetch-registry: true # 是否从注册中心获取服务实例信息(默认为true) logging: level: # 配置指定包及其子包下的所有类的日志级别为debug at.guigu: debug # 配置日志输出的时间戳格式 pattern: dateformat: MM-dd HH:mm:ss:SSS
-
-
运行后可能后报如下错误,原因:在服务消费者(即order-service模块)的pom文件中导入了两种负载均衡(即Ribbon和LoadBalancer)的依赖(即Eureka和Nacos)
-
解决方式
-
方式一:删除掉其中一个依赖,保留要使用的依赖(即Eureka服务与Nacos服务选用一个即可,否则会有冲突)
-
方式二:在配置文件中禁用Eureka或nacos的服务发现功能
# 禁用eureka服务发现,启用 Nacos服务发现 spring.cloud.nacos.discovery.enabled: true eureka.client.enabled: false # 启用eureka服务发现,禁用 Nacos服务发现 spring.cloud.nacos.discovery.enabled: false eureka.client.enabled: true
-
自定义默认负载均衡策略
- Spring Cloud LoadBalancer 仅支持两种负载均衡策略
- 轮询策略
RoundRobinLoadBalancer
------默认 - 随机选择策略
RandomLoadBalancer
- 轮询策略
自定义LoadBalancer的默认负载均衡策略只有一种方法,且只能进行局部配置
-
Step1: 在启动类所能扫描到的包之外创建一个
loadbalancer
包,并在该包下创建一个配置类LoadBalancerConfig
,然后在该类中定义randomLoadBalancer
方法,代码如下package at.loadBalancer; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer; import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer; import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; @Slf4j @Configuration public class LoadBalancerConfig { @Bean public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment, LoadBalancerClientFactory LoadBalancerClientFactory) { String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); log.info("自定义默认负载均衡成功"); return new RandomLoadBalancer(LoadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name); } }
-
Step2: 给服务消费者(即order-service模块)的启动类
OrderApplication
添加@LoadBalancerClients
并在该注解内使用@LoadBalancerClient
注解来指定服务消费者(即user-service模块)的服务名及其对应的配置类package at.guigu; import at.loadBalancer.LoadBalancerConfig; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient; import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @Slf4j @LoadBalancerClients({@LoadBalancerClient(name = "userservice", configuration = LoadBalancerConfig.class )}) @SpringBootApplication public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); log.info("OrderApplication Running"); } /* // 微服务远程调用 // 由于已在config包下创建相关配置类RemoteCallConfig,所以此处注掉,两者功能相同 // @LoadBalanced:负载均衡注解实现服务消费者对Eureka服务拉取/订阅/发现 @Bean @LoadBalanced public RestTemplate restTemplate() { log.info("restTemplate创建成功,微服务远程调用开启"); return new RestTemplate(); } */ }
自定义负载均衡策略
由自定义默认负载均衡策略可知,若想要修改默认的负载均衡策略,必须要实现 自定义默认负载均衡策略 中的两个步骤
但是自定义负载均衡策略属于开发者自己定义一个新的负载均衡规则
-
自定义负载均衡策略时我们可以对照
RoundRobinLoadBalancer
轮询策略以及RandomLoadBalancer
随机选择策略的实现由图可知,两个负载均衡策略的类均实现了
ReactorServiceInstanceLoadBaancer
接口,且都有choose
方法-
进入到
ReactorServiceInstanceLoadBaancer
接口,发现该接口是继承ReactorLoadBalancer<T>
接口,且泛型为服务实例接口(即ServiceInstance
),如图所示 -
继续进入到
ReactorLoadBalancer<T>
接口,发现该接口继承ReactiveLoadBalancer<T>
接口 -
继续进入到
ReactiveLoadBalancer<T>
接口可知,该接口即为顶层接口,且choose方法由该接口定义
-
经过以上分析可知,若想要自定义负载均衡策略,只需要创建一个实现了
ReactorServiceInstanceLoadBalancer
接口的方法即可,实现同集群权重优先调用的步骤如下:
-
Step1: 在启动类所能扫描到的包之外创建一个
loadbalancer
包,并在该包下创建一个实现ReactorServiceInstanceLoadBalancer
接口的实现类NacosSameClusterWeightedRule
,代码如下package at.loadbalancer; import java.math.BigDecimal; import java.util.*; import java.util.stream.Collectors; import com.alibaba.cloud.nacos.NacosDiscoveryProperties; import com.alibaba.nacos.api.naming.pojo.Instance; import com.alibaba.nacos.api.utils.StringUtils; import com.alibaba.nacos.client.naming.core.Balancer; import jakarta.annotation.Resource; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.ObjectProvider; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.loadbalancer.DefaultResponse; import org.springframework.cloud.client.loadbalancer.EmptyResponse; import org.springframework.cloud.client.loadbalancer.Request; import org.springframework.cloud.client.loadbalancer.Response; import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier; import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer; import org.springframework.cloud.loadbalancer.core.SelectedInstanceCallback; import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; import reactor.core.publisher.Mono; @Slf4j // 自定义负载均衡实现需要实现 ReactorServiceInstanceLoadBalancer 接口 以及重写choose方法 public class NacosSameClusterWeightedRule implements ReactorServiceInstanceLoadBalancer { // 注入当前服务的nacos的配置信息 @Resource private NacosDiscoveryProperties nacosDiscoveryProperties; // loadbalancer 提供的访问当前服务的名称 final String serviceId; // loadbalancer 提供的访问的服务列表 ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider; public NacosSameClusterWeightedRule(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) { this.serviceId = serviceId; this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider; } /** * 服务器调用负载均衡时调的放啊 * 此处代码内容与 RandomLoadBalancer 一致 */ public Mono<Response<ServiceInstance>> choose(Request request) { ServiceInstanceListSupplier supplier = this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new); return supplier.get(request).next().map((serviceInstances) -> { return this.processInstanceResponse(supplier, serviceInstances); }); } /** * 对负载均衡的服务进行筛选的方法 * 此处代码内容与 RandomLoadBalancer 一致 */ private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances) { Response<ServiceInstance> serviceInstanceResponse = this.getInstanceResponse(serviceInstances); if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) { ((SelectedInstanceCallback)supplier).selectedServiceInstance((ServiceInstance)serviceInstanceResponse.getServer()); } return serviceInstanceResponse; } /** * 对负载均衡的服务进行筛选的方法 * 自定义 * 此处的 instances 实例列表 只会提供健康的实例 所以不需要担心如果实例无法访问的情况 */ private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) { if (instances.isEmpty()) { return new EmptyResponse(); } // 获取当前服务所在的集群名称 String currentClusterName = nacosDiscoveryProperties.getClusterName(); // 过滤在同一集群下注册的服务 根据集群名称筛选的集合 List<ServiceInstance> sameClusterNameInstList = instances.stream().filter(i-> StringUtils.equals(i.getMetadata().get("nacos.cluster"),currentClusterName)).collect(Collectors.toList()); ServiceInstance sameClusterNameInst; if (sameClusterNameInstList.isEmpty()) { // 如果为空,则根据权重直接过滤所有服务列表 sameClusterNameInst = getHostByRandomWeight(instances); } else { // 如果不为空,则根据权重直接过滤所在集群下的服务列表 sameClusterNameInst = getHostByRandomWeight(sameClusterNameInstList); } return new DefaultResponse(sameClusterNameInst); } private ServiceInstance getHostByRandomWeight(List<ServiceInstance> sameClusterNameInstList){ List<Instance> list = new ArrayList<>(); Map<String,ServiceInstance> dataMap = new HashMap<>(); // 此处将 ServiceInstance 转化为 Instance 是为了接下来调用nacos中的权重算法,由于入参不同,所以需要转换,此处建议打断电进行参数调试,以下是我目前为止所用到的参数,转化为map是为了最终方便获取取值到的服务对象 sameClusterNameInstList.forEach(i->{ Instance ins = new Instance(); Map<String, String> metadata = i.getMetadata(); ins.setInstanceId(metadata.get("nacos.instanceId")); ins.setWeight(new BigDecimal(metadata.get("nacos.weight")).doubleValue()); ins.setClusterName(metadata.get("nacos.cluster")); ins.setEphemeral(Boolean.parseBoolean(metadata.get("nacos.ephemeral"))); ins.setHealthy(Boolean.parseBoolean(metadata.get("nacos.healthy"))); ins.setPort(i.getPort()); ins.setIp(i.getHost()); ins.setServiceName(i.getServiceId()); ins.setMetadata(metadata); list.add(ins); // key为服务ID,值为服务对象 dataMap.put(metadata.get("nacos.instanceId"),i); }); // 调用nacos官方提供的负载均衡权重算法 Instance hostByRandomWeightCopy = ExtendBalancer.getHostByRandomWeightCopy(list); // 根据最终ID获取需要返回的实例对象 return dataMap.get(hostByRandomWeightCopy.getInstanceId()); } } class ExtendBalancer extends Balancer { /** * 根据权重选择随机选择一个 */ public static Instance getHostByRandomWeightCopy(List<Instance> hosts) { return getHostByRandomWeight(hosts); } }
-
Step2: 继续在
loadbalancer
包下创建一个配置类LoadBalancerConfig
,然后在该类中定义randomLoadBalancer
方法,代码如下package at.loadbalancer; import at.loadbalancer.NacosSameClusterWeightedRule; import lombok.extern.slf4j.Slf4j; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer; import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.Environment; @Slf4j @Configuration public class LoadBalancerConfig { @Bean public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(Environment environment, LoadBalancerClientFactory LoadBalancerClientFactory) { String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); log.info("自定义默认负载均衡成功"); return new NacosSameClusterWeightedRule(LoadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name); } }
-
Step3: 给服务消费者(即order-service模块)的启动类
OrderApplication
添加@LoadBalancerClients
并在该注解内使用@LoadBalancerClient
注解来指定服务消费者(即user-service模块)的服务名及其对应的配置类package at.guigu; import at.loadbalancer.LoadBalancerConfig; import lombok.extern.slf4j.Slf4j; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient; import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients; import org.springframework.context.annotation.Bean; import org.springframework.web.client.RestTemplate; @Slf4j @LoadBalancerClients({@LoadBalancerClient(name = "userservice", configuration = LoadBalancerConfig.class )}) @SpringBootApplication public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); log.info("OrderApplication Running"); } /* // 微服务远程调用 // 由于已在config包下创建相关配置类RemoteCallConfig,所以此处注掉,两者功能相同 @Bean public RestTemplate restTemplate() { log.info("restTemplate创建成功,微服务远程调用开启"); return new RestTemplate(); }*/ }
-
自定义LoadBalancer负载均衡策略来源于https://www.jianshu.com/p/3b26ebe4ab3e