欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 汽车 > 时评 > SpringCloud

SpringCloud

2025/7/17 7:08:57 来源:https://blog.csdn.net/linping_wong/article/details/148366975  浏览:    关键词:SpringCloud

0. 分布式基础

分布式配套:日志系统、指标监控、链路追踪、消息处理

单体应用:

所有功能模块都在一个项目上

优点:开发部署简单方便

缺点:无法应对高并发

集群架构:

优点:解决大并发

缺点:模块化升级(有些功能模块需要经常升级)、多语言团队(引入其他语言开发新模块)

分布式架构:

一个大型应用被拆分成多个小型应用,部署在各个机器。

RPC:远程过程调用,HTTP+json只是rpc的一种方式

服务雪崩:一个微服务的故障传播到了整个调用链,影响到了服务器,影响到了服务器中其他模块,进而影响到整个应用。需要引入服务熔断机制,出现故障快速返回,不会引起请求积压。

熔断:快速失败机制,及时释放资源。

分布式 VS. 集群

分布式:工作方式。大型应用拆分为多个小应用,分布在各个服务器上,每个服务器上部署的东西可能都不一样。

集群:物理形态。很多机器就叫集群。

环境准备

创建微服务架构项目

引入 SpringClould、Spring Cloud Alibaba 相关依赖

注意版本适配

 项目工程结构图:

父项目:(SpringBoot)jdk17、改pom文件、将父项目路径下的所有内容除了pom.xml和.idea都删掉

改pom.xml文件:

  1. parent版本
  2. pom打包的方式:<packaging>pom</packaging>
  3. properties
  4. dependencyManagement引入
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>3.3.4</version><relativePath/> <!-- lookup parent from repository --></parent><modelVersion>4.0.0</modelVersion><groupId>com.atguigu</groupId><artifactId>spring-cloud-demo</artifactId><version>1.0-SNAPSHOT</version><packaging>pom</packaging><properties><maven.compiler.source>17</maven.compiler.source><maven.compiler.target>17</maven.compiler.target><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><spring-cloud.version>2023.0.3</spring-cloud.version><spring-cloud-alibaba.version>2023.0.3.2</spring-cloud-alibaba.version></properties><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-dependencies</artifactId><version>${spring-cloud-alibaba.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement></project>

子项目services:(java)

修改子项目的pom文件:<packaging>pom</packaging>

创建商品服务和订单服务模块等:

services的pom文件中添加依赖:

 <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency>

1. Nacos:注册中心

注册中心保存一个微服务与其机器清单列表,当订单服务想要调用商品服务,就要先问一下注册中心,商品服务都存在于哪些机器上,注册中心向订单服务返回机器号列表,订单服务模块可以任意选择一个商品服务机器访问。

注册中心两个功能:服务注册和服务发现

(1)Nacos安装

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service的首 字母简称,一个更易于构建云原生应用的动态服务发现、配置管 理和服务管理平台。

官网:https://nacos.io/zh-cn/docs/v2/quickstart/quick-start.html

安装:

  • 下载安装包【2.4.3】,放到一个没有中文的路径下解压缩,然后启动
  • 启动命令: startup.cmd -m standalone

访问:http://localhost:8848/nacos/

(2)注册中心 - 服务注册

step1:给业务模块(商品和订单模块)加spring-boot-starter-web依赖:

step2:引入服务发现依赖:

 <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency>

step3:添加主程序类:加@SpringBootApplication注解

@SpringBootApplicationpublic class OrderMainApplication {public static void main(String[] args) {SpringApplication.run(OrderMainApplication.class, args);}}

step4:配置nacos地址:application.properties文件中

step5:启动主程序类,看nacos里面是否已经注册。访问:http://localhost:8848/nacos 可以看到服务已经注册上来;

step6:为了更好地看出效果,IDEA左下角启动的服务,复制(右键copy configurations)一个订单模块,并修改端口号(Program arguments):

将所有的服务启动,可以看到注册中心中保存的就是【微服务的名字+ip+端口号】

(3)注册中心 - 服务发现

step1:开启服务发现功能:主启动类上加注解 @EnableDiscoveryClient // 开启服务发现功能的核心注解 

@EnableDiscoveryClient // 开启服务发现功能的核心注解
@SpringBootApplicationpublic class OrderMainApplication {public static void main(String[] args) {SpringApplication.run(OrderMainApplication.class, args);}}

step2:测试。导入测试依赖:spring-boot-starter-test

@SpringBootTest
public class DiscoveryTest {@AutowiredDiscoveryClient discoveryClient; // 或者使用NacosServiceDiscovery的api@Testvoid discoveryClientTest(){for (String service : discoveryClient.getServices()) {System.out.println("service = " + service);			//获取ip+portList<ServiceInstance> instances = discoveryClient.getInstances(serfor (ServiceInstance instance : instances) {System.out.println("ip:"+instance.getHost()+";"+"port = " + instance.getPort());}}}
}

运行结果:

远程调用 - 基本流程

远程调用 - 下单场景

配置 RestTemplate: 

@Configuration
public class UserConfiguration {@BeanRestTemplate restTemplate() {return new RestTemplate();}
}

订单模块向商品模块发送请求:

@Autowired
RestTemplate restTemplate;

以上代码是请求商品的第一个服务,所以:订单调用商品模块发起请求:

9001/9002/9003都启动,则请求的是9001(远程的第一台服务器)。

当把9001停掉之后,会去访问9002

小结:

  1. 使⽤ RestTemplate 可以获取到远程数据
  2. 必须精确指定地址和端⼝
  3. 如果远程宕机将不可⽤

期望:可以负载均衡调⽤,不⽤担⼼远程宕机

远程调用 - 负载均衡

方法1:负载均衡依赖

由于是订单负载均衡调用商品,所以在订单中加入负载均衡的依赖:

<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
@Autowired
LoadBalancerClient loadBalancerClient;
@Autowired
DiscoveryClient discoveryClient;private Product getProductFromRemoteWithLoadBalance(Long productId){//1、获取到商品服务所在的所有机器IP+port(LoadBalancerClient 负载均衡(轮询)地拿地址;DiscoveryClient 获得全部地址)ServiceInstance choose = loadBalancerClient.choose("service-product"); //远程URL JavaString url = "http://"+choose.getHost() +":" +choose.getPort() +"/product/"+productId;log.info("远程请求:{}",url);//2、给远程发送请求Product product = restTemplate.getForObject(url, Product.class);return product;
}

方法2:注解式负载均衡 

不用精准指定地址和接口。

@Configuration
public class UserConfiguration {@LoadBalanced@BeanRestTemplate restTemplate() {return new RestTemplate();}
}

订单模块中发起请求: 

private Product getProductFromRemoteWithLoadBalanceAnnotation(Long productId){String url = "http://service-product/product/"+productId;//2、给远程发送请求; service-product 会被动态替换Product product = restTemplate.getForObject(url, Product.class);return product;
}

以前的url地址:http://localhost:9002/prodect/4

现在的url地址:http://service-product/prodect/4   service-product为被请求的微服务的名字

小结:

  1. 负载均衡调⽤只需要传⼊ 服务名
  2. 请求发起之前会⾃动去注册中⼼确定微服务地址
  3. 如果微服务宕机,会⾃动剔除在线名单,请求将不会发过去

如果注册中⼼宕机,远程调⽤是否可以成功?

  1. 从未调⽤过,如果宕机,调⽤会⽴即失败
  2. 调⽤过,如果宕机,因为缓存名单,调⽤会成功
  3. 调⽤过,如果注册中⼼和对⽅服务宕机,因为会缓存名单,调⽤会阻塞后失败(Connection Refused)

 2. Nacos:配置中心

(1)基本使用

  1. 启动Nacos
  2. 引入依赖(要刷新一下)
    <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
  3. application.properties配置
    #指定配置中⼼地址
    spring.cloud.nacos.server-addr=localhost:8848
    #service-order.properties是在Nacos中配置的
    spring.config.import=nacos:service-order.properties  
  4. 创建data-id(数据集)

在代码中获取配置信息:

(2)动态刷新

① @Value(“${xx}”) 获取配置 + @RefreshScope 实现自动刷新

为了激活配置属性的自动刷新功能,在controller类上加注解:@RefreshScope

@RefreshScope//自动刷新
@RestController
public class OrderController {@AutowiredOrderService orderService;@Value("${order.timeout}")String orderTimeout;@Value("${order.auto-confirm}")String orderAutoConfirm;@GetMapping("/config")public String config(){return "order.timeout="+orderTimeout+";" +"order.auto-confirm="+orderAutoConfirm;}
}

如果在父项目中导入了配置中心的依赖,但是在有的子项目中暂时用不到配置中心的功能,运行时就会报错,避免出现该报错,可以在这样的子项目的配置文件中加上以下配置:(禁用导入检查)

spring.cloud.nacos.config.import-check.enabled=false

② @ConfigurationProperties 无感自动刷新

无需 @RefreshScope,⾃动绑定配置,动态更新

新建一个properties包,将以下类放在该包下:

@Component
@ConfigurationProperties(prefix = "order") //order为nacos配置中的配置前缀
@Data
public class OrderProperties {String timeout;String autoConfirm;String dbUrl;
}
@RestController
public class OrderController {@AutowiredOrderService orderService;@AutowiredOrderProperties orderProperties;@GetMapping("/config")public String config(){return "order.timeout="+orderProperties.getTimeout()+";" +"order.auto-confirm="+orderProperties.getAutoConfirm();}
}

③NacosConfigManager 监听配置变化

  1. ① 项目启动就监听配置文件变化()
  2. ② 发生变化后拿到变化值
  3. ③ 发送邮件

项目启动类中编写应用启动方法applicationRunner,run方法自动运行,是函数式接口,所以可以用函数式编程:

@Bean
ApplicationRunner applicationRunner(NacosConfigManager manager){return args -> {ConfigService configService = manager.getConfigService();configService.addListener("service-order.properties", "DEFAULT_GROUP", new Listener() {@Overridepublic Executor getExecutor() {return Executors.newFixedThreadPool(4);}@Overridepublic void receiveConfigInfo(String configInfo) {System.out.println("configInfo = " + configInfo);}});};
}

Nacos中的数据集 和 application.properties 有相同的 配置项,哪个生效?

以Nacos配置集中为准。 

nacos数据集中配置优先级高,application.properties优先级低,合并时,如果低优先级中有跟高优先级相同的配置,会被丢弃,如果application.properties的import导入多个以逗号分隔的配置文件,如果有相同的,则以前面的为准

(3)数据隔离

需求描述

  • 项目有多套环境:dev,test,prod
  • 每个微服务,同一种配置,在每套环境的值都不一样。
    •  如:database.properties
    •  如:common.properties
  • 项目可以通过切换环境,加载本环境的配置

难点

  • 区分多套环境:namespace
  • 区分多种微服务:group
  • 区分多种配置:数据集data-id
  • 按需加载配置

namespace、dataId、group 配合 spring.config.activate.on-profile 实现配置环境隔离。

在项目中加载哪组配置文件:

当不同的环境中配置不同时,动态选择使用哪个环境:

根据namespace后设置的环境选择以下相对应环境的配置

3. OpenFeign:远程调⽤

(1)基础⼊⻔

官网:Spring Cloud OpenFeign Features :: Spring Cloud Openfeign

OpenFeign 是⼀个声明式远程调⽤客户端;

(2)Declarative REST Client

声明式 REST 客户端 vs 编程式 REST 客户端(RestTemplate)

注解驱动

  • 指定远程地址:@FeignClient
  • 指定请求方式:@GetMapping、@PostMapping、@DeleteMapping ...
  • 指定携带数据:@RequestHeader、@RequestParam、@RequestBody ...
  • 指定结果返回:响应模型 org.springframework.cloud spring-cloud-starter-openfeign

(3)远程调用 - 业务API

步骤:

① 引入依赖:

<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

② 启动类(order模块)加@EnableFeignClients注解

@SpringBootApplication
@EnableFeignClients
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}
}

③ 创建远程调用客户端,并加@FeignClient注解

@FeignClient(value = "service-product",fallback = ProductFeignClientFallback.class) // feign客户端
public interface ProductFeignClient {//mvc注解的两套使用逻辑//1、标注在Controller上,是接受这样的请求//2、标注在FeignClient上,是发送这样的请求@GetMapping("/product/{id}")Product getProductById(@PathVariable("id") Long id);}

测试:

OrderController:

//创建订单
@GetMapping("/create")
public Order createOrder(@RequestParam("userId") Long userId,@RequestParam("productId") Long productId){Order order = orderService.createOrder(productId, userId);return order;
}

OrderserviceImpl: 

@Autowired
ProductFeignClient productFeignClient;@Override
public Order createOrder(Long productId, Long userId) {
//        Product product = getProductFromRemoteWithLoadBalanceAnnotation(productId);//使用Feign完成远程调用Product product = productFeignClient.getProductById(productId);Order order = new Order();order.setId(1L);// 总金额order.setTotalAmount(product.getPrice().multiply(new BigDecimal(product.getNum())));order.setUserId(userId);order.setNickName("zhangsan");order.setAddress("尚硅谷");//远程查询商品列表order.setProductList(Arrays.asList(product));return order;
}

 Feight客户端方法:(Order模块)

@FeignClient(value = "service-product") // feign客户端
public interface ProductFeignClient {@GetMapping("/product/{id}")Product getProductById(@PathVariable("id") Long id);
}

自动给service-product模块发送请求。

@RestController
public class ProductController {@AutowiredProductService productService;//查询商品@GetMapping("/product/{id}")public Product getProduct(@PathVariable("id") Long productId,HttpServletRequest request){Product product = productService.getProductById(productId);return product;}
}
public interface ProductService {Product getProductById(Long productId);
}
@Service
public class ProductServiceImpl implements ProductService {@Overridepublic Product getProductById(Long productId) {Product product = new Product();product.setId(productId);product.setPrice(new BigDecimal("99"));product.setProductName("苹果-"+productId);product.setNum(2);return product;}
}

在浏览器发送请求:localhost://8000/create?userId=777&productId=666

页面响应:

(4)远程调用 - 第三方API

@FeignClient(value = "weather-client", url = "http://aliv18.data.moji.com")
public interface WeatherFeignClient {@PostMapping("/whapi/json/alicityweather/condition")String getWeather(@RequestHeader("Authorization") String auth,@RequestParam("token") String token,@RequestParam("cityId") String cityId);
}
@SpringBootTest
public class WeatherTest {@AutowiredWeatherFeignClient weatherFeignClient;@Testvoid test01(){String weather = weatherFeignClient.getWeather("APPCODE 93b7e19861a24c519a7548b17dc16d75","50b53ff8dd7d9fa320d3d3ca32cf8ed1","2182");System.out.println("weather = " + weather);}
}

如果在Feign客户端指定了url地址,就是给url发送请求,再在方法中指定路径,如果没有指定url地址,则是给value指定的微服务发送请求,需要连上注册中心。

小技巧:如何编写好OpenFeign声明式的远程调用接口

  1. 业务API:直接复制对方Controller签名即可
  2. 第三方API:根据接口文档确定请求如何发

面试题:客户端负载均衡与服务端负载均衡区别

进阶 - 日志

配置文件中配置:

logging:level:com.atguigu.order.feign: debug   # feign客户端所在的包名

配置类中加入

@Bean
Logger.Level feignLoggerLevel() {return Logger.Level.FULL;
}

进阶 - 超时控制

连接超时10s,读取超时60s是默认配置,如果超时,默认返回错误信息。

spring.profiles.active 是激活的环境

spring.profiles.include 是除了激活的环境,还包含哪个环境

spring:cloud:openfeign:client:config:default:logger-level: fullconnect-timeout: 1000read-timeout: 2000service-product:logger-level: fullconnect-timeout: 3000read-timeout: 5000

进阶 - 重试机制

远程调用超时失败后,还可以进行多次尝试,如果某次成功返回ok,如 果多次依然失败则结束调用,返回错误。

设置等待时长100ms,往后每次重试都是1.5倍上次等待时长,所以是150ms,150 * 1.5ms,……,但是最长等待不超过1s,如果超过了1s就当做1s,最大重试次数为5。

在配置类中加:

@Bean
Retryer retryer(){return new Retryer.Default();
}

进阶 - 拦截器

编写拦截器:(例:向请求头中添加token) 

@Component
public class XTokenRequestInterceptor implements RequestInterceptor {/*** 请求拦截器* @param template 请求模板*/@Overridepublic void apply(RequestTemplate template) {System.out.println("XTokenRequestInterceptor ....... ");template.header("X-Token", UUID.randomUUID().toString());}
}

 如果拦截器没有加@Component注解,就需要将拦截器配置在application.yml中:

进阶用法 - Fallback兜底返回

注意:此功能需要整合 Sentinel 才能实现

引入sentinel的依赖

<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

开启熔断:

feign:sentinel:enabled: true

写fallback函数:

@FeignClient(value = "service-product",fallback = ProductFeignClientFallback.class) // feign客户端
public interface ProductFeignClient {//mvc注解的两套使用逻辑//1、标注在Controller上,是接受这样的请求//2、标注在FeignClient上,是发送这样的请求@GetMapping("/product/{id}")Product getProductById(@PathVariable("id") Long id);
}
@Component
public class ProductFeignClientFallback implements ProductFeignClient {@Overridepublic Product getProductById(Long id) {System.out.println("兜底回调....");Product product = new Product();product.setId(id);product.setPrice(new BigDecimal("0"));product.setProductName("未知商品");product.setNum(0);return product;}
}

4. Sentinel:流量保护

服务保护(限流、熔断降级)

官网:https://sentinelguard.io/zh-cn/index.html

功能介绍

随着微服务的流行,服务和服务之间的稳定性变得越来越重要。Spring Cloud Alibaba Sentinel 以流量为切入点,从流量控制、流量路由、熔断降级、系统 自适应过载保护、热点流量防护等多个维度保护服务的稳定性。

Sentinel 具有以下特征:

丰富的应⽤场景:Sentinel 承接了阿⾥巴巴近 10 年的双⼗⼀⼤促流量的核⼼场景,例如秒杀 (即突发流量控制在系统容量可以承受的范围)、消息削峰填⾕、集群流量控制、实时熔断下 游不可⽤应⽤等。

完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接⼊应⽤的单台 机器秒级数据,甚⾄ 500 台以下规模的集群的汇总运⾏情况。

⼴泛的开源⽣态:Sentinel 提供开箱即⽤的与其它开源框架/库的整合模块,例如与 SpringCloud、Apache Dubbo、gRPC、Quarkus 的整合。您只需要引⼊相应的依赖并进⾏简单的 配置即可快速地接⼊ Sentinel。同时 Sentinel 提供 Java/Go/C++ 等多语⾔的原⽣实现。

完善的 SPI 扩展机制:Sentinel 提供简单易⽤、完善的 SPI 扩展接⼝。您可以通过实现扩展 接⼝来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

架构原理

资源&规则

定义资源:

  • 主流框架自动适配(Web Servlet、Dubbo、Spring Cloud、gRPC、Spring WebFlux、Reactor); 所有Web接口均为资源,feign接口
  • 编程式:SphU API
  • 声明式:@SentinelResource

定义规则:

  • 流量控制(FlowRule)
  • 熔断降级(DegradeRule):防止服务雪崩的,
  • 系统保护(SystemRule):CPU太忙了,限制请求
  • 来源访问控制(AuthorityRule)
  • 热点参数(ParamFlowRule)

工作原理

整合使用

 在sentinel-dashboard-1.8.8.jar存放的文件夹中cmd,输入命令:java -jar sentinel-dashboard-1.8.8.jar启动控制台。

访问:localhost:8080,用户名密码都是sentinel

配置sentinel依赖:

<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

配置连接(每个微服务模块都加这个配置)

spring:cloud:sentinel:transport:dashboard: localhost:8080eager: true   # 默认是懒加载的,加上这个之后可以快速加载

对想保护的方法(非controller的方法,一般是serviceImpl)加注解@SentinelResource(value = "createOrder")

启动微服务后可以在sentinel控制台找对应的微服务模块的【簇点链路】,可以看到可以给方法设置【流控】【熔断】【热点】【授权】等。

异常处理

 ① 自定义BlockExceptionHandler(web接口)

@Component
public class MyBlockExceptionHandler implements BlockExceptionHandler {private ObjectMapper objectMapper = new ObjectMapper();@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response,String resourceName, BlockException e) throws Exception {response.setStatus(429); //too many requestsresponse.setContentType("application/json;charset=utf-8");PrintWriter writer = response.getWriter();R error = R.error(500, resourceName + " 被Sentinel限制了,原因:" + e.getClass());String json = objectMapper.writeValueAsString(error);writer.write(json);writer.flush();writer.close();}
}

② BlockHandler (@SentinelResource)

@SentinelResource(value = "createOrder",blockHandler = "createOrderFallback")
@Override
public Order createOrder(Long productId, Long userId) {
//        Product product = getProductFromRemoteWithLoadBalanceAnnotation(productId);//使用Feign完成远程调用Product product = productFeignClient.getProductById(productId);Order order = new Order();order.setId(1L);// 总金额order.setTotalAmount(product.getPrice().multiply(new BigDecimal(product.getNum())));order.setUserId(userId);order.setNickName("zhangsan");order.setAddress("尚硅谷");//远程查询商品列表order.setProductList(Arrays.asList(product));return order;
}//兜底回调
public Order createOrderFallback(Long productId, Long userId, BlockException e){Order order = new Order();order.setId(0L);order.setTotalAmount(new BigDecimal("0"));order.setUserId(userId);order.setNickName("未知用户");order.setAddress("异常信息:"+e.getClass());return order;
}

对于加了@SentinelResource的方法,出现了异常,就使用注解参数对应的方法进行返回,如果没有配置该方法,就返回springboot错误页面

③ OpenFeign调用

如果配置了异常回调,则返回异常回调,否则返回springboot错误页面

@FeignClient(value = "service-product",fallback = ProductFeignClientFallback.class) // feign客户端
public interface ProductFeignClient {//mvc注解的两套使用逻辑//1、标注在Controller上,是接受这样的请求//2、标注在FeignClient上,是发送这样的请求@GetMapping("/product/{id}")Product getProductById(@PathVariable("id") Long id);
}

@Component
public class ProductFeignClientFallback implements ProductFeignClient {@Overridepublic Product getProductById(Long id) {System.out.println("兜底回调....");Product product = new Product();product.setId(id);product.setPrice(new BigDecimal("0"));product.setProductName("未知商品");product.setNum(0);return product;}
}

④ SphU硬编码

@SentinelResource(value = "createOrder",blockHandler = "createOrderFallback")
@Override
public Order createOrder(Long productId, Long userId) {
//        Product product = getProductFromRemoteWithLoadBalanceAnnotation(productId);//使用Feign完成远程调用Product product = productFeignClient.getProductById(productId);Order order = new Order();order.setId(1L);// 总金额order.setTotalAmount(product.getPrice().multiply(new BigDecimal(product.getNum())));order.setUserId(userId);order.setNickName("zhangsan");order.setAddress("尚硅谷");//远程查询商品列表order.setProductList(Arrays.asList(product));
//
//        try {
//            SphU.entry("hahah");
//
//        } catch (BlockException e) {
//            //编码处理
//        }return order;
}

规则 - 流量控制(FlowRule)

流量控制:限制多余请求,从而保护系统资源不被耗尽

并发线程数:需要结合线程池使用,效率比较低。

集群阈值模式:单机均摊,每个机器QPS为1;总体阈值,所有机器加起来QPS为1。

以上配置为每秒放行一个请求,如果请求太快了,会返回:Blocked by Sentinel (flow limiting)。如果想自定义异常返回提示信息,参考【异常处理】部分

规则 - 流量控制(FlowRule)- 阈值类型

QPS: 统计每秒请求数

并发线程数: 统计并发线程数

规则 - 流量控制(FlowRule)- 流控模式

调用关系包括调用方、被调用方;一个方法又可能会调用其它方法,形成一个调用链路的层次关 系;有了调用链路的统计信息,我们可以衍生出多种流量控制手段。

链路模式:对有的链路限制,有的链路不限制。要在配置文件中设置关闭上下文统一:

//创建订单
@GetMapping("/create")
public Order createOrder(@RequestParam("userId") Long userId,@RequestParam("productId") Long productId){Order order = orderService.createOrder(productId, userId);return order;
}@GetMapping("/seckill")
@SentinelResource(value = "seckill-order",fallback = "seckillFallback")
public Order seckill(@RequestParam(value = "userId",required = false) Long userId,@RequestParam(value = "productId",defaultValue = "1000") Long productId){Order order = orderService.createOrder(productId, userId);order.setId(Long.MAX_VALUE);return order;
}

只对秒杀的资源做流量控制。

关联策略:只有在写流量很大时,读的限流才会触发,如果没有写或者和写的流量很小的时候,读不做限制。

@GetMapping("/writeDb")
public String writeDb(){return "writeDb success....";
}@GetMapping("/readDb")
public String readDb(){log.info("readDb...");return "readDb success....";
}

只大量访问read时,不会被限制;只有当大量访问write,再去访问read时,会被限制。

规则 - 流量控制(FlowRule)- 流控效果

注意:只有快速失败支持流控模式(直接、 关联、链路)的设置

快速失败:如果没有超出阈值,则交给业务处理;如果超出了阈值,则超出的请求直接抛出一个BlockedException异常。

warm up:

QPS为10,预热为3秒,所以第一秒处理大约3个,其他被丢弃,3秒内逐渐长到10个,3秒后再稳定在10个。

匀速排队(参考漏桶算法)

假设QPS=2,则每秒处理两个请求,那么多余的请求排队等待,等待下一秒进行处理,但不是永久排队,当超出了timeout就会被丢弃。

规则 - 熔断降级(DegradeRule)

  • 切断不稳定调用
  • 快速返回不积压
  • 避免雪崩效应 

(及时发现不稳定的调用,及时切断)

当D突然中断,一旦G和F感知到D调用慢,则直接切断跟D的联系,直接返回错误。切断不稳定调用的核心功能就是快速返回,这样请求不积压,请求不积压的直接效果就是防止雪崩效应。

最佳实践:熔断降级作为保护自身的手段,通常在客户端(调用端)进行配置。  

A和B都正常,断路器是关闭的;

 当B出现问题,断路器打开,A看到断路器是打开的,就不再调用B,就快速得到一个错误返回; 

断路器半开状态:A向B试探发送一个请求(熔断降级),如果正常可用,则断路器关闭,否则打开

断路器工作原理:

给远程调用增加熔断规则: 

慢调用比例:

RT:response time (ms) 超过多少毫秒没有响应就认为是慢请求。

当5000毫秒内有超过80%的慢请求,则30分钟内的请求不再发给远程服务。

异常比例:

不管有无熔断,都会调用兜底回调,区别是: 有熔断规则时,一定时间(熔断时长内)内就不给远程发送请求,节约了远程调用时间。

熔断规则:让自己系统在对方不稳定的情况下更加健壮,更加快,不用重复去走失败的路。

异常数规则:

5秒内只要有10个异常,远程调用了10次,则之后30秒就不会再发远程请求。

规则 - 热点参数

流控只能在资源级别对资源的访问量进行限制,热点规则可以细粒度到参数,

需求1:每个用户秒杀 QPS 不得超过 1(秒杀下单 userId 级别)

        效果:携带此参数的参与流控,不携带不流控

需求2:6号用户是vvip,不限制QPS(例外情况)

需求3:666号是下架商品,不允许访问

@GetMapping("/seckill")
@SentinelResource(value = "seckill-order",fallback = "seckillFallback")
public Order seckill(@RequestParam(value = "userId",required = false) Long userId,@RequestParam(value = "productId",defaultValue = "1000") Long productId){Order order = orderService.createOrder(productId, userId);order.setId(Long.MAX_VALUE);return order;
}public Order seckillFallback(Long userId,Long productId, Throwable exception){System.out.println("seckillFallback....");Order order = new Order();order.setId(productId);order.setUserId(userId);order.setAddress("异常信息:"+exception.getClass());return order;
}

 blockHandler(优先级高)可以处理BlockException,fallback可以处理业务异常,但是其回调函数的异常类型需要改为Throwable

【需求1:每个用户秒杀 QPS 不得超过 1】

【需求2:6号用户是vvip,不限制QPS(例外情况)】需求一配置的高级选项

【需求3:666号是下架商品,不允许访问】

 规则 - 授权规则

白名单:列出来的应用可以访问资源;

黑名单:列出来的应用不可以访问资源。

 规则 - 系统规则

根据现在系统的情况来限定

5. Gateway:网关

官网:Spring Cloud Gateway

路由

需求

1. 客户端发送 /api/order/** 转到 service-order

2. 客户端发送 /api/product/** 转到 service-product

3. 以上转发有负载均衡效果

新建gateway为服务模块,并引入相关依赖:

<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

 配置文件:

application.yml

spring:profiles:include: routeapplication:name: gatewaycloud:nacos:server-addr: 127.0.0.1:8848# localhost/api/order
server:port: 80

application-gateway.yml

spring:cloud:gateway:routes:- id: orderuri: lb://service-orderpredicates:- Path=/api/order/**- id: producturi: lb://service-productpredicates:- Path=/api/product/** 

 给所有的订单服务controller加url前缀:@RequestMapping("/api/order"),涉及到远程调用,所以给ProductFeignClient远程调用接口的方法也完善url前缀@GetMapping("/api/product/product/{id}")

给所有的商品服务controller加url前缀:@RequestMapping("/api/product")

基础原理

Predicate - 断言

举例:

路径需要带上/search,并且带上参数为q,值为haha才能转到bing,如下:

自定义断言:

断言工厂名Vip是自定义工厂的前缀

@Component
public class VipRoutePredicateFactory extends AbstractRoutePredicateFactory<VipRoutePredicateFactory.Config> {public VipRoutePredicateFactory() {super(Config.class);}@Overridepublic Predicate<ServerWebExchange> apply(Config config) {return new GatewayPredicate() {@Overridepublic boolean test(ServerWebExchange serverWebExchange) {// localhost/search?q=haha&user=leifengyangServerHttpRequest request = serverWebExchange.getRequest();String first = request.getQueryParams().getFirst(config.param);return StringUtils.hasText(first) && first.equals(config.value);}};}@Overridepublic List<String> shortcutFieldOrder() {return Arrays.asList("param", "value");}/*** 可以配置的参数*/@Validatedpublic static class Config {@NotEmptyprivate String param;@NotEmptyprivate String value;public @NotEmpty String getParam() {return param;}public void setParam(@NotEmpty String param) {this.param = param;}public @NotEmpty String getValue() {return value;}public void setValue(@NotEmpty String value) {this.value = value;}}
}

Filter - 过滤器

路径重写 - rewritePath

请求的是/api/order/readDb,controller收到的是/readDb,这样就不用去每个controller加url前缀了

predicates:
- name: Pathargs:patterns: /api/order/**matchTrailingSlash: true
filters:
- RewritePath=/api/order/?(?<segment>.*), /$\{segment}
- OnceToken=X-Response-Token, jwt

 RewritePath属性设置:将【/api/order】后边的部分放入<segment>并返回,达到想要的效果。

过滤器其他功能:

添加响应头的过滤器

访问订单的所有请求,响应中都会有该请求头: 

默认Filter

default-filters:- AddResponseHeader=X-Response-Abc, 123

全局Filter

@Component
@Slf4j
public class RtGlobalFilter implements GlobalFilter, Ordered {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {ServerHttpRequest request = exchange.getRequest();ServerHttpResponse response = exchange.getResponse();String uri = request.getURI().toString();long start = System.currentTimeMillis();log.info("请求【{}】开始:时间:{}",uri,start);//========================以上是前置逻辑=========================// Mono:响应式编程中封装一个或零个数据的响应式流Mono<Void> filter = chain.filter(exchange)   // 异步放行,这段代码后面的代码段不等该filter执行完会立即执行,.doFinally((result)->{//=======================以下是后置逻辑=========================long end = System.currentTimeMillis();log.info("请求【{}】结束:时间:{},耗时:{}ms",uri,end,end-start);}); //放行   10sreturn filter;}@Overridepublic int getOrder() {return 0; // 数字越小,优先级越高}
}

 自定义过滤器工厂

@Component
public class OnceTokenGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {@Overridepublic GatewayFilter apply(NameValueConfig config) {return new GatewayFilter() {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {//每次响应之前,添加一个一次性令牌,支持 uuid,jwt等各种格式return chain.filter(exchange).then(Mono.fromRunnable(()->{ServerHttpResponse response = exchange.getResponse();HttpHeaders headers = response.getHeaders();String value = config.getValue();if ("uuid".equalsIgnoreCase(value)){value = UUID.randomUUID().toString();}if ("jwt".equalsIgnoreCase(value)){value = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ";}headers.add(config.getName(),value);}));}};}
}

相应的配置文件:

全局跨域

前后端分离的项目中,前端给后端发送请求,经常涉及到跨域的问题,单体项目中springboot提供了一种解决方案,就是给controller上标注一个跨域注解@CrossOrigin,则该controller类中的所有方法都允许前端跨域访问,如果controller太多的话,可以在项目级别编写一个跨域Filter,原理:给响应头中添加一些跨域的配置头,比如允许哪些请求来源,允许哪些请求方式,允许什么请求头,但是该方法只能解决一个项目的问题,多个应用模块没办法快速统一配置,所以可以在网关进行统一跨域设置,让所有的请求经过网关,网关将微服务处理完了后,网关给前端的响应都是可以跨域的,

 面试题 :微服务之间的调用经过网关吗?可以过也可以不过,但没必要

 如果要过网关,需要做如下修改:

6. Seata:分布式事务

产生原因

一条连接只能操作一个数据库,但分布式情况下一个链接往往涉及多个数据库,共同控制多个数据库的提交回滚,比较麻烦,seata提供了在分布式场景下保证多个数据库一起提交回滚,从而达到数据一致性状态的一站式解决方案。

环境准备

为每个涉及数据库事务的业务模块做以下两步骤操作:(本案例中库存、下单、账户模块)

给实现方法加@Transactional注解

@Service
public class mpl implements StorageService {@AutowiredStorageTblMapper storageTblMapper;@Transactional@Overridepublic void deduct(String commodityCode, int count) {storageTblMapper.deduct(commodityCode, count);if (count == 5) {throw new RuntimeException("库存不足");}}
}

 给启动类加注解支持@EnableTransactionManagement

@EnableTransactionManagement
@MapperScan("com.atguigu.storage.mapper")
@EnableDiscoveryClient
@SpringBootApplication
public class SeataStorageMainApplication {public static void main(String[] args) {SpringApplication.run(SeataStorageMainApplication.class, args);}
}

以上只是单模块可以实现数据一致性,不能保证多个模块也能实现数据一致性,需要借助seata。

前提:需要引入OpenFeign做远程调用。

先为business的启动类加上@EnableFeignClients

@EnableFeignClients(basePackages = "com.atguigu.business.feign")
@EnableDiscoveryClient
@SpringBootApplication
public class SeataBusinessMainApplication {public static void main(String[] args) {SpringApplication.run(SeataBusinessMainApplication.class, args);}
}

在business模块为库存和订单模块创建feign客户端:

@FeignClient(value = "seata-order")
public interface OrderFeignClient {/*** 创建订单* @param userId* @param commodityCode* @param orderCount* @return*/@GetMapping("/create")String create(@RequestParam("userId") String userId,@RequestParam("commodityCode") String commodityCode,@RequestParam("count") int orderCount);
}
@FeignClient(value = "seata-storage")
public interface StorageFeignClient {/*** 扣减库存* @param commodityCode* @param count* @return*/@GetMapping("/deduct")String deduct(@RequestParam("commodityCode") String commodityCode,@RequestParam("count") Integer count);
}

order模块为account模块创建feign客户端:

@FeignClient(value = "seata-account")
public interface AccountFeignClient {/*** 扣减账户余额* @return*/@GetMapping("/debit")String debit(@RequestParam("userId") String userId,@RequestParam("money") int money);
}

order模块的启动类加上@EnableFeignClients

@EnableFeignClients(basePackages = "com.atguigu.order.feign")
@EnableTransactionManagement
@MapperScan("com.atguigu.order.mapper")
@EnableDiscoveryClient
@SpringBootApplication
public class SeataOrderMainApplication {public static void main(String[] args) {SpringApplication.run(SeataOrderMainApplication.class, args);}}

 业务代码:

business模块:调用了storage的扣减库存和order模块的创建订单

@Service
public class BusinessServiceImpl implements BusinessService {@AutowiredStorageFeignClient storageFeignClient;@AutowiredOrderFeignClient orderFeignClient;@GlobalTransactional@Overridepublic void purchase(String userId, String commodityCode, int orderCount) {//1. 扣减库存storageFeignClient.deduct(commodityCode, orderCount);//2. 创建订单orderFeignClient.create(userId, commodityCode, orderCount);}
}

order模块:调用了account模块的扣减账户

@Service
public class OrderServiceImpl implements OrderService {@AutowiredOrderTblMapper orderTblMapper;@AutowiredAccountFeignClient accountFeignClient;@Transactional@Overridepublic OrderTbl create(String userId, String commodityCode, int orderCount) {//1、计算订单价格int orderMoney = calculate(commodityCode, orderCount);//2、扣减账户余额accountFeignClient.debit(userId, orderMoney);//3、保存订单OrderTbl orderTbl = new OrderTbl();orderTbl.setUserId(userId);orderTbl.setCommodityCode(commodityCode);orderTbl.setCount(orderCount);orderTbl.setMoney(orderMoney);//3、保存订单orderTblMapper.insert(orderTbl);int i = 10/0;return orderTbl;}// 计算价格private int calculate(String commodityCode, int orderCount) {return 9*orderCount;}
}

storage模块:

@Service
public class mpl implements StorageService {@AutowiredStorageTblMapper storageTblMapper;@Transactional@Overridepublic void deduct(String commodityCode, int count) {storageTblMapper.deduct(commodityCode, count);if (count == 5) {throw new RuntimeException("库存不足");}}
}

account模块:

@Service
public class AccountServiceImpl implements AccountService {@AutowiredAccountTblMapper accountTblMapper;@Transactional  //本地事务@Overridepublic void debit(String userId, int money) {// 扣减账户余额accountTblMapper.debit(userId,money);}
}

为每个被调用模块配置日志记录,便于观察执行情况:

添加配置信息

logging:level:com.atguigu.order.feign: debug   # feign客户端所在的包名

配置类:

@Bean
Logger.Level feignLoggerLevel() {return Logger.Level.FULL;
}

原理

TC (Transaction Coordinator) - 事务协调者 维护全局和分支事务的状态,驱动全 局事务提交或回滚。(seata服务器)

TM (Transaction Manager) - 事务管理器 定义全局事务的范围:开始全局事务、 提交或回滚全局事务。(管理全局事务)

RM (Resource Manager) - 资 源管理器 管理分支事务处理的资源,与TC交谈 以注册分支事务和报告分支事务的状 态,并驱动分支事务提交或回滚。(控制本地事务)

项目引入Seata:

(主要步骤:引入seata服务器,加配置文件,加全局事务注解)

Seata服务器下载地址:

Seata Java Download | Apache Seata

解压后bin目录文件夹cmd,执行seata-server.bat启动服务器。

seata客户端页面:7091是web端口,8091是TC协调者端口

在每个微服务中引入seata依赖:去总项目pom文件引入的springcloud-alibaba依赖中找对应的版本的spring-cloud-starter-alibaba-seata依赖,然后放到pom文件中:

<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

每个使用seata的模块中引入配置文件:

 service {#transaction service group mappingvgroupMapping.default_tx_group = "default"#only support when registry.type=file, please don't set multiple addressesdefault.grouplist = "127.0.0.1:8091"#degrade, current not supportenableDegrade = false#disable seatadisableGlobalTransaction = false}

在最大(business)的方法(purchase)入口标注@GlobalTransactional

@Service
public class BusinessServiceImpl implements BusinessService {@AutowiredStorageFeignClient storageFeignClient;@AutowiredOrderFeignClient orderFeignClient;@GlobalTransactional@Overridepublic void purchase(String userId, String commodityCode, int orderCount) {//1. 扣减库存storageFeignClient.deduct(commodityCode, orderCount);//2. 创建订单orderFeignClient.create(userId, commodityCode, orderCount);}
}

二阶提交协议(AT模式)

在一阶进行本地事务提交的时候,为了防止并发,每个事务提交之前都要去seata申请要提交的事务的全局锁,这个全局锁是针对自己数据的,经度比较高。

undo_log

Seata四种模式

默认使用的是AT模式,可以通过配置文件更改模式:

seata:data-source-proxy-mode: XA

(1)AT模式

Seata AT 模式 | Apache Seata

(2)TCC模式

Seata TCC 模式 | Apache Seata

(3)Saga模式

Seata Saga 模式 | Apache Seata

(4)XA模式

Seata XA 模式 | Apache Seata

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词