rocketmq消息及流程

1、为什么用mq
优势
主要有3个:
应用解耦(降低微服务之间的关联)、
异步提速(微服务拿到mq消息后同时工作)、
削峰填谷(可以消息堆积)

劣势
系统可用性降低(MQ一旦宕机整个系统不可用)
复杂度提高(需要解决系统消息一致性、重复消费…)
一致性问题(不同系统拿到mq中的消息后,部分系统处理失败怎么办)

2、rocketmq集群工作流程
image

由上图可以看出,rocketMQ集群=消息服务器集群+命名服务器集群,其中消息服务器集群=生产者集群+broker集群+消费者集群。

命名服务器集群(nameserver cluster)
● 命名服务器集群是管理生产者、broker、消费者的纽带,哪个生产者/broker/消费者可用都是通过命名服务器得知其信息,所以生产者/broker/消费者都需要定时发送心跳给命名服务器
● 命名服务器与生产者的关系:命名服务器记录有许多broker的ip地址,每个生产者发送消息到broker前都需要先去命名服务器获取某个broker的ip,然后再发送消息到broker
● 命名服务器和消息者的关系:命名服务器记录有许多broker的ip地址,消费者想监听broker中的消息,需要先去命名服务器获取某个broker的ip,然后再监听broker中的消息

生产者集群(producer cluster)
● 每个生产者部署在不同的IP上形成了集群
● 生产者的消息=topic+tag,topic用来区分消息类型,一种topic类型的消息可以分布在多个不同的broker中,同类型的消息就用tag区分,如我们系统里的佣金宝的topic是"topic-yjb",然后佣金宝下面可以划分多个tag

消费者集群(consumer cluster)
● 每个消费者部署在不同的IP上形成了集群
● 消费者获取某个broker中的消息理论上有两种方法:
○ pull拉取模式:消费者开启线程定时访问broker,如有消息存在则拉取,缺点是太消耗消费者的资源了,不管有没有消息都会去访问broker
○ push推送模式:消费者起一个监听器监听broker(与broker建立一个长链接),若broker中有消息,则broker会自动推送消息给消费者,一般用这种。其中push模式的底层也是通过消费者主动拉取的方式来实现的,只不过它的名字叫push而已,意思是Broker尽可能实时的推送消息给消费者,和pull模式相比,push模式都帮我们封装了底层,而pull模式就要自己写代码去手动拉取消息,所以pull模式更像拉取,而封装好的push更像是推送。

3、消息类型
同步/异步/单向消息
同步:发送消息是按顺序发送
异步:发送消息是异步的,生产者发送完消息就干其他事情,消费者稍后会在生产者的回调函数中返回消费结果【new SendCallback(){} 】,及时性差
单项消息:生产者只管发出去,并不接收返回值

批量消息
特点:
● 同一批消息的topic应该相同;
● 消息内容大小=(topic+body+其他key/value属性+日志固定20字节)<4M
● 不是延时消息

tag过滤消息
消费者指定特定的tag,则只接收该tag的消息

sql过滤消息
消费者支持类似于sql查询语法那样的消息过滤

//生产者
String msg="hello,小明同学";
Message message=new Message("topic",msg.getBytes("UTF-8"));
message.putUserProperty("name","xiaoming");
message.putUserProperty("age","27");
SendResult sendResult1 =defaultMQProducer.send(message);

//消费者用sql语法过滤出age>26岁的消息,即只接受age>26岁的消息
DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("group1");
defaultMQPushConsumer.subscribe("topic", MessageSelector.bySql("age>26"));

4、消息的特殊处理
顺序消息
场景:在某些业务系统中,一些业务流程处理的顺序必须是按顺序的,比如客户下单:创建订单 -> 付款 -> 推送消息 -> 订单完成,在并发环境下,不可能只有一个客户下单,当多个客户下单时,他们的这四种消息有可能是混乱的。
解决方案:rocketmq默认每个topic在broker中都会有四个队列存放该类数据,队列是FIFO性质的,我们可以利用队列去按序存放这些消息以达到按序消费的目的。
image

生产者主要代码:

DefaultMQProducer defaultMQProducer=new DefaultMQProducer("group1");
defaultMQProducer.setNamesrvAddr("127.0.0.1:9876");
try {
    defaultMQProducer.start();
    List<Order> list=new ArrayList<>();
    //模拟业务流程乱序提交:单个订单消息有序,多个订单间消息无序
    Order order01=new Order(0,"创建订单");
    Order order11=new Order(1,"创建订单");
    Order order02=new Order(0,"付款");

    Order order03=new Order(0,"推送");
    Order order21=new Order(2,"创建订单");
    Order order12=new Order(1,"付款");

    Order order04=new Order(0,"完成");
    Order order13=new Order(1,"推送");
    Order order22=new Order(2,"付款");

    Order order14=new Order(1,"完成");
    Order order23=new Order(2,"推送");
    Order order24=new Order(2,"完成");

list.addAll(new ArrayList<Order>(Arrays.asList(order01,order11,order02,order03,order21,order12,order13,order22,order23,order04,order14,order24)));
for(Order order:list){
    Message message=new Message("topic-order",order.toString().getBytes());
   /*
    *每个topic默认创建4个队列,defaultMQProducer可以通过MessageQueueSelector的select方法设置
    *当前Message发送到"topic-order"的哪个队列:通过订单的唯一属性值,如orderId,对topic中的queue队列数取模,
    *这样同一个订单的不同消息就会被按序放进同一个queue中
    */
   SendResult sendResult=defaultMQProducer.send(message, new MessageQueueSelector() {

            @Override
            public MessageQueue select(List<MessageQueue> queueList, Message msg, Object o) {
                System.out.println("队列数:"+queueList.size());
                //获取队列下标
                int size=order.getOrderId()%queueList.size();
                //计算该message放在"topic-order"哪个队列中
                MessageQueue mq=queueList.get(size);
                return mq;
            }
        },null);
        System.out.println(sendResult);
    }

} catch (Exception e) {
    e.printStackTrace();
}

消费者主要代码:

//使用MessageListenerConcurrently则多个线程服务一个队列,而MessageListenerOrderly是一个线程服务一个队列(topic默认四个队列就是四个线程)
defaultMQPushConsumer.registerMessageListener(new MessageListenerOrderly() {
    @Override
    public ConsumeOrderlyStatus consumeMessage(List<MessageExt> list, ConsumeOrderlyContext consumeOrderlyContext) {
        System.out.println("线程:"+Thread.currentThread().getName()+",队列:"+consumeOrderlyContext.getMessageQueue().getQueueId()+",该队列消息数量:"+list.size());
        for(MessageExt messageExt:list){
            System.out.println(new String(messageExt.getBody()));

        }
        return ConsumeOrderlyStatus.SUCCESS;
    }
});

执行结果:
image

可以看出一个线程服务一个队列,将同类业务的消息都推送到同一个队列中,是可以实现消息的顺序发送的.

事务消息
1. 为什么要用事务消息?
还是以用户下单为例,用户在producer中创建订单(但未提交事务到mysql),然后把下单消息发送给broker(即MQ服务器),MQ服务器再把该消息发给所有订阅了该类topic的消费者,可能出现如下情况:

(1)producer成功进行了数据库操作(即提交事务到mysql),且MQ服务器接收消息成功,然后被消费者消费 -->皆大欢喜

(2)producer成功进行了数据库操作(即提交事务到mysql),但发到MQ服务器失败,进而消费者不能消费该类消息 -->不正常

(3)producer进行数据库操作的时候发生了意外导致数据库操作失败(即提交事务到mysql),但发到MQ服务器成功,进而消费者会去消费该类消息 -->不正常

上面第2、3种情况都是不正常的,解决办法就是引入事务消息,事务消息的过程如下:

image

第一阶段producer先发送一条"half"型消息到MQ服务器,MQ服务器收到后随即返回一个发送成功标识 ->

producer进行数据库操作执行事务,执行成功则发送二次确认(Commit或Rollback)消息给服务器 ->

MQ服务器收到Commit则将第一阶段的"half"型消息标记为可投递,消费者若订阅了该topic则能收到该消息;MQ服务器收到Rollback则删除第一阶段的消息,消费者将接收不到该消息,就当什么事也没发生过 ->

MQ服务器会有一个事务补偿机制:若服务器很久都没有收到producer返回的二次确认commit/rollback,则会主动去调用producer的接口进行回查,然后producer再去数据库中查看事务是否执行成功 ,如成功/失败,则发送commit/rollback给MQ服务器,然后后面的操作同上一步

2. 事务消息和正常消息的区别
(1)事务消息有三种状态:commit状态、回滚状态、中间状态(producer发送了half型消息但未发送commit给到服务器,即未对);commit状态的消息等价于正常消息(可以被消费者感知),但后两种状态的消息对于消费者是不可见的
(2)事务消息仅与生产者有关,仅当事务消息处于commit状态时与正常消息一样可以被消费者感知

3. 代码实现事务消息
生产者主要代码:

//事务消息使用的生产者是TransactionMQProducer
TransactionMQProducer transactionMQProducer=new TransactionMQProducer("group1");
transactionMQProducer.setNamesrvAddr("127.0.0.1:9876");
try {
    //添加事务监听
    transactionMQProducer.setTransactionListener(new TransactionListener(){

        //事务消息过程中包括正常事务(数据库操作)、事务补偿,正常事务在该方法执行
        @Override
        @Transactional
        public LocalTransactionState executeLocalTransaction(Message message, Object o) {
            try{
                
                //模拟数据库操作事务正常提交:insert delete...
                Long orderId= Long.valueOf(new String(message.getBody()));
                insert(orderId);
                System.out.println("本地数据库事务提交成功!");
                //本地执行完数据库操作后(正常事务),事务消息有三种状态:COMMIT_MESSAGE/ROLLBACK_MESSAGE/UNKNOW
                return LocalTransactionState.COMMIT_MESSAGE;
                
            }catch(Exception ex){
                
                //模拟数据库操作事务提交失败:insert delete...

                System.out.println("本地数据库事务提交失败,事务消息rollback:"+ex);
                //本地执行完数据库操作后(正常事务),事务消息有三种状态:COMMIT_MESSAGE/ROLLBACK_MESSAGE/UNKNOW
                return LocalTransactionState.ROLLBACK_MESSAGE;
                
            }

            //业务成功后,也可以直接返回UNKNOW,避免极端的情况下数据库并没有保存成功
            //return LocalTransactionState.UNKNOW;
            
        }

        //事务补偿在该方法执行:若executeLocalTransaction返回UNKNOW或长时间没有返回消息给服务器
        @Override
        public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
            
            System.out.println("执行事务补偿...");
            Long orderId= Long.valueOf(new String(messageExt.getBody()));
            if(null!=selectOne(orderId)){
                return LocalTransactionState.COMMIT_MESSAGE;
            }else{
                //若事务不成功可以返回UNKNOW而不是ROLLBACK_MESSAGE,因为服务器默认回查15次后都是UNKNOW,则会自动回滚haLf型消息
                return LocalTransactionState.UNKNOW;
            }
            
        }
    });
    
    transactionMQProducer.start();
    Order order=new Order(0,"创建订单");
    Message message=new Message("topic-transaction",String.valueOf(order.getOrderId()).getBytes());
    SendResult sendResult=transactionMQProducer.sendMessageInTransaction(message,null);
    System.out.println("sendResult:"+sendResult);

由事务消息的整个流程可知,先执行executeLocalTransaction,再打印发送结果:
image

如果executeLocalTransaction返回UNKNOW,则服务器不断尝试事务补偿:
image

消费者主要代码:

DefaultMQPushConsumer defaultMQPushConsumer = new DefaultMQPushConsumer("group1");
defaultMQPushConsumer.setNamesrvAddr("127.0.0.1:9876");
defaultMQPushConsumer.subscribe("topic-transaction", "*");
defaultMQPushConsumer.registerMessageListener(new MessageListenerConcurrently() {
    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
        list.forEach(msg->{
            System.out.println("收到消息:"+new String(msg.getBody()));
        });
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
});
defaultMQPushConsumer.start();

执行结果如下:
image

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值