RocketMQ入门到精通
MQ简介
MQ全称为Message Queue(消息队列),是在消息传输中保存消息的容器。多用于分布式系统之间进行通信。
Queue:数据结构的一种,特征为”先进先出“

优势
- 
应用解耦 
 假设我现在还要添加一个大数据系统 就不需要修改上游系统的逻辑。
  
- 
异步提速 
 多个消息队列下游系统并行执行逻辑比顺序执行逻辑要快
  
- 
削峰填谷 
 流量值过大的时候可以缓存消息,让下游系统平稳处理数据,避免流量过大系统崩溃
  
劣势
- 
系统可用性降低 
 一旦MQ宕机,就会对所有下游系统造成影响。如何保证MQ高可用?
- 
复杂度提高 
 以前的系统是同步的远程调用,现在通过MQ调用,如何保证重复消费?怎么处理消息丢失情况?如何保证消息传递的顺序性
- 
一致性问题 
 A系统处理完业务,通过MQ给A,B,C三个系统发送消息,如果B、C系统处理成功但是A系统异常,如何保证给消息数据处理的一致性?
RocketMQ
RocketMQ是阿里开源的一款非常优秀的中间件产品,后捐赠给apache基金会的顶级项目,能承受住双十一极致的场景压力(双十一峰值达到万亿级)
基础感念

Windows安装
- 下载rocketmq4.8.0版本
- 配置环境变量
 变量名:ROCKETMQ_HOME
 变量值:环境路径
注:部分电脑重启环境变量才会生效

3. 启动nameserve:
D:\rocketmq-all-4.8.0-bin-release\bin>start mqnamesrv.cmd
- 启动broker:
#autoCreateTopicEnable=true 自动创建topic
D:\rocketmq-all-4.8.0-bin-release\bin>start mqbroker.cmd -n 127.0.0.1:9876 autoCreateTopicEnable=true -c ../conf/broker.conf
Linux安装
自行百度
Java整合RocketMQ
入门案例
生产者
- 引入依赖
 <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-client</artifactId>
            <version>4.8.0</version>
</dependency>
- 编写代码
public static void main(String[] args) throws Exception {
       //1.谁来发
        DefaultMQProducer producer = new DefaultMQProducer("group1");
        //2.发给谁
        producer.setNamesrvAddr("127.0.0.1:9876");
        //3.怎么发
        //4.发什么
        producer.start();
        String content = "hello rocketmq";
        Message message = new Message("topic1", "tag1", content.getBytes());
        SendResult send = producer.send(message);
        //5.发送的结果是什么
        System.out.println(send);
        //6.打扫战场
        //关闭连接
        producer.shutdown();
    }
消费者
- 引入依赖
 <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-client</artifactId>
            <version>4.8.0</version>
</dependency>
- 编写代码
public static void main(String[] args) throws Exception {
        //1.谁来收
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
        //2.从哪里收消息
        consumer.setNamesrvAddr("127.0.0.1:9876");
        //3.监听那个消息队列
        consumer.subscribe("topic1", "*");
        //处理业务,注册实时监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                for (MessageExt data : list) {
                    byte[] body = data.getBody();
                    System.out.println(new String(body));
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
    }
多消费模式
这里存在一个问题 为了保证消费者系统高可用部署了集群,那么消费者会处理(集群*消息数)翻倍的消息数量吗
生产者
生产者发送十条消息
 //1.谁来发
        DefaultMQProducer producer = new DefaultMQProducer("group1");
        //2.发给谁
        producer.setNamesrvAddr("127.0.0.1:9876");
        //3.怎么发
        //4.发什么
        producer.start();
        for (int i = 0; i < 10; i++) {
            String content = "hello rocketmq";
            Message message = new Message("topic2", "tag1", content.getBytes());
            SendResult send = producer.send(message);
            //5.发送的结果是什么
            System.out.println(send);
        }
        //6.打扫战场
        //关闭连接
        producer.shutdown();
    }
消费者
启动两个消费者服务
public static void main(String[] args) throws Exception {
        //1.谁来收
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
        //2.从哪里收消息
        consumer.setNamesrvAddr("127.0.0.1:9876");
        //3.监听那个消息队列
        consumer.subscribe("topic2", "*");
        //处理业务,注册实时监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                for (MessageExt data : list) {
                    byte[] body = data.getBody();
                    System.out.println(new String(body));
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
    }
消费者1

消费者2

为什么会这样呢,这样就涉及到group的概念?
group的概念
group的作用就是用来做负载均衡,假设生产者生产了10条消息,处在相同group的消费者就会平分消息,处在不同group的消费者就会接收全量的消息

上图:Consumer1接受5条,Consumer2接受5条,Consumer3接受10条
当然也可以不修改group来使处在相同组的消费者接受全量的消息
 //默认是负载均衡模式,MessageModel.BROADCASTING就变成全量接受
 consumer.setMessageModel(MessageModel.BROADCASTING);
同步消息
特征:即使性较强,重要的消息,且必须有回执的消息,例如短信,通知
SendResult send = producer.send(message);
异步消息
特征:即时性较弱,但需要有回执的消息,例如订单中的某些信息,吞吐量较高
 //注:异步消息不能关闭mq的连接
 producer.send(message, new SendCallback() {
                //异步回调成功
                public void onSuccess(SendResult sendResult) {
                    System.out.println(sendResult);
                }
                //异步回调失败
                public void onException(Throwable throwable) {
                    System.out.println(throwable.toString());
                }
            });
单向消息
特征:不需要回执的消息,例如日志消息
 producer.sendOneway(message);
延时消息
//1(1s) 2(5s) 3(10s) 4(30s) .. 自行百度等级对应的具体时间
message.setDelayTimeLevel(1);
批量消息
每次生产者和MQ建立连接都是非常消耗性能的(要经历三握四挥)。所以就需要建立一次连接传输全量的数据
         List<Message> data = new ArrayList<Message>();
        for (int i = 0; i < 10; i++) {
            String content = "hello rocketmq";
            Message message1 = new Message("topic5", "tag1", content.getBytes());
            data.add(message1);
        }
        producer.send(data);
注:
- 批量消息应该有相同的topic
- 相同的对象字段类型
- 不能是延时消息
- 消息内容总长度不超过4M
Tag过滤
  //接收全部
  consumer.subscribe("topic2", "*");
  //接受单个
  consumer.subscribe("topic2", "tag1");
  //接受多个tag
  consumer.subscribe("topic2", "tag1 || tag2");
SQL过滤
像写SQL一样在消费方过滤不想要的数据
- 开启SQL过虑支持(默认不开启)
#找到conf/broker.conf 追加以下配置
enablePropertyFilter=true
- 重启broker
start mqbroker.cmd -n 127.0.0.1:9876 autoCreateTopicEnable=true -c ../conf/broker.conf
- 生产者设置Message
 public static void main(String[] args) throws Exception {
        //1.谁来发
        DefaultMQProducer producer = new DefaultMQProducer("group1");
        //2.发给谁
        producer.setNamesrvAddr("127.0.0.1:9876");
        //3.怎么发
        //4.发什么
        producer.start();
        for (int i = 0; i < 10; i++) {
            String content = "hello rocketmq"+i;
            Message message = new Message("topic10", "tag1", content.getBytes());
            //设置sql过滤的追加属性
            message.putUserProperty("age", Integer.toString(i));
            SendResult send = producer.send(message);
            //5.发送的结果是什么
            System.out.println(send);
        }
        //6.打扫战场
        //关闭连接
        producer.shutdown();
    }
- 消费者设置
public static void main(String[] args) throws Exception {
        //1.谁来收
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
        //2.从哪里收消息
        consumer.setNamesrvAddr("127.0.0.1:9876");
        //3.监听那个消息队列 SQL过滤
        consumer.subscribe("topic10", MessageSelector.bySql("age >= 5 "));
        //处理业务,注册实时监听器
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                for (MessageExt data : list) {
                    byte[] body = data.getBody();
                    System.out.println(new String(body));
                }
                return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
    }
这里可以发现MQ已经过滤掉了age>=5 的数据

SpringBoot整合
- Idea创建项目
  
- 勾选SpringMVC改为Web项目
  
- 添加RocketMQ依赖
<dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-spring-boot-starter</artifactId>
            <version>2.0.3</version>
        </dependency>
- 编写配置文件
rocketmq:
  # 配置nameserver地址
  name-server: localhost:9876
  # 配置生产者组名
  producer:
    group: group1
- 编写生产者代码
@RestController
public class SendController {
    @Autowired
    private RocketMQTemplate rocketMQTemplate;
    @GetMapping(value = "test")
    public String send() {
        Map map = new HashMap();
        map.put("name", "范源鑫");
        map.put("age", "18");
        rocketMQTemplate.convertAndSend("topic1:tag1", map);
        return "success";
    }
}
- 编写消费者代码
@Service
@RocketMQMessageListener(topic = "topic1", selectorType = SelectorType.TAG, selectorExpression = "tag1", consumerGroup = "group1")
public class Consumer implements RocketMQListener<Map> {
    @Override
    public void onMessage(Map map) {
        System.out.println("我是消费者A" + map.toString());
    }
}
消息类型案例
生产者的消息类型案例
        //同步消息
        SendResult topic1 = rocketMQTemplate.syncSend("topic1:2", map);
        //异步消息
        rocketMQTemplate.asyncSend("topic1:tag2", map, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                System.out.println("处理成功" + sendResult.toString());
            }
            @Override
            public void onException(Throwable throwable) {
                System.out.println("处理失败" + throwable.getMessage());
            }
        });
        //发送单向消息
        rocketMQTemplate.sendOneWay("topic1:2", map);
        //发送延时消息
        rocketMQTemplate.syncSend("topic1:2", MessageBuilder.withPayload(map).build(), 1, 2);
        //发送批量消息
        ArrayList<Map> list = Lists.newArrayList();
        list.add(map);
        SendResult data = rocketMQTemplate.syncSend("topic1:2", list);
        
消费者根据SQL过滤
@RocketMQMessageListener(topic = "topic1", selectorType = SelectorType.SQL92, selectorExpression = "age >= 10 ", consumerGroup = "group1")
消费者默认的负载改为广播模式
@RocketMQMessageListener(topic = "topic1", selectorType = SelectorType.SQL92, selectorExpression = "age >= 10 ", consumerGroup = "group1",messageModel = MessageModel.BROADCASTING)
消费顺序
假设一个订单的流程 是 创建->付款->完成 之后用消息队列发送会产生什么影响呢
- 创建生产者模拟数据
 private void getData() {
        ArrayList<OrderStep> objects = Lists.newArrayList();
        objects.add(new OrderStep(1L, "创建"));
        objects.add(new OrderStep(2L, "创建"));
        objects.add(new OrderStep(1L, "付款"));
        objects.add(new OrderStep(3L, "创建"));
        objects.add(new OrderStep(2L, "付款"));
        objects.add(new OrderStep(3L, "付款"));
        objects.add(new OrderStep(2L, "完成"));
        objects.add(new OrderStep(3L, "完成"));
        objects.add(new OrderStep(1L, "完成"));
        return objects;
       //同步发送消息
       for (OrderStep datum : data) {
            SendResult topic1 = rocketMQTemplate.syncSend("topic1:tag2", datum);
        }
    }
- 创建两条消费者A,B
@Service
@RocketMQMessageListener(topic = "topic1", selectorType = SelectorType.TAG, selectorExpression = "tag2", consumerGroup = "group1")
public class ConsumerA implements RocketMQListener<OrderStep> {
    @Override
    public void onMessage(OrderStep map) {
        System.out.println("消费者A" + map.toString());
    }
}
@Service
@RocketMQMessageListener(topic = "topic1", selectorType = SelectorType.TAG, selectorExpression = "tag2", consumerGroup = "group1")
public class ConsumerB implements RocketMQListener<OrderStep> {
    @Override
    public void onMessage(OrderStep map) {
        System.out.println("消费者B" + map.toString());
    }
}
运行会发现订单2先完成了。这是什么情况呢?

是因为一个broker默认有4个队列(可修改)消息发送时每个消息可能会被分配到不同的队列中 导致多个消者消费时会可能先读取到队列2的订单完成订单。那怎么保证顺序消费呢,就是在生产者吧同个订单分配到相同的队列内部就可以了

解决:将订单相同的放进同一个队列中
- 发送同步消息
//通过Orderly接口实现顺序发送 关键点是hashcode参数 hashcode参数相同分配的队列就相同
//这里是根据订单的id当作hashcode
SendResult sendResult = rocketMQTemplate.syncSendOrderly("topic1:tag2", orderStep, hashcode);
- 消费方设置同步消息模式(consumeMode = ConsumeMode.ORDERLY)
@RocketMQMessageListener(topic = "topic1", selectorType = SelectorType.TAG, selectorExpression = "tag2", consumerGroup = "group1",consumeMode = ConsumeMode.ORDERLY)
事务消息
- 红色是正常事务
- 蓝色为事务补偿过程
  
注:事务和消费者没有关系
状态:
提交状态:允许进入队列,此消息与非事务消息无区别(属于第四步的提交操作)
回滚状态:不允许进入队列,此消息等同于未发送过消息(属于第四步的回滚操作)
中间状态:完成了half消息的发送,没有对mq进行二次确认(broker主动询问生产者消息事务的状态)
- 创建本地事务消息监听类
package com.fyx.rocketmq.transaction;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Service;
@Service
@RocketMQTransactionListener(txProducerGroup = "tx_order")
public class LocalTrsaction implements RocketMQLocalTransactionListener {
    //正常事务处理
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        if (true) {
            //业务成功把消息提交到broker
            System.out.println("事务正常提交");
            return RocketMQLocalTransactionState.COMMIT;
        }else{
            //业务失败就回滚不发送消息到broker
            System.out.println("事务异常提交");
            return RocketMQLocalTransactionState.ROLLBACK;
        }
    }
    //事务补偿处理
    //正常事务处理返回UNKNOWN时候进入这个方法进行重新进行业务操作
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
    if (true) {
            //业务成功把消息提交到broker
            System.out.println("事务正常提交");
            return RocketMQLocalTransactionState.COMMIT;
        }else{
            //业务失败就回滚不发送消息到broker
            System.out.println("事务异常提交");
            return RocketMQLocalTransactionState.ROLLBACK;
        }
        
    }
}
- 生产者发送事务消息
        Message<OrderStep> message = MessageBuilder.withPayload(new OrderStep(1L, "创建")).build();
        TransactionSendResult sendResult = rocketMQTemplate.sendMessageInTransaction("tx_order", "tx_topic:" + "tx_tag", message, message.getPayload().getOrderId());
        String localTXState = sendResult.getLocalTransactionState().name();
        System.out.println("事务名:"+localTXState);
- 消费者接受消息
@Service(value = "consumer3")
@RocketMQMessageListener(topic = "tx_topic", selectorType = SelectorType.TAG, selectorExpression = "tx_tag", consumerGroup = "tx_group")
public class Consumer implements RocketMQListener<OrderStep> {
    @Override
    public void onMessage(OrderStep map) {
        System.out.println(map.toString());
    }
}
消息重试
当消费者处理完业务没有向broker正常返回处理成功的状态后,boker会对这一条消息进行重复消费消息
大致分为两种消息重试机制,顺序消息和无序消息
- 顺序消息(同步消息)
- 当消费者消费失败后,rocketmq会自动进行消息重试(每次间隔时间1秒)
注:应用会出现消息消费被阻塞的情况,因此,要对顺序消息的消费情况给进行监控,避免阻塞现象的发生
- 无序消息(普通消息、定时消息、延时消息、事务消息)
- 无序消息重试仅适用于负载均衡(集群)模型下的消息消费,不适用于广播模式下的消息消费
- 为保障无序消息的消费,MQ设定了合理的消息重试间隔时常
死信队列
当消息重试达到了指定次数(默认16次)后,MQ将无法被正常消费的消息称为死信消息,死信消息不会被直接抛弃,而是保存到了一个全新的队列中,该队列称为死信队列(Dead-Letter Queue)
死信队列处理:在监控平台中查找死信队列,获取死信的MessageId,然后通过id对死信进行精准消费
重复消费
可能因为网络闪断、生产者宕机、broker重启、订阅方应用重启 会对同一笔订单做多次业务操作(例:如对同一笔订单扣除多次库存)
解决方案:
- 使用业务id作为消息的key使用
- 在消费消息时,客户端对key做判断,未使用过放行,使用过抛弃

 
                
            
         
         浙公网安备 33010602011771号
浙公网安备 33010602011771号