预见猿份
主题
首页面试题在线工具关于我们老苗一对一私教学员评价
实战项目
项目前置基础创新WMS项目Java微服务框架与实战云岚到家项目闪聚支付项目学成在线项目青橙电商项目JVM原理与实战调优分布式事务专题Java高频面试题MySQL从入门到精通Java数据结构与算法老苗一对一私教学员评价blog
blog
  • Java微服务框架与实战

    • 内容介绍
    • 项目安装配置
    • day01-微服务服务注册与发现
    • day01-作业参考
    • day02-服务保护、分布式事务
    • day03-微服务网关与配置中心
    • day04-MQ基础
    • day05-MQ高级
    • day05-RabbitMQ集群部署
    • day06-Elasticsearch入门
    • day07-Elasticsearch高级
    • day08-微服务阶段面试篇上
    • day08-配置同步环境
    • day09-微服务阶段面试篇下
    • day09-redis集群
    • day10-微服务阶段面试题







----- 到底线了 -----

×

欢迎来到预见猿份,本站项目均为站长原创,学习中有问题可直接提交给站长老苗解决(微信:mrt_0607)。

苗润土老师,20余年一线项目经验,2014年加入黑马,星辰wms、云岚到家、学成在线项目作者,历任高级讲师、教学主管及课程研究员。 b站老苗

day02 服务保护、分布式事务 ​

学习目标 ​

  1. 能够说出什么是微服务雪崩
  2. 能够说出常用的微服务保护方案和技术方案
  3. 能够说出什么是熔断降级
  4. 能够基于FallbackFactory编写降级方法
  5. 能够使用Sentinel配置熔断策略并测试通过
  6. 能够使用Sentinel配置限流策略并测试通过
  7. 能够基于Sentinel注解编写降级方法
  8. 能够使用Sentinel配置线程隔离并测试通过
  9. 能够说出CAP原理
  10. 能够使用Seata实现分布式事务控制
  11. 能够说出Seata AT模式的工作原理

📺 视频链接

1 微服务保护 ​

1.1.微服务保护方案 ​

1.1.1 微服务雪崩问题 ​

上次课我们学习了微服务之间的远程调用,微服务通过远程调用进行协作完成业务流程,试想如果出现下边的现象会导致什么问题。

假如商品服务业务并发较高,占用过多Tomcat连接。可能会导致商品服务的所有接口响应时间增加,延迟变高,甚至是长时间阻塞直至查询失败。

此时查询购物车业务需要等待商品查询结果,从而导致购物车业务的响应时间也变长,甚至也阻塞直至无法访问。而此时如果查询购物车的请求较多,可能导致购物车服务的Tomcat连接占用较多,所有接口的响应时间都会增加,整个服务性能很差, 甚至不可用。

依次类推,整个微服务群中与购物车服务、商品服务等有调用关系的服务可能都会出现问题,最终导致整个集群不可用。

这就是级联失败问题,或者叫雪崩问题。

保证服务运行的健壮性,避免级联失败导致的雪崩问题,就属于微服务保护。这章我们就一起来学习一下微服务保护的常见方案以及对应的技术。

1.1.2 微服务保护方案 ​

1.1.2.1 方案介绍 ​

AI:Spring cloud微服务保护的方案

Spring Cloud微服务架构中的服务保护是非常重要的,它能够确保系统的稳定性和可用性,特别是在面对突发流量或者服务异常的情况下。常用的微服务保护方案包括但不限于以下几个方面:

  1. 熔断 (Circuit Breaker)
    熔断机制用于在服务出现问题时快速失败,避免调用链路中的服务相互等待,导致整体系统响应变慢甚至不可用。

如何快速失败呢?当服务的错误率达到一定程度时,断路器(相当于保险丝)会打开,直接返回错误而不是尝试调用服务。一段时间后,断路器会处于半开状态尝试调用服务,如果服务恢复正常,则关闭断路器。

由断路器统计业务执行的异常比例,如果超出阈值则会熔断该业务,拦截访问该业务的一切请求。

  1. 降级 (Degradation)
    断路器会统计访问某个服务的请求数量,统计服务提供方的异常比例,当比例过高表明该接口会影响到其它服务,应该拒绝调用该接口,而是直接走降级逻辑。

降级逻辑 即提供一个简化的响应或者默认的响应来代替正常的服务调用。这样可以保证核心业务不受影响,非核心业务暂时被限制或关闭。

  1. 超时 (Timeout)
    设置合理的超时时间可以避免长时间等待响应导致的问题。当请求超时时,可以选择快速失败并返回错误信息,或者重试等策略。
  2. 线程隔离 (Thread Isolation)
    线程隔离是指为每个服务分配独立的线程池,这样即使某个服务出现问题也不会影响到其他服务。

线程隔离的思想来自轮船的舱壁模式:

轮船的船舱会被隔板分割为N个相互隔离的密闭舱,假如轮船触礁进水,只有损坏的部分密闭舱会进水,而其他舱由于相互隔离,并不会进水。这样就把进水控制在部分船体,避免了整个船舱进水而沉没。

为了避免某个接口故障或压力过大导致整个服务不可用,我们可以限定每个接口可以使用的资源范围,也就是将其“隔离”起来。

如图所示,我们给查询购物车业务限定可用线程数量上限为20,这样即便查询购物车的请求因为查询商品服务而出现故障,也不会导致服务器的线程资源被耗尽,不会影响到其它接口。

  1. 限流 (Rate Limiting)
    限流是最常见的服务保护措施之一,其目的是为了防止服务因为过大的流量而崩溃。

对于某些关键资源或者参数的访问,可以采取特殊的限流措施来防止这些热点成为瓶颈。

限流往往会有一个限流器,数量高低起伏的并发请求曲线,经过限流器就变的非常平稳。这就像是水电站的大坝,起到蓄水的作用,可以通过开关控制水流出的大小,让下游水流始终维持在一个平稳的量。

可以通过以下几种方式进行限流:

基于令牌桶算法:允许一定数量的请求通过,超出则拒绝或排队等待。
基于滑动窗口:在一段时间内对请求进行计数,超过阈值则触发限流。

1.1.2.2 实现工具 ​

在Spring Cloud生态系统中,实现服务保护通常使用的工具包括:

Hystrix: 提供了熔断、限流、超时等功能。

Resilience4j: 是一个轻量级的库,提供了与Hystrix类似的功能,但设计更为现代和简洁。

Sentinel: 阿里巴巴开源的一款流量控制组件,特别适合微服务架构下的流量管理,提供了限流、熔断、降级等多种服务保护功能,并且支持热更新规则。

本课程讲解Sentinel。

1.2 熔断降级 ​

1.2.1. 方案介绍 ​

熔断降级是解决雪崩问题的重要手段,包括熔断和降级两个方案。

熔断是由断路器统计服务调用的异常比例、慢请求比例,如果超出阈值则会熔断该服务。即拦截访问该服务的一切请求;而当服务恢复时,断路器会放行访问该服务的请求。熔断发生在服务调用方即客户端。

降级是当遇到访问失败可以快速返回一些默认数据或者友好提示,用户体验会更好。熔断降级结合后是当线路断开后直接走降级线路避免再次去请求失败线路。降级方法需要在服务调用方即客户端实现。

断路器控制熔断和放行的流程如下:

断路器包括三个状态:

  • closed:关闭状态,断路器放行所有请求,并开始统计异常比例、慢请求比例、异常数。超过阈值则切换到open状态

  • open:打开状态,服务调用被熔断,访问被熔断服务的请求会被拒绝,快速失败,直接走降级逻辑。Open状态5秒后会进入half-open状态

  • half-open:半开状态,放行一次请求,根据执行结果来判断接下来的操作。

    • 请求成功:则切换到closed状态
    • 请求失败:则切换到open状态

实现熔断降级做两件事:

  • 编写服务降级逻辑:就是服务调用失败后的处理逻辑,根据业务场景,可以抛出异常,也可以返回友好提示或默认数据。
  • 异常统计和熔断:统计服务提供方的异常比例,当比例过高表明该接口会影响到其它服务,应该拒绝调用该接口,而是直接走降级逻辑。这里我们用Sentinel完成。

1.2.2. Sentinel安装与集成 ​

1.2.2.1 切换分支 ​

将hmall-micro代码环境切换到dev_02分支。

注意:切换分支前要提交原当前分支的代码。

每位学生在dev_02分支练习完成后提交代码并切换回dev_01分支继续未完成的任务。

1.2.2.2 安装Sentinel ​

实现服务保护的工具有很多,Spring Cloud Alibaba技术栈中Sentinel是实现服务保护的中间件。

Sentinel是阿里巴巴开源的一款服务保护框架,目前已经加入Spring Cloud Alibaba中。官方网站:

home | Sentinel

文档:https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D

Sentinel 的使用可以分为两个部分:

  • 核心库(Jar包):不依赖任何框架/库,能够运行于 Java 8 及以上的版本的运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。在项目中引入依赖即可实现服务限流、隔离、熔断等功能。
  • 控制台(Dashboard):Dashboard 主要负责管理推送规则、监控、管理机器信息等。

为了方便监控微服务,我们先把Sentinel的控制台搭建出来。

课前提供的虚拟机已经安装了sentinel,如下图:

使用课前提供的虚拟机需要设置sentinel容器的时区,如下:

先启动sentinel

Java
docker start sentinel-dashboard
1

登录sentinel容器并设置时区

Java
docker exec -it sentinel-dashboard /bin/bash
ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo Asia/Shanghai > /etc/timezon
1
2

设置完成效果如下 :

如果未使用课前提供的虚拟机需要参考下边的内容安装sentinel:

1)下载jar包

下载地址:

Releases · alibaba/Sentinel

也可以直接使用课前资料提供的版本:

2)运行

将jar包拷贝到 虚拟机/data/soft/sentinel目录下重命名为sentinel-dashboard.jar:

创建Dockerfile文件

JavaScript
FROM openjdk:11-jdk

ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezo
ARG SENTINEL_VERSION=1.8.6

# copy sentinel jar
ADD ./sentinel-dashboard.jar /home/sentinel-dashboard.jar
RUN chmod -R +x /home/sentinel-dashboard.jar

ENTRYPOINT ["sh","-c","java -Dserver.port=8090 -Dcsp.sentinel.dashboard.server=localhost:8090 -Dproject.name=sentinel-dashboard -jar $JAVA_OPTS /home/sentinel-dashboard.jar"]
1
2
3
4
5
6
7
8
9
10
11
12

执行命令创建镜像:

JavaScript
docker build -t sentinel-dashboard .
1

创建并启动容器:

JavaScript
docker run  --name sentinel-dashboard -d -p 9090:8090 sentinel-dashboard:latest
1

其它启动时可配置参数可参考官方文档:

启动配置项

3)访问

访问http://192.168.101.68:9090/ 页面,就可以看到sentinel的控制台了:

需要输入账号和密码,默认都是:sentinel

登录后,即可看到控制台,默认会监控sentinel-dashboard服务本身:

本地运行sentinel ​

如果在测试时发现虚拟中的sentinel不能用可以本地运行sentinel。

将sentinel的jar包放在任意非中文、不包含特殊字符的目录下,重命名为sentinel-dashboard.jar:

然后运行如下命令启动控制台:

Shell
java -Dserver.port=8090 -Dcsp.sentinel.dashboard.server=localhost:8090 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar
1

访问:http://localhost:8090/ 页面

1.2.2.3 项目集成Sentinel ​

在虚拟机启动sentinel

Java
docker start sentinel-dashboard
1

接下来,我们在项目中集成 sentinel,我们在哪个项目中集成 sentinel?

sentinel要完成熔断降级,熔断是在服务调用方即客户端,所以针对购物车服务请求商品服务实现熔断就需要在购物车服务集成 sentienl。

我们在cart-service模块中整合sentinel,连接sentinel-dashboard控制台,步骤如下:
1)引入sentinel依赖

XML
<!--sentinel-->
<dependency>
    <groupId>com.alibaba.cloud</groupId> 
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
1
2
3
4
5

2)配置控制台

修改application.yaml文件,添加下面内容:

YAML
spring:
  cloud:
    sentinel:
      transport:
        dashboard: 192.168.101.68:9090
        client-ip: 192.168.101.1
      http-method-specify: true # 开启请求方式前缀可根据http请求方法区分簇点链路
1
2
3
4
5
6
7

如果是在本机运行的sentinel要配置:

Java
spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8090
      http-method-specify: true # 开启请求方式前缀可根据http请求方法区分簇点链路
1
2
3
4
5
6

3)访问cart-service的任意端点

重启cart-service、item-service,然后访问查询购物车接口,sentinel的客户端就会将服务访问的信息提交到sentinel-dashboard控制台。并展示出统计信息:

点击簇点链路菜单,会看到下面的页面:

所谓簇点链路,就是单机调用链路,是一次请求进入服务后经过的每一个被Sentinel监控的资源。默认情况下,Sentinel会监控SpringMVC的每一个Endpoint(接口)。

因此,我们看到/carts这个接口路径就是其中一个簇点,我们可以对其进行限流、熔断、降级、隔离等保护措施,稍后会详细讲解。

1.2.3. 实现降级 ​

1.2.3.1 开启sentinel ​

下边我们先编写降级逻辑,再实现服务熔断。

AI:feign如何使用sentinel实现降级

首先配置Feign使用Sentinel:

在购物车服务application.yml中配置如下:

YAML
feign:
  sentinel:
    enabled: true # 开启feign对sentinel的支持
1
2
3

1.2.3.2 实现FallbackFactory接口 ​

接下来给FeignClient编写失败后的降级逻辑有两种方式:

  • 方式一:FallbackClass,无法捕获到远程调用的异常

定义一个降级类实现FeignClient接口,并在@FeignClient注解中配置fallback 属性,如下:

Java
@FeignClient(name="item-service",path = "/items",fallback = 降级类名.class)
1
  • 方式二:FallbackFactory,可以捕获远程调用的异常,我们一般选择这种方式。

定义一个降级类实现FallbackFactory接口,并在@FeignClient注解中配置fallbackFactory属性

Java
@FeignClient(name="item-service",path = "/items",fallbackFactory= 降级类名.class)
1

这里我们演示方式二的失败降级处理。

步骤一:在hm-api模块中给ItemClient定义降级处理类,实现FallbackFactory接口:

代码如下:

Java
package com.hmall.api.item;

import com.hmall.api.item.dto.ItemDTO;
import com.hmall.common.utils.CollUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;

import java.util.Collection;
import java.util.List;

/**
 * @author Mr.M
 * @version 1.0
 * @description itemClient的通用降级策略
 * @date 2024/8/5 9:50
 */
@Slf4j
@Component
public class ItemClientFallbackFactory implements FallbackFactory<ItemClient> {
    @Override
    public ItemClient create(Throwable cause) {
        return new ItemClient() {
            @Override
            public List<ItemDTO> queryItemByIds(Collection<Long> ids) {
                log.error("远程调用ItemClient#queryItemByIds方法出现异常,参数:{}", ids, cause);
                cause.printStackTrace();
                // 查询购物车允许失败,查询失败,返回空集合
                return CollUtils.emptyList();
            }
    
            @Override
            public void deductStock(List<OrderDetailDTO> items) {
                log.error("远程调用ItemClient#deductStock扣减库存失败,参数:{}",items,cause);
            }
        };
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

1.2.3.3 配置fallbackFactory ​

步骤二:在hm-api模块中的ItemClient接口中使用ItemClientFallbackFactory:

JavaScript
package com.hmall.api.item;

import com.hmall.api.item.dto.ItemDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.Collection;
import java.util.List;

/**
 * @author Mr.M
 * @version 1.0
 * @description 商品服务Feign接口
 * @date 2024/8/3 16:21
 */
@FeignClient(name="item-service",path = "/items",fallbackFactory = ItemClientFallbackFactory.class)
public interface ItemClient {
    @GetMapping
    List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
    @PutMapping("/stock/deduct")
    public void deductStock(@RequestBody List<OrderDetailDTO> items);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

1.2.3.4 注入降级Bean ​

步骤三:在cart-service启动类添加扫描包配置,扫描降级类并将bean注册到spring容器

1.2.3.5 降级测试 ​

最后测试降级效果。

重启cart-service,并将item-service停止来模拟item-service服务不可用

请求查询购物车接口,cart-service远程调用item-service,由于item-service不可用,Feign执行ItemClientFallbackFactory 中定义的降级逻辑

在ItemClientFallbackFactory 中打断点,在ItemClientFallbackFactory 中捕获到了远程调用的异常,降级方法返回空List<ItemDTO>。

接口测试最终效果,购物车可以正常显示,但由于获取的商品信息为空这里newPrice显示为null。

1.2.3.6 小结 ​

feign远程调用怎么实现降级?

  1. 我们使用的是OpenFeign实现微服务之间的远程调用,使用Sentinel实现熔断降级。
  2. 首先在服务提供方配置sentinel,引入sentinel的依赖,配置sentinel的地址
  3. 在服务调用方开启feign使用sentinel
  4. 在服务调用方编写feign接口并编写降级逻辑,具体方法是编写降级类实现FallbackFactory接口,并在FeignClient注解中配置fallbackFactory。
  5. 服务调用方当无法正常调用服务提供方接口时会走降级逻辑,并捕获到异常。

1.2.4. 服务熔断 ​

1.2.4.1 配置熔断策略 ​

根据熔断方案,sentinel会统计异常比例、慢请求比例、异常数等数据,达到阈值时断路器打开即发生熔断,熔断状态下会走降级路线保证快速响应。

下边在sentinel中配置熔断策略,找到查询商品信息的簇点链路

点击“熔断”,配置异常数,如下图:

2秒内最小请求数为2,异常数达到1发生熔断,熔断时长为20秒。

1.2.4.2 测试异常数 ​

下边进行测试,继续停止item-service商品服务。

启动cart-service购物车服务,请求查询购物车接口。

此时在购物车控制台报异常信息如下:

Java
feign.FeignException$ServiceUnavailable: [503] during [GET] to [http://item-service/items?ids=100000006163] [ItemClient#queryItemByIds(Collection)]: [Load balancer does not contain an instance for the service item-service]
1

当异常数达到Sentinel配置将发生熔断,熔断时间20秒,此时cart-service控制台将不再输出异常信息,因为cart-service走了降级路线不再请求item-service。

通过观察cart-service控制台日志可以发现,发生熔断后sentinel客户端抛出了com.alibaba.csp.sentinel.slots.block.degrade.DegradeException.

熔断时间(20秒)过后断路器成半开状态,再次请求购物车接口, 会尝试请求item-service一次,如果成功此时断路器关闭,如果仍失败断路器打开,关闭item-service日志发现输出一次java.lang.RuntimeException: 测试异常,此时断路器打开继续熔断。

1.2.4.3 测试慢调用比例 ​

下边测试另一种熔断策略:慢调用比例

删除原来的熔断策略

添加新的熔断策略。

这种是按照慢调用比例来做熔断,上述配置的含义是:

  • RT(接口响应时间)超过200毫秒的请求调用就是慢调用
  • 统计最近3000ms内的最少2次请求,如果慢调用比例大于等于50%,则触发熔断
  • 熔断持续时长20s

修改item-service的查询商品信息接口,添加线程休眠模拟处理时间,这样可以制造慢请求的效果。

重启item-service服务

连续请求查询购物车接口,cart-service连续向item-service发生请求,由于每次请求item-service线程休眠1秒导致接口请求时长肯定大于200毫秒,最终发生了熔断。

参考测试“异常数”熔断策略去观察cart-service和item-service的日志来判断熔断发生的情况。

1.2.4.4 测试异常比例(自行测试) ​

请大家自行测试熔断策略: 异常比例

举例:

  • 统计最近3000ms内的最少2次请求,如果异常比例不低于0.5,则触发熔断
  • 熔断持续时长20s

1.2.4.5 面试题 ​

你的项目中熔断降级怎么实现的?

1.3.请求限流 ​

1.3.1 配置限流策略 ​

下边我们对item-service的商品查询接口进行限流,找到商品查询的簇点链路,点击"流控"

在弹出的菜单中填写QPS单机阈值为5,表示每秒最多处理5个请求

AI:什么是QPS

QPS 是 "Queries Per Second" 的缩写,中文通常称为“每秒查询率”或“每秒请求数”。它是一个衡量系统性能的重要指标,特别是在评估 Web 服务器、数据库系统、API 接口等的负载能力时经常使用。

QPS 表示系统在一秒钟内能够处理的查询或请求的数量。这个指标可以帮助开发者和运维人员了解系统的处理能力和性能瓶颈。例如,如果一个 Web 服务的 QPS 为 100,则意味着该服务在一秒钟内最多可以处理 100 个请求。

1.3.2 限流测试 ​

首先去掉item-service服务中查询商品信息接口的休眠代码。

下边我们用jmeter压力测试工具进行压力测试。

JMeter 是一个广泛使用的开源性能测试工具,主要用于测试 Web 应用程序的负载和性能。它是由 Apache Software Foundation 开发和维护的,完全用 Java 编写,因此可以在任何支持 Java 的平台上运行。

找到课程资料中的JMeter安装包apache-jmeter-5.4.1.zip,解压到不包括中文的目录,进入bin下双击打开jmeter.bat

打开软件切换到中文

接下来找到课程资料下的“雪崩测试.jmx” 拖到JMeter面板中

最终效果如下

设置模拟线程数等参数:

表示:总共发送1000个请求,用100秒发送,循环次数是1即执行10秒结束。

根据设置参数可知,每秒发送10个秒在100秒可完成1000个请求。

在“HTTP请求”界面设置压力测试请求的地址

右键“限流测试”点击“启动”

测试结果可以在“汇总报告”中查看,每次请求的次数在“察看结果树”中查看。

从吞吐量可以看出每秒发送10个请求,异常数是0

通过sentinel的实时监控界面可以看出对商品查询接口的请求,通过的QPS为5,拒绝QPS为5。

因为上边我们配置了对商品查询接口限流QPS为5,所以每秒10个请求查询/carts,远程调用商品查询服务接口的QPS只有5,说明被限流了。

注意:如果实时监控图无法正常显示需要进入虚拟机进行时间同步。

Java
ntpdate ntp1.aliyun.com

如果无法执行上边的命令需要下载ntpdate
yum install ntpdate -y
1
2
3
4

再查看每次的请求和响应数据发现,由于编写了商品查询接口远程调用的降级逻辑,当商品查询接口被限流后将会走降级方法,从jmeter测试结果可以看到被限流的请求无法获取商品信息

再查看cart-service的控制台,在降级方法中捕获到了 com.alibaba.csp.sentinel.slots.block.flow.FlowException 异常,这说明接口被限流后sentinel客户端会抛出FlowException 异常。

1.3.3 小节 ​

sentinel限流怎么实现?

我们项目使用Sentinel实现限流控制。

  1. 首先在服务提供方配置sentinel,引入sentinel的依赖,配置sentinel的地址
  2. 通过Sentinel控制台配置限流策略,可以配置QPS阈值、并发线程数等。
  3. 当方法被限流会走降级逻辑。

1.4. sentinel降级 ​

1.4.1 问题描述 ​

下边我们对/carts接口进行限流测试,找到GET/carts簇点

点击“流控” 设置QPS 单机阈值为6

用jmeter测试,通过sentinel进行实时监控,通过QPS为6,拒绝QPS为4,符合我们的预期结果

当/carts接口被限流时我们访问此接口

结果内容:Blocked by Sentinel (flow limiting),从字面内容看是被sentinel限流。

为什么访问item-service的商品查询接口走了降级方法,而访问/carts没有走降级方法呢?

1.4.2 sentinel实现降级 ​

这里我们需要重新梳理下:

前边cart-service远程调用item-service服务的商品查询接口正常走降级方法,这是因为我们编写了ItemClient接口的降级类ItemClientFallbackFactory,并且我们在cart-service服务中集成了sentinel,在cart-service的application.yml配置文件中配置了feign使用sentinel,如下:

JavaScript
feign:
  sentinel:
    enabled: true # 开启feign对sentinel的支持
1
2
3

也就说,在cart-service通过openfeign远程调用item-service时通过sentinel进行降级,最终执行ItemClientFallbackFactory类中指定的降级方法。

这里我们是对/carts进行限流,并没有针对此接口编写降级方法。

AI:sentinel实现降级

针对feign远程调用我们通过实现FallbackFactory接口去编写远程调用接口对应的降级逻辑,而针对非feign远程调用的降级逻辑我们需要使用@SentinelResource注解去实现

首先使用 @SentinelResource 定义资源,编写降级方法

JavaScript
@ApiOperation("查询购物车列表")
@SentinelResource(value = "queryMyCarts", fallback = "queryMyCartsFallback", blockHandler = "queryMyCartsBlockHandler")
@GetMapping
public List<CartVO> queryMyCarts(){

    return cartService.queryMyCarts();
}
//当发生非限流非熔断异常走此方法
public List<CartVO>  queryMyCartsFallback(Throwable throwable){
    log.error("非限流、非熔断异常执行的降级方法,throwable:",  throwable);
    return new ArrayList<>();
}
//当发生熔断、限流走此方法
public List<CartVO>  queryMyCartsBlockHandler( BlockException blockException){
    log.error("触发限流、熔断时执行的降级方法,blockException:",  blockException);
    return new ArrayList<>();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

然后在sentinel中设置资源的流控

删除针对/carts设置的流控规则

再次使用jmeter进行压力测试

观察cart-service控制台,当/carts限流后正常执行降级方法,日志如下:

JavaScript
18:37:31:767 ERROR 4696 --- [nio-8082-exec-9] c.hmall.cart.controller.CartController   : 触发限流、熔断时执行的降级方法,blockException:
1

在降级方法中可以返回特殊的信息,或指定特殊的状态码,根据特殊值前端可展示为类似“网络忙请稍后重试” 这样的信息。

1.4.3 小节 ​

sentienl降级怎么实现?

对于Feign远程调用我们对每个远程调用接口实现降级方法,通过实现FallbackFactory接口去实现降级方法。

对于非Feign远程调用我们使用@SentinelResource注解编写降级方法。

1.5. 线程隔离 ​

1.5.1 方案介绍 ​

首先我们来看下线程隔离功能,无论是Hystix还是Sentinel都支持线程隔离。不过其实现方式不同。

线程隔离有两种方式实现:

  • 线程池隔离:给每个服务调用业务分配一个线程池,利用线程池本身实现隔离效果
  • 信号量隔离:不创建线程池,而是计数器模式,记录业务使用的线程数量,达到信号量上限时,禁止新的请求

如图:

Sentinel的线程隔离就是基于信号量隔离实现的,而Hystix两种都支持,但默认是基于线程池隔离。

1.5.2 Sentinel线程隔离 ​

下边我们使用Sentinel实现基于信号量的线程隔离。

点击查询商品的FeignClient对应的簇点资源后面的流控按钮:

在弹出的表单中填写下面内容:

注意,这里勾选的是并发线程数限制,也就是说这个查询功能最多使用5个线程,而不是5QPS。如果查询商品的接口每秒处理2个请求,则5个线程的实际QPS在10左右,而超出的请求自然会被拒绝。

cart-service调用商品查询控制在5个线程内,通过线程隔离即使商品服务出现问题也不会影响cart-service服务。

下边修改商品查询接口的代码,添加休眠500毫秒的代码,模拟一次请求需要500毫秒,一秒则可处理2次请求,5个线程可接收10 QPS左右。

将/carts的限流规则调大以免影响商品查询接口的限流控制。

下边使用jmeter进行测试,更改线程数为10000,时间为100秒,每秒需要发送100个请求。

启用测试,通过sentinel实时监控,符合我们的预期。

1.6 面试题 ​

你们是怎么做微服务保护的?

Sentinel实现熔断、限流的底层原理是什么?

先参考“微服务阶段面试篇”去自学,在面试篇进行详细讲解。

2 分布式事务 ​

2.1. 什么是分布式事务 ​

2.1.1 概念 ​

首先我们看看项目中的下单业务整体流程:

交互流程如下:

用户创建订单,客户端请求交易服务创建订单

创建订单成功,交易服务请求购物车服务清理购物车,请求库存服务扣减库存

由于订单、购物车、商品分别是三个不同的微服务,而每个微服务都有自己独立的数据库,一次下单事务需要订单、购物车、商品服务分别执行自己的本地事务,是跨多个数据库完成这次下单的事务,像这种,在分布式系统环境下由多个服务通过网络通信协作去完成一次事务,这称之为分布式事务。可简单理解为一个分布式事务等于多个本地事务。

2.1.2 测试分布式事务 ​

分布式事务是无法通过单个数据库事务去控制的,每个微服务都有自己的数据库,一次下单事务需要订单、购物车、商品服务分别执行自己的本地事务,其中一个执行失败其它本地事务是无法回滚的,比如:扣减库存失败无法回滚清理购物车及创建订单的事务。

下边我们测试下单扣减库存,首先部署交易微服务,交易微服务是第一天布置的作业,这里有几点需要注意:

1.需要在ItemClient接口中增加扣减库存的方法,以供交易服务远程调用,还需要在ItemClientFallbackFactory增加扣减库存的降级逻辑。

2.商品服务中的OrderDetailDTO移到了hm-api工程,凡是引用该类的微服务都需要统一引用hm-api下的OrderDetailDTO.

3.交易服务启动类注意添加扫描hm-api下的feign接口

JavaScript
@MapperScan("com.hmall.trade.mapper")
@EnableFeignClients(basePackages = {"com.hmall.api"})
@SpringBootApplication(scanBasePackages = {"com.hmall.trade","com.hmall.api"})
1
2
3

4.暂时将“UserContext.getUser()”获取用户id代码固定为“1”。

我们在下单方法代码的最后位置制造异常,如下:

JavaScript
@Override
    @Transactional
    public Long createOrder(OrderFormDTO orderFormDTO) {
        ....
        if(1==1){
            throw new RuntimeException("测试异常");
        }
        return order.getId();
    }
1
2
3
4
5
6
7
8
9

预期结果是:当扣减库存成功,下单失败,最终扣减库存事务进行回滚。

下边打开交易服务的swagger文档找到下单接口进行测试:

从商品数据库找一个商品id填入请求参数

最终抛出异常,查看商品的库存正常扣除,但是订单数据没有创建成功,最终导致数据不一致。

测试结论:

通过本地事务控制注解 @Transactional是无法控制分布式事务的。

2.1.3 认识分布式事务 ​

下边为了简化分析过程 我们仍然以下单扣减库存为例说明:

在单体架构下实现下单减库存,如下图:

用户请求订单服务,订单服务请求数据库完成创建订单扣减库存,通过本地事务实现,代码如下:

Java
begin transaction; 
//1.本地数据库操作:创建订单
//2.本地数据库操作:减去库存 
commit transation;
1
2
3
4

如果是在微服务架构下,如下图:

用户请求订单服务下单,订单服务请求库存服务扣减库存。

此时代码变为下边这样:

Java
begin transaction; 
//1.本地数据库操作:创建订单
//2.远程调用:减去库存 
commit transation;
1
2
3
4

设想: 当远程调用扣减库存成功了,由于网络问题远程调用并没有返回,此时本地事务提交失败就回滚了创建订单的操作,此时订单没有创建成功而库存却扣减了,最终就导致了下单扣减库存整个事务的数据不一致。

因此在分布式架构下,基于数据库的事务控制无法满足要求,下单操作是一次本地事务,扣减库存是一次本地事务,两次本地事务组成一个完整的事务即下单扣减库存,数据库的本地事务只能控制一次本地事务即下单操作控制下单的本地事务,扣减库存操作控制扣减库存的本地事务,无法保证下单和扣减库存整体事务的原子性和一致性。

造成分布式事务无法控制的根本原因是不同业务的数据通常不在一个数据库中或者不在一个系统中,一次事务需要由多个服务或多个系统远程调用协作去完成,远程协作依赖网络,由于网络问题会导致整体事务不能正常完成。

分布式事务的典型场景是:业务的数据分布在多个数据库,一次事务操作需要跨多个数据库去完成,

需要由多个服务远程调用协作去完成,远程调用依赖网络,由于网络问题会导致整体事务不能正常完成。

如下图所示:

还有非典型的分布式事务场景也需要了解下。

1)单服务请求多数据库完成一次事务

下图中虽然没有跨服务远程调用但一次事务请求两个不同的数据库也属于分布式事务的场景,创建订单会和订单数据库创建连接通过一次本地事务提交数据,减库存会和商品数据库创建连接通过一次本地事务提交数据,因为下单扣减库存是通过两个数据库连接完成,仍然是多次本地事务共同完成一个完整的事务。

3)多服务请求单数据库完成一次事务

下图中虽然用的一个数据库但是通过跨服务远程调用去完成一次事务,也属于分布式事务的场景。

思考下这种场景为什么也属于分布式事务?

2.1.4 小结 ​

什么是本地事务?

基于应用自己的关系型数据库的事务称为本地事务,在service方法通过添加@Transactional注解进行本地事务控制。

什么是分布式事务?

在分布式系统环境下由多个服务通过网络通信协作去完成一次事务,这称之为分布式事务。

分布式事务的场景有哪些?

多个微服务之间通过远程调用完成一次分布式事务,即:跨服务完成一次事务

单服务请求多数据库完成一次事务,即:跨数据源完成一次事务

多服务请求单数据库完成一次事务,即:跨服务完成一次事务

2.2. CAP原理 ​

遇到了分布式事务的场景我们该如何去进行事务控制呢,本节学习如何选型分布式事务的控制方案。

什么是CAP原理

首先需要理解什么是CAP原理,明白了CAP原理有助于我们去选型分布式事务的控制方案。

CAP是 Consistency、Availability、Partition tolerance三个词语的缩写,分别表示一致性、可用性、分区容忍性。

使用下图去理解CAP:

下图表示客户端经过网关访问订单服务,库存服务

一致性: 向系统写一个新数据再次读取到的也一定是这个新数据。拿上图举例,请求订单服务下单,订单服务请求库存服务扣减库存,只要下单成功则库存扣减成功。

可用性:任何时间都可以访问订单服务和库存服务,系统保证可用。

分区容忍性:也叫分区容错性,分布式系统在部署时服务器可能部署在不同的网络分区,比如上图中订单服务在北京,库存服务在上海,由于处于不同的网络分区如果网络通信异常就会导致节点 之间无法通信,当出现由于网络问题导致节点 之间无法通信,此时仍然是对外提供服务的这叫做满足分区容忍性。

CAP理论要强调在分布式系统中C、A、P这三点不能全部满足。

由于是分布式系统就要满足分区容忍性,因为分布式系统难免存在网络分区,不能因为网络异常导致整个系统不可用,所以P是一定要满足的。

满足P,那么C和A不能同时满足。

拿上图举例说明:

当订单服务与库存服务出现网络通信异常,订单服务无法访问库存服务,此时如果要保证数据一致性则下单接口必须不可用,如果要保证可用性数据将出现不一致。

学习了CAP理论我们知道进行分布式事务控制要在C和A中作出取舍,进行分布式事务控制要么保证CP要么保证AP。

具体要根据应用场景进行判断,下边举例说明CP和AP业务场景的例子。

符合CP的场景:满足C舍弃A,强调一致性。

金融系统:一般需要在多个账户之间进行交易或资金转移的操作通常需要满足CP,这是因为在这种场景下,数据的一致性是至关重要的,确保不会发生资金丢失、重复扣款或其他意外情况,源账号和目标账号的转账结果要么都成功要么都失败,不会存在一个成功一个失败的情况。

库存系统:在多个仓库之间进行库存转移或销售操作时,需要确保库存的一致性,防止商品超卖或库存混乱。

  订票系统:需要确保预订信息的一致性,避免出现同一个资源被多次预订的问题。

Zookeeper:可作为注册中心,支持CP,拿主节点选举举例,当主节点异常进行选举,选举期间所有节点不可用,保证数据的一致性。

Redis:Redis主从模式是CP模式,当主从通信异常此时无法向主节点写数据。

Nacos:Nacos也支持CP,不过它默认是AP模式,当客户端注册为非临时节点时此时为CP模式,注册为非临时节点就需要实时上报心跳,即使在一段时间内未收到心跳信息,该实例仍然会保留在服务列表中,适用于配置中心。

符合AP的场景:满足A舍弃C,强调可用性。

AP强调的是可用性,允许短暂的不一致但是要保证最终一致性,在实际应用中符合AP的场景较多。

订单退款:退款后状态为退款中,24小时后退款金额到帐。

积分系统:注册送积分,注册成功积分在24小时后到账。

跨行转账:一般转账支持CP,还有的支持AP,源账号扣减金额后需要等一段时间目标账户才到账,或者源账号扣款后由于目标账号有问题过一段时间将转账金额退回到源账户。

MySQL主从复制:支持AP,向主节点写数据,异步同步到的从节点。

  Nacos:默认支持AP,即临时节点的情况,会实时上报心跳,如果一段时间内未收到心跳信息,Nacos 会将该实例标记为不可用并从服务列表中移除。

在生产中AP场景应用的更多,强调的是可用性,允许出现短暂不一致,最终达到数据一致性。

2.4. 安装Seata ​

继续阅读请移步至:https://mp.weixin.qq.com/s/pmR6jQJLfRwBmuEET1kH6A

← day01-作业参考day03-微服务网关与配置中心 →








如果发现文档内容有错误或排版错乱,请及时联系站长老苗修改,不胜感激。联系方式
关于我们 | 隐私政策 | 豫ICP备2026003386号-4 | 豫公网安备41010202004008号
目录

本页无章节