RocketMQ事务消息
可以认为是一个两阶段提交的消息,来确保分布式系统的最终一致性。事务性消息保证了本地事务的执行和消息的发送能够以原子的方式进行。
事务状态
RocketMQ事务消息有三种状态
- TransactionStatus.CommitTransaction:提交事务,它意味着允许消费者使用此消息。在go client中对应 primitive.CommitMessageState (本文所指go client是github.com/apache/rocketmq-client-go/v2)
- TransactionStatus.RollbackTransaction:回滚事务,这意味着将删除消息,并且不允许使用该消息。在go client对应primitive.RollbackMessageState。
- TransactionStatus.Unknown:未知:中间状态,这意味着需要MQ进行回检以确定状态。在go client对应primitive.UnknowState。
RocketMQ事务消息相关的配置
- transactionTimeout 在指定时间内没有收到最终确认就引发回查。
- transactionCheckMax 指定最多回查指定次数,超过后将消息丢弃并记录错误日志,默认15次。
- transactionCheckInterval 指定设置的多次消息回查的时间间隔,默认60秒。
RocketMQ事务消息的创建
首先创建一个事务消息的producter,然后将事务监听器TransactionListener注册到这个producter上。
事务监听器必须实现两个接口:
- executeLocalTransaction 用于在发送half message成功时执行本地事务,这个方法的返回值就是上面说的事务状态,根据事务状态决定MQ的后续操作。
- checkLocalTransaction 用于回查状态,返回结果也是上面说的事务状态。
基于rocketmq-client-go实现RocketMQ事务消息
生产者代码,这里发生4个消息,有2个会进入回查。最终成功两条。
//事务监听器
type Trans struct {
localTrans *sync.Map
transactionIndex int32
}
func NewTrans() *Trans {
return &Trans{
localTrans: new(sync.Map),
}
}
func (tr *Trans) ExecuteLocalTransaction(msg *primitive.Message) primitive.LocalTransactionState {
switch msg.GetTags() {
case "taga":
return primitive.CommitMessageState
case "tagb":
return primitive.RollbackMessageState
case "tagc","tagd":
return primitive.UnknowState
}
return primitive.UnknowState
}
func (tr *Trans) CheckLocalTransaction(msg *primitive.MessageExt) primitive.LocalTransactionState {
switch msg.GetTags() {
case "tagc":
log.Printf("check commit : %v\n", string(msg.Body) )
return primitive.CommitMessageState
case "tagd":
log.Printf("check rollback: %v\n", string(msg.Body))
return primitive.RollbackMessageState
}
return primitive.RollbackMessageState
}
func main() {
p, _ := rocketmq.NewTransactionProducer(
NewTrans(),
producer.WithNsResolver(primitive.NewPassthroughResolver([]string{"127.0.0.1:9876"})),
producer.WithRetry(1),
)
err := p.Start()
defer p.Shutdown()
if err != nil {
log.Printf("start producer error: %s\n", err.Error())
os.Exit(1)
}
tags := [4]string{"taga","tagb","tagc","tagd"}
topic := "trans"
for i := 0; i < 4; i++ {
msg := primitive.NewMessage(topic, []byte("Hello RocketMQ"+strconv.Itoa(i)))
msg.WithTag(tags[i])
res, err := p.SendMessageInTransaction(context.Background(), msg)
if err != nil {
log.Printf(err.Error())
return
} else {
log.Printf("预投递成功 resid : %s \n",res.MsgID)
}
}
time.Sleep(20 * time.Minute)
}
/**
*
2022/01/25 12:57:00 预投递成功 resid : C0A80165CBC0000000007e5ffd600001
2022/01/25 12:57:00 预投递成功 resid : C0A80165CBC0000000007e5ffd600002
2022/01/25 12:57:00 预投递成功 resid : C0A80165CBC0000000007e5ffd600003
2022/01/25 12:57:00 预投递成功 resid : C0A80165CBC0000000007e5ffd600004
2022/01/25 12:57:07 check rollback: Hello RocketMQ3
2022/01/25 12:57:07 check commit : Hello RocketMQ2
*/
消费者代码
package main
import (
"context"
"github.com/apache/rocketmq-client-go/v2"
"github.com/apache/rocketmq-client-go/v2/consumer"
"github.com/apache/rocketmq-client-go/v2/primitive"
"os"
"testing"
"time"
"log"
)
func main() {
c, _ := rocketmq.NewPushConsumer(
consumer.WithGroupName("testGroup"),
consumer.WithNsResolver(primitive.NewPassthroughResolver([]string{"127.0.0.1:9876"})),
)
topic := "trans"
err := c.Subscribe(topic, consumer.MessageSelector{}, func(ctx context.Context,
msgs ...*primitive.MessageExt) (consumer.ConsumeResult, error) {
for i := range msgs {
log.Printf("订阅到消息: body=%v, tag =%v \n", string(msgs[i].Body),msgs[i].GetTags())
}
return consumer.ConsumeSuccess, nil
})
if err != nil {
log.Printf(err.Error())
}
err = c.Start()
defer c.Shutdown()
if err != nil {
log.Printf(err.Error())
os.Exit(-1)
}
time.Sleep(20*time.Minute)
}
/**
2022/01/25 12:57:00 订阅到消息: body=Hello RocketMQ0, tag =taga
2022/01/25 12:57:07 订阅到消息: body=Hello RocketMQ2, tag =tagc
**/