目录
3、拷贝hmall-service的相关文件倒item-service
restTemplate:小需求可以用到,后面以Feign为主。但是我还是完整地编写了下来,可以当底层原理学习使用。
问题分析:restTemplate存在的问题主要是到时候负载均衡(并发处理)的时候无法同时请求多个ip地址,后面需要服务治理来解决。
调用SpringMVC的拦截器获取用户保存到ThreadLocal(给其他服务拿取)
一、导入黑马商城
1、后端项目
1、复制代码到自己的代码资料区。alt+8打开Services点击下面的蓝色内容选择springboot
记得把虚拟机地址改成自己的。
选择后先不要运行,
修改完成后直接启动服务即可。遇到端口号占用问题,可以参考端口占用。
然后浏览器输入地址测试即可
2、前端项目
nginx不能在中文或者特殊符号路径运行,要拷贝到另一个地方(例如lesson目录)。
另外,不要双击nginx.exe,因为后面不方便关闭。
我们可以在hmall-nginx文件里打开控制台
start nginx
启动失败可以看nginx目录下的logs文件中的内容
我遇到的事80端口被占用的问题,uu们有问题的话可以去网上找解决方案,一般是修改端口范围啥的,我这里为了贪图方便就直接改变了nginx.conf里的80端口为90,然后再浏览器输入地址即可访问黑马商城。
然后还是跟之前一样登录测试,账号jack,密码123。登录报错的可以改jdk11,maven改成自己的。
因为我们之前已经配置springboot项目启动的是local文件,所以我们还要启动虚拟机里面的mysql,
如果报错的话可以重启一下docker
systemctl restart docker
再进行登录测试就可以进来啦~
二、压力测试
1、jmeter软件模拟并发
可以再官网下载,也可以由我分享的网盘下载。
通过网盘分享的文件:apache-jmeter-5.4.1.zip
链接: https://pan.baidu.com/s/1xyApIc8IxCt0a2EuVo4F2Q 提取码: 4567
发现打不开一直是白屏的话直接点击/bin目录下的jmeter.bat,会弹出命令窗和软件(窗口不要关掉,否则软件也会关掉)。
先进入该网址(记得端口号问题,docker和mysql记得开)
localhost:8080/search/list?pageNo=1&pageSize=2
把准备好的黑马商城.jmx拖进jmeter工作区,点击开始,可以进行对比压测前后的延时
可以设置进程数模拟更高的并发
可以看出单体架构有服务崩溃的风险,因此我们要采用微服务架构,保证整体服务不会蚁穴溃堤!
三、搭建微服务架构
1、相关科普介绍
微服务架构,是服务化思想指导下的一套最佳实践架构方案。服务化,就是把单体架构中的功能模块拆分为多个独立项目。
拆分成独立项目,不是一个小项目一个controller,而是一个小项目单独负责一个功能,例如增删改查,登录注册。
各个微服务再各自打包成jar包在tomcat服务器上运行,各自搭配一个mysql数据库。
spring的springboot框架优势:自动装配和依赖管理
spring cloud是封装了各个组件。
2、熟悉项目功能
还是跟之前一样的方式打开nginx,访问localhost:18080端口然后进行登录,查看req和storage。
3、购物车订单支付等
4、商品服务拆分
第一种方式是Project(大工程),第二种方式是maven聚合(用的较多)
1、maven聚合:
父工程子工程,一般在父工程下面创建子工程。
hmall目录下创建新module(item-service),记得选用maven(模版可以选用quickstart)
接下来就是拷贝依赖(可以复制hmall-service的pom文件的全部depedencies和buidl)
然后选择性干掉不用到的服务(这里只要商品服务)
单元测试也不用(因为父工程有)
redis也可以不要,到时用再引回来
maven打包依赖就保留(很重要)
然后就引入点击maven图标。
2、相关配置修改
引用hm-service的启动类并改名字为ItemApplication,文件名修改(current directory)(建议难改的话直接把文件删掉重建)
启动类修改下,然后记得创建不同文件夹
记得在main目录下创建resources文件夹,点击maven source directories下面提示即可。
在hmall-service的resources目录下拷一下这三个,下面的hmall.jks是加密用到的,不用拷
修改applicatoin.yaml,主要修改端口号,微服务名字和mysql端口号(这里改数据库名就好)
一个微服务对应一台mysql成本有点高,我们可以在一个mysql里创建多个database来模拟
引入资料中的hmall-item.sql,直接运行
改一下swagger的接口文档名字和controller信息
Swagger扫描到Controller,会把Controller接口信息作为接口文档信息
下面的hm登录加密可以全部去掉
3、拷贝hmall-service的相关文件倒item-service
拷贝顺序:domain->mapper->service->controller
找与item有关的拷贝下来
ItemServiceImpl的一个地方需要修改:
依赖错误问题:把爆红的引入删掉就行,idea会自动导入
启动
Alt+8打开services:
第一种方法:刷新maven
第二种方法:右击HmallApplication点击Copy Configuration
输入一个名字(例:ItemApplicatoin)
然后找到ItemApplication启动类即可
先别急着启动!!关键一步!!
右击新创建的Application,点击Edit Configuration,设置为local
apply应用后直接启动即可,就可以完成多端口启动微服务。
上面的8080端口可以先关掉。
4、访问swagger测试
5、购物车服务拆分
1、前置工程
(1)创建工程等服务跟上一步相同,照搬即可,命名为cart-service。
(2)直接引入已经经过筛选的依赖(从item-service中全部引进来)
(3)设置启动类,跟之前的操作一样
(4)增加resources文件夹和增加三个配置文件,跟之前的操作一样,无非就是修改端口号、微服务名称、数据库database名、swagger文档说明、controller包扫描位置...
在Impl类中有一些地方需要修改:
先注释掉商品服务(得到差价)和登录用到的拦截器
拷贝完大致如图:
2、运行
alt+8然后记得修改配置为local
3、访问swagger测试
端口号是8082哦
业务成功但是不完整(没有显示商品信息),我们这就来实现完整功能
四、远程调用
1、restTemplate(了解)
需求:请求另一个商品管理的微服务
原理:模拟前端向后端请求,java应用之间也能调用请求
restTemplate:小需求可以用到,后面以Feign为主。但是我还是完整地编写了下来,可以当底层原理学习使用。
打开impl的方法注释,引入ItemDto
在启动类注入RestTemplate的bean注入到spring的bean工厂中。
在实现类引入RestTemplate:
不推荐用@autowired而是用构造函数(但要是要引入的东西很多的话就很繁琐了),所以用lombok的@requiredargsconstructor更方便。
为了防止一些常变量也被当做构造初始化的成员变量,我们在resttemplate前加上final就行了,保证一定要被初始化(requiredargsconstructor)
修改如下:
代码就贴在这里了,exchange那个选有url,method之类的api
// 2.查询商品
// List<ItemDTO> items = itemService.queryItemByIds(itemIds);
// 2.1.利用restTemplate发起http请求,得到http的响应
ResponseEntity<List<ItemDTO>> response = restTemplate.exchange(
"http://localhost:8081/items?ids={ids}",
HttpMethod.GET,
null,
new ParameterizedTypeReference<List<ItemDTO>>() {
},
Map.of("ids", CollectionUtil.join(itemIds, ","))
);
// 2.2.解析相应
if(!response.getStatusCode().is2xxSuccessful()){
//查询失败,直接结束
return;
}
List<ItemDTO> items = response.getBody();
把id拼接成字符串:CollectionUtils.join(ids,",");记得是hutool包下的工具类,以逗号分隔。
重启cart和item两个微服务,然后再接口文档测试,可以看到,已经能查询到商品信息了!!
测试newPrice,修改原来的price,记得保存修改。
可以看到,newPrice已经不一样了,说明接口调用成功。
问题分析:restTemplate存在的问题主要是到时候负载均衡(并发处理)的时候无法同时请求多个ip地址,后面需要服务治理来解决。
2、注册中心
原理:类似生活中的家政公司。
为什么使用nacos?因为不管是nacos还是eruka,都遵循springcloud标准,用起来差不多,而且nacos是阿里巴巴产品,中文文档更容易看懂,这里我们可以参考语雀里的文档:Docs
配置nacos
把资料中的nacos.sql拖进mysql可视化软件(如Navicat)中
接下来就是在docker中部署nacos,等下要引用资料nacos中的custom.env(主要是配置mysql),记得修改里面的虚拟机ip地址。
安装nacos镜像文件(官方下载比较慢,资料中有nacos.tar)
把nacos文件夹(里面有custom.env)和nacos.tar引入虚拟机/root目录
然后加载镜像
docker load -i nacos.tar
输入dis命令查看镜像状态
运行指令即可
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
dps查看容器状态
不放心的话可以查看日志
docker logs -f nacos
访问nacos
浏览器输入虚拟机ip地址+8848(nacos地址)/nacos
进入后会是一个登录界面,账号和密码都是nacos,输入即可。
然后我们就成功进来啦~
3、服务治理
服务注册
在item-service中引入nacos discovery依赖
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
配置地址(快速:输入nacos找到有addr的那一栏)
spring:
cloud:
nacos:
server-addr: 192.168.150.101:8848
两个步骤都做完之后就已经完成了服务注册,接下来我们模拟一下多实例部署
alt+8,右击item-service,点击copy configuration,命名为ItemApplicatoin2即可,然后不要立即点OK,线上部署不同台机器,端口号一定不一样,但是我们是在idea部署,要修改端口号,怎么修改呢?中间那里有个Modify options,点击Add VM options,中间输入-Dserver.port=8083
-Dserver.port=8083
apply应用即可。然后启动该服务:
查看日志可以发现,服务一旦启动就已经开始注册了。
我们再到nacos控制台可以看到服务已经注册成功了。
实例数:同一个服务有两个实例。健康实例数:可以用的实例。
详情点进去可以发现有两个端口号:
服务发现
在cart-service中操作:
在cart-service中引入nacos discovery依赖
<!--nacos 服务注册发现-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
配置地址(快速:输入nacos找到有addr的那一栏)
spring:
cloud:
nacos:
server-addr: 192.168.150.101:8848
改造之前的restTemplate方法
在impl实现类中引入discoveryClient来完成服务的拉取
方法修改前(上)与修改后(下),可以观察到地址已经不是写死的了,这是划时代的改变!
// 2.查询商品
// List<ItemDTO> items = itemService.queryItemByIds(itemIds);
// 2.1.根据服务名称获取服务的实例列表
List<ServiceInstance> instances = discoveryClient.getInstances("item-service");
if(CollectionUtil.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", CollectionUtil.join(itemIds, ","))
);
知识点:手写负载均衡算法instances.get(RandomUtil.RandomInt(instances.size()))
然后我们重启CartApplication,再次请求购物车数据,目的:两个服务都有请求到。
先清空两个服务的日志,然后调用接口(第一次调用有点慢,需要多调用几次),看有无日志刷新即可。
可以看到两个服务都有日志输出,说明负载均衡已经实现了!!
模拟服务宕机:暂停8081服务,到nacos查看item-service详情(可以看到只有一个端口8083了),再次请求购物车数据,看是否还能查询得到数据
可以看到还是可以请求到数据的,我们再反向操作启动服务到nacos中查看......
可以看到不管是服务宕机还是服务启动,随时都能被我们感应到,随时都能拿到最新的服务列表,这就是服务发现的强大之处。不用写死地址,而是动态感知服务的地址。
问题集合
如果发现一直连不上,就两条指令重复输入
docker restart mysql
docker restart nacos
4、OpenFeign
前面写的请求调用步骤太繁琐了,我们需要进一步优化代码,因此改用OpenFeign(声明式)。
快速入门
引入注解、注解启动类
等下需要引入两个依赖和在启动类声明一下注解
之前的负载均衡是Ribbon,现在用的大多是loadbalancer(要知道)
在服务调用者(cart-service)中操作
<!--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>
在启动类添加注解@EnableFeignClients
编写Client客户端,先创建一个接口
然后编写代码(记得在注解里面声明要拉取的服务内容)
@FeignClient("item-service")
public interface ItemClient {
@GetMapping("/items")
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}
改造用restTemplate编写的代码
在实现类中,引入的实例全都不要,只要ItemClient
编写的方法也全都注释掉
注释的代码换成
List<ItemDTO> items = itemClient.queryItemByIds(itemIds);
一行代码全部搞定
测试
alt+8重启服务,到swagger文档进行购物车接口多调试几次,清空两个item-service的控制台,看能否正常进行负载均衡
可以看到请求是正常的两边都有日志输出。
底层原理
动态代理对象invocationhandler
源码其实就是对我们之前注释的代码进行了封装。
连接池
Feign底层发起http请求,依赖于其它的框架。我们通常会使用带有连接池的客户端来代替默认的HttpURLConnection。比如,我们使用OK Http.
引入依赖
<!--OK http 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
配置文件中开启连接池
feign:
okhttp:
enabled: true # 开启OKHttp功能
debug请求可以看到delegate对象已经由原来的默认HttpURLConnection变为OkHttpClient,性能会优化
最佳实践
第一种方案
如果很多业务都要请求商品信息,就会造成代码重复,维护起来也很困难,因此我们不如把请求的服务都放在item-service里,需要用到的就导入pom文件即可。维护人员只在item-service里面修改。代码结构合理容易维护,缺点是项目模块变多会复杂。
第二种方案(课程采用)
耦合度会高一些
hmall下创建新的module(hm-api)直接创建
在cart-service中将下面的两个依赖剪切掉并移到hm-api中
hm-api创建这两个文件夹并引入ItemDTO,引入swagger依赖(alt + enter),然后ItemClient也拿过来
把cart-service中的client包和ItemDTO删除掉
然后在pom文件中引入自己编写的hm-api,刷新一下maven
实现类爆红的话在顶部导包中删除爆红的引入即可,idea会自动导入新的包
当定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用。扫描包默认是当前包及其子包。当前包不可能有FeignClient,因此我们要指定包的范围。
在cart-service的启动类中修改如下
重新启动,清除两个item-service的日志,在swagger文档中测试接口即可。
日志
说明
日志级别为debug时才有日志,我们可以再配置文件中查看
但是我们还是看不到日志,这是因为Feign也有日志级别。
声明bean需要在配置类下(有configuration将其引入)
声明配置
在hm-api模块下创建包和类,声明如下,直接生命@Bean,不要@Configuration
public class DefaultFeignConfig {
@Bean
public Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
cart-service启动类的@EnableFeignClients后面添加
测试
debug模式启动cart-service的启动类,清空控制台,swagger调试
可以看到输出很多东西,这些就是日志
拆分作业
参考微服务拆分作业参考 - 飞书云文档,有问题的话在评论区评论,博主会一一解答。
记得一个地方不要漏了,不然swagger扫描包不到controller
网关(Spring Cloud Gateway)
问题引入
1、后端服务器有很多的端口号,前端不知道要请求哪个ip地址
2、每次操作都要做登录校验,很繁琐,而且还有泄漏密钥的风险
身份校验:你是谁?
路由:住处(几楼几号单元)
转发:带他过去
快速入门
1、创建新模块
2、引入网关依赖
<dependencies>
<!--common-->
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-common</artifactId>
<version>1.0.0</version>
</dependency>
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos discovery-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--负载均衡-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
3、编写启动类
4、配置路由规则(重点)
这里我们先把SearchController从hmall-service拷到item-service中
创建一个resources包下的application.yaml
server:
port: 8080
spring:
application:
name: gateway
cloud:
nacos:
server-addr: 192.168.89.129:8848
gateway:
routes:
- id: item-service
uri: lb://item-service
predicates:
- Path=/items/**,/search/**
- id: user-service
uri: lb://user-service
predicates:
- Path=/users/**,/addresses/**
lb:loadbalance的缩写
测试
启动该网关服务和有两个端口号的商品服务,然后到浏览器输入ip地址测试
localhost:8081/search/list
localhost:8080/search/list
localhost:8083/search/list
可以看到网关正常工作,而且实现了负载均衡
启动UserApplication,然后在黑马商城实现登录测试
过滤器
官网有33中过滤器,值一般就是key-value的形式
重启一下网关和两个商品服务application,然后清空日志,在浏览器输入ip地址,然后看控制台有无指定输出
localhost:8080/items/page
可以看到有输出,过滤器的增加请求头有效果
全局配置过滤器可以放在和route同级下面
登录校验
1、自定义过滤器
GlobalFilter(重点)
tips:对着类按ctrl + H可以看到所有子类(其中有重要的负责路由转发的NettyRoutingFilter)
在网关模块创建一下该类
@Component
public class GlobalFilter implements org.springframework.cloud.gateway.filter.GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// TODO 模拟登录校验逻辑
ServerHttpRequest request = exchange.getRequest();
HttpHeaders headers = request.getHeaders();
System.out.println(headers);
// 放行
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
打断点调试
debug重启,然后在黑马商城点击搜索按钮,然后回来查看是否有headers
可以看到用户的token
回顾:继承一个GlobalFilter,从exchange获得信息,再把上下文对象exchange传给过滤器链chain放行。还要保证过滤器在NettyRoutingFilter之前执行,这里我们用Ordered接口来控制执行顺序。(过滤器执行顺序,值越小,优先级越高)
GatewatFilter
创建新的过滤器
@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {
@Override
public GatewayFilter apply(Object config) {
return new OrderedGatewayFilter(new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
System.out.println("print any filter running");
return chain.filter(exchange);
}
},1);
}
}
记得配置
重新启动服务,在黑马商城点击搜索。看IDEA控制台打印
可以看到有顺序地打印了请求头、然后我们另外自定义的GatewayFilter
有参数的GatewayFilter
改造过滤器
@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<PrintAnyGatewayFilterFactory.Config> {
@Override
public GatewayFilter apply(Config config) {
return new OrderedGatewayFilter(new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String a = config.getA();
String b = config.getB();
String c = config.getC();
System.out.println("a="+a+",b="+b+",c="+c);
System.out.println("print any filter running");
return chain.filter(exchange);
}
},1);
}
@Data
public static class Config{
private String a;
private String b;
private String c;
}
public PrintAnyGatewayFilterFactory() {
super(Config.class);
}
@Override
public List<String> shortcutFieldOrder() {
return List.of("a","b","c");
}
}
改造配置文件
- PrintAny=1,2,3
重启然后刷新黑马商城
可以拿到参数
2、实现登录校验
从hm-service中引入三个配置类倒hm-gateway,其中一个要加上@Component
引入工具类和jwt密钥文件hmall.jks,拷贝一些jwt内容到application.yaml
hm:
jwt:
location: classpath:hmall.jks
alias: hmall
password: hmall123
tokenTTL: 30m
auth:
excludePaths:
- /search/**
- /users/login
- /items/**
- /hi
编写AuthGlobalFilter
package com.hmall.gateway.filters;
import com.hmall.common.exception.UnauthorizedException;
import com.hmall.common.utils.CollUtils;
import com.hmall.gateway.config.AuthProperties;
import com.hmall.gateway.util.JwtTool;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.List;
@Component
@RequiredArgsConstructor
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {
private final JwtTool jwtTool;
private final AuthProperties authProperties;
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取Request
ServerHttpRequest request = exchange.getRequest();
// 2.判断是否不需要拦截
if(isExclude(request.getPath().toString())){
// 无需拦截,直接放行
return chain.filter(exchange);
}
// 3.获取请求头中的token
String token = null;
List<String> headers = request.getHeaders().get("authorization");
if (!CollUtils.isEmpty(headers)) {
token = headers.get(0);
}
// 4.校验并解析token
Long userId = null;
try {
userId = jwtTool.parseToken(token);
} catch (UnauthorizedException e) {
// 如果无效,拦截
ServerHttpResponse response = exchange.getResponse();
response.setRawStatusCode(401);
return response.setComplete();
}
// TODO 5.如果有效,传递用户信息
System.out.println("userId = " + userId);
// 6.放行
return chain.filter(exchange);
}
private boolean isExclude(String antPath) {
for (String pathPattern : authProperties.getExcludePaths()) {
if(antPathMatcher.match(pathPattern, antPath)){
return true;
}
}
return false;
}
@Override
public int getOrder() {
return 0;
}
}
网关增加一个carts
重启服务后到黑马商城测试,先是/items/page(直接放行的,直接进入),然后是/carts(有登录拦截,报401)
技术点:AntPathMatch(spring提供的匹配)
3、网关传递用户
思路:
过滤器中保存用户到请求头
编写业务逻辑
// TODO 5.如果有效,传递用户信息
String userInfo = userId.toString();
ServerWebExchange swe = exchange.mutate()
.request(builder -> builder.header("user-info", userInfo))
.build();
在cart-service中的controller方法中调试(传统方法解析请求头)
记得开启cartApplication服务,然后在控制台查看是否有信息打印出来
调用SpringMVC的拦截器获取用户保存到ThreadLocal(给其他服务拿取)
在common模块中操作,就不用在每个微服务都编写一个springmvc拦截器
创建新类(继承HandlerInterceptor)
package com.hmall.common.interceptors;
import cn.hutool.core.util.StrUtil;
import com.hmall.common.utils.UserContext;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @Author:Nanami
* @Date:2024/11/9 21:20
* @Version: v1.0.0
* @Description: TODO
**/
public class UserInfoInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 1.获取登录用户信息
String userInfo = request.getHeader("user-info");
// 2.判断是否获取了用户,如果有,存入ThreadLocal
if(StrUtil.isNotBlank(userInfo)){
UserContext.setUser(Long.valueOf(userInfo));
}
// 3.放行
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
// 清理用户
UserContext.removeUser();
}
}
要想SpringMVC拦截器实现还需要编写配置类config(继承WebMvcConfigurer),注解@Configuration
package com.hmall.common.config;
import com.hmall.common.interceptors.UserInfoInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @Author:Nanami
* @Date:2024/11/9 21:29
* @Version: v1.0.0
* @Description: TODO
**/
@Configuration
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
}
}
但是这样拦截器还没有生效,因为其他微服务扫描不到common模块的包,我们还得在META-INF下的spring.factories中声明
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hmall.common.config.MyBatisConfig,\
com.hmall.common.config.MvcConfig,\
com.hmall.common.config.JsonConfig
但是重新启动后还是报错了,这是因为网关也引用了common中的springMVC拦截器,网关的底层拦截器和springMVC的拦截器冲突了。
网关的底层拦截器依靠的是非阻塞式的响应式编程WebFlux,因此我们让springMVC的拦截器只在微服务生效而不要在网关生效。
这里还是考察springboot的自动装配原理,让其只在某些特定条件下生效,这里我们引用springmvc的重要api:DispatcherServlet,在配置类加入注解
@Configuration
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new UserInfoInterceptor());
}
}
下一步:记得换掉之前在cart-service的实现类中写死的用户ID
把1L换成UserContext.getUser()
重新启动CartApplication,重新进入黑马商城的/cart.html,控制台打印中看CartMapper的方法能不能从UserContext中取得userId。
可以看到拦截器存入了userId,拦截器生效了。
重新登录,账号为rose,密码为123再来测试
可以看到userId变成了“2”。
快捷键学习:ctrl+o:快速重写
4、OpenFeign传递用户
网关中补充新的路径
- id: pay-service
uri: lb://pay-service
predicates:
- Path=/pay-orders/**
- id: trade-service
uri: lb://trade-service
predicates:
- Path=/orders/**
这里清除购物车的话会清除失败。因为这只是微服务之间互相调用,没有走网关,因此请求头中并没有存入用户的Id
接下来的操作都是在hm-api中操作
先在pom文件中引入hm-common模块
改造DefaultFeignConfig类
@Bean
public RequestInterceptor userInfoRequestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
Long userId = UserContext.getUser();
if(userId != null){
template.header("user-info",userId.toString());
}
}
};
}
另外,trade启动类上面也要加上注解Feign配置类才能生效。
建议重新启动所有服务
可以看到购物车的清除操作有了userId
配置管理
问题引入
1、微服务重复配置过多,维护成本高
2、业务配置经常变动,每次修改都要重启服务
3、网关路由配置写死,如果变更要重启网关
很多微服务可能有相同的配置比如jdbc,swagger,日志等,要修改的话就很麻烦,重启的时候服务不可用,用户的体验就会下降。nacos不仅能担任注册中心的角色,还可以管理微服务的共享配置
配置共享
拷贝cart-service的部分配置文件,这里我们先拷贝jdbc部分,然后到nacos的配置列表里点击最右边的“+”号
相关设置如下:
记得标上配置格式!!勾选yaml
配置内容就贴上下面的代码
spring:
datasource:
url: jdbc:mysql://${hm.db.host:192.168.89.129}:${hm.db.port:3306}/${hm.db.database}?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai
driver-class-name: com.mysql.cj.jdbc.Driver
username: ${hm.db.un:root}
password: ${hm.db.pw:123}
mybatis-plus:
configuration:
default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
global-config:
db-config:
update-strategy: not_null
id-type: auto
点击发布即可。
接下来我们再来配置log和swagger,命名和描述仿照上面的步骤
logging:
level:
com.hmall: debug
pattern:
dateformat: HH:mm:ss:SSS
file:
path: "logs/${spring.application.name}"
knife4j:
enable: true
openapi:
title: ${hm.swagger.title:黑马商城购物车接口文档}
description: ${hm.swagger.desc:黑马商城购物车接口文档}
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:
- ${hm.swagger.package}
接下来就要到idea编写hm.db之类的配置给nacos读取了
拉取共享配置
共享配置已经添加到nacos了,但是微服务还没有拉取这些配置到本地,所以我们接下来就要做拉取共享配置的操作
我们现在是普通的springboot项目,内部加载顺序如下:
springcloud的加载顺序,但是我们也不知道nacos的地址
有了引导文件bootstrap.yaml后,我们就可以知道nacos的地址了
引入依赖
放入cart-service的pom文件中
<!--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>
</dependency>
创建一个bootstrap.yaml文件
spring:
application:
name: cart-service #微服务名称
profiles:
active: dev
cloud:
nacos:
server-addr: 192.168.89.129:8848
config:
file-extension: yaml
shared-configs:
- data-id: share-jdbc.yaml
- data-id: share-log.yaml
- data-id: share-swagger.yaml
把原来的application.yaml公共配置删去,只剩
server:
port: 8082
feign:
okhttp:
enabled: true
hm:
db:
database: hm-cart
swaager:
title: "黑马商城购物车服务接口文档"
package: com.hmall.cart.controller
重新启动服务,随后到黑马商城添加物品到购物车,看是否添加成功,报错的看看文件名啥的有没有书写正确。
配置热更新
定义:当修改配置文件中的配置时,微服务无需重启即可使配置生效。
案例:动态修改购物车限制数量
对count >= 10进行操作
新建一个类
@Data
@Component
@ConfigurationProperties(prefix = "hm.cart")
public class CartProperties {
// 最大商品数量
private Integer maxItems;
}
在实现类注入该类
改写之前的cart >= 10
nacos中添加新的配置管理
发布后重新启动微服务,然后到黑马商城刷新,看一下添加物品到购物车成不成功
这样就成功生效了,接下来就要测试热更新,到nacos中修改maxItems的值,但是不需要重启微服务 (这里是我已经改过的了)
可以看到成功添加进来了,成功实现了热更新。
动态路由
当路由发生改变时,网关中的配置也相应需要更新,然后重启网关,但是网关不能随便重启,因此我们需要动态路由。
引入依赖到网关
<!--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>
</dependency>
复制cart-service的bootstrap.yaml文件
spring:
application:
name: gateway #微服务名称
profiles:
active: dev
cloud:
nacos:
server-addr: 192.168.89.129:8848
config:
file-extension: yaml
shared-configs:
- data-id: share-log.yaml
修改application.yaml
server:
port: 8080 # 端口
hm:
jwt:
location: classpath:hmall.jks # 秘钥地址
alias: hmall # 秘钥别名
password: hmall123 # 秘钥文件密码
tokenTTL: 30m # 登录有效期
auth:
excludePaths: # 无需登录校验的路径
- /search/**
- /users/login
- /items/**
创建一个新类
更新路由配置需要解析yaml文件,但是很麻烦,所以我们改作解析json文件。
Mono是springboot提供的一个响应式编程容器
编写配置类
@Slf4j
@Component
@RequiredArgsConstructor
public class DynamicRouteLoader {
private final NacosConfigManager nacosConfigManager;
private final RouteDefinitionWriter writer;
private final String dataId = "gateway-routes.json";
private final String group = "DEFAULT_GROUP";
private final Set<String> routeIds = new HashSet<>();
@PostConstruct
public void initRouteConfigListener() throws NacosException{
// 1.项目启动时,先拉取一次配置,并且添加配置监听器
String configInfo = nacosConfigManager.getConfigService()
.getConfigAndSignListener(dataId, group, 5000, new Listener() {
@Override
public Executor getExecutor() {
return null;
}
@Override
public void receiveConfigInfo(String configInfo) {
// 2.监听到配置变更,需要去更新路由表
updateConfigInfo(configInfo);
}
});
// 3.第一次读取到配置,也需要更新到路由表
updateConfigInfo(configInfo);
}
public void updateConfigInfo(String configInfo){
log.info("收到最新的网关路由配置信息:{}", configInfo);
// 1.解析配置信息,转为RouteDefinition
List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class);
// 2.删除旧的路由表
for (String routeId : routeIds) {
writer.delete(Mono.just(routeId)).subscribe();
}
routeIds.clear();
// 3.更新路由表
for (RouteDefinition routeDefinition : routeDefinitions) {
// 3.1.更新路由表
writer.save(Mono.just(routeDefinition)).subscribe();
// 3.2.记录路由id,便于下一次更新时删除
routeIds.add(routeDefinition.getId());
}
}
重启服务
接下来不用重启微服务,在nacos中编写路由配置(路由内容),这里虎哥给的id没有加上service,这里可以加上。
[
{
"id": "item-service",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/items/**", "_genkey_1":"/search/**"}
}],
"filters": [],
"uri": "lb://item-service"
},
{
"id": "cart-service",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/carts/**"}
}],
"filters": [],
"uri": "lb://cart-service"
},
{
"id": "user-service",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/users/**", "_genkey_1":"/addresses/**"}
}],
"filters": [],
"uri": "lb://user-service"
},
{
"id": "trade-service",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/orders/**"}
}],
"filters": [],
"uri": "lb://trade-service"
},
{
"id": "pay-service",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/pay-orders/**"}
}],
"filters": [],
"uri": "lb://pay-service"
}
]
发布的时候可以看看控制台有无打印路由信息(没有的话可以重启服务)
浏览器测试
微服务保护和分布式事务
雪崩问题
解决思路:一个是服务提供者想办法避免服务出现故障,另一个是服务调用者去调用服务器的时候发现出现了故障要想办法去隔离这种故障,避免这个故障传递到自己出现级联失败。
解决方案
请求限流:
使QPS线由突起转化为平稳的曲线
线程隔离(舱壁隔离):
服务器只给用户一定的线程数保证tomcat资源不会被消耗完,但是如果不断占用线程去请求故障的服务会浪费资源,故有后面的服务熔断
服务熔断
由断路器统计请求的异常比例或慢调用比例,如果超出阈值则会熔断该业务,则拦截该接口的请求。熔断期间,所有请求快速失败,全都走fallback逻辑。
组件使用选择
Sentinel快速入门
初始Sentinel
Sentinel是阿里巴巴开源的一款微服务流量控制组件。 官方地址: home | Sentinel
我们要做的就是去搭建控制台和安装依赖。
安装和启用jar包
安装步骤可以参考day05-服务保护和分布式事务 - 飞书云文档
这里资料文件夹里面有1.8.6版本的Sentinel,复制到非中文目录(记得去掉版本号)然后在文件终端执行命令即可(操作跟启动nginx一样)。
java -Dserver.port=8090 -Dcsp.sentinel.dashboard.server=localhost:8090 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar
访问localhost:8090端口,会进入到Sentinel登录界面,账号和密码都是Sentinel
然后就进来啦~
连接微服务
在cart-service中引入依赖
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
配置控制台
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8090 #Sentinel的控制台地址
重启一下cart-service,此时Sentinel客户端还监测不到购物车服务,这是因为我们还没有进行接口调用,我们到黑马商城调用几次购物车(多刷新几次)请求然后再回来刷新一下就可以看到了。
簇点链路
Restful风格的API请求路径一般都相同,这会导致簇点资源名称重复。因此我们要修改配置,把请求方式+请求路径作为簇点资源名称
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8090 # Sentinel的控制台地址
http-method-specify: true # 是否设置请求方式作为资源名称
重启服务然后到黑马商城的购物车做一下增删改查操作看一下Sentinel簇点链路变多有没有
可以看到是有变多的。
请求限流
给购物车接口请求限流(点击GET:/carts右边的流控,设置单击阈值为6)
打开jmeter测试,拖进资料中的雪崩测试.jms
点击开始限流测试
查看结果
10个请求只允许6个,这是预期情况,可以看见限流成功了。
线程隔离
还是跟请求限流一样点击流控,但是我们选择并发线程数
我们先模拟业务延迟(相当于1秒只执行两次)qps为2
修改cat-service的application.yaml
server:
port: 8082
tomcat:
threads:
max: 25
accept-count: 25
max-connections: 100
重启购物车服务,然后到黑马商城测试购物车,查看网络请求。
可以看到请求要500多毫秒,模拟成功
我们再试试除了查询之外的操作。可以看到就不用500多毫秒
接下来我们测试查询业务高并发会不会影响别的业务
开始执行jmeter的线程隔离测试,可以看到我这里是直接查询不到了,实际上因为tomcat资源被耗尽,其他的服务也会受到影响。
接下来到sentinel去做线程隔离,控制并发线程数的单机阈值为5,可以看到虽然请求不了,但是查询速度变快了。
Fallback
hm-api模块下创建fallback类
@Slf4j
public class ItemClientFallbackFactory implements FallbackFactory<ItemClient> {
@Override
public ItemClient create(Throwable cause) {
return new ItemClient() {
@Override
public List<ItemDTO> queryItemByIds(Collection<Long> ids) {
log.error("查询商品失败!",cause);
return CollUtils.emptyList();
}
@Override
public void deductStock(List<OrderDetailDTO> items) {
log.error("扣减商品失败!",cause);
throw new RuntimeException(cause);
}
};
}
}
在config包下的配置类添加bean
//注册bean
@Bean
public ItemClientFallbackFactory itemClientFallbackFactory(){
return new ItemClientFallbackFactory();
}
ItemClient类上注解后面添加fallbackFactory
cart-service模块的yaml配置修改
feign:
okhttp:
enabled: true
sentinel:
enabled: true
重启购物车服务,可见feign的远程调用也变成了一个簇点
我们之间在下面控制并发线程数就好,然后访问购物车不断刷新
可以看到请求都是成功的,只是fallback的item信息是null
服务熔断
还是配置商品服务的熔断
然后再去jmeter测试
发现购物车根本不查询了,因为执行了熔断规则
分布式事务
Seata
初识Seata
TM:标志:controller开始。实现类执行完就结束
TC:都正常就提交,有一个不正常就都回滚。
部署TC服务
引入seata数据库表seata-tc.sql
把seata.tar包和seata配置文件都移到虚拟机的/root目录下
加载镜像
docker load -i seata -i seata-1.5.2.tar
dis查看是否加进来
接下来我们要保证nacos.mysql,seata都在hm-net网络下
docker inspect mysql
docker inspect nacos
查看网段有无hm-net
docker network ls
没有的话就添加
docker network create hm-net
添加容器到网段的方法(把mysql、nacos都加进来)
docker network connect hm-net nacos
connect后面就跟上网段名和容器名
添加seata容器,记得把虚拟机IP改成自己的
docker run --name seata \
-p 8099:8099 \
-p 7099:7099 \
-e SEATA_IP=192.168.89.129 \
-v ./seata:/seata-server/resources \
--privileged=true \
--network hm-net \
-d \
seataio/seata-server:1.5.2
查看seata容器情况
docker logs -f seata
打开nacos的服务列表可以看见seata-server
访问seata网站控制台
192.168.89.129:7099
账号和密码都是admin
微服务集成Seata
在nacos编写共享配置文件(记得修改虚拟机IP为自己的)
seata:
registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
type: nacos # 注册中心类型 nacos
nacos:
server-addr: 192.168.150.89.129 # nacos地址
namespace: "" # namespace,默认为空
group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP
application: seata-server # seata服务名称
username: nacos
password: nacos
tx-service-group: hmall # 事务组名称
service:
vgroup-mapping: # 事务组与tc集群的映射关系
hmall: "default"
在购物车、商品、交易服务都引入依赖(有的服务引入seata依赖即可)
<!--统一配置管理-->
<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>
</dependency>
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
不要把依赖放入common模块的原因是不是每个微服务都需要这些依赖
在cart-service的bootstrap.yaml配置项引入seata共享配置
- data-id: share-seata.yaml
item-service的bootstrap.yaml配置(没有则直接引入)
spring:
application:
name: item-service #微服务名称
profiles:
active: dev
cloud:
nacos:
server-addr: 192.168.89.129:8848
config:
file-extension: yaml
shared-configs:
- data-id: share-jdbc.yaml
- data-id: share-log.yaml
- data-id: share-swagger.yaml
- data-id: share-seata.yaml
item-service的application.yaml改造
server:
port: 8081
hm:
db:
database: hm-item
swaager:
title: "黑马商城商品服务接口文档"
package: com.hmall.item.controller
trade-service服务模块的改造也是如此
bootstrap.yaml
spring:
application:
name: trade-service #微服务名称
profiles:
active: dev
cloud:
nacos:
server-addr: 192.168.89.129:8848
config:
file-extension: yaml
shared-configs:
- data-id: share-jdbc.yaml
- data-id: share-log.yaml
- data-id: share-swagger.yaml
- data-id: share-seata.yaml
application.yaml
server:
port: 8085
hm:
db:
database: hm-trade
swaager:
title: "黑马商城交易服务接口文档"
package: com.hmall.trade.controller
三个模块都重启一下
回去docker查看日志查找关键词“TM”"RM”可见服务已经和seata建立起了联系
XA模式(强一致)
1、修改配置文件
在nacos中修改share-seata.yaml并点击发布
seata:
data-source-proxy-mode: XA
2、添加注解
在trade-service模块的OrderServiceImpl的createOrder方法上添加@GlobalTransactional注解
在分支事务的item-service的扣减库存操作上添加@Transactional注解回滚
在分支事务的cart-service的移除操作上添加@Transactional注解回滚
可以测试,数据库设某个商品库存为0,然后支付,看一下购物车的商品有没有移除,没有的话就是回滚了。
在TradeApplication启动类控制台有如下注解:
AT模式 (性能)
实现:
1、引入seata-at.sql表
在trade、item、cart的database都引入该undolog表
2、修改XA模式为AT模式
可以在nacos中注释原来的配置(因为seata默认是AT模式),也可以修改值
重启三个微服务,照常下单操作看有无回滚即可。(提交成功后undolog快照表会删除)