图解分布式事务实现原理(三)

参考

本文参考https://zhuanlan.zhihu.com/p/650791238从零到一搭建 TCC 分布式事务框架,并在小徐的基础上增加个人见解+笔记。
项目地址:https://github.com/xiaoxuxiansheng/gotcc
图解分布式事务实现原理(一):https://blog.csdn.net/qq_40318498/article/details/134430322?spm=1001.2014.3001.5502
图解分布式事务实现原理(二):https://blog.csdn.net/qq_40318498/article/details/134432420?spm=1001.2014.3001.5502


整体架构

TCC 本质上是一种 2PC(two phase commitment protocal 两阶段提交)的实现:

  • 把分布式事务中,负责维护状态数据变更的模块,封装成一个 TCC 组件
  • 把数据的变更状态拆分为对应 Try 操作的【冻结】、对应 Confirm 操作的【成功】以及对应 Cancel 操作的【失败回滚】
  • 抽出一个统筹全局的事务协调者角色 TXManager. 在执行分布式事务时,分为两个阶段:
    • 阶段 I:先对所有组件执行 Try 操作
    • 阶段 II:根据上阶段 Try 操作的执行结果,决定本轮执行 Confirm 还是 Cancel 操作

在这里插入图片描述

在我们实现 TCC 框架的实战环节中,首先需要明确的事情是:

  • 哪部分内容在 TCC 架构中属于通用的流程,这部分内容可以抽取出来放在 sdk 中,以供后续复用
  • 哪部分内容需要给使用方预留出足够的自由度,由使用方自行实现,然后和通用 sdk 进行接轨.

这两点也是很好想,最终,这两部分内容明确如下:

  • 在 TCC sdk 中实现的通用逻辑包含了和事务协调器 txManager 有关的核心流程
  • 事务协调器 TXManager 开启事务以及 try-confirm/cancel 的 2PC 流程串联
  • 事务协调器 TXManager 异步轮询任务,用于推进事务从中间态走向终态
  • TCC 组件的注册流程
  • 需要预定义事务日志存储模块 TXStore 的实现规范(声明 interface)
  • 需要预定义 TCC 组件 TCCComponent 的实现规范(声明 interface)

以及使用方

  • TCC 组件和 TXStore 两部分内容需要由使用方自行实现:
  • 使用方自行实现 TCCComponent 类,包括其 Try、Confirm、Cancel 方法的执行逻辑
  • 使用方自行实现具体的 TXStore 日志存储模块. 可以根据实际需要,选型合适的存储组件和存储方式

在这里插入图片描述

TCC Component

现在我们来思考一下TCC(Try-Confirm-Cancel)组件的定位和功能要求,以下是对每个要点的解释

  • 用户自行实现:TCC组件是由用户自行实现的部分,这意味着开发者需要编写自定义的TCC组件代码来处理事务的Try、Confirm和Cancel操作。用户需要根据业务逻辑和需求来实现这些组件。

  • 注册到RegistryCenter:TCC组件需要在TXManager启动时注册到注册中心(RegistryCenter)。这意味着用户需要配置TXManager以连接到注册中心,并将自定义的TCC组件注册到注册中心,以便TXManager能够获取并使用这些组件。

  • TXManager启动时获取组件:当使用方(应用程序)调用TXManager开启事务时,TXManager会通过注册中心获取已注册的TCC组件。这样,TXManager能够识别并使用用户自定义的TCC组件来执行事务的Try、Confirm和Cancel操作。

  • TCC组件的能力:TCC组件需要具备以下能力:

实现Try、Confirm和Cancel操作:TCC组件需要实现事务的Try、Confirm和Cancel三个阶段的操作,以确保事务的一致性
处理业务逻辑:TCC组件需要能够执行与具体业务相关的操作,包括业务检查、资源预留、业务确认和回滚操作
与TXManager进行交互:TCC组件需要与TXManager进行通信,以接收来自TXManager的指令,并汇报操作的执行结果(成功或失败)。

在这里插入图片描述
那么对应的代码实现可以简单化为:

// tcc 组件
type TCCComponent interface {
	// 返回组件唯一 id
	ID() string
	// 执行第一阶段的 try 操作
	Try(ctx context.Context, req *TCCReq) (*TCCResp, error)
	// 执行第二阶段的 confirm 操作
	Confirm(ctx context.Context, txID string) (*TCCResp, error)
	// 执行第二阶段的 cancel 操作
	Cancel(ctx context.Context, txID string) (*TCCResp, error)
}

这里我们只是简单定义了TCCComponent 接口。

TX Manager

下面是关于事务协调器 TXManager 的定位.

  • TXManager 是整个 TCC 架构中最核心的角色
  • TXManager 作为 gotcc 的统一入口,供使用方执行启动事务和注册组件的操作
  • TXManager 作为中枢系统分别和 RegisterCenter、TXStore 交互
  • TXManager 需要串联起整个 Try-Confirm/Canel 的 2PC 调用流程
  • TXManager 需要运行异步轮询任务,推进未完成的事务走向终态
    在这里插入图片描述

TX Store

TXStore 是用于存储和管理事务日志明细记录的模块:

  • 需要支持事务明细数据的 CRUD 能力
  • 通常情况下,底层需要应用到实际的存储组件作为支持
  • TXStore 在 gotcc 的 sdk 中体现为一个抽象的 interface. 需要由用户完成具体类的实现,并将其注入到 TXManager 当中.

在这里插入图片描述

// 事务日志存储模块
type TXStore interface {
	// 创建一条事务明细记录
	CreateTX(ctx context.Context, components ...component.TCCComponent) (txID string, err error)
	// 更新事务进度:实际更新的是每个组件的 try 请求响应结果
	TXUpdate(ctx context.Context, txID string, componentID string, accept bool) error
	// 提交事务的最终状态, 标识事务执行结果为成功或失败
	TXSubmit(ctx context.Context, txID string, success bool) error
	// 获取到所有未完成的事务
	GetHangingTXs(ctx context.Context) ([]*Transaction, error)
	// 获取指定的一笔事务
	GetTX(ctx context.Context, txID string) (*Transaction, error)
	// 锁住整个 TXStore 模块(要求为分布式锁)
	Lock(ctx context.Context, expireDuration time.Duration) error
	// 解锁TXStore 模块
	Unlock(ctx context.Context) error
}
  • CreateTX:创建一条事务明细记录,会在入参中传入本事务涉及的 TCC 组件列表,同时需要在出参中返回全局唯一的事务 id
  • TXUpdate:更新一条事务明细记录. 这里指的更新,针对于,事务中某个 TCC 组件 Try 响应状态的更新
  • TXSubmit:提交一条事务的执行结果. 要么置为成功,要么置为失败
  • GetHangingTXs:获取所有未完成的事务明细记录
  • GetTX:根据事务 id,获取指定的一条事务明细记录
  • Lock:锁住整个事务日志存储模块(要求为分布式锁)
  • Unlock:解锁整个事务日志存储模块

RegistryCenter

最后是 TCC 组件的注册管理中心 RegistryCenter,负责给 txManager 提供注册和查询 TCC 组件的能力.

在这里插入图片描述

TXManager 核心源码讲解

理完了基本的流程和概念,下面我们一起开启一线实战环节.

TXManager

在这里插入图片描述
下面是关于事务协调器 TXManager 的几个核心字段:

  • txStore:内置的事务日志存储模块,需要由使用方实现并完成注入
  • registryCenter:TCC 组件的注册管理中心
  • opts:内聚了一些 TXManager 的配置项,可以由使用方自定义,并通过 option 注入
  • ctx:用于反映 TXManager 运行生命周期的的 context,当 ctx 终止时,异步轮询任务也会随之退出
  • stop:用于停止 txManager 的控制器. 当 stop 被调用后,异步轮询任务会被终止
type TXManager struct {
    ctx            context.Context
    stop           context.CancelFunc
    opts           *Options
    txStore        TXStore
    registryCenter *registryCenter
}

func NewTXManager(txStore TXStore, opts ...Option) *TXManager {
    ctx, cancel := context.WithCancel(context.Background())
    txManager := TXManager{
        opts:           &Options{},
        txStore:        txStore,
        registryCenter: newRegistryCenter(),
        ctx:            ctx,
        stop:           cancel,
    }
    for _, opt := range opts {
        opt(txManager.opts)
    }
    
    repair(txManager.opts)
    go txManager.run()
    return &txManager
}

事务主流程

下面进入最核心的部分,介绍一下整个分布式事务的运行流程.

主流程

用户可以通过 txManager.Transaction 方法,一键启动动一个分布式事务流程,其中包含的几个核心步骤展示如下图:

在这里插入图片描述
txManager.Transaction 方法是用户启动分布式事务的入口,需要在入参中声明本次事务涉及到的组件以及需要在 Try 流程中传递给对应组件的请求参数:

type RequestEntity struct {
    // 组件名称
    ComponentID string `json:"componentName"`
    // Try 请求时传递的参数
    Request map[string]interface{} `json:"request"`
}

比如可以定义如下的参数:

componentAID := "componentA"
RequestEntity{
	ComponentID: componentAID,
	Request: map[string]interface{}{
		"biz_id": componentAID + "_biz",	
	}
}

txManager.Transaction 对应源码如下,核心步骤均给出了注释. 核心的 try-confirm/cancel 流程,会在后续的 txManager.twoPhaseCommit 方法中展开.

// 启动事务
func (t *TXManager) Transaction(ctx context.Context, reqs ...*RequestEntity) (bool, error) {
    // 1 限制分布式事务执行时长
    tctx, cancel := context.WithTimeout(ctx, t.opts.Timeout)
    defer cancel()

    // 2 获得所有的涉及使用的 tcc 组件
    componentEntities, err := t.getComponents(tctx, reqs...)
    if err != nil {
        return false, err
    }

    // 3 调用 txStore 模块,创建新的事务明细记录,并取得全局唯一的事务 id
    txID, err := t.txStore.CreateTX(tctx, componentEntities.ToComponents()...)
    if err != nil {
        return false, err
    }

    // 4. 开启两阶段提交流程:try-confirm/cancel
    return t.twoPhaseCommit(ctx, txID, componentEntities)
}

接下来我们看一下twoPhaseCommit函数。

2PC 串联

func (t *TXManager) twoPhaseCommit(ctx context.Context, txID string, componentEntities ComponentEntities) (bool, error) {
    // 1 创建子 context 用于管理子 goroutine 生命周期
    // 手握 cancel 终止器,能保证在需要的时候终止所有子 goroutine 生命周期
    cctx, cancel := context.WithCancel(ctx)
    defer cancel()

    // 2 创建一个 chan,用于接收子 goroutine 传递的错误
    errCh := make(chan error)
    // 3 并发启动,批量执行各 tcc 组件的 try 流程
    go func() {
        // 通过 waitGroup 进行多个子 goroutine 的汇总
        var wg sync.WaitGroup
        for _, componentEntity := range componentEntities {
            // shadow
            componentEntity := componentEntity
            wg.Add(1)
            // 并发执行各组件的 try 流程
            go func() {
                defer wg.Done()
                resp, err := componentEntity.Component.Try(cctx, &component.TCCReq{
                    ComponentID: componentEntity.Component.ID(),
                    TXID:        txID,
                    Data:        componentEntity.Request,
                })
                // 出现 tcc 组件执行 try 操作失败,则需要对事务明细记录进行更新,同时把错误通过 chan 抛给父 goroutine
                if err != nil || !resp.ACK {
                    // 对对应的事务进行更新
                    _ = t.txStore.TXUpdate(cctx, txID, componentEntity.Component.ID(), false)
                    errCh <- fmt.Errorf("component: %s try failed", componentEntity.Component.ID())
                    return
                }
                // try 请求成功,则对事务明细记录进行更新. 倘若更新失败,也要视为错误,抛给父 goroutine
                if err = t.txStore.TXUpdate(cctx, txID, componentEntity.Component.ID(), true); err != nil {
                    errCh <- err
                }
            }()
        }

        // 等待所有子 goroutine 运行完成
        wg.Wait()
        // 关闭 errCh,告知父 goroutine 所有任务已运行完成的信息
        close(errCh)
    }()

    successful := true
    // 4 通过 chan,阻塞子 goroutine 执行完成
    // 4.1 但凡出现一个子 goroutine 遇到了错误,则会提前接收到错误,并调用 cancel 方法熔断其他所有子 goroutine 流程
    // 4.2 倘若所有子 goroutine 都执行成功,则会通过 chan 的关闭事件推进流程,对应 err 为 nil
    if err := <-errCh; err != nil {
        // 只要有一笔 try 请求出现问题,其他的都进行终止
        cancel()
        successful = false
    }

    // 5 异步执行第二阶段的 confirm/cancel 流程
    // 之所以是异步,是因为实际上在第一阶段 try 的响应结果尘埃落定时,对应事务的成败已经有了定论
    // 第二阶段能够容忍异步执行的原因在于,执行失败时,还有轮询任务进行兜底
    go t.advanceProgressByTXID(txID)
    
    // 6 响应结果
    // 6.1 倘若所有 try 请求都成功,则 successful 为 try,事务成功
    // 6.2 但凡有一个 try 请求处理出现问题,successful 为 false,事务失败
    return successful, nil
}

该函数的入参是ctx,事务id以及所有的组件。

这段代码实现了一个分布式事务管理器中的两阶段提交(Two-Phase Commit, 2PC)过程。整个过程分为几个主要步骤:

  1. 创建子 Context 用于管理子 Goroutine 生命周期
    使用 context.WithCancel 创建一个可取消的子上下文(cctx)和对应的取消函数(cancel)。这样,在需要的时候可以通过调用 cancel 函数来终止所有子 Goroutine。
  2. 创建错误通道
    初始化一个错误通道(errCh),用于从子 Goroutine 接收错误信息。
  3. 并发启动,执行各组件的 Try 流程
    通过一个匿名 Goroutine 并发地启动各组件的 Try 流程。
    使用 sync.WaitGroup 来等待所有子 Goroutine 完成。
    在子 Goroutine 中:
    对每个组件执行 Try 方法。
    如果 Try 方法执行失败,或者响应的 ACK 字段为 false,则更新事务状态并通过错误通道发送错误。
    如果 Try 请求成功,更新事务状态。如果更新失败,也通过错误通道发送错误。
  4. 阻塞等待子 Goroutine 完成
    从错误通道接收错误。如果接收到错误,调用 cancel 函数终止所有子 Goroutine,并设置 successful 标记为 false。
  5. 异步执行第二阶段的 Confirm/Cancel 流程
    通过一个新的 Goroutine 调用 advanceProgressByTXID 函数来异步处理事务的 Confirm 或 Cancel 阶段。
    第二阶段的执行可以是异步的,因为第一阶段的结果已经确定了事务是否成功。
  6. 返回结果
    返回 successful 标记和 nil(无错误)。如果所有 Try 请求成功,successful 为 true,表示事务成功;如果任一 Try 请求失败,successful 为 false,表示事务失败。

在这里插入图片描述
现在我们看一下advanceProgressByTXID函数

事务进度推进

当一笔事务在第一阶段中所有的 Try 请求都有了响应后,就需要根据第一阶段的结果,执行第二阶段的 Confirm 或者 Cancel 操作,并且将事务状态推进为成功或失败的终态:

  • 倘若所有组件的 Try 响应都是成功,则需要批量调用组件的 Confirm 接口,并在这之后将事务状态更新为成功
  • 倘若存在某个组件 Try 响应为失败,则需要批量调用组件的 Cancel 接口,并在这之后将事务状态更新为失败
  • 倘若当前事务已执行超时,同样需要批量调用组件的 Cancel 接口,并在这之后将事务状态更新为失败
// 传入一个事务 id 推进其进度
func (t *TXManager) advanceProgressByTXID(txID string) error {
    // 获取事务日志明细
    tx, err := t.txStore.GetTX(t.ctx, txID)
    if err != nil {
        return err
    }
    // 推进进度
    return t.advanceProgress(tx)
}
// 传入一个事务 id 推进其进度
func (t *TXManager) advanceProgress(tx *Transaction) error {
    // 1 推断出事务当前的状态
    // 1.1 倘若所有组件 try 都成功,则为 successful
    // 1.2 倘若存在组件 try 失败,则为 failure
    // 1.3 倘若事务超时了,则为 failure
    // 1.4 否则事务状态为 hanging
    txStatus := tx.getStatus(time.Now().Add(-t.opts.Timeout))
    // hanging 状态的事务暂时不处理
    if txStatus == TXHanging {
        return nil
    }

    // 2 根据事务是否成功,定制不同的处理函数
    success := txStatus == TXSuccessful
    var confirmOrCancel func(ctx context.Context, component component.TCCComponent) (*component.TCCResp, error)
    var txAdvanceProgress func(ctx context.Context) error
    if success {
        // 如果事务成功,则需要对组件进行 confirm
        confirmOrCancel = func(ctx context.Context, component component.TCCComponent) (*component.TCCResp, error) {
            return component.Confirm(ctx, tx.TXID)
        }
        // 如果事务成功,则需要在最后更新事务日志记录的状态为成功
        txAdvanceProgress = func(ctx context.Context) error {
            return t.txStore.TXSubmit(ctx, tx.TXID, true)
        }

    } else {
        // 如果事务失败,则需要对组件进行 cancel
        confirmOrCancel = func(ctx context.Context, component component.TCCComponent) (*component.TCCResp, error) {           
            return component.Cancel(ctx, tx.TXID)
        }

        // 如果事务失败,则需要在最后更新事务日志记录的状态为失败
        txAdvanceProgress = func(ctx context.Context) error {           
            return t.txStore.TXSubmit(ctx, tx.TXID, false)
        }
    }

    // 3 批量调用组件,执行第二阶段的 confirm/cancel 操作
    for _, component := range tx.Components {
        // 获取对应的 tcc component
        components, err := t.registryCenter.getComponents(component.ComponentID)
        if err != nil || len(components) == 0 {
            return errors.New("get tcc component failed")
        }
        //components是一个数组,这里取一个元素      
        resp, err := confirmOrCancel(t.ctx, components[0])
        if err != nil {
            return err
        }
        if !resp.ACK {
            return fmt.Errorf("component: %s ack failed", component.ComponentID)
        }
    }
    // 4 二阶段 confirm/cancel 操作都执行完成后,对事务状态进行提交
    return txAdvanceProgress(t.ctx)
}

这段代码是一个事务管理器 (TXManager) 中的 advanceProgress 函数,它用于处理事务的第二阶段操作(即确认或取消)在分布式事务的两阶段提交协议中。以下是代码的详细解读和步骤概述:

  1. 判断事务当前状态
    • 根据事务的情况推断当前状态。状态判断依据包括:
    • 成功(TXSuccessful):如果所有组件的 try 都成功。
    • 失败(TXFailure):如果任何组件的 try 失败,或者事务超时。
    • 挂起(TXHanging):如果事务状态未明确为成功或失败。
    • 如果事务处于挂起状态,函数直接返回,不进行进一步处理。
  2. 根据事务状态选择相应的操作
    • 根据事务的成功或失败状态,选择 confirm 或 cancel 操作:
    • 成功时:使用 confirm 函数处理每个组件。
    • 失败时:使用 cancel 函数处理每个组件。
    • 准备一个函数 txAdvanceProgress 来在所有组件处理完毕后更新事务日志的状态。
  3. 执行第二阶段的 Confirm/Cancel 操作
    • 对于事务中的每个组件:
    • 从注册中心获取相应的 TCC 组件。
    • 执行 confirmOrCancel 函数(根据事务状态决定是确认还是取消)。
    • 检查操作的响应,如果响应不成功,则返回错误。
  4. 提交事务状态
    • 在所有组件的第二阶段操作执行完毕后,使用 txAdvanceProgress 函数提交事务的最终状态。

总结:这个函数实现了分布式事务两阶段提交协议的第二阶段。它首先判断事务的当前状态,然后根据这个状态对事务中的每个组件执行确认(Confirm)或取消(Cancel)操作。最后,它更新事务的最终状态。这是分布式事务管理中非常关键的一部分,确保了事务的一致性和完整性。

在这里插入图片描述

异步轮询流程

接下来聊聊 txManager 的异步轮询流程. 这个流程同样非常重要,是支撑 txManager 鲁棒性的重要机制.

倘若存在事务已经完成第一阶段 Try 操作的执行,但是第二阶段没执行成功,则需要由异步轮询流程进行兜底处理,为事务补齐第二阶段的操作,并将事务状态更新为终态。

启动时机

异步轮询任务是在 txManager 的初始化流程中启动的,通过异步 goroutine 持久运行:

go txManager.run()

轮询流程

异步轮询任务运行时,基于 for 循环 + select 多路复用的方式,实现定时任务的执行.

轮询的时间间隔会根据一轮任务处理过程中是否出现错误,而进行动态调整. 这里调整规则指的是:当一次处理流程中发生了错误,就需要调大当前节点轮询的时间间隔,让其他节点的异步轮询任务得到更大的执行机会.

func (t *TXManager) run() {
    var tick time.Duration
    var err error
    // 1 for 循环自旋式运行任务
    for {
        // 如果处理过程中出现了错误,需要增长轮询时间间隔
        if err == nil {
            tick = t.opts.MonitorTick
        } else {
            tick = t.backOffTick(tick)
        }
        
        // select 多路复用
        select {
        // 倘若 txManager.ctx 被终止,则异步轮询任务退出
        case <-t.ctx.Done():
            return

        // 2 等待 tick 对应时长后,开始执行任务
        case <-time.After(tick):
            // 对 txStore 加Redis分布式锁,避免分布式服务下多个服务节点的轮询任务重复执行
            if err = t.txStore.Lock(t.ctx, t.opts.MonitorTick); err != nil {
                // 取锁失败时(大概率被其他节点占有),不需要增加 tick 时长
                err = nil
                continue
            }
            // 3 获取处于 hanging 状态的事务
            var txs []*Transaction
            if txs, err = t.txStore.GetHangingTXs(t.ctx); err != nil {
                _ = t.txStore.Unlock(t.ctx)
                continue
            }
            // 4 批量推进事务进度
            err = t.batchAdvanceProgress(txs)
            _ = t.txStore.Unlock(t.ctx)
        }
    }
}

这段 Go 语言代码定义了一个名为 TXManager 的结构体的 run 方法。该方法实现了一个异步任务处理流程,主要用于处理分布式事务。我将按照代码中的逻辑分步骤解读:

  1. 循环运行:
    • 方法使用一个 for 循环,这意味着它会不断地运行,直到满足某个退出条件。
  2. 错误处理和轮询间隔:
    • 在每次循环的开始,根据之前的执行是否出错来决定轮询时间间隔 tick。如果没有错误发生,则使用 t.opts.MonitorTick 作为间隔;如果发生错误,则调用 t.backOffTick(tick) 来增加轮询间隔。
  3. select 语句多路复用:
    • 使用 select 语句来同时处理多种情况。
    • 第一种情况是 t.ctx.Done() 通道收到消息,这通常意味着 TXManager 的上下文被取消或终止,此时方法返回,结束运行。
    • 第二种情况是等待 tick 时间后执行任务。
  4. 获取和处理分布式事务:
    • 首先尝试对 txStore 加分布式锁。如果锁定失败(可能是因为其他节点已经占用锁),则跳过当前循环迭代,继续等待下一个 tick。
    • 如果成功加锁,则继续获取处于挂起(hanging)状态的事务列表。
    • 如果获取事务列表时发生错误,则释放锁并继续下一个循环迭代。
  5. 批量处理事务:
    • 对获取到的事务列表进行批量处理,推进事务进度。
    • 处理完毕后释放之前获取的锁。
  6. 错误处理和轮询调整:
    • 在整个过程中,如果任何步骤出现错误,该错误会被记录并用于调整下一次的轮询间隔。

有关于轮询时间间隔的退避谦让策略为:每次对时间间隔进行翻倍,封顶为初始时长的 8 倍:

func (t *TXManager) backOffTick(tick time.Duration) time.Duration {
    tick <<= 1
    if threshold := t.opts.MonitorTick << 3; tick > threshold {
        return threshold
    }
    return tick
}

在这里插入图片描述

批量推进事务进度

下面是异步轮询任务批量推进事务第二阶段执行的流程,核心是开启多个 goroutine 并发对多项事务进行处理:

func (t *TXManager) batchAdvanceProgress(txs []*Transaction) error {
    // 1 创建一个 chan,用于接收子 goroutine 传输的 err
    errCh := make(chan error)
    go func() {
        // 2 通过 waitGroup 聚合多个子 groutine
        var wg sync.WaitGroup
        for _, tx := range txs {
            // shadow
            tx := tx
            wg.Add(1)
            go func() {
                defer wg.Done()
                // 3 推进每笔事务的进度
                if err := t.advanceProgress(tx); err != nil {
                    // 遇到错误则投递到 errCh
                    errCh <- err
                }
            }()
        }
        // 4 收口等待所有子 goroutine 执行完成
        wg.Wait()
        // 5 所有子 goroutine 执行完成后关闭 chan,唤醒阻塞等待的父 goroutine
        close(errCh)
    }()
    // 记录遇到的第一个错误
    var firstErr error
    // 6 父 goroutine 通过 chan 阻塞在这里,直到所有 goroutine 执行完成,chan 被 close 才能往下
    for err := range errCh {
        // 记录遇到的第一个错误
        if firstErr != nil {
            continue
        }
        firstErr = err
    }
    // 7 返回错误,核心是标识执行过程中,是否发生过错误
    return firstErr
}

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

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

相关文章

云骑士数据恢复软件会对硬盘造成伤害吗?

当今时代&#xff0c;数据已经成为我们生活的重要组成部分&#xff0c;而硬盘又是存储数据的主要设备之一。然而&#xff0c;由于各种原因&#xff0c;我们的数据很容易丢失。是的&#xff0c;我们可以通过数据恢复软件来找回丢失的数据&#xff0c;但是这个过程是否会对硬盘造…

系列五、GC垃圾回收【四大垃圾算法-复制算法】

一、堆的内存组成 二、复制算法 2.1、发生位置 复制算法主要发生在新生代&#xff0c;发生在新生代的垃圾回收也被叫做Minor GC。 2.2、 Minor GC的过程 复制>清空》交换 1、eden、from区中的对象复制到to区&#xff0c;年龄1 首先&#xff0c;当eden区满的时候会触发第一…

微软Ignite 2023大盘点:GPT-4 Turbo、DALL-E 3等

11月16日&#xff0c;微软在美国西雅图举办“Microsoft Ignite 2023”全球开发者大会。ChatGPT等生成式AI成为了本次大会的重要主题。 下面「AIGC开放社区」将根据大会发布的内容&#xff0c;盘点重要内容。 GPT-4 Turbo、 DALL E 3等全新模型 16K上下文的新GPT-3.5 Turbo模…

五分钟,Docker安装kafka 3.5,kafka-map图形化管理工具

首先确保已经安装docker&#xff0c;如果是windows安装docker&#xff0c;可参考 wsl2安装docker 1、安装zk docker run -d --restartalways -e ALLOW_ANONYMOUS_LOGINyes --log-driver json-file --log-opt max-size100m --log-opt max-file2 --name zookeeper -p 2181:218…

操作指南|JumpServer堡垒机支持通过Passkey进行登录认证

伴随着互联网应用的深度普及&#xff0c;用户账户安全的重要性不言而喻。密码是目前互联网用户保护在线账户安全和隐私的主要手段。然而&#xff0c;传统密码技术本身也存在着一些缺陷。 首先&#xff0c;密码是由用户创建的&#xff0c;用户通常会出于易用性而非实际的安全性…

解决 uniapp 开发微信小程序 不能使用本地图片作为背景图 问题

参考博文&#xff1a;uniapp微信小程序无法使用本地静态资源图片(背景图在真机不显示)的解决方法_javascript技巧_脚本之家 问题&#xff1a;uniapp 开发微信小程序&#xff0c;当使用本地图片作为 background-image 时&#xff0c;真机无法显示 解决&#xff1a; 方法一&am…

Java智慧工地SaaS管理平台源码:AI/云计算/物联网

智慧工地是指运用信息化手段&#xff0c;围绕施工过程管理&#xff0c;建立互联协同、智能生产、科学管理的施工项目信息化生态圈&#xff0c;并将此数据在虚拟现实环境下与物联网采集到的工程信息进行数据挖掘分析&#xff0c;提供过程趋势预测及专家预案&#xff0c;实现工程…

招聘全球视野:跨境电商的人才策略

在数字时代&#xff0c;跨境电商已经成为全球商业领域的引擎之一。由于全球市场的巨大潜力&#xff0c;跨境电商企业需要确保其人才团队具备全球视野和战略洞察力&#xff0c;以在竞争激烈的环境中脱颖而出。本文将深入探讨招聘全球视野的重要性&#xff0c;并提供一些战略性的…

【开源】基于JAVA的高校宿舍调配管理系统

项目编号&#xff1a; S 051 &#xff0c;文末获取源码。 \color{red}{项目编号&#xff1a;S051&#xff0c;文末获取源码。} 项目编号&#xff1a;S051&#xff0c;文末获取源码。 目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能需求2.1 学生端2.2 宿管2.3 老师端 三、系统…

SQLMAP --TAMPER的编写

跟着师傅的文章进行学习 sqlmap之tamper脚本编写_sqlmap tamper编写-CSDN博客 这里学习一下tamper的编写 这里的tamper 其实就是多个绕过waf的插件 通过编写tamper 我们可以学会 在不同过滤下 执行sql注入 我们首先了解一下 tamper的结构 这里我们首先看一个最简单的例子…

实景三维:穿越时空的奇妙之旅

实景三维&#xff1a;穿越时空的奇妙之旅 在科技的不断进步中&#xff0c;实景三维技术成为了现代社会中不可或缺的一部分。无论是电影、游戏还是建筑设计&#xff0c;实景三维都能够为我们带来身临其境的观感体验。本文将详细介绍实景三维的原理、应用以及未来的发展方向。 一…

腾讯云服务器新用户优惠活动有哪些?腾讯云新用户优惠活动汇总

对于新用户来说&#xff0c;腾讯云服务器提供了许多优惠活动&#xff0c;让你享受到超值的优惠。下面我们就来一起看看&#xff0c;腾讯云服务器新用户优惠活动有哪些吧&#xff01; 1、腾讯云双十一领9999代金券&#xff08;https://1111.mian100.cn&#xff09; 腾讯云双十…

扬帆未来,成就架构之路:十本书籍助力你的架构师梦想 | 文末送书

相信大家都对未来的职业发展有着憧憬和规划&#xff0c;要做架构师、要做技术总监、要做CTO。对于如何实现自己的职业规划也都信心满满&#xff0c;努力工作、好好学习、不断提升自己。 规划职业发展 当涉及未来职业发展时&#xff0c;我们都怀揣着远大的目标和野心。对许多人…

C#实现将Mysql数据迁移到SQL数据库

前提源数据库及目标数据库需要表结构一致 using System; using System.Windows.Forms; using MySql.Data.MySqlClient; using System.Data.SqlClient; namespace MysqlToSQL { public partial class Form1 : Form { private static string mysqlConnString "…

代码随想录 Day48 动态规划16 T647 回文子串 T516最长回文子序列

LeetCode T647 回文子串 题目链接:647. 回文子串 - 力扣&#xff08;LeetCode&#xff09; 题目思路: 我们仍然使用动规五部曲来分析题目 1.确定dp数组含义 这里dp数组表示从下标从i到j这段子串是不是回文子串,是就是true,不是就是false 2.确定dp数组的递推公式 举个例子 这里…

012.tr

1、用tr进行转换 tr是Unix命令行专家工具箱中的一件万能工具。它可用于编写优雅的单行命令。tr可以对来自标准输入的内容进行字符替换、字符删除以及重复字符压缩。tr是translate&#xff08;转换&#xff09;的简写&#xff0c;因为它可以将一组字符转换成另一组字符。 tr只…

ADI 阻抗测量开发板AD5940调试

硬件环境&#xff1a; 评估板A,阻抗测试板 EVAL-AD5940BIOZ&#xff0c;阻抗测试板信息链接如下&#xff1a; https://wiki.analog.com/resources/eval/user-guides/eval-ad5940/hardware/eval-ad5940bioz 评估板B,MCU控制板 EVAL-ADICUP3029&#xff0c;控制板信息链接如下…

【python】——控制语句和组合数据类型(其二)

&#x1f383;个人专栏&#xff1a; &#x1f42c; 算法设计与分析&#xff1a;算法设计与分析_IT闫的博客-CSDN博客 &#x1f433;Java基础&#xff1a;Java基础_IT闫的博客-CSDN博客 &#x1f40b;c语言&#xff1a;c语言_IT闫的博客-CSDN博客 &#x1f41f;MySQL&#xff1a…

鸿蒙OS应用开发初体验

什么是HarmonyOS&#xff1f; HarmonyOS&#xff08;鸿蒙操作系统&#xff09;是华为公司开发的一款基于微内核的分布式操作系统。它是一个面向物联网&#xff08;IoT&#xff09;时代的全场景操作系统&#xff0c;旨在为各种类型的设备提供统一的操作系统平台和开发框架。Har…

docker删除镜像命令

在Docker中删除镜像的命令是 docker rmi。这个命令用于删除一个或多个Docker镜像。使用这个命令时&#xff0c;你需要指定要删除的镜像的ID或名称。以下是一些常用的用法&#xff1a; 删除单个镜像&#xff1a; docker rmi [IMAGE_ID或REPOSITORY:TAG]例如&#xff0c;如果你知…