Go微服务: 分布式之发送带有事务消息的示例

分布式之发送带有事务消息

  • 现在做一个RocketMQ的事务消息的 demo

1 )生产者

go 复制代码
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/apache/rocketmq-client-go/v2"
	"github.com/apache/rocketmq-client-go/v2/primitive"
	"github.com/apache/rocketmq-client-go/v2/producer"
)

// 自定义结构体,为了实现 NewTransactionProducer 第一个参数的接口
type MyListener struct{}

func (hl MyListener) ExecuteLocalTransaction(*primitive.Message) primitive.LocalTransactionState {
	return primitive.CommitMessageState
}

func (hl MyListener) CheckLocalTransaction(*primitive.MessageExt) primitive.LocalTransactionState {
	return primitive.CommitMessageState
}

func main() {
	// ------------ 1. 连接RocketMQ ------------------------
	mqAddr := "127.0.0.1:9876" // 模拟地址
	// NewTransactionProducer 这个方法第一个参数是一个 Listener
	// 是一个接口,需要一个接口体去实现它的方法
	p, err := rocketmq.NewTransactionProducer( // 开启事物消息生产者
		MyListener{},
		producer.WithNameServer([]string{mqAddr}),
	)
	if err != nil {
		panic(err) // 生产环境禁用panic
	}
	// ------------ 2. 启动RocketMQ ------------------------
	err = p.Start()
	if err != nil {
		panic(err)
	}
	// ------------ 3. 发送RocketMQ 消息 ------------------------
	res, err := p.SendMessageInTransaction(
		context.Background(),
		primitive.NewMessage("MyTransactionTopic", []byte("xxxxxxxxxxxyyyyyyyyyyyyyzzzzzzzzzzzz")),
	)
	fmt.Println(res.Status)
	if err != nil {
		panic(err)
	}
	fmt.Printf("发送成功")
	time.Sleep(time.Second * 3600)
	err = p.Shutdown()
	if err != nil {
		panic(err)
	}
}
  • 可见, primitive.LocalTransactionState 是返回值
  • 进入这个包中,它有三个状态
    • CommitMessageState 提交状态
    • RollbackMessageState 回滚状态
    • UnknowState 未知状态
  • 在后续,这三个状态都可以再试一试

2 ) 运行后,在UI界面查看消息

2.1 输出信息如下

conf 复制代码
INFO[0000] change the route for clients                 
INFO[0000] the topic route info changed                  changeTo="{\"OrderTopicConf\":\"\",\"queueDatas\":[{\"brokerName\":\"broker-bbs\",\"readQueueNums\":4,\"writeQueueNums\":4,\"perm\":6,\"topicSynFlag\":0}],\"brokerDatas\":[{\"cluster\":\"DefaultCluster\",\"brokerName\":\"broker-bbs\",\"brokerAddrs\":{\"0\":\"192.168.124.6:10911\"}}]}" changedFrom="<nil>" topic=MyTransactionTopic
0
发送成功

2.2 运行效果

2 )消费者

go 复制代码
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/apache/rocketmq-client-go/v2"
	"github.com/apache/rocketmq-client-go/v2/consumer"
	"github.com/apache/rocketmq-client-go/v2/primitive"
)

func main() {
	mqAddr := "127.0.0.1:9876"
	topic := "MyTransactionTopic"
	groupName := "ddddddd"

	c, err := rocketmq.NewPushConsumer(
		consumer.WithGroupName(groupName),
		consumer.WithNsResolver(primitive.NewPassthroughResolver([]string{mqAddr})),
	)
	if err != nil {
		panic(err)
	}
	err = c.Subscribe(topic, consumer.MessageSelector{},
		func(ctx context.Context, msgList ...*primitive.MessageExt) (consumer.ConsumeResult, error) {
			for i := range msgList {
				fmt.Printf("订阅消息,消费%v \n", msgList[i])
			}
			return consumer.ConsumeSuccess, nil
		})
	if err != nil {
		fmt.Println("消费消息错误: %v", err.Error())
	}
	err = c.Start()
	if err != nil {
		fmt.Println("开启消费这错误: %v", err.Error())
	}
	time.Sleep(time.Hour)
	err = c.Shutdown()
	if err != nil {
		fmt.Println("shutdown消费者错误: %v", err.Error())
	}
}
  • 在RocketMQ中,事务消息的处理机制涉及到生产者和消费者两端的协作,但与普通消息消费模式有所区别

  • 事务消息的消费端并不直接参与到事务的两阶段提交过程中,它更像是一个"半事务消息"的确认者

  • 具体流程如下:

    • 生产者发送事务消息:生产者发送一条半事务消息到MQ服务器,并立即返回,此时消息处于"Prepare"状态
    • MQ Server回调生产者确认:MQ服务器会回调生产者提供的事务监听器(在Go示例中是HappyListener),执行本地事务。生产者需在此阶段执行事务操作并决定是提交还是回滚该消息
    • 生产者根据本地事务结果告知MQ Server:生产者根据本地事务执行结果,通过事务状态检查接口告诉MQ服务器是提交还是回滚这条半事务消息。
    • 消息变为可消费状态:MQ服务器根据生产者的决定,将消息标记为Commit或Rollback,Commit后的消息才对普通消费者可见
  • 因此,对于事务消息的消费者来说,其主要职责是消费那些已经被事务提交成功的消息,而不需要直接参与事务的提交或回滚过程

  • 消费者代码看起来与普通消息的消费者相似,但消费的消息实际上是生产者已经提交成功的事务消息

  • 不过,如果您的需求是希望消费者也以某种形式参与到事务的最终确认中,比如基于消息的消费结果来决定是否提交事务,这在RocketMQ的标准事务消息模型中并不直接支持

  • RocketMQ的事务模型主要关注于保证消息生产和本地事务的原子性,消费者更多的是作为事务结果的后续处理者角色

  • 在提供的消费者示例中,尽管它看起来是一个普通的消费者,但实际上它处理的是生产者通过事务消息流程提交后的内容,这符合事务消息的消费逻辑

  • 如果需要在消费端实现更复杂的逻辑来间接响应事务状态,可能需要结合业务系统进行额外的设计,比如通过监听数据库状态变化、消息队列的死信队列特性或其他补偿机制来处理未决事务

延迟性事务消息

  • 您需要在创建消息时指定消息的延迟等级,而不是在生产者配置或消息发送后进行延迟

  • RocketMQ支持多种延迟等级,每种等级对应不同的延迟时间

  • 注意,事务消息和延迟消息的直接组合在RocketMQ中并不是直接支持的特性

  • 因为事务消息的设计主要是围绕两阶段提交模型,确保消息发送与本地事务的一致性

  • 而延迟消息侧重于消息的定时投递

  • 然而,可以通过间接的方式结合这两个特性,即在事务消息的本地事务逻辑中包含对延迟操作的处理

  • 下面的示例尝试模拟一种结合方式,但请注意,这仅是一种逻辑上的结合,实际应用中需要根据具体业务场景仔细设计和测试

  • 方案思路:

    • 生产者:发送一个事务消息到特定的主题(例如DelayedTransactionTopic),该消息体中携带了需要进行延迟处理的信息。
    • 事务监听器:在ExecuteLocalTransaction方法中,不直接执行长时间的延迟逻辑,而是执行快速操作(如记录消息待处理状态或存入DB),然后返回primitive.Prepared状态。
    • 检查事务状态:在CheckLocalTransaction方法中,检查事务状态,如果需要,触发一个异步任务或消息队列中的消息,该消息携带延迟处理逻辑和真正的延迟时间。
    • 延迟处理服务:这个服务从队列中取出消息并根据消息中的指示进行真正的延迟操作,例如通过内部队列或定时任务系统(如分布式定时任务框架)来实现延迟执行
  • 相关生产者伪代码未完全实现,仅供参考

    go 复制代码
    package main
    
    import (
    	"context"
    	"fmt"
    	"time"
    
    	"github.com/apache/rocketmq-client-go/v2"
    	"github.com/apache/rocketmq-client-go/v2/primitive"
    	"github.com/apache/rocketmq-client-go/v2/producer"
    )
    
    type DelayedTxListener struct{}
    
    func (d DelayedTxListener) ExecuteLocalTransaction(msg *primitive.Message) primitive.LocalTransactionState {
    	// 假设这里将消息标识为待处理,并记录相关信息到数据库
    	fmt.Println("Preparing transaction, storing message meta...")
    	return primitive.Prepared
    }
    
    func (d DelayedTxListener) CheckLocalTransaction(msgExt *primitive.MessageExt) primitive.LocalTransactionState {
    	// 在这里检查消息是否准备好执行延迟操作
    	// 实际操作可能包括从数据库查询该消息的状态
    	// 假设我们已经确定需要进行延迟操作,这里直接模拟提交
    	return primitive.CommitMessageState
    }
    
    func main() {
    	mqAddr := "127.0.0.1:9876"
    	p, err := rocketmq.NewTransactionProducer(
    		DelayedTxListener{},
    		producer.WithNameServer([]string{mqAddr}),
    	)
    	if err != nil {
    		panic(err)
    	}
    	err = p.Start()
    	if err != nil {
    		panic(err)
    	}
    
    	msg := &primitive.Message{
    		Topic: "DelayedTransactionTopic",
    		Body:  []byte("消息内容,可以包含延迟处理的详细信息"),
    	}
    
    	res, err := p.SendMessageInTransaction(context.Background(), msg)
    	if err != nil {
    		panic(err)
    	}
    	fmt.Printf("发送事务消息状态: %v\n", res.Status)
    
    	time.Sleep(time.Second * 3600)
    	err = p.Shutdown()
    	if err != nil {
    		panic(err)
    	}
    }
  • 注意事项

    • 这个示例主要是概念性的,展示了如何在事务消息的上下文中计划后续的延迟处理步骤,而没有直接实现延迟消息的发送。
    • 实际应用中,您可能需要实现一个额外的后台服务或消息队列来处理这些"计划"好的延迟任务,确保它们能在预定时间得到执行。
    • 事务消息的两阶段提交机制仍然适用,只是延迟操作本身不在RocketMQ的直接事务控制范围内,而是作为一种业务逻辑上的后续处理。
    • 务必根据您的具体业务需求和RocketMQ的版本特性,仔细设计和测试这样的解决方案。
相关推荐
_oP_i2 小时前
Pinpoint 是一个开源的分布式追踪系统
java·分布式·开源
慕城南风3 小时前
Go语言中的defer,panic,recover 与错误处理
golang·go
攻心的子乐4 小时前
Kafka可视化工具 Offset Explorer (以前叫Kafka Tool)
分布式·kafka
小林想被监督学习5 小时前
RabbitMQ 的7种工作模式
分布式·rabbitmq
初晴~6 小时前
【Redis分布式锁】高并发场景下秒杀业务的实现思路(集群模式)
java·数据库·redis·分布式·后端·spring·
有一个好名字6 小时前
zookeeper分布式锁模拟12306买票
分布式·zookeeper·云原生
yukai0800810 小时前
【最后203篇系列】002 - 两个小坑(容器时间错误和kafka模块报错
分布式·kafka
LeonNo1111 小时前
golang , chan学习
开发语言·学习·golang
老猿讲编程11 小时前
OMG DDS 规范漫谈:分布式数据交互的演进之路
分布式·dds
C++忠实粉丝11 小时前
服务端高并发分布式结构演进之路
分布式