SpringCloud - Seata 分布式事务

前言

该博客为Sentinel学习笔记,主要目的是为了帮助后期快速复习使用
学习视频:7小快速通关SpringCloud
辅助文档:SpringCloud快速通关
源码地址:cloud-demo

一、简介

官网:https://seata.apache.org/zh-cn/
Seata 是一款开源的分布式事务解决方案,旨在解决微服务架构中跨服务的事务一致性问题。它支持多种事务模型(如AT、TCC、SAGA),能够保证跨多个服务和数据库的数据一致性,简化了分布式事务的管理。
在这里插入图片描述

为什么需要使用 Seata?

在分布式系统中,由于涉及多个微服务和多个数据库,事务的一致性和可靠性成为了一个挑战。传统的事务管理机制(如两阶段提交)不适用于复杂的微服务环境,因为它们的成本高、实现复杂且不易扩展。
Seata 的优势体现在以下几个方面:

  • 分布式事务支持Seata 提供了跨多个服务和数据库的事务支持,确保多个服务的操作能够像一个单一事务一样进行提交或回滚。
  • 性能优化Seata 采用高效的事务协调机制,通过减少分布式事务处理的开销,显著提升系统性能。
  • 可靠性和容错性Seata 的设计保证了事务的一致性和可靠性,即使在部分节点发生故障时,也能保证系统的正确性。
  • 简化开发:使用 Seata 时,开发者不需要自己编写复杂的分布式事务处理逻辑,Seata 提供了易用的注解和API,极大地简化了开发工作。
  • 灵活的事务模型Seata 提供了多种事务模型(ATTCCSAGA),可以根据具体业务场景选择合适的事务管理方式,满足不同的需求。
    在这里插入图片描述

二、快速入门

2.1 环境准备

在这里插入图片描述

2.2 原理

在这里插入图片描述

2.2.1 核心组件

Seata分布式事务的工作原理主要涉及三个核心组件:事务协调器TC)、事务管理器TM)和资源管理器RM)。以下是这些组件的详细工作原理:

  • 事务协调器(TC)TC负责维护全局事务的状态,并协调事务的提交或回滚。 它接收全局事务的开始、提交或回滚请求,协调各个服务的事务执行。TC在收到所有分支事务的提交或回滚确认后,将全局事务状态标记为结束。
  • 事务管理器(TM)TM负责定义和发起全局事务。 当TM发起一个全局事务时,TC创建一个全局事务ID,并将该事务状态设置为“开始”状态。TM通知TC提交事务,TC依次通知各RM提交其分支事务。如果任一分支事务执行失败或出现异常,TM通知TC回滚事务,TC会依次通知各RM进行回滚操作。
  • 资源管理器(RM)RM负责管理分支事务(Branch Transaction),并与本地数据库资源交互,确保各个分支事务的提交和回滚操作。 在全局事务的生命周期内,多个服务会分别执行属于自己的本地事务(即分支事务)。每个分支事务在执行前需要向TC注册,并绑定到全局事务ID上。每个服务的RM负责管理分支事务的执行,并与本地资源交互。执行完成后,RM会向TC报告分支事务的执行结果。

2.2.1 实现步骤

Seata的分布式事务管理通过以下步骤实现:

  1. 全局事务开始:当TM发起一个全局事务时,TC创建一个全局事务ID,并将该事务状态设置为“开始”状态。
  2. 注册分支事务:在全局事务的生命周期内,多个服务会分别执行属于自己的本地事务(即分支事务)。每个分支事务在执行前需要向TC注册,并绑定到全局事务ID上。
  3. 分支事务执行:每个服务的RM负责管理分支事务的执行,并与本地资源交互。执行完成后,RM会向TC报告分支事务的执行结果。
  4. 全局提交或回滚:当所有分支事务都执行成功时,TM通知TC提交事务,TC依次通知各RM提交其分支事务。如果任一分支事务执行失败或出现异常,TM通知TC回滚事务,TC会依次通知各RM进行回滚操作。
  5. 事务结束:TC在收到所有分支事务的提交或回滚确认后,将全局事务状态标记为结束。

2.3 seata-server

  1. 下载:https://seata.apache.org/zh-cn/download/seata-server
  2. 解压并启动:seata-server.bat
    在这里插入图片描述
    在这里插入图片描述
    Server端口【TC事务协调者】:8091
    Web可视化界面:http://localhost:7091,账号和密码都为seata
    在这里插入图片描述
    在这里插入图片描述

2.4 微服务配置

2.4.1 引依赖

	<!-- 分布式事务Seata -->
	<dependency>
	    <groupId>com.alibaba.cloud</groupId>
	    <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
	</dependency>

2.4.2 配置

每个微服务创建 file.conf文件,完整内容如下;
【微服务只需要复制 service 块配置即可】

#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements.  See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License.  You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

transport {
  # tcp, unix-domain-socket
  type = "TCP"
  #NIO, NATIVE
  server = "NIO"
  #enable heartbeat
  heartbeat = true
  # the tm client batch send request enable
  enableTmClientBatchSendRequest = false
  # the rm client batch send request enable
  enableRmClientBatchSendRequest = true
   # the rm client rpc request timeout
  rpcRmRequestTimeout = 2000
  # the tm client rpc request timeout
  rpcTmRequestTimeout = 30000
  # the rm client rpc request timeout
  rpcRmRequestTimeout = 15000
  #thread factory for netty
  threadFactory {
    bossThreadPrefix = "NettyBoss"
    workerThreadPrefix = "NettyServerNIOWorker"
    serverExecutorThread-prefix = "NettyServerBizHandler"
    shareBossWorker = false
    clientSelectorThreadPrefix = "NettyClientSelector"
    clientSelectorThreadSize = 1
    clientWorkerThreadPrefix = "NettyClientWorkerThread"
    # netty boss thread size
    bossThreadSize = 1
    #auto default pin or 8
    workerThreadSize = "default"
  }
  shutdown {
    # when destroy server, wait seconds
    wait = 3
  }
  serialization = "seata"
  compressor = "none"
}
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
}

client {
  rm {
    asyncCommitBufferLimit = 10000
    lock {
      retryInterval = 10
      retryTimes = 30
      retryPolicyBranchRollbackOnConflict = true
    }
    reportRetryCount = 5
    tableMetaCheckEnable = false
    tableMetaCheckerInterval = 60000
    reportSuccessEnable = false
    sagaBranchRegisterEnable = false
    sagaJsonParser = "fastjson"
    sagaRetryPersistModeUpdate = false
    sagaCompensatePersistModeUpdate = false
    tccActionInterceptorOrder = -2147482648 #Ordered.HIGHEST_PRECEDENCE + 1000
    sqlParserType = "druid"
    branchExecutionTimeoutXA = 60000
    connectionTwoPhaseHoldTimeoutXA = 10000
  }
  tm {
    commitRetryCount = 5
    rollbackRetryCount = 5
    defaultGlobalTransactionTimeout = 60000
    degradeCheck = false
    degradeCheckPeriod = 2000
    degradeCheckAllowTimes = 10
    interceptorOrder = -2147482648 #Ordered.HIGHEST_PRECEDENCE + 1000
  }
  undo {
    dataValidation = true
    onlyCareUpdateColumns = true
    logSerialization = "jackson"
    logTable = "undo_log"
    compress {
      enable = true
      # allow zip, gzip, deflater, lz4, bzip2, zstd default is zip
      type = zip
      # if rollback info size > threshold, then will be compress
      # allow k m g t
      threshold = 64k
    }
  }
  loadBalance {
      type = "XID"
      virtualNodes = 10
  }
}
log {
  exceptionRate = 100
}
tcc {
  fence {
    # tcc fence log table name
    logTableName = tcc_fence_log
    # tcc fence log clean period
    cleanPeriod = 1h
  }
}

2.4.3 开启全局事务

在最大的方法入口,标记@GlobalTransactional,即可开启全局事务

2.5 二阶提交协议原理

第一阶段:本地事务

  1. 全局事务开始:业务层发起全局事务请求,事务协调者(TC)注册全局事务并生成全局事务ID。
  2. 分支事务注册:各个分支事务(如扣减库存、创建订单、扣减余额)分别向TC注册,申请必要的资源锁(如数据库表的记录锁)。
  3. 执行本地事务
    • 扣减库存:扣减库存:执行SQL update storage_tbl set count = count - 2 where commodity_code = 'P0001',更新库存数量。
    • 创建订单:执行订单创建相关的本地事务。
    • 扣减余额:执行扣减账户余额的本地事务。
  4. 记录前后镜像:在执行业务SQL前后,分别查询并记录数据的前后镜像,用于生成回滚日志(UNDO LOG)。
  5. 插入回滚日志:将前后镜像组成的回滚日志插入到UNDO LOG表中,以便在需要时进行数据回滚。
  6. 本地事务提交:将业务数据和UNDO LOG一起保存,并汇报自己提交成功与否的结果。

第二阶段:提交阶段

  1. 分支提交
    • 收到TC的提交请求后,各个分支事务立即响应OK,表示准备就绪。
    • 给异步任务队列中添加异步任务,异步和批量地删除相应的UNDO LOG记录。
  2. 分支回滚
    • 如果TC的提交请求未能成功,或者在第一阶段中某个分支事务失败,TC将发起回滚请求。
    • 各个分支事务收到回滚请求后,开启一个本地事务,执行以下任务:
      • 找到对应的UNDO LOG记录。
      • 数据校验:比较后镜像与当前数据,如果不一致,则说明数据被其他渠道修改,需要进行相应处理;如果一致,则执行回滚。
      • 回滚数据:根据UNDO LOG的前镜像内容执行数据修改,完成后删除UNDO LOG

通过以上流程,Seata确保了分布式事务的原子性,即所有分支事务要么全部成功提交,要么全部回滚,从而保证了数据的一致性
在这里插入图片描述

2.6 二阶提交协议可视化

2.6.1 观察初始数据库

在这里插入图片描述

2.6.2 设置断点

我们在业务服务BusinessServiceImpl的主方法purchase中给远程调用方法deductcreate加上断点
在这里插入图片描述
并在订单服务OrderServiceImpl的create方法中添加一个除零异常,并在前面加上一个断点
在这里插入图片描述

2.6.3 发起请求,观察数据变化

在浏览器发送请求,http://localhost:11000/purchase?userId=1&commodityCode=P0001&count=2
这里,我们来到第一个断点扣减库存deduct,可以看到控制台打印开启了一个新的全局事务
在这里插入图片描述
此时在Seata控制台中可以观测到这个全局事务
在这里插入图片描述当前没有任何数据提交,即没有全局锁 -> 只有第一阶段本地提交,要修改数据之前,才会申请自己数据的记录锁,即全局锁

在这里插入图片描述
继续放行到第二个断点位置,创建订单create,观察Seata控制台,开启分支事务并查看
可以看到storage_db的分支事务
在这里插入图片描述
而且在全局锁中也可以看到数据 -> 分支事务一定会提交一个数据
在这里插入图片描述
继续观察storage_db数据库,可以看到库存已经被扣减,并且在undo_log表中有一条记录
在这里插入图片描述
在这里插入图片描述
查看rollback_info字段的内容,
在前镜像" beforeImage "中可以看到,在记录修改之前的值为100 "value": 100
后镜像"afterImage",扣减库存后的数据为,"value": 98

{
  "@class": "org.apache.seata.rm.datasource.undo.BranchUndoLog",
  "xid": "192.168.4.105:8091:7782838703486791689",
  "branchId": 7782838703486791690,
  "sqlUndoLogs": [
    "java.util.ArrayList",
    [
      {
        "@class": "org.apache.seata.rm.datasource.undo.SQLUndoLog",
        "sqlType": "UPDATE",
        "tableName": "storage_tbl",
        "beforeImage": {
          "@class": "org.apache.seata.rm.datasource.sql.struct.TableRecords",
          "tableName": "storage_tbl",
          "rows": [
            "java.util.ArrayList",
            [
              {
                "@class": "org.apache.seata.rm.datasource.sql.struct.Row",
                "fields": [
                  "java.util.ArrayList",
                  [
                    {
                      "@class": "org.apache.seata.rm.datasource.sql.struct.Field",
                      "name": "count",
                      "keyType": "NULL",
                      "type": 4,
                      "value": 100
                    },
                    {
                      "@class": "org.apache.seata.rm.datasource.sql.struct.Field",
                      "name": "id",
                      "keyType": "PRIMARY_KEY",
                      "type": 4,
                      "value": 1
                    }
                  ]
                }
              }
            ]
          ]
        },
        "afterImage": {
          "@class": "org.apache.seata.rm.datasource.sql.struct.TableRecords",
          "tableName": "storage_tbl",
          "rows": [
            "java.util.ArrayList",
            [
              {
                "@class": "org.apache.seata.rm.datasource.sql.struct.Row",
                "fields": [
                  "java.util.ArrayList",
                  [
                    {
                      "@class": "org.apache.seata.rm.datasource.sql.struct.Field",
                      "name": "count",
                      "keyType": "NULL",
                      "type": 4,
                      "value": 98
                    },
                    {
                      "@class": "org.apache.seata.rm.datasource.sql.struct.Field",
                      "name": "id",
                      "keyType": "PRIMARY_KEY",
                      "type": 4,
                      "value": 1
                    }
                  ]
                }
              }
            ]
          ]
        }
      }
    ]
  ]
}

继续放行到第三个断点位置,保存订单orderTblMapper.insert(orderTbl);,观察Seata控制台
可以看到多了account_db的分支事务和全局锁
在这里插入图片描述
在这里插入图片描述
此时数据库的余额和库存均被扣减,并且生成了UNDO LOG记录
在这里插入图片描述
此时继续放行,即会执行int i = 1/0;抛出除零异常,所有数据全部按照前镜像回滚,UNDO LOG记录
在这里插入图片描述

注意:事务事务超时时间为一分钟,调试过程中,一旦超时会报一下错误
may be has finished -> 事务可能完成
branch register failed -> 分支注册事务失败

2025-02-13T22:41:16.010+08:00 ERROR 19556 --- [seata-storage] [io-13000-exec-9] o.a.c.c.C.[.[.[/].[dispatcherServlet]    : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.transaction.TransactionSystemException: JDBC commit failed] with root cause

org.apache.seata.core.exception.RmTransactionException: branch register failed, xid: 192.168.4.105:8091:7782838703486791747, errMsg: TransactionException[Could not found global transaction xid = 192.168.4.105:8091:7782838703486791747, may be has finished.] 

2.7 四种事务模式

2.7.1 AT模式

这是Seata中最常用的一种分布式事务模式,它通过自动的预提交、回滚操作,实现分布式事务的透明化管理。AT模式的核心在于自动处理数据库的事务操作,不需要对业务代码进行侵入性的修改。

2.7.2 TCC模式

适用于需要显式控制分布式事务的场景,确保所有参与者都遵循统一的规则。TCC模式通过三阶段的提交(Try、Confirm、Cancel)来保证最终一致性。适用于夹杂非数据库操作的事务。

2.7.3 Saga模式

适用于复杂的业务流程,每个步骤都可以独立地进行业务逻辑处理。在Saga模式中,业务流程中每个参与者都提交本地事务,当出现某一个参与者失败则补偿前面已经成功的参与者,一阶段正向服务和二阶段补偿服务都由业务开发实现。

2.7.4 XA模式

适用于需要兼容传统数据库系统的场景,支持多种资源管理器。XA模式是强一致性分阶段事务模式,牺牲了一定的可用性,无业务侵入。

2.7.5 切换模式

添加application.yml配置

seata:
  data-source-proxy-mode: AT #默认为AT模式

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/969963.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Java面试宝典:说下Spring Bean的生命周期?

Java面试宝典专栏范围&#xff1a;JAVA基础&#xff0c;面向对象编程&#xff08;OOP&#xff09;&#xff0c;异常处理&#xff0c;集合框架&#xff0c;Java I/O&#xff0c;多线程编程&#xff0c;设计模式&#xff0c;网络编程&#xff0c;框架和工具等全方位面试题详解 每…

基于Swift实现仿IOS闹钟

仿 iOS 系统闹钟 添加闹钟效果图 收到通知效果图 更新日志 2018.09.12 由于 iOS 系统限制了注册本地推送的数量&#xff0c;最大的注册量为 64 条&#xff0c;且一旦超出 64 条&#xff0c;所有的推送都将失效&#xff0c;故而在添加推送的时候做了一个判断&#xff0c;超过…

如何使用 DeepSeek R1 构建开源 ChatGPT Operator 替代方案

开源大型语言模型&#xff08;LLMs&#xff09;的兴起使得创建 AI 驱动的工具比以往任何时候都更容易&#xff0c;这些工具可以与 OpenAI 的 ChatGPT Operator 等专有解决方案相媲美。在这些开源模型中&#xff0c;DeepSeek R1 以其强大的推理能力、自由的可访问性和适应性而脱…

力反馈设备在工厂生产中遥操作机器人的应用优势

工业自动化与智能化已经成为现代工厂提升生产效率、保障人员安全的关键手段。在这一背景下&#xff0c;Haption Virtuose力反馈设备凭借其卓越的性能和广泛的应用前景&#xff0c;在机器人遥操作领域脱颖而出&#xff0c;尤其在工厂生产中展现出了显著的应用优势。本文将深入探…

【STM32】输入捕获实现超声波测距

1.超声波测距原理 &#xff08;超声波发出到 遇到障碍物反弹回来的时间&#xff09;*声速/2就是到障碍物的距离 操作过程&#xff1a; 单片机给TRIG引脚输出一个脉冲&#xff0c;然后超声波模块会将ECHO电平拉高&#xff0c;当超声波遇到障碍物回来时&#xff0c;ECHO电平就会…

phpipam1.7安装部署

0软件说明 phpipam是一个开源Web IP地址管理应用程序&#xff08;IPAM&#xff09; phpipam官网&#xff1a;https://www.phpipam.net/ 1安装环境 操作系统&#xff1a;Rocky Linux9.5x86_64 phpipam版本&#xff1a;1.7 php版本&#xff1a;8.0.30 数据库版本&#xff1a…

【C语言】C语言 好声音比赛管理系统(含源码+数据文件)【独一无二】

&#x1f449;博__主&#x1f448;&#xff1a;米码收割机 &#x1f449;技__能&#x1f448;&#xff1a;C/Python语言 &#x1f449;专__注&#x1f448;&#xff1a;专注主流机器人、人工智能等相关领域的开发、测试技术。 【C语言】C语言 好声音比赛管理系统&#xff08;含…

WPF进阶 | 深入 WPF 依赖项属性:理解其强大功能与应用场景

WPF进阶 | 深入 WPF 依赖项属性&#xff1a;理解其强大功能与应用场景 前言一、依赖项属性基础概念1.1 什么是依赖项属性1.2 依赖项属性与 CLR 属性的区别1.3 依赖项属性的定义与注册 二、依赖项属性的原理深入剖析2.1 依赖项属性系统的工作机制2.2 元数据&#xff08;Metadata…

QML使用ChartView绘制饼状图

一、工程配置 首先修改CMakeLists.txt&#xff0c;按下图修改&#xff1a; find_package(Qt6 6.4 REQUIRED COMPONENTS Quick Widgets) PRIVATEtarget_link_libraries(appuntitledPRIVATE Qt6::QuickPRIVATE Qt6::Widgets )其次修改main.cpp&#xff0c;按下图修改&#xff…

单片机上SPI和IIC的区别

SPI&#xff08;Serial Peripheral Interface&#xff09;和IC&#xff08;Inter-Integrated Circuit&#xff09;是两种常用的嵌入式外设通信协议&#xff0c;它们各有优缺点&#xff0c;适用于不同的场景。以下是它们的详细对比&#xff1a; — 1. 基本概念 SPI&#xff0…

2025年02月12日Github流行趋势

项目名称&#xff1a;data-formulator 项目地址url&#xff1a;https://github.com/microsoft/data-formulator 项目语言&#xff1a;TypeScript 历史star数&#xff1a;4427 今日star数&#xff1a;729 项目维护者&#xff1a;danmarshall, Chenglong-MS, apps/dependabot, mi…

LeetCode《算法通关手册》 1.2 数组排序

Python强推&#xff1a;算法通关手册&#xff08;LeetCode&#xff09; | 算法通关手册&#xff08;LeetCode&#xff09; (itcharge.cn) 目录 文章目录 1.2 数组排序1.2.1 选择排序1.2.2 冒泡排序[283. 移动零 - 力扣&#xff08;LeetCode&#xff09;](https://leetcode.cn/p…

DeepSeek R1打造本地化RAG知识库

本文将详细介绍如何使用Ollama、Deepseek R1大语音模型、Nomic-Embed-Text向量模型和AnythingLLM共同搭建一个本地的私有RAG知识库。 一. 准备工作 什么是RAG&#xff1f; RAG是一种结合了信息检索和大模型&#xff08;LLM&#xff09;的技术&#xff0c;在对抗大模型幻觉、…

网页版贪吃蛇小游戏开发HTML实现附源码!

项目背景 贪吃蛇是一款经典的休闲小游戏&#xff0c;因其简单易玩的机制和丰富的变形而深受玩家喜爱。本次开发目标是实现一款网页版贪吃蛇小游戏&#xff0c;并通过前端与后端结合的方式&#xff0c;提供一个流畅的在线体验。 实现过程 游戏逻辑设计 蛇的移动&#xff1a;…

简易 Shell 实现指南

目录 前言&#xff1a; 一、代码中的核心功能 1. 环境变量获取 2. 当前路径处理 3. 用户输入处理 4. 命令解析 5. 内建命令处理 6. 外部命令执行 7. 错误处理 二、代码中涉及的关键知识点 1. 系统调用 2. 环境变量 3. 字符串处理 4. 文件操作 5. 进程管理 三、…

快速排序

目录 什么是快速排序&#xff1a; 图解&#xff1a; 递归法&#xff1a; 方法一&#xff08;Hoare法&#xff09;&#xff1a; 代码实现&#xff1a; 思路分析&#xff1a; 方法二&#xff08;挖坑法&#xff09;&#xff1a; 代码实现&#xff1a; 思路分析&#xff1a; 非递…

网络安全尹毅 《网络安全》

一 网络安全基本概念 1.网络安全定义 安全在字典中的定义是为了防范间谍活动或蓄意破坏、犯罪、攻击而采取的措施。网络安全就是为了防范计算机网络硬件、软件、数据被偶然或蓄意破坏、篡改、窃听、假冒、泄露、非法访问以及保护网络系统持续有效工作的措施总和。网络安全保护…

6.appender

文章目录 一、前言二、源码解析AppenderUnsynchronizedAppenderBaseOutputStreamAppenderConsoleAppenderFileAppenderRollingFileAppenderFileNamePattern 三、总结 一、前言 前一篇文章介绍了appender、conversionRule、root和logger节点的解析, 为的是为本篇详细介绍它们的…

P9584 「MXOI Round 1」城市

题目描述 小 C 是 F 国的总统&#xff0c;尽管这个国家仅存在于网络游戏中&#xff0c;但他确实是这个国家的总统。 F 国由 n 个城市构成&#xff0c;这 n 个城市之间由 n−1 条双向道路互相连接。保证从任意一个城市出发&#xff0c;都能通过这 n−1 条双向道路&#xff0c;…

什么是Docker多架构容器镜像

什么是Docker多架构容器镜像 在 Docker 中&#xff0c;同一个 Docker 镜像可以在不同的平台上运行&#xff0c;例如在 x86、ARM、PowerPC 等不同的 CPU 架构上。 为了支持这种多平台的镜像构建和管理&#xff0c;Docker 在 17.06 版本时引入了 Manifest 的概念&#xff0c;在…