面试高频——分布式事务详解

面试高频------分布式事务详解

事务几乎是每个后端程序员都会面临的考验,而分布式事务则是初、中、高级程序员的一道分水岭。本文主要给大家介绍一下分布式事务的常见解决方案。

文中代码地址Github:https://github.com/ziyifast/ziyifast-code_instruction/tree/main/go-demo/go-tx

欢迎大家star⭐️~

概念

定义 :分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。
特征 :跨多个服务或数据库的操作需要作为一个整体提交或回滚。需要满足 ACID 属性(原子性、一致性、隔离性、持久性)在分布式环境下的扩展。
示例 :用户下单并扣减库存,涉及订单服务和库存服务两个独立的服务。多个微服务需要协同完成一个业务流程,例如支付 + 发货,需要确保这些跨服务操作要么全部成功,要么全部失败。

理论

CAP

  • 一致性(Consistency):所有节点在同一时间的数据完全一致
  • 可用性(Availability):每个请求都能获得响应
  • 分区容错性(Partition tolerance):系统在节点间网络故障时仍能运作

在分布式系统中,是不存在同时满足一致性 Consistency、可用性 Availability和分区容错性 Partition Tolerance三者的。因为是分布式系统,如果保证了可用性(每个请求都获得响应)那么就会丢失一致性,反之亦然。即:只能是CP或者AP。

一句话总结:在分布式事务中,一致性、可用性和分区容错不可兼得。

  • CP系统:ZooKeeper、etcd - 放弃可用性保证一致性
  • AP系统:Cassandra、DynamoDB - 放弃强一致性保证可用性
  • 实际应用:微服务架构通常选择AP(放弃强一致性保证系统可用性),保证系统可用性

Base

  • Basically Available(基本可用):系统出现故障时,允许损失部分可用性
  • Soft state(软状态):允许系统存在中间状态,不影响整体可用性
  • Eventually consistent(最终一致性):经过一段时间后,数据最终达到一致状态

BASE是对CAP中一致性和可用性权衡的结果,其来源于对大规模互联网系统分布式实践的结论,是基于CAP定理逐步演化而来的。

核心思想:放弃强一致性,通过业务可接受的方式达到最终一致性。

方案

注意:

  1. "最终一致性 = 不一致" ➜ 错误!最终一致性是在一定时间后达到一致,并不是放弃一致性。
  2. "TCC 就是 Saga" ➜ 错误!两者虽都属于补偿型事务模型,但机制不同,TCC 更注重 Try 阶段预留资源。

1. XA协议------2PC两阶段提交

概念:XA是一个分布式事务协议,XA中大致分为两部分:事务管理器和本地资源管理器,其中本地资源管理器往往由数据库实现,而事务管理器作为全局的调度者,负责各个本地资源的提交和回滚。

流程

  • 第一阶段:表决阶段,所有参与者都将本事务能否成功的信息反馈发给协调者。
  • 第二阶段:执行阶段,协调者根据所有参与者的反馈,通知所有参与者,步调一致地在所有分支上提交或者回滚。

如果第2步有任意事务执行失败,则第三步共同回滚

bash 复制代码
1️⃣ 开启 XA 事务
XA START 'txn1'; -- 开启事务 txn1。
UPDATE account SET balance = balance - 100 WHERE user_id = 1;
XA END 'txn1'; -- 标记事务 txn1 结束,准备提交。
XA PREPARE 'txn1'; -- 准备提交,数据已写入但未提交。


2️⃣ 另一台 MySQL 服务器操作
XA START 'txn2';
UPDATE account SET balance = balance + 100 WHERE user_id = 2;
XA END 'txn2';
XA PREPARE 'txn2';


3️⃣ 提交 XA 事务
-- 如果所有数据库都 PREPARE 成功,则提交:
XA COMMIT 'txn1';
XA COMMIT 'txn2';
-- 如果有一个失败,则回滚:
XA ROLLBACK 'txn1';
XA ROLLBACK 'txn2';

注意事项:

  • 同步阻塞:所有参与者必须等待协调者指令,影响系统吞吐量,XA事务适用于关键业务,非关键路径应考虑其他方案
  • 资源锁定时间长:尽量缩短事务执行时间,避免在XA事务中执行耗时操作
  • 悬挂事务处理:需要考虑事务协调者宕机后恢复的处理机制
  • 性能监控:需要监控XA事务的执行时间,避免长时间持有锁

优缺点

✅ 强一致性保证

✅ 主流数据库原生支持

❌ 同步阻塞,性能较差

❌ 协调者单点故障

❌ 数据锁定时间长

应用场景:XA 适用于高一致性、低并发的关键业务,如金融交易、银行转账

  1. 银行转账业务:跨行转账需要保证资金同时从一个账户扣除并添加到另一个账户,必须保证强一致性,不允许出现资金丢失或重复
  2. 传统ERP系统:涉及财务、库存、采购等多个模块的数据同步更新,需要确保业务数据的完整性和一致性

PS:高并发互联网应用、最终一致性可接受的场景、对响应时间敏感的场景可不选择XA模式

案例:MySQL8实现了XA事务,https://mysql.net.cn/doc/refman/8.0/en/xa.html

代码实现

📢本代码仅供参考,切勿直接使用,主要带大家了解流程和大致写法,距离生产使用还需要进行较大优化

go 复制代码
package xa

import (
    "context"
    "database/sql"
    "fmt"
    "log"
    "sync"
    "time"
)

// XATransactionCoordinator XA事务协调者
type XATransactionCoordinator struct {
    participants []XAParticipant
    mutex        sync.RWMutex
    txID         string
}

// XAParticipant XA事务参与者接口
type XAParticipant interface {
    Start(ctx context.Context) error
    ExecuteBusinessLogic(ctx context.Context) error
    End(ctx context.Context) error
    Prepare(ctx context.Context) error
    Commit(ctx context.Context) error
    Rollback(ctx context.Context) error
    GetID() string
}

// NewXATransactionCoordinator 创建新的XA事务协调者
func NewXATransactionCoordinator(txID string) *XATransactionCoordinator {
    return &XATransactionCoordinator{
       participants: make([]XAParticipant, 0),
       txID:         txID,
    }
}

// AddParticipant 添加XA事务参与者
func (c *XATransactionCoordinator) AddParticipant(participant XAParticipant) {
    c.mutex.Lock()
    defer c.mutex.Unlock()
    c.participants = append(c.participants, participant)
}

// ExecuteTwoPhaseCommit 执行两阶段提交
func (c *XATransactionCoordinator) ExecuteTwoPhaseCommit(ctx context.Context) error {
    // 设置默认超时时间
    ctxWithTimeout, cancel := context.WithTimeout(ctx, 30*time.Second)
    defer cancel()

    // 第一阶段:准备阶段
    if err := c.preparePhase(ctxWithTimeout); err != nil {
       // 任一参与者准备失败,全部回滚
       c.rollbackAll(ctxWithTimeout)
       return fmt.Errorf("prepare phase failed: %w", err)
    }

    // 第二阶段:提交阶段
    if err := c.commitPhase(ctxWithTimeout); err != nil {
       // 注意:提交阶段失败需要人工干预
       log.Printf("CRITICAL: commit phase failed, manual intervention required for transaction %s: %v", c.txID, err)
       return fmt.Errorf("commit phase failed: %w", err)
    }

    return nil
}

// preparePhase 准备阶段
func (c *XATransactionCoordinator) preparePhase(ctx context.Context) error {
    c.mutex.RLock()
    defer c.mutex.RUnlock()

    log.Printf("Starting prepare phase for transaction %s with %d participants", c.txID, len(c.participants))

    for i, participant := range c.participants {
       // 执行业务逻辑:XA Start + 业务SQL
       if err := participant.ExecuteBusinessLogic(ctx); err != nil {
          return fmt.Errorf("participant %s business logic execution failed at index %d: %w", participant.GetID(), i, err)
       }

       // 执行XA END
       if err := participant.End(ctx); err != nil {
          return fmt.Errorf("participant %s END failed at index %d: %w", participant.GetID(), i, err)
       }

       // 执行XA PREPARE
       if err := participant.Prepare(ctx); err != nil {
          return fmt.Errorf("participant %s PREPARE failed at index %d: %w", participant.GetID(), i, err)
       }

       log.Printf("Participant %s prepared successfully", participant.GetID())
    }

    log.Printf("Prepare phase completed successfully for transaction %s", c.txID)
    return nil
}

// commitPhase 提交阶段
func (c *XATransactionCoordinator) commitPhase(ctx context.Context) error {
    c.mutex.RLock()
    defer c.mutex.RUnlock()

    log.Printf("Starting commit phase for transaction %s", c.txID)

    for i, participant := range c.participants {
       if err := participant.Commit(ctx); err != nil {
          // 记录已提交的参与者索引,便于追踪
          log.Printf("CRITICAL: Participant %s commit failed at index %d. Already committed participants may require manual recovery.", participant.GetID(), i)
          return fmt.Errorf("participant %s COMMIT failed at index %d: %w", participant.GetID(), i, err)
       }
       log.Printf("Participant %s committed successfully", participant.GetID())
    }

    log.Printf("Commit phase completed successfully for transaction %s", c.txID)
    return nil
}

// rollbackAll 回滚所有参与者
func (c *XATransactionCoordinator) rollbackAll(ctx context.Context) {
    c.mutex.RLock()
    defer c.mutex.RUnlock()

    log.Printf("Rolling back all participants for transaction %s", c.txID)

    for i, participant := range c.participants {
       if err := participant.Rollback(ctx); err != nil {
          log.Printf("Participant %s rollback failed at index %d: %v", participant.GetID(), i, err)
       } else {
          log.Printf("Participant %s rolled back successfully", participant.GetID())
       }
    }
}

// BusinessOperation 业务操作函数类型
type BusinessOperation func(*sql.Tx) error

// DatabaseParticipant 数据库参与者
type DatabaseParticipant struct {
    db         *sql.DB
    xaTxID     string
    id         string
    operations []BusinessOperation
    currentTx  *sql.Tx
}

// NewDatabaseParticipant 创建新的数据库参与者
func NewDatabaseParticipant(db *sql.DB, xaTxID, id string) *DatabaseParticipant {
    return &DatabaseParticipant{
       db:         db,
       xaTxID:     xaTxID,
       id:         id,
       operations: make([]BusinessOperation, 0),
    }
}

// AddOperation 添加业务操作
func (d *DatabaseParticipant) AddOperation(op BusinessOperation) {
    d.operations = append(d.operations, op)
}

// Start 开始XA事务
func (d *DatabaseParticipant) Start(ctx context.Context) error {
    query := fmt.Sprintf("XA START '%s'", d.xaTxID)
    log.Printf("Executing: %s", query)
    _, err := d.db.ExecContext(ctx, query)
    if err != nil {
       log.Printf("XA START failed for participant %s: %v", d.id, err)
       return fmt.Errorf("XA START failed: %w", err)
    }
    log.Printf("XA START succeeded for participant %s", d.id)
    return nil
}

// ExecuteBusinessLogic 执行业务逻辑
func (d *DatabaseParticipant) ExecuteBusinessLogic(ctx context.Context) error {
    // 开始XA事务
    if err := d.Start(ctx); err != nil {
       return err
    }

    // 创建一个普通事务来执行业务SQL
    tx, err := d.db.BeginTx(ctx, nil)
    if err != nil {
       return fmt.Errorf("failed to begin transaction: %w", err)
    }
    d.currentTx = tx

    // 执行所有业务操作
    for i, op := range d.operations {
       if err := op(tx); err != nil {
          tx.Rollback()
          return fmt.Errorf("business operation %d failed: %w", i, err)
       }
    }

    // 注意:这里不提交事务,因为XA事务会在后续阶段处理
    log.Printf("Business logic executed successfully for participant %s", d.id)
    return nil
}

// End 结束XA事务
func (d *DatabaseParticipant) End(ctx context.Context) error {
    // 提交内部事务
    if d.currentTx != nil {
       if err := d.currentTx.Commit(); err != nil {
          log.Printf("Internal transaction commit failed for participant %s: %v", d.id, err)
          return fmt.Errorf("internal transaction commit failed: %w", err)
       }
       d.currentTx = nil
    }

    query := fmt.Sprintf("XA END '%s'", d.xaTxID)
    log.Printf("Executing: %s", query)
    _, err := d.db.ExecContext(ctx, query)
    if err != nil {
       log.Printf("XA END failed for participant %s: %v", d.id, err)
       return fmt.Errorf("XA END failed: %w", err)
    }
    log.Printf("XA END succeeded for participant %s", d.id)
    return nil
}

// Prepare 准备提交XA事务
func (d *DatabaseParticipant) Prepare(ctx context.Context) error {
    query := fmt.Sprintf("XA PREPARE '%s'", d.xaTxID)
    log.Printf("Executing: %s", query)
    _, err := d.db.ExecContext(ctx, query)
    if err != nil {
       log.Printf("XA PREPARE failed for participant %s: %v", d.id, err)
       return fmt.Errorf("XA PREPARE failed: %w", err)
    }
    log.Printf("XA PREPARE succeeded for participant %s", d.id)
    return nil
}

// Commit 提交XA事务
func (d *DatabaseParticipant) Commit(ctx context.Context) error {
    query := fmt.Sprintf("XA COMMIT '%s'", d.xaTxID)
    log.Printf("Executing: %s", query)
    _, err := d.db.ExecContext(ctx, query)
    if err != nil {
       log.Printf("XA COMMIT failed for participant %s: %v", d.id, err)
       return fmt.Errorf("XA COMMIT failed: %w", err)
    }
    log.Printf("XA COMMIT succeeded for participant %s", d.id)
    return nil
}

// Rollback 回滚XA事务
func (d *DatabaseParticipant) Rollback(ctx context.Context) error {
    query := fmt.Sprintf("XA ROLLBACK '%s'", d.xaTxID)
    log.Printf("Executing: %s", query)
    _, err := d.db.ExecContext(ctx, query)
    if err != nil {
       log.Printf("XA ROLLBACK failed for participant %s: %v", d.id, err)
       return fmt.Errorf("XA ROLLBACK failed: %w", err)
    }

    // 回滚内部事务(如果存在)
    if d.currentTx != nil {
       d.currentTx.Rollback()
       d.currentTx = nil
    }

    log.Printf("XA ROLLBACK succeeded for participant %s", d.id)
    return nil
}

// GetID 获取参与者ID
func (d *DatabaseParticipant) GetID() string {
    return d.id
}

// UsageExample 使用示例
func UsageExample() {
    // 创建XA事务协调者
    coordinator := NewXATransactionCoordinator("tx_12345")
    fmt.Println("Usage example:", coordinator)

    // 创建数据库连接(示例)
    // db1, _ := sql.Open("mysql", "user:password@tcp(localhost:3306)/db1")
    // db2, _ := sql.Open("mysql", "user:password@tcp(localhost:3306)/db2")

    // 创建参与者
    // participant1 := NewDatabaseParticipant(db1, "tx_12345", "account_service")
    // participant2 := NewDatabaseParticipant(db2, "tx_12345", "inventory_service")

    // 添加业务操作
    // participant1.AddOperation(func(tx *sql.Tx) error {
    //     _, err := tx.Exec("UPDATE account SET balance = balance - 100 WHERE user_id = 1")
    //     return err
    // })
    //
    // participant2.AddOperation(func(tx *sql.Tx) error {
    //     _, err := tx.Exec("UPDATE inventory SET stock = stock - 1 WHERE product_id = 1")
    //     return err
    // })

    // 添加参与者到协调者
    // coordinator.AddParticipant(participant1)
    // coordinator.AddParticipant(participant2)

    // 执行两阶段提交
    // ctx := context.Background()
    // if err := coordinator.ExecuteTwoPhaseCommit(ctx); err != nil {
    //     log.Fatalf("XA transaction failed: %v", err)
    // }

    // fmt.Println("XA transaction completed successfully")
}

2. 事务补偿------TCC模式try-confirm-cancel

概念 :TCC方案其实是两阶段提交的一种改进,将整个业务逻辑的每个分支显式的分成了Try、Confirm、Cancel三个操作。每个阶段都是本地事务(在单个服务内独立完成,有ACID保证)
流程:Try部分完成业务的准备工作,confirm部分完成业务的提交,cancel部分完成事务的回滚。以电商场景为例:

  1. 发起方(如订单服务)调用所有参与者的Try接口:
  • 若所有Try都成功(库存冻结、余额冻结完成),进入Confirm阶段;
  • 若任一Try失败(如库存不足),进入Cancel阶段。

PS: 这里的try主要是预占库存,标记该库存已被占用,并非真实扣减。可用库存 = 总库存 - 已确认占用 - 预占库存

  1. 发起方调用所有参与者的Confirm接口:
  • 所有服务执行最终提交(扣减库存、扣减余额、更新订单状态)
  1. 若Try阶段有失败,发起方调用所有参与者的Cancel接口:
  • 所有服务释放资源(解冻库存、解冻余额、删除订单)

注意事项

  • 幂等性保障: Confirm和Cancel操作必须保证幂等性,因为协调者可能会重复调用这些操作,以防止重复处理导致的数据不一致。
  • 资源预留机制: Try阶段需要合理预留业务资源,确保在Confirm阶段能够真正提交,在Cancel阶段能够正确释放,避免资源泄漏。
  • 空回滚处理:需要处理业务操作未执行但收到Cancel请求的情况
  • 悬挂问题:需要处理Try操作未返回但收到Cancel或Confirm请求的情况

优缺点

✅高性能:无全局锁机制,避免了XA模式中的资源锁定问题

✅服务自治:不依赖于特定的事务管理器或数据库特性

✅适用性强:适用于各种分布式场景,不仅限于数据库操作

✅最终一致性保证:通过Confirm和Cancel机制确保事务最终一致性

❌实现复杂度高:需要为每个业务操作实现Try、Confirm、Cancel三个方法,业务逻辑侵入性强,开发成本较高

❌幂等性要求:Confirm和Cancel操作必须保证幂等性,需要处理重复调用的情况

❌异常处理复杂:需要考虑空回滚、悬挂等异常情况,Cancel操作可能失败,需要补偿机制

❌资源预留问题:Try阶段需要预留资源,在高并发场景下可能导致资源浪费,时间的资源预留可能影响系统性能

应用场景:高并发交易系统(电商平台)、对性能要求较高的场景(秒杀、抢购)

  • 电商下单流程
  • 金融交易业务
  • 高并发秒杀场景

案例:蚂蚁金服Seata实现了TCC模式

代码实现

📢本代码仅供参考,切勿直接使用,主要带大家了解流程和大致写法,距离生产使用还需要进行较大优化

go 复制代码
package tcc

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

// TCCTransactionManager TCC事务管理器
type TCCTransactionManager struct {
    participants []TCCParticipant
    mutex        sync.RWMutex
    txID         string
}

// TCCParticipant TCC事务参与者接口
type TCCParticipant interface {
    Try(ctx context.Context) error     // 尝试执行业务
    Confirm(ctx context.Context) error // 确认提交
    Cancel(ctx context.Context) error  // 取消回滚
    GetID() string                     // 获取参与者ID
}

// NewTCCTransactionManager 创建新的TCC事务管理器
func NewTCCTransactionManager(txID string) *TCCTransactionManager {
    return &TCCTransactionManager{
       participants: make([]TCCParticipant, 0),
       txID:         txID,
    }
}

// AddParticipant 添加TCC事务参与者
func (tm *TCCTransactionManager) AddParticipant(participant TCCParticipant) {
    tm.mutex.Lock()
    defer tm.mutex.Unlock()
    tm.participants = append(tm.participants, participant)
}

// ExecuteTCC 执行TCC事务
func (tm *TCCTransactionManager) ExecuteTCC(ctx context.Context) error {
    // 设置默认超时时间
    ctxWithTimeout, cancel := context.WithTimeout(ctx, 30*time.Second)
    defer cancel()

    // Try阶段
    if err := tm.tryPhase(ctxWithTimeout); err != nil {
       // Try阶段失败,执行Cancel阶段
       tm.cancelAll(ctxWithTimeout)
       return fmt.Errorf("try phase failed: %w", err)
    }

    // Confirm阶段
    if err := tm.confirmPhase(ctxWithTimeout); err != nil {
       log.Printf("CRITICAL: confirm phase failed for transaction %s: %v", tm.txID, err)
       return fmt.Errorf("confirm phase failed: %w", err)
    }

    return nil
}

// tryPhase Try阶段 - 尝试执行业务
func (tm *TCCTransactionManager) tryPhase(ctx context.Context) error {
    tm.mutex.RLock()
    defer tm.mutex.RUnlock()

    log.Printf("Starting Try phase for transaction %s with %d participants", tm.txID, len(tm.participants))

    for i, participant := range tm.participants {
       if err := participant.Try(ctx); err != nil {
          return fmt.Errorf("participant %s Try failed at index %d: %w", participant.GetID(), i, err)
       }
       log.Printf("Participant %s Try succeeded", participant.GetID())
    }

    log.Printf("Try phase completed successfully for transaction %s", tm.txID)
    return nil
}

// confirmPhase Confirm阶段 - 确认提交
func (tm *TCCTransactionManager) confirmPhase(ctx context.Context) error {
    tm.mutex.RLock()
    defer tm.mutex.RUnlock()

    log.Printf("Starting Confirm phase for transaction %s", tm.txID)

    for i, participant := range tm.participants {
       if err := participant.Confirm(ctx); err != nil {
          log.Printf("CRITICAL: Participant %s Confirm failed at index %d. Manual intervention may be required.", participant.GetID(), i)
          return fmt.Errorf("participant %s Confirm failed at index %d: %w", participant.GetID(), i, err)
       }
       log.Printf("Participant %s Confirm succeeded", participant.GetID())
    }

    log.Printf("Confirm phase completed successfully for transaction %s", tm.txID)
    return nil
}

// cancelAll Cancel阶段 - 全部回滚
func (tm *TCCTransactionManager) cancelAll(ctx context.Context) {
    tm.mutex.RLock()
    defer tm.mutex.RUnlock()

    log.Printf("Canceling all participants for transaction %s", tm.txID)

    for i, participant := range tm.participants {
       if err := participant.Cancel(ctx); err != nil {
          log.Printf("Participant %s Cancel failed at index %d: %v", participant.GetID(), i, err)
       } else {
          log.Printf("Participant %s Cancel succeeded", participant.GetID())
       }
    }
}

// AccountServiceParticipant 账户服务参与者示例
type AccountServiceParticipant struct {
    id       string
    userID   int
    amount   float64
    db       interface{} // 实际项目中应替换为具体数据库连接
    reserved bool        // 标记是否已预留资源
}

// NewAccountServiceParticipant 创建账户服务参与者
func NewAccountServiceParticipant(id string, userID int, amount float64, db interface{}) *AccountServiceParticipant {
    return &AccountServiceParticipant{
       id:     id,
       userID: userID,
       amount: amount,
       db:     db,
    }
}

// Try 尝试阶段 - 冻结账户资金
func (asp *AccountServiceParticipant) Try(ctx context.Context) error {
    // 检查账户余额是否足够
    // balance := asp.getAccountBalance(asp.userID)
    // if balance < asp.amount {
    //     return fmt.Errorf("insufficient balance for user %d", asp.userID)
    // }

    // 冻结资金(预留资源)
    // asp.freezeAccountFund(asp.userID, asp.amount)
    asp.reserved = true

    log.Printf("Account service participant %s: Try phase succeeded - frozen amount %.2f for user %d",
       asp.id, asp.amount, asp.userID)
    return nil
}

// Confirm 确认阶段 - 扣除账户资金
func (asp *AccountServiceParticipant) Confirm(ctx context.Context) error {
    if !asp.reserved {
       return fmt.Errorf("resources not reserved for participant %s", asp.id)
    }

    // 实际扣除账户资金
    // asp.deductAccountFund(asp.userID, asp.amount)

    log.Printf("Account service participant %s: Confirm phase succeeded - deducted amount %.2f for user %d",
       asp.id, asp.amount, asp.userID)
    return nil
}

// Cancel 取消阶段 - 解冻账户资金
func (asp *AccountServiceParticipant) Cancel(ctx context.Context) error {
    if !asp.reserved {
       log.Printf("Account service participant %s: No resources to cancel", asp.id)
       return nil
    }

    // 解冻账户资金
    // asp.unfreezeAccountFund(asp.userID, asp.amount)
    asp.reserved = false

    log.Printf("Account service participant %s: Cancel phase succeeded - unfrozen amount %.2f for user %d",
       asp.id, asp.amount, asp.userID)
    return nil
}

// GetID 获取参与者ID
func (asp *AccountServiceParticipant) GetID() string {
    return asp.id
}

// InventoryServiceParticipant 库存服务参与者示例
type InventoryServiceParticipant struct {
    id        string
    productID string
    quantity  int
    db        interface{} // 实际项目中应替换为具体数据库连接
    reserved  bool        // 标记是否已预留资源
}

// NewInventoryServiceParticipant 创建库存服务参与者
func NewInventoryServiceParticipant(id string, productID string, quantity int, db interface{}) *InventoryServiceParticipant {
    return &InventoryServiceParticipant{
       id:        id,
       productID: productID,
       quantity:  quantity,
       db:        db,
    }
}

// Try 尝试阶段 - 预留库存
func (isp *InventoryServiceParticipant) Try(ctx context.Context) error {
    // 检查库存是否充足
    // stock := isp.getProductStock(isp.productID)
    // if stock < isp.quantity {
    //     return fmt.Errorf("insufficient stock for product %s", isp.productID)
    // }

    // 预留库存(冻结库存)
    // isp.reserveProductStock(isp.productID, isp.quantity)
    isp.reserved = true

    log.Printf("Inventory service participant %s: Try phase succeeded - reserved quantity %d for product %s",
       isp.id, isp.quantity, isp.productID)
    return nil
}

// Confirm 确认阶段 - 扣减库存
func (isp *InventoryServiceParticipant) Confirm(ctx context.Context) error {
    if !isp.reserved {
       return fmt.Errorf("resources not reserved for participant %s", isp.id)
    }

    // 实际扣减库存
    // isp.deductProductStock(isp.productID, isp.quantity)

    log.Printf("Inventory service participant %s: Confirm phase succeeded - deducted quantity %d for product %s",
       isp.id, isp.quantity, isp.productID)
    return nil
}

// Cancel 取消阶段 - 释放库存
func (isp *InventoryServiceParticipant) Cancel(ctx context.Context) error {
    if !isp.reserved {
       log.Printf("Inventory service participant %s: No resources to cancel", isp.id)
       return nil
    }

    // 释放库存(解冻库存)
    // isp.releaseProductStock(isp.productID, isp.quantity)
    isp.reserved = false

    log.Printf("Inventory service participant %s: Cancel phase succeeded - released quantity %d for product %s",
       isp.id, isp.quantity, isp.productID)
    return nil
}

// GetID 获取参与者ID
func (isp *InventoryServiceParticipant) GetID() string {
    return isp.id
}

// UsageExample 使用示例
func UsageExample() {
    // 创建TCC事务管理器
    manager := NewTCCTransactionManager("tcc_tx_12345")

    // 创建参与者(示例中使用mock数据库)
    // db1 := getAccountDBConnection()
    // db2 := getInventoryDBConnection()

    // 创建账户服务参与者
    // accountParticipant := NewAccountServiceParticipant("account_svc", 1, 100.0, db1)

    // 创建库存服务参与者
    // inventoryParticipant := NewInventoryServiceParticipant("inventory_svc", "product_001", 2, db2)

    // 添加参与者到事务管理器
    // manager.AddParticipant(accountParticipant)
    // manager.AddParticipant(inventoryParticipant)

    // 执行TCC事务
    // ctx := context.Background()
    // if err := manager.ExecuteTCC(ctx); err != nil {
    //     log.Fatalf("TCC transaction failed: %v", err)
    // }

    // fmt.Println("TCC transaction completed successfully")
    fmt.Println("TCC usage example created:", manager.txID)
}

3. AT模式

概念 :AT模式通过记录数据快照(Undo Log)实现事务的自动提交和回滚
流程

go 复制代码
// 执行阶段
1. 解析SQL,获取业务数据更新前快照
2. 执行业务SQL更新
3. 保存更新后数据镜像,生成回滚日志

// 提交阶段  
4. 每个分支事务立即提交
5. 异步删除回滚日志

// 回滚阶段
6. 根据回滚日志生成补偿SQL
7. 执行补偿SQL恢复数据

注意事项

  • Undo Log记录完整性: AT模式依赖于Undo Log记录业务数据变更前后完整快照,必须确保Undo Log记录的准确性和完整性,以保证回滚操作能够正确执行。
  • 回滚日志管理: Undo Log会占用额外存储空间,需要实现异步清理机制,在事务提交后及时删除已确认成功的回滚日志,避免日志堆积影响系统性能。

优缺点

✅ 无代码侵入:业务代码无需实现额外接口

✅ 性能较好:相比XA模式,资源锁定周期较短

✅ 易于使用:业务开发者只需关注业务逻辑

❌实现复杂:需要解析SQL并生成对应的补偿SQL

❌ 数据一致性:不是强一致性,而是最终一致性

❌ 存储开销:需要额外存储Undo日志

应用场景

  • 无需代码改造:由于AT模式是无侵入的,只需在方法上添加注解即可,无需修改现有代码。
  • 数据库支持事务:AT模式要求数据库支持本地事务操作。
  • 性能要求不高:虽然AT模式性能较好,但对于性能要求极高的场景,可能需要考虑其他模式。

PS:对强一致性要求极高的场景、简单的单服务操作等不适用
案例:蚂蚁金服Seata实现了AT模式

代码实现 :参考Seata的AT实现,Seata具有完整的SQL解析引擎,能自动解析业务SQL并生成对应的补偿SQL,

https://seata.apache.org/zh-cn/docs/dev/mode/at-mode/

4. 消息队列------最终一致性

注意:"最终一致性 = 不一致" ➜ 错误!最终一致性是在一定时间后达到一致,并不是放弃一致性。

最佳实践:

  • 消息体设计:包含业务ID、操作类型、时间戳等关键信息
  • 监控告警:监控消息堆积、消费延迟、死信队列
  • 灰度与压测:消息系统需验证高可用性和容灾能力
  • 文档与案例:记录常见故障处理流程(如消息重发、数据补偿)

概念 :核心思想是将分布式事务拆分成本地事务进行处理。通过异步解耦的方式,通过第三方中间件
流程

  1. 生产者阶段:生产者(如订单服务)在执行本地事务前,向消息队列发送"待确认"消息(如RocketMQ的半消息)。本地事务成功后,提交消息;若失败,则回滚消息。
  2. 消息队列阶段:消息队列存储消息并提供可靠投递机制(如失败重试、死信队列)。
  3. 消费者阶段:消费者(如库存服务)从队列中消费消息,执行本地事务,并确认消费结果。若失败,则重试或进入死信队列。
  4. 异步补偿:若生产者未提交或回滚消息,消息队列通过回查机制确认事务状态,确保消息最终被正确处理。

图例1:

图例2:

结合代码流程

阶段1:发送半消息(Half Message)

go 复制代码
p.SendMessageInTransaction(context, message)

//Producer 向 Broker 发送"半消息"
//Broker 将消息存储为"prepare"状态,此时Consumer不可见
//Broker 返回半消息发送成功

阶段2:执行本地事务

go 复制代码
func (dl *DemoListener) ExecuteLocalTransaction(msg *primitive.Message) primitive.LocalTransactionState {
    // 1. 执行本地数据库事务
    // 2. 返回状态
}

//CommitMessageState:立即提交,消息对Consumer可见
//RollbackMessageState:立即回滚,删除半消息
//UnknowState:未知,触发后续检查机制

阶段3:事务状态检查(如果返回Unknown)

go 复制代码
func (dl *DemoListener) CheckLocalTransaction(msg *primitive.MessageExt) primitive.LocalTransactionState {
    // 检查本地事务是否成功
}
//Broker 会以固定频率(默认1分钟)回调检查
//最多检查15次,超时后默认回滚

阶段4:消费者消费

go 复制代码
//只有状态为 Commit 的消息才会被消费
//消费者需要实现幂等性,防止重复消费

注意事项

  • 消息幂等性: 消费者必须保证接口的幂等性,同样的消息可能被重复投递,需要通过业务ID或其他唯一标识来防止重复处理导致的数据不一致。
  • 消息可靠性保障: 需要配置合适的消息重试机制、死信队列处理策略,确保消息在各种异常情况下都能得到妥善处理。
  • 监控告警体系: 必须建立完善的消息监控体系,包括消息堆积监控、消费延迟监控、失败率监控等,及时发现和处理异常情况。
  • 消费者异常处理: 消费者在处理消息失败时,需要有明确的重试策略和失败处理机制,避免因个别消息处理失败影响整体消费进度。
  • 顺序性保证:某些业务场景需要保证消息的顺序消费
  • 消息积压处理:需要有处理消息大量积压的应急预案

优缺点

✅ 系统解耦:通过消息队列实现服务间的松耦合

✅ 高可用性:支持失败重试和补偿机制

✅ 异步处理:提高系统响应速度和吞吐量

✅ 削峰填谷:缓冲瞬时高并发请求

✅ 灵活性:易于扩展和维护

❌最终一致性:不是强一致性,存在短暂不一致

❌ 复杂性增加:引入消息队列增加了系统复杂性

❌ 消息重复:需要处理消息重复消费问题

❌ 消息丢失风险:需要考虑消息持久化和可靠性

❌ 调试困难:异步处理使问题排查变得复杂

应用场景

  • 订单支付成功后通知发货
  • 用户注册后发送欢迎邮件
  • 数据同步场景

PS:针对强一致性要求极高、需要即时响应的业务的场景不太适用
案例:RocketMQ RabbitMQ等均可实现,RocketMQ 还有专门的事务型消息,新版的kafka也有。

代码实现

📢本代码仅供参考,切勿直接使用,主要带大家了解流程和大致写法,距离生产使用还需要进行较大优化

producer.go:

go 复制代码
package main

import (
    "context"
    "fmt"
    "github.com/apache/rocketmq-client-go/v2"
    "github.com/apache/rocketmq-client-go/v2/primitive"
    "github.com/apache/rocketmq-client-go/v2/producer"
    "os"
    "strconv"
    "sync"
    "sync/atomic"
    "time"
)

// 事务消息的结构体
type DemoListener struct {
    localTrans       *sync.Map
    transactionIndex int32
}

func NewDemoListener() *DemoListener {
    return &DemoListener{
       localTrans: new(sync.Map),
    }
}

// 执行并发送
func (dl *DemoListener) ExecuteLocalTransaction(msg *primitive.Message) primitive.LocalTransactionState {
    nextIndex := atomic.AddInt32(&dl.transactionIndex, 1)
    fmt.Printf("nextIndex: %v for transactionID: %v\n", nextIndex, msg.TransactionId)
    status := nextIndex % 3
    dl.localTrans.Store(msg.TransactionId, primitive.LocalTransactionState(status+1))
    //在执行SendMessageInTransaction方法的时候会调用此方法ExecuteLocalTransaction,
    //如果ExecuteLocalTransaction 返回primitive.UnknowState 那么brocker就会调用CheckLocalTransaction方法检查消息状态
    // 如果返回  primitive.CommitMessageState 和primitive.RollbackMessageState 则不会调用CheckLocalTransaction
    return primitive.UnknowState
    //return primitive.RollbackMessageState
    //return primitive.CommitMessageState
}

// 检查本地事务是否成功
func (dl *DemoListener) CheckLocalTransaction(msg *primitive.MessageExt) primitive.LocalTransactionState {
    v, existed := dl.localTrans.Load(msg.TransactionId)
    if !existed {
       fmt.Printf("unknow msg: %v, return Commit", msg)
       return primitive.CommitMessageState
    }
    state := v.(primitive.LocalTransactionState)
    fmt.Printf("检查本地事务是否成功 msg transactionID : %v\n", msg.TransactionId)
    switch state {
    case 1:
       fmt.Printf("回滚: %v\n", msg.Body)
       return primitive.RollbackMessageState
    case 2:
       fmt.Printf("未知: %v\n", msg.Body)
       return primitive.UnknowState
    default:
       fmt.Printf("默认提交: %v\n", msg.Body)
       return primitive.CommitMessageState
    }
}

func main() {
    p, _ := rocketmq.NewTransactionProducer(
       NewDemoListener(), //自定义listener
       producer.WithNsResolver(primitive.NewPassthroughResolver([]string{"192.168.3.98:9876"})), //连接地址
       producer.WithRetry(1),               //重试次数
       producer.WithGroupName("testGroup"), //生产者组,可有可无,跟client要对上
    )
    err := p.Start()
    if err != nil {
       fmt.Printf("开启失败: %s\n", err.Error())
       os.Exit(1)
    }

    topic := "test"
    for i := 0; i < 10; i++ {
       res, err := p.SendMessageInTransaction(
          context.Background(),
          primitive.NewMessage(topic, []byte("测试RocketMQ事务消息"+strconv.Itoa(i))),
       )

       if err != nil {
          fmt.Printf("发送消息失败: %s\n", err)
       } else {
          fmt.Printf("发送消息成功=%s\n", res.String())
       }
    }
    time.Sleep(5 * time.Minute)
    err = p.Shutdown()
    if err != nil {
       fmt.Printf("关闭失败: %s", err.Error())
    }
}

consumer.go:

go 复制代码
package main

import (
    "context"
    "fmt"
    "github.com/apache/rocketmq-client-go/v2"
    "github.com/apache/rocketmq-client-go/v2/consumer"
    "github.com/apache/rocketmq-client-go/v2/primitive"
    "os"
    "time"
)

func main() {
    c, _ := rocketmq.NewPushConsumer(
       consumer.WithGroupName("testGroup"), //组,跟服务端对上
       consumer.WithNsResolver(primitive.NewPassthroughResolver([]string{"192.168.3.98:9876"})), //地址
    )
    //消费对应topic
    err := c.Subscribe("test", consumer.MessageSelector{},
       func(ctx context.Context, msgs ...*primitive.MessageExt) (consumer.ConsumeResult, error) {
          for i := range msgs {
             fmt.Printf("订阅回调: %v \n", msgs[i])
          }
          //这个相当于消费者 消息ack,如果失败可以返回 consumer.ConsumeRetryLater
          return consumer.ConsumeSuccess, nil
          //这个相当于失败  要回滚
          //return consumer.ConsumeRetryLater, nil
       })
    if err != nil {
       fmt.Println(err.Error())
    }
    // Note: start after subscribe
    err = c.Start()
    if err != nil {
       fmt.Println(err.Error())
       os.Exit(-1)
    }
    time.Sleep(time.Hour)
    err = c.Shutdown()
    if err != nil {
       fmt.Printf("关闭消费者失败: %s", err.Error())
    }
}

5. SAGA模式

概念:长事务解决方案,将大事务拆分为多个本地事务;每个本地事务都有对应的补偿事务;执行失败时按反向顺序执行补偿事务。每个Saga步骤执行后需记录当前状态,确保在失败时能回滚至前一状态。常见实现方式有编排式(Orchestration)和编配式(Choreography)。

  • 编排式:
    中央协调者:存在一个专门的协调器(SagaOrchestrator)负责管理整个事务流程
    集中控制:协调器明确知道所有参与的服务和执行顺序
    主动通知:由协调器主动调用各个服务执行相应操作
  • 编配式:
    去中心化:没有中央协调者,各服务自主决策
    事件驱动:服务间通过事件消息进行通信
    被动响应:服务监听相关事件并作出响应

流程

go 复制代码
// 正向流程
1. 创建订单 → 2. 预留库存 → 3. 扣减余额 → 4. 创建物流单

// 补偿流程(任一失败时)
4. 取消物流单 ← 3. 返还余额 ← 2. 释放库存 ← 1. 取消订单

注意事项

  • 补偿逻辑完备性: 每个正向操作都必须有对应的补偿操作,补偿逻辑需要尽可能精确地撤销正向操作的影响,对于无法完全撤销的操作需要有明确的处理策略。
  • 异常处理策略: 补偿操作也可能失败,需要实现补偿失败的重试机制和最终处理方案,必要时需要人工介入处理。
  • 超时控制机制: 需要为每个SAGA步骤设置合理的超时时间,避免长时间阻塞影响系统性能,同时要有超时后的处理和补偿机制。
  • 编排式与编配式选择: 编排式适合业务流程相对固定的场景,编配式适合服务自治和松耦合的微服务架构,需要根据实际业务特点选择合适的实现方式。
  • 状态持久化:每个步骤的执行状态需要持久化存储

优缺点

✅ 无长时间资源锁定:每个本地事务独立提交,不阻塞其他操作

✅ 适用于长流程:支持跨服务、跨数据库的复杂业务场景

✅ 高并发友好:减少全局锁竞争,提升系统吞吐量

❌ 补偿逻辑复杂:需要为每个操作设计精确的逆向操作

❌ 数据一致性弱:中间状态对外可见,可能存在短暂不一致

❌ 实现成本高:需要完善的异常处理和状态管理机制

应用场景

  • 旅游预订(机票+酒店+租车组合)
  • 电商下单(创建订单→扣减库存→支付→发货)
  • 金融交易(转账→清算→结算)
  • 长时间运行的事务

代码实现(编排式)

📢注意:

  • 本代码仅供参考,切勿直接使用,主要带大家了解流程和大致写法,距离生产使用还需要进行较大优化。
  • 生产环境使用可参考go分布式事务框架:https://github.com/dtm-labs/dtm
go 复制代码
package main

import (
    "fmt"
    "log"
    "time"
)

// 定义一个简单的服务接口
type Service interface {
    DoSomething() error
    Compensate() error
}

// 模拟服务A
type ServiceA struct {
    data string
}

func (s *ServiceA) DoSomething() error {
    fmt.Println("Service A: Doing something...")
    // 模拟成功的情况
    s.data = "Service A data"
    return nil
}

func (s *ServiceA) Compensate() error {
    fmt.Println("Service A: Compensating...")
    s.data = "" // 撤销操作
    return nil
}

// 模拟服务B
type ServiceB struct {
    data int
}

func (s *ServiceB) DoSomething() error {
    fmt.Println("Service B: Doing something...")
    // 模拟成功的情况
    s.data = 123
    return nil
}

func (s *ServiceB) Compensate() error {
    fmt.Println("Service B: Compensating...")
    s.data = 0 // 撤销操作
    return nil
}

// Saga orchestrator
type SagaOrchestrator struct {
    services  []Service
    completed []bool
}

func NewSagaOrchestrator(services []Service) *SagaOrchestrator {
    return &SagaOrchestrator{
       services:  services,
       completed: make([]bool, len(services)),
    }
}

func (s *SagaOrchestrator) Run() error {
    for i, service := range s.services {
       err := service.DoSomething()
       if err != nil {
          log.Printf("Service %d failed: %v\n", i, err)
          return s.compensate(i)
       }
       s.completed[i] = true
    }
    return nil
}

func (s *SagaOrchestrator) compensate(failedIndex int) error {
    //任务失败,执行对应服务的补偿措施
    fmt.Println("Starting compensation...")
    for i := failedIndex; i >= 0; i-- {
       if s.completed[i] {
          err := s.services[i].Compensate()
          if err != nil {
             log.Printf("Compensation for service %d failed: %v\n", i, err)
             // 这里可以考虑重试补偿操作,或者记录日志并人工介入
             return err
          }
          s.completed[i] = false
       }
    }
    return nil
}

func main() {
    serviceA := &ServiceA{}
    serviceB := &ServiceB{}

    saga := NewSagaOrchestrator([]Service{serviceA, serviceB})

    err := saga.Run()
    if err != nil {
       log.Fatalf("Saga failed: %v\n", err)
    } else {
       fmt.Println("Saga completed successfully!")
    }

    time.Sleep(time.Second) // 模拟等待
}

6. 本地消息表

概念:本地消息表是一种通过数据库事务来保证消息可靠性的分布式事务解决方案。核心思想是将业务操作和消息记录放在同一个数据库事务中,确保数据一致性和消息可靠性。

流程

  1. 消息生产方(也就是发起方),需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。
  2. 消息消费方(也就是发起方的依赖方),需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
  3. 生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。

优缺点

✅ 实现简单:基于成熟的关系型数据库事务机制

✅ 数据一致性:业务数据与消息数据原子性保证

✅ 可靠性高:消息持久化存储,不易丢失

✅ 适用性强:大多数业务场景都能满足需求

❌ 性能瓶颈:依赖数据库性能,大量消息时可能成为瓶颈

❌ 扩展性差:难以水平扩展,受单库限制

❌ 实时性差:需要轮询机制,存在一定延迟

❌ 耦合度高:业务代码与消息表耦合较紧

适用场景

  • 数据一致性要求不是极端严格的场景
  • 消息量不是特别大的业务系统
  • 对实时性要求不太高的异步处理场景
  • 传统单体应用改造为微服务的过渡方案

代码实现

📢注意:本代码仅供参考,切勿直接使用,主要带大家了解流程和大致写法,距离生产使用还需要进行较大优化。

go 复制代码
package main

import (
    "context"
    "database/sql"
    "encoding/json"
    "fmt"
    "log"
    "time"

    _ "github.com/mattn/go-sqlite3"
)

// MessageStatus 消息状态枚举
type MessageStatus int

const (
    MessageStatusPending MessageStatus = iota // 待发送
    MessageStatusSent                         // 已发送
    MessageStatusFailed                       // 发送失败
)

// LocalMessage 本地消息表结构
type LocalMessage struct {
    ID         int64         `json:"id"`
    MessageID  string        `json:"message_id"`  // 消息唯一标识
    Topic      string        `json:"topic"`       // 消息主题
    Content    string        `json:"content"`     // 消息内容
    Status     MessageStatus `json:"status"`      // 消息状态
    RetryCount int           `json:"retry_count"` // 重试次数
    CreatedAt  time.Time     `json:"created_at"`  // 创建时间
    UpdatedAt  time.Time     `json:"updated_at"`  // 更新时间
}

// MessageProducer 消息生产者接口
type MessageProducer interface {
    SendMessage(topic string, content []byte) error
}

// LocalMessageService 本地消息服务
type LocalMessageService struct {
    db        *sql.DB
    producer  MessageProducer
    batchSize int
}

// NewLocalMessageService 创建本地消息服务
func NewLocalMessageService(db *sql.DB, producer MessageProducer) *LocalMessageService {
    service := &LocalMessageService{
       db:        db,
       producer:  producer,
       batchSize: 100,
    }

    // 初始化消息表
    service.initTable()
    return service
}

// initTable 初始化消息表
func (s *LocalMessageService) initTable() {
    createTableSQL := `
    CREATE TABLE IF NOT EXISTS local_messages (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        message_id TEXT UNIQUE NOT NULL,
        topic TEXT NOT NULL,
        content TEXT NOT NULL,
        status INTEGER DEFAULT 0,
        retry_count INTEGER DEFAULT 0,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
        updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
    );`

    _, err := s.db.Exec(createTableSQL)
    if err != nil {
       log.Fatal("Failed to create local_messages table:", err)
    }
}

// SaveMessageInTransaction 在业务事务中保存消息
func (s *LocalMessageService) SaveMessageInTransaction(tx *sql.Tx, messageID, topic string, content []byte) error {
    insertSQL := `
    INSERT INTO local_messages (message_id, topic, content, status, created_at, updated_at)
    VALUES (?, ?, ?, ?, ?, ?)`

    _, err := tx.Exec(insertSQL, messageID, topic, string(content), MessageStatusPending,
       time.Now(), time.Now())
    return err
}

// ProcessPendingMessages 处理待发送消息
func (s *LocalMessageService) ProcessPendingMessages(ctx context.Context) {
    ticker := time.NewTicker(5 * time.Second)
    defer ticker.Stop()

    for {
       select {
       case <-ctx.Done():
          return
       case <-ticker.C:
          s.sendPendingMessages()
       }
    }
}

// sendPendingMessages 发送待处理消息
func (s *LocalMessageService) sendPendingMessages() {
    // 查询待发送消息
    querySQL := `
    SELECT id, message_id, topic, content, retry_count
    FROM local_messages 
    WHERE status = ? AND retry_count < 3
    ORDER BY created_at ASC
    LIMIT ?`

    rows, err := s.db.Query(querySQL, MessageStatusPending, s.batchSize)
    if err != nil {
       log.Printf("Query pending messages failed: %v", err)
       return
    }
    defer rows.Close()

    for rows.Next() {
       var msg LocalMessage
       err := rows.Scan(&msg.ID, &msg.MessageID, &msg.Topic, &msg.Content, &msg.RetryCount)
       if err != nil {
          log.Printf("Scan message failed: %v", err)
          continue
       }

       // 发送消息
       if err := s.sendMessage(&msg); err != nil {
          s.handleSendFailure(&msg)
       } else {
          s.handleSendSuccess(&msg)
       }
    }
}

// sendMessage 发送单条消息
func (s *LocalMessageService) sendMessage(message *LocalMessage) error {
    return s.producer.SendMessage(message.Topic, []byte(message.Content))
}

// handleSendSuccess 处理发送成功
func (s *LocalMessageService) handleSendSuccess(message *LocalMessage) {
    updateSQL := `
    UPDATE local_messages 
    SET status = ?, updated_at = ?
    WHERE id = ?`

    _, err := s.db.Exec(updateSQL, MessageStatusSent, time.Now(), message.ID)
    if err != nil {
       log.Printf("Update message status failed: %v", err)
    } else {
       log.Printf("Message sent successfully: %s", message.MessageID)
    }
}

// handleSendFailure 处理发送失败
func (s *LocalMessageService) handleSendFailure(message *LocalMessage) {
    updateSQL := `
    UPDATE local_messages 
    SET retry_count = retry_count + 1, updated_at = ?
    WHERE id = ?`

    _, err := s.db.Exec(updateSQL, time.Now(), message.ID)
    if err != nil {
       log.Printf("Update message retry count failed: %v", err)
    } else {
       log.Printf("Message send failed, retry count: %d, message: %s",
          message.RetryCount+1, message.MessageID)
    }
}

// MockMessageProducer 模拟消息生产者实现
type MockMessageProducer struct{}

func (m *MockMessageProducer) SendMessage(topic string, content []byte) error {
    // 模拟偶发性发送失败
    if time.Now().Unix()%10 == 0 {
       return fmt.Errorf("network error")
    }

    log.Printf("Message sent to topic '%s': %s", topic, string(content))
    return nil
}

// BusinessService 业务服务示例
type BusinessService struct {
    db      *sql.DB
    message *LocalMessageService
}

// NewBusinessService 创建业务服务
func NewBusinessService(db *sql.DB, message *LocalMessageService) *BusinessService {
    return &BusinessService{
       db:      db,
       message: message,
    }
}

// ProcessOrder 处理订单业务(包含本地消息表)
func (b *BusinessService) ProcessOrder(orderID string, userID string, amount float64) error {
    // 开启数据库事务
    tx, err := b.db.Begin()
    if err != nil {
       return fmt.Errorf("begin transaction failed: %w", err)
    }
    defer tx.Rollback()

    // 1. 业务处理:创建订单记录
    insertOrderSQL := `INSERT INTO orders (order_id, user_id, amount) VALUES (?, ?, ?)`
    _, err = tx.Exec(insertOrderSQL, orderID, userID, amount)
    if err != nil {
       return fmt.Errorf("create order failed: %w", err)
    }

    // 2. 在同一事务中保存消息
    messageContent := map[string]interface{}{
       "order_id": orderID,
       "user_id":  userID,
       "amount":   amount,
       "event":    "order_created",
    }

    contentBytes, _ := json.Marshal(messageContent)
    messageID := fmt.Sprintf("msg_%s_%d", orderID, time.Now().Unix())

    err = b.message.SaveMessageInTransaction(tx, messageID, "order_events", contentBytes)
    if err != nil {
       return fmt.Errorf("save message failed: %w", err)
    }

    // 提交事务
    if err = tx.Commit(); err != nil {
       return fmt.Errorf("commit transaction failed: %w", err)
    }

    log.Printf("Order processed successfully: %s", orderID)
    return nil
}

// 初始化数据库和表结构
func initDatabase() *sql.DB {
    db, err := sql.Open("sqlite3", "./local_message.db")
    if err != nil {
       log.Fatal("Open database failed:", err)
    }

    // 创建订单表示例
    createOrderTableSQL := `
    CREATE TABLE IF NOT EXISTS orders (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        order_id TEXT UNIQUE NOT NULL,
        user_id TEXT NOT NULL,
        amount REAL NOT NULL,
        created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    );`

    _, err = db.Exec(createOrderTableSQL)
    if err != nil {
       log.Fatal("Create orders table failed:", err)
    }

    return db
}

// Example 使用示例
func Example() {
    // 初始化数据库
    db := initDatabase()
    defer db.Close()

    // 创建消息生产者和服务
    producer := &MockMessageProducer{}
    messageService := NewLocalMessageService(db, producer)

    // 创建业务服务
    businessService := NewBusinessService(db, messageService)

    // 启动消息处理器
    ctx, cancel := context.WithCancel(context.Background())
    go messageService.ProcessPendingMessages(ctx)
    defer cancel()

    // 处理订单业务
    err := businessService.ProcessOrder("ORDER_001", "USER_001", 99.99)
    if err != nil {
       log.Printf("Process order failed: %v", err)
    }

    // 等待一段时间观察消息发送
    time.Sleep(10 * time.Second)
}

func main() {
    Example()
}

拓展:开源框架

  • Seata:阿里开源的分布式事务解决方案
  • DTX:腾讯的分布式事务框架
  • Narayana:JBoss的事务管理器
  • 各云厂商的分布式事务服务

总结

方案 一致性 性能 复杂度 适用场景
2PC(XA) 强一致 传统企业、数据库层
TCC 最终一致 高并发、电商
AT 最终一致 大部分业务场景
消息队列 最终一致 异步场景(注册通知)、数据同步
SAGA 最终一致 长流程业务
本地消息表 最终一致 中小型系统

决策树

最佳实践

注意:具体方案需要选择适合自己业务的,同时也要考量成本与收益。

通用原则:

  1. 明确业务一致性要求:区分强一致性和最终一致性场景

强一致性:金融交易、账户余额等核心业务

最终一致性:订单状态更新、消息通知等非核心业务

  1. 权衡一致性与性能:根据业务容忍度选择合适方案

高并发场景优先考虑最终一致性方案

核心交易场景可接受适当性能损失换取强一致性

  1. 完善异常处理:考虑各种异常场景的处理策略

网络超时、服务宕机、数据不一致等场景

建立自动恢复和人工干预机制

  1. 建立监控体系:实时监控事务执行状态和性能指标

事务成功率、失败率、耗时分布

告警阈值设置和通知机制

  1. 充分测试验证:在线下环境充分测试各种异常场景

模拟网络分区、节点故障等异常情况

压力测试和混沌工程实验

设计建议:

  1. 优先考虑业务流程的合理性,避免不必要的分布式事务

通过业务拆分减少跨库操作

使用本地事务+异步消息替代分布式事务

  1. 合理划分服务边界,减少跨服务调用

遵循高内聚低耦合原则

考虑数据访问模式和服务调用频率

  1. 考虑使用事件驱动架构降低系统耦合度

通过消息中间件实现异步通信

采用发布订阅模式解耦服务间依赖

  1. 建立完善的重试和补偿机制

指数退避重试策略

幂等性设计防止重复操作

自动补偿和手动补偿相结合

相关推荐
西岭千秋雪_7 小时前
MySQL集群搭建
java·数据库·分布式·mysql
豐儀麟阁贵7 小时前
9.5格式化字符串
java·开发语言·前端·面试
踏浪无痕9 小时前
我们是如何把登录系统从“一行JWT”升级成企业级SSO的?
后端·面试·架构
CoderYanger9 小时前
动态规划算法-子数组、子串系列(数组中连续的一段):21.乘积最大子数组
开发语言·算法·leetcode·职场和发展·动态规划·1024程序员节
源代码•宸9 小时前
分布式缓存-GO(项目整体架构简介、Ubuntu 22.04 64位安装GoLang、安装Docker、解决Go module 的依赖问题)
经验分享·分布式·后端·ubuntu·缓存·docker·golang
努力学算法的蒟蒻10 小时前
day26(12.6)——leetcode面试经典150
算法·leetcode·面试
Ttang2310 小时前
【SpringCloud1】从单体架构到分布式系统架构
分布式·spring cloud·架构
AI弟11 小时前
推荐系统:带你走进推荐之路(二)
人工智能·python·深度学习·面试·推荐算法