分布式微服务架构的一站式解决方案,是多种微服务架构落地技术的集合体,俗称微服务全家桶
组成
https://cloud.spring.io/spring-cloud-static/Hoxton.SR1/reference/htmlsingle/
https://docs.spring.io/spring-boot/docs/2.2.2.RELEASE/reference/htmlsingle/
maven中
docker启动mysql
$ docker run -d -p 3306:3306 -v /usr/local/mysql/data:/var/lib/mysql -v /usr/local/mysql/conf/mysql.cnf:/etc/mysql/mysql.cnf -e MYSQL_ROOT_PASSWORD=123456 --name mysql-service docker.io/mysql
建项目,指定包和版本–>建模块
开启热部署
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
<configuration>
<fork>truefork>
<addResources>trueaddResources>
configuration>
plugin>
plugins>
build>
ctrl+shift+alt+/
打开registrycompiler.automake.allow.when.app.ruunning
actionSystem.assertFocusAccessFromEdt
微服务间的调用使用RestTemplate:
RestTemplate提供了多种便捷访问远程Http服务的方法
是一种简单便捷的访问restful服务模板类,是Spring提供的用于访问Rest服务的客户端模板工具集
cloud01, 微服务间的调用
工程重构:
cloud02, 单机版
在传统的rpc调用框架中,管理每个服务与服务直接依赖关系比较复杂,所以需要使用服务治理,管理服务与服务之间的依赖关系,可以实现服务调用,负载均衡,容错等,实现服务发现与注册
Eureka采用了C/S的设计架构
Eureka Server: 各个为服务节点通过配置启动后,会在EurekaServer中进行注册,这样EurekaServer中的服务注册表将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观看到
EurekaClient:通过注册中心进行访问,是一个Java客户端,与EurekaServer交互,客户端同时也具备一个内置的,使用轮询负载均衡算法的负载均衡器,在应用启动后,将会向EurekaServer发送心跳,。如果EurekaServer没有接收到某个节点的心跳,EurekaServer将会从服务注册表中把这个服务节点移除
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
dependency>
server:
port: 7001
eureka:
instance:
hostname: localhost # eureka服务端的实例名称
client:
register-with-eureka: false # 不向注册中心注册自己
fetch-registry: false # 表示自己就是注册中心,职责是维护服务实例,并不需要检索服务
service-url:
# 设置与eureka服务交互的地址,查询服务和注册服务都需要依赖这个地址
defaultZone: http://${
eureka.instance.hostname}:${
server.port}/eureka/
@SpringBootApplication
@EnableEurekaServer // 标注是eureka服务
public class EurekaMain {
public static void main(String[] args) {
SpringApplication.run(EurekaMain.class, args);
}
}
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
eureka:
client:
register-with-eureka: true # 将自己注册进eureka server
fetch-registry: true # 从eureka server抓取自己的注册信息,集群必须为true才能配合ribbon使用负载均衡
service-url:
defaultZone: http://localhost:7001/eureka
@SpringBootApplication
@EnableEurekaClient
public class PaymentMain {
public static void main(String[] args) {
SpringApplication.run(PaymentMain.class, args);
}
}
❓ 微服务RPC远程服务调用最核心的是什么?
高可用
cloud03, 集群版
搭建eureka集群,实现负载均衡和故障容错 --> 互相注册,相互守望
# hosts
127.0.0.1 eureka7001.com
127.0.0.1 eureka7002.com
server:
port: 7001
eureka:
instance:
hostname: eureka7001.com # eureka服务端的实例名称
client:
register-with-eureka: false # 不向注册中心注册自己
fetch-registry: false # 表示自己就是注册中心,职责是维护服务实例,并不需要检索服务
service-url:
# 设置与eureka服务交互的地址,查询服务和注册服务都需要依赖这个地址
defaultZone: http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
server:
port: 7002
eureka:
instance:
hostname: eureka7002.com # eureka服务端的实例名称
client:
register-with-eureka: false # 不向注册中心注册自己
fetch-registry: false # 表示自己就是注册中心,职责是维护服务实例,并不需要检索服务
service-url:
# 设置与eureka服务交互的地址,查询服务和注册服务都需要依赖这个地址
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7003.com:7003/eureka/
eureka:
client:
register-with-eureka: true # 将自己注册进eureka server
fetch-registry: true # 从eureka server抓取自己的注册信息,集群必须为true才能配合ribbon使用负载均衡
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/
在同一台服务器上,以不同的端口来搭建集群,ip 或者 主机名相同时,无法形成副本。所以将其中一台迁移到了另外的服务器上了
将支付微服务也调整为集群
❓ 此时由两个微服务payment1和payment2,如何实现负载均衡呢?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YbtDcsYn-1631719232847)(images/eureka_info.png)]
可以看到现在eureka集群有两台,而PAYMENT-SERVICE同名服务有两台机器,此时在order-service代码访问payment-service的ip和端口是写死的,不满足负载均衡。
public static final String PAYMENT_URL = "http://PAYMENT-SERVICE";
@LocalBalanced
赋予RestTemplate
负载均衡的能力@Configuration
public class ApplicationContextConfig {
@Bean
@LoadBalanced
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
这个就是下文的ribbon的负载均衡功能
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3MkR3tUE-1631719232850)(images/eureka.png)]
Actuator集群信息完善
# client config
eureka:
instance:
instance-id: payment8002 # 配置主机名称
prefer-ip-address: true # 访问路径可以显示ip地址
Eureka注册的微服务发现
// payment controller
@RestController
public class PaymentController {
@Autowired
private PaymentService paymentService;
@Value("${server.port}")
private String serverPort;
@Resource
private DiscoveryClient discoveryClient; // 用于暴露微服务信息
...
@GetMapping("/payment/discovery")
public Object discovery() {
List<String> services = discoveryClient.getServices();
for (String service : services) {
System.out.println(service);
}
List<ServiceInstance> instances = discoveryClient.getInstances("PAYMENT-SERVICE");
for (ServiceInstance instance : instances) {
System.out.println(instance.getInstanceId() + "\t" + instance.getHost() + "\t" + instance.getPort() + "\t" + instance.getUri());
}
return discoveryClient;
}
}
@SpringBootApplication
@EnableEurekaClient
@EnableDiscoveryClient // 主启动类添加服务发现注解
public class PaymentMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8001.class, args);
}
}
Eureka的自我保护机制
看到
EMERGENCY!…
就说明eureka进入保护模式。
什么自我保护机制:就是指,某时刻某一个微服务不可用了,eureka不会立刻清理,依旧会对该微服务信息进行保存。如果eurekaserver在一定事件内没有收到某个微服务实例的心跳,eurekaserver就会注销该实例,默认90秒。但是当网络分区故障发生,比如卡顿、延时等,微服务与eurekaserver之间无法正常通行,以上行为可能变得非常危险,因为服务本身是健康的,此时不应该注销这个微服务。eureka通过自我保护模式来解决这个问题。eureka宁可保留错误的服务注册信息,也不盲目注销任何可能健康的服务实例,好死不如赖活着!
如何关闭自我保护?
eureka:
instance:
hostname: eureka7001.com # eureka服务端的实例名称
client:
register-with-eureka: false # 不向注册中心注册自己
fetch-registry: false # 表示自己就是注册中心,职责是维护服务实例,并不需要检索服务
service-url:
# 设置与eureka服务交互的地址,查询服务和注册服务都需要依赖这个地址
defaultZone: http://eureka7002.com:7002/eureka/
server:
enable-self-preservation: false # 关闭自我保护
eureka:
client:
register-with-eureka: true # 将自己注册进eureka server
fetch-registry: true # 从eureka server抓取自己的注册信息,集群必须为true才能配合ribbon使用负载均衡
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
instance:
instance-id: payment8001 # 配置实例名称
prefer-ip-address: true # 访问路径可以显示ip地址
lease-renewal-interval-in-seconds: 1 # client向server发送心跳的事件间隔,单位为秒,默认30
lease-expiration-duration-in-seconds: 2 # eureka server在收到最后一次心跳后等待的时间上限,单位为秒, 默认90。超时将删除微服务
zookeeper是一个分布式协调工具,可以实现注册中心功能。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C4djzTmA-1631719232854)(images/zookeeper.png)]
关闭zookeeper防火墙后启动zookeeper服务器
$ systemctl stop firewalld
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zookeeper-discoveryartifactId>
dependency>
server:
port: 8004
spring:
application:
name: payment-service # 注册到zookeeper注册中心的名称
cloud:
zookeeper:
connect-string: 192.168.80.130:2181
# connect-string: 192.168.80.130:2181,192.168.80.131:2181 #集群
@SpringBootApplication
@EnableDiscoveryClient // 该注解用于向使用consul或者zookeeper作为注册中心时注册服务
public class PaymentMain8004 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain8004.class, args);
}
}
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;
@GetMapping("/payment/zk")
public String paymentzk() {
return "springcloud with zookeeper" + serverPort + UUID.randomUUID().toString();
}
}
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zookeeper-discoveryartifactId>
<exclusions>
<exclusion>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>org.apache.zookeepergroupId>
<artifactId>zookeeperartifactId>
<version>3.4.9version>
<exclusions>
<exclusion>
<artifactId>slf4j-log4j12artifactId>
<groupId>org.slf4jgroupId>
exclusion>
exclusions>
dependency>
$ zkCli.sh
ls /services # 查看有哪些微服务
ls /services/payment # 获取节点列表
get /services/payment-service/7aa84ad6-5ec5-4309-a3d2-63f10e4af278 # 获取节点信息
// 节点信息
{
"name": "payment-service",
"id": "7aa84ad6-5ec5-4309-a3d2-63f10e4af278",
"address": "192.168.190.1",
"port": 8004,
"sslPort": null,
"payload": {
"@class": "org.springframework.cloud.zookeeper.discovery.ZookeeperInstance",
"id": "application-1",
"name": "payment-service",
"metadata": {
}
},
"registrationTimeUTC": 1627912537184,
"serviceType": "DYNAMIC",
"uriSpec": {
"parts": [
{
"value": "scheme",
"variable": true
},
{
"value": "://",
"variable": false
},
{
"value": "address",
"variable": true
},
{
"value": ":",
"variable": false
},
{
"value": "port",
"variable": true
}
]
}
}
$ curl http://localhost:8004/payment/zk
在zookeeper上注册的服务节点时临时的还是持久的?临时的
payment8004 + orderzk80
略
cloud-provider-consul-payment8006 + cloud-consumer-consul-order80
组件 | 语言 | CAP | 服务监控检查 | 对外暴露接口 | Spring Cloud集成 |
---|---|---|---|---|---|
Eureka | Java | AP | 可配置支持 | HTTP | 已集成 |
Consul | Go | CP | 支持 | HTTP/DNS | 已集成 |
Zookeeper | Java | CP | 支持 | 客户端 | 已集成 |
Nacos | AP/CP | 支持 |
分布式环境中老生常谈的一个东西 - CAP
C - consistency 强一致性
A - availability 可用性
P - partition tolerance 分区容错性
CAP理论的核心是: 一个分布式系统不可能同时满足一致性,可用性和分区容错性这三个需求。CAP理论关注粒度是数据,而不是整体系统设计的策略
CA - 单点集群,满足一致性,可用性,通常在可拓展性上不太强大
CP - 满足一致性,分区容错性的系统,通常性能不是特别的高
AP - 满足可用性,分区容错性,通过对数据一致性要求低一些。
所以,分布式系统考虑到集群的拓展,只能选择CP 或者 AP 。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hEFRjWPy-1631719232856)(images/cap.jpg)]
Zookeeper 保证CP
但是zk会出现这样一种情况,当master节点因为网络故障与其他节点失去联系时,剩余节点回重新进行leader选举,问题在于选举leader的时间太长,30~120s且选举期间整个zk集群都是不可用的。这就导致在选举期间注册服务瘫痪,在云部署的环境下,因网络问题使得zk集群失去master节点很大概率会发生的事情。虽然服务能够最终恢复,但是漫长的选举时间导致的注册长期不可用时不能容忍的
Eureka 保证AP
可以容忍数据的不一致
Spring Cloud Ribbon是一套客户端实现负载均衡的工具,主要提供客户端软件负载均衡和服务调用。
在配置文件中列出LoadBalancer后面所有的机器,Ribbon会自动的帮助你基于某种规则(如简单轮询,随机连接等)去连接这些机器。
一句话:负载均衡 + RestTemplate
与Nginx的区别
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-ribbonartifactId>
dependency>
spring-cloud-stater-netflix-eureka-client
自带 spring-cloud-starter-netflix-ribbon
如何替换默认规则呢❓
package com.chmingx.myrule;
@Configuration
public class MyselfRule {
@Bean
public IRule myRule() {
return new RandomRule(); // 随机负载均衡规则
}
}
@RibbonClient
@SpringBootApplication
@EnableEurekaClient
@RibbonClient(name = "PAYMENT-SERVICE", configuration = MyselfRule.class)
public class OrderMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderMain80.class, args);
}
}
orderribbon80
还有其他方法也可以实现替换,可以查询文档
详解ribbon的轮询算法
原理: rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标。 每次服务器重启后rest接口计数从1开始
r e s t 接 口 第 几 次 请 求 数 % 服 务 器 集 群 总 数 量 = 实 际 调 用 服 务 器 位 置 下 标 rest接口第几次请求数 \% 服务器集群总数量 = 实际调用服务器位置下标 rest接口第几次请求数%服务器集群总数量=实际调用服务器位置下标
✍️ 自己手写轮询算法
@LoadBalanced
注释@Configuration
public class ApplicationContexstConfig {
@Bean
// @LoadBalanced // 使RestTemplate具有负载均衡的能力
public RestTemplate getRestTemplate() {
return new RestTemplate();
}
}
public interface LoadBalancer {
ServiceInstance instance(List<ServiceInstance> serviceInstances);
}
public class MyLB implements LoadBalancer {
private AtomicInteger atomicInteger = new AtomicInteger(0);
public final int getAndIncrement() {
int current;
int next;
do {
current = this.atomicInteger.get();
// Integer.MAX_VALUE 为 2147483647 最大的整型
next = current >= 2147483647 ? 0 : current + 1;
} while (!this.atomicInteger.compareAndSet(current, next)); // 此处用到自旋锁, 处理并发下,是第几次访问
System.out.println("*****第几次访问,次数next: " + next);
return next;
}
// rest接口第几次请求数 % 服务器集群总数量 = 实际调用服务器位置下标
@Override
public ServiceInstance instance(List<ServiceInstance> serviceInstances) {
int index = getAndIncrement() % serviceInstances.size();
return serviceInstances.get(index);
}
}
// 在服务端添加测试接口
@GetMapping("/payment/lb") // 用于自定义轮询负载均衡,获取lb节点
public String getPaymentLB() {
return serverPort;
}
@RestController
public class OrderController {
private static final String PAYMENT_URL = "http://PAYMENT-SERVICE";
@Autowired
private RestTemplate restTemplate;
@Autowired
private LoadBalancer loadBalancer; // 注入自定义的负载均衡器
@Autowired
private DiscoveryClient discoveryClient;
// 使用自定义的负载均衡算法
@GetMapping("/consumer/payment/lb")
public String getPaymentLB() {
List<ServiceInstance> instanceList = discoveryClient.getInstances("PAYMENT-SERVICE");
if (instanceList == null || instanceList.size() <= 0) {
return null;
}
ServiceInstance serviceInstance = loadBalancer.instance(instanceList); // 使用自定义的负载均衡器
URI uri = serviceInstance.getUri();
return restTemplate.getForObject(uri + "/payment/lb", String.class);
}
}
orderribbon80
Feign是一个声明式的Web服务端,让编写Web服务客户端变得非常容易。只需创建一个接口,并在接口上添加注解即可。
前面已经使用Ribbon + RestTemplate,利用Rest Template对Http请求进行封装处理,形成一套模板化的调用方法。但是在实际开发中,由于服务依赖调用可能不止一处,往往一个接口会被多处调用,所以通常都会针对每个微服务自行封装一些客户端类来包装这些依赖服务的调用。所以Feign在此基础上做了进一步封装,由它来帮助我们定义和实现依赖服务接口的定义。在Feign实现下,我们只需要一个接口并使用注解的方式来配置它,即可完成对服务提供方的接口绑定,简化了使用Spring Cloud Ribbon时,自动封装服务调用客户端的开发量。
Feign也集成了Ribbon,并且通过轮询实现了客户端的负载均衡.
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B8wfiPeq-1631719232857)(images/ribbon.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nMNYcCRd-1631719232859)(images/openfeign.png)]
Feign OpenFeign
❓ 如何实现OpenFeign的调用呢?
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>com.chmingxgroupId>
<artifactId>commonartifactId>
<version>1.0-SNAPSHOTversion>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
dependencies>
server:
port: 80
eureka:
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka,http://eureka7002.com:7002/eureka
spring:
application:
name: order-service
# 设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
ReadTimeout: 5000 # 建立连接所用时间,适用于网络状况正常状态下,两端连接所用时间
ConnectTimeout: 5000 # 指的是建立连接后从服务器读取到可用资源所用时间
logging:
level:
# feign日志:以什么级别监控哪个接口
com.chmingx.springcloud.service.PaymentFeignService: debug
@SpringBootApplication
@EnableFeignClients // 激活Feign
public class OrderFeignMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderFeignMain80.class, args);
}
}
/**
* 表面上是一个接口,而添加了@FeignClient注解后,动态代理生成了一个controller,
* 所以当别的controller调用这个接口的时候,本质上是那个controller调用这个接口生成的动态代理controller
*/
@Component
@FeignClient(value = "PAYMENT-SERVICE")
public interface PaymentFeignService {
@GetMapping("/payment/{id}")
CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);
}
@RestController
public class OrderFeignController {
@Autowired
private PaymentFeignService paymentFeignService;
@GetMapping("/consumer/payment/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
return paymentFeignService.getPaymentById(id);
}
}
⏰ OpenFeign超时控制
OpenFeign默认等待 1sec,超时则报错.
超时测试
// 超时测试
@GetMapping("/payment/feign/timeout")
public String paymentFeignTimeout() {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return serverPort;
}
@Component
@FeignClient(value = "PAYMENT-SERVICE")
public interface PaymentFeignService {
@GetMapping("/payment/{id}")
CommonResult<Payment> getPaymentById(@PathVariable("id") Long id);
@GetMapping("/payment/feign/timeout")
public String paymentFeignTimeout();
}
@RestController
public class OrderFeignController {
@Autowired
private PaymentFeignService paymentFeignService;
@GetMapping("/consumer/payment/{id}")
public CommonResult<Payment> getPaymentById(@PathVariable("id") Long id) {
return paymentFeignService.getPaymentById(id);
}
// 超时测试
@GetMapping("/payment/feign/timeout")
public String paymentFeignTimeout() {
// openfiegn 默认等待1秒
return paymentFeignService.paymentFeignTimeout();
}
}
✍️ 修改配置
# 设置feign客户端超时时间(OpenFeign默认支持ribbon)
ribbon:
ReadTimeout: 5000 # 建立连接所用时间,适用于网络状况正常状态下,两端连接所用时间
ConnectTimeout: 5000 # 指的是建立连接后从服务器读取到可用资源所用时间
OpenFeign日志级别
@Configuration
public class FeignConfig {
// 配置open feign日志级别
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
logging:
level:
# feign日志:以什么级别监控哪个接口
com.chmingx.springcloud.service.PaymentFeignService: debug
复杂的分布式体系结构中的应用程序有数是个依赖,每个依赖关系在某些时候不可避免地失败。
服务雪崩:
多个微服务之间调用的时候,假设微服务A调用微服务B和微服务C,微服务C又调用其他微服务,这就是所谓的“扇出”, 如果扇出的链路上某个微服务响应时间过长或者不可用,对微服务A的调用就会占用越来越多的系统资源,进而引起系统崩溃,所谓的雪崩效应。
Hystix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时,异常等,Hystrix能够保证在一个依赖出问题的情况下,不会导致整体服务失败,比卖你级联故障,以提高分布式系统的弹性。
断路器本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似保险丝熔断),向调用方返回一个符合预期的,可处理的备选响应(Fallback),而不是长时间等待或者抛出调用方无法处理的异常,这样保证了服务调用方的线程不会被长时间、不必要地占用,从而避免故障在分布式系统中的蔓延,乃至雪崩。
服务降级:假设服务系统不可用了,需要提供一个兜底的解决方法,即可处理的备选响应Fallback
服务熔断:类比保险丝达到最大服务访问后,直接拒绝访问,拉闸限电,然后调用服务降级的方法并返回友好提示
服务限流:秒杀高并发等操作,严禁一窝蜂的过来拥挤,大家排队,一秒钟N个,有序进行
cloud-provider-hystrix-payment8001
cloud-consumer-feign-hystrix-order80
jmeter进行压测
高并发下,8001同一层次的其他接口服务被困死,因为tomcat线程池里面的工作线程已经被抢占完毕,此时再调用,客户端响应缓慢,甚至出现超时错误。因为有上述故障或不佳的表现,才有我们的降级、容错、限流等技术诞生
解决要求:
@HystrixCommand
设置自身调用超时时间的峰值,峰值内可以正常运行;冲过来需要有兜底的处理方法,作为服务fallback
@Service
public class PaymentService {
// 正常访问
public String paymentInfo_OK(Integer id) {
return "线程池: " + Thread.currentThread().getName() + "\t" + "paymentInfo_OK, id: " + id + "\t" + "OK!!!";
}
/**
* 该方法超时之后交给paymentInfo_TimeoutHandler进行处理
*/
@HystrixCommand(fallbackMethod = "paymentInfo_TimeoutHandler", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1000") // 这个线程的超时时间是1秒, 也可以再yaml中配置
})
public String paymentInfo_Timeout(Integer id) {
int timeNumber = 3;
// int age = 10 / 0; // 刻意制造的异样,也可以由fallback方法处理
try {
TimeUnit.SECONDS.sleep(timeNumber);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "线程池: " + Thread.currentThread().getName() + "\t" + "paymentInfo_Timeout, id: " + id + "\t" + "OK!!!" + "\t" + "耗时: " + timeNumber;
}
// paymentInfo_Timeout 发生超时错误时的兜底方法, 注意服务降级的方法需要与原方法保持一致
public String paymentInfo_TimeoutHandler(Integer id) {
return "线程池: " + Thread.currentThread().getName() + "\t" + "系统繁忙或运行错误,请稍后再试" + "\t" + "Timeout!!!" + "\t" + "超时啦!!!";
}
}
# 改变默认超时时间:
hystrix:
command:
default:
execution:
isolation:
thread:
timeoutInMilliseconds: 3000
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker // 激活服务降级
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class, args);
}
}
IDEA的热部署对Java代码修改敏感,但是对@HystrixCommand内属性的修改不敏感,此时建议重启微服务
feign:
hystrix:
enabled: true
@SpringBootApplication
@EnableFeignClients
@EnableHystrix // 开启Hystrix
public class OrderHystrixMain80 {
public static void main(String[] args) {
SpringApplication.run(OrderHystrixMain80.class, args);
}
}
@GetMapping("/consumer/payment/hystrix/timeout/{id}")
@HystrixCommand(fallbackMethod = "paymentTimeoutFallback", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "500")
})
public String paymentInfo_Timeout(@PathVariable("id") Integer id) {
String result = paymentHystrixService.paymentInfo_Timeout(id);
log.info(result);
return result;
}
public String paymentTimeoutFallback(@PathVariable("id") Integer id) {
return "我是消费者80,对方支付系统繁忙,请稍后再试,或者自己运行出错,请检查自己";
}
@DefaultProperties(defaultFallback = "")
, 专门配置了fallback的就调用fallback,没有就调用全局的fallback@RestController
@Slf4j
@DefaultProperties(defaultFallback = "paymentGlobalFallback") // 配置全局的fallback
public class OrderHystrixController {
@Autowired
private PaymentHystrixService paymentHystrixService;
@GetMapping("/consumer/payment/hystrix/global/{id}")
@HystrixCommand // 服务报错后,使用降级兜底的方法
public String paymentInfo_Global(@PathVariable("id") Integer id) {
int age = 10 / 0;
String result = paymentHystrixService.paymentInfo_Timeout(id);
log.info(result);
return result;
}
public String paymentGlobalFallback() {
return "Global异常处理信息,对方支付系统繁忙,请稍后再试,或者自己运行出错,请检查自己";
}
}
@FeignClient
再次优化代码,只需要为Feign客户端定义的接口interface添加一个服务降级处理实现类即可解耦代码。常用来处理宕机问题@Component
@FeignClient(value = "CLOUD-PROVIDER-HYSTRIX-PAYMENT", fallback = PaymentFallbackService.class)
public interface PaymentHystrixService {
@GetMapping("/payment/hystrix/ok/{id}")
public String paymentInfo_OK(@PathVariable("id") Integer id);
@GetMapping("/payment/hystrix/timeout/{id}")
public String paymentInfo_Timeout(@PathVariable("id") Integer id);
}
// 兜底类实现接口
@Component
public class PaymentFallbackService implements PaymentHystrixService {
@Override
public String paymentInfo_OK(Integer id) {
return "---PaymentFallbackService fall back paymentInfo_OK, o(-_-)o";
}
@Override
public String paymentInfo_Timeout(Integer id) {
return "---PaymentFallbackService fall back paymentInfo_Timeout, o(-_-)o";
}
}
熔断机制:熔断机制时应对雪崩效应的一种微服务链路保护机制,当扇出链路的某个微服务出错不可用或响应时间太长时,会进行微服务降级,进而熔断该节点微服务的调用,快速返回错误的响应信息。当检测到该节点微服务调用响应正常后,恢复调用链路。
在SpringCloud里,熔断机制通过Hystrix实现,Hystrix会监控微服务间调用的状况,当失败的调用到一定阈值,缺省5秒内20次调用失败,就会启动熔断机制。熔断机制注解是@HystrixCommand
@Service
public class PaymentService {
// ---------- 服务熔断 ---------------
@HystrixCommand(fallbackMethod = "paymentCircuitBreaker_fallback", 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 paymentCircuitBreaker(@PathVariable("id") Integer id) {
if (id < 0) {
throw new RuntimeException("****** id 不能为负数");
}
String serialNumber = IdUtil.simpleUUID(); // UUID.randomUUID().toString();
return Thread.currentThread().getName() + "\t" + "调用成功,流水号: " + serialNumber;
}
public String paymentCircuitBreaker_fallback(@PathVariable("id") Integer id) {
return "id 不能为负数,请稍后再试, T_T, id: " + id;
}
}
三要素:
当服务熔断发生后,将不会再调用主逻辑,而是直接调用降级fallback。通过断路器,实现了自动地发现错误,并将降级逻辑切换为主逻辑,减少响应延迟效果。
❓ 服务如何恢复呢?
当断路器打开,对主逻辑进行熔断之后,hystrix会启动一个休眠时间窗,在这个时间窗内,降级逻辑时临时成为主逻辑,当休眠时间窗到期,断路器进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求返回正常,那么断路器将会闭合,主逻辑恢复,如果请求依然有问题,则断路器继续进入打开,状态,休眠时间长重新计时
略
Hystrix Dashboard 准实时的调用监控
cloud-consumer-hystrix-dashboard9001
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboardartifactId>
dependency>
server:
port: 9001
@SpringBootApplication
@EnableHystrixDashboard
public class HystrixDashboardMain9001 {
public static void main(String[] args) {
SpringApplication.run(HystrixDashboardMain9001.class, args);
}
}
spring-boot-starter-actuator
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker //
public class PaymentHystrixMain8001 {
public static void main(String[] args) {
SpringApplication.run(PaymentHystrixMain8001.class, args);
}
/**
* 此配置时为了服务监控而配置,与服务容错本身无关,springcloud升级后的坑
*
* ServletRegistrationBean因为SpringBoot的默认路径不是 /hystrix.stream
* 只要在自己的项目里配置上下面的servlet就可以了
* @return
*/
@Bean
public ServletRegistrationBean getServlet() {
HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
registrationBean.setLoadOnStartup(1);
registrationBean.addUrlMappings("/hystrix.stream");
registrationBean.setName("HystrixMetricsStreamServlet");
return registrationBean;
}
}
GateWay: 是在Spring生态系统之上构建的API网关服务,基于Spring5, Spring Boot2,Project Reactor等。旨在提供一种简单而有效的方式来对API进行路由,以及提供一些强大的过滤器功能,例如熔断、限流、重试。 GateWay是基于异步非阻塞模型上进行开发的,性能方面不用太担心。
GateWay特性:
❓ Java web中Servlet生命周期?
servlet由servlet container进行生命周期管理,container启动时构造servlet对象并调用servlet init()进行初始化。contianer运行时接受请求,并为每个请求分配一个线程(一般从线程池中获取空闲线程)然后调用service(),container关闭时,调用servlet destroy()销毁servlet。
这是一个阻塞的网络I/O
servlet3.1以后出现非阻塞异步I/O
概念:
web请求,通过一些匹配条件,定位到真正的服务节点,并在这个转发过程的前后,进行一些精细化控制,predicate就是我们匹配条件;而Filter,就可以理解为一个无所不能的拦截器。有了这两个元素,再加上目标uri,就可以实现一个具体的路由了
Gateway的流程:
客户端向SpringCloud GateWay发出请求,然后再Gateway Handler Mapping中找到与请求相匹配的路由,将其发送到Gateway Web Handler。Handler再通过指定的过滤器链来将请求发送到我们实际的服务执行业务逻辑,然后返回。过滤器可以再发送代理请求前或后执行逻辑。比如参数校验,权限验证,流量监控,日志输出,协议转换,或修改响应内容,响应头等。
cloud-gateway-gateway9527
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
server:
port: 9527
spring:
application:
name: cloud-gateway
eureka:
instance:
hostname: cloud-gateway-service
# 服务提供者provider注册进eureka服务列表内
client:
service-url:
register-with-eureka: true
fetch-registry: true
defaultZone: http://eureka7001.com:7001/eureka
@SpringBootApplication
@EnableEurekaClient
public class GateWayMain9527 {
public static void main(String[] args) {
SpringApplication.run(GateWayMain9527.class, args);
}
}
spring:
cloud:
gateway:
routes:
- id: payment_routh # payment_route # 路由的id,没有规则,但要求唯一,建议配合服务名
uri: http://localhost:8001 # 匹配后提供服务的路由地址
predicates:
- Path=/payment/get/** # 断言,路径相匹配的进行路由
- id: payment_routh2 # payment_route3
uri: http://localhost:8001
predicates:
- Path=/payment/lb/**
# 配置前
$ curl http://localhost:8001/payment/get/1
# 配置后, 可以掩藏真实网关
$ curl http://localhost:9527/payment/get/1
@Configuration
public class GateWayConfig {
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder routeLocatorBuilder) {
RouteLocatorBuilder.Builder routes = routeLocatorBuilder.routes();
routes.route("customer_route_locator",
r -> r.path("/guonei").uri("http://news.baidu.com/guonei")).build();
return routes.build();
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T1gikHiN-1631719232860)(images/gateway.png)]
默认情况下,Gateway会根据注册中心注册的服务列表,以注册中心上微服务名为路径,创建动态路由进行转发,从而实现动态路由的功能
# 动态路由,通过微服务名
spring:
application:
name: cloud-gateway
cloud:
gateway:
discovery:
locator:
enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: payment_routh # 路由id,要求唯一
uri: lb://cloud-provider-payment # 匹配后提供服务的路由地址, 需要注意的是uri的协议为lb,表示启用负载均衡
predicates:
- Path=/payment/get/** # 断言,路径相匹配进行路由
- id: payment_routh2
uri: lb://cloud-provider-payment
predicates:
- Path=/payment/lb/**
spring:
cloud:
gateway:
routes:
- id: after_route
uri: https://example.org
predicates:
# 这个时间后才能起效
- After=2017-01-20T17:42:47.789-07:00[America/Denver]
spring:
cloud:
gateway:
routes:
- id: cookie_route
uri: https://example.org
predicates:
- Cookie=chocolate, ch.p # cookie名字,正则
# 该命令相当于发get请求,且没带cookie
curl http://localhost:9527/payment/lb
# 带cookie的
curl http://localhost:9527/payment/lb --cookie "chocolate=chip"
spring:
cloud:
gateway:
routes:
- id: header_route
uri: https://example.org
predicates:
- Header=X-Request-Id, \d+
# 带指定请求头的参数的CURL命令
curl http://localhost:9527/payment/lb -H "X-Request-Id:123"
路由过滤器可用于修改进入HTTP的请求和返回的HTTP响应,路由过滤器只能指定路由进行使用。Spring Cloud Gateway中内置了多种路由过滤器,他们都由GatewayFilter的工厂类来生产
生命周期:
种类:
使用方法可查看官网, 举个例子
- id: payment_routh2
uri: lb://cloud-provider-payment
predicates:
- Path=/payment/lb/**
- Method=GET,POST
filters:
- AddRequestParameter=X-Request-Id,1024 # 过滤器工厂会在匹配的请求头上加一对请求头,名称为X-Request-Id, 值为1024
/**
* 自定义全局过滤器
*/
@Component
@Slf4j
public class MyLogGatewayFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("**** come in MyLogGatewayFilter: " + new Date());
String uname = exchange.getRequest().getQueryParams().getFirst("uname");
if (uname == null) {
log.info("****** 用户名为Null,非法用户,禁止访问 ****");
exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
@Override
public int getOrder() {
return 0; // 返回过滤器优先级,数值越小,优先级越高
}
}
测试
$ curl http://localhost:9527/payment/lb?uname=zs
SpringCloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同微服务应用的松油环境提供一个中心化的外部配置中心
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3nzLfAL1-1631719232862)(images/springcloudconfig.png)]
cloud-config-center-3344
创建git仓库 [email protected]:chmingx/springcloud-config.git
pom
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-config-serverartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
dependencies>
server:
port: 3344
spring:
application:
name: cloud-config-center # 注册进eureka服务器的微服务名
cloud:
config:
server:
git:
# uri: [email protected]:chmingx/springcloud-config.git # gitee上面git仓库的名称, 报错是因为ssh版本太高,
uri: https://gitee.com/chmingx/springcloud-config.git
# 搜索目录
search-paths:
- springcloud-config
force-pull: true
username: chmingx
password: 1024.chm
# 读取分支
label: master
# 服务注册到eureka地址
eureka:
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka
@SpringBootApplication
@EnableConfigServer
public class ConfigCenterMain3344 {
public static void main(String[] args) {
SpringApplication.run(ConfigCenterMain3344.class, args);
}
}
$ curl http://config-3344.com:3344/master/config-dev.yml
$ curl http://config-3344.com:3344/config-dev.yml
$ curl http://config-3344.com:3344/config-dev.yml/master
注意
如果使用ssh报错是因为, openssh版本太高了
ssh-keygen -m PEM -t rsa 重新生成旧格式的key,变可解决
-m 参数指定密钥的格式,PEM(也就是RSA格式)是之前使用的旧格式
Spring Cloud会创建一个 Bootstrap Context, 作为Spring应用的 Application Context的父上下文,初始化的时候, Bootstrap Context负责从外部源加载配置属性并解析配置。这两个上下文共享一个从外部获取的Environment
Bootstrap属性有更高优先级,默认情况下,他们不会被本地配置覆盖。Bootstrap Context和ApplicationContext有着不同的约定。
bootstrap.yaml的优先级比application.yaml高,先加载
cloud-config-client-3355
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-configartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
dependencies>
server:
port: 3355
spring:
application:
name: config-client
cloud:
# Config 客户端配置
config:
label: master # 分支
name: config # 配置文件名
profile: dev # 读取后缀名称 上述3个综合: master分支上config-dev.yml的配置文件被读取, http://config-3344.com:3344/master/conf9g-dev.yml
uri: http://config-3344.com:3344
eureka:
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka
@SpringBootApplication
@EnableEurekaClient
public class ConfigClientMain3355 {
public static void main(String[] args) {
SpringApplication.run(ConfigClientMain3355.class, args);
}
}
@RestController
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/configInfo")
public String getConfigInfo() {
return configInfo;
}
}
$ curl http://localhost:3355/configInfo
❓ 如果有人修改了配置文件,会发现cloud-config-center上及时更新了,但是cloud-config-client需要重启微服务后才能刷新,那么如何实现动态刷新呢?
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
# bootstrap.yaml
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*" # 暴露所有的监控信息,可以配置,比如只暴露info, health等
@RestController
@// 需要热加载的bean需要加上@RefreshScope
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/configInfo")
public String getConfigInfo() {
return configInfo;
}
}
$ curl -X POST "http://localhost:3355/actuator/refresh"
❓ 这种手动刷新的方法比较麻烦,如果微服务数量很大,如果有些要刷新,有些不需要,也比较麻烦,所以引入消息总线帮忙处理
消息总线:在微服务交媾的系统中,通常会使用轻量级的消息代理来构建一个共用的消息主题,并让系统所有微服务实例都连接上来。由于该主题产生的消息会被所有实例监听和消费,所以称为消息总线。
SpringCloudBus是用来将分布式系统的节点与轻量级消息系统连接起来的框架,它整合了Java的事件处理机制和消息中间件的功能。能管理和传播分布式系统间的消息,就像一个分布式执行器,可以用于广播状态更改,事件推送等
Spring Cloud Bus + Spring Cloud Config 可以实现配置的动态自动刷新
支持: RabbitMQ + Kafka
原理: Config Client实例会监听一个MQ中同一个topic(默认是SpringCloudBus),当一个微服务刷新数据的时候,他会把这个消息放到Topic中,这样其他监听同一个Topic的服务就能得到通知,然后去更新自身配置
# docker运行rabbitmq
$ docker run -d -p 5672:5672 -p 15672:15672 --name rabbitmq rabbitmq:management
☑️ 利用消息总线触发一个服务端ConfigServer的/bus/refresh, 而刷新所有客户端的配置
cloud-config-center-3344
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bus-amqpartifactId>
dependency>
spring:
# rabbitmq 相关配置
rabbitmq:
host: 192.168.80.130
port: 5672
username: guest
password: guest
# 暴露bus刷新配置的端点
management:
endpoints:
web:
exposure:
include: 'bus-refresh' # 要用单引号
bus-refresh是actuator的刷新操作
cloud-config-client-3355
cloud-config-client-3366
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-bus-amqpartifactId>
dependency>
spring:
# 配置rabbitmq
rabbitmq:
host: 192.168.80.130
port: 5672
username: guest
password: guest
# 暴露监控端点
management:
endpoints:
web:
exposure:
include: "*"
curl -X POST "http://localhost:3344/actuator/bus-refresh"
可以指定某个具体实例生效,而不是全部
公式: http://localhost:3344/actuator/bus-refresh/{destination}
# 只通知3355,不通知3366
curl -X POST "http://localhost:3344/actuator/bus-refresh/config-client:3355"
SpringCloud Stream作为中间层, 屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型
https://spring.io/projects/spring-cloud-stream#overview
应用程序通过input或者output来与SpringCloud Stream中binder对象交互,而binder负责与消息中间件交互,所以,只需要搞清楚如何与SpringCloud Stream交互就可以方便使用消息驱动的方式
目前支持: RabbitMQ / Kafka
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-nVeKyzI5-1631719232863)(images/stream.png)]
SpringCloud Stream中的消息通信方式遵循了发布-订阅模式,Topic主题进行广播,在RabbitMQ就是Exchange, 在Kafka中就是Topic
cloud-stream-rabbit-provider8801
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-stream-rabbitartifactId>
dependency>
server:
port: 8801
spring:
application:
name: cloud-stream-provider
rabbitmq:
host: 192.168.80.130
port: 5672
username: guest
password: guest
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息
defaultRabbit: # 表示定义的名称, 用于binding整合
type: rabbit # 消息组件类型
# environment: # 设置rabbitmq的相关的环境配置
# spring:
# rabbitmq:
# host: 192.168.80.130
# port: 5672
# username: guest
# password: guest
bindings: # 服务整合处理
output: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置"text/plain"
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
eureka:
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳事件间隔2
lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔
instance-id: send-8801.com # 在信息列表时显示主机名
prefer-ip-address: true # 访问的路径变为IP地址
@SpringBootApplication
@EnableEurekaClient
public class StreamMQMain8801 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8801.class, args);
}
}
public interface IMessageProvider {
public String send();
}
/**
* 该service是与rabbitmq交互,无需@Service注解
*/
@EnableBinding(Source.class) // 定义消息的推送管道
public class MessageProviderImpl implements IMessageProvider {
@Autowired
private MessageChannel output; // 消息发送管道, 与配置文件中output相呼应
@Override
public String send() {
String serial = UUID.randomUUID().toString();
output.send(MessageBuilder.withPayload(serial).build());
System.out.println("*****serial: " + serial);
return serial;
}
}
@RestController
public class SendMessageController {
@Autowired
private IMessageProvider messageProvider;
@GetMapping(value = "/sendMessage")
public String sendMessage() {
return messageProvider.send();
}
}
curl http://localhost:8801/sendMessage
cloud-stream-rabbitmq-consumer8802
cloud-stream-rabbitmq-consumer8803
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-stream-rabbitartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
dependencies>
server:
port: 8802
spring:
application:
name: cloud-stream-consumer
rabbitmq:
host: 192.168.80.130
port: 5672
username: guest
password: guest
cloud:
stream:
binders: # 在此处配置要绑定的rabbitmq的服务信息
defaultRabbit: # 表示定义的名称, 用于binding整合
type: rabbit # 消息组件类型
# environment: # 设置rabbitmq的相关的环境配置
# spring:
# rabbitmq:
# host: 192.168.80.130
# port: 5672
# username: guest
# password: guest
bindings: # 服务整合处理
input: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置"text/plain"
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
eureka:
client:
service-url:
defaultZone: http://eureka7001.com:7001/eureka
instance:
lease-renewal-interval-in-seconds: 2 # 设置心跳事件间隔2
lease-expiration-duration-in-seconds: 5 # 如果现在超过了5秒的间隔
instance-id: receive-8802.com # 在信息列表时显示主机名
prefer-ip-address: true # 访问的路径变为IP地址
@SpringBootApplication
@EnableEurekaClient
public class StreamMQMain8802 {
public static void main(String[] args) {
SpringApplication.run(StreamMQMain8802.class, args);
}
}
@Component
@EnableBinding(Sink.class)
public class ReceiveMessageListenerController {
@Value("${server.port}")
private String serverPort;
@StreamListener(Sink.INPUT)
public void input(Message<String> message) {
System.out.println("consumer 1, ----> 接受到: " + message.getPayload() + "\t" + "Port: " + serverPort);
}
}
❓ 重复消费问题
比如一个订单同时被两个服务收到,那么就会造成数据错误,我们得避免这种情况。可以用Stream的消息分组来解决。
在Stream中处于同一个group中的多个消费者是竞争关系,就能够保证消息只会被其中一个应用消费一次,而不同组可以重复消费
修改yaml,为消费者分组
bindings: # 服务整合处理
input: # 这个名字是一个通道的名称
destination: studyExchange # 表示要使用的Exchange名称定义
content-type: application/json # 设置消息类型,本次为json,文本则设置"text/plain"
binder: defaultRabbit # 设置要绑定的消息服务的具体设置
group: chmingxA # 分组
❓ 去掉8802分组,保留8803分组,并停掉8802和8803,8801发送消息后,重启8802,8802不会受到消息,而重启8803后会收到消息
这是因为,exchange数据发送到队列中,由于02重启没有设置分组,会重新创建队列并监听,而03还是监听原来队列。
所以分组可以实现消息持久化,防止数据丢失
sleuth 监控 + zipkin 呈现
监控微服务的调用
cloud-consumer-order80
cloud-provider-payment8001
均添加
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-zipkinartifactId>
dependency>
spring:
application:
name: cloud-provider-payment
# 微服务链路监控相关配置
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
probability: 1 # 采样率介于0~1,1表示全部采集
一个更易于构建云原生应用的动态服务发现、配置管理和服务平台。
Nacos: dynamic naming and configuration service
Nacos=注册中心+配置中心
Nacos=Eureka+Config+Bus
$ docker ps -a|grep Exited|awk '{print $1}' # 查看所有没有运行的容器
$ docker rm `docker ps -a|grep Exited|awk '{print $1}'` # 删除所有停止了的容器
$ docker rm $(sudo docker ps -a -q)
$ docker pull nacos/nacos-server
$ docker run --env MODE=standalone --name nacos -d -p 8848:8848 nacos/nacos-server
$ firewall-cmd --zone=public --query-port=8848/tcp # 查询端口是否打开
$ firewall-cmd --zone=public --add-port=8848/tcp --permanent # 永久开发8848端口
虚拟机中记得关闭防火墙
cloud04
cloudalibaba-provider-payment9001
cloudalibaba-provider-payment9002
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
dependencies>
server:
port: 9001
spring:
application:
name: nacos-payment-provider
cloud:
nacos:
discovery:
server-addr: localhost:8848 # 配置nacos地址
# 打开监控端点
management:
endpoints:
web:
exposure:
include: '*'
@SpringBootApplication
@EnableDiscoveryClient
public class PaymentMain9001 {
public static void main(String[] args) {
SpringApplication.run(PaymentMain9001.class, args);
}
}
@RestController
public class PaymentController {
@Value("${server.port}")
private String serverPort;
@GetMapping("/payment/{id}")
public String getPaymentById(@PathVariable("id") Integer id) {
return "nacos registry, serverPort: " + serverPort + "\t id" + id;
}
}
cloudalibaba-consumer-order83
各个注册中心对比
C是所有节点在同一事件看到的数据是一致的;A是所有的请求都会收到响应
如果不需要存储服务级别信息且服务实例时通过nacos-client注册,并能够保持心跳上报,那么就可以选择AP模式。当前主流的服务如Spring Cloud和Dubbo都适用于AP模式,AP模式为了服务的可能性而减弱一致性,因此AP模式下只支持注册临时实例
如果需要服务级别便捷或存储配置信息,那么CP时必须,K8S服务和DNS服务则使用与CP模式,CP模式下则支持注册持久实例,此时则是以Raft协议为集群运行模式,该模式下注册实例之前必须先注册服务,如果服务不存在,则会返回错误
Nacos支持CP和AP
# 切换命令
curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP'
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
dependencies>
# nacos配置
server:
port: 3377
spring:
application:
name: nacos-config-client
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
config:
server-addr: localhost:8848 #Nacos作为配置中心地址
file-extension: yaml #指定yaml格式的配置
# ${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
# nacos-config-client-dev.yaml
# nacos-config-client-test.yaml ----> config.info
spring:
profiles:
active: dev # 表示开发环境
#active: test # 表示测试环境
#active: info
@SpringBootApplication
@EnableDiscoveryClient
public class NacosConfigClientMain3377 {
public static void main(String[] args) {
SpringApplication.run(NacosConfigClientMain3377.class, args);
}
}
@RestController
@RefreshScope //支持Nacos的动态刷新功能
public class ConfigClientController {
@Value("${config.info}")
private String configInfo;
@GetMapping("/config/info")
public String getConfigInfo() {
return configInfo;
}
}
在 Nacos Spring Cloud中,dataId的完整格式如下:
#${spring.application.name}-${spring.profile.active}.${spring.cloud.nacos.config.file-extension}
# nacos-config-client-dev.yaml
${prefix}.${file-extension}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NYLCDcDy-1631719232865)(images/nacos-config.png)]
namespace + group + dataid
nacos_config
docker run --env MODE=standalone --name nacos -d -p 8848:8848 nacos/nacos-server
docker exec -it <container id> bash
vim conf/application.properties
# 修改对应的mysql参数
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://localhost:3306/nacos_config?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useSSL=true&serverTimezone=Asia/Shanghai
db.user=root
db.password=123456
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mFjyRWL5-1631719232867)(images/nacos-cluster.png)]
默认Nacos使用嵌入式数据库实现数据的存储。所以,如果启动多个默认配置下的Nacos节点,数据存储是存在一致性问题的。为了解决这个问题,Nacos采用了集中式存储的方式来支持集群化部署,目前只支持MySQL的存储。
Nacos支持三种部署模式
Nginx + 3 Nacos + mysql
192.168.80.130:8847
192.168.80.130:8848
192.168.80.130:8849
-p
, 就以port启动nacos服务$ ./start.sh -p 8847
$ ./start.sh -p 8848
$ ./start.sh -p 8849
将为服务注册进nacos集群
# application.yaml
server:
port: 9002
spring:
application:
name: nacos-payment-provider
c1oud:
nacos:
discovery:
#配置Nacos地址
#server-addr: Localhost:8848
#换成nginx的1111端口,做集群
server-addr: 192.168.111.144:1111
management:
endpoints:
web:
exposure:
inc1ude: '*'
Sentinel是分布式系统的流量防卫兵
随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。
Sentinel 具有以下特征:
丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。
完善的 SPI 扩展点:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。
服务雪崩
服务降级
服务熔断
服务限流
运行sentinel
$ java -Dserver.port=6666 -jar sentinel-dashboard-1.7.2.jar
cloudalibaba-sentinel-service8401
<dependencies>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>cn.hutoolgroupId>
<artifactId>hutool-allartifactId>
<version>5.7.5version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
dependencies>
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
sentinel:
transport:
dashboard: localhost:8080 #配置Sentinel dashboard地址
port: 8719 # 8719是sentinel所在的后台要和sentinel前台的dashboard交互要用的端口, 如果8719被占用,会自动往上加1,直到找到未被占用端口
management:
endpoints:
web:
exposure:
include: '*'
feign:
sentinel:
enabled: true # 激活Sentinel对Feign的支持
@SpringBootApplication
@EnableDiscoveryClient
public class MainApp8401 {
public static void main(String[] args) {
SpringApplication.run(MainApp8401.class, args);
}
}
@RestController
public class FlowLimitController {
@GetMapping("/testA")
public String testA() {
return "------ testA";
}
@GetMapping("/testB")
public String testB() {
return "------ testB";
}
}
sentinel 是懒加载的
概念:
Warm Up:即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮
应用场景 如:秒杀系统在开启的瞬间,会有很多流量上来,很有可能把系统打死,预热方式就是把为了保护系统,可慢慢的把流量放进来,慢慢的把阀值增长到设置的阀值
__匀速排队__方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。
这种方式主要用于处理间隔性突发的流量,例如消息队列。想象一下这样的场景,在某一秒有大量的请求到来,而接下来的几秒则处于空闲状态,我们希望系统能够在接下来的空闲期间逐渐处理这些请求,而不是在第一秒直接拒绝多余的请求。
熔断降级概述
除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。例如,支付的时候,可能需要远程调用银联提供的 API;查询某个商品的价格,可能需要进行数据库查询。然而,这个被依赖服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。
现代微服务架构都是分布式的,由非常多的服务组成。不同服务之间相互调用,组成复杂的调用链路。以上的问题在链路调用中会产生放大的效果。复杂链路上的某一环不稳定,就可能会层层级联,最终导致整个链路都不可用。因此我们需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩。熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。
流控是保护服务不挂掉 熔断是服务已经有问题了防止雪崩
RT(平均响应时间,秒级)
异常比列(秒级)
QPS >= 5且异常比例(秒级统计)超过阈值时,触发降级;时间窗口结束后,关闭降级 。
异常数(分钟级)
超过阈值时,触发降级;时间窗口结束后,关闭降级
异常数是按照分钟统计的,时间窗口一定要大于等于60秒
何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如:
热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
前提条件 - 热点参数的注意点,参数必须是基本类型或者String
@SentinelResource - 处理的是sentinel控制台配置的违规情况,有blockHandler方法配置的兜底处理;
RuntimeException int age = 10/0,这个是java运行时报出的运行时异常RunTimeException,@SentinelResource不管
Sentinel 系统自适应限流从整体维度对应用入口流量进行控制,结合应用的 Load、CPU 使用率、总体平均 RT、入口 QPS 和并发线程数等几个维度的监控指标,通过自适应的流控策略,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。
@SentinelResource
@RestController
public class RateLimitController {
@GetMapping("/byResource")
@SentinelResource(value = "byResource", blockHandler = "handleException")
public String byResource() {
return "200, 按资源名称限流测试, OK";
}
public String handleExeception(BlockException blockException) {
return "444, 按资源名称限流测试, Fail";
}
@GetMapping("/rateLimit/byUrl")
@SentinelResource("byUrl")
public String byUrl() {
return "200, 按url限流测试, OK";
}
}
升级代码
创建自定义的限流处理逻辑类
/**
* 自定义限流处理类,提供处理限流兜底方法
*/
public class CustomerBlockHandler {
public static String handlerException(BlockException blockException) {
return "444, 自定义blockhandler处理限流 ------- 1";
}
public static String handlerException2(BlockException blockException) {
return "444, 自定义blockhandler处理限流 ------- 2";
}
}
配置使用自定义限流处理逻辑
@RestController
public class RateLimitController {
// 使用自定义的用户处理类
@GetMapping("/rateLimit/customerBlockHandler")
@SentinelResource(value = "customerBlockHandler",
blockHandlerClass = CustomerBlockHandler.class, blockHandler = "handlerException2")
public String customerBlockHandler() {
return "200, 自定义限流处理, OK";
}
}
@SentinelResource 用于定义资源,并提供可选的异常处理和 fallback 配置项。 @SentinelResource 注解包含以下属性:
cloudalibaba-provider-payment9003
cloudalibaba-provider-payment9004
cloudalibaba-consumer-order84
<dependencies>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-sentinelartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-actuatorartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
dependencies>
server:
port: 84
spring:
application:
name: nacos-order-consumer
cloud:
nacos:
discovery:
server-addr: localhost:8848
sentinel:
transport:
#配置Sentinel dashboard地址
dashboard: localhost:8080
#默认8719端口,假如被占用会自动从8719开始依次+1扫描,直至找到未被占用的端口
port: 8719
#消费者将要去访问的微服务名称(注册成功进nacos的微服务提供者)
service-url:
nacos-user-service: http://nacos-payment-provider
# 激活Sentinel对Feign的支持
feign:
sentinel:
enabled: true
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients // 开启feign的支持
public class OrderNacosMain84 {
public static void main(String[] args) {
SpringApplication.run(OrderNacosMain84.class, args);
}
}
@Component
@FeignClient(value = "nacos-payment-provider", fallback = PaymentFallbackService.class)
public interface PaymentService {
@GetMapping("/payment/{id}")
public String getPaymentById(@PathVariable("id") Integer id);
}
@Component
public class PaymentFallbackService implements PaymentService{
@Override
public String getPaymentById(Integer id) {
return "openfeign, 服务降级, 444";
}
}
@RestController
public class CircleBreakerController {
@Value("${service-url.nacos-user-service}")
private String serviceUrl;
@Autowired
private RestTemplate restTemplate;
@GetMapping("/consumer/payment/{id}")
@SentinelResource(value = "consumer",
blockHandler = "blockHandler", // 负责处理Sentinel控制台配置违规
fallback = "fallbackHandler", // 负责处理java内部异常
exceptionsToIgnore = {
IllegalArgumentException.class} // 指定忽略的异常
)
public String getPaymentById(@PathVariable("id") Integer id) {
if (id == 0) {
// exceptionsToIgnore属性有IllegalArgumentException.class,
//所以IllegalArgumentException不会跳入指定的兜底程序
throw new IllegalArgumentException("非法参数异常");
} else if (id == 1) {
throw new NullPointerException("空指针异常");
}
return restTemplate.getForObject(serviceUrl + "/payment/" + id, String.class, id);
}
public String blockHandler(Integer id, BlockException blockException) {
return "blockHandler-sentinel限流,无此流水: blockException" + blockException.getMessage();
}
public String fallbackHandler(Integer id, Throwable throwable) {
return "兜底异常handlerFallback,exception内容 " + throwable.getMessage();
}
// ------ openfeign -----
@Autowired
private PaymentService paymentService;
@GetMapping("/consumer/openfeign/payment/{id}")
public String getPaymentThroughFeign(@PathVariable("id") Integer id) {
return paymentService.getPaymentById(id);
}
}
将流量控制规则写进nacos中, cloudalibaba-sentinel-service8401
<dependency>
<groupId>com.alibaba.cspgroupId>
<artifactId>sentinel-datasource-nacosartifactId>
dependency>
server:
port: 8401
spring:
application:
name: cloudalibaba-sentinel-service
cloud:
nacos:
discovery:
server-addr: localhost:8848 #Nacos服务注册中心地址
sentinel:
transport:
dashboard: localhost:8080 #配置Sentinel dashboard地址
port: 8719 # 8719是sentinel所在的后台要和sentinel前台的dashboard交互要用的端口, 如果8719被占用,会自动往上加1,直到找到未被占用端口
# 将sentinel的监控规则持久化保存到nacos中
datasource:
ds1:
nacos:
server-addr: localhost8848
dataId: cloudalibaba-sentinel-service # 与spring.application.name一样
groupId: DEFAULT_GROUP
data-type: json
rule-type: flow
management:
endpoints:
web:
exposure:
include: '*'
feign:
sentinel:
enabled: true # 激活Sentinel对Feign的支持
[{
"resource": "/rateLimit/byUrl", // 资源名称
"IimitApp": "default", // 来源应用
"grade": 1, // 阈值类型,0表示线程数,1表示QPS
"count": 1, // 单机阈值
"strategy": 0, // 流控模式 0表示直接, 1表示关联, 2表示链路
"controlBehavior": 0, // 流控效果 0表示快速失败, 1表示Warm up, 2表示排队等待
"clusterMode": false // 是否为集群
}]
❓ 每个服务内部的数据一致性由本地事务来保证, 但是全局的数据一致性问题没法保证,一次业务操作需要跨多个数据源或需要跨多个系统进行远程调用,就会产生分布式事务问题。
Seata是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。
分布式事务处理过程的一ID+三组件模型:
处理过程:
略
seata-order-service2001
seata-storage-service2002
seata-account-service2003
CREATE DATABASE seata_order;
CREATE DATABASE seata_storage;
CREATE DATABASE seata_account;
CREATE TABLE seata_order.t_order (
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
`count` INT(11) DEFAULT NULL COMMENT '数量',
`money` DECIMAL(11,0) DEFAULT NULL COMMENT'金额',
`status` INT(1) DEFAULT NULL COMMENT '订单状态: 0:创建中; 1:已完结',
) ENGINE=INNODB AUTO_INCREMENT=` DEFAULT CHARSET=utf8;
SELECT * FROM t_order;
CREATE TABLE seata_storage.t_storage (
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY,
`product_id` BIGINT(11) DEFAULT NULL COMMENT '产品id',
`total` INT(11) DEFAULT NULL COMMENT '总库存',
`used` INT(11) DEFAULT NULL COMMENT '已用库存',
`residue` INT(11) DEFAULT NULL COMMENT '剩余库存'
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO seata_storage.t_storage(`id`, `product_id`, `total`, `used`, `residue`)
VALUES ('1', '1', '100', '0','100');
SELECT * FROM t_storage;
CREATE TABLE seata_account.t_account(
`id` BIGINT(11) NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'id',
`user_id` BIGINT(11) DEFAULT NULL COMMENT '用户id',
`total` DECIMAL(10,0) DEFAULT NULL COMMENT '总额度',
`used` DECIMAL(10,0) DEFAULT NULL COMMENT '已用余额', I
`residue` DECIMAL(10,0) DEFAULT '0' COMMENT '剩余可用额度'
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO seata_account.t_account(`id`, `user_id`, `total`, `used`, `residue`)
VALUES ('1', '1', '1000', '0', '1000');
SELECT * FROM t_account;
seata/conf/db_undo_log.sql , 每个库都要建
-- the table to store seata xid data
-- 0.7.0+ add context
-- you must to init this sql for you business databese. the seata server not need it.
-- 此脚本必须初始化在你当前的业务数据库中,用于AT 模式XID记录。与server端无关(注:业务数据库)
-- 注意此处0.3.0+ 增加唯一索引 ux_undo_log
drop table `undo_log`;
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
@GlobalTransactional(name = "fsp-create-order", rollbackFor = Exception.class) // //rollbackFor = Exception.class表示对任意异常都进行回滚