分布式事务-可靠消息最终一致性(RocketMq半消息)

分布式事务-可靠消息最终一致性

技术线:Java、SpringBoot、SpringCloud

可靠消息最终一致性事务

指事务发起方执行完成本地事务后并发出一条消息,事务参与者(消息消费者)一定能够接收消息并处理事务成功,此方案强调的是只要消息发给事务参与方最终事务要达到一致。
在这里插入图片描述

上图方案是利用消息中间件完成。事务发起方(Producer)将消息发给消息中间件,事务参与方从消息中间件接收消息,事务发起方和消息中间件之间,事务参与方(Consumer)和消息中间件之间都是通过网络通信,由于网络通信的不确定性会导致分布式事务问题。

因此可靠消息最终一致性方案要解决以下几个问题:

  1. 本地事务与消息发送的原子性问题

    本地事务与消息发送的原子性问题即:事务发起方在本地事务执行成功后消息必须发出去,否则就丢弃消息。即实现本地事务和消息发送的原子性,要么都成功,要么都失败。本地事务与消息发送的原子性问题是实现可靠消息最终一致性方案的关键问题。

  2. 事务参与方接收消息的可靠性

    事务参与方必须能够从消息队列接收到消息,如果接收消息失败可以重复接收消息

  3. 消息重复消费问题

    由于网络2的存在,若某一个消费节点超时但是消费成功,此时消息中间件会重复投递此消息,就导致了消息的重复消费。

    要解决消息重复消费的问题就要实现事务参与方的方法幂等性。

解决方案

RocketMQ事务消息方案

RocketMQ事务消息设计主要是为了解决Producer端的消息发送与本地事务执行的原子性问题。RcoketMQ的设计中broker与producer端的双向通信能力,使得broker天生可以作为一个事务协调者存在;而RocketMQ本身提供的存储机制为事务消息提供了持久化能力;RocketMQ的高可用机制以及可靠消息设计则为事务消息在系统发生异常时仍然能够保证达成事务的最终一致性。

在RocketMQ4.3后实现了完整的事务消息,实际上其实是对本地消息表的一个封装,将本地消息表移动到了MQ内部,解决Producer端的消息发送与本地事务执行的原子性问题。
在这里插入图片描述

执行流程:
  1. Producer 发送事务消息

    Producer(MQ发送方)发送事务消息至MQ Server,MQ Server将消息状态标记为Prepared(预备状态),注意此时这条消息消费者(MQ订阅方)是无法消费到的。

@Override
    public void sendUpdateAccountBalance(AccountChangeEvent accountChangeEvent)
    {
        //1 AccountChangeEvent对象换成json,fastjson有安全问题,暂时省略。。。fastjson2
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("accountChange",accountChangeEvent);
        String jsonString = jsonObject.toJSONString();
        //2 生产MQ需要的Message消息实体,和MQ数据传递对接。
        Message<String> message = MessageBuilder.withPayload(jsonString).build();
        
        //3 发送一条事务消息先通知MQ,我mysql需要干活update数据了,预先通知你一声,盼回复。
        //String txProducerGroup, String destination, Message<?> message, Object arg
        TransactionSendResult transactionSendResult = rocketMQTemplate.sendMessageInTransaction
                ("producer_group_txmsg_bank1", "topic_txmsg", message, null);
        log.error("rocketMQTemplate:{},result{}","rocketMQTemplate模板调用结束。。。。。", transactionSendResult);
    }
  1. MQ Server 回应消息发送成功

    MQ Server接收到Producer发送的消息则回应发送成功表示MQ已接收到消息。

@Component
@Slf4j
@RocketMQTransactionListener(txProducerGroup = "producer_group_txmsg_bank1")
public class ProducerTxmsgListener implements RocketMQLocalTransactionListener
{
    @Autowired
    AccountInfoService accountInfoService;
    @Autowired AccountInfoDao accountInfoDao;

    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o)
    {
        try {
            //1 json处理
            String messageString = new String((byte[])message.getPayload());
            log.error("收到事务消息【{}】", messageString);
            
            JSONObject jsonObject = JSONObject.parseObject(messageString);
            String accountChangeString = jsonObject.getString("accountChange");
            //2 将accountChangeString 转换成为AccountChangeEvent消息实体
            AccountChangeEvent accountChangeEvent = JSONObject.parseObject(accountChangeString, AccountChangeEvent.class);
            //3 执行本地事务,通知mysql侧扣减转账金额,看mysql本地事务执行情况,
            // 3.1 mysql ok,commit
            // 3.2 mysql error,rollback;
            accountInfoService.doUpdateAccountBalance(accountChangeEvent);
            //4 当上一步执行成功,当返回RocketMQLocalTransactionState.COMMIT,自动向mq发送commit消息,mq将消息的状态改为可消费
            log.error("本地数据库业务逻辑执行完毕,开始提交当前半消息");
            return RocketMQLocalTransactionState.COMMIT;
            // 如果需要测试mq发送确认消息到broker,可以讲提交状态改为COMMIT,就可以测试checkLocalTransaction,会调起回查功能
//            return RocketMQLocalTransactionState.COMMIT;
        } catch (Exception e) {
            e.printStackTrace();
            log.error("本地数据库业务逻辑执行失败,回滚当前半消息");
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }
}
  1. Producer执行本地事务

    Producer端执行业务代码逻辑,通过本地数据库事务控制。

/**
     * 被MQ通知,mysql开始执行本地事务,进行写操作,扣钱
     * @param accountChangeEvent
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void doUpdateAccountBalance(AccountChangeEvent accountChangeEvent)
    {
        //1 幂等性判断,大于零就不执行了,error
        // 当前demo通过mysql做幂等判断,生产建议用分布式锁+数据库唯一值
        if(accountInfoDao.isExistTx(accountChangeEvent.getTxNo()) > 0) {
            return;
        }
        //2 扣减金额
        int updateAmount = accountInfoDao.updateAccountBalance(accountChangeEvent.getAccountNo(), accountChangeEvent.getAmount() * -1);
        if (updateAmount != 1) {
            log.error("扣减金额失败");
            throw new RuntimeException("扣减金额失败");
        }

        //3 幂等性插入,向mysql里面的de_duplication表插入唯一流水,作为幂等判断初始数据
        int addTxNo = accountInfoDao.addTx(accountChangeEvent.getTxNo());
        if (addTxNo != 1) {
            log.error("幂等插入失败");
            throw new RuntimeException("幂等插入失败");
        }

        //4 异常测试,不是主流业务逻辑
        if(accountChangeEvent.getAmount() == -33){
            throw new RuntimeException("人为制造异常-33");
        }
    }
  1. 消息投递

    若Producer本地事务执行成功则自动向MQ Server发送commit消息,MQ Server接收到commit消息后将“增加积分信息”状态标记为可消费,此时MQ订阅方(积分服务)即正常消费消息;
    若Producer本地事务执行失败则自动向MQ Server发送Rollback消息,MQ Server接收到rollback消息后,将删除“增加积分消息”。
    MQ订阅方(积分服务)消费消息,消费成功则向MQ回应Ack,否则将重复接收消息。这里ack默认自动开启。

@Component
@Slf4j
@RocketMQTransactionListener(txProducerGroup = "producer_group_txmsg_bank1")
public class ProducerTxmsgListener implements RocketMQLocalTransactionListener
{
    @Autowired
    AccountInfoService accountInfoService;
    @Autowired AccountInfoDao accountInfoDao;

    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o)
    {
        try {
            //1 json处理
            String messageString = new String((byte[])message.getPayload());
            log.error("收到事务消息【{}】", messageString);
            JSONObject jsonObject = JSONObject.parseObject(messageString);
            String accountChangeString = jsonObject.getString("accountChange");
            //2 将accountChangeString 转换成为AccountChangeEvent消息实体
            AccountChangeEvent accountChangeEvent = JSONObject.parseObject(accountChangeString, AccountChangeEvent.class);
            //3 执行本地事务,通知mysql侧扣减转账金额,看mysql本地事务执行情况,
            // 3.1 mysql ok,commit
            // 3.2 mysql error,rollback;
            accountInfoService.doUpdateAccountBalance(accountChangeEvent);
            //4 当上一步执行成功,当返回RocketMQLocalTransactionState.COMMIT,自动向mq发送commit消息,mq将消息的状态改为可消费
            log.error("本地数据库业务逻辑执行完毕,开始提交当前半消息");
            
            
            // 以上本地事务执行成功,进行commit投递消息
            return RocketMQLocalTransactionState.COMMIT;
            
            
            // 如果需要测试mq发送确认消息到broker,可以讲提交状态改为COMMIT,就可以测试checkLocalTransaction,会调起回查功能
//            return RocketMQLocalTransactionState.COMMIT;
        } catch (Exception e) {
            e.printStackTrace();
            log.error("本地数据库业务逻辑执行失败,回滚当前半消息");
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }
}
  1. 事务回查

    如果执行Producer端本地事务过程中,执行端挂掉,或者超时,MQ Server将会不停的询问同组的其它Producer来获取事务执行状态,这个过程叫事务回查。MQ Server会根据事务回查结果来决定是否投递消息。以上主干流程已由RocketMQ实现,对用户侧来说,用户需要分别实现本地事务执行以及本地事务回查方法,因此只需关注本地事务的执行状态即可。

/**
     * 事务状态回查,查询bank1是否扣减金额成功,你那边数据库操作成功还是失败
     * @param message
     * @return
     */
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
        //解析message,转成AccountChangeEvent
        String messageString = new String((byte[]) message.getPayload());
        log.error("事务状态回查开始,消息【{}】", messageString);
        JSONObject jsonObject = JSONObject.parseObject(messageString);
        String accountChangeString = jsonObject.getString("accountChange");
        //将accountChange(json)转成AccountChangeEvent
        AccountChangeEvent accountChangeEvent =
                JSONObject.parseObject(accountChangeString, AccountChangeEvent.class);
        //MQ里面的事务id
        String txNo = accountChangeEvent.getTxNo();
        int existTx = accountInfoDao.isExistTx(txNo);
        if(existTx>0){
            log.error("回查发现流水号已存在,事务执行成功,提交当前半消息");
            return RocketMQLocalTransactionState.COMMIT;
        }else{
            log.error("回查发现流水号不存在,事务执行失败,回滚当前半消息");
            return RocketMQLocalTransactionState.UNKNOWN;
        }
    }
可靠消息最终一致性就是保证消息从生产方经过消息中间件传递到消费方的一致性,RocketMQ主要解决了两个问题:
  1. 本地事务与消息发送的原子性问题。(回查)
  2. 事务参与方接收消息的可靠性。(ack)
可靠消息最终一致性事务适合执行周期长且实时性要求不高的场景。引入消息机制后,同步的事务操作变为基于消息执行的异步操作,避免了分布式事务中的同步阻塞操作的影响,并实现了两个服务的解耦。
本地消息表方案

不是本文重点,后续出专门的介绍

源码

点我拉取源码

FAQ

1:RocketMq消费失败默认重试16次(一定要做幂等)
2:RocketMq事务消息失败默认回查15次

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
RocketMQ 是一个开源的分布式消息队列系统,它支持可靠消息传输和最终一致性RocketMQ可靠消息传输是通过消息的持久化和复制来实现的。当生产者发送消息时,消息会被持久化到本地磁盘,并且会根据配置的复制因子将消息复制到其他的 Broker 节点上。这样即使某个 Broker 节点出现故障,消息仍然可以从其他节点获取。 RocketMQ 通过使用主题(Topic)和分区(Partition)的概念来实现消息的负载均衡和扩展性。一个主题可以由多个分区组成,每个分区可以在不同的 Broker 节点上存储。这样可以保证同一个主题的消息在多个节点上进行分布式存储,提高了系统的可靠性和可扩展性。 最终一致性是指当消息被消费者消费后,消息队列系统会保证所有消费者看到的消息顺序是一致的。RocketMQ 使用了消息消费者组(Consumer Group)的概念,每个消费者组内的消费者共同消费一个主题的消息,系统会确保每个消费者按照相同的顺序消费消息。 此外,RocketMQ 还提供了事务消息和顺序消息等特性来满足不同业务场景下的需求,进一步提高了消息传输的可靠性和一致性。 总结来说,RocketMQ 通过持久化、复制、负载均衡、分区和消费者组等机制来实现可靠消息传输和最终一致性。这使得 RocketMQ分布式系统中被广泛应用于解决可靠消息传输的需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值