0. 分布式基础
分布式配套:日志系统、指标监控、链路追踪、消息处理
单体应用:
所有功能模块都在一个项目上
优点:开发部署简单方便
缺点:无法应对高并发

集群架构:
优点:解决大并发
缺点:模块化升级(有些功能模块需要经常升级)、多语言团队(引入其他语言开发新模块)

分布式架构:
一个大型应用被拆分成多个小型应用,部署在各个机器。
RPC:远程过程调用,HTTP+json只是rpc的一种方式


服务雪崩:一个微服务的故障传播到了整个调用链,影响到了服务器,影响到了服务器中其他模块,进而影响到整个应用。需要引入服务熔断机制,出现故障快速返回,不会引起请求积压。
熔断:快速失败机制,及时释放资源。
分布式 VS. 集群
分布式:工作方式。大型应用拆分为多个小应用,分布在各个服务器上,每个服务器上部署的东西可能都不一样。
集群:物理形态。很多机器就叫集群。

环境准备
创建微服务架构项目
引入 SpringClould、Spring Cloud Alibaba 相关依赖
注意版本适配


项目工程结构图:

父项目:(SpringBoot)jdk17、改pom文件、将父项目路径下的所有内容除了pom.xml和.idea都删掉
改pom.xml文件:
- parent版本
- pom打包的方式:<packaging>pom</packaging>
- properties
- 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

小结:
- 使⽤ RestTemplate 可以获取到远程数据
- 必须精确指定地址和端⼝
- 如果远程宕机将不可⽤
期望:可以负载均衡调⽤,不⽤担⼼远程宕机
远程调用 - 负载均衡

方法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为被请求的微服务的名字
小结:
- 负载均衡调⽤只需要传⼊ 服务名
- 请求发起之前会⾃动去注册中⼼确定微服务地址
- 如果微服务宕机,会⾃动剔除在线名单,请求将不会发过去
如果注册中⼼宕机,远程调⽤是否可以成功?
- 从未调⽤过,如果宕机,调⽤会⽴即失败
- 调⽤过,如果宕机,因为缓存名单,调⽤会成功
- 调⽤过,如果注册中⼼和对⽅服务宕机,因为会缓存名单,调⽤会阻塞后失败(Connection Refused)

2. Nacos:配置中心

(1)基本使用
-
启动Nacos
-
引入依赖(要刷新一下)
XML<dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> </dependency>
-
application.properties配置
XML#指定配置中⼼地址 spring.cloud.nacos.server-addr=localhost:8848 #service-order.properties是在Nacos中配置的 spring.config.import=nacos:service-order.properties
-
创建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 监听配置变化
- ① 项目启动就监听配置文件变化()
- ② 发生变化后拿到变化值
- ③ 发送邮件
在项目启动类中编写应用启动方法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声明式的远程调用接口
- 业务API:直接复制对方Controller签名即可
- 第三方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:网关


路由
需求
-
客户端发送 /api/order/** 转到 service-order
-
客户端发送 /api/product/** 转到 service-product
-
以上转发有负载均衡效果

新建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