惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Recent Announcements
Recent Announcements
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
B
Blog
T
The Blog of Author Tim Ferriss
J
Java Code Geeks
腾讯CDC
D
Docker
G
Google Developers Blog
D
DataBreaches.Net
雷峰网
雷峰网
Blog — PlanetScale
Blog — PlanetScale
S
SegmentFault 最新的问题
The Cloudflare Blog
有赞技术团队
有赞技术团队
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Stack Overflow Blog
Stack Overflow Blog
大猫的无限游戏
大猫的无限游戏
量子位
美团技术团队
aimingoo的专栏
aimingoo的专栏
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Engineering at Meta
Engineering at Meta
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More

Blog Feed

基于 Debezium + Kafka 实现 CDC - 字节星球 gRPC Resolver 实现 Nacos 订阅 - 字节星球 且慢!先来实现分布式锁! - 字节星球 基于 TCC 写一个所谓的分布式事务#1 - 字节星球 写一个所谓的通用状态机 - 字节星球 锐评简笔记/某站商店/某彦 - 字节星球 浅谈 DDD 领域驱动设计架构 - 字节星球 浅谈分布式事务#2 - 字节星球 浅谈分布式事务 - 字节星球 聊聊大学里面的奇葩 - 字节星球 MySQL 加锁机制分析与死锁排查 - 字节星球 Golang pprof 案例实战 - 字节星球 Golang 手写一个 Channel - 字节星球 字节星球终于全栈上新! - 字节星球 欢迎试用 TodoList - 字节星球 记一次“说走就走”的成都行 - 字节星球 Docker Desktop修改默认存储路径 - 字节星球 流程中心使用指南 - 字节星球 微分方程小手册 - 字节星球 近期小记 - 字节星球 字节星球(肥柴之家)搬家了! - 字节星球 如何顺利注册ChatGPT? - 字节星球 披着CLion的外衣实则在讲CMake - 字节星球 守望之墓/电子骨灰盒 - 字节星球 Python笔记 第三章 - 字节星球 WePlanet现已发布! - 字节星球 简单选择排序和堆排序 - 字节星球 希尔排序 - 字节星球 插入排序 - 字节星球 快速排序 - 字节星球
基于 TCC 写一个所谓的分布式事务#2 - 字节星球
2026-08-04 · via Blog Feed

2026/8/4 18:35:00Henry4 阅读0 点赞0 评论

紧接上篇,实现 TCC 的使用方。

实现 TCC Component

首先需要我们需要定义一些状态,比如事务的状态,业务方的状态(此处只是作为模拟,不涉及具体逻辑),并定义最基本的 Component 结构。

状态定义

GO

// tcc 组件侧记录的一笔事务的状态
type TXStatus string

func (t TXStatus) String() string {
	return string(t)
}

const (
	TXTried     TXStatus = "tried"     // 已执行 try 操作
	TXConfirmed TXStatus = "confirmed" // 已执行 confirm 操作
	TXCanceled  TXStatus = "canceled"  // 已执行 cancel 操作
)

// 一笔事务对应数据的状态
type DataStatus string

func (d DataStatus) String() string {
	return string(d)
}

const (
	DataFrozen     DataStatus = "frozen"     // 冻结态
	DataSuccessful DataStatus = "successful" // 成功态
)

结构定义

首先实现一个简单的 TCC Component 结构,里面应该包含一个 id,还有一个 client,这里的 client 其实是一个 Redis Client,主要用于数据存储和分布式锁,locker 则是我们上一篇基于 Redis 实现的分布式锁。

GO

type MockComponent struct {
	id     string
	client redislib.UniversalClient
	locker dlock.Locker
}

GO

func NewMockComponent(id string, client redislib.UniversalClient, locker dlock.Locker) *MockComponent {
	return &MockComponent{
		id:     id,
		client: client,
		locker: locker,
	}
}

下面按照顺序来分别实现 TCC Component 的基本方法。

Try

Try 方法的流程之前已经介绍过,事务方调用 Component Try 时,首先会添加分布式锁,然后进行幂等校验,将资源冻结(等到后续 Confirm 后才最终落库提交),然后记录事务的状态。

GO

func (m *MockComponent) Try(ctx context.Context, req *gotcc.TCCReq) (*gotcc.TCCResp, error) {
	// 基于 txID 维度加锁,这里不涉及耗时操作,直接继承 locker 的配置即可
	mutex := m.locker.NewMutex(pkg.BuildTXLockKey(m.id, req.TXID))
	if err := mutex.Lock(ctx); err != nil {
		return nil, err
	}
	defer func() {
		unlockCtx, unlockCancel := context.WithTimeout(context.WithoutCancel(ctx), 3*time.Second)
		defer unlockCancel()
		_ = mutex.Unlock(unlockCtx)
	}()

	// 基于 txID 幂等性去重,注意这里需要特殊排除一下键不存在的情况
	txStatus, err := m.client.Get(ctx, pkg.BuildTXKey(m.id, req.TXID)).Result()
	if err != nil && !errors.Is(err, redislib.Nil) {
		return nil, err
	}

	res := &gotcc.TCCResp{
		ComponentID: m.id,
		TXID:        req.TXID,
	}
	switch txStatus {
	// 重复的 try 请求,给予成功的响应
	case TXTried.String(), TXConfirmed.String():
		res.ACK = true
		return res, nil
	// 先 cancel,后收到 try 请求,拒绝
	case TXCanceled.String():
		return res, nil
	default:
	}

	// 执行 try 操作,将数据状态置为 frozen
	bizID := gocast.ToString(req.Data["biz_id"])
	// 存储 bizID 和事务的关系,Confirm/Cancel 后删除,这里没有设置 TTL
	if err := m.client.SetNX(ctx, pkg.BuildTXDetailKey(m.id, req.TXID), bizID, 0).Err(); err != nil {
		return nil, err
	}

	// 要求必须从零到一把 bizID 对应的数据置为冻结态
	created, err := m.client.SetNX(ctx, pkg.BuildDataKey(m.id, req.TXID, bizID), DataFrozen.String(), 0).Result()
	if err != nil {
		return nil, err
	}
	if !created {
		return res, nil
	}

	// 更新事务状态至 Tried
	if err := m.client.Set(ctx, pkg.BuildTXKey(m.id, req.TXID), TXTried.String(), 0).Err(); err != nil {
		return nil, fmt.Errorf("set transaction tried, txid=%s: %w", req.TXID, err)
	}

	// try 请求执行成功
	res.ACK = true

	// TEST ComponentB Try Failed
	// if strings.Contains(req.ComponentID, "componentB") {
	// 	res.ACK = false
	// }

	return res, nil
}

Confirm

Confirm 则是将冻结的业务数据推向终态,本方法由 TXManager 在所有 Components Try 均返回成功响应后调用。

GO

func (m *MockComponent) Confirm(ctx context.Context, txID string) (*gotcc.TCCResp, error) {
	// 基于 txID 维度加锁
	mutex := m.locker.NewMutex(pkg.BuildTXLockKey(m.id, txID))
	if err := mutex.Lock(ctx); err != nil {
		return nil, err
	}
	defer func() {
		unlockCtx, unlockCancel := context.WithTimeout(context.WithoutCancel(ctx), 3*time.Second)
		defer unlockCancel()
		_ = mutex.Unlock(unlockCtx)
	}()

	// 要求 txID 此前状态为 tried
	// 如果该键不存在也直接抛出错误
	txStatus, err := m.client.Get(ctx, pkg.BuildTXKey(m.id, txID)).Result()
	if err != nil {
		return nil, err
	}

	res := &gotcc.TCCResp{
		ComponentID: m.id,
		TXID:        txID,
	}

	switch txStatus {
	// 已 confirm,直接幂等响应为成功
	case TXConfirmed.String():
		res.ACK = true
		return res, nil
	// 只有状态为 try 放行
	case TXTried.String():
	// 其他情况直接拒绝
	default:
		return res, nil
	}

	// 获取事务对应的 bizID
	bizID, err := m.client.Get(ctx, pkg.BuildTXDetailKey(m.id, txID)).Result()
	if err != nil {
		return nil, err
	}

	// 要求对应的数据状态此前为 frozen
	dataStatus, err := m.client.Get(ctx, pkg.BuildDataKey(m.id, txID, bizID)).Result()
	if err != nil {
		return nil, err
	}
	switch dataStatus {
	case DataFrozen.String():
		// 把对应数据处理状态置为 successful
		if err = m.client.Set(ctx, pkg.BuildDataKey(m.id, txID, bizID), DataSuccessful.String(), 0).Err(); err != nil {
			return nil, err
		}
	// 数据已经推进状态了,只需推进 TX 状态即可
	case DataSuccessful.String():
	// 状态非法
	default:
		return res, nil
	}

	// 把事务状态更新为成功
	if err := m.client.Set(
		ctx,
		pkg.BuildTXKey(m.id, txID),
		TXConfirmed.String(),
		0,
	).Err(); err != nil {
		return nil, fmt.Errorf("set transaction confirmed, txid=%s: %w", txID, err)
	}

	// 处理成功,给予成功的响应
	res.ACK = true
	return res, nil
}

Cancel

和 Confirm 类似,这里是处理 TCC 事务失败的链路。在实际业务实现中这里应该对冻结的数据操作进行回滚。

GO

func (m *MockComponent) Cancel(ctx context.Context, txID string) (*gotcc.TCCResp, error) {
	// 基于 txID 维度加锁
	mutex := m.locker.NewMutex(pkg.BuildTXLockKey(m.id, txID))
	if err := mutex.Lock(ctx); err != nil {
		return nil, err
	}
	defer func() {
		unlockCtx, unlockCancel := context.WithTimeout(context.WithoutCancel(ctx), 3*time.Second)
		defer unlockCancel()
		_ = mutex.Unlock(unlockCtx)
	}()

	tccResp := &gotcc.TCCResp{
		ACK:         true,
		ComponentID: m.id,
		TXID:        txID,
	}

	// 查看事务的状态,只要不是 confirmed,就无脑置为 canceled
	// 其次事务如果根本不存在,也应该抛出错误
	txStatus, err := m.client.Get(ctx, pkg.BuildTXKey(m.id, txID)).Result()
	if err != nil {
		return nil, err
	}
	switch txStatus {
	// 非法流转链路
	case TXConfirmed.String():
		return nil, fmt.Errorf("invalid tx status: %s, txid: %s", txStatus, txID)
	// 直接幂等返回
	case TXCanceled.String():
		return tccResp, nil
	// 进入正常 Cancel 链路
	case TXTried.String():
	default:
		return nil, fmt.Errorf(
			"unexpected tx status: %s, txid: %s",
			txStatus,
			txID,
		)
	}

	// 根据事务获取对应的 bizID
	bizID, err := m.client.Get(ctx, pkg.BuildTXDetailKey(m.id, txID)).Result()
	if err != nil {
		return nil, err
	}

	// 删除对应的 frozen 冻结记录
	if err = m.client.Del(ctx, pkg.BuildDataKey(m.id, txID, bizID)).Err(); err != nil {
		return nil, err
	}

	// 把事务状态更新为 canceled
	if err := m.client.Set(
		ctx,
		pkg.BuildTXKey(m.id, txID),
		TXCanceled.String(),
		0,
	).Err(); err != nil {
		return nil, fmt.Errorf("set transaction canceled, txid=%s: %w", txID, err)
	}

	return tccResp, nil
}

想必大伙儿已经发现,在 Try/Confirm/Cancel 方法中,同样维护了一份事务状态,即:

GO

const (
	TXTried     TXStatus = "tried"     // 已执行 try 操作
	TXConfirmed TXStatus = "confirmed" // 已执行 confirm 操作
	TXCanceled  TXStatus = "canceled"  // 已执行 cancel 操作
)

看过之前 TCC 框架原理应该清楚,在 TXStore 中也维护了一份事务状态,那这个事务状态和 Component 中的有何不同?其实就是视角不同。

TXStore 中维护的是全局视角下的事务状态,即由多个 Components 组成的分布式事务状态,而 Component 中维护的仅仅是当前实例下的事务状态(单机事务)。

实现 TXStore

数据库及 DAO

这里我们简单设计一下数据库结构及其 DAO 实现,只需要记录分布式事务状态和各个 Components Try 状态即可,为了简单,Components Try 状态直接采用 JSON 存储:

SQL

CREATE TABLE IF NOT EXISTS `tx_record`
(
    `id`                       bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    `status`                   varchar(16) NOT NULL COMMENT '事务状态 hanging/successful/failure',
    `component_try_statuses`   json DEFAULT NULL COMMENT '各组件 try 接口请求状态 hanging/successful/failure',
    `deleted_at`        datetime     DEFAULT NULL COMMENT '删除时间',
    `created_at`        datetime     NOT NULL COMMENT '创建时间',
    `updated_at`        datetime     DEFAULT NULL ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    PRIMARY KEY (`id`) USING BTREE COMMENT '主键索引',
    KEY `idx_status` (`status`) COMMENT '事务状态索引'
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT '事务日志记录';

GO

type QueryOption func(db *gorm.DB) *gorm.DB

func WithID(id uint) QueryOption {
	return func(db *gorm.DB) *gorm.DB {
		return db.Where("id = ?", id)
	}
}

func WithStatus(status gotcc.TXStatus) QueryOption {
	return func(db *gorm.DB) *gorm.DB {
		return db.Where("status = ?", status.String())
	}
}

GO

type TXRecordPO struct {
	gorm.Model
	Status               string `gorm:"status"`
	ComponentTryStatuses string `gorm:"component_try_statuses"`
}

func (t TXRecordPO) TableName() string {
	return "tx_record"
}

type ComponentTryStatus struct {
	ComponentID string `json:"componentID"`
	TryStatus   string `json:"tryStatus"`
}

type TXRecordDAO struct {
	db *gorm.DB
}

func NewTXRecordDAO(db *gorm.DB) *TXRecordDAO {
	return &TXRecordDAO{
		db: db,
	}
}

func (t *TXRecordDAO) GetTXRecords(ctx context.Context, opts ...QueryOption) ([]*TXRecordPO, error) {
	db := t.db.WithContext(ctx).Model(&TXRecordPO{})
	for _, opt := range opts {
		db = opt(db)
	}

	var records []*TXRecordPO
	return records, db.Scan(&records).Error
}

func (t *TXRecordDAO) CreateTXRecord(ctx context.Context, record *TXRecordPO) (uint, error) {
	return record.ID, t.db.WithContext(ctx).Model(&TXRecordPO{}).Create(record).Error
}

// UpdateComponentStatus 更新事务中的 Component 状态
func (t *TXRecordDAO) UpdateComponentStatus(ctx context.Context, id uint, componentID string, status string) error {
	return t.LockAndDo(ctx, id, func(ctx context.Context, dao *TXRecordDAO, record *TXRecordPO) error {
		var statuses map[string]*ComponentTryStatus
		if err := json.Unmarshal([]byte(record.ComponentTryStatuses), &statuses); err != nil {
			return err
		}

		componentStatus, ok := statuses[componentID]
		if !ok {
			return fmt.Errorf("invalid component: %s in txid: %d", componentID, id)
		}
		if componentStatus.TryStatus == status {
			return nil
		}

		if componentStatus.TryStatus == gotcc.TryHanging.String() {
			componentStatus.TryStatus = status
			body, _ := json.Marshal(statuses)
			record.ComponentTryStatuses = string(body)
			return dao.UpdateTXRecord(ctx, record)
		}

		return fmt.Errorf("invalid status: %s of component: %s, txid: %d", statuses[componentID].TryStatus, componentID, id)
	})
}

func (t *TXRecordDAO) UpdateTXRecord(ctx context.Context, record *TXRecordPO) error {
	return t.db.WithContext(ctx).Updates(record).Error
}

// LockAndDo 加锁拿到对应 id 的事务记录,然后对其执行 do 函数
func (t *TXRecordDAO) LockAndDo(ctx context.Context, id uint, do func(ctx context.Context, dao *TXRecordDAO, record *TXRecordPO) error) error {
	return t.db.Transaction(func(tx *gorm.DB) error {
		// 加写锁
		var record TXRecordPO

		if err := tx.WithContext(ctx).Clauses(clause.Locking{Strength: "UPDATE"}).First(&record, id).Error; err != nil {
			return err
		}

		txDAO := NewTXRecordDAO(tx)
		return do(ctx, txDAO, &record)
	})
}

结构定义

TXStore 采用关系型数据库的方式记录事务状态,所以这里我们将之前定义的 DAO 写入结构体,此外同样需要分布式锁 locker,这里的分布式锁仅仅是为了避免 TXManager 的全局轮询被多实例同时执行:

GO

type TXRecordDAO interface {
	GetTXRecords(ctx context.Context, opts ...expdao.QueryOption) ([]*expdao.TXRecordPO, error)
	CreateTXRecord(ctx context.Context, record *expdao.TXRecordPO) (uint, error)
	UpdateComponentStatus(ctx context.Context, id uint, componentID string, status string) error
	UpdateTXRecord(ctx context.Context, record *expdao.TXRecordPO) error
	LockAndDo(ctx context.Context, id uint, do func(ctx context.Context, dao *expdao.TXRecordDAO, record *expdao.TXRecordPO) error) error
}

GO

type MockTXStore struct {
	locker dlock.Locker
	mutex dlock.Mutex
	dao    TXRecordDAO
}

func NewMockTXStore(dao TXRecordDAO, locker dlock.Locker) *MockTXStore {
	return &MockTXStore{
		dao:    dao,
		locker: locker,
	}
}

事务操作

GO

// CreateTX 创建一条事务,并注入 TCC Components
func (m *MockTXStore) CreateTX(ctx context.Context, components ...gotcc.TCCComponent) (string, error) {
	// 创建一项内容,里面以唯一事务 id 为 key
	componentTryStatuses := make(map[string]*expdao.ComponentTryStatus, len(components))
	for _, component := range components {
		componentTryStatuses[component.ID()] = &expdao.ComponentTryStatus{
			ComponentID: component.ID(),
			TryStatus:   gotcc.TryHanging.String(),
		}
	}

	// 记录每个 Component 的处理状态
	statusesBody, _ := json.Marshal(componentTryStatuses)
	txID, err := m.dao.CreateTXRecord(ctx, &expdao.TXRecordPO{
		Status:               gotcc.TXHanging.String(),
		ComponentTryStatuses: string(statusesBody),
	})
	if err != nil {
		return "", err
	}

	return gocast.ToString(txID), nil
}

// TXUpdate 在事务日志中更新 Component 状态
func (m *MockTXStore) TXUpdate(ctx context.Context, txID string, componentID string, accept bool) error {
	_txID := gocast.ToUint(txID)
	status := gotcc.TXFailure.String()
	if accept {
		status = gotcc.TXSuccessful.String()
	}
	return m.dao.UpdateComponentStatus(ctx, _txID, componentID, status)
}

// GetHangingTXs 轮询用,获取还在 hanging 状态的事务
func (m *MockTXStore) GetHangingTXs(ctx context.Context) ([]*gotcc.Transaction, error) {
	records, err := m.dao.GetTXRecords(ctx, expdao.WithStatus(gotcc.TXHanging))
	if err != nil {
		return nil, err
	}

	txs := make([]*gotcc.Transaction, 0, len(records))
	for _, record := range records {
		componentTryStatuses := make(map[string]*expdao.ComponentTryStatus)
		_ = json.Unmarshal([]byte(record.ComponentTryStatuses), &componentTryStatuses)
		components := make([]*gotcc.ComponentTryEntity, 0, len(componentTryStatuses))
		for _, component := range componentTryStatuses {
			components = append(components, &gotcc.ComponentTryEntity{
				ComponentID: component.ComponentID,
				TryStatus:   gotcc.ComponentTryStatus(component.TryStatus),
			})
		}

		txs = append(txs, &gotcc.Transaction{
			TXID:       gocast.ToString(record.ID),
			Status:     gotcc.TXHanging,
			CreatedAt:  record.CreatedAt,
			Components: components,
		})
	}

	return txs, nil
}

// TXSubmit 提交事务的最终状态
func (m *MockTXStore) TXSubmit(ctx context.Context, txID string, success bool) error {
	do := func(ctx context.Context, dao *expdao.TXRecordDAO, record *expdao.TXRecordPO) error {
		if success {
			if record.Status == gotcc.TXFailure.String() {
				return fmt.Errorf("invalid tx status: %s, txid: %s", record.Status, txID)
			}
			record.Status = gotcc.TXSuccessful.String()
		} else {
			if record.Status == gotcc.TXSuccessful.String() {
				return fmt.Errorf("invalid tx status: %s, txid: %s", record.Status, txID)
			}
			record.Status = gotcc.TXFailure.String()
		}
		return dao.UpdateTXRecord(ctx, record)
	}
	return m.dao.LockAndDo(ctx, gocast.ToUint(txID), do)
}

// GetTX 获取指定的一笔事务
func (m *MockTXStore) GetTX(ctx context.Context, txID string) (*gotcc.Transaction, error) {
	records, err := m.dao.GetTXRecords(ctx, expdao.WithID(gocast.ToUint(txID)))
	if err != nil {
		return nil, err
	}
	if len(records) != 1 {
		return nil, errors.New("get tx failed")
	}

	componentTryStatuses := make(map[string]*expdao.ComponentTryStatus)
	_ = json.Unmarshal([]byte(records[0].ComponentTryStatuses), &componentTryStatuses)

	components := make([]*gotcc.ComponentTryEntity, 0, len(componentTryStatuses))
	for _, tryItem := range componentTryStatuses {
		components = append(components, &gotcc.ComponentTryEntity{
			ComponentID: tryItem.ComponentID,
			TryStatus:   gotcc.ComponentTryStatus(tryItem.TryStatus),
		})
	}
	return &gotcc.Transaction{
		TXID:       txID,
		Status:     gotcc.TXStatus(records[0].Status),
		Components: components,
		CreatedAt:  records[0].CreatedAt,
	}, nil
}

实现全局锁

TXStore 全局分布式锁在 TXManager 轮询中使用。

GO

func (m *MockTXStore) Lock(ctx context.Context, expireDuration time.Duration) error {
	m.mutex = m.locker.NewMutex(pkg.BuildTXRecordLockKey(), dlock.WithExpiry(expireDuration))
	return m.mutex.Lock(ctx)
}

func (m *MockTXStore) Unlock(ctx context.Context) error {
	if m.mutex == nil {
		return errors.New("tx store mutex is not locked")
	}
	return m.mutex.Unlock(ctx)
}

测试运行

下列测试所有 Components Try 都会通过,如果需要测试失败的情况,可以修改一下 Try 方法,使得其中某个 Component Try 执行失败。

GO

const (
	dsn      = "请输入你的 mysql dsn"
	address  = "请输入你的 redis ip"
	password = "请输入你的 redis 密码"
)

func main() {
	redisClient := pkg.NewRedisClient(address, password)
	locker := dlock.NewRedisLocker(redisClient, dlock.DefaultConfig())


	mysqlDB, err := pkg.NewDB(dsn)
	if err != nil {
		fmt.Println(err)
		return
	}

	componentAID := "componentA"
	componentBID := "componentB"
	componentCID := "componentC"

	// 构造出对应的 tcc component
	componentA := NewMockComponent(componentAID, redisClient, locker)
	componentB := NewMockComponent(componentBID, redisClient, locker)
	componentC := NewMockComponent(componentCID, redisClient, locker)

	// 构造出事务日志存储模块
	txRecordDAO := dao.NewTXRecordDAO(mysqlDB)
	txStore := NewMockTXStore(txRecordDAO, locker)

	txManager := gotcc.NewTXManager(txStore, gotcc.WithMonitorTick(time.Second))
	defer txManager.Stop()

	// 完成各组件的注册
	if err := txManager.Register(componentA); err != nil {
		fmt.Println(err)
		return
	}

	if err := txManager.Register(componentB); err != nil {
		fmt.Println(err)
		return
	}

	if err := txManager.Register(componentC); err != nil {
		fmt.Println(err)
		return
	}

	ctx, cancel := context.WithTimeout(context.Background(), time.Second*30)
	defer cancel()
	_, success, err := txManager.Transaction(ctx, []*gotcc.RequestEntity{
		{ComponentID: componentAID,
			Request: map[string]interface{}{
				"biz_id": componentAID + "_biz",
			},
		},
		{ComponentID: componentBID,
			Request: map[string]interface{}{
				"biz_id": componentBID + "_biz",
			},
		},
		{ComponentID: componentCID,
			Request: map[string]interface{}{
				"biz_id": componentCID + "_biz",
			},
		},
	}...)
	if err != nil {
		fmt.Printf("tx failed, err: %v", err)
		return
	}
	if !success {
		fmt.Println("tx failed")
	}

	// 这里等待一分钟,观察 TxManager 异步轮询 Hanging Transaction
	<-time.After(time.Minute)

	fmt.Println("success")
}

结束!垮台!