目录

一、微服务保护

1、服务保护方案

(1)请求限流

(2)线程隔离

(3)服务熔断

2、Sentinel

(1)安装

(2)微服务整合

3、请求限流

4、线程隔离

(1)OpenFeign整合Sentinel

(2)配置线程隔离

5、服务熔断

(1)编写降级逻辑

(2)服务熔断

二、分布式事务

1、Seata事务管理

2、部署TC服务

(1)准备数据库表

(2)准备配置文件

(3)Docker部署

3、微服务集成Seata

(1)引入依赖

(2)改造配置

(3)改造数据库表

(4)测试

4、如何实现分布式事务

(1)XA模式

(2)AT模式

简述AT模式与XA模式最大的区别是什么?


一、微服务保护

微服务保护:保证服务运行的健壮性,避免级联失败导致的雪崩问题。

1、服务保护方案

微服务保护的方案有很多,比如:

  • 请求限流

  • 线程隔离

  • 服务熔断

        这些方案或多或少都会导致服务的体验上略有下降,比如:

  • 请求限流:降低了并发上限
  • 线程隔离:降低了可用资源数量
  • 服务熔断:降低了服务的完整度,部分服务变的不可用或弱可用。

        因此这些方案都属于服务降级的方案。但通过这些方案,服务的健壮性得到了提升,接下来,我们就逐一了解这些方案的原理。

(1)请求限流

        服务故障最重要原因,就是并发太高,解决了这个问题,就能避免大部分故障。当然,接口的并发不是一直很高,而是突发的。因此请求限流,就是限制或控制接口访问的并发流量,避免服务因流量激增而出现故障。

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

(2)线程隔离

        当一个业务接口响应时间长,而且并发高时,就可能耗尽服务器的线程资源,导致服务内的其它接口受到影响。所以我们必须把这种影响降低,或者缩减影响的范围。线程隔离正是解决这个问题的好办法。

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

    

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

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

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

(3)服务熔断

        线程隔离虽然避免了雪崩问题,但故障服务(商品服务)依然会拖慢服务调用方(购物车服务)的接口响应速度。而且商品查询的故障依然会导致查询购物车功能出现故障,购物车业务也变的不可用了。

所以,我们要做两件事情:

  • 编写服务降级逻辑:服务调用失败后的处理逻辑,根据业务场景,可以抛出异常,也可以返回友好提示或默认数据。

  • 异常统计和熔断:统计服务提供方的异常比例,当比例过高表明该接口会影响到其它服务,应该拒绝调用该接口,而是直接走降级逻辑。

 

2、Sentinel

        微服务保护的技术有很多,但在目前国内使用较多的还是Sentinel,所以接下来我们学习Sentinel的使用。

(1)安装

        Sentinel是阿里巴巴开源的一款服务保护框架,目前已经加入SpringCloudAlibaba中。

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

  • 核心库(Jar包):不依赖任何框架/库,能够运行于 Java 8 及以上的版本的运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。在项目中引入依赖即可实现服务限流、隔离、熔断等功能。

  • 控制台(Dashboard):Dashboard 主要负责管理推送规则、监控、管理机器信息等。

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

【1】下载jar包

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

【3】运行如下命令启动控制台:

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

启用sentinel时这个终端窗口不要关闭!!

【4】访问http://localhost:8090页面,就可以看到sentinel的控制台了:

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

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

(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: localhost:8090

【3】访问cart-service的任意端点

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

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

簇点链路 = 当前服务内部所有被 Sentinel 监控的 API 接口的调用关系视图

        在 Spring MVC 中,每一个被 @RequestMapping标注的接口(比如 @GetMapping("/user/{id}")),默认都会被 Sentinel 自动识别为一个【资源】,并纳入监控。Sentinel 通过默认拦截 Spring MVC 的端点,自动帮你收集这些资源,方便你针对每个接口做限流熔断。

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

【5】配置接口名格式

        但我们的SpringMVC接口是按照Restful风格设计,因此购物车的查询、删除、修改等接口全部都是/carts路径:

        默认情况下Sentinel会把路径作为簇点资源的名称,无法区分路径相同但请求方式不同的接口,查询、删除、修改等都被识别为一个簇点资源,这显然是不合适的。

        所以我们可以选择打开Sentinel的请求方式前缀,把【请求方式 + 请求路径】作为簇点资源名:

首先,在cart-serviceapplication.yml中添加下面的配置:

spring:
  cloud:
    sentinel:
      transport:
        dashboard: localhost:8090
      http-method-specify: true # 开启请求方式前缀

        然后,重启服务,通过页面访问购物车的相关接口,可以看到sentinel控制台的簇点链路发生了变化:

3、请求限流

在簇点链路后面点击流控按钮,即可对其做限流配置:

这样就把查询购物车列表这个簇点资源的流量限制在了每秒6个,也就是最大QPS为6

利用Jemeter做限流测试,我们每秒发出10个请求:

最终监控结果如下:

可以看出GET:/carts这个接口的通过QPS稳定在6附近,而拒绝的QPS在4附近,符合我们的预期。

4、线程隔离

        限流可以降低服务器压力,尽量减少因并发流量引起的服务故障的概率,但并不能完全避免服务故障。一旦某个服务出现故障,我们必须隔离对这个服务的调用,避免发生雪崩。

        比如,查询购物车的时候需要查询商品,为了避免因商品服务出现故障导致购物车服务级联失败,我们可以把购物车业务中【查询商品】的部分隔离起来,限制可用的线程资源。

        这样,即便商品服务出现故障,最多导致【查询购物车业务】故障,并且可用的线程资源也被限定在一定范围,不会导致整个购物车服务崩溃。

        所以,我们要对查询商品的FeignClient接口做线程隔离。

(1)OpenFeign整合Sentinel

【1】修改cart-service模块的application.yml文件,开启Feign的sentinel功能:

feign:
  sentinel:
    enabled: true # 开启feign对sentinel的支持

❗️需要注意的是,默认情况下SpringBoot项目的tomcat最大线程数是200,允许的最大连接是8492,单机测试很难打满。

【2】所以我们需要配置一下cart-service模块的application.yml文件,修改tomcat连接:

server:
  port: 8082
  tomcat:
    threads:
      max: 50 # 允许的最大线程数
    accept-count: 50 # 最大排队等待数量
    max-connections: 100 # 允许的最大连接

然后重启cart-service服务,可以看到查询商品的FeignClient自动变成了一个簇点资源:

(2)配置线程隔离

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

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

我们利用Jemeter测试,每秒发送100个请求:

最终测试结果如下:

进入查询购物车的请求每秒大概在100,而在查询商品时却只剩下每秒10左右,符合我们的预期。

        此时如果我们通过页面访问购物车的其它接口,例如添加购物车、修改购物车商品数量,发现不受影响:

        响应时间非常短,这就证明线程隔离起到了作用,尽管查询购物车这个接口并发很高,但是它能使用的线程资源被限制了,因此不会影响到其它接口。

5、服务熔断

        在上一小节,我们利用线程隔离对查询购物车业务进行隔离,保护了购物车服务的其它接口。

        由于查询商品的功能耗时较高,再加上线程隔离限定了线程数为5,导致接口吞吐能力有限,最终QPS只有10左右。这就导致了几个问题:

  • 超出的QPS上限的请求就只能抛出异常,从而导致购物车的查询失败。
    • 但从业务角度来说,即便没有查询到最新的商品信息,购物车也应该展示给用户,用户体验更好。
    • 解决方法:给查询失败设置一个降级处理逻辑。
  • 由于查询商品的延迟较高,从而导致查询购物车的响应时间也变的很长。
    • 这样不仅拖慢了购物车服务,消耗了购物车服务的更多资源,而且用户体验也很差。
    • 对于商品服务这种不太健康的接口,我们应该直接停止调用,直接走降级逻辑,避免影响到当前服务。
    • 解决方法:将商品查询接口熔断

(1)编写降级逻辑

        触发限流或熔断后的请求不一定要直接报错,也可以返回一些默认数据或者友好提示,用户体验会更好。

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

  • 方式一:FallbackClass,无法对远程调用的异常做处理

  • 方式二:FallbackFactory,可以对远程调用的异常做处理,我们一般选择这种方式。

        这里我们采取方式二的失败降级处理。

【1】在hm-api模块中给ItemClient定义降级处理类,实现FallbackFactory

        当通过Feign调用远程ItemClient服务失败时,会自动执行这里定义的降级逻辑,避免因服务不可用导致整个调用链崩溃。

@Slf4j
public class ItemClientFallback 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);
                // 查询购物车允许失败,查询失败,返回空集合
                return CollUtils.emptyList();
            }

            @Override
            public void deductStock(List<OrderDetailDTO> orderDetailDTOS) {
                // 库存扣减业务需要触发事务回滚,查询失败,抛出异常
                throw new BizIllegalException(cause);
            }
        };
    }
}

【2】在hm-api模块中的com.hmall.api.config.DefaultFeignConfig类中将ItemClientFallback注册为一个Bean

    @Bean
    public ItemClientFallback itemClientFallback(){
        return new ItemClientFallback();
    }

【3】在hm-api模块中的ItemClient接口中使用ItemClientFallbackFactory

@FeignClient(value = "item-service", // 1. 服务名称
    configuration = DefaultFeignConfig.class, // 2. 自定义配置
    fallbackFactory = ItemClientFallback.class) // 3. 服务降级工厂

【4】测试

重启后,再次测试,发现被限流的请求不再报错,走了降级逻辑:

但是未被限流的请求延时依然很高,导致最终的平均响应时间较长。

        这是因为没有进行服务熔断:调用的其中一个服务已经非常慢了,但是后续的请求还是源源不断地来,导致接受服务的请求很慢。所以我们可以统计慢请求的比例,如果超过该比例,则进行熔断,让后面来的服务直接走fallback逻辑,减少平均响应时间。

(2)服务熔断

        查询商品的延迟较高(模拟的500ms),从而导致查询购物车的延迟也变的很长。这样不仅拖慢了购物车服务,消耗了购物车服务的更多资源,而且用户体验也很差。

        对于商品服务这种不太健康的接口,我们应该停止调用,直接走降级逻辑,避免影响到当前服务。也就是将商品查询接口熔断,当商品服务接口恢复正常后,再允许调用,这其实就是断路器的工作模式了。

        Sentinel中的断路器不仅可以统计某个接口的慢请求比例,还可以统计异常请求比例

  • 当这些比例超出阈值时,就会熔断该接口,即拦截访问该接口的一切请求,降级处理。
  • 当该接口恢复正常时,再放行对于该接口的请求。

断路器的工作状态切换由一个状态机来控制:

状态机包括三个状态:

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

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

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

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

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

我们可以在控制台通过点击簇点后的【熔断】按钮来配置熔断策略:

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

  • RT(延迟)超过200ms的请求调用就是慢调用

  • 统计最近1000ms内的最少5次请求,如果慢调用比例不低于0.5,则触发熔断

  • 熔断持续时长20s

配置完成后,再次利用Jemeter测试,可以发现:

        在一开始一段时间是允许访问的,后来触发熔断后,查询商品服务的接口【通过QPS直接为0】,所有请求都被熔断了,而查询购物车的本身并没有受到影响。

        ❓为什么图中两个服务的QPS在同一时间段内都降为0 ?

        答:服务熔断后,item-service 服务不可用,而carts接口在那时需要调用items接口,因为items服务被熔断,因此carts服务在那时拒绝QPS和通过QPS均为0。

        此时整个购物车查询服务的平均RT影响不大。

二、分布式事务

        由于订单、购物车、商品分别在三个不同的微服务,而每个微服务都有自己独立的数据库,因此下单过程中就会跨多个数据库完成业务。而每个微服务都会执行自己的本地事务:

        整个业务中,各个本地事务是有关联的,因此每个微服务的本地事务,也可以称为分支事务。多个有关联的分支事务一起就组成了全局事务我们必须保证整个全局事务同时成功或失败

        我们知道每一个分支事务就是传统的单体事务,都可以满足ACID特性(原子性),但全局事务跨越多个服务、多个数据库,是否还能满足呢?

        通过测试,我们会发现:在购物车页面选中全部商品,然后在数据库将其中某件商品的库存改为0,返回购物车页面点击下单,最终因库存不足导致下单失败。但是,当我们查看购物车列表时,发现购物车数据依然被清空了,并未回滚。

        事务并未遵循ACID的原则,归其原因就是参与事务的多个子业务在不同的微服务,跨越了不同的数据库。虽然每个单独的业务都能在本地遵循ACID,但是它们互相之间没有感知,不知道有人失败了,无法保证最终结果的统一,也就无法遵循ACID的事务特性。

        这就是分布式事务问题,出现以下情况之一就可能产生分布式事务问题:

  • 业务跨多个服务实现

  • 业务跨多个数据源实现

        接下来这一节我们就一起来研究下如何解决分布式事务问题。

1、Seata事务管理

        解决分布式事务的方案有很多,但实现起来都比较复杂,因此我们一般会使用开源的框架来解决分布式事务问题。在众多的开源分布式事务框架中,功能最完善、使用最多的就是阿里巴巴在2019年开源的Seata了。

        其实分布式事务产生的一个重要原因,就是参与事务的多个分支事务互相无感知,不知道彼此的执行状态。因此解决分布式事务的思想非常简单:

        就是找一个统一的事务协调者,与多个分支事务通信,检测每个分支事务的执行状态,保证全局事务下的每一个分支事务同时成功或失败即可。大多数的分布式事务框架都是基于这个理论来实现的。

        Seata也不例外,在Seata的事务管理中有三个重要的角色:

  • TC (Transaction Coordinator) - 事务协调者维护全局和分支事务的状态,协调全局事务提交或回滚。

  • TM (Transaction Manager) - 事务管理器定义全局事务的范围、开始全局事务、提交或回滚全局事务。

  • RM (Resource Manager) - 资源管理器管理分支事务,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

Seata的工作架构如图所示:

  • 其中,TMRM可以理解为Seata的客户端部分,引入到参与事务的微服务依赖中即可。
  • 将来TMRM就会协助微服务,实现本地分支事务与TC之间交互,实现事务的提交或回滚。
  • TC服务则是事务协调中心,是一个独立的微服务,需要单独部署。

2、部署TC服务

(1)准备数据库表

执行课前资料提供的seata-tc.sql,导入数据库表:

  • branch_table 分支表
  • global_table 全局表
  • 剩下两张lock表就是锁,TC服务会基于数据库表实现一个锁功能来确保分布式事务执行的过程中的线程安全

(2)准备配置文件

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

将整个seata文件夹拷贝到虚拟机的/root目录:

(3)Docker部署

需要注意,要确保nacos、mysql都在hm-net网络中。

如果某个容器不再hm-net网络,可以参考下面的命令将某容器加入指定网络:

docker network connect [网络名] [容器名]

在虚拟机的/root目录执行下面的命令:

docker run --name seata \
-p 8099:8099 \
-p 7099:7099 \
-e SEATA_IP=192.168.150.101 \
-v ./seata:/seata-server/resources \
--privileged=true \
--network hm-net \
-d \
seataio/seata-server:1.5.2

3、微服务集成Seata

        参与分布式事务的每一个微服务都需要集成Seata,我们以trade-service为例。

(1)引入依赖

        为了方便各个微服务集成seata,我们需要把seata配置共享到nacos,因此trade-service模块不仅仅要引入seata依赖,还要引入nacos依赖:

<!--统一配置管理-->
  <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
  </dependency>
  <!--读取bootstrap文件-->
  <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-bootstrap</artifactId>
  </dependency>
  <!--seata-->
  <dependency>
      <groupId>com.alibaba.cloud</groupId>
      <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
  </dependency>

(2)改造配置

        首先在nacos上添加一个共享的seata配置,命名为shared-seata.yaml

seata:
  registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
    type: nacos # 注册中心类型 nacos
    nacos:
      server-addr: 192.168.150.101:8848 # nacos地址
      namespace: "" # namespace,默认为空
      group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP
      application: seata-server # seata服务名称
      username: nacos
      password: nacos
  tx-service-group: hmall # 事务组名称
  service:
    vgroup-mapping: # 事务组与tc集群的映射关系
      hmall: "default"

然后,改造trade-service模块,添加bootstrap.yaml

spring:
  application:
    name: trade-service # 服务名称
  profiles:
    active: dev
  cloud:
    nacos:
      server-addr: 192.168.150.101 # nacos地址
      config:
        file-extension: yaml # 文件后缀名
        shared-configs: # 共享配置
          - dataId: shared-jdbc.yaml # 共享mybatis配置
          - dataId: shared-log.yaml # 共享日志配置
          - dataId: shared-swagger.yaml # 共享日志配置
          - dataId: shared-seata.yaml # 共享seata配置

可以看到这里加载了共享的seata配置。

然后改造application.yaml文件,内容如下:

server:
  port: 8085
feign:
  okhttp:
    enabled: true # 开启OKHttp连接池支持
  sentinel:
    enabled: true # 开启Feign对Sentinel的整合
hm:
  swagger:
    title: 交易服务接口文档
    package: com.hmall.trade.controller
  db:
    database: hm-trade

参考上述办法分别改造hm-carthm-item两个微服务模块。

(3)改造数据库表

seata的客户端在解决分布式事务的时候需要记录一些中间数据,保存在数据库中。

因此我们要先准备一个这样的表。

将课前资料的seata-at.sql分别文件导入hm-trade、hm-cart、hm-item三个数据库中:

至此,微服务整合的工作就完成了,可以参考上述方式对hm-itemhm-cart模块完成整合改造。

(4)测试

接下来测试分布式事务。

  • 找到trade-service模块下的OrderServiceImpl类中的createOrder方法,也就是下单业务方法。
  • 将其上的@Transactional注解改为Seata提供的@GlobalTransactional。

@GlobalTransactional注解就是在标记事务的起点,将来TM就会基于这个方法判断全局事务范围,初始化全局事务。重启trade-serviceitem-servicecart-service三个服务。

再次测试,发现分布式事务的问题解决了.

4、如何实现分布式事务

实现原理基于两阶段提交

一阶段

  • 事务协调者TC通知每个事务参与者RM执行本地事务

  • 本地事务执行完成后报告【事务执行状态】给事务协调者TC,此时事务不提交,继续持有数据库锁(等待其他微服务事务完成本地事务,保证操作原子性)

二阶段

  • 事务协调者TC基于一阶段的报告来判断下一步操作

    • 如果一阶段都成功,则通知所有事务参与者,提交事务

    • 如果一阶段任意一个参与者失败,则通知所有事务参与者回滚事务

(1)XA模式

【1】基本架构

Seata对原始的XA模式做了简单的封装和改造,以适应自己的事务模型,基本架构如图

一阶段:

1.1 【事务管理器TM】向【事务协调者TC】发起请求,开启一个全局事务

1.2 【TM】调用各个分支服务的【资源管理器RM】

1.3   各分支的【RM】向【TC】注册自己的分支事务

1.4   各分支在数据库中执行业务SQL操作,RM拦截事务提交

1.5   各分支的【RM】将自身事务的执行状态报告给【TC】

二阶段:

2.1 【TM】向【TC】发起提交or回滚全局事务的申请

2.1 【TC】接收到申请后,开始检查各分支事务的完成状态

2.3  如果一阶段所有分支事务都成功执行,则【TC】通知所有事务参与者:提交事务

           如果一阶段存在失败的分支事务,则【TC】通知所有事务参与者:回滚事务

【2】XA模式优缺点

优点:

  • 事务的强一致性,满足ACID原则

  • 常用数据库都支持,实现简单,并且没有代码侵入

缺点:

  • 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差

  • 依赖关系型数据库实现事务

【3】实现步骤

首先,我们要在配置文件中指定要采用的分布式事务模式。

我们可以在Nacos中的共享shared-seata.yaml配置文件中设置:

seata:
  data-source-proxy-mode: XA

其次,我们要利用@GlobalTransactional标记分布式事务的入口方法

(2)AT模式

AT模式同样是分阶段提交的事务模型,不过弥补了XA模型中资源锁定周期过长的缺陷

【1】基本架构

一阶段:

1.1 【事务管理器TM】向【事务协调者TC】发起请求,开启一个全局事务

1.2 【TM】调用各个分支服务的【资源管理器RM】

1.3   各分支的【RM】向【TC】注册自己的分支事务

1.4   各分支的【RM】在数据库中执行业务SQL操作,提交记录快照undo-log

1.5   各分支的【RM】将自身事务的执行状态报告给【TC】

二阶段:

2.1 【事务管理器TM】向【事务协调者TC】发起提交or回滚全局事务的申请

2.2 【TC】在收到申请后,检查所有分支事务的完成状态

2.3   若检查发现一阶段所有分支事务均成功完成,则【TC】通知所有【TM】提交各自的事务

        若发现一阶段中存在失败的分支事务,则【TC】通知所有【TM】回滚各自的事务

2.4   如果均成功,【TC】通知所有分支删除快照undo-log

        如果有一个失败,【TC】通知所有分支按照快照undo-log恢复数据

【2】梳理流程

用一个真实的业务来梳理下AT模式的原理。

比如,现在有一个数据库表,记录用户余额:

id

money

1

100

其中一个分支业务要执行的SQL为:

 update tb_account set money = money - 10 where id = 1

AT模式下,当前分支事务执行流程如下:

一阶段

  1. TM发起并注册全局事务到TC

  2. TM调用分支事务

  3. 分支事务准备执行业务SQL

  4. RM拦截业务SQL,根据where条件查询原始数据,形成快照。

{
  "id": 1, "money": 100
}

  1. RM执行业务SQL,提交本地事务,释放数据库锁。此时 money = 90

  2. RM报告本地事务状态给TC

二阶段

  1. TM通知TC事务结束

  2. TC检查分支事务状态

    1. 如果都成功,则立即删除快照

    2. 如果有分支事务失败,需要回滚。读取快照数据({"id": 1, "money": 100}),将快照恢复到数据库。此时数据库再次恢复为100

流程图如下:

【3】AT模式与XA模式的区别

简述AT模式与XA模式最大的区别是什么?
  • XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。

  • XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。

  • XA模式强一致;AT模式最终一致

可见,AT模式使用起来更加简单,无业务侵入,性能更好。因此企业90%的分布式事务都可以用AT模式来解决。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐