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 复制代码
<?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://mave
 n.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.ver
 sion>
 </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文件中添加依赖:

XML 复制代码
 <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:引入服务发现依赖:

XML 复制代码
 <dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
 </dependency>

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

java 复制代码
@SpringBootApplication
 public 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 // 开启服务发现功能的核心注解

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

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

java 复制代码
@SpringBootTest
public class DiscoveryTest {
	@Autowired
	DiscoveryClient discoveryClient; // 或者使用NacosServiceDiscovery的api
	
	@Test
	void discoveryClientTest(){
		for (String service : discoveryClient.getServices()) {
			System.out.println("service = " + service);			
			//获取ip+port
			List<ServiceInstance> instances = discoveryClient.getInstances(ser
			for (ServiceInstance instance : instances) {
				System.out.println("ip:"+instance.getHost()+";"+"port = " + instance.getPort());
			}
		}
	}
}

运行结果:

远程调用 - 基本流程

远程调用 - 下单场景

配置 RestTemplate:

java 复制代码
@Configuration
public class UserConfiguration {
	@Bean
	RestTemplate restTemplate() {
		return new RestTemplate();
	}
}

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

java 复制代码
@Autowired
RestTemplate restTemplate;

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

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

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

小结:

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

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

远程调用 - 负载均衡

方法1:负载均衡依赖

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

XML 复制代码
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
java 复制代码
@Autowired
LoadBalancerClient loadBalancerClient;
@Autowired
DiscoveryClient discoveryClient;

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

方法2:注解式负载均衡

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

java 复制代码
@Configuration
public class UserConfiguration {
    @LoadBalanced
	@Bean
	RestTemplate restTemplate() {
		return new RestTemplate();
	}
}

订单模块中发起请求:

java 复制代码
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. 引入依赖(要刷新一下)

    XML 复制代码
    <dependency>
         <groupId>com.alibaba.cloud</groupId>
         <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
  3. application.properties配置

    XML 复制代码
    #指定配置中⼼地址
    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

java 复制代码
@RefreshScope//自动刷新
@RestController
public class OrderController {
	@Autowired
	OrderService 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;
	}
}

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

XML 复制代码
spring.cloud.nacos.config.import-check.enabled=false

② @ConfigurationProperties 无感自动刷新

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

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

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

③NacosConfigManager 监听配置变化

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

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

java 复制代码
@Bean
ApplicationRunner applicationRunner(NacosConfigManager manager){
	return args -> {
		ConfigService configService = manager.getConfigService();
		configService.addListener("service-order.properties", "DEFAULT_GROUP", new Listener() {
			@Override
			public Executor getExecutor() {
				return Executors.newFixedThreadPool(4);
			}
			
			@Override
			public 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

步骤:

① 引入依赖:

XML 复制代码
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

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

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

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

java 复制代码
@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:

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

OrderserviceImpl:

java 复制代码
@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模块)

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

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

java 复制代码
@RestController
public class ProductController {

    @Autowired
    ProductService productService;

    //查询商品
    @GetMapping("/product/{id}")
    public Product getProduct(@PathVariable("id") Long productId,
                              HttpServletRequest request){

        Product product = productService.getProductById(productId);
        return product;
    }
}
java 复制代码
public interface ProductService {
    Product getProductById(Long productId);
}
java 复制代码
@Service
public class ProductServiceImpl implements ProductService {
    @Override
    public 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

java 复制代码
@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);
}
java 复制代码
@SpringBootTest
public class WeatherTest {

    @Autowired
    WeatherFeignClient weatherFeignClient;
    @Test
    void 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:根据接口文档确定请求如何发

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

进阶 - 日志

配置文件中配置:

XML 复制代码
logging:
  level:
    com.atguigu.order.feign: debug   # feign客户端所在的包名

配置类中加入

java 复制代码
@Bean
Logger.Level feignLoggerLevel() {
	return Logger.Level.FULL;
}

进阶 - 超时控制

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

spring.profiles.active 是激活的环境

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

XML 复制代码
spring:
  cloud:
    openfeign:
      client:
        config:
          default:
            logger-level: full
            connect-timeout: 1000
            read-timeout: 2000
          service-product:
            logger-level: full
            connect-timeout: 3000
            read-timeout: 5000

进阶 - 重试机制

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

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

在配置类中加:

java 复制代码
@Bean
Retryer retryer(){
     return new Retryer.Default();
}

进阶 - 拦截器

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

java 复制代码
@Component
public class XTokenRequestInterceptor implements RequestInterceptor {

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

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

进阶用法 - Fallback兜底返回

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

引入sentinel的依赖

XML 复制代码
<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

开启熔断:

XML 复制代码
feign:
  sentinel:
    enabled: true

写fallback函数:

java 复制代码
@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);
}
java 复制代码
@Component
public class ProductFeignClientFallback implements ProductFeignClient {
    @Override
    public 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依赖:

XML 复制代码
<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

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

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

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

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

异常处理

① 自定义BlockExceptionHandler(web接口)

java 复制代码
@Component
public class MyBlockExceptionHandler implements BlockExceptionHandler {
    private ObjectMapper objectMapper = new ObjectMapper();
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response,
                       String resourceName, BlockException e) throws Exception {
        response.setStatus(429); //too many requests
        response.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)

java 复制代码
@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错误页面

java 复制代码
@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);
}
java 复制代码
@Component
public class ProductFeignClientFallback implements ProductFeignClient {
    @Override
    public 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硬编码

java 复制代码
@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)- 流控模式

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

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

java 复制代码
//创建订单
@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;
}

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

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

java 复制代码
@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号是下架商品,不允许访问

java 复制代码
@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为服务模块,并引入相关依赖:

XML 复制代码
<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

XML 复制代码
spring:
  profiles:
    include: route
  application:
    name: gateway
  cloud:
    nacos:
      server-addr: 127.0.0.1:8848

# localhost/api/order
server:
  port: 80

application-gateway.yml

XML 复制代码
spring:
  cloud:
    gateway:
      routes:
      - id: order
        uri: lb://service-order
        predicates:
         - Path=/api/order/**
      - id: product
        uri: lb://service-product
        predicates:
         - 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是自定义工厂的前缀

java 复制代码
@Component
public class VipRoutePredicateFactory extends AbstractRoutePredicateFactory<VipRoutePredicateFactory.Config> {


    public VipRoutePredicateFactory() {
        super(Config.class);
    }

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        return new GatewayPredicate() {
            @Override
            public boolean test(ServerWebExchange serverWebExchange) {
                // localhost/search?q=haha&user=leifengyang
                ServerHttpRequest request = serverWebExchange.getRequest();

                String first = request.getQueryParams().getFirst(config.param);

                return StringUtils.hasText(first) && first.equals(config.value);
            }
        };
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("param", "value");
    }

    /**
     * 可以配置的参数
     */
    @Validated
    public static class Config {

        @NotEmpty
        private String param;


        @NotEmpty
        private 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: Path
  args:
	patterns: /api/order/**
	matchTrailingSlash: true
filters:
- RewritePath=/api/order/?(?<segment>.*), /$\{segment}
- OnceToken=X-Response-Token, jwt

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

过滤器其他功能:

添加响应头的过滤器

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

默认Filter

XML 复制代码
default-filters:
	- AddResponseHeader=X-Response-Abc, 123

全局Filter

java 复制代码
@Component
@Slf4j
public class RtGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public 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);
                }); //放行   10s
        return filter;
    }

    @Override
    public int getOrder() {
        return 0; // 数字越小,优先级越高
    }
}

自定义过滤器工厂

java 复制代码
@Component
public class OnceTokenGatewayFilterFactory extends AbstractNameValueGatewayFilterFactory {
    @Override
    public GatewayFilter apply(NameValueConfig config) {
        return new GatewayFilter() {
            @Override
            public 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注解

java 复制代码
@Service
public class mpl implements StorageService {

    @Autowired
    StorageTblMapper storageTblMapper;

    @Transactional
    @Override
    public void deduct(String commodityCode, int count) {
        storageTblMapper.deduct(commodityCode, count);
        if (count == 5) {
            throw new RuntimeException("库存不足");
        }
    }
}

给启动类加注解支持@EnableTransactionManagement

java 复制代码
@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

java 复制代码
@EnableFeignClients(basePackages = "com.atguigu.business.feign")
@EnableDiscoveryClient
@SpringBootApplication
public class SeataBusinessMainApplication {

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

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

java 复制代码
@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);
}
java 复制代码
@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客户端:

java 复制代码
@FeignClient(value = "seata-account")
public interface AccountFeignClient {

    /**
     * 扣减账户余额
     * @return
     */
    @GetMapping("/debit")
    String debit(@RequestParam("userId") String userId,
                        @RequestParam("money") int money);
}

order模块的启动类加上@EnableFeignClients

java 复制代码
@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模块的创建订单

java 复制代码
@Service
public class BusinessServiceImpl implements BusinessService {

    @Autowired
    StorageFeignClient storageFeignClient;

    @Autowired
    OrderFeignClient orderFeignClient;




    @GlobalTransactional
    @Override
    public void purchase(String userId, String commodityCode, int orderCount) {
        //1. 扣减库存
        storageFeignClient.deduct(commodityCode, orderCount);

        //2. 创建订单
        orderFeignClient.create(userId, commodityCode, orderCount);
    }
}

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

java 复制代码
@Service
public class OrderServiceImpl implements OrderService {

    @Autowired
    OrderTblMapper orderTblMapper;



    @Autowired
    AccountFeignClient accountFeignClient;

    @Transactional
    @Override
    public 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模块:

java 复制代码
@Service
public class mpl implements StorageService {

    @Autowired
    StorageTblMapper storageTblMapper;



    @Transactional
    @Override
    public void deduct(String commodityCode, int count) {
        storageTblMapper.deduct(commodityCode, count);
        if (count == 5) {
            throw new RuntimeException("库存不足");
        }
    }
}

account模块:

java 复制代码
@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    AccountTblMapper accountTblMapper;

    @Transactional  //本地事务
    @Override
    public void debit(String userId, int money) {
        // 扣减账户余额
        accountTblMapper.debit(userId,money);
    }
}

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

添加配置信息

XML 复制代码
logging:
  level:
    com.atguigu.order.feign: debug   # feign客户端所在的包名

配置类:

java 复制代码
@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文件中:

java 复制代码
<dependency>
	<groupId>com.alibaba.cloud</groupId>
	<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>

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

java 复制代码
 service {
   #transaction service group mapping
   vgroupMapping.default_tx_group = "default"
   #only support when registry.type=file, please don't set multiple addresses
   default.grouplist = "127.0.0.1:8091"
   #degrade, current not support
   enableDegrade = false
   #disable seata
   disableGlobalTransaction = false
 }

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

java 复制代码
@Service
public class BusinessServiceImpl implements BusinessService {

    @Autowired
    StorageFeignClient storageFeignClient;

    @Autowired
    OrderFeignClient orderFeignClient;
    
    @GlobalTransactional
    @Override
    public 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模式,可以通过配置文件更改模式:

java 复制代码
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

相关推荐
君若雅2 分钟前
我如何借助 Trae 三分钟搞定开源项目中的隐藏 BUG
java·后端·trae
汪子熙16 分钟前
浅谈笔者对 AI 技术降低软件项目开发成本的一些思考
后端
IT-ZXT88823 分钟前
Spring 框架之IOC容器加载重要组件
java·后端·spring
用户67570498850226 分钟前
HTTP2.0 从原理到实践,保证把你治得服服帖帖!
后端
noravinsc1 小时前
django paramiko 跳转登录
后端·python·django
声声codeGrandMaster1 小时前
Django之表格上传
后端·python·django
ghie90901 小时前
SpringCloud-基于SpringAMQP实现消息队列
后端·spring cloud·ruby
程序员葵安2 小时前
【Java Web】9.Maven高级
java·数据库·后端·maven
程序员爱钓鱼2 小时前
Go 并发编程基础:通道(Channel)的使用
后端·google·go
FogLetter2 小时前
JavaScript 内存探秘:栈与堆的奇幻之旅
javascript·后端