消息中间件选型和关于RocketMQ的事务处理

消息中间件选型和关于RocketMQ的事务处理

市面上比较流行的消息中间件以及如何选型

关于消息中间件的几个思考

为什么需要消息中间件

消息队列之所以作为高并发系统下面核心组件之一,主要具有削峰填谷、异步操作、系统解耦、提高性能、蓄流压测等优势。

消息中间件有哪些缺点
  1. 系统的复杂性提高
  2. 系统的健壮性降低,需要考虑中间件宕机带来的影响
如何保证消息队列的高可用

通过集群来保证,Master、Slave等

如何保证消息的不重复消费

消息在生产和消费的过程中增加幂等性的保证

如何保证消息的不丢失

生产过程丢失:同步确认模式
消息中间件存储丢失:及时放入持久化的队列

如何保证消息的顺序性

通过算法放入同一个消息队列

目前市面上比较常见的消息中间件有RocketMq、Kafka、RabbitMq。
我们先来介绍一下三种中间件的优缺点:

消息中间件优点缺点
kafak1. 高吞吐量,单机可以抗住十几万的QPS
2. 性能高,发送消息毫秒级
3. 高可用,支持集群部署,部分宕机不影响使用
1. 因为消息不是直接写入磁盘,而是写入磁盘缓冲区,所以可能会造成数据的丢失
2. 功能单一,只支持订阅-发布
RabbitMQ1. 保证消息不丢失
2. 高可用,部分宕机不影响使用
3. 功能丰富,消息重试、死信队列
1. 吞吐量低,大概每秒几万QPS
2. 开发语言是erlang,很难进行改造
RocketMQ1. 吞吐量高,单机器每秒十几万QPS
2. 高可用、高性能
3. 保证数据不丢失
4. 支持大规模集群
5. 功能丰富,如延迟消息、消息回溯
6. java开发,易改造
1. 社区活跃度一般
2. 在MQ核心中没有实现JMS接口,因此对某些系统迁移需要修改大量代码

选型建议:
首先我们可以看一下社区的活跃度 Kafka > RocketMQ(阿里团队) > RabbitMQ > ActiveMQ
kafka: 主要特点是消费消息,高吞吐量。所以如果我们对高吞吐量、高性能有要求的话,可以考虑kafka,但是kafka功能比较单一。综合来看,kafka目前很适合对于大量日志的收集。
RabbitMQ: 社区活跃度高,较为稳定,不利于开发和维护,适合数据量小,稳定的小公司选型。
RocketMQ: 业务中如果存在高并发的场景或者需要符合很多的业务场景,建议选择RocketMQ,毕竟经历过双十一的磨砺。

RocketMQ

RocketMQ的官网地址

功能特性:

  1. 订阅-发布
  2. 消息顺序: 按照发送的顺序来消费,例如订单的结算。顺序消息也分为全局顺序消息和分区顺序消息,全局顺序是指某个Topic下的所有消息都要保证顺序;部分顺序消息只要保证每一组消息被顺序消费即可。
  3. 消息过滤: 消费者根据tag对于消息效率,减少无用的网络传输
  4. 消息可靠性
  5. 至少一次: 每个消息至少发送一次。消费者在消费之后手动提交ack消息
  6. 回溯消息: 消息在消费之后会保存一段时间,可以重新消费
  7. 事务消息(*): 通过事务消息达到分布式事务的最终一致
  8. 定时消息: 消息发送到broker之后,不会被立即消费,会在特定的时候投递给指定的topic。比如电商里,提交了一个订单就可以发送一个延时消息,1h后去检查这个订单的状态,如果还是未付款就取消订单释放库存。
  9. 消息重试: Rocket为每个消费者组设置一个重试队列
  10. 消息重投: 在消息量大,网络抖动、消息重复的情况下,消息重投会导致消息重复的问题
  11. 流量控制
  12. 死信队列: 在消息重试之后达到重试的最大次数之后,会被放入另外一个队列被存储。

架构

BrokerServer: 主要负责处理来自客户端的请求,维护topic信息等功能。Broker分为Master和Slave,通过制定小童的BrokerName,不同的BrokerId来定义。BrokerId=0为Master,非0位Slave。目前只有BrokerID=1的从服务器才会参与消息的读负载
NameServer: 是一个topic的路由中心,支持broker的动态注册和发现;路由信息动态管理,每个NameServer上面保存了一个完整的路由信息
Producer: 消息发布角色
Consumer: 消息消费的角色,支持push推、pull拉两种方式进行消费。定期从NameServer中拉去路由信息,并和Broker的Master、Slave建立长连接。
工作流程:

  1. 启动NameServer,等待broker、producer、consumer连接上来,相当于注册中心
  2. Broker启动,与NameServer建立连接,并发送心跳包(包括broker的信息和topic的信息)
  3. Producer发送消息时通过NameServer查看topic在哪一台broker上面
  4. Consumer消费消息

消息事务

主要分为以下几个步骤:

  1. 生产者发送办消息到MQ Server,暂时不能被投递和消费
  2. MQ Server返回成功接收
  3. 生产者执行本地事务
  4. 生产者向MQ Server发送提交还是回滚
  5. 如果MQ Server没有收到反馈,则主动询问生产者
  6. 生产者检查本地事务状态
  7. 如果提交,Consumer消费
  8. 如果回滚,Consumer不消费

生产者:

    @RequestMapping("sendTransaction")
    public Object sendTransaction(@RequestParam(value = "msg",required = false)String msg){
        String transactionId = UUID.randomUUID().toString();
        org.springframework.messaging.Message<String> message = MessageBuilder.withPayload(msg)
                .setHeader(RocketMQHeaders.TRANSACTION_ID, transactionId).build();
        //发送半消息
        TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction("topic03",message,transactionId);

        //执行本地事务
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return sendResult;
    }
//监听发送
@RocketMQTransactionListener
public class MQLocalListener implements RocketMQLocalTransactionListener {


    /**
     * 用于执行本地事务
     * @param msg
     * @param arg
     * @return
     */
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        MessageHeaders headers = msg.getHeaders();
        String transactionId = (String) headers.get(RocketMQHeaders.TRANSACTION_ID);
        log.info("[执行本地事务]消息体参数: transactionId={}", transactionId);
        //执行本地方法
        try {
            Thread.sleep(2000);
            String payload = new String((byte[]) msg.getPayload());
            if ("zzz".equals(payload)){
                log.info("[回滚本地事务]");
                return RocketMQLocalTransactionState.ROLLBACK;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
            //异常,发送回滚信号
            return RocketMQLocalTransactionState.ROLLBACK;
        }
        //正常,发送提交信号
        return RocketMQLocalTransactionState.COMMIT;
    }

    /**
     * 回查本地事务执行结果
     * @param msg
     * @return
     */
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        MessageHeaders headers = msg.getHeaders();
        String transactionId = headers.get(RocketMQHeaders.TRANSACTION_ID, String.class);
        log.info("【回查本地事务】transactionId={}", transactionId);
        //查询日志记录
        // 如果存在则返回 RocketMQLocalTransactionState.COMMIT
        // 如果失败则返回 RocketMQLocalTransactionState.ROLLBACK
        return RocketMQLocalTransactionState.COMMIT;
    }
}

消费者:

    public static void main(String[] args) throws MQClientException {
        int i=0;
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();
        consumer.setNamesrvAddr("127.0.0.1:9876");
        consumer.subscribe("topic03","*");
        consumer.setConsumerGroup("topic03-group");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                String msg = new String(msgs.get(0).getBody());
                System.out.println("收到消息:"+msg);
                // 标记该消息已经被成功消费
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
    }

消费者消费失败如何处理?

  • 正常消费,会返回 ConsumeConcurrentlyStatus.CONSUME_SUCCESS
  • 异常消费,会触发重试机制 ConsumeConcurrentlyStatus.RECONSUME_LATER
    如何进行重试?
    重试的消息会进入一个 "%RETRY%"+ConsumeGroup 的重试队列中
    然后RocketMQ默认允许每条消息最多重试16次,每次重试的间隔时间与延迟消息的延迟级别是对应的。不过取的是延迟级别的后16级别。如果消息重试16次后仍然失败,消息将不再投递。转为进入死信队列。另外一条消息无论重试多少次,这些重试消息的MessageId始终都是一样的。

如何保证消息消费的幂等性?
首先,我们要了解消息幂有三种情况:

  1. at most once 最多一次:每条消息最多被消费一次,rocketMQ通过异步发送、sendOneWay等方式保证
  2. at least once 至少一次:每条消息至少消费一次,rocketMQ通过同步发送、事务消息等方式保证
  3. exactly once 刚好一次:每条消息只会消费一次,也是MQ中最难保证的一种。需要业务系统自行保证消息的幂等性,比如每条消息新建一个唯一的编号。

根据上面的事务消息的例子,我们可以讨论一下为什么rocket可以解决分布式事务的问题?

首先,我们需要了解为什么需要分布式事务。
有一个大家熟悉的例子,A账号向B账号转账100元,如果两个账号的操作在同一个事务中操作,要么一起成功,要么一起失败,我们就不用操心了。
但是存在一个问题,如果A银行账号和B银行账号不在同一个事务,那么就会存在3个问题。

  1. A扣除了100块,然后发送消息给B账号失败
  2. A先发送消息给B账号,然而A账号扣钱失败
  3. A消息和操作都成功了,B账号加钱失败

我们现在来看一下RocketMQ是怎么解决这个问题的:
RocketMQ采用的是二阶段提交,分为:准备阶段和确认阶段
准备阶段:将消息放在新的队列,消费者无法感知
确认阶段:Committe和RollBack
那么,现在就有几个动作可能会失败了,按照执行顺序来说: 发送准备消息、执行事务、发送确认消息,根据这几个动作失败分为3个异常

  1. 发送准备消息失败,不影响后面的操作
  2. 发送准备消息成功,执行事务失败,消费者无法感知消息,所以不影响
  3. 发送准备消息和执行事务成功,发送确认消息因网络故障失败,这个会影响到消息的消费
    那么,Rocket为了解决异常3,采用如果没有收到确认消息,会回调生产者的方法来查看事务的状态。
    其中,如果生产者没有问题,而消费者消费数据时出问题了,会进行重试,再超过重试次数之后交由人工处理。或者是消费数据出问题之后,业务上实现逻辑进行生产者数据的回滚。因为RocketMQ是保证最终的一致性,消费者的失败并不会回滚生产者。

如果喜欢博主,可以关注我的公众号哈
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值