Todos os produtos
Search
Central de documentação

ApsaraMQ for RocketMQ:Send and receive transactional messages

Última atualização: Jun 27, 2026

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:

图片1.png

  1. O produtor envia uma mensagem parcial (half message) ao broker.

  2. O broker persiste a mensagem parcial e confirma o recebimento.

  3. O produtor executa a transação local.

  4. Com base no resultado da transação local, o produtor confirma ou reverte a mensagem parcial.

  5. 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.

  6. 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 ID

    • ALIBABA_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.

TransCheckImmunityTime

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 TransCheckImmunityTime com um valor que cubra a duração esperada da transação local para reduzir verificações desnecessárias.

Enviar mensagens transacionais

O envio de uma mensagem transacional envolve três etapas:

  1. Publicar uma mensagem parcial -- Envie a mensagem ao broker em estado de semi-confirmação.

  2. Confirmar ou reverter -- Finalize a transação com base na lógica de negócios local.

  3. 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

${HTTP_ENDPOINT}

Endpoint HTTP obtido na página Instance Details no console do ApsaraMQ for RocketMQ

${TOPIC}

Nome do tópico

${INSTANCE_ID}

ID da instância. Defina como uma string vazia se a instância não tiver namespace.

${GROUP_ID}

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