两阶段提交(2PC)原理:准备阶段、提交/回滚阶段、协调者与参与者角色

两阶段提交(Two-Phase Commit,2PC)是分布式事务中最经典的原子提交协议。它通过引入一个协调者(Coordinator)和多个参与者(Participant),将一次全局事务的提交过程拆分为两个阶段,从而保证所有节点要么全部提交,要么全部回滚。2PC 是理解后续所有分布式事务协议(如 3PC、Paxos、Raft 事务)的基础。

核心角色定义

角色 职责 状态管理
协调者(Coordinator) 全局事务的发起者与决策者,负责向所有参与者发送准备请求,收集投票,并最终决定提交或中止。 维护全局事务日志,记录事务状态(Init → Prepared → Committed / Aborted)。
参与者(Participant) 本地事务的执行者,负责执行本地操作并锁定资源,向协调者反馈“可以提交”或“必须回滚”。 维护本地事务日志,记录本地分支状态(Working → Prepared → Committed / Rollback)。
角色映射到 Go 工程:在 Go 微服务架构中,协调者通常是一个独立的 Transaction Manager(TM) 服务,参与者则是各个业务服务(如 Order Service、Inventory Service)。TM 通过 gRPC 或消息队列与参与者通信。

第一阶段:准备阶段(Voting Phase)

协调者向所有参与者发送 Prepare 请求,询问它们是否能够提交事务。参与者收到请求后,执行以下步骤:

  1. 执行本地事务:执行 SQL 或业务操作,但不提交(处于未提交状态)。
  2. 锁定资源:对涉及的数据行、表或资源加锁,防止其他事务修改。
  3. 写入本地日志:将事务状态标记为 Prepared,确保即使宕机也能恢复。
  4. 返回投票:如果一切正常,返回 Yes(同意提交);如果失败,返回 No(必须回滚)。
资源锁定风险:参与者一旦回复 Yes,就必须持有资源锁直到第二阶段结束。如果协调者崩溃或网络分区,这些锁可能长时间不释放,导致系统吞吐量骤降。这是 2PC 的主要性能瓶颈。

第二阶段:提交/回滚阶段(Decision Phase)

协调者收集所有参与者的投票后,做出最终决策:

  • 全部 Yes → 全局提交:协调者向所有参与者发送 Commit 指令。参与者收到后,正式提交本地事务,释放资源锁,并写入 Committed 日志。
  • 任一 No 或超时 → 全局回滚:协调者向所有参与者发送 Abort 指令。参与者收到后,回滚本地事务,释放资源锁,并写入 Rollback 日志。
关键约束:第二阶段必须保证“全有或全无”。协调者在发送 Commit 或 Abort 之前,必须将决策写入本地日志(Write-Ahead Log),以防止自身崩溃后丢失决策。

2PC 完整流程 SVG 图

下图展示了协调者与两个参与者之间的完整交互流程,包含准备阶段和提交/回滚阶段的所有消息与状态转换。

两阶段提交(2PC)完整流程 协调者 参与者 A 参与者 B 准备阶段 提交/回滚阶段 协调者 参与者 A 参与者 B Prepare Prepare 执行 + 锁定 执行 + 锁定 Yes Yes 决策:提交 Commit Commit 提交 + 释放锁 提交 + 释放锁 ACK ACK 事务完成 说明 若任一参与者回复 No 或超时, 协调者发送 Abort 替代 Commit。

2PC 状态机与日志结构

协调者和参与者在 2PC 过程中维护严格的状态机。以下表格展示了协调者的状态转换路径:

当前状态 触发事件 下一状态 动作
Init 开始全局事务 Preparing 写入日志:BEGIN GLOBAL TX
Preparing 发送 Prepare 给所有参与者 Waiting 启动超时计时器
Waiting 收到所有 Yes Committing 写入日志:GLOBAL COMMIT
Waiting 收到任一 No 或超时 Aborting 写入日志:GLOBAL ABORT
Committing 发送 Commit 给所有参与者 Finished 等待所有 ACK
Aborting 发送 Abort 给所有参与者 Finished 等待所有 ACK

Go 语言中的 2PC 协调者骨架代码

以下代码展示了协调者在 Go 中的核心逻辑,使用 sync.WaitGroup 和 context 管理并发投票:

package twopc

import (
    "context"
    "fmt"
    "sync"
    "time"
)

type Participant interface {
    Prepare(ctx context.Context) (bool, error)
    Commit(ctx context.Context) error
    Rollback(ctx context.Context) error
}

type Coordinator struct {
    participants []Participant
    timeout      time.Duration
}

func (c *Coordinator) Execute(ctx context.Context) error {
    // 第一阶段:准备
    votes := make(chan bool, len(c.participants))
    var wg sync.WaitGroup

    for _, p := range c.participants {
        wg.Add(1)
        go func(part Participant) {
            defer wg.Done()
            ok, err := part.Prepare(ctx)
            if err != nil || !ok {
                votes <- false
                return
            }
            votes <- true
        }(p)
    }

    // 等待所有准备完成或超时
    done := make(chan struct{})
    go func() {
        wg.Wait()
        close(done)
    }()

    select {
    case <-done:
        // 所有参与者已投票
    case <-time.After(c.timeout):
        return fmt.Errorf("prepare phase timeout")
    }

    close(votes)
    allYes := true
    for v := range votes {
        if !v {
            allYes = false
            break
        }
    }

    // 第二阶段:提交或回滚
    if allYes {
        for _, p := range c.participants {
            if err := p.Commit(ctx); err != nil {
                return fmt.Errorf("commit failed: %w", err)
            }
        }
        return nil
    }

    for _, p := range c.participants {
        if err := p.Rollback(ctx); err != nil {
            return fmt.Errorf("rollback failed: %w", err)
        }
    }
    return fmt.Errorf("transaction aborted by participant vote")
}
工程要点:上述代码省略了持久化日志、重试机制和幂等性处理。生产级 2PC 协调者必须将每个阶段的决策写入本地磁盘日志,并在崩溃恢复时根据日志状态重放或中止未完成的事务。

2PC 的优缺点与适用场景

维度 评价
强一致性 保证所有节点数据最终一致,无中间状态不一致问题。
同步阻塞 参与者在准备阶段锁定资源,直到第二阶段结束才释放,高并发下性能差。
单点故障 协调者宕机可能导致参与者长时间阻塞,需要人工介入或超时机制。
适用场景 短事务、低并发、强一致性要求高的场景(如金融转账、库存扣减)。
不适用场景 长事务、高并发、微服务间频繁调用的场景(推荐使用 Saga 或 TCC)。

2PC 在 Go 生态中的实践

Go 社区中,2PC 通常不直接作为独立框架使用,而是作为底层协议被更高级的分布式事务中间件封装。常见的实践模式包括:

  • 数据库原生 XA 事务:通过 database/sql 的 BeginTx 和 Prepare 接口实现,但 Go 的 XA 支持较弱,不推荐跨数据库使用。
  • 自定义协调者 + 消息队列:使用 Kafka 或 RabbitMQ 作为协调者与参与者之间的通信通道,利用消息的持久化特性实现日志可靠性。
  • 分布式事务中间件:如 DTM(Distributed Transaction Manager)支持 2PC 模式,提供 Go SDK,开发者只需实现 Prepare/Commit/Rollback 回调接口。
推荐学习路径:先通过 DTM 的 2PC 示例理解协调者与参与者的通信模式,再阅读 DTM 源码中关于日志恢复和超时处理的实现,最后尝试自己实现一个简化版 2PC 协调者以加深理解。