SpringCloud integra RocketMQ para alcançar consistência final de mensagens confiáveis [tabela de mensagem local, esquema de mensagem de transação RocketMQ]

índice

1. Uma visão geral da eventual consistência de mensagens confiáveis

Em segundo lugar, a solução [solução de tabela de mensagem local]

Terceiro, a solução [programa de mensagem de transação RocketMQ]

Quatro, RocketMQ realiza transação consistente final de mensagem confiável

Cinco, resumo


1. Uma visão geral da eventual consistência de mensagens confiáveis


Esquema de consistência eventual de mensagem confiável significa que quando o iniciador da transação completa a transação local e envia uma mensagem, o participante da transação (consumidor da mensagem) deve ser capaz de receber a mensagem e processar a transação com sucesso. Este esquema enfatiza que, desde que a mensagem seja enviada ao participante da transação Os assuntos finais das partes devem ser consistentes . Este programa é concluído usando o middleware de mensagem, conforme mostrado na figura a seguir:

O iniciador da transação (produtor da mensagem) envia a mensagem para o middleware da mensagem, o participante da transação recebe a mensagem do middleware da mensagem, entre o iniciador da transação e o middleware da mensagem e entre o participante da transação (consumidor da mensagem) e o middleware da mensagem Toda a comunicação é feita pela rede e a incerteza da comunicação da rede causará problemas de transação distribuída. Portanto, a eventual consistência de mensagens confiáveis ​​deve resolver os seguintes problemas:
[1] A atomicidade das transações locais e do envio da mensagem : o iniciador da transação deve enviar a mensagem após a transação local ser executada com sucesso, caso contrário, a mensagem é descartada. Ou seja, para atingir a atomicidade das transações locais e do envio de mensagens , todas são bem-sucedidas ou ambas falham. A atomicidade das transações locais e do envio de mensagens é a questão chave para perceber a consistência eventual de mensagens confiáveis . Vamos tentar este tipo de operação primeiro, enviar primeiro a mensagem e depois operar o banco de dados : neste caso, a consistência da operação do banco de dados e da mensagem enviada não pode ser garantida, porque a mensagem pode ser enviada com sucesso e a operação do banco de dados falhou.

begin transaction;

//1.发送MQ

//2.数据库操作

commit transation;

A segunda solução é executar as operações do banco de dados primeiro e, em seguida, enviar as mensagens : neste caso, parece não haver problema.Se a mensagem MQ falhar ao ser enviada, uma exceção será lançada, fazendo com que a transação do banco de dados retroceda. mas:

  • Se for uma exceção de tempo limite , o banco de dados será retrocedido, mas o MQ foi enviado normalmente, o que também causará inconsistências.
  • Se a mensagem for enviada com sucesso, a JVM trava repentinamente quando a transação é finalmente confirmada e a transação não é confirmada com sucesso, resultando em inconsistência de dados entre os dois sistemas.
begin transaction;

//1.数据库操作

//2.发送MQ

commit transation;

[2] Confiabilidade da mensagem recebida pelos participantes da transação : Os participantes da transação devem ser capazes de receber mensagens da fila de mensagens, caso a recepção falhe, eles podem receber mensagens repetidamente .
[3] O problema do consumo repetido de mensagens: devido à existência da etapa 2, se um nó consumidor atingir o tempo limite, mas o consumo for bem-sucedido, o middleware da mensagem entregará repetidamente a mensagem neste momento, resultando no consumo repetido da mensagem. Para resolver o problema de consumo repetido de mensagens, é necessário perceber a idempotência do método dos participantes da transação .

Em segundo lugar, a solução [solução de tabela de mensagem local  ]


A solução de tabela de mensagem local foi proposta originalmente pelo eBay. O núcleo desta solução é garantir a consistência das operações de serviço de dados e mensagens por meio de transações locais e, em seguida, enviar a mensagem para o middleware de mensagem por meio de uma tarefa cronometrada e esperar até que a mensagem seja enviada com sucesso ao consumidor. Exclua a mensagem. A seguir está um exemplo de registro para enviar pontos para ilustrar: o exemplo a seguir tem duas interações de microsserviço, serviço de usuário e serviço de ponto, serviço de usuário é responsável por adicionar usuários e serviço de ponto é responsável por adicionar pontos.

[ O processo de interação é o seguinte ] : [1] Registro do usuário: O serviço do usuário adiciona novos usuários nos assuntos locais e adiciona "registro de mensagem de ponto". (A tabela de usuário e a tabela de mensagem têm a garantia de ser consistente por meio de transações locais.) Abaixo está o pseudocódigo. Nesse caso, a operação do banco de dados local e o log de mensagem integral armazenado estão na mesma transação, e a operação do banco de dados local e a operação do log de mensagem de gravação são atômicos.

begin transaction;

//1.新增用户

//2.存储积分消息日志

commit transation;

[ 2 ] Registro de verificação de tarefa agendada: Como garantir que as mensagens sejam enviadas para a fila de mensagens? Após a primeira etapa, a mensagem foi gravada na tabela de log de mensagens e um thread independente pode ser iniciado para varrer as mensagens na tabela de log de mensagens regularmente e enviá-las para o middleware de mensagens. O log de mensagens é excluído após o middleware de mensagens relatar que o envio foi bem-sucedido, caso contrário Aguarde até que a tarefa agendada tente novamente no próximo ciclo.
[3] Notícias do consumidor: como garantir que os consumidores possam consumir notícias? Aqui você pode usar o mecanismo ack ( confirmação de mensagem ) do MQ. Os consumidores monitoram o MQ. Se o consumidor receber a mensagem e o processamento de negócios for concluído, ele enviará um ack (confirmação da mensagem) para o MQ, o que significa que o consumidor concluiu o consumo normal da mensagem e MQ Não enviará mais mensagens aos consumidores, caso contrário, os consumidores continuarão a tentar enviar mensagens aos consumidores. O serviço de ponto recebe a mensagem "adicionar pontos" e começa a aumentar os pontos. Após os pontos aumentarem com sucesso, o middleware da mensagem responderá com um ack, caso contrário, o middleware da mensagem entregará a mensagem repetidamente. Visto que as mensagens serão entregues repetidamente, a função "adicionar pontos" do serviço de ponto precisa ser idempotente .

Terceiro, a solução [programa de mensagem de transação RocketMQ  ]


RocketMQ é um middleware de mensagens distribuído do Alibaba. Ele foi aberto em 2012 e tornou-se oficialmente o principal projeto Apache em 2017. Entende-se que toda a linha de produtos de mensagens do Alibaba Group, incluindo produtos de mensagens da Alibaba Cloud e subsidiárias adquiridas, funciona no RocketMQ, e o RocketMQ alcançou um desempenho atraente durante a promoção Double Eleven nos últimos anos. O Apache RocketMQ versão 4.3 ou posterior oferece suporte oficial para mensagens de transação , fornecendo suporte conveniente para implementação de transação distribuída. Mensagens de transação RocketMQ projetadas principalmente para resolver mensagem de problema de átomos com a execução transacional local Fim do produtor , RocketMQ projeta o  corretor de capacidade de comunicação bidirecional com o lado do produtor , para que o  corretor nascido como um coordenador de transação exista ; o mecanismo armazenado que o próprio RocketMQ fornece Fornece recursos de persistência para mensagens de transação ; o mecanismo de alta disponibilidade do RocketMQ e o design de mensagem confiável garantem que as mensagens transacionais ainda possam garantir a consistência final da transação quando o sistema estiver anormal. Após o RocketMQ 4.3, uma mensagem de transação completa é implementada, que na verdade é um encapsulamento da tabela de mensagem local.A tabela de mensagem local é movida dentro do MQ para resolver o problema atômico de envio de mensagem e execução de transação local no lado do produtor.

[O processo de execução é o seguinte ] : Para facilitar o entendimento, também usamos o exemplo de cadastro para enviar pontos para descrever todo o processo. O produtor é o emissor do MQ, neste caso é o serviço ao usuário, responsável por adicionar novos usuários. O assinante MQ é o consumidor da mensagem.Neste exemplo, é o serviço de ponto e é responsável por adicionar pontos.
[1] Produtor envia mensagem de transação: Produtor (remetente MQ) envia mensagem de transação para MQ Server, MQ Server marca o status da mensagem como Preparado (estado preparado), observe que este consumidor de mensagem (assinante MQ) não pode consumir neste momento Chegou. Neste exemplo, o produtor envia a "mensagem de adição de pontos" ao MQ Server.
[2] O MQ Server responde à mensagem enviada com sucesso: o MQ Server recebe a mensagem enviada pelo produtor e responde com uma resposta bem-sucedida. Indica que o  MQ recebeu a mensagem .
[3] O produtor executa transações locais: o produtor executa a lógica do código de negócios e é controlado pelas transações do banco de dados local. Neste exemplo, o produtor executa a operação de adição de usuário.
[4] Entrega de mensagem: Se a transação local do produtor for executada com sucesso, ele enviará automaticamente uma mensagem de confirmação para MQServer. Após receber a  mensagem de confirmação , o servidor MQ marcará o status "Mensagem de aumento de pontos" como consumível, Neste momento, o assinante MQ (serviço pontual) consome mensagens normalmente. Se a execução da transação local do Produtor  falhar, ele enviará automaticamente uma mensagem de rollback para o MQ Server, e o MQ Server excluirá a "Mensagem de pontos de aumento" após receber a mensagem de rollback . Os assinantes do MQ (serviço de ponto) consomem mensagens e, se o consumo for bem-sucedido , eles responderão ao MQ com ack , caso contrário, receberão mensagens repetidamente. Aqui, o ack responde automaticamente por padrão, ou seja, responde automaticamente ao ack quando o programa é executado normalmente.
[5] Revisão da transação: Se o lado da execução travar ou atingir o tempo limite durante a execução da transação local no lado do produtor, o MQ Server consultará continuamente outros produtores no mesmo grupo para obter o status de execução da transação.Este processo é chamado de revisão da transação . O MQ Server decidirá se deve entregar a mensagem com base no resultado da revisão da transação. O processo principal acima foi implementado pela RocketMQ. No lado do usuário, o usuário precisa implementar a execução da transação local e métodos de verificação de volta da transação local , portanto, apenas o status de execução da transação local (manter a tabela de status da transação local) é suficiente . RoacketMQ fornece  interface RocketMQLocalTransactionListener:

public interface RocketMQLocalTransactionListener {

/**发送prepare消息成功此方法被回调,该方法用于执行本地事务

* @param msg 回传的消息,利用transactionId即可获取到该消息的唯一Id

* @param arg 调用send方法时传递的参数,当send时候若有额外的参数可以传递到send方法中,这里能获取到

* @return 返回事务状态,COMMIT:提交 ROLLBACK:回滚 UNKNOW:回调

*/

RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg);


/**@param msg 通过获取transactionId来判断这条消息的本地事务执行状态

* @return 返回事务状态,COMMIT:提交 ROLLBACK:回滚 UNKNOW:回调

*/

RocketMQLocalTransactionState checkLocalTransaction(Message msg);

}

[6] Enviar mensagens de transação: A seguir está a API fornecida pela RocketMQ para enviar mensagens de transação:

TransactionMQProducer producer = new TransactionMQProducer("ProducerGroup");

producer.setNamesrvAddr("127.0.0.1:9876");

producer.start();

//设置TransactionListener实现

producer.setTransactionListener(transactionListener);

//发送事务消息

SendResult sendResult = producer.sendMessageInTransaction(msg, null);

Quarto, RocketMQ realiza a transação consistente final de mensagens confiáveis


[ Descrição do negócio ] Por meio do middleware RocketMQ, ele realiza a transação distribuída consistente final de mensagens confiáveis ​​e simula o processo de transação de transferência de duas contas. As duas contas estão em bancos diferentes (Zhang San no banco1 e Li Si no banco2), banco1 e banco2 são dois microsserviços. O processo de transação é que Zhang San transfere uma determinada quantia para Li Si. As etapas da transação acima, Zhang San deduzindo o valor e enviando uma mensagem de transferência para o banco2, as duas operações devem ser uma transação integral.

[ Componentes do programa ]:

Banco de dados: MySQL-5.7.25

Incluindo dois bancos de dados bank1 e bank2.

JDK : 64 位 jdk1.8.0_201

servidor rocketmq: RocketMQ-4.5.0

cliente rocketmq: RocketMQ-Spring-Boot-starter.2.0.2-RELEASE

Estrutura de microsserviço: spring-boot-2.1.3, spring-cloud-Greenwich.RELEASE

A relação entre microsserviços e bancos de dados:

dtx / dtx-txmsg-demo / dtx-txmsg-demo-bank1 Banco 1, operar conta Zhang San, conectar ao banco de dados banco1

dtx / dtx-txmsg-demo / dtx-txmsg-demo-bank2 Banco 2, operar conta Li Si, conectar-se ao banco de dados banco2

[ Código principal ] : A estrutura técnica do programa é a seguinte:

[O processo de interação é o seguinte ] :

[1] Banco1 envia uma mensagem de transferência para o servidor MQ;
[2] Banco1 realiza transações locais e
deduz o valor; [3] Banco2 recebe mensagens, realiza transações locais e adiciona o valor;
[ Banco de dados ] : Adicionar de_duplicação nos bancos de dados banco1 e banco2 , Tabela de registro de transações (tabela de deduplicação), usada para controle idempotente de transações .

DROP TABLE IF EXISTS `de_duplication`;

CREATE TABLE `de_duplication` (

`tx_no` varchar(64) COLLATE utf8_bin NOT NULL,

`create_time` datetime(0) NULL DEFAULT NULL,

PRIMARY KEY (`tx_no`) USING BTREE

) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;

[ Dependência de versão ] : a versão de rocketmq-spring-boot-starter é especificada no projeto pai

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.0.2</version>
</dependency>

[ Configuração rocketMQ ] : Configure o endereço do rocketMQ nameServer e o grupo de produção em application-local.propertis.

rocketmq.producer.group = producer_bank2

rocketmq.name-server = 127.0.0.1:9876

[ Código da camada de serviço Zhang San ] :

import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;


/**
 * @author Administrator
 * @version 1.0
**/
@Service
@Slf4j
public class AccountInfoServiceImpl implements AccountInfoService {

    @Autowired
    AccountInfoDao accountInfoDao;

    @Autowired
    RocketMQTemplate rocketMQTemplate;

    //向mq发送转账消息
    @Override
    public void sendUpdateAccountBalance(AccountChangeEvent accountChangeEvent) {
        //将accountChangeEvent转成json
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("accountChange", accountChangeEvent);
        String jsonString = jsonObject.toJSONString();
        //生成message类型
        Message<String> message = MessageBuilder.withPayload(jsonString).build();
        //发送一条事务消息

        /**
         * String txProducerGroup 生产组
         * String destination topic,
         * Message<?> message, 消息内容
         * Object arg 参数
         */
        rocketMQTemplate.sendMessageInTransaction("producer_group_txmsg_bank1", "topic_txmsg", message, null);

    }


    //更新账户,扣减金额
    @Override
    @Transactional
    public void doUpdateAccountBalance(AccountChangeEvent accountChangeEvent) {
        //幂等判断,txNo是在Ctroller中生成的 UUID,全局唯一
        if (accountInfoDao.isExistTx(accountChangeEvent.getTxNo()) > 0) {
            return;
        }

        //扣减金额
        accountInfoDao.updateAccountBalance(accountChangeEvent.getAccountNo(), accountChangeEvent.getAmount() * -1);
        //添加事务日志
        accountInfoDao.addTx(accountChangeEvent.getTxNo());
        if (accountChangeEvent.getAmount() == 3) {
            throw new RuntimeException("人为制造异常");
        }

    }

}

[ Zhang San RocketMQLocalTransactionListener ] : Escreva a classe de implementação da interface RocketMQLocalTransactionListener para implementar dois métodos de execução de transações locais e revisão de transações.

import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;


/**
 * @author Administrator
 * @version 1.0
 **/
@Component
@Slf4j
//生产者组与发送消息时定义组相同
@RocketMQTransactionListener(txProducerGroup = "producer_group_txmsg_bank1")
public class ProducerTxmsgListener implements RocketMQLocalTransactionListener {

    @Autowired
    AccountInfoService accountInfoService;

    @Autowired
    AccountInfoDao accountInfoDao;

    //事务消息发送后的回调方法,当消息发送给mq成功,此方法被回调
    @Override
    @Transactional
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
        try {
            //解析message,转成AccountChangeEvent
            String messageString = new String((byte[]) message.getPayload());
            JSONObject jsonObject = JSONObject.parseObject(messageString);
            String accountChangeString = jsonObject.getString("accountChange");
            //将accountChange(json)转成AccountChangeEvent
            AccountChangeEvent accountChangeEvent = JSONObject.parseObject(accountChangeString, AccountChangeEvent.class);
            //执行本地事务,扣减金额
            accountInfoService.doUpdateAccountBalance(accountChangeEvent);
            //当返回RocketMQLocalTransactionState.COMMIT,自动向mq发送commit消息,mq将消息的状态改为可消费
            return RocketMQLocalTransactionState.COMMIT;

        } catch (Exception e) {
            e.printStackTrace();
            return RocketMQLocalTransactionState.ROLLBACK;
        }


    }


    //事务状态回查,查询是否扣减金额
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
        //解析message,转成AccountChangeEvent
        String messageString = new String((byte[]) message.getPayload());
        JSONObject jsonObject = JSONObject.parseObject(messageString);
        String accountChangeString = jsonObject.getString("accountChange");
        //将accountChange(json)转成AccountChangeEvent
        AccountChangeEvent accountChangeEvent = JSONObject.parseObject(accountChangeString, AccountChangeEvent.class);
        //事务id
        String txNo = accountChangeEvent.getTxNo();
        int existTx = accountInfoDao.isExistTx(txNo);
        if (existTx > 0) {
            return RocketMQLocalTransactionState.COMMIT;
        } else {
            return RocketMQLocalTransactionState.UNKNOWN;
        }
    }

}

[ Código da camada de serviço Li Si ] :

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author Administrator
 * @version 1.0
 **/
@Service
@Slf4j
public class AccountInfoServiceImpl implements AccountInfoService {

    @Autowired
    AccountInfoDao accountInfoDao;

    //更新账户,增加金额
    @Override
    @Transactional
    public void addAccountInfoBalance(AccountChangeEvent accountChangeEvent) {
        log.info("bank2更新本地账号,账号:{},金额:{}", accountChangeEvent.getAccountNo(), accountChangeEvent.getAmount());
        if (accountInfoDao.isExistTx(accountChangeEvent.getTxNo()) > 0) {
            return;
        }
        //增加金额
        accountInfoDao.updateAccountBalance(accountChangeEvent.getAccountNo(), accountChangeEvent.getAmount());
        //添加事务记录,用于幂等
        accountInfoDao.addTx(accountChangeEvent.getTxNo());
        if (accountChangeEvent.getAmount() == 4) {
            throw new RuntimeException("人为制造异常");
        }

    }

}

[Classe do monitor MQ]:  monitore o tópico de destino implementando a interface RocketMQListener

import com.alibaba.fastjson.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @author Administrator
 * @version 1.0
 **/
@Component
@Slf4j
@RocketMQMessageListener(consumerGroup = "consumer_group_txmsg_bank2", topic = "topic_txmsg")
public class TxmsgConsumer implements RocketMQListener<String> {
    
    @Autowired
    AccountInfoService accountInfoService;

    //接收消息
    @Override
    public void onMessage(String message) {
        log.info("开始消费消息:{}", message);
        //解析消息
        JSONObject jsonObject = JSONObject.parseObject(message);
        String accountChangeString = jsonObject.getString("accountChange");
        //转成AccountChangeEvent
        AccountChangeEvent accountChangeEvent = JSONObject.parseObject(accountChangeString, AccountChangeEvent.class);
        //设置账号为李四的
        accountChangeEvent.setAccountNo("2");
        //更新本地账户,增加金额
        accountInfoService.addAccountInfoBalance(accountChangeEvent);
        
    }

}

Cinco, resumo


A consistência final de mensagens confiáveis ​​é garantir a consistência das mensagens do produtor ao consumidor por meio do middleware da mensagem. Nesse caso, RocketMQ é usado como o middleware da mensagem. RocketMQ resolve principalmente duas funções:
[1] Transações locais e envio de mensagens Problemas atômicos ;
[2] A confiabilidade da mensagem recebida pelos participantes da transação ; a

eventual consistência de mensagens confiáveis ​​é adequada para cenários com longos ciclos de execução e baixos requisitos de tempo real . Após a introdução do mecanismo de mensagem, a operação de transação síncrona passa a ser a operação assíncrona baseada na execução da mensagem, o que evita a influência da operação de bloqueio síncrona na transação distribuída e realiza o desacoplamento dos dois serviços.

original

● A otimização de desempenho do Tomcat8 mais forte da história

Por que o Alibaba pode resistir a 10 bilhões em 90 segundos? - A evolução da arquitetura distribuída de alta simultaneidade do lado do servidor

Plataforma de comércio eletrônico B2B - função de pagamento eletrônico ChinaPay UnionPay

Aprenda o bloqueio distribuído do Zookeeper, deixe os entrevistadores olharem para você com admiração

Solução de bloqueio distribuído de microsserviço do SpringCloud e-commerce de pico de Redisson

Confira mais bons artigos, entre na conta oficial - por favor - excelente no passado

Uma conta pública profunda e comovente 0.0

Acho que você gosta

Origin blog.csdn.net/a1036645146/article/details/109362669
Recomendado
Clasificación