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

相关推荐
M1A12 小时前
小红书重磅升级!公众号文章一键导入,深度内容轻松入驻
后端
0wioiw03 小时前
Go基础(④指针)
开发语言·后端·golang
DKPT3 小时前
JVM中如何调优新生代和老生代?
java·jvm·笔记·学习·spring
李姆斯4 小时前
复盘上瘾症:到底什么时候该“复盘”,什么时候不需要“复盘”
前端·后端·团队管理
javachen__4 小时前
Spring Boot配置error日志发送至企业微信
spring boot·后端·企业微信
seabirdssss5 小时前
使用Spring Boot DevTools快速重启功能
java·spring boot·后端
喂完待续5 小时前
【序列晋升】29 Spring Cloud Task 微服务架构下的轻量级任务调度框架
java·spring·spring cloud·云原生·架构·big data·序列晋升
OC溥哥9996 小时前
Flask论坛与个人中心页面开发教程完整详细版
后端·python·flask·html
Volunteer Technology7 小时前
三高项目-缓存设计
java·spring·缓存·高并发·高可用·高数据量
迷知悟道8 小时前
java面向对象四大核心特征之抽象---超详细(保姆级)
java·后端