SpringCloud

springCloud

单体应用

单体应用的特点

  1. 单体应用就是传统意义的,单个应用程序的应用
  2. 单体软件一般采用,分包的方式,来实现代码的解耦和管理
  3. 单体应用一般分为MVC三层架构,也可以分成表现层,业务层,持久层。本身起到一定的代码分割管理、方便维护
  4. 单体应用的特点是整个应用其实是一个web项目,是一个工程,运行在JVM(java虚拟机)
  5. 在单体应用中,springMvc(或者servlet)充当控制层 ,mabatis(或者JDBC) 充当持久层,Spring则充当整合表现层、业务层、持久层的作用

单体应用的缺陷

  1. 当项目越来越大,代码量越来越多,造成编译、打包费时,越来越影响效率。
  2. 当业务越来越多,不同的业务会重建新的项目,不同的项目的功能模块可能会出现重复建设的情况,造成浪费。
  3. 可伸缩性差. 单体应用中的功能模块的使用场景,并发量,消耗的资源类型各有不同(例如一个电商项目中,商品模块消耗的支援大,其他模块相对较少),对于资源的利用又互相影响,单体应用无法做到按照模块需求分配资源
  4. 系统错误隔离性差,可用性差.任何一个模块的错误均可能造成整个系统的宕机.

架构演变

SOA

  1. 定义

    • SOA是面向服务的架构。是一种软件开发模式,它允许不同的服务通过服务接口进行可重用和互操作的组合,形成应用程序。列如一个项目中新增当地天气情况image-20231117193137836

    • SOA其实就是约定了一种规范(如何调用PI)

    • SOA是一种粗粒度、松耦合服务架构,服务之间通过简单、精确定义接口进行通讯,不涉及底层编程接口和通讯模型。SOA可以看作是B/S模型、XML(标准通用标记语言的子集)/Web Service技术之后的自然延伸

  2. 分布式架构

    对一个复杂的业务系统进行垂直分层,每个垂直应用实际上是一个独立的子系统,它们共同组成了整个应用系统,这些子系统可以部署在不同的服务器上,这些服务器可以在不同的地域,当分布式应用之间的调用越来越多,整个系统的复杂度急剧上升,SOA可以降低分布式应用之间的耦合。

  3. ESB服务总线 原理图image-20231116225828579

  4. 面向服务的架构思想

    • SAOP,REST,RPC 就是SOA指定的规范(落地方案)解决项目之前如何传送数据
    1. 基于http+xml(内容) -> SOAP(WEB service)->缺点:数据冗余
    2. 基于http+json->REST springCloud ->缺点 :安全性不高
    3. 基于Socker+文本 /二进制数据 -> dubbo 基于RPC->用字节流传输

Web server

  1. 定义:

    Web Service是一个平台独立的,低耦合的,自包含的、基于可编程的web的应用程序,可使用开放的XML(标准通用标记语言下的一个子集)标准来描述、发布、发现、协调和配置这些应用程序,用于开发分布式的交互操作的应用程序

  2. 举例:调用天气信息

    WeatherWS Web 服务 (webxml.com.cn)

    返回的数据:

    <ArrayOfString xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns="http://WebXml.com.cn/">
    <string>内蒙古 呼和浩特</string>
    <string>呼和浩特</string>
    <string>355</string>
    <string>2023/11/17 19:31:52</string>
    <string>今日天气实况:气温:-2.5℃;风向/风力:西北风 2级;湿度:43%</string>
    <string>紫外线强度:强。</string>
    <string>感冒指数:易发,天冷易感冒,注意防范。 运动指数:较不宜,天气寒冷,推荐您进行室内运动。 过敏指数:极不易发,无需担心过敏,可放心外出,享受生活。 穿衣指数:冷,建议着棉衣加羊毛衫等冬季服装。 洗车指数:适宜,天气较好,适合擦洗汽车。 紫外线指数:强,涂擦SPF大于15、PA+防晒护肤品。 </string>
    <string>11月17日 晴</string>
    <string>-8℃/5℃</string>
    <string>西风转西南风小于3级</string>
    <string>0.gif</string>
    <string>0.gif</string>
    </ArrayOfString>
  3. 技术栈

    • XML(标准通用标记语言下的一个子集):XML是在web上传送结构化数据的伟大方式,Web services要以一种可靠的自动的方式操作数据,HTML(标准通用标记语言下的一个应用)不会满足要求,而XML可以使web services十分方便的处理数据,它的内容与表示的分离十分理想
    • SOAP:SOAP使用XML消息调用远程方法,这样web services可以通过HTTP协议的post和get方法与远程机器交互,而且,SOAP更加健壮和灵活易用
    • Web Service描述语言WSDL 就是用机器能阅读的方式提供的一个正式描述文档而基于XML(标准通用标记语言下的一个子集)的语言,用于描述Web Service及其函数、参数和返回值。因为是基于XML的,所以WSDL既是机器可阅读的,又是人可阅读的。

微服务

微服务是SOA的一种落地方案,SOA是一种面向服务的架构思想,微服务也同样推崇这种思想.

微服务架构

image-20231121200151730

前提:要有粗略的框架(脚手架)->spring boot

为什么微服务要结合云技术

什么是云

云服务是一种通过互联网提供各种资源和服务的模式,例如计算、存储、数据库、分析、机器学习等。云服务可以根据用户的需求动态地扩展或缩减,而无需管理底层的硬件或软件

云服务的种类
  1. 基础设施即服务(IaaS):提供简化的基础设施管理,大规模的水平可伸缩性,通过地理分布实现高冗余,可以跨多个云供应商进行移植并且允许开发人员通过产品覆盖更广泛的受众。
  2. 平台即服务 (PaaS)
  3. 软件即服务 ( SaaS)
  4. 区别 各种服务暴露的情况image-20231116231152480
云和微服务
  • 核心概念:就是每个服务都被打包和部署为离散的独立程序。服务实例应迅速启动,服务的每一个实例都是完全相同的。所以服务都将部署到以下某个环境之中.
  • 优点:能够快速启动和关闭微服务实例,以响应可伸缩性和服务故障事件. 简单来说就是快速部署
  • 基于云的微服务以弹性的概念为中心。在需要时,可以在云上几分钟之内快速布置启动新的虚拟机和容器,如服务需求下降,可以关闭虚拟服务器。这样可显著提高应用程序的水平可伸缩性,也使应用程序更有弹性.

微服务开发要点

  • 位置透明

    微服务之间的通信不依赖具体的物理位置,而是通过服务注册和发现机制,动态的获取服务的地址和端口 解决方案 eurka、nacos

  • 有弹性/可伸缩

    指微服务应该具备在面对各种故障和压力时,能够保持正常运行或快速恢复的能力。

    • 服务的可观察性:能够通过日志、监控、跟踪等手段,实时了解服务的运行状态、性能指标、异常情况等,以便及时发现和定位问题。
    • 服务的容错性:即能够通过熔断、降级、重试等策略,避免单个服务的故障导致整个系统的不可用或性能下降(服务器负载不起,拒绝一些请求,或者告诉请求方多长时间后处理好)
    • 服务的伸缩性:能够通过负载均衡、自动扩缩容等机制,根据请求量的变化,动态调整服务的数量和分配,以满足不同的负载需求。
    • 服务的可恢复性:即能够通过备份、回滚、灾备等措施,在发生严重故障时,能够快速恢复服务的正常运行。
  • 可重复

    将这些公共的部分抽取出来,封装成公共模块或服务(产品,API),供其他模块或服务调用。这样可以提高代码的复用性、可维护性和一致性,减少冗余和错误。

微服务模式

  • 路由模式: 简单来说就是请求过来之后定位到那个服务上面去,给这个请求找条路

    • 服务注册 / 服务发现:微服务容易被发现,并且服务的位置不硬编码到应用程序中还可以找到他
    • 服务路由:为所有服务提供单点入口,以便将安全策略和路由规则统一应用于微服务应用程序中的多个服务实例
  • 弹性模式:简单来说就是服务数量可多可少,服务质量可高可低

    • 客户端负载均衡:在服务客户端上缓存服务实例的位置,以便对微服务的多个实例的调用负载均衡到该微服务的所有健康实例(Ribbon)
    • 断路器模式:阻止客户继续调用出现故障的或遭遇性能问题的服务。出现故障的微服务调用能够快速失败,以便主叫客户端可以快速响应并采取适当的措施。(Hystrix)
    • 后备模式:当服务调用失败,提供插件机制,允许服务的客户端尝试通过调用微服务之外的其他方法来执行工作。(Hystrix fallback)
    • 舱壁模式:微服务应用程序使用多个分布式资源来执行工作。一个服务不能调用不会影响其他的服务(Docker)
  • 安全模式:

    • 验证:确定服务的客户端就是它们声称的那个主体
    • 授权:确定调用微服务的客户端是否允许执行它们正在进行的操作
    • 凭证管理和传播:避免客户端每次都要提供凭证信息才能访问事务中涉及的服务调用
    • 使用基于令牌的安全方案,可以实现服务验证和授权,而无需传递客户端凭证
  • 日志记录和跟踪模式:

    • 日志:将服务所生成的日志关联到一起,一个节点可以看到所有的日志

    • 跟踪:知道是那个服务环节出现问题

特点

  1. 微服务是一种架构风格,将一个复杂的应用拆分成多个独立自治的服务,服务与服务间通过松耦合的形式交互,通常采用轻量级的协议

  2. 微服务遵循单一职责原则,每个服务只负责一个业务领域或功能,服务的粒度较小,便于开发、测试和部署。

  3. 微服务支持技术异构性,不同的服务可以采用不同的编程语言、框架和数据存储技术,根据业务需求和团队能力进行选择。

  4. 微服务具有高可扩展性,可以根据不同服务的负载情况进行水平扩展或缩减,提高资源利用率和性能。

  5. 微服务具有高可靠性,一个服务的故障不会影响整个系统的运行,可以通过隔离、熔断等机制提高系统的容错能力。

  6. 微服务具有灵活组合性,可以通过组合已有的服务来实现新的功能或业务场景,提高复用性和开发效率。

优势

  1. 可以提高开发效率和部署速度
  2. 可以提高系统的可靠性和容错性
  3. 微服务架构可以提高系统的可扩展性和性能
  4. 可以提高系统的技术多样性和创新性
  5. 提高系统的灵活组合性和复用性

挑战

  1. 服务太多,依赖复杂,运维难度大
  2. 运维复杂度增加,部署服务数量多,监控进程多导致整体运维复杂度提升

SpringCloud简介

  1. cloud与微服务的关系:微服务是思想, cloud是解决方案

  2. cloud是一系列框架的集合

    版本 cloud 2 cloud1
    服务注册与发现 nacos eureka
    服务远程调用 openFeign Feign
    服务降级\服务熔断\服务限流 sentinel hystrix
    分布式事务 seata \
    配置中心 nacos spring cloud config
    总线 nacos stream
    网关 gateway gateway
    链路追踪 sleuth sleuth
    …… …… ……

服务注册与发现image-20231117212737222

CAP

  • 一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)和分区容错性(Partition tolerance)这三项中的两项 分布式系统一定满足分区容错性image-20231117223241076
  • 一致性是指所有节点在同一时间看到的数据是相同的,即数据的正确性和一致性得到保证
  • 可用性是指每个请求都能得到响应,不会出现错误或超时,即服务的可靠性和响应性得到保证
  • 分区容错性是指当网络发生故障或延迟时,系统仍然能够继续运行,不会挂掉,即系统的稳定性和容错性得到保证
  • 对于涉及到钱财等敏感数据的场景,需要保证一致性C和分区容错性P(牺牲可用性)
  • 对于大型互联网应用的场景,需要保证可用性A和分区容错性P(牺牲一致性),并尽可能实现最终一致性

常见产品的区分

image-20231117221644728

Nacos

注册服务发现

作用:

每个系统引入注册中心后,可以把自己的信息注册到nacos ,我们假设订单系统要去调用库存系统,订单系统可以拿到库存系统对应服务的名称,而库存系统在注册中心注册了自己的信息,订单系统可以通过名称去拿到库存系统服务的端口号和ip地址,再通过负载均衡或长轮询去选择用那个

image-20231121203501545

官方文档:https://nacos.io/zh-cn/docs/architecture.html

启动nacos:命令窗口进入nacos的bin目录下 执行

startup.cmd -m standalone
--默认为集群方式打开 standalone为单体打开

image-20231117230303166

可通过localhost:8848/nacos 进入他的页面

nacos服务的工作流程

  • 配置管理:应用或服务可以向Nacos服务器发布或获取配置信息,如应用参数、路由规则、灰度策略等。Nacos服务器会保存配置信息到内存或数据库中,并根据长轮询的方式推送配置变更通知。长轮询:长轮询是一种服务器选择尽可能长的时间保持和客户端连接打开的技术,仅在数据变得可用或达到超时阙值后才提供响应

  • 如何通过http 发布或者获取配置

    发布:
    curl -X POST “http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=nacos.cfg.dataId&group=test&content=HelloWorld

    获取:curl -X GET “http://127.0.0.1:8848/nacos/v1/cs/configs?dataId=nacos.cfg.dataId&group=test

  • DNS服务:应用或服务可以向Nacos服务器查询域名对应的IP列表,实现基于DNS协议的服务发现和负载均衡。Nacos服务器会根据权重路由等策略返回IP列表,并缓存DNS记录。

  • 元数据管理:应用或服务可以向Nacos服务器查询或更新元数据信息,如服务描述、生命周期、依赖关系、健康状态、流量管理等。Nacos服务器会提供可视化的仪表盘和API来管理元数据信息。

  • 服务注册发现:

    • 服务注册:Nacos Client会通过发送REST请求的方式向Nacos Server注册自己的服务,提供自身的元数据,比如IP地址、端口等信息。Nacos Server接收到注册请求后,就会把这些元数据信息存储在一个双层的内测Map中。

      curl -X POST 'http://127.0.0.1:8848/nacos/v1/ns/instance?serviceName=nacos.naming.serviceName&ip=20.18.7.10&port=8080'
    • 服务心跳-检测:在服务注册后,Nacos Client会维护一个定时心跳来持续通知Nacos Server,说明服务一直处于可用状态,防止被剔除。默认5s发送一次心跳。

    • 服务同步:Nacos Server集群之间会互相同步服务实例,用来保证服务信息的一致性。Nacos Server 集群是为了防止单点故障image-20231118170433327

    • 服务发现:服务消费者(Nacos Client)在调用服务提供者的服务时,会发送一个REST请NacosServer,获取上面注册的服务清单,并且缓存在NacosClient本地,同时会在Nacos Client本地开启一个定时任务定时拉取服务端最新的注册表信息更新到本地缓存image-20231118171332891
      Spring

    • 服务健康检查:Nacos Server会开启一个定时任务用来检查注册服务实例的健康情况,对于超过15s没有收到客户端心跳的实例会将它的healthy属性置为false(客户端服务发现时不会发现),如果某个实例超过30秒没有收到心跳,直接剔除该实例(被剔除的实例如果恢复发送心跳则会重新注册)

spring cloud alibaba版本必须依赖spring cloud版本,可在官方文档中查找

Spring Boot

Spring Cloud

start.spring.io/actuator/info

版本说明 · alibaba/spring-cloud-alibaba Wiki (github.com)

使用注册中心

  • 在cloud项目中导入依赖
 <dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
  • 注意:这里因为依赖仲裁,所以不需要指定版本
  • 因为他是一个starter,肯定有配置项,可以参考官方文档去设置
server:
port: 9004 #指定服务端口号
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848 #nacos端口号
application:
name: resorder #注册中心的名字
  • 在要注册的启动类上加入注解
@EnableDiscoveryClient
  • 一个类启多个服务怎么搞?增加Boot配置后 给他加一个虚拟机配置image-20231123205706183
-Dserver.port=9001 --修改端口即可启动多个服务
  • 我们通过nacos暴露的端口在注册中心查看image-20231123205813238
  • 构建一个消费模块 同样需要注册和加入注解
  • 将RestTemplate交给Spring托管
package com.y.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

/**
* @program: cloud-demo
* @description: 获取注册的服务
* @author: ChestnutDuck
* @create: 2023-11-23 19:45
**/
@Configuration
public class webconfig {
@LoadBalanced //负载均衡 我们有两个服务节点 通过这个去处理
@Bean
public RestTemplate restTemplate(){
return new RestTemplate(); //底层其实就是发请求给对应的服务器
}
}
  • 将RestTemplate注入到对应的控制类 调用其中的方法去发请求
package com.y.wen.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;

/**
* @program: cloud-demo
* @description: 消费端测试访问resfood
* @author: ChestnutDuck
* @create: 2023-11-23 19:32
**/
@RequestMapping("order")
@RestController
@Slf4j
public class ordercontoller {
@Autowired
public RestTemplate restTemplate;
@RequestMapping(value = "addCart",method = {RequestMethod.POST})
public Map<String, Object> addCart(@RequestParam Integer fid, @RequestParam Integer num, HttpSession httpSession){
Map<String,Object> map=new HashMap<>();
Map <String,Object> result = this.restTemplate.getForObject("http://localhost:9001/food/findByFid?fid=" + fid,Map.class );
log.info("发送请求后得到商品信息"+result);
return map;
}
}
  • 想看发送的信息可以修改日志级别,你要看内个日志的信息,可以看这

    个类在那个包下面 去配置文件中配置列如 RestTemplate在package org.springframework.web.client这个包中,配置信息如下:

logging:
level:
root: info
org.springframework.web.client: debug
org.apache: error
file:
path: logs #日志信息

loadbalance(负载均衡)

  • 负载均衡是对系统的高可用、网络峰值压力的缓解和处理能力扩容的重要手段之一,是分布式系统基础架构
  • loadbalancer使用Reactor模式实现响应式编程,提高了性能和可扩展性
  • loadbalancer提供的负载均衡有2种,默认的轮询(RoundRobinLoadBalancer)和随机(RandomLoadBalancer)

实现原理

  • 维护一个下挂可用的服务端清单,通过心跳检测来剔除故障的服务端节点以保证清单中都是可以正常访问的服务端节点。当客户端发送请求到负载均衡设备的时候,该设备按某种算法(比如线性轮询、按权重负载、按流量负载等)从维护的可用服务端清单中取出一台服务端端地址,然后进行转发.

负载均衡两种实现方法

  • 服务器端负载均衡:nginx 自身保存服务清单, 客户端的所有请求统一交给 nginx,由 nginx 进行实现负载均衡请求转发
  • 客户端负载均衡:LoadBalancer 是从 Nacos 注册中心服务器端上获取服务注册信息列表,缓存到本地,然后在本地实现负载均衡策略image-20231125143553749

使用

  • 对全局( 所有服务 )RestTemplate注解@LoadBalanced
@Configuration
public class webconfig {
@LoadBalanced //负载均衡 我们有两个服务节点 通过这个去处理
@Bean
public RestTemplate restTemplate(){
return new RestTemplate(); //底层其实就是发请求给对应的服务器
}
}
  • 导入loadbalance的依赖,父容器托管不需要版本
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
  • 将之前写死的固定Ip和端口号访问的方式进行改进
@RequestMapping("order")
@RestController
@Slf4j
public class ordercontoller {
@Autowired
public RestTemplate restTemplate; //springboot里的
@RequestMapping(value = "addCart",method = {RequestMethod.POST})
public Map<String, Object> addCart(@RequestParam Integer fid, @RequestParam Integer num, HttpSession httpSession){
String url="http://resfood/food/findByFid?fid=";
// 服务名 请求前缀 参数(根据自己传参的方式选择是否加这个)
Map<String,Object> map=new HashMap<>();
// http://192.168.118.162:9000/food/findByFid?fid= 使用ip和端口固定访问一个服务
Map <String,Object> result = this.restTemplate.getForObject(url + fid,Map.class );
log.info("发送请求后得到商品信息"+result);
return map;
}
}
  • 我们再去发请求,就会发现它按轮询的方式调用

loadbalance是如挂载的

loladbalance源码分析

创建负载均衡

原理已经了解了,下面仿照源码实现一个简单的负载均衡

  1. 实现ReactorServiceInstanceLoadBalancer 接口,实现负载均衡算法,下面基本是复制官方的代码,讲一下如何实现
package com.y.config;

import org.springframework.beans.factory.ObjectProvider;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
import org.springframework.cloud.client.loadbalancer.EmptyResponse;
import org.springframework.cloud.client.loadbalancer.Request;
import org.springframework.cloud.client.loadbalancer.Response;
import org.springframework.cloud.loadbalancer.core.NoopServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.SelectedInstanceCallback;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import reactor.core.publisher.Mono;

import java.util.List;
import java.util.Random;

/**
* @program: cloud-demo
* @description: 参照官方定义的类自定义的负载均衡器
* @author: ChestnutDuck
* @create: 2023-11-24 20:50
**/
public class OnlyOneLoadBalancer implements ReactorServiceInstanceLoadBalancer {
String serviceId;
ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;
public OnlyOneLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider){
this.serviceInstanceListSupplierProvider=serviceInstanceListSupplierProvider;
}
public OnlyOneLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider, String serviceId) {
this.serviceId=serviceId;
this.serviceInstanceListSupplierProvider=serviceInstanceListSupplierProvider;
}

@Override
public Mono<Response<ServiceInstance>> choose(Request request) {
ServiceInstanceListSupplier supplier = (ServiceInstanceListSupplier)this.serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);
return supplier.get(request).next().map((serviceInstances) -> {
return this.processInstanceResponse(supplier, serviceInstances);
});
}

private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances) {
Response<ServiceInstance> serviceInstanceResponse = this.getInstanceResponse(serviceInstances);
if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {
((SelectedInstanceCallback)supplier).selectedServiceInstance((ServiceInstance)serviceInstanceResponse.getServer());
}

return serviceInstanceResponse;
}

private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances) {
if (instances.isEmpty()) {
return new EmptyResponse();
} else {
ServiceInstance instance = (ServiceInstance)instances.get(0);
return new DefaultResponse(instance);
}
}
}
  1. 托管这个类,仿照LoadBalancerClientFactory这个类
package com.y.config;

import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClientConfiguration;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
import org.springframework.cloud.loadbalancer.core.ReactorServiceInstanceLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

/**
* @program: cloud-demo
* @description: 托管
* @author: ChestnutDuck
* @create: 2023-11-25 08:49
**/
@Configuration

public class MyLoadBalanceConfig {
@Bean
public ReactorServiceInstanceLoadBalancer myOnlyOneReactorServiceInstanceLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory) {
String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);
//随机RandomLoadBalancer;轮询RoundRobinLoadBalancer
return new OnlyOneLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class));
}
}
  1. 要用就要去配置 要在使用了@loadbalancer注解的类上配置两种方案

    • 全局配置负载均衡器策略
    @LoadBalancerClient(name = "resfood", configuration = MyLoadBalanceConfig.class)
    //name 对应注册的服务名称
    //MyLoadBalanceConfig 是托管负载均衡器的配置类
    • 对每个服务分别指定负载均衡器策略
@LoadBalancerClients(
value = { @LoadBalancerClient(value = "resfood", configuration = MyLoadBalanceConfig.class)
}, defaultConfiguration = LoadBalancerClientConfiguration.class
)
//defaultConfiguration 默认方案使用轮询

openfeign

  • RestTemplate的不足:我们之前发请求时是将请求路径写死,这种方式 编译成字节码文件后无法修改,且编写相对繁琐,我们应该用面向对象的方式去实现 ,openfeign帮我们解决这个问题
  • 之前用RestTemplate 底层就是发一个请求,url还需要我们自己编写image-20231125164540037
  • 而openfeign 只需要定义服务接口,他会生成对应的代理对象image-20231125164646214
  • openfeign 会将服务的相应转换成Java对象并返回
  • Spring Cloud OpenFeign是一个声明式的REST客户端,它可以让我们用注解的方式来调用其他服务的接口。它基于Feign,并支持Spring MVC的注解和Spring Web的HttpMessageConverters。

动态代理补充

在测试类中使用会生成代理对象的字节码文件

System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
package com.y;

/**
* @program: cloud-demo
* @description:
* @author: ChestnutDuck
* @create: 2023-11-25 10:04
**/
public class Test {
public static void main(String[] args) {
System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true");
Hello target=new HelloImpl();//目标类
CustomInvocationHandler handler=new CustomInvocationHandler(target);
//生成代理对象
Object proxy=handler.createProxy();
System.out.println(proxy); //Proxy代理对象
Hello hi=(Hello) proxy ;//hi==proxy
hi.sayHello();
hi.sayBye();
}
}

会在根目录下生成代理对象的字节码文件和他的目录com/sun/porxy

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package com.sun.proxy;

import com.y.Hello;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.lang.reflect.UndeclaredThrowableException;

public final class $Proxy0 extends Proxy implements Hello {
private static Method m1; //这是被代理接口的方法,接口有几个代理对象里就有几个
private static Method m4;
private static Method m2;
private static Method m3;
private static Method m0;

public $Proxy0(InvocationHandler var1) throws {
super(var1);
}

public final boolean equals(Object var1) throws {
try {
return (Boolean)super.h.invoke(this, m1, new Object[]{var1});

} catch (RuntimeException | Error var3) {
throw var3;
} catch (Throwable var4) {
throw new UndeclaredThrowableException(var4);
}
}

public final void sayHello() throws {
try {
super.h.invoke(this, m4, (Object[])null);
//这里的h就是传进来的InvocationHandler对象
//调用的其实还是 CustomInvocationHandler的invoke
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final String toString() throws {
try {
return (String)super.h.invoke(this, m2, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final void sayBye() throws {
try {
super.h.invoke(this, m3, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

public final int hashCode() throws {
try {
return (Integer)super.h.invoke(this, m0, (Object[])null);
} catch (RuntimeException | Error var2) {
throw var2;
} catch (Throwable var3) {
throw new UndeclaredThrowableException(var3);
}
}

static {
try {
m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
m4 = Class.forName("com.y.Hello").getMethod("sayHello");
m2 = Class.forName("java.lang.Object").getMethod("toString");
m3 = Class.forName("com.y.Hello").getMethod("sayBye");
m0 = Class.forName("java.lang.Object").getMethod("hashCode");
} catch (NoSuchMethodException var2) {
throw new NoSuchMethodError(var2.getMessage());
} catch (ClassNotFoundException var3) {
throw new NoClassDefFoundError(var3.getMessage());
}
}
}

什么是Feign

  • 是一个 Http 请求调用的轻量级框架,可以以 Java 接口注解的方式调用 Http 请求,而不用像 Java 中通过封装 HTTP 请求报文的方式直接调用。
  • 通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的请求,这种请求相对而言比较直观。

解决了什么问题

  • 封装 HTTP 调用流程,面向接口编程但做了大量的适配工作

feign 声明式注解

Annotation Interface Target Usage
@RequestLine Method 定义HttpMethod 和 UriTemplate. UriTemplate 中使用{} 包裹的表达式,可以通过在方法参数上使用@Param 自动注入
@Param Parameter 定义模板变量,模板变量的值可以使用名称的方式使用模板注入解析
@Headers Method, Type 定义头部模板变量,使用@Param 注解提供参数值的注入。如果该注解添加在接口类上,则所有的请求都会携带对应的Header信息;如果在方法上,则只会添加到对应的方法请求上
@QueryMap Parameter 定义一个Map或 POJO,参数值将会被转换成URL上的 query 字符串上
@HeaderMap Parameter Map ->Http Headers
@Body Method 定义一个模板,类似于UriTemplate和HeaderTemplate,它使用@Param注释值来解析相应的表达式。
public interface FeignService {
// @Headers
@RequestLine("GET /api/documents/{contentType}")
@Headers("Accept: {contentType}")
String getDocumentByType(@Param("contentType") String type);

// @QueryMap: Map or POJO
@RequestLine("GET /find")
V find(@QueryMap Map<String, Object> queryMap);
@RequestLine("GET /find")
V find(@QueryMap CustomPojo customPojo);

// @HeaderMap: Map
@RequestLine("POST /")
void post(@HeaderMap Map<String, Object> headerMap);

// @Body
@RequestLine("POST /")
@Headers("Content-Type: application/xml")
@Body("<login \"user_name\"=\"{user_name}\" \"password\"=\"{password}\"/>")
void xml(@Param("user_name") String user, @Param("password") String password);

@RequestLine("POST /")
@Headers("Content-Type: application/json")
@Body("%7B\"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D")
void json(@Param("user_name") String user, @Param("password") String password);
}

使用

  • 导入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  • 配置要暴露的api接口 在对应的l上加注解@FeignClient(”这里是注册的项目名”) 然后给对应的接口配置请求路径
package com.y.api;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.Map;

/**
* @program: cloud-demo
* @description: 食物所有的api,测试openfeign
* @author: ChestnutDuck
* @create: 2023-11-25 11:16
**/
@FeignClient("resfood")
public interface ResFoodApi {
//这里的路径多带food 是因为注册服务的对应控制类上加了RequestMapping("food") 多加了一层访问路径
@RequestMapping("food/detailCountAdd")
public Map<String,Object> detailCountAdd(Integer fid);
@RequestMapping("food/findAll")

public Map<String,Object> findAll();

@RequestMapping("food/findByFid")
public Map<String,Object> findById(@RequestParam Integer fid);
@RequestMapping(value = "food/findByPage",method =RequestMethod.POST)
public Map<String,Object> findByPage(@RequestParam int pageno,@RequestParam int pagesize,
@RequestParam String sortby,@RequestParam String sort);

}
  • 要在对应的消费模块的启动类上加EnableFeignClients,这里是订单
package com.y;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

/**
* @program: cloud-demo
* @description: 订单启动类
* @author: ChestnutDuck
* @create: 2023-11-22 20:24
**/
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages= {"com.y.api"}) //暴露api的包路径
public class ResOrderApp {
public static void main(String[] args) {
SpringApplication.run(ResOrderApp.class,args);
}
}

  • 修改请求方式
package com.y.web.controller;

import com.y.api.ResFoodApi;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;

/**
* @program: cloud-demo
* @description: 消费端测试访问resfood
* @author: ChestnutDuck
* @create: 2023-11-23 19:32
**/
@RequestMapping("order")
@RestController
@Slf4j
public class ordercontoller {
@Autowired
public ResFoodApi resFoodApi;
@Autowired
public RestTemplate restTemplate; //springboot里的
@RequestMapping(value = "addCart",method = {RequestMethod.POST})
public Map<String, Object> addCart(@RequestParam Integer fid, @RequestParam Integer num, HttpSession httpSession){
Map<String,Object> map=new HashMap<>();
// 第一种: http://192.168.118.162:9000/food/findByFid?fid= 使用ip和端口固定访问一个服务
//第二种:
// String url="http://resfood/food/findByFid?fid=";
// // 服务名 请求前缀
// Map <String,Object> result = this.restTemplate.getForObject(url + fid,Map.class );
Map<String,Object> result=this.resFoodApi.findById(fid);
log.info("发送请求后得到商品信息"+result);
return map;
}
}

配置中心(nacos)

  • 没统一存放配置之前,有个明显的问题,就是当修改了配置之后。必须重启服务,否则配置会失效。
  • 启动配置文件,不应该加到配置中心

处理的问题

  1. 集中管理配置文件
  2. 不同坏境不同配置,动态化的配置更新
  3. 运行期间,不需要去服务器修改配置文件,服务会从配置中心拉取自己的信息
  4. 配置信息以 rest 接口暴露

架构图

image-20231129194610051

配置文件到测试环境

  • 在nacos创建命名空间,在此命名空间下创建配置image-20231129194853663

image-20231129194940926

  • dataid区分配置文件

  • DataId的要求: ${prefix}-${spring.profiles.active}.${file-extension}

    ​ 前缀(服务注册) 激活的环境 文件格式

  • 将配置信息复制到配置内容中

  • 导入依赖

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
  • 这个bootstrap的依赖 是因为你要告诉它你的配置信息在哪里,否则会读不到你在配置中心的配置
  • 在resource下新建bootstrap.yml文件在里面加入下面依(bootstrap.yml用来程序引导时执行,比application.yml先加载,应用于更加早期配置信息读取,可以理解成系统级别的一些参数配置,这些参数一般是不会变动的。一旦bootStrap.yml被加载,则内容不会被覆盖)
spring:
cloud:
nacos:
config:
server-addr: localhost:8848 #nacos端口
namespace: res134 #命名空间的名称
group: DEFAULT_GROUP
username: nacos #底层应用了security验证
password: nacos
prefix: resfood #这里是Data Id的前缀也是你注册的服务名
file-extension: yml # Data Id的后缀
profiles:
active: dev #开发环境
2023-11-29 19:43:09.955  INFO 16680 --- [  restartedMain] o.s.b.d.a.OptionalLiveReloadServer       : LiveReload server is running on port 35729
2023-11-29 19:43:10.215 INFO 16680 --- [ restartedMain] o.s.b.a.e.web.EndpointLinksResolver : Exposing 20 endpoint(s) beneath base path '/actuator'
2023-11-29 19:43:10.276 INFO 16680 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 9000 (http) with context path ''
2023-11-29 19:43:10.285 INFO 16680 --- [ restartedMain] c.a.n.p.a.s.c.ClientAuthPluginManager : [ClientAuthPluginManager] Load ClientAuthService com.alibaba.nacos.client.auth.impl.NacosClientAuthServiceImpl success.
2023-11-29 19:43:10.286 INFO 16680 --- [ restartedMain] c.a.n.p.a.s.c.ClientAuthPluginManager : [ClientAuthPluginManager] Load ClientAuthService com.alibaba.nacos.client.auth.ram.RamClientAuthServiceImpl success.
2023-11-29 19:43:10.415 INFO 16680 --- [ restartedMain] c.a.c.n.registry.NacosServiceRegistry : nacos registry, DEFAULT_GROUP resfood 127.0.0.1:9000 register finished
2023-11-29 19:43:10.721 INFO 16680 --- [ restartedMain] s.a.ScheduledAnnotationBeanPostProcessor : No TaskScheduler/ScheduledExecutorService bean found for scheduled processing
2023-11-29 19:43:10.727 INFO 16680 --- [ restartedMain] com.y.ResFoodApp : Started ResFoodApp in 6.633 seconds (JVM running for 7.355)
2023-11-29 19:43:10.737 INFO 16680 --- [ restartedMain] c.a.c.n.refresh.NacosContextRefresher : [Nacos Config] Listening config: dataId=resfood, group=DEFAULT_GROUP
2023-11-29 19:43:10.738 INFO 16680 --- [ restartedMain] c.a.c.n.refresh.NacosContextRefresher : [Nacos Config] Listening config: dataId=resfood.yml, group=DEFAULT_GROUP
2023-11-29 19:43:10.738 INFO 16680 --- [ restartedMain] c.a.c.n.refresh.NacosContextRefresher : [Nacos Config] Listening config: dataId=resfood-dev.yml, group=DEFAULT_GROUP
2023-11-29 19:43:11.029 INFO 16680 --- [on(1)-127.0.0.1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2023-11-29 19:43:11.031 INFO 16680 --- [on(1)-127.0.0.1] o.s.web.servlet.DispatcherServlet : Completed initialization in 2 ms

  • 最下面几行可以看到已经监听了配置中心对应的配置

动态刷新

@RefreshScope
  • 这个注解要加到类或者方法上,不能加到属性中,ctrl点击这个注解image-20231130205107477

  • 在target上声明了加到类或方法上

  • 现在对配置中心的配置进行修改,后台会感知你的操作返回给你信息

2023-11-30 20:54:02.341  INFO 28248 --- [ternal.notifier] o.s.c.e.event.RefreshEventListener       : Refresh keys changed: [res.pattern.dataFormat]
  • 注意:使用动态刷新,确保你引入了actuator依赖并开放了resfreshScope端点
  • 实际上就是nacos服务器向actuator/refresh发了个请求image-20231130210706501

共享配置

  • 多个模块需要用到相同的配置,列如连接数据库,这时可以吧公共的配置提取出来,放到一个新建的配置里,再在bootstarp.yml中加载这个配置

  • 这里将mysql和redis提取出来,记得要把之前配置文件关于MySQL和redis的内容删掉image-20231130223308841

  • 注意这里命名规范,前面的名字顺便取,但是一定要以.yml结尾

  • 在bootstarp.yml下新增共享文件配置shared-configs

spring:
cloud:
nacos:
config:
server-addr: localhost:8848
namespace: res134
group: DEFAULT_GROUP
username: nacos
password: nacos
prefix: resfood
file-extension: yml
shared-configs:
- {data-id: 'mysqllocal.yml',refresh: true}
- {data-id: 'redislocal.yml',refresh: true}
profiles:
active: prod

  • refresh 表示是否启用动态刷新 ,group可以不用配,他会给你默认的
  • 启动时控制台会打印如下信息
2023-11-30 21:32:03.224  INFO 17716 --- [  restartedMain] o.s.b.d.a.OptionalLiveReloadServer       : LiveReload server is running on port 35729
2023-11-30 21:32:03.484 INFO 17716 --- [ restartedMain] o.s.b.a.e.web.EndpointLinksResolver : Exposing 20 endpoint(s) beneath base path '/actuator'
2023-11-30 21:32:03.541 INFO 17716 --- [ restartedMain] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8000 (http) with context path ''
2023-11-30 21:32:03.550 INFO 17716 --- [ restartedMain] c.a.n.p.a.s.c.ClientAuthPluginManager : [ClientAuthPluginManager] Load ClientAuthService com.alibaba.nacos.client.auth.impl.NacosClientAuthServiceImpl success.
2023-11-30 21:32:03.551 INFO 17716 --- [ restartedMain] c.a.n.p.a.s.c.ClientAuthPluginManager : [ClientAuthPluginManager] Load ClientAuthService com.alibaba.nacos.client.auth.ram.RamClientAuthServiceImpl success.
2023-11-30 21:32:03.675 INFO 17716 --- [ restartedMain] c.a.c.n.registry.NacosServiceRegistry : nacos registry, DEFAULT_GROUP resfood 173.18.22.84:8000 register finished
2023-11-30 21:32:03.988 INFO 17716 --- [ restartedMain] s.a.ScheduledAnnotationBeanPostProcessor : No TaskScheduler/ScheduledExecutorService bean found for scheduled processing
2023-11-30 21:32:03.993 INFO 17716 --- [ restartedMain] com.y.ResFoodApp : Started ResFoodApp in 6.814 seconds (JVM running for 7.541)
2023-11-30 21:32:04.003 INFO 17716 --- [ restartedMain] c.a.c.n.refresh.NacosContextRefresher : [Nacos Config] Listening config: dataId=resfood, group=DEFAULT_GROUP
2023-11-30 21:32:04.004 INFO 17716 --- [ restartedMain] c.a.c.n.refresh.NacosContextRefresher : [Nacos Config] Listening config: dataId=redislocal.yml, group=DEFAULT_GROUP
2023-11-30 21:32:04.004 INFO 17716 --- [ restartedMain] c.a.c.n.refresh.NacosContextRefresher : [Nacos Config] Listening config: dataId=resfood.yml, group=DEFAULT_GROUP
2023-11-30 21:32:04.005 INFO 17716 --- [ restartedMain] c.a.c.n.refresh.NacosContextRefresher : [Nacos Config] Listening config: dataId=mysqllocal.yml, group=DEFAULT_GROUP
2023-11-30 21:32:04.005 INFO 17716 --- [ restartedMain] c.a.c.n.refresh.NacosContextRefresher : [Nacos Config] Listening config: dataId=resfood-prod.yml, group=DEFAULT_GROUP
2023-11-30 21:32:04.505 INFO 17716 --- [2)-173.18.22.84] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
2023-11-30 21:32:04.507 INFO 17716 --- [2)-173.18.22.84] o.s.web.servlet.DispatcherServlet : Completed initialization in 2 ms

数据库迁移

  • 现在用的数据源是druid,在依赖中引入了druid的starter,他会帮我们自动配置,要想实现动态刷新,要在对应的类或者方法上加上@RefreshScope注解,但是这个数据源不是我们自己创建的,他已经被编译成了字节码文件,那么我们想实现动态刷新,就不能用starter的方案,要自己创建一个数据源
package com.y.config;

import com.alibaba.druid.pool.DruidDataSource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;

/**
* @program: cloud-demo
* @description:
* @author: ChestnutDuck
* @create: 2023-11-30 22:58
**/
@Configuration
@Slf4j
@RefreshScope
public class MyDruidDataSource {
@Value("${spring.datasource.url}")
private String url;
@Value("${spring.datasource.driver-class-name}")
private String driverClassName;
@Value("${spring.datasource.username}")
private String username;
@Value("${spring.datasource.password}")
private String password;

@Bean //IOC
@Primary //优先使用这个代码IOC
@RefreshScope
public DataSource druid(){
log.info("使用的编程式的数据源创建.");
DruidDataSource ds=new DruidDataSource();
ds.setUsername(username);
ds.setPassword(password);
ds.setDriverClassName(this.driverClassName);
ds.setUrl(url);
return ds;
}
}

  • 类上面加是实现属性动态刷新,属性变时,druid会重建
  • @Primary表示如果有多个数据源要优先使用这个
  • 更改配置后他会动态刷新创建一个新的SQLSession
  • 控制台打印信息如下
2023-11-30 23:10:04.596  INFO 27032 --- [ternal.notifier] o.s.c.e.event.RefreshEventListener       : Refresh keys changed: [spring.datasource.url]
Creating a new SqlSession

服务哨兵(sentinel)

  • 主要完成一些流量控制 、服务治理,解决服务雪崩问题
  • 以流量为切入点,从流量控制熔断降级,系统负载保护等多维度保护服务的稳定性

组件图

image-20231202201119475

特点:

  • 多种限流算法:包括令牌桶、漏桶等,可以根据业务场景选择合适的算法。
  • 多种限流维度:包括QPS、并发线程数、异常比例等,可以根据不同的维度来进行限流。
  • 多种应用场景:支持Dubbo、Spring Cloud、gRPC等多种RPC框架的服务发现和调用。
  • 动态规则源:支持多种数据源,如Nacos、Zookeeper、Apollo等,可以动态推送和更新规则。
  • 实时监控:提供实时的监控和统计功能,可以查看服务的运行状态和指标、

程序结构image-20231202201850170

执行原理image-20231202202254382

  • 将不同的Slot按照顺序串在一起(责任链模式),从而将不同的功能(熔断、降级等)组合在一起。

并发测试

  • 评估系统在同时处理多个用户请求时的性能。在这种测试 中,系统会暴露于一定数量的用户负载下,并且会记录系统的响应时间、吞吐量和资源利用率等指标。
  • 线程数:对应的并发用户
  • Ramp Up 时间 线程准备时长,对应测试时间。例如线程数为10,准备时长为10 就是一秒启动线程
  • 循环次数:代表每个线程发多少次请求

jmeter的使用

  • 使用前保证项目环境可用

  • 一个线程组表示一组用户image-20231201224733842

  • 测试对应的服务,新建http请求项image-20231201224950077

  • 查看测试结果image-20231201225537390

  • image-20231201225606101

  • 可以在工具里使用函数助手 列如random 可以让参数不固定

  • 聚合报告:提供有关事务响应时间、吞吐量和错误率的信息。

  • 查看结果树:显示每个请求的响应,包括请求头、请求正文和响应正文。

  • 监听器图形结果:将测试结果可视化,以便更轻松地分析性能问题。

  • 断言结果:验证响应是否满足特定条件。

  • 分布式负载测试图:显示不同服务器上的负载情况

服务雪崩

什么是雪崩

  • 服务雪崩就是服务提供者不可用导致服务调用者不可用,并将不可用逐渐放大。换句话说如果服务调用是链式调用,假如一个服务失败,导致整条链路的服务都失败的情形,我们称之为服务雪崩。

发生服务雪崩的原因和阶段

  • 第一阶段:硬件故障、程序bug、缓存击穿(系统会缓存你最近访问的数据,假如你访问的数据在缓存中没有,导致请求会去数据库中查询,这样会让性能下降,可能导致一些服务不可用)、用户大量请求
  • 第二阶段 :调用段可能因为网络问题不断的刷新,加大流量和并发可能性(用户重试)
  • 第三阶段: 服务调用者不可用:导致资源被耗尽

解决方案

  • 出现缓存击穿:1. 缓冲预热:提前将一些热点数据存入缓存中 2.提前筛选:不让错误的请求定位到缓冲
  • 用户大量请求:做筛选,可以限制同一个ip的多次访问
  • 超时处理:设定超时时间,请求超过一定时间没有响应就返回错误信息,不会无休止的等待
  • 服务降级:拒绝服务,直接返回错误信息、页面拒绝、延迟持久化(将数据缓冲起来,不立马去操作数据库修改数据)
服务熔断
  • 当下游的服务因为某种原因突然变得不可用或响应过慢,上游服务为了保证自己整体服务的可用性,不再继续调用目标服务,直接返回,快速释放资源。如果目标服务情况好转则恢复调用
  • 服务熔断是服务降级的一种
断路器模式
  • 断路器模式可以比作一个空气开关,当压力过大时,自动断开,待压力变小时再恢复工作
原理
  • 原理: 当远程服务被调用时,断路器将监视这个调用,如调用时间太长,断路器将会介入并中断调用。此外,断路器将监视所有对远程资源的调用,如对某一个远程资源的调用失败次数足够多,那么断路器会出现并采取快速失败,阻止将来调用失败的远程资源.
状态图

img

  • 当一个请求访问时。断路器默认是关闭的,你的请求可以到对应的服务上去,如果流量突然增大或者底层服务不可调用时,请求次数达到失败阈值,断路器会从close状态变为open状态,此时请求无法到达服务上,当处于open状态时,会休眠五秒,休眠结束后会尝试关闭,将一小部分请求放过去,试一试服务是否可用,如果可用则变为关闭状态,不可用则返回为open状态
  • 状态统计:滑动窗口:统计最近访问的请求数量/统计一定时间内请求数量
服务降级
  • 当下游服务因为某种原因响应过慢,下游服务主动停掉一些不重要的服务,释放服务器资源,增加响应速度
  • 当下游服务因为某些原因不可用时,上游主动调用一些本地的降级逻辑,避免卡顿,迅速返回信息给用户
  • 非关键服务保证服务ap(可用) 减轻cp(一致性)
舱壁模式
  • 规范每个业务用的线程数,避免耗尽资源,尽量减少崩坏服务对其他服务的影响

sentinel的使用

<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-core</artifactId>
<version>1.8.6</version>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-transport-simple-http</artifactId>
<version>1.8.6</version>
</dependency>
  • simple-http:是一个简版的http服务器,用于与dashboard通讯 默认端口9999
  • 定义资源
    public static void main(String[] args) {
//配置流控规则
initFlowRules();
//模拟访问 while(true)
while(true){
//1.5.0 版本开始可以直接利用 try-with-resources 特性
try(Entry entry= SphU.entry("HelloWorld")){
//被保护的逻辑
System.out.println("要执行的操作。。。");

} catch (BlockException e) {
// throw new RuntimeException(e);
System.out.println("blocked!");
}
}
  • 定义资源也可以使用注解**@SentinelResource**(“HelloWorld”)
  • 定义规则
private static void initFlowRules(){
List<FlowRule> rules = new ArrayList<>(); //规则列表
FlowRule rule = new FlowRule();
rule.setResource("HelloWorld"); //此规则对那个资源起作用
rule.setGrade(RuleConstant.FLOW_GRADE_QPS); //每秒钟请求数
// Set limit QPS to 20.
rule.setCount(100000); //每秒多少个请求
rules.add(rule);
FlowRuleManager.loadRules(rules); //添加规则到规则管理器
}
  • 启动项目,注意启动时要配置虚拟机参数
-Dcsp.sentinel.dashboard.server=localhost:9999
  • 效果如下image-20231202161641793

sentinel客户端整合springcloud

  • 导入sentinel客户端的依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
  • 在application.yml中配置
spring:
cloud:
sentinel: #sentinel配置
transport:
port: 8719 #跟控制台交流的端口,随意指定一个未使用的端口即可
dashboard: localhost:9999 #dashboard地址与端口
eager: true #表示 Sentinel 会在应用启动时立即进行初始化。这意味着 Sentinel 会立即加载规则、统计信息等相关的数据,并开始监控和限流
@SpringBootApplication
public class Application {

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

@Service
public class TestService {
//业务层
@SentinelResource(value = "sayHello")
public String sayHello(String name) {
return "Hello, " + name;
}
}

@RestController
public class TestController {
//控制层
@Autowired
private TestService service;

@GetMapping(value = "/hello/{name}")
public String apiHello(@PathVariable String name) {
return service.sayHello(name);
}
}
  • @SentinelResource 注解用来标识资源是否被限流、降级
  • 控制层不需要 @SentinelResource 注解因为在 Web 层直接使用 Spring Cloud Alibaba 自带的 Web 埋点适配。而业务层需要加 @SentinelResource 注解加到服务实现上,列如上面的sayHello
  • 现在可以直接在客户端中查看(这里是自己的项目) ,里面所有访问服务的请求都会在下面列出来image-20231202163155241

服务流控

  • 配合压力测试工具测试流控模式

服务流控模式

关联控制
  • 当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量
  • image-20231203190515005
  • 访问优先请求时超过10个则限制被限请求
  • 如果用排队则将被限请求放到等待队列中,在超时时间内,待优先请求执行完后再执行
  • 测试结果image-20231203191105888
链路流控
  • 资源通过调用关系,相互之间构成一棵调用树,如图所示
          machine-root
/ \
/ \
Entrance1 Entrance2
/ \
/ \
DefaultNode(nodeA) DefaultNode(nodeA)
  • 上图用两个入口分别调用nodeA,通过链路可只统计入口1的请求,这样可以对入口1的请求进行限流
  • 链路限流底层默认将每条链路都统计,我们在测试时要在appilcation.yml中sentinel下加入 web-content-unify: flase 关闭链路整合的功能
  • 测试在订单控制层新加两个方法,让他们调用同一个服务,代码如下
    @Autowired
public GoodsBiz goodsBiz;
@RequestMapping(value = "serviceA",method = RequestMethod.GET)
public Map<String,Object> serviceA(){
Map<String,Object> map=new HashMap<>();
goodsBiz.goodsInfo();
map.put("code",200);
return map;
}
@RequestMapping(value = "serviceB",method = RequestMethod.GET)
public Map<String,Object> serviceB(){
Map<String,Object> map=new HashMap<>();
goodsBiz.goodsInfo();
map.put("code",200);
return map;
}

@Service
public class GoodsBiz {
@SentinelResource("goodsInfo") //将此方法定义为sentinel管理的资源
public void goodsInfo(){
System.out.println("商品信息");
}
}

测试
  • 这是没限流之前的结果流控效果image-20231203194723074

  • 加入流控规则

  • 访问goodsInfo的资源超过阈值就限制从serviceA过来的请求

  • 给A限流后结果如下image-20231203195606296

流控效果

直接失败
  • 超过阈值直接失败image-20231203155606541

  • QPS: 单位时间内,请求接口次数限制

  • 线程数: 单位时间内,请求并发数限制,这里处理的并发请求是指tomcat中开启多少个线程来处理请求 如果系统繁忙可设置阈值减少系统负荷

  • image-20231203155316866

Warm Up
  • 即预热/冷启动方式,当系统长期处于低水位的情况下,某一时刻,流量突然增加,直接把系统拉升到高水位可能把系统压垮,通过冷启动,让通过的流量缓慢增加,在一定的时间内将流量加到阈值,给系统一个预热,避免系统被压垮

  • image-20231203163936125

  • 表示流量在两秒内加到10

  • 测试效果image-20231203170233206

匀速排队
  • 匀速排队方式会严格控制请求通过的间隔时间,让请求以匀速的速度通过,该种方法主要用于处理间隔性突发的流量,列如,在某一时刻大量的请求过来,但后面几秒处于空闲状态,希望系统会在空闲期间逐渐处理请求,而不是直接拒绝请求或者使系统宕机

  • 暂时不支持每秒大于1000的请求

  • image-20231203171832069

  • 超时时间单位为毫秒,超过10个剩下的排队等待,如果等待时间超过超时时间则请求失败

  • 测试结果image-20231203172608134

熔断降级

  • 参考官方文档熔断降级 · alibaba/Sentinel Wiki (github.com)

  • 除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。一个服务常常会调用别的模块,可能是另外的一个远程服务、数据库,或者第三方 API 等。例如,支付的时候,可能需要远程调用银联提供的 API;查询某个商品的价格,可能需要进行数据库查询。然而,这个被依赖服务的稳定性是不能保证的。如果依赖的服务出现了不稳定的情况,请求的响应时间变长,那么调用服务的方法的响应时间也会变长,线程会产生堆积,最终可能耗尽业务自身的线程池,服务本身也变得不可用。为了防止整个链路不可用,我们需要对不稳定的弱依赖服务调用进行熔断降级,暂时切断不稳定调用,避免局部不稳定因素导致整体的雪崩

慢调用比列

  • 设置最大响应时间,如果请求响应时间大于阈值,则统计为慢调用

  • 如果单位统计时长内发的请求数多,慢调用的比例大,会导致系统堆积,在接下来的熔断时长内请求会自动被熔断

  • 用线程睡眠模拟慢调用

@GetMapping("payAction")
public Map<String,Object> payAction(Integer flag) throws InterruptedException {
if (flag==null){
Thread.sleep(3000);
}
Map<String, Object> map = new HashMap<>();
map.put("code",200);
return map;
}
  • 增加熔断规则image-20231203203344545
  • 注意:这里让每个请求睡3秒,熔断时长为两秒,压力测试时,请求不可太快,否则会没有效果,因为是模拟测试,测试没达到预期时,修改请求发送时间和熔断时间
  • 测试结果image-20231203210318167
  • 这里是在5秒内发100个请求,就是一秒20个超过了最小请求数,而每个线程都睡3秒肯定超过了慢调用的阈值和比列,所以系统进行熔断,经过两秒后(设置的熔断时长)发送一个请求过去看系统是否可用,发现不可用后继续熔断
  • image-20231203210504970

异常比列

  • 当单位统计时长内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。

异常数

  • 当单位统计时长内的异常数目超过阈值之后会自动进行熔断。
  • 异常数只能是业务异常,主动抛异常模拟测试
@GetMapping("payAction")
public Map<String,Object> payAction(Integer flag) throws InterruptedException {
Random r=new Random();
int a= r.nextInt(5);
if (a==2||a==4){
//40%的异常
throw new RuntimeException("我要异常咯");
}
Map<String, Object> map = new HashMap<>();
map.put("code",200);
return map;
}
  • image-20231203211320245

  • 在这个统计时长内异常数超过6就熔断

  • 结果如下image-20231203211606029

  • 熔断尝试关闭放一个请求过去,通过的请求数增加

热点限流

  • 热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。
  • Sentinel Parameter Flow Control
  • 尽管controller会自动设置资源名,但是对于热点流,要自己设置资源名,在一个热点资源上加@SentinelResource("hotkey-page")
  • 测试image-20231203221046317
  • 现在将分页查作为热点,在一秒内访问第一页的请求超过5个就熔断

流控回调

场景

  • 如果流控规则起作用了,限制了一些请求,sentinel以 http响应码 4xx/5xx形式回送信息. 被限制的请求返回给用户的信息对用户并不友好,用户在使用对应服务时,不知道服务发生了什么导致不可用,所以可以用流控回调处理返回给用户的信息

解决方案blockHandler

  • blockHandler 用于处理被 Sentinel 阻止的请求,例如当请求超过限流阈值时,Sentinel 会自动阻止该请求,并调用指定的 blockHandler 方法进行处理 , 这样可以将流控这种异常信息转为业务要处理的消息格式.
  • 在@SentinelResource资源时 加入回调方法
blockHandler = "回调函数的名字"
  • 回调的函数要和对应资源的参数类型保持一致

  • 例如在hotkey-page这个资源上加流控回调

  @SentinelResource(value = "hotkey-page",blockHandler = "handleBlock")
@RequestMapping(value = "/findByPage",method = {RequestMethod.POST,RequestMethod.GET})
public Map<String,Object> findByPage(@RequestParam int pageno,@RequestParam int pagesize,
@RequestParam String sortby,@RequestParam String sort)

//对应的回调函数
public Map<String, Object> handleBlock(int pageno, int pagesize, String sortby, String sort , BlockException exception) {
exception.printStackTrace();
Map<String, Object> map = new HashMap<>();
map.put("code", 0);
String info ="exception:"+exception.getMessage()+", rule:"+exception.getRule();
map.put("msg",info);
return map;
  • 为这个资源设置阈值为1 发10个请求看效果

  • 没有阈值回调前,返回的请求image-20231212162546077

  • image-20231212163009077

  • 可根据自己的实际情况去返回相应的错误信息

fallback

  • 出现业务异常,sentinel的默认方案是 以http 500码回送一条信息,用户同样不知道请求的服务出现了什么问题,对用户不友好。 fallback 则用于处理方法执行过程中的异常,例如当方法抛出异常时,Sentinel 会自动调用指定的 fallback 方法进行处理。
  • 使用和上面blockHandler 步骤一致,只需注意异常类型为Throwable ,Throwable是所有异常的根类在测试时可通过手动关闭数据库服务来模拟义务异常

统一异常管理

  • 上述两种情况是对单个资源进行处理,如果需要处理的资源很多,会加大我们编码的复杂度,所以可以直接对controller中出现的异常统一管理
  • 利用spring boot的全局异常处理机制集中式处理异常
    • @ControllerAdvice注解采用aop机制处理controller中的异常
    • @ExceptionHandler(RuntimeException.class)捕获某种异常类型
    • @ResponseBody返回json响应
  • 新增配置类如下
package com.y.config;

import org.springframework.core.annotation.Order;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;
import java.util.Map;

/**
* @program: cloud-demo
* @description: springBoot 针对业务异常的统一处理切面
* @author: ChestnutDuck
* @create: 2023-12-11 20:42
**/
@ControllerAdvice // Controller控制器,ioc, Advice: aop中的增强
@Order(-100000) //一个连接点可以加很多增强,这个表示他在那一层,把他设置成-100000,表示他在最高层,里面的异常一层一层往出抛,到这里集中处理
// AOP技术
public class CustomerExceptionHandlerAdvice {

public class CustomerExceptionHandler {
@ExceptionHandler(RuntimeException.class) //dao->service(事务回滚针对RuntimeException)->controller->advice
@ResponseBody
public Map<String,Object> handleRuntimeException(RuntimeException exception){
Map<String,Object> map=new HashMap<>();
map.put("code",0);
map.put("msg","runtimeException occured"+exception.getMessage());
return map;
}
}
}
  • @ExceptionHandler 对那个异常处理

  • 新增这个配置类后,可以做到统一管理

  • 测试,为了防止之前的blockHander和fallback的干扰,先将这两个配置去掉

  • 关闭数据库连接模拟业务异常,返回给用户的还是200状态码,测试结果如下image-20231212170352212

  • 这个不能处理流控异常因为请求是由sentinel直接处理的,请求没到控制层,这是对业务层的处理

对流控异常统一处理

  • 利用springmvc的全局处理异常机制集中处理
  • 把流控异常抛出到外面统一由MySentinelExceptionHandle 处理
  • 引入依赖
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-spring-webmvc- adapter</artifactId>
<!--转换适配器-->
</dependency>
<!--单独对 ParamFlowException处理 -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-parameter-flow-control</artifactId>
</dependency>
  • 新增类完成异常处理
	package com.y.config;

import com.alibaba.csp.sentinel.adapter.spring.webmvc.callback.BlockExceptionHandler;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.csp.sentinel.slots.block.authority.AuthorityException;
import com.alibaba.csp.sentinel.slots.block.degrade.DegradeException;
import com.alibaba.csp.sentinel.slots.block.flow.FlowException;
import com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowException;
import com.alibaba.csp.sentinel.slots.system.SystemBlockException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;

/**
* @program: cloud-demo
* @description: sentinel异常BlockException及子类(各种流控异常) 同义转换处理
* @author: ChestnutDuck
* @create: 2023-12-11 21:10
**/
@Component
public class MySentinelExceptionHandler implements BlockExceptionHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, BlockException ex) throws Exception {
String msg = null;
if (ex instanceof FlowException) {
msg = "访问频繁,请稍候再试";
} else if (ex instanceof DegradeException) {
msg = "系统降级";
}else if( ex instanceof ParamFlowException){
msg="热点参数异常:"+ ex.getMessage()+","+((ParamFlowException) ex).getResourceName()+","+ex.getRule() ;
}


else if (ex instanceof SystemBlockException) {
msg = "系统规则限流或降级";

} else if (ex instanceof AuthorityException) {
msg = "授权规则不通过";

} else {
msg = "未知限流降级";
}
// http状态码
response.setStatus(200);
response.setCharacterEncoding("utf-8");
response.setHeader("Content-Type", "application/json;charset=utf-8");
response.setContentType("application/json;charset=utf-8");
Map map=new HashMap();
map.put("code",0);
map. put("msg", msg);
//ObjectMapper 将map对象转为json对象
ObjectMapper om=new ObjectMapper();
String json=om.writeValueAsString( map );
PrintWriter writer=response.getWriter();
writer.write( json );
writer.flush();
}

}

  • 为什么要request和response 因为sentinel异常是在有请求过来发生的,请求过来会被sentinel 拦截 sentinel底层会统计流控信息
  • block异常有很多子类,根据不同子类返回相应信息
  • 异常信息会送前台需要输出流,输出流从responce中取
  • 测试流控异常,看是否起作用 将阈值设置为1 结果如下
  • image-20231212202750700