使用Golang和GRPC实现TCC分布式事务(下)

背景

在上篇文章中,介绍了TCC分布式事务的基本流程,并实现了RM(resource manager)的基本结构,同时利用redis进行子事务屏蔽。

接下来,我们将进一步深入,继续实现APP(Application)TM(Transaction Manager)

前言

让我们再次回顾一下我们的项目tcc-toy的结构图

因为采用了Brokerless的网络模型,接下来的APP和TM将被整合为一个单一的实体进行实现,以便于简化我们的实现过程。

实现

首先是我们整个框架的时序图:

sequenceDiagram participant Main[TM] participant Main[APP] participant RM1 participant RM2 Main[APP]->>+RM1: Try RM1 -->> -Main[APP]: success Main[APP]->>+RM2: Try RM2 -->> -Main[APP]: success Main[TM] ->> +RM1: Commit/Cancel Main[TM] ->> +RM2: Commit/Cancel RM1 -->> -Main[TM]: Success RM2 -->> -Main[TM]: Success

定义用户的基本操作方法:

go 复制代码
transaction_manager.TCCCall(func(tx *transaction_manager.Transaction) transaction_manager.Operation {
   _, err := tx.CallTry("localhost:9210", &pb.TryRequest{Param: "tx1"})
   if err != nil {
      log.Printf("tx1 try failed: %v", err)
      return tx.Cancel()
   }
   _, err = tx.CallTry("localhost:9211", &pb.TryRequest{Param: "tx2"})
   if err != nil {
      log.Printf("tx2 try failed: %v", err)
      return tx.Cancel()
   }
   log.Printf("tx1 and tx2 try success")
   return tx.Commit()
})

在这里,用户通过调用TCCCall函数来启动事务,并传入一个函数变量txFunc。在txFunc函数中,用户会调用TransactionCallTry方法。当CallTry被调用时,Transaction会自动记录下参与事务的RM,这些信息将在后续的Commit或Cancel操作中使用。

在函数的最后,用户需要根据CallTry的结果以及自身的业务逻辑,决定是执行Commit操作还是Cancel操作。

实现Transaction结构体和TCCCall函数:

go 复制代码
type Transaction struct {
    xid       string                     # 事务ID
    ctx       context.Context
    resources []pb.ResourceManagerClient # 连接RM的GRPC客户端
}

func TCCCall(txFunc func(*Transaction) Operation) (err error) {
    t := &Transaction{
       xid:       uuid.New().String(),
       ctx:       context.Background(),
       resources: make([]pb.ResourceManagerClient, 0),
    }
    o := txFunc(t)
    if o == commit {
       t.commit()
    } else if o == cancel {
       t.cancel()
    } else {
       t.cancel()
       err = fmt.Errorf("unknown operation %v", o)
    }
    return
}

TCCCall函数的工作流程如下:首先,它会初始化一个Transaction实例并生成一个事务ID。接着,它会调用用户传入的txFunc函数。根据txFunc的返回值,TCCCall函数将决定是执行回滚还是提交操作。

我们可以看到,txFunc的返回值是一个我们自定义的类型Operation,它有两个可能的值:

go 复制代码
type Operation int

const (
    commit Operation = iota
    cancel
)

func (t *Transaction) Commit() Operation {
    return commit
}

func (t *Transaction) Cancel() Operation {
    return cancel
}

这样使用的时候,只需要return tx.Commit()或者return tx.Cancel(),尽可能地减少返回错误的概率,也更加的直观。

接下来,我们来开发调用Try接口的实现方法:

go 复制代码
func (t *Transaction) CallTry(host string, req *pb.TryRequest) (*pb.TryReply, error) {
    conn, err := grpc.Dial(host, grpc.WithInsecure(), grpc.WithBlock())
    if err != nil {
        return nil, err
    }
    resourceClient := pb.NewResourceManagerClient(conn)
    t.resources = append(t.resources, resourceClient)
    req.Xid = t.xid
    return resourceClient.Try(t.ctx, req)
}

这个函数功能相对比较简单,首先根据用户提供的host参数创建一个grpc客户端,然后将这个客户端记录到Transaction实例中,最后调用Try接口。

然后是commit方法:

go 复制代码
func (t *Transaction) commit() {
    wg := sync.WaitGroup{}
    retryResources := make([]pb.ResourceManagerClient, 0)
    resources := t.resources
    for {
       rwLock := sync.RWMutex{}
       for i := range resources { // 遍历所有的RM,调用Commit方法
          r := t.resources[i]
          wg.Add(1)
          go func(r pb.ResourceManagerClient) {
             defer wg.Done()
             _, err := r.Commit(t.ctx, &pb.CommitRequest{Xid: t.xid})
             rwLock.Lock()
             if err != nil {
                retryResources = append(retryResources, r)
             }
             rwLock.Unlock()
          }(r)
       }
       wg.Wait()
       if len(retryResources) > 0 {
          resources = retryResources
          retryResources = make([]pb.ResourceManagerClient, 0)
          t.backoff()
       } else {
          break
       }
    }
}

使用协程同时调用所有RMCommit接口,然后等待所有的结构返回结果后再结束函数。需要注意的是,到了这一步,所有的事务都必须完成Commit或者Cancel,否则的话就会出现数据不一致的问题。所以这里在调用Commit失败之后,会将失败的RM存入一个切片中,在短暂暂停之后,会继续调用这些RM的Commit接口。

cancel方法和commit类似,只是将Commit接口改成了Cancel接口。这里就不贴出来了,详情看tcc-toy

到这里我们整个项目就完成了,接下来看一下整个项目是怎样运行的。

例子

首先是我们的RM,RM需要先定义我们事务的3个接口,Try/Commit/Cancel(在Try里面随机选择返回成功或者失败),然后在main函数里面启动两个RM服务,分别在9210和9211端口:

go 复制代码
type MockResourceManager struct {
    pb.UnimplementedResourceManagerServer
}

func (manager *MockResourceManager) Try(_ context.Context, req *pb.TryRequest) (*pb.TryReply, error) {
    log.Printf("try -- xid: %s, param: %s\n", req.Xid, req.Param)
    if rand.Intn(10)%2 == 0 { // 随机选择返回错误或者成功
       return &pb.TryReply{}, errors.New("random error")
    }
    return &pb.TryReply{}, nil
}

func (manager *MockResourceManager) Commit(_ context.Context, req *pb.CommitRequest) (*pb.CommitReply, error) {
    log.Printf("commit -- xid: %s\n", req.Xid)
    return &pb.CommitReply{}, nil
}

func (manager *MockResourceManager) Cancel(_ context.Context, req *pb.CancelRequest) (*pb.CancelReply, error) {
    log.Printf("cancel -- xid: %s\n", req.Xid)
    return &pb.CancelReply{}, nil
}

func main() {
    redisCli := redis.NewClient(&redis.Options{Addr: "mydomain.com:6379", Password: "123456"})
    wg := sync.WaitGroup{}
    resourceManager := func(port int) {
       defer wg.Done()
       // 启动一个resource manager
       mock := &MockResourceManager{}
       rm := resource_manager.New(mock, redisCli, resource_manager.WithPort(port))
       if err := rm.Run(); err != nil {
          log.Printf("resource manager failed: %v", err)
          return
       }
    }
    wg.Add(2)
    go resourceManager(9210)
    go resourceManager(9211)
    wg.Wait()
}

然后是我们的APP,使用TCCCall开启事务,然后使用CallTry方法调用对应的RM服务的接口,最后选择Commit或者Cancel:

go 复制代码
func main() {
    err := transaction_manager.TCCCall(func(tx *transaction_manager.Transaction) transaction_manager.Operation {
       _, err := tx.CallTry("localhost:9210", &pb.TryRequest{Param: "tx1"})
       if err != nil {
          log.Printf("tx1 try failed: %v", err)
          return tx.Cancel()
       }
       _, err = tx.CallTry("localhost:9211", &pb.TryRequest{Param: "tx2"})
       if err != nil {
          log.Printf("tx2 try failed: %v", err)
          return tx.Cancel()
       }
       log.Printf("tx1 and tx2 try success")
       return tx.Commit()
    })
    if err != nil {
       log.Fatalf("transaction failed: %v", err)
    }
}

成功执行了APP之后,可以看到APP的日志

log 复制代码
2023/12/12 22:40:22 tx1 and tx2 try success
# 或者
2023/12/12 22:40:07 tx2 try failed: rpc error: code = Unknown desc = random error

总结

至此,《使用Golang和GRPC实现TCC分布式事务》系列已圆满结束。尽管这个项目相对简单,更像是一个玩具项目,但它对我来说是一次很好的学习和实践机会。我们通过这个项目,深入了解了TCC分布式事务的基本流程,并探索了如何结合Golang和GRPC来构建一个轻量级的分布式事务管理系统。

这个项目虽小,但实践中我们涉及了事务管理器(Transaction Manager)和资源管理器(Resource Manager)的实现,以及使用Redis作为子事务屏蔽的关键组件。希望通过这个项目,读者能够更深入地理解TCC事务的机制和设计原则。

欢迎读者在练习过程中尝试使用不同的编程语言来实现类似的分布式事务系统。这不仅有助于加深对TCC事务的理解,还能够锻炼读者在分布式系统设计和开发方面的能力。

最后,期待大家分享自己的实现成果和经验,共同学习、讨论!🎉

相关推荐
李洋-蛟龙腾飞公司1 小时前
HarmonyOS Next 应用元服务开发-分布式数据对象迁移数据文件资产迁移
分布式·华为·harmonyos
vvw&3 小时前
如何在 Ubuntu 22.04 上安装 Graylog 开源日志管理平台
linux·运维·服务器·ubuntu·开源·github·graylog
技术路上的苦行僧4 小时前
分布式专题(10)之ShardingSphere分库分表实战指南
分布式·shardingsphere·分库分表
HelloGitHub4 小时前
跟着 8.6k Star 的开源数据库,搞 RAG!
开源·github
GitCode官方5 小时前
GitCode 光引计划投稿 | GoIoT:开源分布式物联网开发平台
分布式·开源·gitcode
小扳6 小时前
微服务篇-深入了解 MinIO 文件服务器(你还在使用阿里云 0SS 对象存储图片服务?教你使用 MinIO 文件服务器:实现从部署到具体使用)
java·服务器·分布式·微服务·云原生·架构
zquwei16 小时前
SpringCloudGateway+Nacos注册与转发Netty+WebSocket
java·网络·分布式·后端·websocket·网络协议·spring
sdaxue.com17 小时前
帝国CMS:如何去掉帝国CMS登录界面的认证码登录
数据库·github·网站·帝国cms·认证码
m0_7482475517 小时前
github webhooks 实现网站自动更新
github
张国荣家的弟弟18 小时前
【Yonghong 企业日常问题04】永洪BI可视化工具Linux部署全攻略(部署详解版)
linux·运维·github