# springcloud-learn **Repository Path**: carp-notes/springcloud-learn ## Basic Information - **Project Name**: springcloud-learn - **Description**: springcloud学习笔记 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 1 - **Created**: 2021-12-20 - **Last Updated**: 2026-01-08 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 一、微服务架构 微服务架构是一种架构模式,它提倡将单一应用划分成一组小的服务,服务之间互相协调、互相配合,为用户提供最终价值。每个服务运行在其独立的进程中,服务与服务间采用轻量级的通信机制互相协作(通常是基于HTTP协议的RestFul API)。每个服务都围绕着具体业务进行构建,并且能够被独立的部署到生产环境、类生产环境等。另外,应当尽量避免统一的、集中式的服务管理机制,对具体的一个服务而言,应根据上下文,选择合适的语言、工具对其进行构建。 SpringCloud是分布式微服务一站式解决方案,是多种微服务架构落地技术的集合体,俗称微服务全家桶 ![1633956522261](./images/1633956522261.png) # 二、版本选择 ##### SpringCloud: 2020.0.3 1)查看SpringCloud最新稳定版 ![1634123992594](./images/1634123992594.png) 2)查看SpringCloud版本依赖,https://start.spring.io/actuator/info ```json { ... "bom-ranges": { ... "spring-cloud": { "Hoxton.SR12": "Spring Boot >=2.2.0.RELEASE and <2.4.0.M1", "2020.0.4": "Spring Boot >=2.4.0.M1 and <2.5.6-SNAPSHOT", "2020.0.5-SNAPSHOT": "Spring Boot >=2.5.6-SNAPSHOT and <2.6.0-M1", "2021.0.0-M1": "Spring Boot >=2.6.0-M1 and <2.6.0-M3", "2021.0.0-M2": "Spring Boot >=2.6.0-M3 and <2.6.0-SNAPSHOT", "2021.0.0-SNAPSHOT": "Spring Boot >=2.6.0-SNAPSHOT" }, ... }, ... } ``` 3)查看SpringCloud推荐Springboot版本 ![1634124785544](./images/1634124785544.png) # 三、组件使用 ![1634125715292](./images/1634125715292.png) # 四、注册中心 ## 4.1、Eureka ### 1、Eureka基础知识 #### 服务治理 ​ Spring Cloud 封装了 Netflix 公司开发的 Eureka 模块来实现服务治理。 ​ 在传统的rpc远程调用框架中,管理每个服务与服务之间依赖关系比较复杂,管理比较复杂,所以需要使用服务治理,**管理服务于服务之间依赖关系,可以实现服务调用、负载均衡、容错等,实现服务发现与注册**。 #### 服务注册和发现 ​ Eureka采用了CS的设计架构,Eureka Server作为服务注册功能的服务器,它是服务注册中心。而系统的其他微服务,使用Eureka的客户端连接到Eureka Server并维持心跳连接。这样的维护人员就可以通过Eureka Server来监控系统中各个微服务是否正常运行。 #### Eureka组件:Eureka Server、Eureka Client ​ Eureka Server提供服务注册服务;各个微服务节点通过配置启动后,会在EurekaServer中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观看到。 ​ EurekaClient通过注册中心进行访问是一个Java客户端,用于简化Eureka Server的交互,客户端同时也具备一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳(默认周期为30秒)。如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,EurekaServer将会从服务注册表中把这个服务节点移除(默认90秒) ### 2、Eureka单机 #### 1)、EurekaServer ###### (a)pom.xml 导入spring-cloud-starter-netflix-eureka-server包 ```xml org.springframework.cloud spring-cloud-starter-netflix-eureka-server ``` ###### (b)application.yml 完善eureka服务 ```yaml server: port: 8001 servlet: context-path: /provider-payment # 服务上下文路径 spring: application: name: provider-payment # 服务名 eureka: instance: # eureka服务端的实例名称 hostname: localhost client: # 不向注册中心注册自己 register-with-eureka: false # false表示自己就是注册中心,职责是维护服务实例,并不需要检索服务 fetch-registry: false service-url: defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ ``` ###### (c)Application.java ![1636025062007](./images/1636025062007.png) #### 2)、EurekaClient (a)pom.xml ```xml org.springframework.cloud spring-cloud-starter-netflix-eureka-client ``` (b)application.yml ```yaml server: port: 8001 servlet: context-path: /provider-payment spring: application: name: provider-payment datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/cloud-01-payment?useUnicode=true&characterEncoding=utf-8&useSSL=false&useOldAliasMetadataBehavior=true username: root password: root mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.liyu.commons.entity eureka: client: # 将自己注册进服务注册中心 register-with-eureka: true # 是否从注册中心抓取已有的注册信息,默认为true,集群必须设置为true,才能配合ribbon使用负载均衡 fetch-registry: true service-url: defaultZone: http://localhost:8000/eureka ``` (c)Application.java ![1636025417413](./images/1636025417413.png) #### 3)、效果 访问EurekaServer服务注册中心 ![1636025577427](./images/1636025577427.png) ### 3、Eureka集群 #### 1)、hosts ![1636025787119](./images/1636025787119.png) #### 2)、EurekaServer ![1636544988905](./images/1636544988905.png) #### 3)、EurekaServer2 ![1636545043508](./images/1636545043508.png) #### 4)、EurekaClient ![1636545511793](./images/1636545511793.png) 5)、效果 ![1636545564433](./images/1636545564433.png) ### 4、Eureka消费客户端 #### 1)使用RestTemplate ###### (a)pom.xml ```xml com.liyu 03-api-commons ${project.parent.version} org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.projectlombok lombok org.springframework.boot spring-boot-starter-test org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.boot spring-boot-maven-plugin 2.5.6 true ``` ###### (b)application.yml ```yaml server: port: 8101 servlet: context-path: /provider-order spring: application: name: provider-order eureka: client: # 将自己注册进服务注册中心 register-with-eureka: true # 是否从注册中心抓取已有的注册信息,默认为true,集群必须设置为true,才能配合ribbon使用负载均衡 fetch-registry: true service-url: # defaultZone: http://localhost:8000/eureka defaultZone: http://eureka.server1.com:8000/eureka/,http://eureka.server2.com:9000/eureka/ ``` ###### (c)Application.java ```java @SpringBootApplication(exclude= {DataSourceAutoConfiguration.class}) @EnableEurekaClient public class ConsumerOrderApplication { public static void main(String[] args) { SpringApplication.run(ConsumerOrderApplication.class,args); } } ``` ###### (d)ContextConfig.java ```java @Configuration public class ContextConfig { @Bean @LoadBalanced // 负载均衡使用 public RestTemplate getRestTemplate(){ return new RestTemplate(); } } ``` ###### (d)OrderController.java ```java @RestController @Slf4j @RequestMapping("/order") public class OrderController { public static final String PAYMENT_URL = "http://PROVIDER-PAYMENT"; @Resource private RestTemplate restTemplate; @Resource private DiscoveryClient discoveryClient; @GetMapping("{id}") public Result get(@PathVariable("id") Long id) { return restTemplate.getForObject(PAYMENT_URL + "/provider-payment/payment/" +id, Result.class); } @GetMapping("/discovery") public Result> getDiscovery() { List services = discoveryClient.getServices(); services.stream().forEach(service -> { log.info("--------------service-name:" + service+"------------------------------"); List instances = discoveryClient.getInstances(service); instances.stream().forEach( instance -> { log.info("instance-name:" + instance.getServiceId()); }); log.info("-------------------------------------------------------------------------"); }); return new Result<>(200, services, null); } } ``` ## 4.2、Zookeeper ### 1、集成Zookeeper #### 1)、Provider生产者 ###### (a)pom.xml ​ 需要引用spring-cloud-starter-zookeeper-discovery包 ```xml ...... org.springframework.cloud spring-cloud-starter-zookeeper-discovery ...... ``` ###### (b)application.yml 配置服务名,和Zookeeper服务ip、端口,集群使用“,”分隔 ```yaml server: port: 8101 spring: application: name: provider-payment # 服务名 cloud: zookeeper: connect-string: 123.56.163.139:2181 #zookeeper注册中心 ``` ###### (c)Application.java 给启动类添加**@EnableDiscoveryClient**注解 ```java @SpringBootApplication(exclude= {DataSourceAutoConfiguration.class}) // @EnableDiscoveryClient注解用于向Consul或Zookeeper服务中心注册服务 @EnableDiscoveryClient public class PaymentApplicaion { public static void main(String[] args) { SpringApplication.run(PaymentApplicaion.class, args); } } ``` ###### (d)实现生产者服务 ```java @RestController @RequestMapping("/payment") @Slf4j public class PaymentController { @Resource private ServerProperties serverProperties; @GetMapping("{id}") public Result get(@PathVariable("id") Long id) { return new Result<>(200, id, "payment服务1,port:->" + + serverProperties.getPort() ); } } ``` #### 2)、Consumer消费者 ###### (a)pom.xml 同Provider一样,需要引用spring-cloud-starter-zookeeper-discovery包 ```xml ...... org.springframework.cloud spring-cloud-starter-zookeeper-discovery ...... ``` ###### (b)application.yml 配置服务名,和Zookeeper服务ip、端口,集群使用“,”分隔 ```yaml server: port: 8201 spring: application: name: consumer-order # 服务名 cloud: zookeeper: connect-string: 123.56.163.139:2181 #zookeeper注册中心 ``` ###### (c)Application.java 给启动类添加**@EnableDiscoveryClient**注解 ```java @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) @EnableDiscoveryClient public class ZkOrderApplication { public static void main(String[] args) { SpringApplication.run(ZkOrderApplication.class, args); } } ``` ###### (d)Config.java 注册**RestTemplate**Bean对象,注意要加上**@LoadBalanced**注解 ```java @Configuration public class OrderConfig { @LoadBalanced @Bean RestTemplate getRestTemplate() { return new RestTemplate(); } } ``` ###### (e)调用生产者服务 使用restTemplate对象通过服务名调用消费者提供的服务 ```java @RestController @RequestMapping("/order") public class OrderController { public static final String PAYMENT_URL = "http://provider-payment"; @Resource private RestTemplate restTemplate; @GetMapping("{id}") public Result get(@PathVariable("id") Long id) { return restTemplate.getForObject(PAYMENT_URL + "/payment/" + id, Result.class); } } ``` #### 3)、访问消费端口 ![1636979474311](./images/1636979474311.png) #### 4)、登入Zookeeper查看注册情况 ###### (a)打开Zookeeper客户端 ```shell # 进入zookeeper目录 cd /java/zookeeper/zookeeper-3.6.2/ # 打开zookeeper服务 ./bin/zkCli.sh ``` ###### (b)查看注册了哪些服务 ```shell ls / ``` ![1636980012396](./images/1636980012396.png) ###### (c)查看services ```shell ls /services ``` ![1636980108428](./images/1636980108428.png) ###### (d)查看服务有哪些实例 ![1636980201371](./images/1636980201371.png) ###### (e)查看服务实例信息 注意,这里使用get命令 ```bash get /services/provider-payment/474ad98d-bca4-4d8f-ba53-1cf2d0e21a20 ``` ![1636980347537](./images/1636980347537.png) 使用json工具格式化后的内容 ```json { "name": "provider-payment", "id": "474ad98d-bca4-4d8f-ba53-1cf2d0e21a20", "address": "DESKTOP-FOF8BJH", "port": 8101, "sslPort": null, "payload": { "@class": "org.springframework.cloud.zookeeper.discovery.ZookeeperInstance", "id": "application-1", "name": "provider-payment", "metadata": {} }, "registrationTimeUTC": 1636974286344, "serviceType": "DYNAMIC", "uriSpec": { "parts": [ { "value": "scheme", "variable": true }, { "value": "://", "variable": false }, { "value": "address", "variable": true }, { "value": ":", "variable": false }, { "value": "port", "variable": true } ] } } ``` ## 4.3、Consul ### 1、Consul概述 Consul是一套开源的**分布式服务发现和配置管理系统**,由**HashiCorp**公司使用**Go语言**开发。 提供了微服务系统中的**服务治理、配置中心、控制总线**等功能。这些功能中的每一个都可以根据需要单位使用,也可以一起使用以构建全方位的服务网络,总之Consul提供了一种完整的服务网格解决方案。 ### 2、安装 #### 1)、下载资源 ###### (a)访问官网https://www.consul.io ###### (b)点击Download,选择Centos下的64-bit压缩包下载 ###### (c)将资源包上传到服务器上 ![1636984359893](./images/1636984359893.png) #### 2)、解压安装 ###### (a)、解压 ``` unzip consul_1.10.3_linux_amd64.zip ``` ###### (b)、移动 ```bash mv consul /java/consul ``` ###### (c)、启动 ```bash # client这里使用的是阿里云公网IP ./consul agent -dev -ui -node=consul-dev -client=172.17.77.192> consul.log & ``` ###### (d)、安装成功,访问公网IP的8500端口 ![1636984836598](./images/1636984836598.png) ### 3、开发示例 #### 1)、生产者 ###### (a)、pom.xml 引入consul启动包 ```xml ... ... org.springframework.cloud spring-cloud-starter-consul-discovery ... ``` ###### (b)、application.yml 修改配置 ```yaml server: port: 8101 spring: application: name: consul-provider-payment # 服务名 cloud: consul: host: 123.56.163.139 port: 8500 discovery: service-name: ${spring.application.name} # 注册服务地址 hostname: 127.0.0.1 ``` ###### (c)、Application.java 添加@EnableDiscoveryClient注解 ```java @SpringBootApplication(exclude= {DataSourceAutoConfiguration.class}) // @EnableDiscoveryClient注解用于向Consul或Zookeeper服务中心注册服务 @EnableDiscoveryClient public class PaymentApplicaion { public static void main(String[] args) { SpringApplication.run(PaymentApplicaion.class, args); } } ``` ###### (d)、服务Controller ```java @RestController @RequestMapping("/payment") @Slf4j public class PaymentController { @Resource private ServerProperties serverProperties; @GetMapping("{id}") public Result get(@PathVariable("id") Long id) { return new Result<>(200, id, "payment服务1,port:->" + serverProperties.getPort() ); } } ``` #### 2)、消费者 ###### (a)、pom.xml 同生产者一样,加入consul包 ```xml ... ... org.springframework.cloud spring-cloud-starter-consul-discovery ... ``` ###### (b)、application.yml 同生产者配置一样,注意端口和服务名不一致 ```yaml server: port: 8201 spring: application: name: consul-consumer-order cloud: consul: host: 123.56.163.139 port: 8500 discovery: service-name: ${spring.application.name} hostname: 127.0.0.1 ``` ###### (c)、Application.java 同生产者一样 ```java @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) @EnableDiscoveryClient public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } } ``` ###### (d)、Config.java 把restTemplate放入Bean工厂 ```java @Configuration public class OrderConfig { @LoadBalanced @Bean RestTemplate getRestTemplate() { return new RestTemplate(); } } ``` ###### (e)、消费Controller 使用restTemplate消费生产者提供的服务 ```java @RestController @RequestMapping("/order") public class OrderController { public static final String PAYMENT_URL = "http://consul-provider-payment"; @Resource private RestTemplate restTemplate; @GetMapping("{id}") public Result get(@PathVariable("id") Long id) { return restTemplate.getForObject(PAYMENT_URL + "/payment/" + id, Result.class); } } ``` #### 3)、效果 ###### (a)、服务消费接口 ![1637049223249](./images/1637049223249.png) ###### (b)、查看Consul中注册了哪些服务 ![1637049576810](./images/1637049576810.png) **注意:在服务中我们发现了注册的服务,但是确带有红叉。** ###### (c)、异常分析 进入服务详细信息页,发现提示127.0.0.1:8201拒绝接连。这是因为我们使用的是阿里云公网ip,不能直接访问到我们的本地网络,故提示错误。 ![1637049655828](./images/1637049655828.png) ### 4.4、3个注册中心的对比 | 注册中心 | 语言 | CAP | 服务健康检测 | 对外暴露接口 | SpringCloud集成 | | --------- | ---- | ---- | -------------- | ------------ | --------------- | | Eureka | Java | AP | 可以配置成支持 | HTTP | 已集成 | | Zookeeper | Java | CP | 支持 | 客户端 | 已集成 | | Consul | Go | CP | 支持 | HTTP、DNS | 已集成 | 什么是CAP? CAP原则又称CAP定理,指的是在一个[分布式系统](https://baike.baidu.com/item/分布式系统/4905336)中, Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。 一致性(C): 在分布式系统中的所有数据备份,在同一时刻是否同样的值。(等同于所有节点访问同一份最新的数据副本) 可用性(A): 保证每个请求不管成功或者失败都有响应 分区容忍性(P): 系统中任意信息的丢失或失败不会影响系统的继续运作 ![capimg](./images/cap.png) # 五、服务调用 ## 5.1、Ribbon ### 1、概述 #### 1)、什么是Ribbon **Ribbon 是一个基于 HTTP 和 TCP 的 客服端负载均衡工具,它是基于 Netflix Ribbon 实现的。** 它不像 Spring Cloud 服务注册中心、配置中心、API 网关那样独立部署,但是它几乎存在于每个 Spring Cloud 微服务中。包括 Feign 提供的声明式服务调用也是基于该 Ribbon 实现的。 Ribbon 默认提供很多种负载均衡算法,例如轮询、随机等等。甚至包含自定义的负载均衡算法。 #### 2)、解决什么 **Ribbon 提供了一套微服务的负载均衡解决方案。** #### 3)、两种负载均衡方案的区别 ###### (a)、集中式负载均衡(服务器端负载均衡) 在服务端和客户端之间使用独立的服务设施(可以是硬件F5;也可以是软件,如nginx),消费者由该设施访问请求通过某种策略转发到生产者。 ###### (b)、进程内负载均衡(客户端负载均衡) 将负载均衡逻辑集成在客户端,客户端在注册中心获得可用的服务,然后在这些服务中选择一个合适的服务进行调用。 Ribbon属于客户端负载均衡 ### 2、使用 #### 1)、注意 在spring-cloud-netflix-eureka-client老版本中默认是集成了Ribbon,但是现在新版已替换成loadbalancer ![1637128130826](./images/1637128130826.png) #### 2)、修改包的版本 ###### (a)、pom.xml 这里将cloud、boot、eureka版本进行下降 ```xml 2.2.5.RELEASE Hoxton.SR12 2.3.12.RELEASE ``` #### 3)、Ribbon自带的负载策略 ![IRule](./images/IRule.png) ###### (a)、RandomRule(随机) ```java /** * Randomly choose from all living servers * 随机从全部活着的服务中获得服务 */ @edu.umd.cs.findbugs.annotations.SuppressWarnings(value = "RCN_REDUNDANT_NULLCHECK_OF_NULL_VALUE") public Server choose(ILoadBalancer lb, Object key) { if (lb == null) { return null; } Server server = null; while (server == null) { // 判断当前线程是否中断,如果中断清除中断状态,并退出 if (Thread.interrupted()) { return null; } // 获得有效的服务实例列表(只读) List upList = lb.getReachableServers(); // 获得全部服务实例列表 List allList = lb.getAllServers(); int serverCount = allList.size(); // 不存在服务返回null if (serverCount == 0) { /* * No servers. End regardless of pass, because subsequent passes * only get more restrictive. */ return null; } // 获得一个小于服务实例总数的随机数 int index = chooseRandomInt(serverCount); // 获得有效服务实例 server = upList.get(index); if (server == null) { /* * The only time this should happen is if the server list were * somehow trimmed. This is a transient condition. Retry after * yielding. */ Thread.yield(); continue; } if (server.isAlive()) { return (server); } // Shouldn't actually happen.. but must be transient or a bug. server = null; Thread.yield(); } return server; } ``` ###### (b)、RoundRobinRule(轮询) ```java public Server choose(ILoadBalancer lb, Object key) { if (lb == null) { log.warn("no load balancer"); return null; } Server server = null; int count = 0; // 如果连续10次都是无效的,则返回null while (server == null && count++ < 10) { // 获得有效实例列表 List reachableServers = lb.getReachableServers(); // 获得全部实例列表 List allServers = lb.getAllServers(); int upCount = reachableServers.size(); int serverCount = allServers.size(); if ((upCount == 0) || (serverCount == 0)) { log.warn("No up servers available from load balancer: " + lb); return null; } // 获得下一个实例下标 int nextServerIndex = incrementAndGetModulo(serverCount); server = allServers.get(nextServerIndex); if (server == null) { /* Transient. */ Thread.yield(); continue; } // 如果该实例有效则返回 if (server.isAlive() && (server.isReadyToServe())) { return (server); } // Next. server = null; } if (count >= 10) { log.warn("No available alive servers after 10 tries from load balancer: " + lb); } return server; } ``` ###### (c)、WeightedResponseTimeRule 对RoundRobinRule的扩展,响应速度越快,越容易被选择 ```java public Server choose(ILoadBalancer lb, Object key) { if (lb == null) { return null; } Server server = null; while (server == null) { // get hold of the current reference in case it is changed from the other thread // 获得权重值集合,默认30秒刷新一次,该值使用volatile关键字,保证每次都是获得最新的数据 List currentWeights = accumulatedWeights; if (Thread.interrupted()) { return null; } List allList = lb.getAllServers(); int serverCount = allList.size(); if (serverCount == 0) { return null; } int serverIndex = 0; // last one in the list is the sum of all weights // 获得权值(根据响应时间计算) double maxTotalWeight = currentWeights.size() == 0 ? 0 : currentWeights.get(currentWeights.size() - 1); // No server has been hit yet and total weight is not initialized // fallback to use round robin // 如果服务器都很快,或者权值未初始化,执行轮询方法 if (maxTotalWeight < 0.001d || serverCount != currentWeights.size()) { server = super.choose(getLoadBalancer(), key); if(server == null) { return server; } } else { // generate a random weight between 0 (inclusive) to maxTotalWeight (exclusive) // 获得随机权值 double randomWeight = random.nextDouble() * maxTotalWeight; // pick the server index based on the randomIndex int n = 0; // 权值越小越容易获得使用 for (Double d : currentWeights) { if (d >= randomWeight) { serverIndex = n; break; } else { n++; } } server = allList.get(serverIndex); } if (server == null) { /* Transient. */ Thread.yield(); continue; } if (server.isAlive()) { return (server); } // Next. server = null; } return server; } ``` 权值计算: 1、初始化方法,执行定时任务,并执行一次更新权重的方法 ```java void initialize(ILoadBalancer lb) { if (serverWeightTimer != null) { serverWeightTimer.cancel(); } serverWeightTimer = new Timer("NFLoadBalancer-serverWeightTimer-" + name, true); // 延迟0秒执行一次,然后固定时间执行一次,默认30秒 serverWeightTimer.schedule(new DynamicServerWeightTask(), 0, serverWeightTaskTimerInterval); // do a initial run // 运行获得权重的方法 ServerWeight sw = new ServerWeight(); sw.maintainWeights(); Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() { public void run() { logger .info("Stopping NFLoadBalancer-serverWeightTimer-" + name); serverWeightTimer.cancel(); } })); } ``` 2、定时任务中,执行更新权重的方法 ```java class DynamicServerWeightTask extends TimerTask { public void run() { ServerWeight serverWeight = new ServerWeight(); try { serverWeight.maintainWeights(); } catch (Exception e) { logger.error("Error running DynamicServerWeightTask for {}", name, e); } } } ``` 3、更新权重的方法ServerWeight.maintainWeights ```java class ServerWeight { public void maintainWeights() { ILoadBalancer lb = getLoadBalancer(); if (lb == null) { return; } // CAS自旋锁,判断当前是否有任务正在执行计算权重方法,如果有,返回false则不执行任务 if (!serverWeightAssignmentInProgress.compareAndSet(false, true)) { return; } try { logger.info("Weight adjusting job started"); AbstractLoadBalancer nlb = (AbstractLoadBalancer) lb; // 获得负载均衡器状态总控 LoadBalancerStats stats = nlb.getLoadBalancerStats(); if (stats == null) { // no statistics, nothing to do return; } double totalResponseTime = 0; // find maximal 95% response time // 遍历服务 for (Server server : nlb.getAllServers()) { // this will automatically load the stats if not in cache // 获得服务状态 ServerStats ss = stats.getSingleServerStat(server); // 获得平均响应时间和 totalResponseTime += ss.getResponseTimeAvg(); } // weight for each server is (sum of responseTime of all servers - responseTime) // 每个服务器的权重为(所有服务器的responseTime之和- responseTime) // so that the longer the response time, the less the weight and the less likely to be chosen // 响应时间越长权重越小,越容易被选中 Double weightSoFar = 0.0; // create new list and hot swap the reference List finalWeights = new ArrayList(); for (Server server : nlb.getAllServers()) { ServerStats ss = stats.getSingleServerStat(server); double weight = totalResponseTime - ss.getResponseTimeAvg(); weightSoFar += weight; finalWeights.add(weightSoFar); } setWeights(finalWeights); } catch (Exception e) { logger.error("Error calculating server weights", e); } finally { serverWeightAssignmentInProgress.set(false); } } } ``` **例**:响应时间:1、2、3、4 总响应时间:10 权重:9、8、7、6 权值:9、17、24、30 ###### (d)RetryRule(轮询,无效则一定时间内继续轮询) ```java /* * Loop if necessary. Note that the time CAN be exceeded depending on the * subRule, because we're not spawning additional threads and returning * early. */ public Server choose(ILoadBalancer lb, Object key) { long requestTime = System.currentTimeMillis(); long deadline = requestTime + maxRetryMillis; Server answer = null; // 获得轮询得到的服务 answer = subRule.choose(key); // 如果服务无效,且当前时间小于最后期限 if (((answer == null) || (!answer.isAlive())) && (System.currentTimeMillis() < deadline)) { // 最后期限时刻,中断线程 InterruptTask task = new InterruptTask(deadline - System.currentTimeMillis()); // 线程是否中断,如果中断则不循环,并恢复线程 while (!Thread.interrupted()) { // 获得轮询服务 answer = subRule.choose(key); // 判断服务是否有效,且当前时间小于最后期限则继续执行轮询方法 if (((answer == null) || (!answer.isAlive())) && (System.currentTimeMillis() < deadline)) { /* pause and retry hoping it's transient */ Thread.yield(); } else { break; } } task.cancel(); } if ((answer == null) || (!answer.isAlive())) { return null; } else { return answer; } } ``` ###### (e)、BestAvailableRule 先过滤由于多次故障而处于断路器跳闸状态的服务,返回选中并发量最小的服务 ```java public Server choose(Object key) { if (loadBalancerStats == null) { return super.choose(key); } List serverList = getLoadBalancer().getAllServers(); // 初始化最小并发量 int minimalConcurrentConnections = Integer.MAX_VALUE; long currentTime = System.currentTimeMillis(); Server chosen = null; for (Server server: serverList) { // 获得服务状态 ServerStats serverStats = loadBalancerStats.getSingleServerStat(server); // 判断当前时间断路器是否断开 if (!serverStats.isCircuitBreakerTripped(currentTime)) { // 获得有效的请求连接,即并发数 int concurrentConnections = serverStats.getActiveRequestsCount(currentTime); // 获得并发量最小的服务 if (concurrentConnections < minimalConcurrentConnections) { minimalConcurrentConnections = concurrentConnections; chosen = server; } } } // 如果没有,则执行轮询方法 if (chosen == null) { return super.choose(key); } else { return chosen; } } ``` ###### (f)、AvailabilityFilteringRule 想过滤故障实例,再选择并发较小的实例 ```java @Override public Server choose(Object key) { int count = 0; // 轮询获得一个服务 Server server = roundRobinRule.choose(key); // 最多判断10次 while (count++ <= 10) { // 判断是否满足条件 if (predicate.apply(new PredicateKey(server))) { return server; } server = roundRobinRule.choose(key); } return super.choose(key); } ``` 判断方法 ```java @Override public boolean apply(@Nullable PredicateKey input) { LoadBalancerStats stats = getLBStats(); if (stats == null) { return true; } return !shouldSkipServer(stats.getSingleServerStat(input.getServer())); } private boolean shouldSkipServer(ServerStats stats) { // 有效 && 断路器未断开 || 有效连接数 >= 最小连接数 if ((CIRCUIT_BREAKER_FILTERING.get() && stats.isCircuitBreakerTripped()) || stats.getActiveRequestsCount() >= activeConnectionsLimit.get()) { return true; } return false; } ``` ###### (g)、ZoneAvoidanceRule 默认规则,复合判断服务所在区域的性能和可用性,选择服务; 1、先看父类**PredicateBasedRule** ```java public abstract class PredicateBasedRule extends ClientConfigEnabledRoundRobinRule { // 子类实现该方法,提供AbstractServerPredicate实例 public abstract AbstractServerPredicate getPredicate(); @Override public Server choose(Object key) { ILoadBalancer lb = getLoadBalancer(); // 提供子类提供的类来对服务列表进行过滤 Optional server = getPredicate().chooseRoundRobinAfterFiltering(lb.getAllServers(), key); if (server.isPresent()) { return server.get(); } else { return null; } } } ``` 2、ZoneAvoidanceRule源码 getPredicate提供了父类需要的compositePredicate ```java public AbstractServerPredicate getPredicate() { return compositePredicate; } ``` ZoneAvoidanceRule在实例化的时候,创建了compositePredicate对象 ```java public ZoneAvoidanceRule() { super(); ZoneAvoidancePredicate zonePredicate = new ZoneAvoidancePredicate(this); AvailabilityPredicate availabilityPredicate = new AvailabilityPredicate(this); compositePredicate = createCompositePredicate(zonePredicate, availabilityPredicate); } ``` 并且在createCompositePredicate方法中,加入了两种Predicate过滤方式 ```java private CompositePredicate createCompositePredicate(ZoneAvoidancePredicate p1, AvailabilityPredicate p2) { return CompositePredicate.withPredicates(p1, p2) .addFallbackPredicate(p2) .addFallbackPredicate(AbstractServerPredicate.alwaysTrue()) .build(); } ``` 3、AbstractServerPredicate 查看chooseRoundRobinAfterFiltering方法干了什么 ```java public Optional chooseRoundRobinAfterFiltering(List servers, Object loadBalancerKey) { // 获得合规的服务 List eligible = getEligibleServers(servers, loadBalancerKey); if (eligible.size() == 0) { return Optional.absent(); } return Optional.of(eligible.get(incrementAndGetModulo(eligible.size()))); } ``` getEligibleServers,中执行apply方法,判断服务是否合规 ```java public List getEligibleServers(List servers, Object loadBalancerKey) { if (loadBalancerKey == null) { return ImmutableList.copyOf(Iterables.filter(servers, this.getServerOnlyPredicate())); } else { List results = Lists.newArrayList(); for (Server server: servers) { // 执行apply方法,查看是否合规 if (this.apply(new PredicateKey(loadBalancerKey, server))) { results.add(server); } } return results; } } ``` 4、CompositePredicate 查看getEligibleServers是怎么获得合规的服务的 ```java public List getEligibleServers(List servers, Object loadBalancerKey) { List result = super.getEligibleServers(servers, loadBalancerKey); // 获得过滤策略 Iterator i = fallbacks.iterator(); // 如果!(服务数>=最小服务数(默认1) && 服务数 > 服务数最小过滤比(默认0)) && 存在下一个过滤服务 while (!(result.size() >= minimalFilteredServers && result.size() > (int) (servers.size() * minimalFilteredPercentage)) && i.hasNext()) { AbstractServerPredicate predicate = i.next(); // 执行子过滤策略获得合规服务 result = predicate.getEligibleServers(servers, loadBalancerKey); } return result; } ``` 5、ZoneAvoidancePredicate ```java @Override public boolean apply(@Nullable PredicateKey input) { if (!ENABLED.get()) { return true; } // 获得服务区域 String serverZone = input.getServer().getZone(); if (serverZone == null) { // there is no zone information from the server, we do not want to filter // out this server return true; } // 总控判断 LoadBalancerStats lbStats = getLBStats(); if (lbStats == null) { // no stats available, do not filter return true; } // 如果只有一个区域是有效的,不进行过滤 if (lbStats.getAvailableZones().size() <= 1) { // only one zone is available, do not filter return true; } // 创建快照 Map zoneSnapshot = ZoneAvoidanceRule.createSnapshot(lbStats); if (!zoneSnapshot.keySet().contains(serverZone)) { // The server zone is unknown to the load balancer, do not filter it out return true; } logger.debug("Zone snapshots: {}", zoneSnapshot); // 获得有效区域集合 Set availableZones = ZoneAvoidanceRule.getAvailableZones(zoneSnapshot, triggeringLoad.get(), triggeringBlackoutPercentage.get()); logger.debug("Available zones: {}", availableZones); if (availableZones != null) { // 当前服务是否是有效的 return availableZones.contains(input.getServer().getZone()); } else { return false; } } ``` #### 4)、自定义负载策略 ##### (1)、自定义负载策略 ###### (a)、自定义规则类 ```java /** * 自定义规则,这里取第一个 */ public class MyLoadBalancerRule extends AbstractLoadBalancerRule { public Server choose(ILoadBalancer lb, Object key) { if (lb == null) { return null; } Server server = null; int count = 0; while (server == null && count ++ <10) { if (Thread.interrupted()) { return null; } List upList = lb.getReachableServers(); for (Server s : upList) { System.out.println(s.getId() + ":" + s.getHostPort() ); } int serverCount = upList.size(); if (serverCount == 0) { return null; } server = upList.get(0); if (server == null) { Thread.yield(); continue; } if (server.isAlive()) { return (server); } server = null; Thread.yield(); } return server; } @Override public Server choose(Object key) { return choose(getLoadBalancer(), key); } @Override public void initWithNiwsConfig(IClientConfig clientConfig) { } } ``` ###### (b)、创建自己的Rule配置类,注入自定义Rule ```java @Configuration public class MySelfRule { @Bean public IRule getRule() { return new MyLoadBalancerRule(); } } ``` ###### (c)、主启动类上使用@RibbonClient或@RibbonClients,扫描自定义Rule配置类 ```java @SpringBootApplication(exclude= {DataSourceAutoConfiguration.class}) @EnableEurekaClient @RibbonClient(value = "service", configuration = MySelfRule.class) public class ConsumerOrderApplication { public static void main(String[] args) { SpringApplication.run(ConsumerOrderApplication.class,args); } } ``` ## 5.2、OpenFeign ### 1、概述 #### 1)、OpenFeign的特点 ​ ![1637227087275](./images/1637227087275.png) ​ 声明式 REST 客户端:Feign 创建了用 JAX-RS 或 Spring MVC 注释修饰的接口的动态实现。即简化了调用http的调用。 #### 2)、OpenFeign和Feign的区别 | Feign | OpenFeign | | ------------------------------------------------------------ | ------------------------------------------------------------ | | Feign是Springcloud组件中的一个轻量级Restful的HTTP服务客户端,Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务。 | OpenFeign是springcloud在Feign的基础上支持了SpringMVC的注解,如@RequestMapping等等。OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。 | | spring-cloud-starter-feign | spring-cloud-starter-openfeign | ### 2、使用 ##### 1)、Eureka服务端 ​ 同 `四 -> 4.2 -> 2 -> 1)` 一致。 ##### 2)、生产者 ​ 同 `四 -> 4.2 -> 2 -> 2)` 一致。 ##### 3)、消费者 ###### (a)、pom.xml 引入OpenFeign包 ```xml org.springframework.cloud spring-cloud-starter-openfeign ``` ###### (b)、服务调用接口 实现生产者服务对应接口,需要添加`@FeignClient`,并且`@RequestMapping`地址需要和服务端Controller一致 ```java @Service @FeignClient(value = "PROVIDER-PAYMENT") @RequestMapping("/payment") public interface IPaymentService { /** * 需要和生产者服务url地址保持一致 */ @GetMapping("{id}") Result get(@PathVariable("id") Long id); } ``` ###### (c)、消费端Controller 调用刚刚实现的FeignClient接口 ```java @RestController @Slf4j @RequestMapping("/order") public class OrderController { @Resource private IPaymentService paymentService; @GetMapping("{id}") public Result get(@PathVariable("id") Long id) { return paymentService.get(id); } } ``` ###### (d)、主启动类 主启动类上需要添加`@EnableFeignClients`注解启动`OpenFeign`主启动类上需要添加`@EnableFeignClients`注解启动`OpenFeign` ```java @SpringBootApplication(exclude= {DataSourceAutoConfiguration.class}) @EnableEurekaClient @EnableFeignClients public class ConsumerOrderApplication { public static void main(String[] args) { SpringApplication.run(ConsumerOrderApplication.class,args); } } ``` ##### 4)、超时时间 我们可以通过`feign.config.default`设置默认配置,`connectTimeout`和`readTimeout`可以设置超时时间。 ```yaml feign: client: config: # 设置默认配置 default: # 建立连接时间 connectTimeout: 5000 # 建立连接后,从服务器获得资源的时间 readTimeout: 5000 ``` **注意:**老版默认超时时间是1秒,且可以通过`ribbon.readTimeout`和`ribbon.connectTimeout`设置超时时间。 ```yaml #设置feign 客户端超时时间(openFeign默认支持ribbon) ribbon: #指的是建立连接所用的时间,适用于网络状况正常的情况下,两端连接所用的时间 ReadTimeout: 5000 #指的是建立连接后从服务器读取到可用资源所用的时间 ConnectTimeout: 5000 ``` ##### 5)、日志增强 为每个创建的 Feign 客户端创建一个记录器。默认情况下,记录器的名称是用于创建 Feign 客户端的接口的完整类名。Feign logging 只响应`DEBUG`级别。 ###### (a)、application.yml ```yaml logging: level: com.liyu.service.IPaymentService: debug ``` ###### (b)、设置日志打印内容 `Logger.Level`您可以为每个客户端配置的对象告诉 Feign 要记录多少。选择是: - `NONE`, 无日志记录(**默认**)。 - `BASIC`, 只记录请求方法和 URL 以及响应状态码和执行时间。 - `HEADERS`, 记录基本信息以及请求和响应标头。 - `FULL`, 记录请求和响应的标头、正文和元数据。 ```java @Configuration public class FooConfiguration { @Bean // feign.Logger Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } } ``` ###### (c)、日志效果 ![1637239171026](./images/1637239171026.png) # 六、服务降级 #### 背景 ​ 分布式系统环境下,服务间相互依赖非常常见,一个业务调用通常依赖多个基础服务。当其中一个服务不可用时,其他服务请求线程被阻塞,但有大量的服务调用时,最终可能导致整个服务资源被耗尽,无法继续对外提供服务。并且这种不可用可能会沿着调用链向上传递,这种现象被称为雪崩。例如下图: ![img](./images/170502_7fqS_2663573.png) #### **雪崩效应常见场景** - 硬件故障:如服务器宕机,机房断电,光纤被挖断等。 - 流量激增:如异常流量,重试加大流量等。 - 缓存穿透:一般发生在应用重启,所有缓存失效时,以及短时间内大量缓存失效时。大量的缓存不命中,使请求直击后端服务,造成服务提供者超负荷运行,引起服务不可用。 - 程序BUG:如程序逻辑导致内存泄漏,JVM长时间FullGC等。 - 同步等待:服务间采用同步调用模式,同步等待造成的资源耗尽。 #### **雪崩效应应对策略** 针对造成雪崩效应的不同场景,可以使用不同的应对策略,没有一种通用所有场景的策略,参考如下: - 硬件故障:多机房容灾、异地多活等。 - 流量激增:服务自动扩容、流量控制(限流、关闭重试)等。 - 缓存穿透:缓存预加载、缓存异步加载等。 - 程序BUG:修改程序bug、及时释放资源等。 - 同步等待:资源隔离、MQ解耦、不可用服务调用快速失败等。资源隔离通常指不同服务调用采用不同的线程池;不可用服务调用快速失败一般通过熔断器模式结合超时机制实现。 ### 结论 ​ 综上所述,如果一个应用不能对来自依赖的故障进行隔离,那该应用本身就处在被拖垮的风险中。 因此,为了构建稳定、可靠的分布式系统,我们的服务应当具有自我保护能力,当依赖服务不可用时,当前服务启动自我保护功能,从而避免发生雪崩效应。 ## 6.1、Hystrix ### 1、概述 ###### (a)是什么? ​ Hystrix是一个用于处理分布式系统的延迟和容错的开源库,在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等,Hystrixs能保证在一个依赖出问题的情况下,不会导致整体服务失败,避免级联故障,以提高分布式系统的弹性。 ​ “断路器”本身是一种开关设置,当某个服务单元发生故障之后,通过断路器故障监控(类似熔断保险丝),向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常,这样就保证了服务调用方的线程不会被长时间的、不必要的占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。 ###### (b)、服务降级 ​ 当服务器压力剧增的情况下,根据实际业务情况及流量,对一些服务和页面有策略的不处理或换种简单的方式处理,从而释放服务器资源以保证核心交易正常运作或高效运作。 **哪些情况会出现服务降级?** - 程序运行异常 - 响应超时 - 服务熔断触发服务降级 - 线程池/信号量打满也会导致服务降级 ###### (c)、服务熔断 ​ 服务熔断的作用类似于我们家用的保险丝,当某服务出现不可用或响应超时的情况时,为了防止整个系统出现雪崩,暂时停止对该服务的调用。 ​ **注意:**一般熔断后,会调用服务降级。 ###### (d)、服务限流 ​ 在高并发场景下,为保证在现有资源条件下服务正常运行,使用服务限流让请求和并发在应用可接受的范围内,达到高可用的目的。 ### 2、使用 #### 1)、模拟高并发 ###### (a)、服务端 服务端实现两个接口,一个正常接口,一个延迟访问 ```java @GetMapping("{id}") public Result get(@PathVariable("id") Long id) throws InterruptedException { long startMills = System.currentTimeMillis(); log.info("payment服务1"); Payment payment = paymentService.getById(id); long endMills = System.currentTimeMillis(); return new Result<>(200, payment, "payment服务1_get,时间:" + (endMills - startMills)); } @GetMapping("/timeout/{id}") public Result getByTimeout(@PathVariable("id") Long id) throws InterruptedException { long startMills = System.currentTimeMillis(); log.info("payment服务1"); Payment payment = paymentService.timeoutGetById(id); long endMills = System.currentTimeMillis(); return new Result<>(200, payment, "payment服务1_timeout,时间:" + (endMills - startMills)); } ``` ```java @Service public class PaymentServiceImpl extends ServiceImpl implements IPaymentService { @Override public Payment timeoutGetById(Long id) throws InterruptedException { // 睡3秒 TimeUnit.SECONDS.sleep(3); return getById(id); } } ``` ###### (b)、查看访问的正常情况 `http://localhost:8101/payment/1`访问很快没有延迟 ![1637493040153](./images/1637493040153.png) `http://localhost:8101/payment/timeout/1`访问存在延迟 ![1637493134669](./images/1637493134669.png) ###### (c)、JMeter对延迟接口进行并发访问 **添加线程组,设置并发量** ![1637493282320](./images/1637493282320.png) **添加接口,并完善接口信息** ![1637493362568](./images/1637493362568.png) ![1637493394147](./images/1637493394147.png) ###### (d)、再次访问接口 `http://localhost:8101/payment/1`本来应该很快,但是却变慢了,出现了小圈圈 ![1637493546047](./images/1637493546047.png) `http://localhost:8101/payment/timeout/1`访问存在延迟 ![1637493583668](./images/1637493583668.png) ###### (e)、接口变慢分析 大量并发请求堆积在`timeout`接口,导致系统需要对该接口倾斜更多资源,故`get`接口资源不足,响应资源就会变慢。 #### 2)、服务降级 ##### (1)、服务端降级测试 ###### (a)、pom.xml ```xml org.springframework.cloud spring-cloud-starter-netflix-hystrix ``` ###### (b)、降级配置注解@HystrixCommand ```java @HystrixCommand(fallbackMethod = "timeoutGetById_Handler", commandProperties = { // 设置最大超时时间为3s @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000") }) @GetMapping("/timeout/{id}") public Result getByTimeout(@PathVariable("id") Long id) throws InterruptedException { long startMills = System.currentTimeMillis(); log.info("payment服务1"); // 设置睡眠时间大于5秒 TimeUnit.SECONDS.sleep(5); Payment payment = paymentService.getById(id); long endMills = System.currentTimeMillis(); return new Result<>(200, payment, "payment服务1_timeout,时间:" + (endMills - startMills)+",线程:" + Thread.currentThread().getName()); } /** * 降级执行方法 */ public Result timeoutGetById_Handler() { log.info("payment服务1"); return new Result<>(200, null, "payment服务1_timeout服务超时,线程" + Thread.currentThread().getName()); } ``` ###### (c)、Application.java 插入EnableCircuitBreaker注解,新版废弃,可以直接使用EnableHystrix注解 ```java @SpringBootApplication @EnableEurekaClient //@EnableCircuitBreaker @EnableHystrix @MapperScan(basePackages = {"com.liyu.**.mapper"}) public class PaymentApplication { public static void main(String[] args) { SpringApplication.run(PaymentApplication.class,args); } } ``` ###### (d)、查看效果 成功执行了fallback方法 ![1637503529977](./images/1637503529977.png) ##### (2)、消费端消费降级 ###### (a)、pom.xml ```xml org.springframework.cloud spring-cloud-starter-netflix-hystrix ``` ###### (b)、消费端controller ```java @HystrixCommand(fallbackMethod = "timeoutGetById_Handler", commandProperties = { // 设置最大超时时间为3s @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "1500") }) @GetMapping("/timeout/{id}") public Result getByTimeout(@PathVariable("id") Long id) throws InterruptedException { // int a = 10/0; // 测试错误时是否会走降级方法 return paymentService.getByTimeout(id); } /** * 降级执行方法 */ public Result timeoutGetById_Handler(Long id) { log.info("payment服务1"); return new Result<>(200, null, "order服务,payment超时或移出,请稍后再试,线程" + Thread.currentThread().getName()); } ``` ###### (c)、Application.java ```java @SpringBootApplication(exclude= {DataSourceAutoConfiguration.class}) @EnableEurekaClient @EnableFeignClients @EnableHystrix public class ConsumerOrderApplication { public static void main(String[] args) { SpringApplication.run(ConsumerOrderApplication.class,args); } } ``` ###### (d)、appliction.yml 新版可略 ```properties feign.hystrix.enabled = true ``` ###### (e)、效果 ![1637504614080](./images/1637504614080.png) ##### (3)、膨胀问题 上面的降级都有对单个方法进行一对一的降级配置,实际上应该是多个方法对应一个降级方法。 ###### (a)、@DefaultProperties 在需要统一降级处理的类上加@DefaultProperties注解,然后在需要降级的方法上加@HystrixCommand ```java @RestController @Slf4j @RequestMapping("/order") @DefaultProperties(defaultFallback = "timeoutGetById_Handler") public class OrderController { @Resource private IPaymentService paymentService; @GetMapping("{id}") public Result get(@PathVariable("id") Long id) { return paymentService.get(id); } @GetMapping("/timeout/{id}") @HystrixCommand public Result getByTimeout(@PathVariable("id") Long id) throws InterruptedException { int a = 10/0; return paymentService.getByTimeout(id); } /** * 降级执行方法 */ public Result timeoutGetById_Handler() { log.info("order服务timeoutGetById_Handler"); return new Result<>(200, null, "order服务,payment超时或移出,请稍后再试,线程" + Thread.currentThread().getName()); } } ``` ###### (b)、访问效果 ![1637583720611](./images/1637583720611.png) ##### (4)、解耦 上述中,降级的方法,和降级后执行方法在一个类中,应该进行解耦合进行解耦 ###### (a)、PaymentFallbackServiceImpl PaymentFallbackServiceImpl实现IPaymentService接口,并加入bean容器,里面实现各个fallback方法 ```java @Component public class PaymentFallbackServiceImpl implements IPaymentService { @Override public Result get(Long id) { return new Result<>(200, null, "get_Handler,线程:" + Thread.currentThread().getName()); } @Override public Result getByTimeout(Long id) { return new Result<>(200, null, "getByTimeout_Handler,线程:" + Thread.currentThread().getName()); } } ``` ###### (b)、IPaymentService IPaymentService接口的@FeignClient注解添加fallback指向刚刚创建的降级处理类 ```java @Service @FeignClient(value = "PROVIDER-PAYMENT", fallback = PaymentFallbackServiceImpl.class) //@RequestMapping("/payment") // 如果不隐藏,会导致子类也会进行RequertMapping的重复映射 public interface IPaymentService { /** * 需要和生产者服务url地址保持一致 */ @GetMapping("/payment/{id}") Result get(@PathVariable("id") Long id); @GetMapping("/payment/timeout/{id}") Result getByTimeout(@PathVariable("id") Long id); } ``` ###### (c)、controller ```java @RestController @Slf4j @RequestMapping("/order") public class OrderController { @Resource private IPaymentService paymentService; @GetMapping("{id}") public Result get(@PathVariable("id") Long id) { return paymentService.get(id); } @GetMapping("/timeout/{id}") public Result getByTimeout(@PathVariable("id") Long id) throws InterruptedException { // int a = 10/0; return paymentService.getByTimeout(id); } } ``` ###### (d)、appliaction.yml ```java feign: # 老版本配置 # hystrix: # enabled: true # 2020版本之后 circuitbreaker: enabled: true ``` ###### (e)、效果 ![1637586006905](./images/1637586006905.png) #### 3)、服务熔断 ##### (1)、简单案例 注意:这里使用生产者作为测试 ###### (a)、Controller ```java @GetMapping("/circuit/{id}") public String circuit(@PathVariable("id") Long id) throws InterruptedException { return paymentService.circuit(id); } ``` ###### (b)、Service ```java @Override @HystrixCommand(fallbackMethod = "circuit_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 circuit(Long id) { if (id == null || id <0) { throw new RuntimeException("id不能小于0"); } return "payment服务1,方法circuit,线程:" + Thread.currentThread().getName() + ",时间:" + DateUtils.formatDate(new Date(), "yyyy-MM-dd HH:mm:ss"); } public String circuit_fallback(Long id) { return "payment服务1,方法circuit_fallback,id不能小于0,线程:" + Thread.currentThread().getName(); } ``` ###### (c)、正常效果 正确id效果 ![1637668297964](./images/1637668297964.png) 错误id效果 ![1637668363520](./images/1637668363520.png) ###### (d)、熔断效果 我们多次发送错误请求,后查看正确请求返回结果,可以发现他也返回错误请求 ![1637668436110](./images/1637668436110.png) 等一会再发送正确请求,这时返回正确结果 ![1637668524801](E:\Files\资料笔记\spring\springcloud\images\1637668524801.png) ##### (2)、总结 ###### (a)、熔断类型 - **熔断打开**:请求不再进行调用当前服务,内部设置时钟一般为MTTR(平均故障处理时间),当打开时长达到所设时钟则进入**半熔断状态**。 - **熔断关闭**:熔断关闭不会对服务进行熔断。 - **熔断半开**:部分请求根据规则调用当前服务,如果请求成功且符合规则,则认为当前服务恢复正常,关闭熔断。 ###### (b)、断路器打开或关闭的条件 **开启条件** - 当满足一定阈值(默认:10秒内超过20个请求) - 当失败率达到一定的时候(默认:10秒超过50%的请求失败) **关闭条件** - 一段时间后(默认是5秒),断路器进入半开状态,会让其中一个请求进行转发。如果成功,断路器会关闭,若还是失败,继续开启 **注意**:当断路器打开的时候,所有请求都不会进行转发 ###### (c)、断路器打开后 1. 再有请求调用的时候,将不会调用主逻辑,而是直接调用降级fallback。通过断路器,实现了自动地发现错误并将降级逻辑切换为主逻辑,减少响应时间 2. 原来的主逻辑要如何恢复 Hystrix提供了自动恢复功能。 主逻辑打开后,hystrix会启动一个休眠时间窗,在这个时间窗内,降级逻辑成为临时主逻辑; 当休眠时间窗口到期,断路器进入半开状态,释放一次请求到原来的主逻辑上,如果此次请求返回正常,那么断路器将关闭,主逻辑恢复,如果依然有问题,断路器继续进入打开状态,休眠窗口重新计时。 ##### (3)、其他配置注解 ```java //========================All @HystrixCommand(fallbackMethod = "str_fallbackMethod", groupKey = "strGroupCommand", commandKey = "strCommand", threadPoolKey = "strThreadPool", commandProperties = { // 设置隔离策略,THREAD 表示线程池 SEMAPHORE:信号池隔离 @HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"), // 当隔离策略选择信号池隔离的时候,用来设置信号池的大小(最大并发数) @HystrixProperty(name = "execution.isolation.semaphore.maxConcurrentRequests", value = "10"), // 配置命令执行的超时时间 @HystrixProperty(name = "execution.isolation.thread.timeoutinMilliseconds", value = "10"), // 是否启用超时时间 @HystrixProperty(name = "execution.timeout.enabled", value = "true"), // 执行超时的时候是否中断 @HystrixProperty(name = "execution.isolation.thread.interruptOnTimeout", value = "true"), // 执行被取消的时候是否中断 @HystrixProperty(name = "execution.isolation.thread.interruptOnCancel", value = "true"), // 允许回调方法执行的最大并发数 @HystrixProperty(name = "fallback.isolation.semaphore.maxConcurrentRequests", value = "10"), // 服务降级是否启用,是否执行回调函数 @HystrixProperty(name = "fallback.enabled", value = "true"), // 是否启用断路器 @HystrixProperty(name = "circuitBreaker.enabled", value = "true"), // 该属性用来设置在滚动时间窗中,断路器熔断的最小请求数。例如,默认该值为 20 的时候, // 如果滚动时间窗(默认10秒)内仅收到了19个请求, 即使这19个请求都失败了,断路器也不会打开。 @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "20"), // 该属性用来设置在滚动时间窗中,表示在滚动时间窗中,在请求数量超过 // circuitBreaker.requestVolumeThreshold 的情况下,如果错误请求数的百分比超过50, // 就把断路器设置为 "打开" 状态,否则就设置为 "关闭" 状态。 @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"), // 该属性用来设置当断路器打开之后的休眠时间窗。 休眠时间窗结束之后, // 会将断路器置为 "半开" 状态,尝试熔断的请求命令,如果依然失败就将断路器继续设置为 "打开" 状态, // 如果成功就设置为 "关闭" 状态。 @HystrixProperty(name = "circuitBreaker.sleepWindowinMilliseconds", value = "5000"), // 断路器强制打开 @HystrixProperty(name = "circuitBreaker.forceOpen", value = "false"), // 断路器强制关闭 @HystrixProperty(name = "circuitBreaker.forceClosed", value = "false"), // 滚动时间窗设置,该时间用于断路器判断健康度时需要收集信息的持续时间 @HystrixProperty(name = "metrics.rollingStats.timeinMilliseconds", value = "10000"), // 该属性用来设置滚动时间窗统计指标信息时划分"桶"的数量,断路器在收集指标信息的时候会根据 // 设置的时间窗长度拆分成多个 "桶" 来累计各度量值,每个"桶"记录了一段时间内的采集指标。 // 比如 10 秒内拆分成 10 个"桶"收集这样,所以 timeinMilliseconds 必须能被 numBuckets 整除。否则会抛异常 // @HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "10"), // 该属性用来设置对命令执行的延迟是否使用百分位数来跟踪和计算。如果设置为 false, 那么所有的概要统计都将返回 -1。 @HystrixProperty(name = "metrics.rollingPercentile.enabled", value = "false"), // 该属性用来设置百分位统计的滚动窗口的持续时间,单位为毫秒。 @HystrixProperty(name = "metrics.rollingPercentile.timeInMilliseconds", value = "60000"), // 该属性用来设置百分位统计滚动窗口中使用 “ 桶 ”的数量。 @HystrixProperty(name = "metrics.rollingPercentile.numBuckets", value = "60000"), // 该属性用来设置在执行过程中每个 “桶” 中保留的最大执行次数。如果在滚动时间窗内发生超过该设定值的执行次数, // 就从最初的位置开始重写。例如,将该值设置为100, 滚动窗口为10秒,若在10秒内一个 “桶 ”中发生了500次执行, // 那么该 “桶” 中只保留 最后的100次执行的统计。另外,增加该值的大小将会增加内存量的消耗,并增加排序百分位数所需的计算时间。 @HystrixProperty(name = "metrics.rollingPercentile.bucketSize", value = "100"), // 该属性用来设置采集影响断路器状态的健康快照(请求的成功、 错误百分比)的间隔等待时间。 @HystrixProperty(name = "metrics.healthSnapshot.intervalinMilliseconds", value = "500"), // 是否开启请求缓存 @HystrixProperty(name = "requestCache.enabled", value = "true"), // HystrixCommand的执行和事件是否打印日志到 HystrixRequestLog 中 @HystrixProperty(name = "requestLog.enabled", value = "true"), }, threadPoolProperties = { // 该参数用来设置执行命令线程池的核心线程数,该值也就是命令执行的最大并发量 @HystrixProperty(name = "coreSize", value = "10"), // 该参数用来设置线程池的最大队列大小。当设置为 -1 时,线程池将使用 SynchronousQueue 实现的队列, // 否则将使用 LinkedBlockingQueue 实现的队列。 @HystrixProperty(name = "maxQueueSize", value = "-1"), // 该参数用来为队列设置拒绝阈值。 通过该参数, 即使队列没有达到最大值也能拒绝请求。 // 该参数主要是对 LinkedBlockingQueue 队列的补充,因为 LinkedBlockingQueue // 队列不能动态修改它的对象大小,而通过该属性就可以调整拒绝请求的队列大小了。 @HystrixProperty(name = "queueSizeRejectionThreshold", value = "5"), } ) ``` ##### (4)、官网流程图 ![img](./images/hystrix-flow.png) #### 4)、Hystrix Dashboard ##### (1)、创建Dashboard工程 ###### (a)、pom.xml ```xml org.springframework.cloud spring-cloud-starter-netflix-hystrix-dashboard ``` ###### (b)、application.yml ```yaml server: port: 8301 spring: application: name: hystrix-dashboard main: allow-bean-definition-overriding: true hystrix: dashboard: proxy-stream-allow-list: "localhost" ``` ###### (c)、Application.java 主启动类上添加`@EnableHystrixDashboard`注解,开启监控仪表盘 ```java @SpringBootApplication(exclude= {DataSourceAutoConfiguration.class}) @EnableHystrixDashboard public class HystrixDashboardApplication { public static void main(String[] args) { SpringApplication.run(HystrixDashboardApplication.class,args); } } ``` ###### (d)、启动页面效果 ![1637754572531](./images/1637754572531.png) ##### (2)、修改Hystrix服务生产者 ###### (a)、application.yml 添加如下配置,否则hystrix.stream没有响应 ```ymal management: endpoints: web: exposure: include: '*' endpoint: health: show-details: always ``` ###### (b)、访问 ![1637756153082](./images/1637756153082.png) ##### (3)、监听 ###### (a)、输入hystrix.stream 注意:老版的是ip:port/hystrix.stream,新版是ip:port/actuator/hystrix.stream ![1637758381110](./images/1637758381110.png) 注意:一开始是loading,必须要访问监听的服务才有数据 ![1637758202035](./images/1637758202035.png) ![1637758707960](./images/1637758707960.png) ![1637758594205](./images/1637758594205.png) # 七、服务网关 ## 7.1、gateway ### 1、概念 ​ SpringCloud Gateway是SpringCloud的一个全新项目,基于Spring 5.0 + SpringBoot 2.0 + Project Reactor等技术开发的网关,为微服务架构提供一种简单有效的API路由管理方式。 ​ SpringCloud Gateway作为Spring Cloud 生态系统中的网关,目标是替代Zuul,在SpringCloud2.0以上版本中,没有对新版的Zuul2.0以上最新高性能版本进行集成,仍然使用Zuul1.x的非Reactor模式的老版本。而为了提升网关的性能,SpringCloud Gateway是基于WebFlux框架实现的,而WebFlux框架底层则使用了高性能的Reactor模式通信框架Netty。 **三大核心概念:** - **Route(路由):**路由是构建网关的基本模块,由目标ID和目标URI,一系列断言和过滤器组成,如果断言为true则匹配该路由。 - **Predicate(断言):**参考点是Java8的java.util.function.Predicate,开发人员可以匹配HTTP请求中的所有内容,如果请求与断言相匹配则进行路由匹配。 - **Filter(过滤):**指的是Spring框架中的GatewayFilter的实例,使用过滤器,可以在请求被路由前或者之后进行修改。 ### 2、使用 #### 1)、简单使用 ##### (1)、yaml配置方式 ###### (a)、pom.xml 引入`spring-cloud-starter-gateway`包,**注意**:gateway不需要引入`spring-boot-starter-web`包否则会报错 ```xml org.springframework.cloud spring-cloud-starter-gateway ``` ###### (b)、application.yml ```yaml server: port: 8401 spring: application: name: cloud-gateway # 服务名 cloud: gateway: routes: - id: payment-routh uri: http://localhost:8101 predicates: - Path=/payment/** eureka: client: # 将自己注册进服务注册中心 register-with-eureka: true # 是否从注册中心抓取已有的注册信息,默认为true,集群必须设置为true,才能配合ribbon使用负载均衡 fetch-registry: true service-url: # 单击方式 defaultZone: http://localhost:8000/eureka instance: instance-id: gateway # 服务实例id prefer-ip-address: true # 是否以IP地址注册到服务中心 ``` ###### (c)、效果 ![1638019900297](./images/1638019900297.png) ##### (2)、代码配置方式 ###### (a)、Config.java ```java @Bean public RouteLocator routes(RouteLocatorBuilder builder) { return builder.routes().route("baidu—guonei_route", r -> r.path("/guonei").uri("http://news.baidu.com/guonei") ).build(); } ``` ###### (b)、效果 ![1638020099904](./images/1638020099904.png) ##### (3)、整合Eureka ###### (a)、pom.xml ```xml org.springframework.cloud spring-cloud-starter-netflix-eureka-client ``` ###### (b)、yaml 打开从注册中心动态创建路由的功能,`spring.cloud.gateway.discovery.locator.enabled=true`,新版默认开启 ```yaml spring: application: name: cloud-gateway # 服务名 cloud: gateway: routes: - id: payment-routh uri: lb://provider-payment predicates: - Path=/payment/** discovery: locator: enabled: true # 开启从注册中心动态创建路由的功能,利用微服务名进行路由 ``` ###### (c)、效果 ![1638020950030](./images/1638020950030.png) ![1638020976461](./images/1638020976461.png) #### 2)、断言 ##### (1)、有哪些断言? 查看gateway启动日志,发现断言存在这些,我们上面的简单实例只使用了Path; ![1638021651707](./images/1638021651707.png) 可以通过官网查看http://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-request-predicates-factories ![1638024187383](./images/1638024187383.png) ##### (2)、After:在指定时间之后才可路由 ###### (a)、yml 添加断言After,时间可以通过ZonedDateTime获得 ```yml spring: ... cloud: gateway: routes: - id: payment-routh uri: lb://provider-payment predicates: - After=2021-11-27T22:46:26.350+08:00[Asia/Shanghai] ... ``` ###### (b)、效果 在配置的时间之后才可访问 ![1638024345612](./images/1638024345612.png) ![1638024416425](./images/1638024416425.png) ##### (3)、Before:在指定时间之后路由 ```yaml spring: cloud: gateway: routes: - id: before_route uri: https://example.org predicates: - Before=2017-01-20T17:42:47.789-07:00[America/Denver] ``` ##### (4)、Between:在指定时间之间路由 ```yaml spring: cloud: gateway: routes: - id: between_route uri: https://example.org predicates: - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver] ``` ##### (5)、Cookie:判断Cookie是否满足条件 ###### (a)、yaml ```yaml spring: application: name: cloud-gateway # 服务名 cloud: gateway: routes: - id: payment-routh uri: lb://provider-payment predicates: - Path=/payment/** # - After=2021-11-27T22:46:26.350+08:00[Asia/Shanghai # 判断cookie中username是否为admin - Cookie=username, admin ``` ###### (b)、效果 ![1638066171937](./images/1638066171937.png) ##### (6)、Header:请求头判断 ```yaml spring: application: name: cloud-gateway # 服务名 cloud: gateway: routes: - id: payment-routh uri: lb://provider-payment predicates: - Path=/payment/** # - After=2021-11-27T22:46:26.350+08:00[Asia/Shanghai # cookie - Cookie=username, admin # cookie中username是否为admin # 请求头 - Header=token, \d+ # 需要有请求头token,并且满足正则表达式 ``` ![1638081132158](./images/1638081132158.png) ![1638081160850](./images/1638081160850.png) ##### (7)、Host:消息头中Host路由 ##### (8)、Method:请求方式路由 ##### (9)、Path:请求地址路由 ##### (10)、Query:根据query参数路由 ##### (11)、RemoteAddr:根据请求IP路由 ##### (12)、Weight:根据权重路由 #### 3)、过滤器 ![1638082980793](./images/1638082980793.png) ##### (1)、自定义全局过滤器 ###### (a)、实现GlobalFilter接口 ```java @Component @Slf4j public class MyGlobalFilter implements GlobalFilter, Ordered { @Override public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) { String aaa = exchange.getRequest().getQueryParams().getFirst("aaa"); log.info("aaa-》{}" + aaa); if ("bbb".equals(aaa)) { return chain.filter(exchange); } exchange.getResponse().setStatusCode(HttpStatus.NOT_ACCEPTABLE); return exchange.getResponse().setComplete(); } @Override public int getOrder() { return 0; } } ``` ###### (b)、效果 ![1638083987121](./images/1638083987121.png) ![1638084049173](./images/1638084049173.png) ##### (2)、自定义GatewayFilter过滤器 ###### (a)、继承实现`AbstractGatewayFilterFactory` 并重写apply方法,`shortcutFieldOrder`方法,以及构造方法 注意:这里`shortcutFieldOrder`需要重写,否则不能进行简单方式赋值 ```java @Component @Slf4j public class MyParamGatewayFilterFactory extends AbstractGatewayFilterFactory { public MyParamGatewayFilterFactory(){ /** * 如果不传自定义Config会报错 */ super(AbstractGatewayFilterFactory.NameConfig.class); } @Override public GatewayFilter apply(AbstractGatewayFilterFactory.NameConfig config) { /** * 拦截所有请求,如果参数中包含name,把参数打印到控制台 */ return (exchange, chain) -> { //获取请求对象 ServerHttpRequest request = exchange.getRequest(); //获取所有参数 String name = request.getQueryParams().getFirst("name"); //如果name有值 = null "" " " " " if (StringUtils.hasLength(name) && config.getName().equals(name)) { return chain.filter(exchange);//放行请求 } exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND); return exchange.getResponse().setComplete(); }; } /** * 快捷字段顺序,会自动填充值 * 只有重写了这个方法,才能把写 * - MyParam=aaa, bbb格式 * 否则,配置格式如下 * - name: MyParam * args: * name: aaa * value: bbb * */ @Override public List shortcutFieldOrder() { return Arrays.asList("name"); } } ``` ###### (b)、效果 ![1638091792062](./images/1638091792062.png) ![1638092319816](./images/1638092319816.png) # 八、配置中心 ​ 微服务意味着要将单体应用中的业务拆分成一个个子服务,每个服务的粒度相对较小,因此系统中会出现大量的服务。由于每个服务都需必要的配置才能启动运行,所以一套集中式的、动态的配置管理设施是必不可少的。 ## 8.1、Config ### 1、概述 ​ SpringCloud Config为微服务架构中的微服务提供集中化的外部配置支持,配置服务器为各个不同的微服务应用的所有环境提供了一个中心化的外部配置。 SpringCloud Config分为服务端和客户端两个部分。 服务端:分布式配置中心,它是一个独立的微服务应用,用来连接配置服务并为客户端提供获得配置信息,加密/解密信息等访问接口 客户端:通过指定的配置中心来管理应用资源,以及与业务相关的配置内容,并在启动的时候从配置中心获取和加载配置信息,配置服务器默认采用git来存储配置信息,这样就有助于对环境进行版本管理,并且可以通过git客户端工具来方便的管理和访问配置内容。 ### 2、使用 #### 1)、简单使用 ##### (1)、远程配置仓库 创建远程git仓库,并保存自己的配置文件,在配置文件中写一些测试配置,仓库我这里是创建在gitee上的:https://gitee.com/liyukx/cloud-config-learn.git ![1638189456690](./images/1638189456690.png) ![1638189490849](./images/1638189490849.png) ##### (2)、总控中心 ###### (a)、pom.xml 需要引入`spring-cloud-config-server`和`spring-cloud-starter-config`包 ```xml org.springframework.cloud spring-cloud-starter-config org.springframework.cloud spring-cloud-config-server org.springframework.boot spring-boot-starter-test test org.springframework.cloud spring-cloud-starter-netflix-eureka-client org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator ``` ###### (b)、yaml ```yaml spring: application: name: cloud-config-server cloud: config: server: git: uri: https://gitee.com/liyukx/cloud-config-learn.git # 如果是私有的需要配置仓库账号密码 username: *** password: *** default-label: master ``` ###### (c)、Application.java 启动类上需要`@EnableConfigServer`注解,开启控制服务中心 ```java @EnableConfigServer @SpringBootApplication public class ConfigApplication { public static void main(String[] args) { SpringApplication.run(ConfigApplication.class, args); } } ``` ###### (d)、效果 官方有5种访问方式,任选一直查看自己的配置。 ![1638190410028](./images/1638190410028.png) ![1638190258818](E:\Files\资料笔记\spring\springcloud\images\1638190258818.png) ##### (3)、客户端 ###### (a)、pom.xml 需要导入`spring-cloud-starter-config` 注意:2020+版本之后需要额外导入`spring-cloud-starter-bootstrap`包 ```xml org.springframework.cloud spring-cloud-starter-config org.springframework.cloud spring-cloud-starter-bootstrap ``` ###### (b)、bootstrap.yml bootstrap.yml属于系统级的配置,权重高于application.yml ```yaml spring: application: name: config-client cloud: config: label: master # 分支名称 name: learn-config # 配置文件名 profile: dev # 后缀名 uri: http://localhost:8080 # 配置中心地址 ``` ###### (c)、application.yml ```yml server: port: 8001 config: name: local profile: default eureka: client: # 将自己注册进服务注册中心 register-with-eureka: true # 是否从注册中心抓取已有的注册信息,默认为true,集群必须设置为true,才能配合ribbon使用负载均衡 fetch-registry: true service-url: # 单击方式 defaultZone: http://localhost:8000/eureka instance: instance-id: payment8101 # 服务实例id prefer-ip-address: true # 是否以IP地址注册到服务中心 ``` ###### (d)、自定义properties类 ```java @ConfigurationProperties(prefix = "config") @Component @Data public class LearnProperties { private String name; private String profile; ``` ###### (e)、Controller返回配置信息 ```java @RestController @RequestMapping("learn") public class LearnController { @Resource private LearnProperties learnProperties; @GetMapping public LearnProperties get() { return learnProperties; } } ``` ###### (f)、效果 这里不是我们在application.yml中配置的local和default,而是远程仓库的配置 ![1638195053157](/./images/1638195053157.png) #### 2)、手动刷新 在1)中的配置输入加载的远程仓库的配置,但是不会自动刷新配置,如果git发生修改,是不会更新的 ##### (1)、客户端 以下在(2)的基础上修改 ###### (a)、pom.xml 引入`spring-boot-starter-actuator`包 ```xml org.springframework.boot spring-boot-starter-actuator ``` ###### (b)、yaml 配置actuator,监控断点 ```yaml # 暴露监控端点 management: endpoints: web: exposure: include: "*" ``` ###### (c)、Controller 添加`@RefreshScope`注解 ###### (d)、效果 我们先重启项目 然后修改远程仓库配置信息 查看配置,发现没有更新 需要Post请求访问http://localhost:8001/actuator/refresh刷新配置(注意:比较慢) 再次查看,配置已成功更新 ![1638196020551](./images/1638196020551.png) ![1638196063690](./images/1638196063690.png) #### 3)、存在问题 每次都需要对单个服务发送post请求手动刷新;即希望达到广播的效果。 # 九、消息总线 ## 9.1、Bus ### 1、概述 SpringCloud Bus 配合Spring Cloud Config 使用可以实现配置的动态刷新。 一种支持SpringCloud的消息总线,但是目前只支持RabbitMq和kafka。 是用来将分布式系统的节点与轻量级消息系统连接起来的框架,它整合了java的事件处理机制和消息中间件的功能。 **什么是总线?** 在微服务架构的系统中,通常会使用轻量级的消息代理来构建一个公用的消息主体,并让系统中所有微服务实例都连接上来。由于该主题中产生的消息会被所有实例监听和消费,所以称为消息总线。在总线上的各个实例,都可以方便的广播一些需要让其他连接在该主体上的实例都知道的消息。 **基本原理** ConfigClient实例都监听MQ中同一个topic(默认是SpringCloudBus)。当一个服务刷新数据的时候,它把这个信息放入到Topic中,这样其他监听同一个Topic的服务器就能得到通知,然后更新自身的配置。 ### 2、使用 #### 1)、全局广播 ##### (1)、rabbitmq ##### (2)、config服务端 ###### (a)、pom.xml ```xml org.springframework.cloud spring-cloud-starter-bus-amqp ``` ###### (b)、application.yml ```yml spring: application: name: cloud-config-server cloud: config: server: git: uri: https://gitee.com/liyukx/cloud-config-learn.git username: *** password: *** default-label: master rabbitmq: host: 123.56.163.139 port: 5672 username: liyu password: liyu1228 eureka: ... # 暴露bus刷新配置的端点 management: endpoints: web: exposure: include: busrefresh # 注意:老版是bus-refresh,新版是busrefresh ``` ##### (3)、config客户端 ###### (a)、pom.xml 同服务端一样 ###### (b)、bootstarp.yml ```yaml spring: application: name: config-client cloud: config: label: master # 分支名称 name: learn-config # 配置文件名 profile: dev # 后缀名 uri: http://localhost:8080 # 配置中心地址 rabbitmq: host: 123.56.163.139 port: 5672 username: *** password: *** # 暴露监控端点 management: endpoints: web: exposure: include: "*" ``` ###### (c)、同上总共创建3个客户端 ##### (4)、全局刷新效果 ###### (a)、查看原本的配置 ![1638363418102](./images/1638363418102.png) ###### (b)、更新git仓库中的配置 ![1638363501256](./images/1638363501256.png) ###### (c)、访问配置中心的刷新接口 这里需要注意:老版是bus-refresh,新版是busfresh ![1638363698248](E:\Files\资料笔记\spring\springcloud\images\1638363698248.png) ###### (d)、访问客户端接口查看配置 更新成功。 ![1638363788124](./images/1638363788124.png) ##### (5)、定点刷新效果 ###### (a)、修改git仓库中的配置 ![1638364413756](./images/1638364413756.png) ###### (b)、定点刷新 **刷新格式如下:** ```java {配置中心IP:端口}/actuator/busrefresh/{app:index:id} ``` **app:**`vcap.application.name`或`spring.application.name` **index:** `vcap.application.instance_index`或`spring.application.index`、`local.server.port`、`server.port` **id:**`vcap.application.instance_id` ![1638364724127](./images/1638364724127.png) ###### (c)、效果 ![1638365053438](./images/1638365053438.png) ![1638365119505](./images/1638365119505.png) # 十、消息驱动 ## 10.1、Spring Cloud Stream ### 1、概述 ​ Spring Cloud Stream 是一个用于构建与共享消息系统连接的高度可扩展的事件驱动微服务的框架 ​ 屏蔽底层消息中间件的差异,降低切换成本,统一消息的编程模型。 ###### 核心组件: **Destination Binders**:负责提供与外部消息传递系统集成的组件。 **Destination Bindings**:外部消息系统和最终用户提供的应用程序代码(生产者/消费者)之间的桥梁。 **Message**:生产者和消费者用来与目标绑定器(以及通过外部消息传递系统的其他应用程序)进行通信的规范数据结构。 目前只支持Rabbit、kafka ### 2、使用 我这里使用的是rabbit #### 1)、老版使用方法 ##### (1)、生产者 ###### (a)、pom.xml ```xml org.springframework.cloud spring-cloud-starter-stream-rabbit ``` ###### (b)、application.yml ```yaml server: port: 8101 spring: application: name: stream-provider cloud: stream: binders: # 配置绑定信息 defaultRabbit: # 表示定义的名称,用于binding整合 type: rabbit environment: # rabbit相关环境配置 spring: rabbitmq: host: 123.56.163.139 port: 5672 username: liyu password: liyu1228 bindings: # 服务的整合处理 output: # 这个名字是一个通道的名称 destination: learn.stream.exchange # 表示要使用的交换机名称定义 content-type: application/json # 设置消息类型为json default-binder: - ${spring.cloud.stream.binders.defaultRabbit} rabbitmq: host: 123.56.163.139 port: 5672 username: liyu password: liyu1228 ``` ###### (c)、实现消息发送方法 使用`@EnableBinding({Source.class})`开启流,从bean工厂获得消息通道,注意名称要和`spring.cloud.stream.bindings`配置的一样 ```java @EnableBinding({Source.class}) @Component("messageProvider") public class MessageProviderImpl implements IMessageProvider { @Resource private MessageChannel output; // 消息发送管道(output和我们配置的一样) @Override public String send() { String uuid = UUID.randomUUID().toString(); output.send(MessageBuilder.withPayload(uuid).build()); return uuid; } } ``` ###### (d)、实现controller ```java @RestController @RequestMapping("send") public class SendMessageController { @Resource private IMessageProvider messageProvider; @GetMapping public String send(){ return messageProvider.send(); } } ``` ###### (e)、效果 出现配置的交换机,但是此时没有消息队列 ![1638533850026](./images/1638533850026.png) ##### (2)、消费者 ###### (a)、pom.xml ```xml org.springframework.cloud spring-cloud-starter-stream-rabbit ``` ###### (b)、application.yml ```yaml server: port: 8201 spring: application: name: stream-consumer cloud: stream: binders: # 配置绑定信息 stream-rabbit: # 表示定义的名称,用于binding整合 type: rabbit # environment: # rabbit相关环境配置 # spring: # rabbitmq: # host: 123.56.163.139 # port: 5672 # username: liyu # password: liyu1228 bindings: # 服务的整合处理 input: # 这个名字是一个通道的名称 group: learn.stream.group1 destination: learn.stream.exchange # 表示要使用的交换机名称定义 content-type: application/json # 设置消息类型为json default: binder: stream-rabbit rabbitmq: host: 123.56.163.139 port: 5672 username: liyu password: liyu1228 ``` ###### (c)、listener ```java @EnableBinding(Sink.class) public class MessageListener { /** * 方法名要和配置的名称一致 * @param message */ @StreamListener(Sink.INPUT) public void input(Message message) { System.out.println("这里是消费者8201-input,接收到信息:" + message.getPayload()); } } ``` ##### (3)、效果 生产者发送消息 ![1638593288158](./images/1638593288158.png) 消费端成功消费消息 ![1638593341668](./images/1638593341668.png) #### 2)、新版 ##### (1)、生产者 ###### (a)、pom.xml ```xml org.springframework.cloud spring-cloud-starter-stream-rabbit ``` ###### (b)、application.yml ```yaml server: port: 8101 spring: application: name: stream-provider rabbitmq: host: 123.56.163.139 port: 5672 username: liyu password: liyu1228 ``` ###### (c)、生产消息 注入StreamBridge,并发送消息 ```java @Component("newMessageProvider") public class NewMessageProviderImpl implements IMessageProvider { @Resource private StreamBridge streamBridge; @Override public String send() { String message = UUID.randomUUID().toString(); //这里说明一下这个 streamBridge.send 方法的参数 第一个参数是exchange或者topic 就是主题名称 //默认的主题名称是通过 //输入: <方法名> + -in- + //输出: <方法名> + -out- + //这里我们接收的时候就要用inputNew方法 参数是consumer接收 //consumer的参数类型是这里message的类型 streamBridge.send("inputNew-in-0", message); return message; } } ``` ##### (2)、消费者 ###### (a)、pom.xml ```xml org.springframework.cloud spring-cloud-starter-stream-rabbit ``` ###### (b)、application.yml ```yml server: port: 8201 spring: application: name: stream-consumer rabbitmq: host: 123.56.163.139 port: 5672 username: liyu password: liyu1228 ``` ###### (c)、listener ```java @Component public class NewMessageListener { //这里接收rabbitmq的条件是参数为Consumer 并且 方法名和生产者方法名相同 //这里的返回值是一个匿名函数 返回类型是consumer 类型和提供者的类型一致 //supplier发送的exchange是 send-in-0 这里只需要用send方法名即可 @Bean Consumer inputNew() { return str -> { System.out.println("我是消费者8201-inputNew,我收到了消息:"+str); }; } } ``` ##### (3)、效果 ![1638594827945](./images/1638594827945.png) ![1638594858492](./images/1638594858492.png) # 十一、链路跟踪 ## 11.1、Sleuth ### 1、概述 微服务架构上通过业务来划分服务的,通过REST调用,对外暴露的一个接口,可能需要很多个服务协同才能完成这个接口功能,如果链路上任何一个服务出现问题或者网络超时,都会形成导致接口调用失败。随着业务的不断扩张,服务之间互相调用会越来越复杂。 Spring Cloud Sleuth 主要功能就是在分布式系统中提供追踪解决方案,并且兼容支持了 zipkin,你只需要在pom文件中引入相应的依赖即可。 ### 2、使用 ##### (1)、下载并启动zinkin服务端 https://zipkin.io/pages/quickstart.html ![1638605207624](./images/1638605207624.png) ![1638605247520](./images/1638605247520.png) ##### (2)、生产者 ###### (a)、pom.xml ```xml org.springframework.cloud spring-cloud-starter-zipkin ``` ###### (b)、appliation.yml ```xml spring: application: name: provider-payment # 服务名 zipkin: base-url: http://localhost:9411 sleuth: sampler: probability: 1 # 采样率值介于0-1之间,1表示全部采集 ``` ##### (3)、消费者 同上,记得调用生产者 ##### (4)、效果 访问消费端口 ![1638605419426](./images/1638605419426.png) 在zipkin服务端查看服务链路调用详情 ![1638605512707](./images/1638605512707.png) # # **SpringCloud Alibaba** # 一、Nacos ## 1.1、简介 Nacos: *Dynamic Naming and Configuration Service* 一个更易于构建云原生应用的动态服务发现、配置管理和服务管理平台。 注册中心 + 配置中心 ###### 支持CP和AP两种模式,切换命令 ```shell curl -X PUT '$NACOS_SERVER:8848/nacos/v1/ns/operator/switches?entry=serverMode&value=CP' ``` ![img](./images/2021120501.png) ## 1.2、下载安装 ###### 1、下载 https://github.com/alibaba/nacos/releases ![1638621245188](E:\Files\资料笔记\spring\springcloud\images\1638621245188.png) ###### 2、安装 进入解压的bin目录下,进入cmd,输入启动命令 注意:不要直接点击starup.cmd,需要加上standalone(表示单击模式运行) ```shell startup.cmd -m standalone ``` 启动成功如下: ![1638622437929](./images/1638622437929.png) 访问控制台,默认账号密码是nacos/nacos ![1638622406778](./images/1638622406778.png) ![1638622473444](./images/1638622473444.png) ## 1.3、使用 ### 1、服务注册调用 ##### (1)、父项目 ###### (a)、父pom.xml 在父pom中引入`spring-cloud-alibaba-dependencie` ```xml 4.0.0 org.example 12-alibaba-nacos pom 1.0-SNAPSHOT 12-provider-payment 12-consumer-order UTF-8 12 12 4.12 1.2.17 1.18.22 2.3.2.RELEASE 2.2.6.RELEASE 2.2.10.RELEASE org.springframework.boot spring-boot-dependencies ${spring.boot.version} pom import com.alibaba.cloud spring-cloud-alibaba-dependencies ${spring.cloud.alibaba.version} pom import org.springframework.cloud spring-cloud-starter-openfeign ${spring.cloud.version} junit junit ${junit.version} log4j log4j ${log4j.version} org.projectlombok lombok ${lombok.version} true ``` ##### (2)、服务生产者 ###### (a)、子服务pom.xml 在生产者pom中引入`spring-cloud-starter-alibaba-nacos-discovery` ```xml 12-alibaba-nacos org.example 1.0-SNAPSHOT 4.0.0 12-provider-payment com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery org.springframework.boot spring-boot-starter-test org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.projectlombok lombok junit junit test ``` ###### (b)、application.yml ```yaml server: port: 9001 spring: application: name: nacos-provider-payment cloud: nacos: discovery: server-addr: http://localhost:8848 management: endpoints: web: exposure: include: "*" ``` ###### (c)、Application.java 主启动类上需要添加`@EnableDiscoveryClient`注解 ```java @SpringBootApplication @EnableDiscoveryClient public class PaymentApplication { public static void main(String[] args) { SpringApplication.run(PaymentApplication.class, args); } } ``` ###### (d)、服务controller ```java @RestController @RequestMapping("payment") public class PaymentController { @Resource private ServerProperties serverProperties; @GetMapping public String get() { return "生产者:"+serverProperties.getPort(); } } ``` ###### (e)、查看nacaos控制台 ![1638627085987](./images/1638627085987.png) ###### (f)、利用idea再起一个生产者服务 ![1638706752302](./images/1638706752302.png) ![1638706785445](./images/1638706785445.png) ##### (3)、消费者 ###### (a)、pom.xml 引入`spring-cloud-starter-alibaba-nacos-discovery`和`spring-cloud-starter-openfeign`的包 ``` com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery org.springframework.cloud spring-cloud-starter-openfeign org.springframework.boot spring-boot-starter-test org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-actuator org.projectlombok lombok junit junit test ``` ###### (b)、application.yml ```yaml server: port: 9101 spring: application: name: nacos-consumer-order cloud: nacos: discovery: server-addr: http://localhost:8848 management: endpoints: web: exposure: include: "*" ``` ###### (c)、Application.java 主启动类上加上`@EnableFeignClients`、`@EnableDiscoveryClient`注解 ```java @SpringBootApplication @EnableFeignClients @EnableDiscoveryClient public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } } ``` ###### (d)、服务调用service ```java @Service @FeignClient(value = "nacos-provider-payment") public interface IPaymentService { @GetMapping("payment") String get(); } ``` ###### (e)、消费者controller ```java @RestController @RequestMapping("order") public class OrderController { @Resource private IPaymentService paymentService; @RequestMapping public String get() { return "消费者消费-" + paymentService.get(); } } ``` ##### (4)、访问效果 ![1638707199695](./images/1638707199695.png) ### 2、配置中心 ##### (1)、客户端 ###### (a)、pom.xml 需要额外引入`spring-cloud-starter-alibaba-nacos-config` ```xml com.alibaba.cloud spring-cloud-starter-alibaba-nacos-config com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery ``` ###### (b)、bootstarp.yml 配置`spring.nacos.config`相关配置 ```yaml spring: application: name: nacos-config-client cloud: nacos: discovery: server-addr: 127.0.0.1:8848 config: server-addr: 127.0.0.1:8848 file-extension: yaml ``` ###### (c)、application.yml ```yaml server: port: 9301 spring: profiles: active: dev ``` ###### (d)、Properties类及Controller ```java @ConfigurationProperties(prefix = "config") @Component @Data public class LearnProperties { private String name; private String profile; } ``` ```java @RestController @RequestMapping("learn") @RefreshScope public class LearnController { @Resource private LearnProperties learnProperties; @GetMapping public LearnProperties get() { return learnProperties; } } ``` ##### (2)、在nacos控制台新建配置 ###### (a)、配置dataId dataId取值规则 ``` ${prefix}-${spring.profiles.active}.${file-extension} ``` - `prefix` 默认为 `spring.application.name` 的值,也可以通过配置项 `spring.cloud.nacos.config.prefix`来配置。 - `spring.profiles.active` 即为当前环境对应的 profile - `file-exetension` 为配置内容的数据格式,可以通过配置项 `spring.cloud.nacos.config.file-extension` 来配置。目前只支持 `properties` 和 `yaml` 类型。 ###### (b)、示例 ![1638710553960](E:\Files\资料笔记\spring\springcloud\images\1638710553960.png) ###### (c)、配置发布会自动刷新client的配置 ![1638710587126](E:\Files\资料笔记\spring\springcloud\images\1638710587126.png) ##### (3)、分组方式 ###### (a)、Data Id 数据ID ###### (b)、Group 分组 ###### (c)、Namespace 命名空间 ### 3、集群 ##### (1)、修改application.properties文件 ```properties spring.datasource.platform=mysql db.num=1 db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC db.user=root db.password=root ``` ##### (2)、修改cluster.conf文件 这里一定要注意端口问题(有可能报错信息和端口无关) ``` # 注意,2.0版本后需要4个端口号,使用时记得规避 # 默认是8848、7848、9848、9849 # raft port: ${server.port} - 1000 # grpc port: ${server.port} + 1000 # grpc port for server: ${server.port} + 1001 172.17.77.192:8838 172.17.77.192:8848 172.17.77.192:8858 ``` ##### (3)、修改startup.sh脚本 这里注意内存大小:我的因为阿里云服务是2c4g不够,所以参数配置修改的比较低 ![1639044757162](./images/1639044757162.png) ##### (4)、启动服务 复制总共三份nacos文件夹 nacos、nacos2、nacos3 启动服务 ```shell ./nacos/bin/startup.sh -t 8838 ./nacos2/bin/startup.sh -t 8848 ./nacos3/bin/startup.sh -t 8858 ``` ##### (5)、配置nginx转发 ```nginx https { ... upstream nacos { server 127.0.0.1:8838; server 127.0.0.1:8848; server 127.0.0.1:8858; } ... server { ... location /nacos { proxy_pass http://nacos; index index.html; } ... } } ``` ```shell nginx -s reload ``` ##### (6)、页面效果 ![1639049293508](./images/1639049293508.png) # 二、Sentinel ## 2.1、概念 ##### 是什么? 随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。 ##### Sentinel 具有以下特征 - **丰富的应用场景**:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。 - **完备的实时监控**:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。 - **广泛的开源生态**:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Apache Dubbo、gRPC、Quarkus 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。同时 Sentinel 提供 Java/Go/C++ 等多语言的原生实现。 - **完善的 SPI 扩展机制**:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。 ##### Sentinel 的主要特性: ![Sentinel-features-overview](./images/50505538-2c484880-0aaf-11e9-9ffc-cbaaef20be2b.png) ##### Sentinel 的开源生态: ![Sentinel-opensource-eco](./images/84338449-a9497e00-abce-11ea-8c6a-473fe477b9a1.png) ##### Sentinel 分为两个部分: - 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。 - 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。 ## 2.2、使用 ### 1、启动sentinel dashboard ###### (a)、下载 ![1639051794628](./images/1639051794628.png) ###### (b)、使用java -jar 或者idea启动jar ![1639051901097](./images/1639051901097.png) ###### (c)、访问 默认账户密码:sentinel/sentinel,默认端口是8080 ![1639051979602](./images/1639051979602.png) ### 2、初始化监控 ###### (a)、pom.xml ```xml com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery com.alibaba.cloud spring-cloud-starter-alibaba-sentinel ``` ###### (b)、application.yml ```yaml server: port: 8101 spring: application: name: sentinel-service cloud: nacos: discovery: server-addr: localhost:8848 sentinel: transport: # sentinel dashboard 地址 dashboard: localhost:8000 # 默认8719端口,如果被占用会自动+1依次扫描,直至找到未被占用的端口 # 这里的 spring.cloud.sentinel.transport.port 端口配置会在应用对应的机器上启动一个 Http Server, # 该 Server 会与 Sentinel 控制台做交互。比如 Sentinel 控制台添加了一个限流规则,会把规则数据 push 给 # 这个 Http Server 接收,Http Server 再将规则注册到 Sentinel 中 port: 8719 management: endpoints: web: exposure: include: "*" ``` ###### (c)、Application.java ```java @SpringBootApplication @EnableDiscoveryClient public class ServiceApplication { public static void main(String[] args) { SpringApplication.run(ServiceApplication.class, args); } } ``` ###### (d)、controller ```java @RestController @RequestMapping("service") public class ServiceController { @Resource private ServerProperties serverProperties; @GetMapping public String get() { return "服务service:"+serverProperties.getPort(); } } ``` ###### (e)、查看实施监控 注意要先访问服务接口,否则由于sentinel是懒加载,是看不到服务的 ![1639205792428](./images/1639205792428.png) ### 3、流量控制 #### 1)、模式-直接(默认) ##### (1)、QPS每秒查询率(Query Per Second) **** ###### (a)、添加QPS流控配置 ![1639207180970](./images/1639207180970.png) ###### (b)、效果 当每秒访问量达到设定的阈值时,会提示错误 ![1639207326656](./images/1639207326656.png) ##### (2)、线程数控制 ###### (a)、修改配置,让每秒只有一个线程 ![1639207818368](./images/1639207818368.png) ###### (b)、修改contoller 延迟响应时间,保证一秒钟一个服务会占用一个线程。 ###### (c)、效果 当一秒内访问第二次时,会报错 ![1639208277541](./images/1639208277541.png) #### 2)、模式-关联 当关联资源访问达到阈值时,限制当前资源的服务 ##### (1)、QPS ###### (a)、修改controller 添加`/service/b`资源 ```java @RestController @RequestMapping("service") public class ServiceController { @Resource private ServerProperties serverProperties; @GetMapping public String get() { return "服务service:"+serverProperties.getPort()+";方法get"; } @GetMapping("b") public String getB() { return "服务service:"+serverProperties.getPort()+";方法getB"; } } ``` ###### (b)、修改配置 ![1639209592475](./images/1639209592475.png) ###### (c)、通过postman模拟并发访问`/service/b`资源 ![1639210163472](./images/1639210163472.png) ###### (d)、效果 ![1639210103361](./images/1639210103361.png) #### 3)、模式-链路 ##### (1)、QPS ###### (a)、创建新工程,调用上面的服务 注意:需要引入openfeign ###### (b)、application.yml feign对sentinel的支持 ```yaml server: port: 8201 spring: application: name: sentinel-consumer cloud: nacos: discovery: server-addr: http://localhost:8848 sentinel: transport: dashboard: localhost:8000 port: 8719 feign: sentinel: enabled: true # 添加feign对sentinel的支持 management: endpoints: web: exposure: include: "*" ``` ###### (c)、服务调用 service ```java @FeignClient("sentinel-service") public interface IProviderService { @GetMapping("service") String get() ; @GetMapping("service/b") String getB() ; } ``` controller ```java @RestController @RequestMapping("consumer") public class ConsumerController { @Resource private IProviderService providerService; @GetMapping public String get() { return providerService.get(); } } ``` ###### (d)、sentinel配置流控 开启feign对sentinel的支持后,会自动生成资源名,密码规则:{请求方式}:http://{服务名}/{api地址} ![1639274524336](./images/1639274524336.png) 给资源配置链路流控 ![1639274480326](./images/1639274480326.png) (e)、效果 直接访问服务提供接口,无任何问题 ![1639274764994](./images/1639274764994.png) 通过生产者调用时,每秒QPS超过阈值会报错 ![1639274830541](./images/1639274830541.png) #### 3)、效果-预热( Warm Up) ###### (a)、配置 ![1639233131100](./images/1639233131100.png) ###### (b)、效果 一直快速服务接口,查看效果,开始限制最高2,后面逐步升高阈值 ![1639233874766](./images/1639233874766.png) #### 4)、效果-排队等待 ###### (a)、配置 ![1639235772028](./images/1639235772028.png) ###### (b)、快速访问接口,查看效果 ![1639235704587](./images/1639235704587.png) ### 4、熔断降级 #### 1)、慢调用比例(`SLOW_REQUEST_RATIO`) 选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(`statIntervalMs`)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。 ###### (a)、Controller.java 访问添加等待,保证接口响应时间较慢 ```java @Slf4j public class ServiceController { @Resource private ServerProperties serverProperties; @GetMapping public String get() { log.info("get...."); return "服务service:"+serverProperties.getPort()+";方法get"; } @GetMapping("b") public String getB() throws InterruptedException { TimeUnit.SECONDS.sleep(1); log.info("getB...."); return "服务service:"+serverProperties.getPort()+";方法getB"; } } ``` ###### (b)、配置熔断规则 ![1639276967329](./images/1639276967329.png) ###### (c)、一秒访问5次后,再访问 熔断后,无法访问,经过熔断时间后,可以访问一次,由于响应时间还是大于阈值,故下次依旧熔断 ![1639277428648](./images/1639277428648.png) ![1639276299906](./images/1639276299906.png) #### 2)、异常比例(`ERROR_RATIO`) 当单位统计时长(`statIntervalMs`)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 `[0.0, 1.0]`,代表 0% - 100%。 ###### (a)、Controller.java 写一个根据id判断如果是奇数报错的方法 ```java @GetMapping("{id}") public String getById(@PathVariable("id") Integer id) { log.info("getById:"+id+"...."); if (id%2!=0){ throw new RuntimeException("id不是偶数"); } return "服务service:"+serverProperties.getPort()+";方法getById"; } ``` ###### (b)、配置异常比例熔断规则 ![1639313476635](./images/1639313476635.png) ###### (c)、效果 5秒内先进行1次正常访问,然后进行4次异常访问,再看进行异常或正常访问的效果 ![1639313677152](./images/1639313677152.png) #### 3)、异常数(`ERROR_COUNT`) 当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。 ###### (a)、修改配置 ![1639314238533](./images/1639314238533.png) ###### (b)、效果 5秒内,访问5次异常访问,再正常访问,查看访问效果 ![1639314735422](./images/1639314735422.png) ### 5、热点限流 #### 1)、概述 何为热点?热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如: - 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制 - 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制 热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。 #### 2)、简单实例 ###### (a)、@SentinelResouce注解 使用@SentinelResouce注解,给资源命名,并指定降级方法(注意:报错不会走降级方法,除非配置了相应的异常熔断规则); ```java @GetMapping("{id}") @SentinelResource(value = "getById", blockHandler = "getByIdHanler") public String getById(@PathVariable("id") Integer id) { log.info("getById:"+id+"...."); if (id%2!=0){ throw new RuntimeException("id不是偶数"); } return "服务service:"+serverProperties.getPort()+";方法getById"; } public String getByIdHanler(Integer id, BlockException blockException) { return "服务service:"+serverProperties.getPort()+";方法getById出现异常->getByIdHanler"; } ``` ###### (b)、添加热点限流配置 注意:新增时没有高级选项,高级选项在编辑时可选 ![1639318810724](./images/1639318810724.png) ###### (c)、效果 当一秒中访问超过一次后,后续访问,都走异常降级方法 ![1639318263891](./images/1639318263891.png) 当我们访问参数是4时,需要访问超过三次,才走降级方法 ![1639318916551](./images/1639318916551.png) ### 6、系统限流 #### 1)、概念 ##### 概述 Sentinel 系统自适应保护从整体维度对应用入口流量进行控制,结合应用的 Load、总体平均 RT、入口 QPS 和线程数等几个维度的监控指标,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。 即从服务系统层面进行限流。 ##### 系统规则 系统规则支持以下的阈值类型: - **Load**(仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护。系统容量由系统的 `maxQps * minRt` 计算得出。设定参考值一般是 `CPU cores * 2.5`。 - **CPU usage**(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0)。 - **RT**:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。 - **线程数**:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。 - **入口 QPS**:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。 #### 2)、入口QPS ###### (a)、新增系统规则 ![1639388091505](./images/1639388091505.png) ###### (b)、效果 多次访问该系统服务时,进入熔断 ![1639388172011](./images/1639388172011.png) ### 7、注解 #### 1)、`@SentinelResource` 注解 注意:注解方式埋点不支持 private 方法。 ##### (1)、简介 `@SentinelResource` 用于定义资源,并提供可选的异常处理和 fallback 配置项。 ` ##### (2)、属性 注:1.6.0 之前的版本 fallback 函数只针对降级异常(`DegradeException`)进行处理,**不能针对业务异常进行处理**。 特别地,若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 `BlockException` 时只会进入 `blockHandler` 处理逻辑。若未配置 `blockHandler`、`fallback` 和 `defaultFallback`,则被限流降级时会将 `BlockException` **直接抛出**。 ###### (a)、`value` 资源名称,必需项(不能为空) ###### (b)、`entryType` entry 类型,标记是出口流量,还是入口流量,可选项(默认为 `EntryType.OUT`) ###### (c)、`blockHandler` / `blockHandlerClass` `blockHandler` 对应处理 `BlockException` 的函数名称,可选项。blockHandler 函数访问范围需要是 `public`,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 `BlockException`。blockHandler 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 `blockHandlerClass` 为对应的类的 `Class` 对象。 注意对应的函数必需为 static 函数,否则无法解析。 ###### (d)、`fallback` fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了 `exceptionsToIgnore` - 返回值类型必须与原函数返回值类型一致; - 方法参数列表需要和原函数一致,或者可以额外多一个 `Throwable` 类型的参数用于接收对应的异常。 - fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 `fallbackClass` 为对应的类的 `Class` 对象,注意对应的函数必需为 static 函数,否则无法解析。 ###### (e)、`defaultFallback`(since 1.6.0) 默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所以类型的异常(除了 `exceptionsToIgnore` - 返回值类型必须与原函数返回值类型一致; - 方法参数列表需要为空,或者可以额外多一个 `Throwable` 类型的参数用于接收对应的异常。 - defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 `fallbackClass` 为对应的类的 `Class` 对象,注意对应的函数必需为 static 函数,否则无法解析。 ###### (f)、`exceptionsToIgnore`(since 1.6.0) 用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。 ##### 2)、异常fallback简单用例 ###### (a)、@SentinelResource指定fallabck ```java @GetMapping("{id}") @SentinelResource(value = "getById", blockHandler = "getByIdHanler", fallback = "getByIdFallback") public String getById(@PathVariable("id") Integer id) throws Exception { log.info("getById:"+id+"...."); if (id%2!=0){ throw new Exception("id不是偶数"); } return "服务service:"+serverProperties.getPort()+";方法getById"; } ``` ```java public String getByIdFallback(Integer id, Throwable e) { return "服务service:"+serverProperties.getPort()+";方法getById出现异常->getByIdFallback"; } ``` ###### (b)、注入SentinelResourceAspect ```java // 注解支持的配置Bean @Bean public SentinelResourceAspect sentinelResourceAspect() { return new SentinelResourceAspect (); } ``` ###### (c)、效果 进行异常参数服务,是否走异常处理方法 ![1639402330190](./images/1639402330190.png) ### 8、持久序列化 #### 1)、整合数据源 ##### (1)、可以整合哪些数据源 https://github.com/alibaba/spring-cloud-alibaba/wiki/Sentinel#%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8-sentinel ![1639487607306](./images/1639487607306.png) ##### (2)、整合nacos ###### (a)、pom.xml ```xml com.alibaba.csp sentinel-datasource-nacos ``` ###### (b)、application.yml 添加`spring.cloud.sentinel.datasource.ds1.nacos`配置 ```yaml spring: cloud: sentinel: datasource: ds1: nacos: server-addr: localhost:8848 data-id: sentinel-service groupId: DEFAULT_GROUP data-type: json rule-type: flow ``` ###### (c)、添加nacos配置 ![1639488488282](./images/1639488488282.png) ```json [ { "resource": "/service", // 资源名称 "limitApp": "default", // 来源应用 "grade": 1, // 阈值类型,0线程数,1QPS "count": 1, // 单机阈值 "strategy": 0, // 流控模式,0直接,1关联,2链路 "controlBehavior": 0, // 流控效果,0快速失败,1Warm Up,2排队等待 "clusterMode": false // 是否集群 } ] ``` ###### (d)、效果 服务启动后,无需重新配置 ![1639488594119](./images/1639488594119.png) ![1639488571250](./images/1639488571250.png) ##### (3)、问题 修改或新增流控配置后,重启服务,配置会重置;即我们修改的配置没有更新到nacos中。 #### 2)、Sentinel数据持久化 ##### (1)、问题解决方案 ![1639532415910](./images/1639532415910.png) 根据官网描述,目前的push模式的数据源(配置中心)都是只读的。对于配置中心类型的数据源(如 ZooKeeper),我们推荐在推送规则时直接推送至配置中心,然后配置中心再自动推送至所有的客户端(即 `Dashboard -> Config Center -> Sentinel DataSource -> Sentinel`),**目前需要自行改造控制台**。可以参见:[在生产环境中使用 Sentinel 控制台](https://github.com/alibaba/Sentinel/wiki/在生产环境中使用-Sentinel)。 ![1639533600841](./images/1639533600841.png) #### 3)、扩展改造控制台(Push模式) 实现Sentinel Dashboard与Nacos之间的相互通信 ##### (1)、从github拉取Sentinel源码 https://github.com/alibaba/Sentinel.git ##### (2)、流控规则 ###### (a)、pom.xml ![1639559271801](./images/1639559271801.png) ###### (b)、自定义拉取和推送规则 在测试类中已有定义号的拉取和推送规则,将它复制到指定位置;复制的java文件有4个:`NacosConfig`、`NacosConfigUtil`、`FlowRuleNacosPublisher`、`FlowRuleNacosProvider` ![1639559078873](./images/1639559078873.png) ###### (c)、NacosConfig修改 添加地址、命名空间配置; ![1639559717444](./images/1639559717444.png) ###### (d)、修改启动配置文件 注意:当命名空间是public,`nacos.namespace`等于空值 ![1639559796535](./images/1639559796535.png) ###### (e)、FlowControllerV1.java 官方推荐修改`FlowControllerV2`(这里不推荐,因为需要修改路由),我们这里直接修改`FlowControllerV1` **注入nacos拉取和推送的bean** ```java @Autowired @Qualifier("flowRuleNacosProvider") private DynamicRuleProvider> ruleProvider; @Autowired @Qualifier("flowRuleNacosPublisher") private DynamicRulePublisher> rulePublisher; ``` ![1639560989710](./images/1639560989710.png) **添加更新nacos配置的方法** ```java /** * 更新nacos配置 * @param app * @throws Exception */ private void publishRules(String app) throws Exception { List rules = repository.findAllByApp(app); rulePublisher.publish(app, rules); } ``` **修改`apiAddFlowRule`、`apiUpdateFlowRule`、`apiDeleteFlowRule`方法中,更新内存数据的方法为更新nacos数据源方法** ![1639561395189](./images/1639561395189.png) **修改`apiQueryMachineRules`方法中,从内存中查询数据的方式,为从nacos中获得配置** ![1639561496871](E:\Files\资料笔记\spring\springcloud\images\1639561496871.png) ###### (f)、配置客户端 ![1639562107466](./images/1639562107466.png) ###### (g)、效果 访问客户端接口确定没有流控;查看nacos,确定没有流控配置 ![1639562375152](./images/1639562375152.png) 去sentinel控制台,新增流控规则 ![1639562405915](./images/1639562405915.png) 查看nacos是否出现配置 ![1639562457055](./images/1639562457055.png) 并发访问服务接口,是否出现熔断 ![1639564997494](./images/1639564997494.png) ##### (3)、其他模块改造 和流控类似,对其他模块进行改造;如`param-flow`(热点)、`degrade`(熔断)、`system`(系统)、`authority`(授权)进行改造。 参考博客: https://blog.csdn.net/u014386444/article/details/112064291 ##### (4)、客户端配置数据源 ```yaml spring: cloud: sentinel: datasource: flow: nacos: server-addr: localhost:8848 data-id: ${spring.application.name}-flow-rules groupId: SENTINEL_GROUP data-type: json rule-type: flow namespace: dev param: nacos: server-addr: localhost:8848 data-id: ${spring.application.name}-param-rules groupId: SENTINEL_GROUP data-type: json rule-type: param-flow namespace: dev degrade: nacos: server-addr: localhost:8848 data-id: ${spring.application.name}-degrade-rules groupId: SENTINEL_GROUP data-type: json rule-type: degrade namespace: dev system: nacos: server-addr: localhost:8848 data-id: ${spring.application.name}-system-rules groupId: SENTINEL_GROUP data-type: json rule-type: system namespace: dev authority: nacos: server-addr: localhost:8848 data-id: ${spring.application.name}-authority-rules groupId: SENTINEL_GROUP data-type: json rule-type: authority namespace: dev ``` # 三、Seata ## 3.1、概述 #### 1、是什么? Seata 是一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。 #### 2、Seata术语 ##### TC (Transaction Coordinator) - 事务协调者 维护全局和分支事务的状态,驱动全局事务提交或回滚。 ##### TM (Transaction Manager) - 事务管理器 定义全局事务的范围:开始全局事务、提交或回滚全局事务。 ##### RM (Resource Manager) - 资源管理器 管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚 #### 3、处理过程 1、TM向TC申请开启一个全局事务,全局事务创建成功,并生成一个唯一XID; 2、XID在微服务调用链路中上下文中传播; 3、RM向TC注册分支事务,将其纳入XID对应全局事务的管辖; 4、TM向TC发起针对XID的全局提交或回滚决议; 5、TC调度XID下管辖的全部分支事务完成提交或回滚请求; ![img](./images/TB1rDpkJAvoK1RjSZPfXXXPKFXa-794-478.png) ## 3.2、使用 ### 1、启动服务端 ##### (1)、修改file.conf ###### (a)、文件在conf目录下 https://github.com/seata/seata-samples/blob/master/doc/quick-integration-with-spring-cloud.md ```nginx ## transaction log store, only used in seata-server store { ## store mode: file、db、redis mode = "db" ## file store property file { ## store location dir dir = "sessionStore" # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions maxBranchSessionSize = 16384 # globe session size , if exceeded throws exceptions maxGlobalSessionSize = 512 # file buffer size , if exceeded allocate new buffer fileWriteBufferCacheSize = 16384 # when recover batch read size sessionReloadReadSize = 100 # async, sync flushDiskMode = async } ## database store property db { ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp)/HikariDataSource(hikari) etc. datasource = "druid" ## mysql/oracle/postgresql/h2/oceanbase etc. dbType = "mysql" driverClassName = "com.mysql.jdbc.Driver" url = "jdbc:mysql://127.0.0.1:3306/seata" user = "root" password = "root" minConn = 5 maxConn = 30 globalTable = "global_table" branchTable = "branch_table" lockTable = "lock_table" queryLimit = 100 maxWait = 5000 } ## redis store property redis { host = "127.0.0.1" port = "6379" password = "liyu1228" database = "0" minConn = 1 maxConn = 10 queryLimit = 100 } } ``` ###### (b)、修改日志存储设置为db ![1639634223913](./images/1639634223913.png) ##### (2)、数据库 ###### (a)、创建数据库seata ###### (b)、建表 在conf/db_store.sql文件中,如果没有,可以去官网拷贝 https://github.com/seata/seata/blob/1.3.0/script/server/db/mysql.sql(注意版本,我用的1.3.0) ```sql -- -------------------------------- The script used when storeMode is 'db' -------------------------------- -- the table to store GlobalSession data CREATE TABLE IF NOT EXISTS `global_table` ( `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `status` TINYINT NOT NULL, `application_id` VARCHAR(32), `transaction_service_group` VARCHAR(32), `transaction_name` VARCHAR(128), `timeout` INT, `begin_time` BIGINT, `application_data` VARCHAR(2000), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`xid`), KEY `idx_gmt_modified_status` (`gmt_modified`, `status`), KEY `idx_transaction_id` (`transaction_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; -- the table to store BranchSession data CREATE TABLE IF NOT EXISTS `branch_table` ( `branch_id` BIGINT NOT NULL, `xid` VARCHAR(128) NOT NULL, `transaction_id` BIGINT, `resource_group_id` VARCHAR(32), `resource_id` VARCHAR(256), `branch_type` VARCHAR(8), `status` TINYINT, `client_id` VARCHAR(64), `application_data` VARCHAR(2000), `gmt_create` DATETIME(6), `gmt_modified` DATETIME(6), PRIMARY KEY (`branch_id`), KEY `idx_xid` (`xid`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; -- the table to store lock data CREATE TABLE IF NOT EXISTS `lock_table` ( `row_key` VARCHAR(128) NOT NULL, `xid` VARCHAR(96), `transaction_id` BIGINT, `branch_id` BIGINT NOT NULL, `resource_id` VARCHAR(256), `table_name` VARCHAR(32), `pk` VARCHAR(36), `gmt_create` DATETIME, `gmt_modified` DATETIME, PRIMARY KEY (`row_key`), KEY `idx_branch_id` (`branch_id`) ) ENGINE = InnoDB DEFAULT CHARSET = utf8; ``` ##### (3)、修改registry.conf 该配置用于指定 TC 的注册中心和配置文件,默认都是 file; 如果使用其他的注册中心,要求 Seata-Server 也注册到该配置中心上 ***注意:这里命名空间应该是seata*** ![1639635313897](./images/1639635313897.png) ![1639712014060](./images/1639712014060.png) ##### (4)、导入配置到nacos ###### (a)、从官网下载源码 https://github.com/seata/seata/archive/refs/tags/v1.3.0.zip ###### (b)、修改config.txt 文件在`/script/config-center`下 ![1639969002868](./images/1639969002868.png) ###### (c)、将配置导入nacos 进入`/script/config-center/nacos`目录下, ```shell # 导入,t:命名空间,g:分组 nacos-config.sh -h 127.0.0.1 -p 8848 -t seata -g SEATA_GROUP ``` ###### (d)、查看nacos配置 ![1639969365593](./images/1639969365593.png) ##### (5)、启动服务 查看服务是否成功注册进nacos ![1639635647627](./images/1639635647627.png) ### 2、业务段 ##### (1)、业务概述 参考文档: https://seata.io/zh-cn/docs/user/quickstart.html 用户购买商品的业务逻辑。整个业务逻辑由3个微服务提供支持: - 仓储服务:对给定的商品扣除仓储数量。 - 订单服务:根据采购需求创建订单。 - 帐户服务:从用户帐户中扣除余额。 ##### (2)、数据库 ###### (a)、创建业务对应数据库 - 仓储:`seata_storage`。 - 订单:`seata_order`。 - 帐户:`seata_account`。 ###### (b)、创建业务表 - 仓储`seata_storage`:`ly_storage` ```sql DROP TABLE IF EXISTS `ly_storage`; CREATE TABLE `ly_storage` ( `id` int(11) NOT NULL AUTO_INCREMENT, `commodity_code` varchar(255) DEFAULT NULL, `count` int(11) DEFAULT 0, PRIMARY KEY (`id`), UNIQUE KEY (`commodity_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` - 订单`seata_order`:`ly_order` ```sql DROP TABLE IF EXISTS `ly_order`; CREATE TABLE `ly_order` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` varchar(255) DEFAULT NULL, `commodity_code` varchar(255) DEFAULT NULL, `count` int(11) DEFAULT 0, `money` int(11) DEFAULT 0, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` - 帐户`seata_account`:`ly_account` ```sql DROP TABLE IF EXISTS `ly_account`; CREATE TABLE `ly_account` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` varchar(255) DEFAULT NULL, `money` int(11) DEFAULT 0, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` ###### (c)、添加 undo_log 表 在业务相关的数据库中添加 undo_log 表,用于保存需要回滚的数据 ```sql 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 ``` ###### (d)、初始化数据 初始化用户u001账户余额是200,初始化商品库存是100 ![1639709475532](./images/1639709475532.png) ##### (3)、业务Model 这里只写order部分 ###### (a)、pom.xml 添加`spring-cloud-starter-alibaba-seata`依赖 ```xml com.alibaba.cloud spring-cloud-starter-alibaba-seata ``` ###### (b)、file.conf、registry.conf reginstry.conf ,拷贝server配置文件即可 **修改file.conf** 从官网拷贝file.conf配置文件内容 https://github.com/seata/seata-samples/blob/master/doc/quick-integration-with-spring-cloud.md ```nginx transport { # tcp udt unix-domain-socket type = "TCP" #NIO NATIVE server = "NIO" #enable heartbeat heartbeat = true #thread factory for netty thread-factory { boss-thread-prefix = "NettyBoss" worker-thread-prefix = "NettyServerNIOWorker" server-executor-thread-prefix = "NettyServerBizHandler" share-boss-worker = false client-selector-thread-prefix = "NettyClientSelector" client-selector-thread-size = 1 client-worker-thread-prefix = "NettyClientWorkerThread" # netty boss thread size,will not be used for UDT boss-thread-size = 1 #auto default pin or 8 worker-thread-size = 8 } shutdown { # when destroy server, wait seconds wait = 3 } serialization = "seata" compressor = "none" } service { #vgroup->rgroup vgroupMapping.my_test_tx_group = "default" #only support single node default.grouplist = "127.0.0.1:8091" #degrade current not support enableDegrade = false #disable disable = false #unit ms,s,m,h,d represents milliseconds, seconds, minutes, hours, days, default permanent max.commit.retry.timeout = "-1" max.rollback.retry.timeout = "-1" } client { async.commit.buffer.limit = 10000 lock { retry.internal = 10 retry.times = 30 } report.retry.count = 5 } ## transaction log store store { ## store mode: file、db mode = "file" ## file store file { dir = "sessionStore" # branch session size , if exceeded first try compress lockkey, still exceeded throws exceptions max-branch-session-size = 16384 # globe session size , if exceeded throws exceptions max-global-session-size = 512 # file buffer size , if exceeded allocate new buffer file-write-buffer-cache-size = 16384 # when recover batch read size session.reload.read_size = 100 # async, sync flush-disk-mode = async } ## database store db { ## the implement of javax.sql.DataSource, such as DruidDataSource(druid)/BasicDataSource(dbcp) etc. datasource = "dbcp" ## mysql/oracle/h2/oceanbase etc. db-type = "mysql" url = "jdbc:mysql://127.0.0.1:3306/seata" user = "mysql" password = "mysql" min-conn = 1 max-conn = 3 global.table = "global_table" branch.table = "branch_table" lock-table = "lock_table" query-limit = 100 } } lock { ## the lock store mode: local、remote mode = "remote" local { ## store locks in user's database } remote { ## store locks in the seata's server } } recovery { committing-retry-delay = 30 asyn-committing-retry-delay = 30 rollbacking-retry-delay = 30 timeout-retry-delay = 30 } transaction { undo.data.validation = true undo.log.serialization = "jackson" } ## metrics settings metrics { enabled = false registry-type = "compact" # multi exporters use comma divided exporter-list = "prometheus" exporter-prometheus-port = 9898 } ``` ###### (b)、自定义事务组名称 ![1639710827152](E:/Files/%E8%B5%84%E6%96%99%E7%AC%94%E8%AE%B0/spring/springcloud/images/1639710827152.png) ###### (c)、修改日志存储设置为db ![1639634223913](E:/Files/%E8%B5%84%E6%96%99%E7%AC%94%E8%AE%B0/spring/springcloud/images/1639634223913.png) ###### (c)、application.yaml 配置seata和nacos ```yaml server: port: 8101 spring: application: name: seata-order cloud: nacos: discovery: server-addr: localhost:8848 namespace: dev alibaba: seata: # 需要和seata-server服务端配置的分组对应 tx-service-group: ly_tx_group datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/seata_order?useSSL=false&useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true username: root password: root mybatis: mapper-locations: classpath*:/mapper/*.xml mybatis-plus: configuration: # 是否开启自动驼峰命名规则(camel case)映射 map-underscore-to-camel-case: true management: endpoints: web: exposure: include: "*" ``` ###### (d)、Application.java 开启openfeign和mybatis的扫描 ```java @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) @EnableDiscoveryClient @EnableFeignClients(basePackages = "com.liyu.commons.*.feign") // 扫描 @FeignClient 注解 @MapperScan("com.liyu.*.mapper") public class OrderApplication { public static void main(String[] args) { SpringApplication.run(OrderApplication.class, args); } } ``` ###### (e)、数据库配置、和mybatis配置 ```java @Configuration @EnableConfigurationProperties({MybatisProperties.class}) public class DataSourceConfiguration { @Bean @ConfigurationProperties(prefix = "spring.datasource") public DataSource dataSource() { return new DruidDataSource(); } @Bean public DataSourceProxy dataSourceProxy(DataSource dataSource) { return new DataSourceProxy(dataSource); } } ``` ```java @Configuration public class MybatisPlusConfig { @Bean(name = "globalConfig") @Primary public GlobalConfig globalConfig() { GlobalConfig globalConfig = new GlobalConfig(); return globalConfig; } @Bean("sqlSessionFactory") @Primary public SqlSessionFactory dbSqlSessionFactory(@Qualifier("dataSource") DataSource dataSource, GlobalConfig globalConfig) throws Exception { // MybatisSqlSessionFactoryBean MybatisSqlSessionFactoryBean factoryBean = new MybatisSqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); factoryBean.setGlobalConfig(globalConfig); PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver(); // 需要在这里指定xml文件的位置,不然自定义的sql会报Invalid bound statement异常 factoryBean.setMapperLocations(resolver.getResources("classpath*:mapper/*Mapper.xml")); // 导入mybatis配置 构造方法,解决动态数据源循环依赖问题 MybatisConfiguration mybatisConfiguration = new MybatisConfiguration(); // 配置打印sql语句 mybatisConfiguration.setLogImpl(StdOutImpl.class); factoryBean.setConfiguration(mybatisConfiguration); return factoryBean.getObject(); } } ``` ###### (f)、完成订单业务代码 并通过feign调用库存和账户的服务 ![1639712648150](E:\Files\资料笔记\spring\springcloud\images\1639712648150.png) ##### (4)、异常测试 ###### (a)、使用postman发送生成订单请求 ![1639709804331](./images/1639709804331.png) ###### (b)、查看数据库我们发现 由于账户余额不足,账户未扣款,但是订单成功生成,并且商品扣除成功扣减 ![1639709887370](./images/1639709887370.png) ![1639709941522](./images/1639709941522.png) ![1639709975758](./images/1639709975758.png) ##### (5)、分布式事务测试 ###### (a)、给创建订单方法添加注解`@GlobalTransactional` ![1639710177370](./images/1639710177370.png) ###### (b)、恢复初始数据,并发送异常请求 ![1639710438142](./images/1639710438142.png) 数据成功回滚 这里演示默认的AT模式 ###### (a)、发送请求,在order执行本地事务之前 此时数据库中并没有全局事务信息。 ![1639728622249](./images/1639728622249.png) (b)、