继续接上一篇~
说道服务网关,也是微服务架构中非常重要的一个组件!我们的网关是所有微服务的入口,几乎所有微服务架构都需要服务网关,让网关统一的挡在"前面"
,帮助我们做一些日志记录、限流、权鉴、安全架构等工作。例如医院的门诊室,先由门诊室进行初步诊断,再分发到各个科室具体医治。
然而关于Zuul
组件,netflix
公司也发现了存在一些问题,目前已经停更!想推出Zuul2
,但是由于开发人员的变动和技术上选型的分歧,迟迟未能推出Zuul2
。市面上使用的企业也逐渐在下降,因此按Spring
官网的推荐我们应该把学习成本放在新一代的网关组件Gateway
身上。
gateway
网关组件是Spring
社区自研的全新网关组件,也是由于Zuul
的停更,Zuul2
迟迟未推出,导致SpringCloud
没有更好的网关组件代替,因此社区吸收了Zuul
优秀的理念,并且建立在Spring Boot 2.x、Spring WebFlux
和Project Reactor
之上,自研了一套网关组件Spring Cloud Gateway
。更加符合Spring
未来发展趋势。
官网:https://spring.io/projects/spring-cloud-gateway
gateway
就是原zuul 1.x
版的代替。是在Spring
生态系统之上构建的API网关服务,基于Spring 5、Spring Boot 2
和Project Reactor
等技术。
gateway
旨在提供一种简单而有效的方式来对API进行路由,以及提供一些强大的过滤器功能,例如:熔断、限流、重试等。
官网介绍说为了提高网关的性能,SpringCloud Gateway
是基于WebFlux框架实现的,而WebFlux框架底层使用的是高性能的Reactor
模式通信框架Netty
。SpringCloud Gateway
的目标是提供统一的路由方式且基于Filter
链的方式提供了网关基本的功能,例如:安全、监控/指标、限流等。
一方面因为Zuul 1.0
已经进入了维护阶段,而且Gateway
是SpringCloud
团队研发的,是亲儿子产品,值得信赖。而且很多功能Zuul
都没有用起来而到了Gateway
这里非常的简单便捷。
Gateway
是基于异步非阻塞模型上进行开发的,性能方面不需要担心。虽然Netflix
也发布了最新的Zuul 2
,但SpringCloud
貌似没有整合的计划,而且Netflix
相关组件都宣布进入维护期,前景堪忧!
多方面综合考虑Gateway
是很理想的网关组件选择。
最重要的一点,Spring Cloud Gateway
具有如下特性:
Predicate(断言)
和Filter(过滤器)
,并且易于编写Hystrix
的断路器功能SpringCloud
服务发现功能更多关于Zuul
与Gateway
的区别与联系请观看视频讲解:https://www.bilibili.com/video/BV18E411x7eT?p=67
Route(路由)
:路由是构建网关的基本模块,它由ID,目标URI,一系列的断言和过滤器组成,如果断言为true
则匹配该路由。
Predicate(断言)
:参考的是Java8的java.util.function.Predicate
。开发人员可以匹配HTTP
请求中的所有内容(例如请求头或请求参数),如果请求与断言相匹配则进行路由
Filter(过滤)
:指的是Spring
框架中GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后对请求进行修改。
总结下来就是,我们的web请求,通过一些匹配条件,定位到真正的服务节点。并在这个转发过程的前后,进行一些精细化控制。Predicate就是我们的匹配条件,而Filter,就可以理解为一个无所不能的拦截器。有了拦截器。有了这两个元素,再加上目标URI,就可以实现一个具体的路由了。
核心逻辑:路由转发 + 执行过滤器链
如上图所示!客户端向Spring Cloud Gateway发出请求。然后再Gateway Handler Mapping
中找到与请求相匹配的路由。将其发送到Gateway Web Handler。
Handler再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器之间用虚线分开是因为过滤器可能会在代理请求之前pre
或之后post
执行业务逻辑。
Filter在pre
类型的过滤器可以做参数校验、权限校验、流量监控、日志输出、协议转换等。在post
类型的过滤器中可以做响应内容、响应头修改、日志的输出、流量监控等有着非常重要的作用。
了解完理论,最终还是要回到代码中!落地实现才是重点!
1、使用Maven
或者SpringBoot
的初始化向导新建cloud_gateway9527
模块pom文件依赖如下
pom.xml
<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>cloudartifactId>
<groupId>com.laizhenghua.springcloudgroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>cloud_gateway9527artifactId>
<dependencies>
<dependency>
<groupId>com.laizhenghua.springcloudgroupId>
<artifactId>cloud_api_commonsartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
dependencies>
project>
还是和以前一样,先引入这些坐标!以后需要用到什么我们在引入什么即可。
2、编写application.yml
文件
server:
port: 9527
spring:
application:
name: gateway-service
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
3、编写主程序
/**
* @description: 主程序
* @date: 2022/2/16 20:56
*/
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
@EnableEurekaClient
public class GatewayMain {
public static void main(String[] args) {
SpringApplication.run(GatewayMain.class, args);
}
}
启动eureka
注册中心,启动8001
服务!注意8001
服务提供的地址有
http://127.0.0.1:8001/payment/testError
http://127.0.0.1:8001/payment/testOk
http://127.0.0.1:8001/payment/circuitBreaker/{id}
那么网关如何做路由映射呢?我们目前不想暴露8001端口,希望在8001外面套一层9527!完成路由的匹配与配置断言规则我们一般在application.yml
文件中配置(也可以编写配置类进行配置),例如:
server:
port: 9527
spring:
application:
name: gateway-service
cloud:
gateway:
routes:
- id: paymentCircuitBreakerRoute # 路由的ID,没有固定的规则但要求唯一,建议配合服务名
uri: http://127.0.0.1:8001 # 匹配后提供服务的路由地址(目标地址)
predicates:
- Path=/payment/circuitBreaker/** # 断言,路径相匹配的进行路由
- id: paymentRoute
uri: http://127.0.0.1:8001
predicates:
- Path=/payment/**
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
启动Gateway
服务,启动集成Spring Cloud Gateway
的服务坑比较多!除了最常见的spring-boot-starter-web
与spring-boot-starter-webflux
依赖冲突!更多的是SpringCloud
的版本适配原因。推荐版本:
<properties>
<spring-boot.version>2.1.1spring-boot.version>
<spring-cloud.version>Hoxton.SR1spring-cloud.version>
<com-alibaba-cloud.version>2.2.1.RELEASEcom-alibaba-cloud.version>
properties>
未添加网关组件之前
添加网关组件之后(我们可以只暴露9527这个端口,完成更多高级的操作)
1、在配置文件application.yml
中配置,详见上一章节!
2、代码中注入RouteLocator
的Bean实例。这里重点介绍。
定个目标:我们要实现的效果就是通过9527
网关访问到外网的百度新闻网站(http://news.baidu.com/
)
GatewayConfig.java
/**
* @description: SpringCloud Gateway Config
* @date: 2022/2/16 22:55
*/
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
routes.route("newsRoute", r -> r.path("/news").uri("http://news.baidu.com/")).build();
return routes.build();
}
}
测试(访问:http://127.0.0.1:9527/news
):
推荐还是在配置文件中编写,方便后面维护(不需要修改代码!)。
前面我们介绍的配置方式,存在一定的局限性!就是把路由写死了。不方便服务提供者扩容(增加集群数量),例如前面介绍的Riibon
组件,调用服务时自带负载均衡,而现在有了网关后,我们只暴露一个端口(9527
),而集群服务有多个端口,如何让网关按照特定的策略(负载均衡、随机等)路由到某个端口,便成了一大问题!解决方案:根据服务名配置动态路由。
Gateway
的动态路由:默认情况下Gateway
会根据注册中注册的服务列表,以注册中心上的服务名为路径创建动态路由进行转发,从而事项动态路由的功能。
准备环境:一个7001Eureka
注册中心 + 两个服务提供者8001/8002
。
准备好环境后,我们需要修改网关服务的配置文件(开启从注册中心动态创建路由的功能等)
application.yml
server:
port: 9527
spring:
application:
name: gateway-service
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: paymentCircuitBreakerRoute # 路由的ID,没有固定的规则但要求唯一,建议配合服务名
uri: http://127.0.0.1:8001 # 匹配后提供服务的路由地址(目标地址)
predicates:
- Path=/payment/circuitBreaker/** # 断言,路径相匹配的进行路由
- id: paymentRoute
# uri: http://127.0.0.1:8001
uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
predicates:
- Path=/payment/**
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka/
注意查看路由ID = paymentRoute
uri的配置。启动9527Gateway
网关服务进行测试!
这样就实现了网关的动态路由功能,以服务名为路径创建动态路由并进行负载均衡转发。
我们在启动网关服务时,控制台会打印出如下日志:
关于以上列举的断言配置规则,官网上也有详细介绍
官网地址:https://docs.spring.io/spring-cloud-gateway/docs/2.2.9.RELEASE/reference/html/#gateway-request-predicates-factories
After Route Predicate
:采用一个参数(datetime
是一个javaZoneDateTime
)在指定日期时间之后才会进行路由转发,配置示例:
spring:
application:
name: gateway-service
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: paymentRoute
uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
predicates:
- Path=/payment/**
- After=2022-02-19T11:17:52.335+08:00[Asia/Shanghai]
如何获取指定的时间格式呢?
@Test
public void getSysDate() {
ZonedDateTime zonedDateTime = ZonedDateTime.now(); // 默认时区
System.out.println(zonedDateTime); // 2022-02-19T11:17:52.335+08:00[Asia/Shanghai]
}
Before Route Predicate
与Between Route Predicate
与上面类似,见名知意,略。
Cookie Route Predicate
:需要配置两个参数分别是Cookie
的Cookie name与Java的一个正则表达式,路由规则会通过获取对应的Cookie name
值和正则表达式去匹配,如果匹配上就会执行路由,如果没有匹配上则不执行,配置示例:
spring:
application:
name: gateway-service
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: paymentRoute
uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
predicates:
- Path=/payment/**
- Cookie=username,Alex
使用curl
命令进行测试
curl http://127.0.0.1:9527/payment/getPaymentById/1 --cookie "username=Alex"
Header Route Predicate
:需要配置两个参数一个是属性名称和一个正则表达式,这个属性值和正则表达式匹配则执行,配置示例:
spring:
application:
name: gateway-service
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: paymentRoute
# uri: http://127.0.0.1:8001
uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
predicates:
- Path=/payment/**
# - After=2022-02-19T11:17:52.335+08:00[Asia/Shanghai]
# - Cookie=username, Alex
- Header=X-Request-Id, \d+ # 请求头要有 X-Request-Id 属性并且值为整数的正则表达式
重启测试:
Query Route Predicate
:需要配置两个参数,一个是属性名,一个是属性值,属性值可以是正则表达式。配置示例:
server:
port: 9527
spring:
application:
name: gateway-service
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: paymentRoute
# uri: http://127.0.0.1:8001
uri: lb://cloud-payment-service # 匹配后提供服务的路由地址
predicates:
- Path=/payment/**
- Query=age, \d+ # 要有参数名age并且还要是整数才能路由
重新启动网关服务进行测试
curl http://127.0.0.1:9527/payment/getPaymentById/1?age=18
小结:说白了,Predicate
就是为了实现一组匹配规则,让请求过来找到对应的Route
进行处理。
前面我们也介绍了SpringCloud Gateway
的三大核心概念,过滤器就是其中之一。也是SpringCloud Gateway
的特色功能。
据官网介绍得知:SpringCloud Gateway
内置了多种路由过滤器,他们都是由GatewayFilter
的工厂类来产生。路由过滤器可用于修改进入的HTTP请求和返回的HTTP响应。路由过滤器只能指定路由进行使用。
SpringCloud Gateway
内置的过滤器(局部的和全局的):
内置的过滤器局部的总共有30多个全局的也有10多个,也不一定要全部梳理一遍,用到那个去官网查询配置示例即可。这里重要的是我们要学会自定义全局的GatewayFilter
,来完成项目上所需的功能例如全局日志记录、统一网关鉴权等
。
自定义过滤器GatewayFilter
需要实现两个接口分别是GlobalFilter
和Ordered
。例如:
/**
* @description: 自定义的 GatewayFilter 主要实现功能全局日志记录
* @date: 2022/2/19 20:23
*/
@Slf4j
@Component
public class LogGatewayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("come in LogGatewayFilter -> " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
String username = exchange.getRequest().getQueryParams().getFirst("username");
if (username == null) {
log.info("username is null");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0;
}
}
重新启动网关服务测试带和不带username
请求参数。
小结:官网上内置的过滤器其实对于我们来说应用场景不多,更多的是使用我们自定义的过滤器,去完成某个业务!
服务的配置中心在分布式微服务架构中也非常重要,它主要解决服务的配置文件统一管理与修改!例如前面我们从服务的注册与发现学到服务的配置中心,已经搭建了10多个服务模块!每个模块都有自己的application.yml
文件,越来越臃肿,导致后面维护非常不方便(假如需要修改4个服务的数据库连接信息,需要手动一个个找出来修改并重新启动服务,非常的麻烦与容易出错)这也是分布式微服务面临的一大问题。
微服务意味着将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需要必要的配置信息才能运行,所以一套集中式的、动态的配置管理设施是必不可少缺少的。
SpringCloud
提供了configServer
来解决这个问题。
SpringCloud Config
为微服务架构的微服务提供了集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置。
在配置中心可以配置公用的和私用的配置,功能非常的强大,比如上述所述修改数据库连接信息问题,我们就可以把连接信息配到公用的配置文件,修改方便,并且不用重启服务。
SpringCloud Config分为服务端
和客户端
两部分。
git
来存储配置信息,这样就有助于对环境配置进行版本管理,并且可以通过git
客户端工具来方便的管理和访问配置内容。总结下来Spring Cloud Config
的功能有:
dev/test/prod
REST
接口的形式暴露前面我们已经介绍了,Spring Cloud Config
服务端的配置信息是使用git
来存储的,因此我们需要与GitHub
整合配置!
1、使用自己的GitHub
账号新建一个名为springcloud-config
的新Repository
。
2、由上一步获取Repository
地址:https://github.com/laizhenghua2/springcloud-config.git
3、本地电脑目录下新建git
仓库并clone
,并新建如下文件(yml文件以UTF-8格式保存)
4、提交至仓库
git add .
git commit -m "init .yml config file"
git push -u origin master
# 踩坑记录:使用命令 ssh-keygen -t rsa -C "[email protected]" 生成的公钥,使用notepad++打开后复制,会损坏公钥,导致 SSH keys 添加不成功
clip < ~/.ssh/id_rsa.pub
# 需要使用此命令直接把公钥复制到剪贴板
至此GitHub
这边已经准备完毕!接下来就是搭建config server
。
1、使用maven
或初始化向导新建cloud_config_center3344
模块,pom.xml
依赖坐标如下
<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>cloudartifactId>
<groupId>com.laizhenghua.springcloudgroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>cloud_config_center_3344artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-config-serverartifactId>
dependency>
project>
暂时先添加这些,后面使用到什么添加什么即可。
2、编写application.yml
配置文件
server:
port: 3344
spring:
application:
name: cloud-config-center
cloud:
config:
server:
git:
uri: https://github.com/laizhenghua2/springcloud-config.git # GitHub HTTPS 仓库地址
search-paths:
- spring-cloud-config # 搜索目录
force-pull: true
username: xxxxx # GitHub 账号
password: xxxxx # GitHub 密码
label: main # 读取的分支
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://127.0.0.1:7001/eureka/
3、编写主程序
/**
* @description: 主程序
* @date: 2022/2/20 10:24
*/
@SpringBootApplication
@EnableConfigServer
public class ConfigCenterMain {
public static void main(String[] args) {
SpringApplication.run(ConfigCenterMain.class, args);
}
}
启动服务,测试通过config
微服务是否可以从GitHub
上获取配置内容。
OK,成功实现了用SpringCloud Config
通过GitHub
获取配置信息。
关于配置读取在官网SpringCloud config
章有详细文档,包括前面整合GitHub
会出现的问题以及解决方案等都有详细的文档。
在这里主要整理下常用的配置读取规则:
1、/{label}/{application}-{profile}.yml
:label就是分支,application是文件名,profile是环境名。读取示例:
http://127.0.0.1:3344/main/config-dev.yml
# 因此我们编写外部配置文件时,一般文件名与环境名使用 - 连接
2、/{application}-{profile}.yml
:默认找config
服务配置的分支,读取示例:
http://127.0.0.1:3344/config-test.yml
3、/{application}/{profile}/{label}
,读取示例:
http://127.0.0.1:3344/main/config/prod/master
前面我们也说了,SpringCloud Config
分为客户端与服务端,因此还需要搭建一个客户端模块(个人感觉好麻烦,还是学Nacos比较香)。
1、使用maven
或初始化向导新建cloud_config_client_3355
模块,pom.xml
依赖坐标如下
<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>cloudartifactId>
<groupId>com.laizhenghua.springcloudgroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>cloud_config_client_3355artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-configartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
dependencies>
project>
2、编写配置文件(编写bootstrap.yml
系统级配置文件),为了配置文件的加载顺序和分级管理这里编写bootstrap.yml
application.yml
:是用户级的资源配置项
bootstrap.yml
:是系统级的,优先级更高
Spring Cloud
会创建一个Bootstrap Context
,作为Spring
应用的Application Context
的父上下文。初始化的时候,Bootstrap Context
负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的Environment
。
Bootstrap
属性有更高的优先级,默认情况下,他们不会被本地配置覆盖。Boorstrap Context
和Application Context
有着不同的约定,所以增加了一个bootstrap.yml
文件、保证Bootstrap Context
和Application Context
的配置分离。
因此要将client模块下的application.yml
文件改为bootstrap.yml
这是很关键的一步。
server:
port: 3355
spring:
application:
name: config-client
cloud:
config:
label: main # 分支名称
name: config # 配置文件名称
profile: dev
uri: http://127.0.0.1:3344 # 配置中心的地址
# 上面配置项综合起来就是 main 分支上的config-dev.yml 读取路径为 http://127.0.0.1:3344/main/config-dev.yml
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://127.0.0.1:7001/eureka/
3、编写主程序
/**
* @description:
* @date: 2022/2/20 19:55
*/
@SpringBootApplication
@EnableEurekaClient
public class ConfigClientMain {
public static void main(String[] args) {
SpringApplication.run(ConfigClientMain.class, args);
}
}
4、编写业务方法进行测试
/**
* @description:
* @date: 2022/2/20 19:58
*/
@Slf4j
@RestController
@RequestMapping(path = "/config")
public class ConfigController {
@Value(value = "${config.info}")
private String configInfo;
@RequestMapping(path = "/getConfigInfo", method = RequestMethod.GET)
public R getConfigInfo() {
return R.ok().put("data", configInfo);
}
}
5、启动3355服务进行测试
至此,成功实现了客户端3355访问SpringCloud Config 3344
通过GitHub
获取配置信息。
当然还有一些问题我们没有解决,例如:在GitHub
中修改配置文件,config服务端确定实时同步过来,而config客户端却不可以!
服务端
客户端(version:0.1)
向这种情况我们重启config客户端就能同步过来!但是每次修改后都要重启或重新加载,Spring Cloud Config
显然不可能这么设计!因此Spring Cloud Config
提供了动态刷新功能,避免每次更新配置都要重启客户端服务3355
。
1、首先需要添加actuator
依赖,主要目的是自己模块发生变化后其他模块要能监控到。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
2、bootstrap.yml
添加新的配置,暴露监控的端点
management:
endpoints:
web:
exposure:
include: "*" # 暴露监控端点
3、controller
类添加@RefreshScope
注解,开启刷新功能
/**
* @description: ConfigController
* @date: 2022/2/20 19:58
*/
@Slf4j
@RefreshScope
@RestController
@RequestMapping(path = "/config")
public class ConfigController {
@Value(value = "${config.info}")
private String configInfo;
@RequestMapping(path = "/getConfigInfo", method = RequestMethod.GET)
public R getConfigInfo() {
return R.ok().put("data", configInfo);
}
}
4、重新启动config
客户端服务进行测试。注意:我们改了这么多配置,动态刷新配置还是没有生效!这是因为需要发送一个POST
请求才能刷新config
客户端服务。
curl -X POST "http://127.0.0.1:3355/actuator/refresh"
现在我们的动态刷新功能就生效了,比起重新启动服务好了那么一点。弊端就是每次修改配置文件,都需要发一次这样的请求,才能生效。并且最重要的一点如果有多个config
客户端,每个客户端都要手动发一次POST
请求,还是很麻烦。
因此对于自动刷新功能我们还有优化的空间,通过广播的方式,一次通知处处生效。实现分布式自动刷新配置功能。而广播涉及到了服务间的通信问题,需要一个新的组件Spring Cloud Bus
来完成。
Nacos属于重点组件!需要认真准备。未完待续~
https://blog.csdn.net/m0_46357847/article/details/123142004
什么是总线?
在微服务架构的系统中,通常会使用轻量级的消息代理
来构建一个共同的消息主题,并让系统中的所有微服务实例都连接上来。由于该主题中生产的消息会被所有实例监听和消费,所以称它为消息总线
。在总线上的各个实例,都可以方便的广播一些需要让其他连接在该主题上的实例都知道的消息。
基本原理
ConfigClient
实例都监听MQ中同一个topic(默认是SpringCloudBus)。当一个服务刷新数据的时候,它会吧这个消息放入到topic中,这样其他监听同一个topic的服务就能得到通知,然后去更新自身的配置。
前面我们也引出了可以通过广播的方式通知各个Spring Cloud Config
客户端服务,让各个服务刷新配置!所以Spring Cloud Config
与Spring Cloud Bus
可以说是相辅相成!一起使用才能发挥出更多的优势。
再次声明:Spring Cloud Bus
配合Spring Cloud Config
使用可以实现配置的动态刷新。
Spring Cloud Bus
是用来将分布式系统的节点与轻量级消息系统链接起来的框架,它整合了Java
的事件处理机制和消息中间件的功能。
Spring Cloud Bus
支持两种消息代理:RabbitMQ
和Kafka
。
Spring Cloud Bus
的具体功能:管理和传播分布式系统间的消息,就像一个分布式执行器,可用于广播状态更改、事件推送等,也可以当作微服务间的通信通道。
直接使用Docker
进行构建,拆箱即用省去一切安装烦恼!
# 1.拉取镜像,注意一定要下标签带类似 3.8.14-management 有 management(管理界面)的,方便我们从web端查看
docker pull rabbitmq:3.8.14-management
# 2.启动 RabbitMQ 容器
[root@laizhenghua /]# docker run -d -p 5672:5672 -p 15672:15672 --name rabbitmq ee045987e252
32b2eb19574db54e977254665b78f2ab769e5fb594a395253bb96c4547a99b22
# 3.此时就可以通过 15672 端口访问 rabbitMQ 的管理界面
简单吧!真的是一键部署,我们后面的广播演示等,都必须要有良好的RabbitMQ
环境!另外为了更好的看到实验效果,在上面配置中心的环境下我们新增cloud_config_client_3366
服务模块。前面搭建步骤已经写过很多了,这里就不重复啰嗦了。
1、pom.xml
<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>cloudartifactId>
<groupId>com.laizhenghua.springcloudgroupId>
<version>1.0-SNAPSHOTversion>
parent>
<modelVersion>4.0.0modelVersion>
<artifactId>cloud_config_client_3366artifactId>
<dependencies>
<dependency>
<groupId>com.laizhenghua.springcloudgroupId>
<artifactId>cloud_api_commonsartifactId>
<version>${project.version}version>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-configartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
dependencies>
project>
2、bootstrap.yml
server:
port: 3366
spring:
application:
name: config-client
cloud:
config:
label: main # 分支名称
name: config # 配置文件名称
profile: dev
uri: http://127.0.0.1:3344 # 配置中心的地址
# 上面配置项综合起来就是 main 分支上的config-dev.yml 读取路径为 http://127.0.0.1:3344/main/config-dev.yml
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://127.0.0.1:7001/eureka/
management:
endpoints:
web:
exposure:
include: "*" # 暴露监控端点
3、修改业务方法,区分是哪一个服务
/**
* @description: ConfigController
* @date: 2022/2/20 19:58
*/
@Slf4j
@RefreshScope
@RestController
@RequestMapping(path = "/config")
public class ConfigController {
@Value(value = "${config.info}")
private String configInfo;
@Value(value = "${server.port}")
private String serverPort;
@RequestMapping(path = "/getConfigInfo", method = RequestMethod.GET)
public R getConfigInfo() {
return R.ok().put("data", configInfo).put("port", serverPort);
}
}
至此,我们的环境已经搭建完毕。前面也说了Spring Cloud Bus
配合Spring Cloud Config
使用可以实现配置的动态刷新,那么为什么要这样设计呢?实现这一功能的思想又是什么呢?
1、利用消息总线触发一个客户端ConfigClient的/bus/refresh
,而刷新所有客户端的配置。
2、利用消息总线触发一个服务端ConfigServer的/bus/refresh
,而刷新所有客户端的配置。
两种广播方式,应该选择第二种,由ConfigServer
来/bus/refresh
更合适因为:
因此我们在ConfigServer-3344
服务模块中配置全局广播的支持,实现自动刷新配置。
1、新增RabbitMQ
的stater坐标依赖以及actuator
的坐标依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bus-amqpartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
2、修改application.yml
文件,新增RabbitMQ
和actuator
的一些配置
server:
port: 3344
spring:
application:
name: cloud-config-center
cloud:
config:
server:
git:
uri: https://github.com/laizhenghua2/springcloud-config.git # GitHub HTTPS 仓库地址
search-paths:
- spring-cloud-config # 搜索目录
force-pull: true
username: xxxx # GitHub 账号
password: xxxx # GitHub 密码
label: main # 读取的分支
# RabbitMQ的配置
rabbitmq:
host: 180.76.238.29
port: 5672
username: guest
password: guest
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://127.0.0.1:7001/eureka/
# RabbitMQ相关配置,暴露bus刷新配置的端点
management:
endpoints:
web:
exposure:
include: 'bus-refresh'
3、给cloud_config_client_3355
和cloud_config_client_3366
客户端添加消息总线的支持
pom.xml
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bus-amqpartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
bootstrap.yml
server:
port: 3366
spring:
application:
name: config-client
cloud:
config:
label: main # 分支名称
name: config # 配置文件名称
profile: dev
uri: http://127.0.0.1:3344 # 配置中心的地址
# 上面配置项综合起来就是 main 分支上的config-dev.yml 读取路径为 http://127.0.0.1:3344/main/config-dev.yml
rabbitmq:
host: 180.76.238.29
port: 5672
username: guest
password: guest
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://127.0.0.1:7001/eureka/
management:
endpoints:
web:
exposure:
include: "*" # 暴露监控端点
4、启动服务,进行测试,一次修改,广播通知,处处生效。
curl -X POST "http://127.0.0.1:3344/actuator/bus-refresh"
SpringCloud Stream
的产生绝非偶然,传统的消息中间件有ActiveMQ / RabbitMQ / RocketMQ / Kafka
等,当项目到达一定规模的时候,有可能存在两种MQ!例如项目后台使用RabbitMQ
,大数据这边使用Kafka
,此时也带来了一些痛点,切换、开发、维护非常的不方便。
因此出现了一种新的技术SpringCloud Stream
,官方定义是:一个构建消息驱动微服务的框架。让我们不再关注具体的MQ
的细节,我们只需要用一种适配绑定的方式,自动的给我们在各种MQ
内切换。就像Hibernate
一样统一提供sesstion
API,无需关注各数据库厂商的语法。
如上图蓝色部分所示,SpringCloud Stream
的核心就是Binder
,应用程序通过inputs
或者outputs
来与Spring Cloud Stream中的Binder
对象交互,通过我们配置来binding(绑定)
,而Spring Cloud Stream 的Binder
对象负责与消息中间件交互,所以我们只需搞清楚如何与Spring Cloud Stream 交互就可以方便使用消息驱动的方式。
另外Spring Cloud Stream
为一些供应商的消息代理中间件产品提供了个性化的自动化配置实现,引用了发布-订阅、消费组、分区的三个核心概念。但是目前只支持RabbitMQ
和Kafka
。
一句话总结就是:屏蔽底层消息中间件的差异,降低切换成本,统一消息的变成模型。
在传统的MQ
中,我们知道生产者 / 消费者之间是靠消息媒介传递消息内容(Message),消息也必须走特定的通道(Message Channel),因此我们更多关注的是通道里的消息如何被消费?谁来负责收发处理?
消息通道Message Channel
的子接口Subscribale Channel
,由MessageHandler
消息处理器所订阅。
而到了Stream
这里,设定一个目标绑定器(Destination Binder)
负责在消息通道里的消息收发处理!
在没有绑定器这个概念的情况下,我们的SpringBoot应用要直接与消息中间加进行信息交互的时候,由于消息中间件构建的初衷不同,他们的实现细节上也会有较大的差异,通过定义绑定器作为中间层,完美的实现了应用程序与消息中间件细节之间的隔离。通过向应用程序暴露统一的Channel
通道,使得应用程序不需要再考虑各种不同的消息中间件的实现。
即:通过定义绑定器Bander
作为中间层,实现了应用程序与消息中间件细节之间的隔离。
而且Stream
中的消息通信方式也遵循了发布-订阅模式,使用Topic
主题进行广播,在RabbitMQ
就是Exchange
,在Kafka
中就是Topic
。
1、Binder
:很方便的连接中间件,屏蔽差异,是应用于消息中间件的封装,目前只实现了Kafka
与RabbitMQ
的Binder。
2、Channel
:通道,是队列Queue
的一种抽象,在消息通讯系统中就是实现存储和转发的媒介,通过Channel
队列进行配置
3、Source
和Sink
:简单的可以理解为参照对象是Spring Cloud Stream
自身,从Stream
发布消息就是输出,接受消息就是输入。
注解 | 说明 |
---|---|
@Input |
注解标识输入通道,通过该输入通道接收到的消息进入应用程序 |
@Output |
注解标识输入通道,发布的消息将通过该通道离开应用程序 |
@StreamListener |
监听队列,用于消费者的队列的消息接收 |
@EnableBinding |
指信道channel和exchange绑定在一起 |
关于实验环境首先需要确保RabbitMQ
环境OK!其次除了注册中心,还需要有如下模块:
cloud_rabbitmq_provider_8801
作为生产者进行发消息模块cloud_rabbitmq_consumer_8802
作为消息接收模块cloud_rabbitmq_provider_8803
作为消息接收模块
搭建工程模块略!建议就是使用SpringBoot
的初始化向导进行搭建,版本适配问题经常发生,导致项目启动各种报错。 SpringBoot
的初始化向导自动集成了spring-boot-starter-parent
,也就是版本仲裁机制,让我们无需关注依赖的版本号。因此为了避免报错建议还是使用初始化向导进行搭建。
例如核心的依赖(这里使用consul
)做为服务的注册中心。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-consul-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-stream-rabbitartifactId>
dependency>
提供者配置文件application.yml
示例(注意bindings
配置项提供者是output
):
server:
port: 8801
spring:
application:
name: cloud-provider-service
rabbitmq:
host: IP地址
port: 5672
username: admin
password: admin
cloud:
# consul 的配置
consul:
host: IP地址
port: 8500
discovery:
service-name: ${spring.application.name}
heartbeat:
enabled: true
stream:
binders: # 配置需要绑定的rabbitmq消息
defaultRabbit:
type: rabbit # 消息组件类型
bindings:
output: # 这个名字是一个通道的名称
destination: testExchange # 表示要使用的Exchange名称
content-type: application/json # 设置消息类型,本次为json
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
消费者者配置文件application.yml
示例(注意bindings
配置项消费者是input
):
server:
port: 8802
spring:
application:
name: cloud-consumer-service
rabbitmq:
host: IP地址
port: 5672
username: admin
password: admin
cloud:
# consul 的配置
consul:
host: IP地址
port: 8500
discovery:
service-name: ${spring.application.name}
heartbeat:
enabled: true
stream:
binders: # 配置需要绑定的rabbitmq消息
defaultRabbit:
type: rabbit # 消息组件类型
bindings:
input: # 这个名字是一个通道的名称
destination: testExchange # 表示要使用的Exchange名称
content-type: application/json # 设置消息类型,本次为json
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
搭建好所有工程后,我们启动我们的服务。
启动成功后,查看RabbitMQ
的exchanges
我们发现正是我们配置文件配置的通道。OK
至此所有环境已经准备完毕!接下来就是编写业务测试方法进行实验。
1、provider_8801
编写消息发送方法
controller
/**
* @description: MessageController
* @date: 2022/2/23 20:02
*/
@Slf4j
@RestController
@RequestMapping(path = "/provider")
public class MessageController {
@Autowired
private MessageService messageService;
@RequestMapping(path = "/send", method = RequestMethod.GET)
public R sendMessage() {
return R.ok().put("data", messageService.send());
}
}
service
/**
* @description: MessageService
* @date: 2022/2/22 22:27
*/
@Slf4j
@EnableBinding(value = {Source.class}) // 定义消息的推送管道
public class MessageServiceImpl implements MessageService {
@Autowired
private MessageChannel output; // 消息发送管道
@Override
public String send() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String date = sdf.format(new Date());
output.send(MessageBuilder.withPayload(date).build());
log.info("message -> " + date);
return date;
}
}
2、8801服务执行发送方法
我们发现消息提供者8801
服务已经发送了两条消息,做为消息消费者的8802
和8803
如何通过Stream
消费消息呢?
3、消息消费者8802/8803
编写监听(消费)方法
/**
* @description: ConsumerController
* @date: 2022/2/23 21:02
*/
@Slf4j
@RestController
@EnableBinding(Sink.class)
public class ConsumerController {
@Value(value = "${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT)
public void receiveMessage(Message<String> message) {
log.info("service [" + serverPort + "] message received : " + message.getPayload());
}
}
4、重新启动消费者服务,进行测试(生产者调用方法即可看到效果)
前面的案例,我们已经完成了使用SpringCloudStream
进行消息的生产与消费,并且没有写一行操作RabbitMQ
的Java
代码,完全屏蔽了RabbitMQ
的内部实现细节。
到这里看似已经万事大吉了,其实并没有,通过Stream
消费消息还会产生两种问题:
这两种问题使用过程中也是一定要避免的,特别是重复消费问题,如果处理不好Sream
会带来不必要的麻烦!
也就是说目前消费者是集群环境,会同时消费8801
发送过来的消息,存在重复消费的问题。就会造成数据错误,我们得避免这种情况,此时我们就可以使用Stream
中的消息分组来解决。
消息分组:在Stream
中处于同一个Group
中的多个消费者是竞争关系,就能保证消息只会被其中的一个应用消费一次,不同组是可以全面消费的(重复消费)。
总结:同一组内会发生竞争关系,只有其中一个可以消费。
可以看下RabbitMQ
的交换机的Bandings
,默认是有几个服务就分几个组(组流水号不一样)!导致不同组全面消费。
那么如何让8802
和8803
变为一组呢?修改配置(增加一个group
配置项)!例如8802配testA
,例如8803配testB
bindings:
input: # 这个名字是一个通道的名称
destination: testExchange # 表示要使用的Exchange名称
content-type: application/json # 设置消息类型,本次为json
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
group: testB
效果如下,配置同一个,就可以让他们变为同一组,进而解决重复消费问题,并且分为同一组后自动采取轮询策略,交替消费服务。