微服务模块构建
一、创建父工程
创建Maven父工程用于依赖的版本控制管理
-
创建一个maven工程,只保留pom文件
需要做相关的修改:
-
修改Pom文件
-
将父工程的‘packaging’设置为pom
-
添加依赖管理版本统一管理和依赖管理
<packaging>pom</packaging> <!--统一管理jar包版本--> <properties> <projetc.build.sourceEncoding>UTF-8</projetc.build.sourceEncoding> <maven.compiler.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> <junit.version>4.12</junit.version> <log4j.version>1.2.17</log4j.version> <lonbok.version>1.16.18</lonbok.version> <mysql.version>5.1.47</mysql.version> <druid.version>1.1.16</druid.version> <mybatis.spring.boot.version>1.3.0</mybatis.spring.boot.version> </properties> <!--子模块继承后,不用写version--> <dependencyManagement> <dependencies> <!--spring boot 2.2.2--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>2.3.8.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <!--spring cloud Hoxten--> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>Hoxton.SR10</version> <type>pom</type> <scope>import</scope> </dependency> <!--spring cloud alibaba 2.1.0--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-alibaba-dependencies</artifactId> <version>2.1.0.RELEASE</version> <type>pom</type> <scope>import</scope> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.version}</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>${druid.version}</version> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>${mybatis.spring.boot.version}</version> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <fork>true</fork> <addResources>true</addResources> </configuration> </plugin> </plugins> </build>
-
二、创建provider-payment子模块
在父工程的基础上创建新的Module,需要继承父工程
1、修改POM文件
修改新建的Module的pom文件,添加相关的依赖(由于在父工程中已经进行相关的管理,所以只需要加入groupId和artifactId即可)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.5</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2、修改yml配置文件
server:
port: 8001 # 端口
spring:
application:
name: cloud-provider-payment # 微服务名称
datasource: # 数据库链接
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: org.gjt.mm.mysql.Driver
url: jabc:mysql://localhost:3306/dbcloud02?useUnicode=true&characterEncoding=utf-8&useSSL=false
data-username: root
data-password: root
mybatis: # 整合mybatis
mapper-locations: classpath:mapper/*.xml # 映射配置文件位置
type-aliases-package: com.zjj.cloud02.pojo # 别名所在包
3、创建启动类
@SpringBootApplication
public class PaymentMain {
public static void main(String[] args) {
SpringApplication.run(PaymentMain.class,args);
}
}
4、业务逻辑
-
创建数据库
create table `payment`( id BIGINT(20) primary key auto_increment, `serial` VARCHAR(200) DEFAULT '' )ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8
-
创建实体类、创建统一返回的结果CommonResult类
-
创建Dao层:PaymentMapper、PaymentMapper.xml
-
创建service层,PaymentService及PaymentServiceImpl
-
创建Controller层,PaymentController
-
使用postman测试
三、创建consumer-payment子模块
1.修改pom文件
2.创建yml文件:端口80
3.创建启动类
4.业务逻辑
// controller
@RestController
@Slf4j
public class PaymentController {
@Autowired
private RestTemplate restTemplate; // 这里需要创建一个配置类,注册RestTemplate的Bean
static final String host = "http://localhost:8001";
@GetMapping("/consumer/save")
public CommonResult saveOrder(Payment payment){
return restTemplate.postForObject(host+"/payment/save",payment,CommonResult.class );
}
@GetMapping("/consumer/select/{id}")
public CommonResult selectById(@PathVariable(value = "id") long id){
return restTemplate.getForObject(host+"/payment/select/"+id, CommonResult.class);
}
}
// config
@Configuration
public class ApplicationConfig {
@Bean
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
}
四、抽取通用部分,构建Common通用子模块
该模块用于存放重复类或工具类
Eureka组件
一、创 建eureka模块
1.修改pom,需要用到新的依赖,如下
<!--eureka导入-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
2.修改yml文件
server:
port: 5000
spring:
application:
name: eureka
eureka:
instance:
hostname: localhost # eureka服务的IP
client:
registerWithEureka: false # 本身就是注册中心,不需要注册
fetchRegistry: false # 本身就是注册中心,不需要获取注册信息
serviceUrl:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/
3.创建启动类
需要在主启动类上添加@EnableEurekaServer
注解,表名当前微服务是eureka服务注册中心
二、注册provider
1.添加依赖
<!--eureka-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
2.修改yml
# 添加eurek相关配置
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://localhost:5000/eureka/ # 注册中心地址
3.修改主启动类
添加@EnableEurekaClient
注解,表名当前微服务是eureka服务注册中心,可省略
三、注册Consumer
同上。
四、eureka注册中心集群
1.创建一个新的eureka注册中心(同第一个注册中心),端口:5001
2.修改yml配置文件
由于是在本地集群模拟,所以需要修改hosts文件,配置如下
127.0.0.1 eureka5000.com
127.0.0.1 eureka5001.com
127.0.0.1 eureka5002.com
# 5000
server:
port: 5000
spring:
application:
name: eureka5000
eureka:
instance:
hostname: eureka5000.com # eureka1的地址名称
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://eureka5001.com:5001/eureka/ # 在eureka5001注册该注册中心
# 5001
server:
port: 5001
spring:
application:
name: eureka5001
eureka:
instance:
hostname: eureka5001.com # eureka2的地址名称
client:
registerWithEureka: false
fetchRegistry: false
serviceUrl:
defaultZone: http://eureka5000.com:5000/eureka/ # 在eureka5000注册该注册中心
在使用eureka注册中心进行集群的时候,需要相互注册
3.修改consumer、provider的注册位置,需要同时指向两个注册中心
defaultZone: http://eureka5000.com:5000/eureka/,http://eureka5001.com:5001/eureka/
五、服务提供者集群
1.创建一个新的provider,代码级别的内容与8001端口的一致,不需要修改
2.配置文件只需要改变端口即可,将端口改为8002(因为是集群,所以微服务的名称需要一致,在eureka中可以看到如下)
六、开启Consumer的负载均衡(轮询)
(与nginx的负载均衡类似)
1.在consumer的配置文件中注册RestTemplate的方法上加上@LoadBalancer
,开启负载均衡
2.在Controller中,不再使用provider的IP+PORT,而是使用provider的微服务名称(在eureka中的注册名称)
测试:测试结果可见,每次访问都会来回切换两个provider的访问。
tips:服务信息的显示配置如下:
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka5001.com:5001/eureka/,http://eureka5002.com:5002/eureka/
instance:
instance-id: payment8001 # 设置该微服务在eureka中的名称
prefer-ip-address: true # 显示ip
七、Discovery的初步接触
1.在provider的controller中注入DiscoveryClient
@Autowired
private DiscoveryClient discoveryClient;
2.编写方法获取相关信息
@GetMapping(value = "/payment/get/discovery")
public Object discovery(){
// 获取全部的实例
List<String> services = discoveryClient.getServices();
for (String s : services) {
log.info("======="+s);
}
// 获取某个实例的相关信息
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PROVIDER-PAYMENT");
for (ServiceInstance instance : instances) {
log.info(instance.getInstanceId() + "\t" + instance.getHost() + "\t" + instance.getPort() + "\t" + instance.getUri());
}
return discoveryClient;
}
3.在启动类上添加@EnableDiscoveryClient
注解
4.访问该url即可获取相关信息
eureka的自我保护机制:eureka在一段时间内没有收到某个微服务的心跳,不会立刻将该微服务剔除。会保留错误的微服务,使eureka更加健壮。
相关配置:
# 客户端
# eureka客户端向服务端发送心跳的时间间隔
lease-renewal-interval-in-seconds: 1
# 服务器收到最后一次心跳后,最长的等待时间,超时将剔除
lease-expiration-duration-in-seconds: 2
# 服务器端
server:
enable-self-preservation: false # 关闭自我保护机制
使用zookeeper
待
使用consul组件
待
Ribbon负载均衡工具
使用在客户端
一、RestTemplate简单使用
ForEntity与ForObject的区别:ForEntity返回的是一个对象里面包含相应状态码,响应头,响应体等信息;而ForObject返回的是一个字符串,主要包响应体
// 使用postForObject,返回json
@GetMapping("/consumer/save")
public CommonResult saveOrder(Payment payment){
return restTemplate.postForObject(Payment_URL+"/payment/save",payment,CommonResult.class );
}
// 使用postForEntity,ResponseEntity对象
@GetMapping("/consumer/save/postForEntity")
public CommonResult saveOrder1(Payment payment){
ResponseEntity<CommonResult> entity = restTemplate.postForEntity(Payment_URL + "/payment/save", payment, CommonResult.class);
if (entity.getStatusCode().is2xxSuccessful()){
return new CommonResult(200,"postForEntity success",entity.getBody());
}else{
return new CommonResult(444,"postForEntity false");
}
}
// 使用getForObject,返回字符串
@GetMapping("/consumer/select/{id}")
public CommonResult selectById(@PathVariable(value = "id") long id){
return restTemplate.getForObject(Payment_URL+"/payment/select/"+id, CommonResult.class);
}
// 使用getForEntity,返回entity对象
@GetMapping("/consumer/select/getForEntity/{id}")
public CommonResult selectById2(@PathVariable(value = "id") long id){
ResponseEntity<CommonResult> entity = restTemplate.getForEntity(Payment_URL + "/payment/select/" + id, CommonResult.class);
if(entity.getStatusCode().is2xxSuccessful()){
return new CommonResult(200,"getForEntity success",entity.getBody());
}else{
return new CommonResult(444,"getForEntity false");
}
}
二、Ribbon自带负载均衡算法的使用切换
Ribbon的规则都是实现于IRule接口,有以下的算法:
- RoundRobinRule:轮询(默认)
- RandomRule:随机
- RetryRule:
- WeightedResponseTimeRule:按照响应速度选择
- BestAvailableRule:
- AvailabilityFilteringRule:
- ZoneAvoidanceRule:
操作步骤:
-
创建一个新的包(
注意:此包不能在@ConponentScan注解所在包及其子包下,否则会失效
) -
在该包下创建一个配置类,注册一个规则
@Configuration public class MyRule { @Bean public RandomRule getRule(){ return new RandomRule(); // 随机规则 } }
-
在主启动类上添加注解
@RibbonClient
,表示使用配置方式@RibbonClient(name = "CLOUD-PROVIDER-PAYMENT",configuration = MyRule.class) // 服务名称,配置类
轮询机制的实现原理:
使用当前是第几次访问(current)%服务数量(modulo),以得到的余数为下标选择对应的服务器;
当服务器重启后,将会从1开始计数。
List<ServiceInstance> instances = discoveryClient.getInstances("CLOUD-PROVIDER-PAYMENT");
instances.get(1);
instances.get(2);
instances.get(1);
instances.get(2);
......
OpenFeign组件
与Ribbon一样,作用在客户端。只需要编写接口,并添加注解@FeignClient
即可使用。
Feign中带有Ribbon,所以配置Feign后,会自动开启负载均衡
一、简单的使用
1.新建一个consumer的module
2.添加新的依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
3.配置yml文件
server:
port: 82
eureka:
client:
register-with-eureka: false
service-url:
defaultZone: http://eureka5000.com:5000/eureka/,http://eureka5001.com:5001/eureka/
spring:
application:
name: FeignConsumer
4.创建启动类
注意添加@EnableFeignClient
注解,表示启用Feign组件
5.创建接口
需要添加注解@FeignClient,参数为调用的微服务名称
@Component
@FeignClient(value = "CLOUD-PROVIDER-PAYMENT")
public interface PaymentService {
// 抽象方法为服务提供方的service接口的抽象方法,
// 在这里直接使用服务提供方的controller方法,但是底层依然是使用service接口的方法
@GetMapping("/payment/select/{id}")
public CommonResult selectPaymentById(@PathVariable(value = "id") long id);
}
二、Feign超时
Feign默认等待服务端响应的时间为1s,超时将会报错
测试:在服务提供端,设置sleep : 3000,此时consumer调用服务端服务就会进入超时状态
设置超时时间(使用ribbon设置):
# 修改配置文件
ribbon:
ReadTimeout: 3000
ConnectTimeout: 3000
三、Feign的日志
通过配置Feign的日志,可以打印Feign的相关信息。可以打印Feign的以下几个level
- NONE
- BASIC
- HEADERS
- FULL
配置步骤:
1.创建配置类,注册Feign的配置级别
@Configuration
public class LogConfig {
@Bean
Logger.Level getLevel(){
return Logger.Level.FULL;
}
}
2.yml配置答应日志的级别
logging:
level:
# 接口的全类名:级别
com.zjj.cloud01.service.PaymentService: debug
Hystrix断路器
一、相关概念
在分布式中,一个客户请求可能会有很多个依赖,当一个依赖出现故障时可能会导致该用户长时间等待和占用线程,故而可能引发资源浪费、甚至系统崩溃的后果。为了避免这种情况,服务熔断机制就应运而生,当某个服务出现故障时,系统可以根据预备方案做出处理,避免出现系统破溃等问题。
Hystrix时处理分布式系统延迟和容错的开源库
Hystrix功能:
- 服务降级
- 服务熔断
- 接近实时的监控
- …
相关概念:
-
服务降级(FallBack):当服务出现故障,给用户反馈(如提示系统繁忙,请稍后再试等)
触发降级的原因:
- 程序异常
- 超时
- 服务熔断触发降级
- 线程池导致服务降级
-
服务熔断(break):当服务器达到最大访问量后,直接拒绝访问,并调用服务降级方法返,友好返回
-
服务限流(flowlimit):高并发操作时,限制访问,如每秒钟只能访问n个
JMETER压测:
环境:
- 两个服务:1.直接返回(模拟简单请求,速度快)2.等待3秒返回(模拟复杂请求,速度慢)
- 在正常情况下,二者互不干扰
测试:使用jmeter压测开启200个线程和100次循环/每秒。
压测可以直观显示:当大量访问复杂的服务时,服务器(tSpringboot默认omcat)内部线程资源就会被全部使用,从而导致简单的请求也需要排队等待线程的分配,所以简单的服务响应时间也会变得很慢。这时,就需要使用到Hystrix的服务降级等机制。
二、服务降级
既可以放在消费者,也可放在提供者,一般放在消费者端
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-puMyLd9y-1619615404043)(C:\Users\18264\Desktop\学习文档\springCloud\springCloud.assets\image-20210425184645608.png)]
1.服务提供端自身的服务降级:
如果服务端调用自身的方法出错(超时、系统错误等),就会服务降级,返回可预期的结果
-
为方法添加注解
@HystrixCommand
-
创建降级处理方法
相关信息可以在源码HystrixCommandProperties类中查看
@Override @HystrixCommand(fallbackMethod = "fallBackMethod",commandProperties = { // 设置超时时间 @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "4000") }) public String Hystrix_ERROR(Long id) { int a = 1/0; // try { // Thread.sleep(3000); // } catch (InterruptedException e) { // e.printStackTrace(); // } return "【" + Thread.currentThread().getName() + "】 Hystrix_ERROR id:" + id; } public String fallBackMethod(Long id){ return "【" + Thread.currentThread().getName() + "】 系统错误或请求超时 id:" + id; }
-
为主启动类加上注解
@EnableCircuitBreaker
结果:返回 “【” + Thread.currentThread().getName() + “】 系统错误或请求超时 id:” + id;
2.在Consumer端进行服务降级
-
添加依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency>
-
修改yml
# 开启Hystrix功能 feign: hystrix: enabled: true
-
主启动类添加注解
@EnableHystrix
-
为需要处理的请求添加@HystrixCommand注解
@GetMapping("/consumer/Hystrix/error/{id}") @HystrixCommand(fallbackMethod = "consumerFallBackMethod",commandProperties = { // 设置超时时间 @HystrixProperty(name="execution.isolation.thread.timeoutInMilliseconds",value = "2000") }) public String hystrix_error(@PathVariable(value = "id") Long id){ return paymentService.Hystrix_ERROR(id); } public String consumerFallBackMethod(Long id){ return "consumer timeout ~~~~"; }
结果:由于在服务端设置的超时时间是4000ms,而客户端设置的是2000ms,在业务逻辑中设置的sleep(3000),所以会直接返回:
"consumer timeout ~~~~"
@DefaultProperties(defaultFallback = “global_Fallback_method”)注解的使用:
当为每一个方法绑定一个服务降级方法会导致代码膨胀,所以可以使用通用的降级方法。
在类上添加注解@DefaultProperties(),则对于该类下的每个加有@HystrixCommand注解的服务方法,都会使用默认(通用)的降级方法;而对于特殊的需要单独定制的服务方法,只需要在@HystrixCommand()的属性中添加具体的方法名称并创建该方法即可。
3.对Consumer的feign接口进行服务降级
由于,客户端使用feign实现负载均很等操作,所以一定会使用标注@FeignClient注解的接口,所以只需要实现该接口,即可作为服务降级方法。
如此设置,则不在客户端的controller的方法上标注@HystrixCommand也可以在出错或其他异常的情况下实现服务降级。
因为实现接口时,实现类中会重写方法,所以接口中的请求方法与实现类中的服务降级方法一一对应,就能解决代码混乱的情况
-
修改yml
# 开启Hystrix功能 feign: hystrix: enabled: true
-
实现Feign的接口:注意添加@Componet注解才能被容器扫描到
@Component public class PaymentServiceImpl implements PaymentService{ @Override public String Hystrix_OK(Long id) { return "Hystrix_OK method error"; } @Override public String Hystrix_ERROR(Long id) { return "Hystrix_ERROR method error"; } }
-
@FeignClient
注解添加属性@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT",fallback = PaymentServiceImpl.class) // 指定服务降级方法类类
三、服务熔断
什么是服务熔断?
当服务出现故障的时候(服务不可用或响应时间太长),对服务进行降级,处于自我保护的目的,当失败达到一定阈值时,执行熔断(类似于保险丝)。其后,对部分请求尝试调用方法,进入半开状态。当服务正常后,会自动恢复链路调用。
流程:服务降级>>>熔断开启>>>服务恢复
开启服务熔断同样使用@HystrixCommand注解
测试:
背景:调用服务端的服务:通过id查询相关信息,如果id的值>=0则正常访问,如果id<0则抛出异常,请求失败,服务降级
-
服务端service代码
@HystrixCommand(fallbackMethod = "Hystrix_break_method",commandProperties = { // 开启服务熔断 @HystrixProperty(name = "circuitBreaker.enabled", value = "true"), // 请求数量(阈值) @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), // 窗口期(时间) @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), // 在窗口期内请求失败百分比,当达到该值时,开启熔断 @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60") }) public String Hystrix_break(Long id){ if(id < 0 ){ throw new RuntimeException("id错误"); } String ID = IdUtil.simpleUUID(); return "【"+ Thread.currentThread().getName() +"】 ID:" + ID; } /*服务熔断服务降级方法*/ public String Hystrix_break_method(Long id){ return "请求失败,id错误:" + id; }
-
服务端Controller代码
/*======服务熔断测试=======*/ @GetMapping("/hystrix/break/{id}") public String Hystrix_break(@PathVariable(value = "id") Long id){ return paymentService.Hystrix_break(id); }
-
结果:当id为正常id时,会返回正常的预期结果;当id为负数时,便会返回服务降级提供的错误信息返回;当在10秒内的10次请求的失败率达到60%时,出现服务熔断,即:当发送正确请求时,也会返回服务降级的结果,等待一段时间后,再次发送正确请求,返回正确的结果(服务自动恢复)
注意:当在窗口期内请求次数没有达到设定的请求次数阈值,即使请求全部失败,也不会开启熔断。默认值:10s内20次请请求,50%通过率
四、服务限流
使用alibaba的sentienl
五、HystrixDashboard
Hystrix的监控可视化,可以查看请求的数量,请求成功与否,熔断器是否打开
配置步骤:
监控程序配置:
-
创建一个新的Module,用于开启监控程序
-
pom新增依赖有:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId> </dependency>
-
yml配置
server: port: 9001 hystrix: dashboard: proxy-stream-allow-list: - 'localhost'
-
主启动类添加注解
@EnableHystrixDashboard
被监控程序配置:
-
添加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
-
修该yml
management: endpoints: web: exposure: include: 'hystrix.stream'
访问地址:http://localhost:8001/actuator/hystrix.stream (被监控程序ip+port/actuator/hystrix.stream)即可图形化显示
Gateway网关
为什么用Gateway?与Zuul的区别?
Gatewway的三个核心概念:
- 路由
- 断言
- 过滤
一、路由转发
配置:
-
创建新的module用于启动gateway
-
添加依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
注:网关需要注册进入eureka,所以需要添加eureka-client依赖;添加gateway依赖(该依赖由spring官方提供),在添加该依赖时,一定不能在添加web依赖(如spring-boot-starter-web等),否则会报错
-
配置yml
server: port: 9500 spring: application: name: Payment-Gateway # 配置网关信息 cloud: gateway: # 配置路由信息 routes: - id: payment-route1 # 路由名称 uri: http://localhost:8001 # 转发的地址 predicates: - Path=/payment/select/** # 断言,只有当url与断言的url配置(true)时才进行转发 - id: payment-route2 #第二个路由 uri: http://localhost:8001 predicates: - Path=/payment/port/** # 需要将网关注册进入注册中心 eureka: client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://eureka5001.com:5001/eureka/,http://eureka5002.com:5002/eureka/ instance: instance-id: gateway prefer-ip-address: true
对网关配置解释:在以上配置中,包含两个路由,两个服务地址都是在同一个微服务项目中;当url与“/payment/select/**”匹配时,转发到该地址,否则不转发;第二个路由也同理。
-
配置启动类
@SpringBootApplication @EnableEurekaClient
添加两个注解即可
-
测试:
在配置完成后,依次启动eureka,provider8001,gateway。
在注册中心中可以看到注册了两个微服务(provider8001、gateway)。
调用服务方式一:http://localhost:8001/payment/select/2 可以调用其服务
调用服务方式二:http://localhost:9500/payment/select/2 也可以调用其服务
由此可见,网关配置实现,即访问网关地址,由网关对其进行解析和路由转发,以达到保护微服务的目的。
路由配置方式二:代码配置
创建一个配置类实现配置
@Configuration
public class GatewayConfig {
@Bean
public RouteLocator getRoutes(RouteLocatorBuilder builder){
RouteLocatorBuilder.Builder routes = builder.routes();
routes.route("toute1", // 路由id
r -> r.path("/springboot") // 网关url
.uri("https://spring.io/projects/spring-boot")) // 转发地址
.build();
routes.route("toute2",
r -> r.path("/springcloud")
.uri("https://spring.io/projects/spring-cloud"))
.build();
routes.route("toute3",
r -> r.path("/guoji")
.uri("https://news.baidu.com/guoji"))
.build();
return routes.build();
}
}
访问localhost:9500/guoji即可转发到https://news.baidu.com/guoji(但是该网站的其他页面点击无效,因为没有对其进行配置);测试中,前两个没有成功显示,只显示了一半(css等没显示),个人认为时https的原因。
二、动态路由
由于服务提供者(provider)一般都是集群的,所以不能将uri写死,这就需要动态路由使用微服务名称来实现动态路由
修改yml配置文件,主要变动如下:
- 开启动态路由:discovery.locator.enabled=true
- 配置uri:uri: lb://CLOUD-PROVIDER-PAYMENT
server:
port: 9500
spring:
application:
name: Payment-Gateway
# 配置网关信息
cloud:
gateway:
# 开启动态路由
discovery:
locator:
enabled: true
# 配置路由信息
routes:
- id: payment-route1 # 路由名称
# uri: http://localhost:8001 # 转发的地址
uri: lb://CLOUD-PROVIDER-PAYMENT # 匹配后提供服务的路由地址(服务名称)
predicates:
- Path=/payment/select/** # 断言,只有当url与断言的url配置(true)时才进行转发
- id: payment-route2 #第二个路由
# uri: http://localhost:8001
uri: lb://CLOUD-PROVIDER-PAYMENT
predicates:
- Path=/payment/port/**
# 需要将网关注册进入注册中心
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka5001.com:5001/eureka/,http://eureka5002.com:5002/eureka/
instance:
instance-id: gateway
prefer-ip-address: true
测试结果,通过网关访问:http://localhost:9500/payment/select/1 将会负载均衡,在两台微服务(8001、8002)之间切换提供服务。
三、断言规则
官网:https://docs.spring.io/spring-cloud-gateway/docs/3.0.2/reference/html/#gateway-request-predicates-factories
断言规则:
-
# 表示请求的时间必须在该时间之后 - After=2017-01-20T17:42:47.789-07:00[America/Denver]
-
# 表示请求时间必须在该时间之前 - Before=2017-01-20T17:42:47.789-07:00[America/Denver]
-
#表示请求时间必须在改时间段之内 - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
-
# 表示请求必须带有cookie,且以k,v键值对方式 - Cookie=chocolate, ch.p
-
# 表示请求头要包含后面正则表达式中的信息 - Header=X-Request-Id, \d+
-
# 主机 - Host=**.somehost.org,**.anotherhost.org
-
# 请求方法 - Method=GET,POST
-
# 请求路径 - Path=/red/{segment},/blue/{segment}
-
# 请求参数 - Query=green
-
# 远程主机地址指定 - RemoteAddr=192.168.1.1/24
-
- Weight=group1, 2
个别规则的测试:
-
在指定时间之后才能进行访问。如下:在上海时间xxxxx之后才能访问
predicates: - Path=/payment/select/** # 断言,只有当url与断言的url配置(true)时才进行转发 - After=2021-04-26T12:32:06.165+08:00[Asia/Shanghai]
-
只有在带有cookie的请求才能访问
predicates: - Path=/payment/select/ - Cookie=username,zjj
测试:
使用dos命令行进行测试
C:\Users\18264>curl http://localhost:9500/payment/select/1 # 访问失败
C:\Users\18264>curl http://localhost:9500/payment/select/1 --cookie "username=zjj" # 访问成功
四、Filter
对请求的过滤,符合条件的给予放行
GaateFilter
官网:https://docs.spring.io/spring-cloud-gateway/docs/3.0.2/reference/html/#gatewayfilter-factories
GlobalFilter
官网:https://docs.spring.io/spring-cloud-gateway/docs/3.0.2/reference/html/#global-filters
自定义GlobalFilter
配置一个自定义的过滤器
需求:设置一个过滤器,只有当请求中包含有username的key时,且key不为空时,请求有效
只需要创建一个过滤器类,实现接口 GlobalFilter, Ordered,并将其注入容器即可
@Component
@Slf4j
public class ParamKeyFilter implements GlobalFilter, Ordered {
// 该方法对请求进行过滤检查
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("********进入ParamKeyFilter过滤器"+ ZonedDateTime.now());
// 获取请求中的username的值
String username = exchange.getRequest().getQueryParams().getFirst("username");
// 如果username为空,则设置响应状态码为请求失败
if(username == null){
log.info("非法请求,请求失败");
exchange.getResponse().setRawStatusCode(HttpStatus.SC_NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
// 否则,放行
return chain.filter(exchange);
}
// 该方法表示该过滤器的处理级别(优先级)
@Override
public int getOrder() {
return 0;
}
}
Config配置
什么是SpringCloud Config?
SpringCloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的所有环境提供了一个中心化的外部配置。
Config分为服务端和客户端,服务端也称为分布式配置中心,他是一个独立的微服务应用,用来连接配置服务器并为客户端提供获取配置信息,加密、解密信息等访问接口。客户端则是通过指定的配置中心来管理引用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息。
一、ConfigServer
配置中心是一个微服务,所以需要创建新的module
git中的配置信息为:
-
创建新的module:springCloud01-config-center
-
添加pom
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-config-server</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency>
-
配置yml
server: port: 2000 spring: application: name: config-center cloud: config: server: git: # git仓库名称 uri: https://gitee.com/wuraoo/springcloud01_config.git # 搜索目录 search-paths: springcloud01_config # 分支 label: master eureka: client: fetch-registry: true register-with-eureka: true service-url: defaultZone: http://eureka5001.com:5001/eureka/,http://eureka5002.com:5002/eureka/ instance: prefer-ip-address: true instance-id: config-center
-
配置主启动类:需要添加开启config注解
@EnableConfigServer
-
测试:访问http://localhost:2000/master/config-info.yml 地址,即可看到git仓库中的配置文件的内容,即配置成功
访问地址规范:
/{application}/{profile}[/{label}]
/{application}-{profile}.yml
/{label}/{application}-{profile}.yml
/{application}-{profile}.properties
/{label}/{application}-{profile}.properties
取第三种解释:配置中心地址(localhost:2000)/分支名称/微服务名称-环境名称.yml
label分支名称; application:微服务名称; profile:环境名称
二、ConfigClient
在springCloud中有两种配置文件:application.yml、bootstrap.yml。application.yml是微服务的自己的配置文件,而bootstrap.yml属于系统配置文件,且bootstrap.yml的加载优先级大于application.yml;在springCloud开发中,这两种配置文件一起使用,bootstrap.yml用于由配置中心统一管理配置文件。
通过配置实现:client获取配置中心的统一管理配置(实际过程为:client-Config>>>serverConfig>>>gitConfig)
配置过程:
-
创建新的module,config-client2100
-
添加依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-config</artifactId> </dependency>
-
配置bootstrap.yml
server: port: 2100 spring: application: name: config-client2100 cloud: # spring cloud 配置 config: # 分支 label: master # 项目名称 一般在git中的配置文件命名格式为:{application}-{profile}.yml name: config # 环境版本 profile: info # 配置中心地址 uri: http://localhost:2000 eureka: client: register-with-eureka: true fetch-registry: true service-url: defaultZone: http://eureka5001.com:5001/eureka/,http://eureka5002.com:5002/eureka/
-
创建启动类
-
创建controller
@RestController public class ConfigController { @Value("${config.info}") private String info; @GetMapping("/getConfig") public String getConfig(){ return info; } }
代码逻辑为访问该微服务"/getConfig"的URL,将会得到服务中心(其实是git中的)的配置文件信息;
@Value代码表示:由于使用bootstrap.yml,所以会优先加载其;又因为在bootstrap.yml中配置了使用配置中心的配置信息,所以配置中心的配置信息会加载到bootstrap.yml中,所以可使用@Value来直接后去配置信息。
-
测试:
首先测试配置中心是否能够访问到git中的配置文件
在通过client2100访问:/getConfig地址是否能够得到配置信息。
三、Client动态刷新配置
**问题:**挡在git上修改配置信息后,配置中心(config-server)能够立刻更新(不需要重启服务),而客户端(config-client)不能更新配置信息,只能重启才能更新配置,如何解决?
-
修改pom,添加依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>
-
修改bootstrap.yml
management: endpoints: web: exposure: include: "*"
-
controller添加注解,
@RefreshScope
-
git跟新配置信息后,需要给client发送post请求,则client端便会更新配置
curl -X POST http://localhost:2100/actuator/refresh
完成如上操作即可实现不用重启client微服务也能够更新配置
Bus消息总线
bus简单理解就是拓扑结构中的总线结构,即广播。
springCloudBus支持两种消息代理:RabbitMQ和Kafka
准备:需要安装Erlang和rabbitmq环境,安装成功访问地址:localhost:15672跳转到登陆界面即安装成功
一、Bus广播-全部配置更新
准备:新建一个module与client2100一样的client2200
1.消息总线通知一个客户端(Client),进而更新全部客户端的配置
2.消息总线通知一个服务端(Server),进而更新全部客户端的配置
推荐使用第二种通知总控:
- 保持了微服务的职责单一性,配置中心始终是管理配置的;而客户端有其自己的功能,不需要额外添加功能
- 如果配置在client,则增加了该客户端的压力,如果该client宕机,则全盘无法操作;同时破坏了客户端集群的平等性(模块结构不一样了)
配置:
-
ConfigServer2000:
pom配置,添加依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-amqp</artifactId> </dependency>
修改yml
#rabbitmq相关配置 这一段属于spring下面的配置 spring: rabbitmq: host: localhost port: 5672 # 15672是web管理端口 5672是访问端口 username: guest password: guest # 暴露 bus刷新配置的端点 management: endpoints: web: exposure: include: 'bus-refresh'
-
ConfigClient2100:
添加依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-amqp</artifactId> </dependency>
修改yml
rabbitmq: host: localhost port: 5672 username: guest password: guest
-
ConfigClient2200:
添加依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-bus-amqp</artifactId> </dependency>
修改yml
rabbitmq: host: localhost port: 5672 username: guest password: guest
-
配置完成后,当运维人员修改git上的配置文件时,配置中心会自动更新;客户端只需要对配置中心发送post请求:
curl -X POST http://localhost:2000/actuator/bus-refresh
即可是是实现一处广播,处处更新
注意:client的主启动类上要添加注解``@RefreshScope`,才能开启自动刷新
二、定点更新
当某些情况下,需要对个别的客户端不需要更新,则可以使用顶点广播更新
使用命令:
http://配置中心地址:端口/actuator/bus-refresh/{application名称:具体端口}
如:
curl -X POST http://localhost:2000/actuator/bus-refresh/config-client:2200
表示只更新微服务名称为config-client、端口号为2200的微服务配置。
Stream消息驱动
当一个架构中存在两个MQ时,为了避免不同的消息队列的不同和需要同时掌握两种技术,则可以引入cloudStream。
CloudStream由Binder(绑定器对象)来实现与不同的MQ之间的交互,屏蔽了各种MQ之间的细节,只关注她们之间的交互。CloudStream目前只支持RabbitMQ和Kafka之间的转换。
一、消息生产者构建
新建module
-
添加依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-rabbit</artifactId> </dependency>
-
修改yml
server: port: 8100 spring: application: name: stream-rabbit-provider cloud: stream: binders: # 配置要绑定的额rabbitmq的服务信息 defaultRabbit: # 表示定义的名称,用于binding的整合 type: rabbit # 消息组件类型 environment: # rabbitmq的环境 spring: rabbitmq: host: localhost port: 5672 username: guest password: guest bindings: # 服务的整合 output: # 通道名称 destination: studyExchange # 要使用的Exchange的名称 content-type: application/json # 消息类型 binder: defaultRabbit #要绑定消息服务的具体设置 eureka: client: service-url: defaultZone: http://eureka5001.com:5001/eureka/,http://eureka5002.com:5002/eureka/
-
创建主启动类
-
创建controller
@RestController public class SendMessageController { @Autowired private MessageService messageService; @GetMapping("/sendMessage") public String sendMessage(){ return messageService.send(); } }
表示没发送其次请求,向rabbitmq发送一个消息
-
创建service
@EnableBinding(Source.class) // 定义消息推送管道,source表示消息生产者 public class MessageServiceImpl implements MessageService { // 消息通道 @Autowired private MessageChannel output; @Override public String send() { String s = UUID.randomUUID().toString(); System.out.println("===========s"); // 使用MessageBuilder发送消息 output.send(MessageBuilder.withPayload(s).build()); return null; } }
这里需要用到注解
@EnableBinding
-
完成配置后,启动微服务,每次访问url,就可以在rabbitmq的管理页面看到消息波峰流量
二、消息消费者构建
新建module
-
添加pom
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-stream-rabbit</artifactId> </dependency>
-
修改yml
# 在yml中,出了端口、名称等常规配置与消息生产者不同外,只有下面的一点与生产者不同 input: # 将output通道变为input通道即可 destination: studyExchange # 要使用的Exchange的名称(与生产者一致)
-
创建主启动类
-
创建controller监听消息
@RestController @EnableBinding(Sink.class) // 定义消息管道,表示当前为接收消息管道 public class ReciveMessageController { @Value("${server.port}") private String port; // 消息监听注解,固定配置 @StreamListener(Sink.INPUT) public void reciveMessage(Message<String> message){ // 需要传入Message对象,用于获取消息 System.out.println("port: "+ port + " 接收到消息:"+message.getPayload()); } }
-
测试:当对消息生产者发送请求后,生产者或发送消息至rabbitmq;然后消息消费者会监听消息,当监听到消息后,便在控制台打印消息。
三、分组
在SpringCloudStream中,处于同一个分组的消息消费者时竞争关系,即只有一个能获得消息
1.重复消费
当消息消费者存在多个时,他们都订阅了同一个消息,而此时他们处于不同的分组,那么就会造成消息生产者发送一次消息,同时被多个消费者收到,就造成的重复消费的问题。
实际案例:在商城系统中,当8001发送一个订单消息,同时被多个消费者收到,就会产生一个订单产生的多次,就会造成重复扣款等情况。
自定义分组:
yml配置:
只需要添加group配置即可
input: # 通道名称
destination: studyExchange # 要使用的Exchange的名称
content-type: application/json # 消息类型
binder: defaultRabbit #要绑定消息服务的具体设置
group: consumer1
input: # 通道名称
destination: studyExchange # 要使用的Exchange的名称
content-type: application/json # 消息类型
binder: defaultRabbit #要绑定消息服务的具体设置
group: consumer2
如下:
由此可以得到:如果将两个消息消费者的分组设置为同一个分组即可避免重复消费
2.持久化
什么是持久化?
当配置了分组后,当消息消费者停止工作了(stop、关闭),此时如果消息生产者发送消息,那么在配置由group的的消息消费者在重新启动的时候就会收到之前没有收到的消息,而没有配置group的消息消费者则收不到之前的消息。
测试:将上一步中的client关闭,将8300的group配置注释,而8200的group配置保留,请求8100(消息生产者)发送消息。在启动两个client,会发现8200会收到这些消息,而8300收不到这些消息。
Sleuth分布式请求链路跟踪
Sleuth提供完整的请求服务跟踪解决方案
需要配合zipkin-jar包一起使用
在现在的版本中,只需要导入GAV即可,其中包含的sleuth和zipkin
具体配置:
-
为每个微服务添加依赖
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-zipkin</artifactId> </dependency>
-
yml添加配置
spring: application: name: cloud-provider-payment # 微服务名称 zipkin: base-url: http://localhost:9411 # zipkin地址 sleuth: sampler: probability: 1 # 采集的数量 0-1之间
启动zipkin-jar包后,访问:http://localhost:9411即可查看相关的请求消息。