赞
踩
@SpringCloud+RabbitMQ+Docker+Redis+搜索+分布式:
先确立思路,在order模块中的pojo实体类封装了userId和user,可以通过userId到数据库中查询到该user的数据并封装,最后返回该订单信息
实现步骤:
第一步:创建RestTemplate并注入Spring容器
/**
* 创建RestTemplate并注入String容器
*/
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
代码位置:写在order模块的主类中
第二步:在order的service层中,找到查询的方法,利用restTemplate的getForObject(url,User.class)获得User
getForObject:
参数一:传递地址,查询的请求,查询到该用户
参数二:传递返回值对象的字节码
@Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private RestTemplate restTemplate; public Order queryOrderById(Long orderId) { // 1.查询订单 Order order = orderMapper.findById(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.封装user到Order order.setUser(user); // 4.返回 return order; } }
两步轻松搞定
重点在于restTemplate的getForObject方法
这时候就需要引入eureka,将微服务的信息注册进来,登记在eureka,eureka本身也是一个微服务也就会将自己注册进来。
每三十秒会发送一次注册信息,检查服务是否挂了。
搭建EurekaServer一共三步
第一步:创建新模块EurekaServer,引入eureka-server依赖
<dependencies>
<!--eureka服务端-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
</dependencies>
第二步:添加@EnableEurekaServer 注解
@EnableEurekaServer // 自动装配
@SpringBootApplication
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class,args);
}
}
第三步:在eureka的application.yml中配置eureka地址
server:
port: 10086 # 服务端口
spring:
application:
name: eurekaserver # eureka的服务名称
# eureka本身也是微服务,也会将自己注册到微服务上
eureka:
client:
service-url:
# eureka的地址信息,有多个用逗号隔开,将来可能有多个eureka,组成eureka集群
defaultZone: http://127.0.0.1:10086/eureka
在我们的项目结构中,eureka是服务端,导入的依赖就是server,那么有服务端必然有客户端,其他的微服务就是一个个的客户端,所以导入的依赖是client,注意这个区别。
实现步骤,一共两步,和服务端相比只是少加一个注释:
第一步:在user-service的pom.xml导入客户端的依赖
<!--eureka客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
第二步:在user-service微服务的application.yml中配置user-service的地址,第二步和前面配置eureka写的内容区别只是在于修改服务名称
#这边可能有同学复制粘贴后发现爆红,原因是yml中不能出现两段spring,合并他们就好了
spring:
application:
name: userservice # userservice的服务名称
# 注册服务到eureka上
eureka:
client:
service-url:
# eureka的地址信息,这边写的是将信息注册到哪?注册到 eureka 上
defaultZone: http://127.0.0.1:10086/eureka
实现结果:
这三服务我都写完后,重启这三个服务,访问eureka可以看到注册进来的服务。
过了三十秒后,我重新访问eureka,可以看到只有三条服务了。
模拟启动两个user-service:
第一步,选中服务复制配置
第二步:写上-Dserver.port=8082,注意避开端口冲突
第三步:多出了一个服务,启动它
第四步:重新访问eureka,可以看到user-service有两个端口了
小结:从大观上看,可以想象成一共就是两个微服务,eureka作为服务端,其他微服务都作为客户端,将自己的信息注册到eureka服务端上。同时,eureka本身也是一个微服务,所以也需要将自己的信息登记到eureka上。
实际操作的时候,eureka搭建完后,只需要重复2.1.小节的两个步骤。
能够理解并操作起来,恭喜你已经学会服务注册了!
第一步:修改OrderService的代码,修改访问的url路径,用服务名代替ip、端口,不再写死代码。
// 2.1.url路径
String url = "http://userservice/user/" + order.getUserId();
原:参考第一节
// 2.1.url路径
String url = "http://localhost:8081/user/" + order.getUserId();
防止同学们看不懂或者忘记了,我贴出完整代码吧。注意是order-service的service层,是order去拉取user,userservice对应的是user-service的yml文件中spring.application.name服务名称
@Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private RestTemplate restTemplate; public Order queryOrderById(Long orderId) { // 1.查询订单 Order order = orderMapper.findById(orderId); // 2.利用RestTemplate发送http请求,查询用户 // 2.1.url路径 String url = "http://userservice/user/" + order.getUserId(); // 2.2.发送http请求,实现远程调用 User user = restTemplate.getForObject(url, User.class); // 3.封装user到Order order.setUser(user); // 4.返回 return order; } }
第二步:在order-service项目的启动类OrderApplication中的RestTemplate添加负载均衡注解:@LoadBalanced
@SpringBootApplication public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } /** * 创建RestTemplate并注入String容器 * @return */ @Bean @LoadBalanced public RestTemplate restTemplate(){ return new RestTemplate(); } }
学会后简直不要太爽!访问了两次不同的id,两个user-service都有查询,实现了负载均衡。
战术小结:
1.修改OrderService的代码,修改访问的url路径,用服务名代替ip、端口。
2.在order-service项目的启动类OrderApplication中的RestTemplate添加负载均衡注解:@LoadBalanced
不再写死地址,简单一句话:通过服务名访问目标服务。写上一个负载均衡的注解,即可实现负载均衡,如果有同学想专研什么是负载均衡,可以自行查找资料,本章只说简单明了的使用。
一共就两个步骤,轻松掌握!
第二节总结:
1.搭建EurekaServer
2.服务注册
3.服务发现
spring:
# 实际中大家肯定也有这些数据库的连接配置
datasource:
url: jdbc:mysql://localhost:3306/cloud_order?useSSL=false
username: root
password:
driver-class-name: com.mysql.jdbc.Driver
#今天重点在这
application:
name: orderservice # orderservice的服务名称
削微写一下个人理解:Ribbon是负责微服务之间调用的负载均衡。后面学习的网关,它也有负载均衡,但他是对外的。
下一章的4.基于Feign远程调用提到的feign,它里面就集成了负载均衡,所以理解它就行,而且负载均衡也不需要涉及到底层代码,想了解的可以自行研究(注:快速了解Spring Cloud和笔记才是本文的作用)。
负载均衡前面的理论大家可以去黑马程序员的视频中了解吧,实际上也不会用到随机分配的形式,这里我稍微记录一下修改负载均衡规则的两种方式:
1.代码方式:在order-service中的OrderApplication类中,定义一个新的IRule
这种方式的服务范围是全体。
@Bean
public IRule randomRule() {
return new RandomRule();
}
这里图片中停用,只是因为两种方式二选一,我选择了配置的方式,使用并没有任何问题。图片只是给大伙看一个结构和写的地方。
2.配置文件方式:在order-service的application.yml文件中,添加新的配置也可以修改规则。
这种方式只针对某个服务而言
userservice:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule
# 配置负载均衡规则,写上全限定类名
Ribbon默认采用的是懒加载,即第一次访问时才会去创建LoadBalanceClient,请求时间会很长。
而饥饿加载会在项目启动时创建,降低第一次访问的耗时,通过下面配置开启饥饿加载:
# 还是在Order-service,只是举例,比如订单要拉取用户信息,所以修改订单服务的配置
ribbon:
eager-load:
enabled: true # 开启饥饿加载
clients: # 指定饥饿加载的服务名称,clients是一个集合,可以指定多个服务
- userservice
# - xxxservice
# - ...
http客户端Feign
使用Feign的步骤如下三步:(依旧以order和user举例)
第一步:引入依赖。
找到order的pom.xml文件
<!--feign客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
第二步:在order-service的启动类添加注解@EnableFeignClients开启Feign的功能。
@SpringBootApplication
//添加这个注解,与前面的代码变化不大,复制注解即可,其他的无需多余复制
@EnableFeignClients
public class OrderApplication {
public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
}
第三步:声明一个远程调用,编写Feign客户端:
主要是基于SpirngMVC的注解来声明远程调用的信息,比如:
创建客户端,做接口声明
并添加@FeignClient注解,声明是user的微服务名称,并写上返回值的方法
//路径
@FeignClient("userservice")
public interface UserClient {
//返回值
@GetMapping("/user/{id}")
User findUserById (@PathVariable("id") Long id);
}
对照以前的代码
//路径
String url = "http://userservice/user/" + order.getUserId();
//返回值
User user = restTemplate.getForObject(url, User.class);
一一对应
接下来改造order的service层以前的代码
@Service public class OrderService { @Autowired private OrderMapper orderMapper; @Autowired private UserClient userClient; public Order queryOrderById(Long orderId) { // 1.查询订单 Order order = orderMapper.findById(orderId); // 2.利用Feign远程调用 User user = userClient.findUserById(order.getUserId()); // 3.封装user到Order order.setUser(user); // 4.返回 return order; } /*@Autowired private RestTemplate restTemplate; public Order queryOrderById(Long orderId) { // 1.查询订单 Order order = orderMapper.findById(orderId); // 2.利用RestTemplate发送http请求,查询用户 // 2.1.url路径 String url = "http://userservice/user/" + order.getUserId(); // 2.2.发送http请求,实现远程调用 User user = restTemplate.getForObject(url, User.class); // 3.封装user到Order order.setUser(user); // 4.返回 return order; }*/ }
对比
原先的代码
修改后
好处:不在代码中写url,url后面的参数可能会有很多,在代码中实现会有一长串的代码,把原来代码中的url放到接口中管理。
而且Feign集成了负载均衡的功能
战术小结:
Feign的使用步骤
PS:以上讲解都是对消费者的操作,
比如user为order提供数据,就是提供者,
order拉取user的数据,就是消费者。
配置Feign日志有两种方式:
方式一:配置文件方式
feign:
client:
config:
default: # 这里用default就是全局配置,如果写服务名称,则只针对某个微服务的配置
loggerLevel: FULL # 日志级别
feign:
client:
config:
userservice: # 这里用default就是全局配置,如果写服务名称,则只针对某个微服务的配置
loggerLevel: FULL # 日志级别
在控制台中,查询语句后就有了访问的日志
方式二:Java代码方式,需要声明一个Bean
还没加注解,所以还没生效,注解有两种方式
public class DefaultFeignConfiguration {
@Bean
public Logger.Level logLevel(){
return Logger.Level.BASIC;
}
}
主类,这是第一种注解配置,全局配置,放到@EnableFeignClients注解
@SpringBootApplication //配置defaultConfiguration 这样就是全局有效了,DefaultFeignConfiguration是自定义的类,传递它的字节码 @EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration.class) public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } /** * 创建RestTemplate并注入String容器 * @return */ @Bean @LoadBalanced public RestTemplate restTemplate(){ return new RestTemplate(); } }
第二种配置,局部配置,是放到@FeignClient注解
// 注意DefaultFeignConfiguration是自己定义的Bean,不要硬记
@FeignClient(value = "userservice",configuration = DefaultFeignConfiguration.class)
public interface UserClient {
@GetMapping("/user/{id}")
User findUserById (@PathVariable("id") Long id);
}
战术小结:
Feign日志配置:
1.方式一是配置文件,feign.client.config.xxx.loggerLevel
– 如果xxx是default则代表全局
– 如果xxx是服务名称,例如userservice则代表某服务
2.方式二是Java代码配置Logger.Level这个Bean
– 如果在@EnableFeignClients注释声明则代表全局
– 如果在@FeignClient注释中声明则代表某服务
Feign底层的客户端实现:
URLConnection:默认实现,不支持连接池
Apache HttpClient :支持连接池
OKHttp:支持连接池
因此优化Feign性能主要包括
1.使用连接池代替默认的URLConnection
2.日志级别,最好用basic或none(别用日志当然是最好的)
实现方式:
Feign的性能优化-连接池配置
引入依赖:
<!--引入HttpClient依赖-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
配置连接池,使用HttpClient或者OKHttp代替URLConnection
(这个不用我说应该知道放哪了吧)
feign:
httpclient:
enabled: true # 支持HttpClient的开关
max-connections: 200 # 最大连接数
max-connections-per-route: 50 # 单个路径的最大连接数
就这两个内容了:
真正业务中需要对max-connections和max-connections-per-route进行压测,设置多少比较合适。
日志参考4.1.Feign自定义配置的内容
什么是最佳实践,简单来讲就是企业在使用一个东西的过程中,各种踩坑,最后总结出来一个相对比较好的一个使用方式,这就是最佳实践了。
方式一(继承)︰给消费者的FeignClient和提供者的controller定义统一的父接口作为标准。
(这句话听起来已经不是非常抽象了,是十分抽象,首先清楚什么是消费者什么是提供者,可以方便阅读一些,该项目结构需结合本章内容,仅供参考,因为我的order去拉取user的数据,所以order为消费者,另一方就是提供者)
order中的UserClient和user中的UserController有着相似的地方,一样的路径和参数,那么就可以做一个提取,中间做一个UserAPI接口作为规范,两边同时实现这个接口,但是有一个缺点是紧耦合,从图-4.3.3可以看出,虽然是紧耦合且官方也不推荐这种方式,但企业中还是经常使用这种方式。
图-4.3.1
图-4.3.2
图-4.3.3
方式二(抽取) ∶将FeignClient抽取为独立模块,并且把接口有关的POJO、默认的Feign配置都放到这个模块中,提供给所有消费者使用
但坏处依然存在,没有一个完美的解决方案,比如接口中有ABCD四个方法,order只需要AB两个方法,但是CD也会被同时引用进来,就有些多余了。
实现方式二 :(方式一没讲)
实现最佳实践方式二的步骤如下:
1.首先创建一个module,命名为feign-api,然后引入feign的starter依赖;
2.将order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中;
3.在order-service中引入feign-api的依赖;
4.修改order-service中的所有与上述三个组件有关的import部分,改成导入feign-api中的包;(因为全都移走了,所以要重新导入feign-api中的包)
5.重启测试。
第一步:新建module,选择maven,命名为feign-api,然后引入feign的starter依赖;
引入依赖
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
第二步:将order-service中编写的UserClient、User、DefaultFeignConfiguration都复制到feign-api项目中;
创建好路径
剪切图片中三个包到新的路径下
feign中
order肯定会报错,pom.xml重新引入feign的统一api就行了
第三步:在order-service中引入feign-api的依赖;
引入feign的统一api,注意这一段是根据新建的feign-api写
<!--引入feign的统一api-->
<dependency>
<groupId>cn.itcast.demo</groupId>
<artifactId>feign-api</artifactId>
<version>1.0</version>
</dependency>
第四步:修改order-service中的所有与上述三个组件有关的import部分,改成导入feign-api中的包;(因为全都移走了,所以要重新导入feign-api中的包)
回到报错的地方,alt+Enter,重新导入包
完成结果:
第五步:重启测试
这时候启动失败
报错原因是:OrderService中的UserClient所在的包变成feign的包,默认扫描的包是order包,所以扫描不到
当定义的FeignClient不在SpringBootApplication的扫描包范围时,这些FeignClient无法使用。有两种方式解决:方式一是全拿来,方式二指定哪一个
写上解决问题
@EnableFeignClients(clients = UserClient.class,defaultConfiguration = DefaultFeignConfiguration.class)
总结:
不同包的FeignClient的导入有两种方式:
1.在@EnableFeignClients注解中添加basePackages,指定FeignClient所在的包
2.在@EnableFeignClients注解中添加clients,指定具体FeignClient的字节码
为什么需要网关?
所有的微服务暴露在外面,任何人都可以发请求访问,是不是有些不安全呢?所以就需要网关把守。
(补充:注册中心的负载均衡是微服务和微服务之间的调用,注册中心是对内做的,网关是对外做的)
请求限流:比如动物园访问人数可以承受5000人,这时候来了20000人,先进去一部分人,其他人外面排队等着,等里面的人出来了,排队的人再进去。
在spring cloud中网关的实现包括两种:(技术栈)
Zuul是基于Servlet的实现,属于阻塞式编程。
SpringCloudGateway则是基于Spring5中提供的WebFlux,属于响应式编程的实现,具备更好的性能。
战术小结:
网关的作用:
·对用户请求做身份认证、权限校验
·将用户请求路由到微服务,并实现负载均衡
·对用户请求做限流
搭建网关服务步骤:
1.创建新的module,引入SpringCloudGateway的依赖和nacos的服务发现依赖
<!--网关gateway依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos服务注册发现依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
由于我公司中没有使用到nacos,我暂时跳过了nacos部分,所以我自己尝试将nacos换成eureka。
理论上是gateway也是一个微服务,需要注册到nacos,实现服务注册和发现,所以我现在使用eureka,那就注册到eureka,没什么问题,主要是自己对于这个理解吧,本节主要还是学习掌握gateway。
Spring Cloud生态需要解决的有四个问题:
1.api网关
2.通信
3.服务注册和发现
4.熔断机制
无论使用什么技术,只要能够解决以上四个问题即可,至于哪个性能更好就是另外的问题了。
更有一些公司使用了K8S,里面集成了服务注册和发现。
<!--eureka客户端依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2.编写路由配置,application.yml配置上eureka
server: port: 10010 spring: application: name: gateway # gateway的服务名称 cloud: # nacos: # server-addr: nacos:8848 # nacos地址 gateway: routes: - id: user-service # 路由标示,必须唯一 uri: lb://userservice # 路由的目标地址 predicates: # 路由断言,判断请求是否符合规则 - Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是则符合 - id: order-service uri: lb://orderservice predicates: - Path=/order/** default-filters: - AddRequestHeader=Truth,Itcast is freaking awesome! eureka: client: service-url: # eureka的地址信息,有多个用逗号隔开,将来可能有多个eureka,组成eureka集群 defaultZone: http://127.0.0.1:10086/eureka
gateway配置参考图
断言:是编程术语,表示为一些布尔表达式。
uri:使用服务名称的时候就不是写http了,是写lb
lb:代表LoadBanlance
predicates:路由断言
3.创建Gateway启动类
当然创建module选择的是spring boot这步省了,选择maven就自己创建吧,写多了加深印象
启动类
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class,args);
}
}
启动运行,没问题,nacos或者eureka可以任自己选择,由于我对nacos了解的不多,所以我不好下定论。
一个运行的流程
1.微服务都会注册到注册中心;
2.由用户发起一个请求,假设请求是http://localhost:10010/user/1,网关是10010端口,所以请求会进入这个网关;
3.网关无法处理这个业务,只能根据路由规则去做判断,在配置文件中我定义了两个规则,/user/** 和 /order/** ,user开头的带到userservice,order开头的带到orderservice。按照前面用户发送的请求,所以会带到userservice;
4.网关自然会拿着userservice去注册中心里找到对应的地址;
5.最后就是做负载均衡,挑一个user服务。
战术小结
网关搭建步骤:
1.创建项目,引入nacos服务发现和gateway依赖
2.配置application.yml,包括服务基本信息、nacos地址、路由
路由配置包括:
1.路由id:路由的唯一标示
2.路由目标(uri) :路由的目标地址,http代表固定地址,lb代表根据服务名负载均衡
3.路由断言(predicates) :判断路由的规则,
4.路由过滤器(filters) :对请求或响应做处理(刚刚没有提到,配置中也是可以配的,后续会有说明)
·我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件;
·例如Path=/user/**是按照路径匹配,这个规则是由org.springframework.cloud.gateway.handler.predicate.PathRoutePredicateFactory类来处理的;
·像这样的断言工厂在SpringCloudGateway还有十几个(每个都有自己判断的规则和条件,参考下图)
(上一节使用的就是Path)
可以参考官方文档,里面带有配置的示例,点击跳转官方文档
有时间可以自己玩一玩,照着官方文档抄就完事了
本节小结 :
PredicateFactory的作用是什么?
读取用户定义的断言条件,对请求做出判断
Path=/user/**是什么含义?
路径是以/user开头的就认为是符合的
GatewayFilter 是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理:
期间做了什么处理呢?
官方31种过滤器文档(点击跳转),不用全记,看名字其实就可以看出是什么,都有语法和说明
实现方式 :在gateway中修改application.yml文件,给userservice的路由添加过滤器:
spring:
application:
name: gateway # gateway的服务名称
cloud:
gateway:
routes:
- id: user-service # 路由标示,必须唯一
uri: lb://userservice # 路由的目标地址
predicates: # 路由断言,判断请求是否符合规则
- Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是则符合
filters:
- AddRequestHeader=iskey,isValue # 添加请求头,放上键和值
稍微修改一下user的Controller,获取请求头的参数,required设置可以不传参数,打印出值
public class UserController { @Autowired private UserService userService; /** * 路径: /user/110 * * @param id 用户id * @return 用户 */ @GetMapping("/{id}") public User queryById(@PathVariable("id") Long id, //获取请求头的参数,required设置可以不传参数 @RequestHeader(value = "iskey",required = false) String iskey) { System.out.println(iskey); return userService.queryById(id); } }
重启网关看结果,输出的值是正确的
添加全局过滤器,default-filters默认过滤器
spring: application: name: gateway # gateway的服务名称 cloud: gateway: routes: - id: user-service # 路由标示,必须唯一 uri: lb://userservice # 路由的目标地址 predicates: # 路由断言,判断请求是否符合规则 - Path=/user/** # 路径断言,判断路径是否是以/user开头,如果是则符合 - id: order-service uri: lb://orderservice predicates: - Path=/order/** default-filters: # 给全局添加过滤器,默认过滤器,会对所有路由请求都生效 - AddRequestHeader=iskey,isValue # 添加请求头,键为iskey,值为isValue
本节小结
过滤器的作用是什么?
1.对路由的请求或响应做加工处理,比如添加请求头
2.配置在路由下的过滤器只对当前路由的请求生效
defaultFilters的作用是什么?
1.对所有路由都生效的过滤器
全局过滤器GlobalFilter
全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。
区别在于GatewayFilter通过配置定义,处理逻辑是固定的。
而GlobalFilter的逻辑需要自己写代码实现。
定义方式 是 实现 GlobalFilter接口。
这个接口只有一个方法,filter过滤
参数
exchange:请求上下文,从请求进入网关开始,一直到结束为止,整个流程过程中都可以共享exchange对象,这个对象可以拿到请求相关的信息、响应相关的信息,也可以往里存东西取一个东西都可以。
chian:是过滤器链,除了这个过滤器还有其它过滤器,就是放行,调用这个过滤器链,让它往后走,等于这里过滤器处理完了,要交给下一个过滤器处理了。
返回值
Mono:
案例:
1.在gateway服务创建AuthorizeFillter
2.实现GlobalFilter接口,重写filter方法
import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.GlobalFilter; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.http.HttpStatus; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.stereotype.Component; import org.springframework.util.MultiValueMap; import org.springframework.web.server.ServerWebExchange; import reactor.core.publisher.Mono; import java.lang.annotation.Annotation; //@Order(-1) // 过滤器链执行的顺序,值越小,优先度越高,和Ordered接口二选一 @Component // 注入到容器 public class AuthorizeFillter implements GlobalFilter, Ordered { @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // 1.获取请求参数 ServerHttpRequest request = exchange.getRequest(); // 获取请求 MultiValueMap<String, String> params = request.getQueryParams(); // 获取参数 // 2.获取参数中的 authorization 参数 String auth = params.getFirst("authorization"); // 获取第一个匹配的参数 // 3.判断参数值是否等于 admin if("admin".equals(auth)){ // 4.是,放行 return chain.filter(exchange); } // 5.否,拦截 // 5.1.设置状态码,UNAUTHORIZED未认证 exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); // 5.2.拦截请求 return exchange.getResponse().setComplete(); } @Override public int getOrder() { return -1; } }
运行查看结果
加上参数,访问成功
本节小结
全局过滤器的作用是什么?
对所有路由都生效的过滤器,并且可以自定义处理逻辑
实现全局过滤器的步骤?
1.实现GlobalFilter接口
2.添加@Order注解或实现Ordered接口,设置过滤器链执行顺序
3.编写处理逻辑
请求进入网关会碰到三类过滤器:当前路由的过滤器、DefaultFilter、GlobalFilter
这三个过滤器的执行顺序会是什么样呢?(可以直接看小结)
请求路由后,会将当前路由过滤器和DefaultFilter、GlobalFilter,合并到一个过滤器链(集合)中,排序后依次执行每个过滤器
·每一个过滤器都必须指定一个int类型的order值,order值越小,优先级越高,执行顺序越靠前。
·GlobalFilter通过实现Ordered接口,或者添加@Order注解来指定order值,由我们自己指定
·路由过滤器和defaultFilter的order由Spring指定,默认是按照声明顺序从1递增。
·当过滤器的order值一样时,会按照defaultFilter >路由过滤器>GlobalFilter的顺序执行。
本节小结
路由过滤器、defaultFilter、全局过滤器的执行顺序?
1.order值越小,优先级越高
2.当order值一样时,顺序是defaultFilter最先,然后是局
部的路由过滤器,最后是全局过滤器
跨域问题处理
跨域:域名不一致就是跨域,主要包括:
跨域问题:浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题。
重点:“浏览器” 禁止跨域 “ajax”请求。
所以orderservice、userservice和浏览器、Ajax没有任何关系,不会有跨域问题。
浏览器禁止跨域手段是拦截响应,实际上请求还是会发送到服务器。
解决方案:CORS
网关处理跨域采用的同样是CORS方案,并且只需要简单配置即可实现:
配置yml
spring: cloud: gateway: globalcors: # 全局的跨域处理 add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题 corsConfigurations: '[/**]': allowedOrigins: # 允许哪些网站的跨域请求 - "http://localhost:8090" - "http://www.leyou.com" allowedMethods: # 允许的跨域ajax的请求方式 - "GET" - "POST" - "DELETE" - "PUT" - "OPTIONS" allowedHeaders: "*" # 允许在请求中携带的头信息 allowCredentials: true # 是否允许携带cookie maxAge: 360000 # 这次跨域检测的有效期
本章小结:
不需要记,复制粘贴,只要会改配置文件就行了
本节内容:
企业开发中有一种神奇的生物存在,就是产品经理,ta会不停地提出需求添加业务,这时候同步处理每个服务,处理耗时会被加长,如图 6-1-1所示,处理一个业务总耗时为500ms,也就是说一秒只能处理两个业务,性能下降、吞吐量下降。
假设图 6-1-1仓储服务扛不住压力,它挂了,请求来访问仓储服务必然是阻塞了,支付服务调用仓储调不通,卡在这里,一个请求卡住,后续的请求都得排队等待,卡得越来越多,支付服务资源被耗尽,请求就进不去支付服务了。
图 6-1-1
同步存在的问题:
耦合度高:每次加入新的需求,都要修改原来的代码;
性能下降:调用者需要等待服务提供者响应,如果调用链过长则响应时间等于每次调用的时间之和;
资源浪费:调用链中的每个服务在等待响应过程中,不能释放请求占用的资源,高并发场景下会极度浪费系统资源;
级联失败:如果服务提供者出现问题,所有调用方都会跟着出问题,如同多米诺骨牌一样,迅速导致整个微服务群故障。
本节小结:
同步调用的优点:
同步调用的问题:
异步调用常见实现就是事件驱动模式
优势一:服务解耦
当有新的业务,不再需要修改支付服务的代码,和它没关系了,只需要新的服务去订阅Broker。如果需要去掉某一个业务,那只需要该业务的服务取消订阅Broker即可。
优势二:性能提升,吞吐量提高
优势三:服务没有强依赖,不担心级联失败问题
假设仓储服务挂了,也和支付服务没有关系,这边支付业务已经完成了,“钱到账了”通知发布出去了,后续怎么处理那就是其他服务各自的事情。
优势四:流量削峰
当来了多个请求,Broker有缓冲的作用,然后再排序安排执行,这时候并发量被砍平了,这就是流量削峰。
本节小结:
异步通信的优点:
·耦合度低
·吞吐量提升
·故障隔离
·流量削峰
异步通信的缺点:
·依赖于Broker的可靠性、安全性、吞吐能力
·架构复杂了,业务没有明显的流程线,不好追踪管理
异步同步各自都有优缺点,那什么时候使用同步?什么时候使用异步?
事实上大多都是使用同步,事实上对并发没有很高的要求,对时效性要求较高,就是我查询了一个信息,我立马需要在下一个业务中用到,比如查到了订单,马上要去查用户,需要立刻使用,那这得同步调用。异步只是通知了要做什么,什么时候做完不能及时反馈。
MQ(MessageQueue),中文是消息队列,字面来看就是存放消息的队列。也就是事件驱动架构中的Broker。
·RabbitMQ概述和安装
·常见消息模型
·快速入门
RabbitMQ是基于Erlang语言开发的开源消息通信中间件,官网地址: https://www.rabbitmq.com/
安装RabbitMQ:(点击跳转)
这边需要补充docker,在后面的补充有docker的文章链接。
RabbitMQ的结构和概念:
本节小结:
RabbitMQ中的几个概念:
MQ的官方文档中给出了5个MQ的Demo示例,对应了几种不同的用法:
官方的HelloWorld是基于最基础的消息队列模型来实现的,只包括三个角色:
生产者
// 1.建立连接 ConnectionFactory factory = new ConnectionFactory(); // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码 factory.setHost("192.168.150.101"); factory.setPort(5672); // RabbitMQ端口是5672,ui管理台控制台是15672 factory.setVirtualHost("/"); factory.setUsername("itcast"); factory.setPassword("123321"); // 1.2.建立连接 Connection connection = factory.newConnection(); // 2.创建通道Channel Channel channel = connection.createChannel(); // 3.创建队列 String queueName = "simple.queue"; channel.queueDeclare(queueName, false, false, false, null); // 4.订阅消息 channel.basicConsume(queueName, true, new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { // 5.处理消息 String message = new String(body); System.out.println("接收到消息:【" + message + "】"); } }); System.out.println("等待接收消息。。。。");
消费者
// 1.建立连接 ConnectionFactory factory = new ConnectionFactory(); // 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码 factory.setHost("192.168.150.101"); factory.setPort(5672); factory.setVirtualHost("/"); factory.setUsername("itcast"); factory.setPassword("123321"); // 1.2.建立连接 Connection connection = factory.newConnection(); // 2.创建通道Channel Channel channel = connection.createChannel(); // 3.创建队列 String queueName = "simple.queue"; channel.queueDeclare(queueName, false, false, false, null); // 4.订阅消息 channel.basicConsume(queueName, true, new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { // 5.处理消息 String message = new String(body); System.out.println("接收到消息:【" + message + "】"); } }); System.out.println("等待接收消息。。。。");
本节小结:
基本消息队列的消息发送流程:
1.建立connection(连接)
2.创建channel(通道,通过这个通道进行发送接收)
3.利用channel声明队列
4.利用channel向队列发送消息
基本消息队列的消息接收流程:
1.建立connection
2.创建channel
3.利用channel声明队列
4.定义consumer的消费行为handleDelivery05.利用channel将消费者与队列绑定
AMQP全称:Advanced Message Queuing Protocol(高级消息队列协议)是用于在应用程序之间传递业务消息的开放标准。该协议与语言和平台无关,更符合微服务中独立性的要求。
Spring AMQP是基于AMQP协议定义的一套API规范,提供了模板来发送和接收消息。包含两部分,其中spring-amqp是基础抽象,spring-rabbit是底层的默认实现。
SpringAmqp的官方地址: https://spring.io/projects/spring-amqp
提供了以下内容:
利用SpringAMQP实现HelloWorld中的基础消息队列功能
流程如下:
1.在父工程中引入spring-amqp的依赖
2.在publisher服务中利用RabbitTemplate发送消息到simple.queue这个队列
3.在consumer服务中编写消费逻辑,绑定simple.queue这个队列
步骤一:引入AMQP依赖
<!--AMQP依赖,包含RabbitMQ-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
步骤二:在publisher中编写测试方法,向simple.queue发送消息
1.在publisher服务中编写application.yml,添加mq连接信息:
spring:
rabbitmq:
host: 192.168.150.101 # rabbitMQ的ip地址,注意改成自己的
port: 5672 # 端口
username: itcast
password: 123321
virtual-host: /
2.在publisher服务中新建一个测试类,编写测试方法:(这段代码不会创建消息队列,会发到原有的消息队列,如果发现没有消息可能是这个原因,需要手动创建队列,选择的队列必须是rabbitmq里存在的队列)
public class SpringAmqpTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSendMessage2SimpleQueue() {
String queueName = "simple.queue"; // 队列名称
String message = "hello, spring amqp!"; // 消息
rabbitTemplate.convertAndSend(queueName, message); // 发送
}
}
测试
本节小结:
什么是AMQP?
·应用间消息通信的一种协议,与语言和平台无关。
SpringAMQP如何发送消息?
·引入amqp的starter依赖·配置RabbitMQ地址
·利用RabbitTemplate的convertAndSend方法
步骤三:在consumer中编写消费逻辑,监听simple.queue
1.在consumer服务中编写application.yml,添加mq连接信息:(无论是接收消息还是发送消息,都要知道MQ在哪)
spring:
rabbitmq:
host: 192.168.150.101 # rabbitMQ的ip地址
port: 5672 # 端口
username: itcast
password: 123321
virtual-host: /
现在要清楚接收哪个队列?监听这个队列要做什么?就是什么行为,这两点要清楚
2.在consumer服务中新建一个类,编写消费逻辑:(加一个类,写一个方法,这个方法就是处理消息的行为,这个要告诉spring,所以加一个注解@Component,把类声明成一个Bean,spring就可以找到它了。)
(然后在方法上加一个注解@RabbitListener,告诉它监听哪个队列)
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue")
public void listenWorkQueue(String msg) throws InterruptedException {
System.out.println("消费者接收到simple.queue的消息:【" + msg + "】" + LocalTime.now());
}
}
本节小结:
SpringAMQP如何接收消息?
1.引入amqp的starter依赖
2.配置RabbitMQ地址
3.定义类,添加@Component注解
4.类中声明方法,添加@RabbitListener注解,方法参数就时消息
注意:消息一旦消费就会从队列删除,RabbitMQ没有消息回溯功能
consumer:消费者
publisher:生产者
假设publisher一秒发送50条消息,consumer1只能处理40条消息,就会多出10条消息,那这个consumer1处理的完吗?显然处理不完,消息只能堆积在队列当中,队列是有存储上限的,堆满了之后,再有消息发送过来就进不去了。
增加一个consumer2,就可以合作处理消息,这就是挂两个消费者的原因。
Work queue不是一个新的队列类型,其实就是一个普通的队列,只是设计的时候多了几个消费者。
作用:可以提高消息处理速度,避免队列消息堆积。
Work Queue模型图
案例:
模拟WorkQueue,实现一个队列绑定多个消费者
基本思路如下:
1.在publisher服务中定义测试方法,每秒产生50条消息,发送到simple.queue
2.在consumer服务中定义两个消息监听者,都监听simple.queue队列
3.消费者1每秒处理50条消息,消费者2每秒处理10条消息
实现:
1.在publisher服务中定义测试方法,每秒产生50条消息,发送到simple.queue
@RunWith(SpringRunner.class) @SpringBootTest public class SpringAmqpTest { @Autowired private RabbitTemplate rabbitTemplate; @Test public void testSendMessage2WorkQueue() throws InterruptedException { String queueName = "simple.queue"; String message = "hello, message__"; for (int i = 1; i <= 50; i++) { rabbitTemplate.convertAndSend(queueName, message + i); Thread.sleep(20); } } }
2.在consumer服务中定义两个消息监听者,都监听simple.queue队列
@Component public class SpringRabbitListener { // @RabbitListener(queues = "simple.queue") // public void listenSimpleQueue(String msg) { // System.out.println("消费者接收到simple.queue的消息:【" + msg + "】"); // } @RabbitListener(queues = "simple.queue") public void listenWorkQueue1(String msg) throws InterruptedException { System.out.println("消费者1接收到消息:【" + msg + "】" + LocalTime.now()); Thread.sleep(20); } @RabbitListener(queues = "simple.queue") public void listenWorkQueue2(String msg) throws InterruptedException { System.err.println("消费者2........接收到消息:【" + msg + "】" + LocalTime.now()); // err 是为了打印颜色不一样,有所差异 Thread.sleep(200); } }
运行结果是:
总耗时处理了5秒钟,队列分配是消费者1都为偶数,消费者2都为奇数,由于消费者2处理速度较慢,所以导致处理速度被消费者2拖长,没有考虑消费者的能力。这是由于RabbitMQ内部机制造成的,消息预取机制。消息预取是当大量的消息到队列中,consumer1和consumer2都会提前把消息拿过来,不管它能不能处理,先拿了再说,所以就平均了所有的消息。但是consumer1处理得快,consumer2处理的慢,所以导致处理时长被拖长。
怎么控制呢?
消费预取机制
修改消费者consumer的application.yml文件,设置listener.simple.prefetch这个值,可以控制预取消息的上限:(设置为:1,每次只能获取一条消息,处理完成才能获取下一个消息)
spring:
rabbitmq:
host: 192.168.150.101 # rabbitMQ的ip地址
port: 5672 # 端口
username: itcast
password: 123321
virtual-host: /
listener:
simple:
prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息
关于SpringAMQP后续再补充,我先看ES了。
elasticsearch是一款非常强大的开源搜索引擎,可以帮助我们从海量数据中快速找到需要的内容。
elasticsearch结合kibana、Logstash、Beats,也就是elastic stack (ELK)。被广泛应用在日志数据分析、实时监控等领域。
elasticsearch可以将日志可视化展示出来。比如在线上系统报错了,怎么找?运行的时候不可能打断点Debug,只能采用日志的方式。
实时监控,运行状态也是数据,CUP情况、内存情况、访问的频率等等,这些信息也会被elasticsearch展示出来。
elasticsearch是elastic stack的核心,负责存储、搜索、分析数据。
elasticsearch底层是Luncene。
elasticsearch的发展:
Lucene是一个Java语言的搜索引擎类库,是Apache公司的顶级项目,由DougCutting于1999年研发。
官网地址: https://lucene.apache.org/。
Lucene的优势:
Lucene的缺点:
2004年Shay Banon基于Lucene开发了Compass
2010年Shay Banon重写了Compass,取名为Elasticsearch。
官网地址: https://www.elastic.co/cn/
相比与lucene,elasticsearch具备下列优势:
搜索引擎技术排名:
本节小结:
什么是elasticsearch?
什么是elastic stack (ELK) ?
什么是Lucene?
传统数据库(如MySQL)采用正向索引
elasticsearch采用倒排索引:
保存每一个词条,比如存“手机”这个词条,在id为1和2出现过,就记录“1,2”。
之后有再多的词条也是这样记录,总会有重复的词条,但是不重复记录,只存唯一的一个,重复词条出现在文档后面记录id,这样可以确定词条不会有重复的。因为词条它的唯一性,可以为它创建索引了。
先根据用户输入的词条,去词条列表中查找对应的id;
第二次拿着文档id,去查找文档;
虽然查找了两次,但两次都是根据索引进行查询,所以查询效率比逐条扫描高很多。
比如查找“华为手机”,分词后得到“华为”和“手机”,它们保存的id,2出现最多,那索引2就会往前排,剩下1,3排序。
平时搜索的结果也可以看出是分词后进行查找。
正向索引是根据文档找到词,
倒排索引是根据词找到文档。
本节小结:
什么是文档和词条?
什么是正向索引?
什么是倒排索引?
elasticsearch是面向文档存储的,可以是数据库中的一条商品数据,一个订单信息。
文档数据会被序列化为json格式后存储在elasticsearch中。
这边给出N多文档,根据字段相同进行分类,不同类型放到不同的索引库
概念对比
Mysql:擅长事务类型操作(ACID原则)可以确保数据的安全和一致性(还有隔离性等等)
Elasticsearch:擅长海量数据的搜索、分析、计算
可以互补
本节小结:
文档:一条数据就是一个文档,es中是Json格式
字段:Json文档中的字段
索引:同类型文档的集合
映射:索引中文档的约束,比如字段名称、类型
elasticsearch与数据库的关系:
安装elasticsearch、kibana
暂时保留,后续补充
处理中文分词,一般会使用lK分词器。官方文档:https://github.com/medcl/elasticsearch-analysis-ik
ik分词器-拓展词库
要拓展ik分词器的词库,只需要修改一个ik分词器目录中的config目录中的lkAnalyzer.cfg.xml文件:
然后在名为ext.dic的文件中,添加想要拓展的词语即可
要禁用某些敏感词条,只需要修改一个ik分词器目录中的config目录中的lkAnalyzer.cfg.xml文件:
然后在名为stopword.dic的文件中,添加想要拓展的词语即可
分词器的作用是什么?
IK分词器有几种模式?
IK分词器如何拓展词条?如何停用词条?
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。