Em sistemas distribuídos, uma operação local de banco de dados e a entrega de uma mensagem downstream devem ter sucesso ou falhar simultaneamente. Mensagens padrão não garantem essa atomicidade: uma mensagem pode ser entregue mesmo se a transação local for revertida. O ApsaraMQ for RocketMQ oferece um recurso de processamento de transações distribuídas semelhante à eXtended Architecture (X/Open XA) para resolver esse problema. Ele utiliza mensagens transacionais para garantir consistência eventual: o broker mantém cada mensagem em estado de semi-confirmação e a entrega somente após o produtor confirmá-la explicitamente.
Este tópico fornece exemplos de código em Go para enviar e receber mensagens transacionais com o SDK cliente HTTP.
Como funciona
Uma mensagem transacional passa pelo seguinte ciclo de vida:

O produtor envia uma mensagem parcial (half message) ao broker.
O broker persiste a mensagem parcial e confirma o recebimento.
O produtor executa a transação local.
Com base no resultado da transação local, o produtor confirma ou reverte a mensagem parcial.
Se o broker não receber confirmação nem reversão dentro do intervalo
TransCheckImmunityTime, ele inicia uma verificação de retorno. Essa verificação se repete a cada 10 segundos por até 24 horas.Após a confirmação, o broker entrega a mensagem aos consumidores.
Para mais detalhes sobre o modelo de mensagens transacionais, consulte Mensagens transacionais.
Pré-requisitos
SDK cliente HTTP para Go instalado. Para obter detalhes, consulte Preparar o ambiente.
Instância, tópico e grupo de consumidores criados no console do ApsaraMQ for RocketMQ. Para obter detalhes, consulte Criar recursos.
-
Variáveis de ambiente configuradas com suas credenciais da Alibaba Cloud. Para obter detalhes sobre como obter um par de AccessKey, consulte Criar um par de AccessKey.
ALIBABA_CLOUD_ACCESS_KEY_ID-- Seu AccessKey IDALIBABA_CLOUD_ACCESS_KEY_SECRET-- Seu AccessKey secret
Observações de uso
|
Restrição |
Detalhes |
|
Exclusividade de tipo de tópico |
Cada tópico suporta apenas um tipo de mensagem. Tópicos criados para mensagens normais não enviam nem recebem mensagens transacionais. |
|
|
Controla o atraso antes da primeira verificação de retorno. Valores válidos: 10 a 300 segundos. Após a primeira verificação, o broker verifica novamente a cada 10 segundos por até 24 horas. |
|
Transações em andamento |
Se uma transação local ainda estiver em execução durante uma verificação de retorno, retorne um status desconhecido em vez de confirmar ou reverter prematuramente. Defina |
Enviar mensagens transacionais
O envio de uma mensagem transacional envolve três etapas:
Publicar uma mensagem parcial -- Envie a mensagem ao broker em estado de semi-confirmação.
Confirmar ou reverter -- Finalize a transação com base na lógica de negócios local.
Tratar verificações de retorno -- Responda às consultas de status iniciadas pelo broker para mensagens parciais não confirmadas.
O exemplo a seguir envia quatro mensagens transacionais. A primeira é confirmada imediatamente após a publicação. As três restantes são resolvidas durante as verificações de retorno com base na propriedade da mensagem a:
a=1: Confirmar na primeira verificação de retorno.a=2: Confirmar após a segunda verificação de retorno (ConsumedTimes > 1).a=3: Reverter.
Substitua os seguintes espaços reservados pelos valores reais:
|
Espaço reservado |
Descrição |
|
|
Endpoint HTTP obtido na página Instance Details no console do ApsaraMQ for RocketMQ |
|
|
Nome do tópico |
|
|
ID da instância. Defina como uma string vazia se a instância não tiver namespace. |
|
|
ID do grupo de consumidores |
package main
import (
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/aliyunmq/mq-http-go-sdk"
"github.com/gogap/errors"
)
var loopCount = 0
// ProcessError logs acknowledgment errors returned by commit or rollback operations.
func ProcessError(err error) {
// If a commit or rollback is attempted after the receipt handle expires
// (either TransCheckImmunityTime or the consumeHalfMessage timeout),
// the operation fails. This example uses a 10-second timeout for consumeHalfMessage.
if err == nil {
return
}
fmt.Println(err)
for _, errAckItem := range err.(errors.ErrCode).Context()["Detail"].([]mq_http_sdk.ErrAckItem) {
fmt.Printf("\tErrorHandle:%s, ErrorCode:%s, ErrorMsg:%s\n",
errAckItem.ErrorHandle, errAckItem.ErrorCode, errAckItem.ErrorMsg)
}
}
// ConsumeHalfMsg processes uncommitted half messages through back-checks.
// Based on the message property "a", it commits, rolls back, or defers the decision.
func ConsumeHalfMsg(mqTransProducer *mq_http_sdk.MQTransProducer) {
for {
if loopCount >= 10 {
return
}
loopCount++
endChan := make(chan int)
respChan := make(chan mq_http_sdk.ConsumeMessageResponse)
errChan := make(chan error)
go func() {
select {
case resp := <-respChan:
{
var handles []string
fmt.Printf("Consume %d messages---->\n", len(resp.Messages))
for _, v := range resp.Messages {
handles = append(handles, v.ReceiptHandle)
fmt.Printf("\tMessageID: %s, PublishTime: %d, MessageTag: %s\n"+
"\tConsumedTimes: %d, FirstConsumeTime: %d, NextConsumeTime: %d\n\tBody: %s\n"+
"\tProperties:%s, Key:%s, Timer:%d, Trans:%d\n",
v.MessageId, v.PublishTime, v.MessageTag, v.ConsumedTimes,
v.FirstConsumeTime, v.NextConsumeTime, v.MessageBody,
v.Properties, v.MessageKey, v.StartDeliverTime, v.TransCheckImmunityTime)
a, _ := strconv.Atoi(v.Properties["a"])
var comRollErr error
if a == 1 {
// Property a=1: commit the transaction.
comRollErr = (*mqTransProducer).Commit(v.ReceiptHandle)
fmt.Println("Commit---------->")
} else if a == 2 && v.ConsumedTimes > 1 {
// Property a=2: commit only after a second back-check.
comRollErr = (*mqTransProducer).Commit(v.ReceiptHandle)
fmt.Println("Commit---------->")
} else if a == 3 {
// Property a=3: roll back the transaction.
comRollErr = (*mqTransProducer).Rollback(v.ReceiptHandle)
fmt.Println("Rollback---------->")
} else {
// Unknown state: defer to the next back-check.
fmt.Println("Unknown---------->")
}
ProcessError(comRollErr)
}
endChan <- 1
}
case err := <-errChan:
{
if strings.Contains(err.(errors.ErrCode).Error(), "MessageNotExist") {
fmt.Println("\nNo new message, continue!")
} else {
fmt.Println(err)
time.Sleep(time.Duration(3) * time.Second)
}
endChan <- 1
}
case <-time.After(35 * time.Second):
{
fmt.Println("Timeout of consumer message ??")
return
}
}
}()
// Poll for uncommitted half messages using long polling.
// If no half message is available, the request is held on the broker
// for up to the specified polling duration before returning.
(*mqTransProducer).ConsumeHalfMessage(respChan, errChan,
3, // Max messages per batch (valid range: 1-16)
3, // Long polling duration in seconds (valid range: 1-30)
)
<-endChan
}
}
func main() {
// Step 1: Configure the client.
// Get the HTTP endpoint from the Instance Details page in the ApsaraMQ for RocketMQ console.
endpoint := "${HTTP_ENDPOINT}"
// Read credentials from environment variables.
accessKey := os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_ID")
secretKey := os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET")
// Specify the topic. The topic must already exist in the console.
topic := "${TOPIC}"
// Specify the instance ID. Set to "" if the instance has no namespace.
instanceId := "${INSTANCE_ID}"
// Specify the consumer group ID.
groupId := "${GROUP_ID}"
client := mq_http_sdk.NewAliyunMQClient(endpoint, accessKey, secretKey, "")
mqTransProducer := client.GetTransProducer(instanceId, topic, groupId)
// Step 2: Start a goroutine to handle back-checks for uncommitted half messages.
go ConsumeHalfMsg(&mqTransProducer)
// Step 3: Publish four transactional messages.
// The first message (i=0) is committed immediately.
// The remaining three are resolved during back-checks.
for i := 0; i < 4; i++ {
msg := mq_http_sdk.PublishMessageRequest{
MessageBody: "I am transaction msg!",
Properties: map[string]string{"a": strconv.Itoa(i)},
}
// Set the back-check delay: seconds before the first status check.
// Valid values: 10-300. After the first check, the broker re-checks every 10s.
msg.TransCheckImmunityTime = 10
resp, pubErr := mqTransProducer.PublishMessage(msg)
if pubErr != nil {
fmt.Println(pubErr)
return
}
fmt.Printf("Publish ---->\n\tMessageId:%s, BodyMD5:%s, Handle:%s\n",
resp.MessageId, resp.MessageBodyMD5, resp.ReceiptHandle)
if i == 0 {
// Commit the first message immediately using its receipt handle.
ackErr := mqTransProducer.Commit(resp.ReceiptHandle)
fmt.Println("Commit---------->")
ProcessError(ackErr)
}
}
// Wait for the back-check goroutine to complete.
for loopCount < 10 {
time.Sleep(time.Duration(1) * time.Second)
}
}
Inscrever-se em mensagens transacionais
Após a confirmação de uma mensagem transacional, o broker a entrega aos consumidores como qualquer outra mensagem. Use a API de consumidor padrão para se inscrever.
O exemplo a seguir consome mensagens no modo de long polling e reconhece cada lote. Se o broker não receber um reconhecimento antes que NextConsumeTime expire, ele reentrega a mensagem.
package main
import (
"fmt"
"os"
"strings"
"time"
"github.com/aliyunmq/mq-http-go-sdk"
"github.com/gogap/errors"
)
func main() {
// Configure the client with the same parameters used for the producer.
endpoint := "${HTTP_ENDPOINT}"
accessKey := os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_ID")
secretKey := os.Getenv("ALIBABA_CLOUD_ACCESS_KEY_SECRET")
topic := "${TOPIC}"
instanceId := "${INSTANCE_ID}"
groupId := "${GROUP_ID}"
client := mq_http_sdk.NewAliyunMQClient(endpoint, accessKey, secretKey, "")
// Pass an empty string as the message tag to subscribe to all tags.
mqConsumer := client.GetConsumer(instanceId, topic, groupId, "")
for {
endChan := make(chan int)
respChan := make(chan mq_http_sdk.ConsumeMessageResponse)
errChan := make(chan error)
go func() {
select {
case resp := <-respChan:
{
var handles []string
fmt.Printf("Consume %d messages---->\n", len(resp.Messages))
for _, v := range resp.Messages {
handles = append(handles, v.ReceiptHandle)
fmt.Printf("\tMessageID: %s, PublishTime: %d, MessageTag: %s\n"+
"\tConsumedTimes: %d, FirstConsumeTime: %d, NextConsumeTime: %d\n"+
"\tBody: %s\n"+
"\tProps: %s\n",
v.MessageId, v.PublishTime, v.MessageTag, v.ConsumedTimes,
v.FirstConsumeTime, v.NextConsumeTime, v.MessageBody, v.Properties)
}
// Acknowledge messages after processing.
// Unacknowledged messages are redelivered after NextConsumeTime.
ackerr := mqConsumer.AckMessage(handles)
if ackerr != nil {
fmt.Println(ackerr)
if errAckItems, ok := ackerr.(errors.ErrCode).Context()["Detail"].([]mq_http_sdk.ErrAckItem); ok {
for _, errAckItem := range errAckItems {
fmt.Printf("\tErrorHandle:%s, ErrorCode:%s, ErrorMsg:%s\n",
errAckItem.ErrorHandle, errAckItem.ErrorCode, errAckItem.ErrorMsg)
}
} else {
fmt.Println("ack err =", ackerr)
}
time.Sleep(time.Duration(3) * time.Second)
} else {
fmt.Printf("Ack ---->\n\t%s\n", handles)
}
endChan <- 1
}
case err := <-errChan:
{
if strings.Contains(err.(errors.ErrCode).Error(), "MessageNotExist") {
fmt.Println("\nNo new message, continue!")
} else {
fmt.Println(err)
time.Sleep(time.Duration(3) * time.Second)
}
endChan <- 1
}
case <-time.After(35 * time.Second):
{
fmt.Println("Timeout of consumer message ??")
endChan <- 1
}
}
}()
// Consume messages in long polling mode.
// The default network timeout is 35 seconds.
mqConsumer.ConsumeMessage(respChan, errChan,
3, // Max messages per batch (valid range: 1-16)
3, // Long polling duration in seconds (valid range: 1-30)
)
<-endChan
}
}
Tópicos relacionados
Mensagens transacionais: Modelo de mensagens transacionais, ciclo de vida e fundamentos de design.
Enviar e receber mensagens normais: Publicação e consumo básicos de mensagens.
Enviar e receber mensagens agendadas: Entrega atrasada de mensagens para um horário especificado.