2. Go语言并发基础:goroutine、channel、sync包在事务协调中的应用

分布式事务的本质是跨多个独立资源(数据库、消息队列、服务)的状态协调。在Go语言中,这种协调能力直接依赖于语言内置的并发原语:goroutine、channel 和 sync 包。本章将剖析这三者在事务协调中的具体角色与实战模式。

2.1 并发原语与事务协调的映射关系

事务协调的每个阶段都可以映射到Go的并发模型上。下表展示了核心原语与事务职责的对应关系:

Go并发原语 事务协调职责 典型场景
goroutine 轻量级执行单元,承载分支事务 每个参与方的事务执行、补偿操作
channel 协程间通信,传递事务状态/结果 协调者收集分支事务投票结果
sync.WaitGroup 等待一组分支事务完成 等待所有参与方准备就绪
sync.Mutex / RWMutex 保护共享事务上下文 事务日志写入、全局锁防重入
sync.Once 确保幂等操作只执行一次 事务提交/回滚的幂等控制
sync.Cond 条件等待,协调阶段切换 等待所有分支事务到达指定状态
设计原则: 在分布式事务中,goroutine 负责“执行”,channel 负责“通信”,sync 包负责“同步”。三者组合使用,可以构建出无锁或低锁的协调器核心。

2.2 goroutine:分支事务的轻量执行单元

在SAGA或TCC模式中,每个分支事务(Branch Transaction)都应该运行在独立的 goroutine 中。这样做的好处是:

  • 并发执行:多个分支事务可以同时执行,减少整体协调延迟。
  • 隔离性:单个分支事务的panic不会影响其他分支。
  • 资源轻量:goroutine的栈初始仅几KB,可以轻松创建数千个分支事务协程。

以下是一个典型的SAGA协调器启动分支事务的代码模式:

// SagaCoordinator 启动所有分支事务
func (s *SagaCoordinator) ExecuteBranchTransactions(ctx context.Context, branches []Branch) error {
    errCh := make(chan error, len(branches))
    
    for _, branch := range branches {
        go func(b Branch) {
            // 每个分支事务在独立goroutine中执行
            if err := b.Execute(ctx); err != nil {
                errCh <- err
                return
            }
            errCh <- nil
        }(branch)
    }
    
    // 收集结果(后续结合channel讲解)
    for range branches {
        if err := <-errCh; err != nil {
            return err // 触发补偿流程
        }
    }
    return nil
}
避坑指南: goroutine 中必须捕获 panic,否则会导致整个协调器进程崩溃。建议在分支事务goroutine入口处使用 defer recover() 并将panic转为error返回。

2.3 channel:事务状态的有序传递

channel 是Go中协程间通信的首选方式。在事务协调中,channel 主要用于:

  • 结果收集:协调者通过channel接收各分支事务的执行结果。
  • 状态广播:协调者通过channel向所有分支广播提交或回滚指令。
  • 超时控制:结合 select 和 time.After 实现事务超时。

下图展示了基于channel的事务协调流程:

基于channel的事务协调流程 协调者 (Coordinator) 分支事务 1 分支事务 2 分支事务 3 结果收集 channel 协调者通过channel收集所有分支事务的结果,根据结果决定提交或回滚

使用channel实现结果收集的典型代码:

// 使用channel收集分支事务投票结果
func (c *Coordinator) CollectVotes(ctx context.Context, branches []Branch) (bool, error) {
    voteCh := make(chan VoteResult, len(branches))
    
    for _, branch := range branches {
        go func(b Branch) {
            select {
            case <-ctx.Done():
                voteCh <- VoteResult{ID: b.ID, OK: false, Err: ctx.Err()}
            default:
                ok, err := b.Prepare(ctx)
                voteCh <- VoteResult{ID: b.ID, OK: ok, Err: err}
            }
        }(branch)
    }
    
    // 带超时的结果收集
    timeout := time.After(5 * time.Second)
    for i := 0; i < len(branches); i++ {
        select {
        case result := <-voteCh:
            if !result.OK || result.Err != nil {
                return false, result.Err
            }
        case <-timeout:
            return false, ErrTransactionTimeout
        }
    }
    return true, nil
}

2.4 sync.WaitGroup:等待分支事务就绪

在二阶段提交(2PC)或TCC模式的第一阶段,协调者需要等待所有分支事务完成准备操作。 sync.WaitGroup 是等待一组goroutine完成的标准工具。

使用模式:

func (c *Coordinator) WaitForAllBranches(ctx context.Context, branches []Branch) error {
    var wg sync.WaitGroup
    errCh := make(chan error, len(branches))
    
    for _, branch := range branches {
        wg.Add(1)
        go func(b Branch) {
            defer wg.Done()
            if err := b.Prepare(ctx); err != nil {
                errCh <- err
            }
        }(branch)
    }
    
    wg.Wait() // 阻塞直到所有分支完成Prepare
    close(errCh)
    
    // 检查是否有错误
    for err := range errCh {
        if err != nil {
            return err // 触发补偿
        }
    }
    return nil
}
关键点: wg.Add(1) 必须在启动goroutine之前调用,否则可能出现竞态条件。推荐在循环中先调用 wg.Add(1),再 go func()。

2.5 sync.Mutex:保护共享事务上下文

在事务协调过程中,多个goroutine可能同时访问共享的事务上下文(如事务日志、状态机)。sync.Mutex 用于保护这些临界区。

典型场景——事务日志的并发写入:

type TransactionLog struct {
    mu    sync.Mutex
    entries []LogEntry
}

func (l *TransactionLog) Append(entry LogEntry) {
    l.mu.Lock()
    defer l.mu.Unlock()
    l.entries = append(l.entries, entry)
}

// 在分支事务goroutine中调用
go func() {
    log.Append(LogEntry{
        TxID:      txID,
        BranchID:  branchID,
        Status:    StatusPrepared,
        Timestamp: time.Now(),
    })
}()

2.6 sync.Once:确保幂等提交/回滚

分布式事务中,网络重试可能导致协调者多次发送提交或回滚指令。sync.Once 确保关键操作只执行一次,是实现幂等性的基础。

type BranchTransaction struct {
    commitOnce sync.Once
    rollbackOnce sync.Once
}

func (bt *BranchTransaction) Commit(ctx context.Context) error {
    var err error
    bt.commitOnce.Do(func() {
        err = bt.doCommit(ctx) // 真正的提交逻辑
    })
    return err
}

func (bt *BranchTransaction) Rollback(ctx context.Context) error {
    var err error
    bt.rollbackOnce.Do(func() {
        err = bt.doRollback(ctx) // 真正的回滚逻辑
    })
    return err
}

2.7 综合实战:基于并发原语的SAGA协调器

下面是一个完整的SAGA协调器核心结构,综合运用了上述所有原语:

type SagaCoordinator struct {
    branches   []Branch
    results    chan BranchResult
    wg         sync.WaitGroup
    mu         sync.Mutex
    commitOnce sync.Once
    rollbackOnce sync.Once
    state      SagaState
}

type SagaState int

const (
    StateInit SagaState = iota
    StateExecuting
    StateCompensating
    StateCompleted
    StateFailed
)

func (s *SagaCoordinator) Execute(ctx context.Context) error {
    s.mu.Lock()
    s.state = StateExecuting
    s.mu.Unlock()
    
    // 1. 启动所有分支事务
    s.results = make(chan BranchResult, len(s.branches))
    for _, branch := range s.branches {
        s.wg.Add(1)
        go func(b Branch) {
            defer s.wg.Done()
            err := b.Execute(ctx)
            s.results <- BranchResult{ID: b.ID, Err: err}
        }(branch)
    }
    
    // 2. 等待所有分支完成
    s.wg.Wait()
    close(s.results)
    
    // 3. 检查结果
    for result := range s.results {
        if result.Err != nil {
            s.rollbackOnce.Do(func() {
                s.startCompensation(ctx)
            })
            return result.Err
        }
    }
    
    // 4. 全部成功,提交
    s.commitOnce.Do(func() {
        s.finalizeCommit(ctx)
    })
    return nil
}

func (s *SagaCoordinator) startCompensation(ctx context.Context) {
    s.mu.Lock()
    s.state = StateCompensating
    s.mu.Unlock()
    
    var compWg sync.WaitGroup
    for _, branch := range s.branches {
        compWg.Add(1)
        go func(b Branch) {
            defer compWg.Done()
            b.Compensate(ctx)
        }(branch)
    }
    compWg.Wait()
    
    s.mu.Lock()
    s.state = StateFailed
    s.mu.Unlock()
}
实战建议: 在生产级SAGA协调器中,建议将 sync.Once 与数据库中的幂等键结合使用。即使协调器进程重启,也能通过数据库记录判断是否已经执行过提交/回滚。

2.8 并发原语选择决策表

在实际开发中,根据不同的协调场景选择合适的并发原语:

协调场景 推荐原语 不推荐方案 原因
等待所有分支完成 sync.WaitGroup 空channel轮询 WaitGroup语义清晰,无忙等待
收集分支结果 带缓冲的 chan 共享内存+锁 channel天然支持协程安全通信
保护事务状态机 sync.Mutex channel做锁 Mutex性能更高,语义直接
幂等提交/回滚 sync.Once 自行实现标志位 Once保证并发安全且只执行一次
超时控制 select + time.After sleep轮询 select实现非阻塞超时
广播通知所有分支 sync.Cond 或关闭channel 逐个发送消息 关闭channel可同时通知所有接收方
性能警示: 在事务协调中,避免在热点路径上使用 sync.Mutex 保护大块代码。优先使用channel传递状态,只在必须保护共享状态时使用锁。过度加锁会显著降低事务吞吐量。