02-服务保护、分布式事务
AI:Spring cloud微服务保护的方案Spring Cloud微服务架构中的服务保护是非常重要的,它能够确保系统的稳定性和可用性,特别是在面对突发流量或者服务异常的情况下。熔断 (Circuit Breaker)熔断机制用于在服务出现问题时快速失败,避免调用链路中的服务相互等待,导致整体系统响应变慢甚至不可用。如何快速失败(fast fail)呢?当服务的错误率达到一定程度时,断路器(相当
学习目标
- 能够说出什么是微服务雪崩
- 能够说出常用的微服务保护方案和技术方案
- 能够说出什么是熔断降级
- 能够基于FallbackFactory编写降级方法
- 能够使用Sentinel配置熔断策略并测试通过
- 能够使用Sentinel配置限流策略并测试通过
- 能够基于Sentinel注解编写降级方法
- 能够使用Sentinel配置线程隔离并测试通过
- 能够说出CAP原理
- 能够使用Seata实现分布式事务控制
- 能够说出Seata AT模式的工作原理
1 微服务保护
1.1.微服务保护方案
1.1.1 微服务雪崩问题
上次课我们学习了微服务之间的远程调用,微服务通过远程调用进行协作完成业务流程,试想如果出现下边的现象会导致什么问题:
假如商品服务业务并发较高,占用过多Tomcat连接。可能会导致商品服务的所有接口响应时间增加,延迟变高,甚至是长时间阻塞直至查询失败。
此时查询购物车业务需要等待商品查询结果,从而导致购物车业务的响应时间也变长,甚至也阻塞直至无法访问。而此时如果查询购物车的请求较多,可能导致购物车服务的Tomcat连接占用较多,所有接口的响应时间都会增加,整个服务性能很差, 甚至不可用。

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

这就是级联失败问题,或者叫雪崩问题。【因为一个底层服务不可用,最终导致整个服务集群不可用】
保证服务运行的健壮性,避免级联失败导致的雪崩问题,就属于微服务保护。这章我们就一起来学习一下微服务保护的常见方案以及对应的技术。
1.1.2 微服务保护方案
1.1.2.1 方案介绍
AI:Spring cloud微服务保护的方案
Spring Cloud微服务架构中的服务保护是非常重要的,它能够确保系统的稳定性和可用性,特别是在面对突发流量或者服务异常的情况下。常用的微服务保护方案包括但不限于以下几个方面:
- 熔断 (Circuit Breaker) 熔断机制用于在服务出现问题时快速失败,避免调用链路中的服务相互等待,导致整体系统响应变慢甚至不可用。
如何快速失败(fast fail)呢?当服务的错误率达到一定程度时,断路器(相当于保险丝)会打开,直接返回错误而不是尝试调用服务。一段时间后,断路器会处于半开状态尝试调用服务,如果服务恢复正常,则关闭断路器。
【知识拓展】
AI:fast fail和safe fail区别
答:
Fast Fail(快速失败):旨在快速暴露问题,防止错误扩散或导致更严重的后果,如医疗、金融场景。缺点是:导致系统中断,影响用户体验【直接抛异常】
Safe Fail(安全失败):旨在最大程度保证系统可用和安全性,如在线服务、云计算平台。缺点是:可能导致问题被掩盖,增加修复难度。【try-catch,返回一个默认值(即降级)】
由断路器统计业务执行的异常比例,如果超出阈值则会熔断该业务,拦截访问该业务的一切请求。

- 降级 (Degradation) 断路器会统计访问某个服务的请求数量,统计服务提供方的异常比例,当比例过高表明该接口会影响到其它服务,应该拒绝调用该接口,而是直接走降级逻辑。
降级逻辑 即提供一个简化的响应或者默认的响应来代替正常的服务调用。这样可以保证核心业务不受影响,非核心业务暂时被限制或关闭。
熔断后,接口还通吗?
不通,直接异常
降级后,接口还通吗?
通,但返回的是降级逻辑,即类似一个默认值,故业务逻辑不一定闭环,后续还需要人工补偿

- 超时 (Timeout) 设置合理的超时时间可以避免长时间等待响应导致的问题。当请求超时时,可以选择快速失败并返回错误信息,或者重试等策略。
常见的远程调用框架,都设置了超时机制。
AI:目前Http、Dubbo、WebService都有超时机制吗?
答:是的,HTTP、Dubbo 和 WebService 都支持超时机制,但它们的实现方式和配置方法有所不同
HTTP:连接超时、读取超时
Dubbo:服务调用超时(默认3s),超时后自动重试2次
WebService:连接超时、读取超时
- 线程隔离 (Thread Isolation) 线程隔离是指为每个服务分配独立的线程池,这样即使某个服务出现问题也不会影响到其他服务。
线程隔离的思想来自轮船的舱壁模式:

轮船的船舱会被隔板分割为N个相互隔离的密闭舱,假如轮船触礁进水,只有损坏的部分密闭舱会进水,而其他舱由于相互隔离,并不会进水。这样就把进水控制在部分船体,避免了整个船舱进水而沉没。
为了避免某个接口故障或压力过大导致整个服务不可用,我们可以限定每个接口可以使用的资源范围,也就是将其“隔离”起来。

如图所示,我们给查询购物车业务限定可用线程数量上限为20,这样即便查询购物车的请求因为查询商品服务而出现故障,也不会导致服务器的线程资源被耗尽,不会影响到其它接口。
- 限流 (Rate Limiting) 限流是最常见的服务保护措施之一,其目的是为了防止服务因为过大的流量而崩溃。
对于某些关键资源或者参数的访问,可以采取特殊的限流措施来防止这些热点成为瓶颈。
限流往往会有一个限流器,数量高低起伏的并发请求曲线,经过限流器就变的非常平稳。这就像是水电站的大坝,起到蓄水的作用,可以通过开关控制水流出的大小,让下游水流始终维持在一个平稳的量。

可以通过以下几种方式进行限流(有兴趣的可以看看下面两种实现方案,前期可以仅做了解):
- 基于令牌桶算法:允许一定数量的请求通过,超出则拒绝或排队等待。
- 基于滑动窗口:在一段时间内对请求进行计数,超过阈值则触发限流。
1.1.2.2 实现工具
在Spring Cloud生态系统中,实现服务保护通常使用的工具包括:
Hystrix: 提供了熔断、限流、超时等功能,是SpringCloud原生组件。
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分支继续未完成的任务
工作中也经常这样来回切换分支,因为不同需求在不同分支里,我们经常都是并行开发
大家入职后,也可能同时负责3-4个项目,所以尽早习惯【多线程并行的开发模式】
1.2.2.2 安装Sentinel
实现服务保护的工具有很多,Spring Cloud Alibaba技术栈中Sentinel是实现服务保护的中间件。
Sentinel是阿里巴巴开源的一款服务保护框架,目前已经加入Spring Cloud Alibaba中。官方网站:
https://sentinelguard.io/zh-cn/
https://github.com/alibaba/Sentinel/wiki/%E4%BB%8B%E7%BB%8D
Sentinel 的使用可以分为两个部分:
- 核心库(Jar包):不依赖任何框架/库,能够运行于Java8及以上的版本的运行时环境,同时对 Dubbo/Spring Cloud 等框架也有较好的支持。在项目中引入依赖即可实现服务限流、隔离、熔断等功能。
- 控制台(Dashboard):Dashboard 主要负责管理推送规则、监控、管理机器信息等。
为了方便监控微服务,我们先把Sentinel的控制台搭建出来。
课前提供的虚拟机已经安装了sentinel,如下图:

使用课前提供的虚拟机需要设置sentinel容器的时区,如下:
先启动sentinel
docker start sentinel-dashboard
登录sentinel容器并设置时区
- 进入容器:docker exec -it sentinel-dashboard /bin/bash
- 执行命令:ln -snf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && echo Asia/Shanghai > /etc/timezon
设置完成效果如下 :

如果未使用课前提供的虚拟机,需要参考下边的内容安装sentinel:
1)下载jar包
下载地址:https://github.com/alibaba/Sentinel/releases
也可以直接使用课前资料提供的版本:

2)运行
将jar包拷贝到 虚拟机/data/soft/sentinel目录下重命名为sentinel-dashboard.jar:
创建Dockerfile文件
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"]
执行命令创建镜像:
docker build -t sentinel-dashboard .
创建并启动容器:
docker run --name sentinel-dashboard -d -p 9090:8090 sentinel-dashboard:latest
其它启动时可配置参数可参考官方文档:官网文档链接
3)访问
访问:http://192.168.101.68:9090/ 页面,就可以看到sentinel的控制台了:

需要输入账号和密码,默认都是:sentinel
登录后,即可看到控制台,默认会监控sentinel-dashboard服务本身:

本地运行sentinel
如果在测试时发现虚拟中的sentinel不能用,可以本地运行sentinel。
将sentinel的jar包放在任意非中文、不包含特殊字符的目录下,重命名为sentinel-dashboard.jar:

然后运行如下命令启动控制台:
java -Dserver.port=8090 -Dcsp.sentinel.dashboard.server=localhost:8090 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar
访问:http://localhost:8090/ 页面
1.2.2.3 项目集成Sentinel
在虚拟机启动sentinel【上面已经执行过,这里是再次提醒、确认一下】
docker start sentinel-dashboard
接下来,我们在项目中集成 sentinel,我们在哪个项目中集成 sentinel?
sentinel要完成熔断降级,熔断是在服务调用方,所以针对购物车服务请求商品服务实现熔断就需要在购物车服务集成 sentienl。
这里可能部分同学有疑问,问什么不是服务提供方呢?所以我们顺便推导一下,假设是提供方熔断:
(1)提供方是熔断了,但是上游调用方还是有大量请求,压力依然存在,只是加快了下游的响应速度,前提是牺牲了原有的业务逻辑实现,并不能保障整体微服务的可靠性
(2)调用方熔断,就是我根本不调用你下游(你此刻慢、报错多那我就先不调用你),而是返回一个默认逻辑,这个默认逻辑实现应该由接口提供方实现
我们在cart-service模块中整合sentinel,连接sentinel-dashboard控制台,步骤如下: 1)引入sentinel依赖
<!--sentinel-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
2)配置控制台
修改application.yaml文件,添加下面内容:
spring:
cloud:
sentinel:
transport:
dashboard: 192.168.101.68:9090
client-ip: 192.168.101.1
http-method-specify: true # 开启请求方式前缀可根据http请求方法区分簇点链路
如果是在本机运行的sentinel要配置:
spring:
cloud:
sentinel:
transport:
dashboard: localhost:8090
http-method-specify: true # 开启请求方式前缀可根据http请求方法区分簇点链路
3)访问cart-service的任意端点
重启cart-service、item-service,然后访问查询购物车接口:swagger链接

sentinel的客户端就会将服务访问的信息提交到sentinel-dashboard控制台。并展示出统计信息:

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

所谓簇点链路,就是单机调用链路,是一次请求进入服务后经过的每一个被Sentinel监控的资源。默认情况下,Sentinel会监控SpringMVC的每一个Endpoint(接口)。
因此,我们看到/carts这个接口路径就是其中一个簇点,我们可以对其进行限流、熔断、降级、隔离等保护措施,稍后会详细讲解。
1.2.3. 实现降级
1.2.3.1 开启sentinel
下边我们先编写降级逻辑,再实现服务熔断。
AI(Cursor)提示詞
帮我在已有工程里,对于itemclient实现降级策略,技术使用Sentinel,注意实现方案是implements FallbackFactory,降级类写在api的工程里,并最终在cart-service调用item-service时使用
首先配置Feign使用Sentinel:
在购物车服务application.yml中配置如下(默认已写好):
feign:
sentinel:
enabled: true # 开启feign对sentinel的支持
1.2.3.2 实现FallbackFactory接口
接下来给FeignClient编写失败后的降级逻辑有两种方式:
- 方式一:FallbackClass,无法捕获到远程调用的异常
定义一个降级类实现FeignClient接口,并在@FeignClient注解中配置fallback 属性,如下:
@FeignClient(name="item-service",path = "/items",fallback = 降级类名.class)
- 方式二:FallbackFactory,可以捕获远程调用的异常,我们一般选择这种方式。
定义一个降级类实现FallbackFactory接口,并在@FeignClient注解中配置fallbackFactory属性
@FeignClient(name="item-service",path = "/items",fallbackFactory= 降级类名.class)
这里我们演示方式二的失败降级处理。
步骤一:在hm-api模块中给ItemClient定义降级处理类,实现FallbackFactory接口:

代码如下:
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;
@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.3 配置fallbackFactory
步骤二:在hm-api模块中的ItemClient接口中使用ItemClientFallbackFactory:
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 注入降级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。

启动 ItemServiceApplication,再次请求购物车接口,验证发现此时返回了 newPrice数据

1.2.3.6 小结
feign远程调用怎么实现降级?
- 我们使用的是OpenFeign实现微服务之间的远程调用,使用Sentinel实现熔断降级。
- 首先配置sentinel,引入sentinel的依赖,配置sentinel的地址
- 在服务调用方开启feign使用sentinel
- 在服务调用方编写feign接口并编写降级逻辑,具体方法是编写降级类实现FallbackFactory接口,并在FeignClient注解中配置fallbackFactory。
降级逻辑到底谁实现?假设A调用B,希望当接口异常(异常比例、慢请求达到阈值)做降级时候能兜底
调用方:我来决定你默认返回什么
提供方:我告诉你,默认返回什么
【到底谁实现,需要靠业务来定,个人开发经验:建议提供方做实现(你不要干预一个提供接口的人默认做什么事情,这是别人的领域,也可以保证职责清晰)】
- 服务调用方当无法正常调用服务提供方接口时会走降级逻辑,并捕获到异常。
1.2.4. 服务熔断
1.2.4.1 配置熔断策略
根据熔断方案,sentinel会统计异常比例、慢请求比例、异常数等数据,达到阈值时断路器打开即发生熔断,熔断状态下会走降级路线保证快速响应。
下边在sentinel中配置熔断策略,找到查询商品信息的簇点链路

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

2秒内最小请求数为2,异常数达到1发生熔断,熔断时长为20秒。
1.2.4.2 测试异常数
下边进行测试,继续停止item-service商品服务。
启动cart-service购物车服务,连续请求查询购物车接口(达到熔断条件)。
此时在购物车控制台报异常信息如下:
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]
当异常数达到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 (Response Time接口响应时间),超过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.请求限流
这里马上有同学可能会困惑:Sentinel的降级和下面这个限流,好像很相似,那具体有什么区别呢?
降级:常和熔断搭配使用,统称熔断降级。当触发熔断后走降级逻辑,被降级的请求走默认值。确保核心可用
限流:超过阈值的请求,进行排队等待或直接拒接,没有默认值一说。防止系统过载
1.3.1 配置限流策略
下边我们对item-service的商品查询接口进行限流,找到商品查询的簇点链路,点击"流控"

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

AI:什么是QPS
QPS 是 "Queries Per Second" 的缩写,中文通常称为“每秒查询率”或“每秒请求数”。它是一个衡量系统性能的重要指标,特别是在评估 Web 服务器、数据库系统、API 接口等的负载能力时经常使用。
QPS 表示系统在一秒钟内能够处理的查询或请求的数量。这个指标可以帮助开发者和运维人员了解系统的处理能力和性能瓶颈。例如,如果一个 Web 服务的 QPS 为 100,则意味着该服务在一秒钟内最多可以处理 100 个请求。
你的主打项目必须要提前准备好:QPS、TPS、RT、下载量、日活、最大数据量的表是什么
1.3.2 限流测试
首先去掉item-service服务中查询商品信息接口的休眠代码。然后启动item服务

下边我们用jmeter压力测试工具进行压力测试: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,说明被限流了。
注意:如果实时监控图无法正常显示需要进入虚拟机进行时间同步。
ntpdate ntp1.aliyun.com
如果无法执行上边的命令需要下载ntpdate
yum install ntpdate -y
此时,需要耐心等待一下后重试,就可以看到有结果了:通过只有5。
如果你还是没有,也可以调整Jmeter参数,把1000,100改成:100,10,循环次数:永远


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

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

查看Jmeter也可以看到大量拒绝信息

1.3.3 小节
sentinel限流怎么实现?
我们项目使用Sentinel实现限流控制。
- 首先在服务提供方配置sentinel,引入sentinel的依赖,配置sentinel的地址
- 通过Sentinel控制台配置限流策略,可以配置QPS阈值、并发线程数等。
- 当方法被限流会走直接拒绝。
1.4. sentinel降级
上面讲的是A--Feign/Dubbo->B服务时候,对B的做熔断、限流,可如果我只想对A本身做限流怎么实现?
1.4.1 问题描述
下边我们对/carts接口进行限流测试,找到GET/carts簇点

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

截止目前,我们一共设置了两个流控规则

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

当/carts接口被限流时我们访问此接口(需要在Jmeter压测期间测试),需要连续多点击几次

结果内容: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,如下:
feign:
sentinel:
enabled: true # 开启feign对sentinel的支持
也就说,在cart-service通过openfeign远程调用item-service时通过sentinel进行降级,最终执行ItemClientFallbackFactory类中指定的降级方法。这里我们是对/carts进行限流,并没有针对此接口编写降级方法。
AI:sentinel实现降级
针对feign远程调用我们通过实现FallbackFactory接口去编写远程调用接口对应的降级逻辑,而针对非feign远程调用的降级逻辑我们需要使用@SentinelResource注解去实现
首先使用 @SentinelResource 定义资源,编写降级方法
@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<>();
}
然后在sentinel中设置资源的流控


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

再次使用jmeter进行压力测试
观察cart-service控制台,当/carts限流后正常执行降级方法,日志如下:
18:37:31:767 ERROR 4696 --- [nio-8082-exec-9] c.hmall.cart.controller.CartController : 触发限流、熔断时执行的降级方法,blockException:
在降级方法中可以返回特殊的信息,或指定特殊的状态码,根据特殊值前端可展示为类似“网络忙请稍后重试” 这样的信息。此时限流期间访问购物车接口,发现就走了降级逻辑,返回空集合:

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实时监控,符合我们的预期。

不知道细心的你是否发现了一个事情:我们设置的Sentinel通过数量,为什么不那么精准呢?
- 窗口边界:一个请求可能刚好跨越两个窗口,导致单个窗口的统计值略高于设定值。
- 滑动窗口精度:滑动窗口的划分粒度可能无法完全精确,尤其在请求密集时。
1.6 面试题
你们是怎么做微服务保护的?
Sentinel实现熔断、限流的底层原理是什么?(在面试篇进行详细讲解)
2 分布式事务
2.1. 什么是分布式事务
2.1.1 概念
首先我们看看项目中的下单业务整体流程:

交互流程如下:
用户创建订单,客户端请求交易服务创建订单
创建订单成功,交易服务请求购物车服务清理购物车,请求库存服务扣减库存
由于订单、购物车、商品分别是三个不同的微服务,而每个微服务都有自己独立的数据库,一次下单事务需要订单、购物车、商品服务分别执行自己的本地事务,是跨多个数据库完成这次下单的事务,像这种,在分布式系统环境下由多个服务通过网络通信协作去完成一次事务,这称之为分布式事务。可简单理解为一个分布式事务等于多个本地事务。
2.1.2 测试分布式事务
分布式事务是无法通过单个数据库事务去控制的,每个微服务都有自己的数据库,一次下单事务需要订单、购物车、商品服务分别执行自己的本地事务,其中一个执行失败其它本地事务是无法回滚的,比如:扣减库存失败无法回滚清理购物车及创建订单的事务。
下边我们测试下单扣减库存,首先部署交易微服务,交易微服务是第一天布置的作业,这里有几点需要注意:
1.需要在ItemClient接口中增加扣减库存的方法,以供交易服务远程调用,还需要在ItemClientFallbackFactory增加扣减库存的降级逻辑,即现在已有的代码【已实现】
@PutMapping("/stock/deduct")
public void deductStock(@RequestBody List<OrderDetailDTO> items);
2.商品服务中的OrderDetailDTO移到了hm-api工程,凡是引用该类的微服务都需要统一引用hm-api下的OrderDetailDTO【已实现】
3.交易服务启动类 trade-service 注意添加扫描hm-api下的feign接口
@MapperScan("com.hmall.trade.mapper")
@EnableFeignClients(basePackages = {"com.hmall.api"})
@SpringBootApplication(scanBasePackages = {"com.hmall.trade","com.hmall.api"})
4.暂时将“UserContext.getUser()”获取用户id代码固定为“1”。
我们在下单方法代码的最后位置制造异常,如下【注意要有下面的:@Transactional】:
@Override
@Transactional
public Long createOrder(OrderFormDTO orderFormDTO) {
....
if(1==1){
throw new RuntimeException("测试异常");
}
return order.getId();
}
预期结果是:当扣减库存成功,下单失败,最终扣减库存事务进行回滚。
此时商品库存我们截个图

下边启动:item-service、trade-service,打开交易服务的swagger文档找到下单接口进行测试:
从商品数据库找一个商品id填入请求参数

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

测试结论:
通过本地事务控制注解 @Transactional是无法控制分布式事务的。
2.1.3 认识分布式事务
下边为了简化分析过程 我们仍然以下单扣减库存为例说明:
在单体架构下实现下单减库存,如下图:

用户请求订单服务,订单服务请求数据库完成创建订单扣减库存,通过本地事务实现,代码如下:
begin transaction;
//1.本地数据库操作:创建订单
//2.本地数据库操作:减去库存
commit transation;
如果是在微服务架构下,如下图:
用户请求订单服务下单,订单服务请求库存服务扣减库存。

此时代码变为下边这样:
begin transaction;
//1.本地数据库操作:创建订单
//2.远程调用:减去库存
commit transation;
设想: 当远程调用扣减库存成功了,由于网络问题远程调用并没有返回,此时本地事务提交失败就回滚了创建订单的操作,此时订单没有创建成功而库存却扣减了,最终就导致了下单扣减库存整个事务的数据不一致。
因此在分布式架构下,基于数据库的事务控制无法满足要求,下单操作是一次本地事务,扣减库存是一次本地事务,两次本地事务组成一个完整的事务即下单扣减库存,数据库的本地事务只能控制一次本地事务即下单操作控制下单的本地事务,扣减库存操作控制扣减库存的本地事务,无法保证下单和扣减库存整体事务的原子性和一致性。
造成分布式事务无法控制的根本原因是不同业务的数据通常不在一个数据库中或者不在一个系统中,一次事务需要由多个服务或多个系统远程调用协作完成,远程协作依赖网络,由于网络问题会导致整体事务不能正常完成。
分布式事务的典型场景是:业务的数据分布在多个数据库,一次事务操作需要跨多个数据库去完成,
需要由多个服务远程调用协作去完成,远程调用依赖网络,由于网络问题会导致整体事务不能正常完成。
如下图所示:

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

2)多服务请求单数据库完成一次事务
下图中虽然用的一个数据库但是通过跨服务远程调用去完成一次事务,也属于分布式事务的场景。
思考下这种场景为什么也属于分布式事务?

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
2.4.1 认识Seata
解决分布式事务的方案有很多,但实现起来都比较复杂,因此我们一般会使用开源的框架来解决分布式事务问题。在众多的开源分布式事务框架中,功能最完善、使用最多的就是阿里巴巴在2019年开源的 Seata 了。
https://seata.io/zh-cn/docs/overview/what-is-seata.html
其实分布式事务产生的一个重要原因,就是参与事务的多个分支事务互相无感知,不知道彼此的执行状态。因此解决分布式事务的思想非常简单:
就是找一个统一的事务协调者,与多个分支事务通信,检测每个分支事务的执行状态,保证全局事务下的每一个分支事务同时成功或失败即可。大多数的分布式事务框架都是基于这个理论来实现的。
Seata也不例外,在Seata的事务管理中有三个重要的角色:
- TC (Transaction Coordinator) -事务协调者:维护全局和分支事务的状态,协调全局事务提交或回滚,相当于监控中心。
- TM (Transaction Manager) -事务管理器:定义全局事务的范围、开始全局事务、提交或回滚全局事务。
- RM (Resource Manager) -资源管理器:管理分支事务,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
Seata的工作架构如图所示:

其中,TM和RM可以理解为Seata的客户端部分,引入到参与事务的微服务依赖中即可。将来TM和RM就会协助微服务,实现本地分支事务与TC之间交互,实现事务的提交或回滚。
而TC服务则是事务协调中心,是一个独立的微服务,需要单独部署。
Seata支持四种不同的分布式事务解决方案:
- XA:强一致性,唯一一个强一致,无业务侵入,注解即可
- AT:最终一致性,无业务侵入,注解即可,是默认的模式
- TCC:最终一致性,有业务侵入,适合复杂定制化业务
- SAGA:最终一致性,有业务侵入,适合长事务模式,较少使用
2.4.2 准备数据库表
Seata支持多种存储模式,但考虑到持久化的需要,我们一般选择基于数据库存储。执行课前资料提供的
《seata-tc.sql》,导入数据库表【默认已实现】:

2.4.3 准备配置文件
课前资料准备了一个seata目录,其中包含了seata运行时所需要的配置文件:

注意:需要修改自己虚拟机的ip地址和MySQL数据库的账户和密码。
我们将整个seata文件夹拷贝到虚拟机的/root目录【已实现】:

2.4.4 Docker部署
以下Docker部署均已实现,大家了解安装、运行步骤即可
如果镜像下载困难,也可以把课前资料提供的镜像上传到虚拟机并加载:

然后,将课前资料中的seata-1.5.2.tar上传至虚拟机的/root目录。
首先导入镜像文件:
docker load -i seata-1.5.2.tar
进入虚拟机的/root目录执行下面的命令:
注意修改虚拟机的ip地址
docker run --name seata \
-p 8099:8099 \
-p 7099:7099 \
-e SEATA_IP=192.168.101.68 \
-v /root/seata:/seata-server/resources \
-d \
seataio/seata-server:1.5.2
查询容器

Seata控制台:http://192.168.101.68:7099/,账号/密码:admin/admin

2.5. 微服务集成Seata
接下来我们实现下单扣减库存的需求,参与分布式事务的每一个微服务都需要集成Seata,下单扣减库存涉及两个微服务,即交易微服务、商品微服务,下边我们以交易微服务为例在trade-service中集成Seata。
2.5.1.引入依赖
分别在交易服务、商品服务引入seata依赖【引入标准:每一个参与调用链路的微服务都需要】。
<!--seata-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
2.5.2.Seata配置
分别在交易服务、商品服务引入seata配置。
内容如下:
seata:
registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
type: nacos # 注册中心类型 nacos
nacos:
server-addr: 192.168.101.68:8848 # nacos地址
namespace: "" # namespace,默认为空
group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP
application: seata-server # seata服务名称
tx-service-group: hmall # 事务组名称
service:
vgroup-mapping: # 事务组与tc集群的映射关系
hmall: "default"
tx-service-group: 事务组是seata进行全局事务管理的逻辑单元,通常按项目为单位进行定义
vgroup-mapping:事务组与tc集群的映射,配置事务组名称与seata TC集群的映射关系
"defualt":seata集群名称,seata服务实例上传至nacos,在nacos中查看seata集群如下:

2.5.3.seata数据库表
seata的客户端在解决分布式事务的时候需要记录一些中间数据,保存在数据库中。因此我们要先准备一个这样的表。将课前资料的seata-at.sql分别文件导入hm-trade、hm-item两个数据库中【已实现】:

OK,至此为止,微服务整合的工作就完成了。

2.5.4.实现分布式事务
前边在没有使用Seata是无法控制分布式事务的,接下来我们用seata控制分布式事务。
我们找到trade-service模块下的com.hmall.trade.service.impl.OrderServiceImpl类中的createOrder方法,也就是下单业务方法。
将其上的@Transactional注解改为Seata提供的@GlobalTransactional:

@GlobalTransactional注解就是在标记事务的起点,将来TM就会基于这个方法判断全局事务范围,初始化全局事务。
我们重启trade-service、item-service服务,再次测试分布式事务:
1.仍然在下单方法最后添加异常,扣减库存成功下单失败观察最终扣减库存是否会回滚。
2.下单正常但扣减库存失败观察最终下单数据是否回滚。
那么,Seata是如何解决分布式事务的呢?
2.6. Seata 工作模式
Seata支持四种不同的分布式事务解决方案,Seata默认使用的是AT模式。
- XA:强一致性,唯一一个强一致,无业务侵入,注解即可
- AT:最终一致性,无业务侵入,注解即可,是默认的模式
- TCC:最终一致性,有业务侵入,适合复杂定制化业务
- SAGA:最终一致性,有业务侵入,适合长事务模式,较少使用
这里我们以XA模式和AT模式来给大家讲解其实现原理。
2.6.1 AT模式
AT模式的基本流程图:

阶段一RM的工作:
- 注册分支事务
- 记录undo-log(数据快照)
- 执行业务sql并提交
- 报告事务状态
阶段二提交时RM的工作:
- 删除undo-log即可
阶段二回滚时RM的工作:
- 根据undo-log恢复数据到更新前
下边我们用一个真实的业务来梳理下AT模式的原理。
比如,现在有一个数据库表,记录用户余额:
id |
money |
|
1 |
100 |
其中一个分支业务要执行的SQL为:
update tb_account set money = money - 10 where id = 1
AT模式下,当前分支事务执行流程如下:
一阶段:
TM发起并注册全局事务到TCTM调用分支事务- 分支事务准备执行业务SQL
RM拦截业务SQL,根据where条件查询原始数据,形成快照。
{
"id": 1, "money": 100
}
RM执行业务SQL,提交本地事务,释放数据库锁。此时 money = 90RM报告本地事务状态给TC
二阶段:
TM通知TC事务结束TC检查分支事务状态
-
- 如果都成功,则立即删除快照
- 如果有分支事务失败,需要回滚。读取快照数据({"id": 1, "money": 100}),使用快照恢复数据库,此时数据库再次恢复为100。
流程图:

演示AT模式
下边通过断点调试演示AT模式的整体流程:
首先在提交订单方法中模拟异常并打断点

当代码执行到断点处每个分支事务已经执行完成,
通过观察seata控制台,已经开启一个全局事务

分支事务如下

上图中其中一个分支事务27595734695333932即商品服务已经成功扣减库存,我们可以观察hm-item数据库的item表的stock字段值
当抛出异常后全局事务回滚,每个分支事务全部回滚
再次查询hm-item数据库的item表,发现stock库存值已恢复。
2.6.2 XA模式
XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 描述了全局的TM与局部的RM之间的接口,几乎所有主流的数据库都对 XA 规范 提供了支持。
XA是规范,目前主流数据库都实现了这种规范,实现的原理都是基于两阶段提交。
正常情况:

异常情况:

一阶段:
- 事务协调者通知每个事务参与者执行本地事务
- 本地事务执行完成后报告事务执行状态给事务协调者,此时事务不提交,继续持有数据库锁
二阶段:
- 事务协调者基于一阶段的报告来判断下一步操作
- 如果一阶段都成功,则通知所有事务参与者,提交事务
- 如果一阶段任意一个参与者失败,则通知所有事务参与者回滚事务
Seata对原始的XA模式做了简单的封装和改造,以适应自己的事务模型,基本架构如图:

RM一阶段的工作:
- 注册分支事务到
TC - 执行分支业务sql但不提交
- 报告执行状态到
TC
TC二阶段的工作:
TC检测各分支事务执行状态
-
- 如果都成功,通知所有RM提交事务
- 如果有失败,通知所有RM回滚事务
RM二阶段的工作:
- 接收
TC指令,提交或回滚事务
XA模式的优点是什么?
- 事务的强一致性,满足ACID原则
- 常用数据库都支持,实现简单,并且没有代码侵入
XA模式的缺点是什么?
- 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
- 依赖关系型数据库实现事务
简述AT模式与XA模式最大的区别是什么?
XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。XA模式强一致;AT模式最终一致
可见,AT模式使用起来更加简单,无业务侵入,性能更好。因此企业90%的分布式事务都可以用AT模式来解决。
2.6.3 TCC模式
TCC模式与AT模式非常相似,每阶段都是独立事务,不同的是TCC通过人工编码来实现数据恢复。需要实现三个方法:
- Try:资源的检测和预留;
- Confirm:完成资源操作业务;要求 Try 成功 Confirm 一定要能成功。
- Cancel:预留资源释放,可以理解为try的反向操作。
举例,一个扣减用户余额的业务。假设账户A原来余额是100,需要余额扣减30元。
- 阶段一( Try ):检查余额是否充足,如果充足则冻结金额增加30元,可用余额扣除30
初识余额:

余额充足,可以冻结:

此时,总金额 = 冻结金额 + 可用金额,数量依然是100不变。事务直接提交无需等待其它事务。
- 阶段二(Confirm):假如要提交(Confirm),则冻结金额扣减30
确认可以提交,不过之前可用金额已经扣减过了,这里只要清除冻结金额就好了:

此时,总金额 = 冻结金额 + 可用金额 = 0 + 70 = 70元
- 阶段二(Canncel):如果要回滚(Cancel)则冻结金额扣减30,可用余额增加30
需要回滚,那么就要释放冻结金额,恢复可用金额:

Seata中的TCC模型依然延续之前的事务架构,如图:

TCC模式的每个阶段是做什么的?
- Try:资源检查和预留
- Confirm:业务执行和提交
- Cancel:预留资源的释放
TCC的优点是什么?
- 一阶段完成直接提交事务,释放数据库资源,性能好
- 相比AT模型,无需生成快照,无需使用全局锁,性能最强
- 不依赖数据库事务,而是依赖补偿操作,可以用于非事务型数据库
TCC的缺点是什么?
- 有代码侵入,需要人为编写try、Confirm和Cancel接口,太麻烦
- 软状态,事务是最终一致
- 需要考虑Confirm和Cancel的失败情况,做好幂等处理
2.6.4 面试题
seata是怎么进行分布式事务控制的?/Seata的工作原理?
作业
对下单方法分布式事务控制
实现完整的分布式事务控制并进行测试,流程如下:

提示:
根据上图分析可知下单方法的分布式事务控制范围包括:下单、清理购物车、扣减库存,分别隶属交易服务、购物车服务、商品服务。
要想使用seata控制下单方法的分布式事务,需要将相关微服务集成 seata。
实现完成参考老师课堂上的方法进行分布式事务控制的测试。
对支付方法分布式事务控制
除下单业务外,用户如果选择余额支付,前端会将请求发送到pay-service模块。而这个模块要做三件事情:
- 直接从user-service模块调用接口,扣除余额付款
- 更新本地(pay-service)交易流水表状态
- 通知交易服务(trade-service)更新其中的业务订单状态
流程如图:

显然,这里也存在分布式事务问题。
前端会提交支付请求,业务接口的入口在com.hmall.pay.controller.PayController类的tryPayOrderByBalance方法:

对应的service方法如下:
@Override
@Transactional
public void tryPayOrderByBalance(PayOrderDTO payOrderDTO) {
// 1.查询支付单
PayOrder po = getById(payOrderDTO.getId());
// 2.判断状态
if(!PayStatus.WAIT_BUYER_PAY.equalsValue(po.getStatus())){
// 订单不是未支付,状态异常
throw new BizIllegalException("交易已支付或关闭!");
}
// 3.尝试扣减余额
userClient.deductMoney(payOrderDTO.getPw(), po.getAmount());
// 4.修改支付单状态
boolean success = markPayOrderSuccess(payOrderDTO.getId(), LocalDateTime.now());
if (!success) {
throw new BizIllegalException("交易已支付或关闭!");
}
// 5.修改订单状态
tradeClient.markOrderPaySuccess(po.getBizOrderNo());
}
将上边的方法改为使用seata控制分布式事务并进行测试。
测试方法:
首先通过交易服务提交订单,拿到业务订单号
然后通过支付服务创建支付单,如下:

再通过支付服务完成余额支付,如下:

测试完成注意观察相关数据库表的数据变化是否符合预期。
更多推荐

所有评论(0)