RocketMQ 场景及使用

一、整体介绍

  1. RocketMQ 是一款分布式、队列模型的消息中间件
  2. 支持分布式事务
  3. 天然的支持集群模型、负载均衡、水平扩展能力
  4. 亿级别的消息堆积能力
  5. 采用零拷贝的原理, 循序写盘,随机读
  6. 底层通信框架采用netty NIO 框架
  7. NameServer 代替Zookeeper ,更轻量级
  8. 消息失败重试机制,消息可查询(可设置时间间隔和重试次数)

应用场景

  • 削峰填谷:诸如秒杀、抢红包、企业开门红等大型活动时皆会带来较高的流量脉冲,或因没做相应的保护而导致系统超负荷甚至崩溃,或因限制太过导致请求大量失败而影响用户体验,消息队列 MQ 可提供削峰填谷的服务来解决该问题。
  • 异步解耦:交易系统作为淘宝/天猫主站最核心的系统,每笔交易订单数据的产生会引起几百个下游业务系统的关注,包括物流、购物车、积分、流计算分析等等,整体业务系统庞大而且复杂,消息队列 MQ 可实现异步通信和应用解耦,确保主站业务的连续性。
  • 顺序收发: 细数日常中需要保证顺序的应用场景非常多,比如证券交易过程时间优先原则,交易系统中的订单创建、支付、退款等流程,航班中的旅客登机消息处理等等。与先进先出(First In First Out,缩写 FIFO)原理类似,消息队列 MQ 提供的顺序消息即保证消息 FIFO。
  • 分布式事务一致性:交易系统、支付红包等场景需要确保数据的最终一致性,大量引入消息队列 MQ 的分布式事务,既可以实现系统之间的解耦,又可以保证最终的数据一致性。
  • 大数据分析: 数据在“流动”中产生价值,传统数据分析大多是基于批量计算模型,而无法做到实时的数据分析,利用阿里云消息队列 MQ 与流式计算引擎相结合,可以很方便的实现将业务数据进行实时分析。
  • 分布式缓存同步: 天猫双 11 大促,各个分会场琳琅满目的商品需要实时感知价格变化,大量并发访问数据库导致会场页面响应时间长,集中式缓存因为带宽瓶颈限制商品变更的访问流量,通过消息队列 MQ 构建分布式缓存,实时通知商品数据的变化。
    更多使用场景参考 -> 阿里使用场景

二、 RocketMQ 安装

Git地址:https://github.com/apache/rocketmq/tree/release-4.3.0

1、 在解压后的文件夹中执行maven命令,获取程序运行包(生成的包在rocketmq-distribution/target路径下):

mvn -Prelease-all -DskipTests clean install -U

2、rocketmq是一个集群模型的消息队列,这里我们用两台服务器来部署rocketmq,为了方便和区分,分别把两台服务器标注一下角色,如下节点配置:

IP 角色 模式
106.53.92.xxx nameServer1,brokerServer1 Master1
47.105.189.xx nameServer2,brokerServer2 Master2

3、创建消息队列信息保存路径

/usr/local/rocketmq/store/index
/usr/local/rocketmq/store/commitlog
/usr/local/rocketmq/store/consumequeue

4、修改broker配置


brokerClusterName=rocketmq-cluster
# broker 名字,不同文件命名不一样
brokerName=broker-a
# 0表示Master >0 表示slave
brokerId=0
brokerIP1=本机IP
# nameServer地址,多个使用分号分割
namesrvAddr=rocketmq-nameserver1:9876
# 默认创建的队列数
defaultTopicQueueNums=4
# 是否允许broker 自动创建topic 
autoCreateTopicEnable=true
# 是否允许broker自动创建订阅组
autoCreateSubscriptionGroup=true
# Broker 对外服务的监听端口
listenPort=10911
# 删除文件时间点,默认凌晨4点
deleteWhen=04
# 文件保留时间,默认48小时
fileReservedTime=120
# commitLog每个文件的大小默认1G
mapedFileSizeCommitLog=1073741824
# consumQueue每个文件默认存30W条
mapedFileSizeConsumeQueue=300000
# 检测物理文件磁盘空间
diskMaxUsedSpaceRatio=88
#存储路径 
storePathRootDir=/usr/local/rocketmq/store
# commitLog存储路径 
storePathCommitLog=/usr/local/rocketmq/store/commitlog
# 消费队列存储路径 
storePathConsumeQueue=/usr/local/rocketmq/store/consumequeue
# 消息索引存储路径 
storePathIndex=/usr/local/rocketmq/store/index
# checkpoint 文件存储路径 
storeCheckpoint=/usr/local/rocketmq/store/checkpoint
# abort 文件存储路径 
abortFile=/usr/local/rocketmq/store/abort
# ASYNC_MASTER 异步赋值 SYNC_MASTER 同步刷盘  SLAVE
brokerRole=ASYNC_MASTER
# 刷盘方式 ASYNC_FLUSH 异步刷盘   SYNC_FLUSH同步刷盘
flushDiskType=ASYNC_FLUSH

如果配置主从模式,则需要修改broke-a-s.properties

# 0表示Master >0 表示slave
brokerId=1
brokerIP1=本机IP
# ASYNC_MASTER 异步赋值 SYNC_MASTER 同步刷盘  SLAVE
brokerRole=SLAVE

5、 修改bin 下的runbroker.sh和 runserver.sh

JAVA_OPT="${JAVA_OPT} -server -Xms1g -Xmx1g -Xmn1g"

6、运行

// 启动nameserver
nohup sh ./mqnamesrv &
// 启动broker
nohup sh ./mqbroker -c ../conf/2m-2s-async/broker-a.properties > /dev/null 2&>1 &
// 关闭broker
sh mqshutdown broker
// 关闭nameServer
sh mqshutdown namesrv

可以用netstat -ntlp 查看一下端口占用情况

6、RocketMQ-Console(Git地址)

进入console模块,修改application.properties

rocketmq.config.namesrvAddr=106.53.92.208:9876
	// 在pom目录下打包
    $ mvn clean package -Dmaven.test.skip=true
    // 运行
    $ java -jar target/rocketmq-console-ng-1.0.0.jar
    // 如果配置文件没有填写Name Server
    $ java -jar target/rocketmq-console-ng-1.0.0.jar --rocketmq.config.namesrvAddr='10.0.74.198:9876;10.0.74.199:9876'

三、 代码部分

1、一个简单的实例

消息生产者

public class Producer {
    private static final Logger logger = LoggerFactory.getLogger("Customer");
    
    public static void main(String[] args) throws Exception {
        DefaultMQProducer producer = new DefaultMQProducer("rocketmq-cluster");
        // 设置nameserver 地址
        producer.setNamesrvAddr(MQConstant.NAMESERVER1);
        // 启动实例
        producer.start();
        for (int i = 0; i < 10; i++) {
            // 加入TagA TagB 两个标签用于测试
            String tag = "TagA";
            if(i%2==0){ tag = "TagB"; }
            // 创建消息
            Message msg = new Message("test", tag ,"key"+i,
                    ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)
            );
            // 发送消息
            SendResult sendResult = producer.send(msg);
            logger.info(JSONObject.toJSONString(sendResult));
        }
        // 一旦生产者实例不再使用,则关闭该实例
        producer.shutdown();
    }
}

消息消费者

public class Customer {
    private static final Logger logger = LoggerFactory.getLogger("Customer");
    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("rocketmq-cluster");
        // nameserver 地址
        consumer.setNamesrvAddr(MQConstant.NAMESERVER1);
        // 设置消息offset 位置
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        // 订阅消息主题
        consumer.subscribe("test","*");
        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                MessageExt me = list.get(0);
                //for (MessageExt me : list){
                try {
                    String topic = me.getTopic();
                    String tags = me.getTags();
                    String keys = me.getKeys();
                    String msgBody = "";
                    msgBody = new String(me.getBody(),"utf-8");
                    logger.info("topic:{}, tags:{}, keys:{} {}",topic,tags,keys,msgBody);
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                }
                //}
                return  ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.out.println("消费者已启动");
    }
}

消费者中, 如果消息第一次消费失败怎么办?
customer 中可以添加消息重试机制,当消息第一次失败可以可以进行重试,代码如下

public class Customer {

    private static final Logger logger = LoggerFactory.getLogger("Customer");

    public static void main(String[] args) throws Exception {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("rocketmq-cluster");
        // nameserver 地址
        consumer.setNamesrvAddr(MQConstant.NameServerAndSlave);
        // 设置消息offset 位置
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        // 订阅消息主题
        consumer.subscribe("test","*");

        consumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
                MessageExt me = list.get(0);
                //for (MessageExt me : list){
                try {
                    String topic = me.getTopic();
                    String tags = me.getTags();
                    String keys = me.getKeys();
                    String msgBody = "";
                    msgBody = new String(me.getBody(),"utf-8");
                    int i = 2/0;
                    logger.info("topic:{}, tags:{}, keys:{} {}",topic,tags,keys,msgBody);
                } catch (Exception e) {
                    e.printStackTrace();
                    int reconsumeTimes = me.getReconsumeTimes();
                    logger.info("重试次数:{}",reconsumeTimes);
                    if(reconsumeTimes==3){
                        logger.error("日志补偿。。。");
                        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                    }
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                }
                //}
                return  ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
            }
        });
        consumer.start();
        System.out.println("消费者已启动");
    }
}

2、生产者

2.1、延迟消息

  • 延迟消息,消息发送到broker后,要特定的时间才会被Consumer消费
  • 目前只支持固定精度的定时消息
  • msg.setDelayTimeLevel(2); 方法设置

2.2、发送消息到指定队列

	 // 发送到指定的队列中去
      SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
          @Override
          public MessageQueue select(List<MessageQueue> list, Message message, Object o) {
              Integer queueNum = (Integer) o;
              return list.get(queueNum);
          }
      }, 1);

3、生产者

3.1 PushConsumer 消费模式 - 集群模式

  • Clustering 模式 (默认)
  • GroupName 用于把多个Comsummer组织到一起
  • 相同Groupname 的Consumer 只消费所订阅消息的一部分
  • 目的:达到天然的负载均衡机制

适用场景
适用于消费端集群化部署,每条消息只需要被处理一次的场景。此外,由于消费进度在服务端维护,可靠性更高。具体消费示例如下图所示。
在这里插入图片描述
注意事项
集群消费模式下,每一条消息都只会被分发到一台机器上处理。如果需要被集群下的每一台机器都处理,请使用广播模式。
集群消费模式下,不保证每一次失败重投的消息路由到同一台机器上。

当我们启动两个消费之 A, B,然后再生产10条消息, 这时我们可以看到A消费了6条,B消费了4条,为啥会这样,不能实现负载均衡吗?
因为消费者通过监听消息对列来实现消息的接受, 加入四个消息对列, 生产者分别向两个消息队列投了三条消息,另外两个投了两条,则会出现这种情况。

21:47:04.981 [main] INFO  Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":2,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B3490000","offsetMsgId":"6A355CD000002A9F000000000002045E","queueOffset":40,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.028 [main] INFO  Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":3,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B3950001","offsetMsgId":"6A355CD000002A9F0000000000020515","queueOffset":60,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.065 [main] INFO  Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":0,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B3C40002","offsetMsgId":"6A355CD000002A9F00000000000205CC","queueOffset":39,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.112 [main] INFO  Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":1,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B3E90003","offsetMsgId":"6A355CD000002A9F0000000000020683","queueOffset":64,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.149 [main] INFO  Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":2,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B4180004","offsetMsgId":"6A355CD000002A9F000000000002073A","queueOffset":41,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.185 [main] INFO  Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":3,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B43E0005","offsetMsgId":"6A355CD000002A9F00000000000207F1","queueOffset":61,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.219 [main] INFO  Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":0,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B4610006","offsetMsgId":"6A355CD000002A9F00000000000208A8","queueOffset":40,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.259 [main] INFO  Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":1,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B4830007","offsetMsgId":"6A355CD000002A9F000000000002095F","queueOffset":65,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.298 [main] INFO  Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":2,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B4AB0008","offsetMsgId":"6A355CD000002A9F0000000000020A16","queueOffset":42,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}
21:47:05.338 [main] INFO  Customer - [main,31] - {"messageQueue":{"brokerName":"broker-a","queueId":3,"topic":"test"},"msgId":"C0A8006C25B018B4AAC24CC5B4D20009","offsetMsgId":"6A355CD000002A9F0000000000020ACD","queueOffset":62,"regionId":"DefaultRegion","sendStatus":"SEND_OK","traceOn":true}

如果存在这样一个场景, 一个producer 的同一个topic下分别有TagA、TagB、TagC , 这三个tag 分别需要被三个不同的consumer 消费,这样该如何实现?
我们所知的pushConsumer的集群模式不能很好地去实现这种需求,会有其他办法吗?

3.2 PushConsumer 消费模式 - 广播模式

  • BroadCasting 模式 (广播模式)

  • 同一个ConsumerGroup 里的Consumer 都消费订阅topic 全部信息,也就是一条消息会被每一个Consumer消费

  • consumer.setMessageModel(MessageModel.BROADCASTING);方法设置广播模式

广播消费:当使用广播消费模式时,消息队列 MQ 会将每条消息推送给集群内所有注册过的消费者,保证消息至少被每个消费者消费一次。
在这里插入图片描述
注意事项

  • 广播消费模式下不支持顺序消息。
  • 广播消费模式下不支持重置消费位点。
  • 每条消息都需要被相同订阅逻辑的多台机器处理。
  • 消费进度在客户端维护,出现重复消费的概率稍大于集群模式。
  • 广播模式下,消息队列 MQ 保证每条消息至少被每台客户端消费一次,但是并不会重投消费失败的消息,因此业务方需要关注消费失败的情况。
  • 广播模式下,客户端每一次重启都会从最新消息消费。客户端在被停止期间发送至服务端的消息将会被自动跳过,请谨慎选择。
  • 广播模式下,每条消息都会被大量的客户端重复处理,因此推荐尽可能使用集群模式。
  • 广播模式下服务端不维护消费进度,所以消息队列 MQ 控制台不支持消息堆积查询、消息堆积报警和订阅关系查询功能。

通过上述可以看到广播模式并不推荐使用,我们可以通过集群模式来模拟广播模式

使用集群模式模拟广播
适用场景:
适用于每条消息都需要被多台机器处理,每台机器的逻辑可以相同也可以不一样的场景。具体消费示例如下图所示。
在这里插入图片描述
如果业务需要使用广播模式,也可以创建多个 Group ID,用于订阅同一个 Topic。

注意事项
消费进度在服务端维护,可靠性高于广播模式。
对于一个 Group ID 来说,可以部署一个消费者实例,也可以部署多个消费者实例。当部署多个消费者实例时,实例之间又组成了集群模式(共同分担消费消息)。假设 Group ID 1 部署了三个消费者实例 C1、C2、C3,那么这三个实例将共同分担服务器发送给 Group ID 1 的消息。同时,实例之间订阅关系必须保持一致。

3.3 PullConsumer 消息拉取消费模式

pull方式里,取消息的过程需要用户自己写,首先通过打算消费的Topic拿到MessageQueue的集合,遍历MessageQueue集合,然后针对每个MessageQueue批量取消息,一次取完后,记录该队列下一次要取的开始offset,直到取完了,再换另一个MessageQueue。


/**
 * 消息拉取模式
 **/
public class PullConsumer {
    private static final Logger log = LoggerFactory.getLogger("PullConsumer");
    //保存上一次消费的消息位置
    private static final Map offsetTable = new HashMap();

    public static void main(String[] args) throws MQClientException {
        //实例化pullConsumer
        DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("rocketmq-cluster");
        consumer.setNamesrvAddr(MQConstant.Master_Slave);
        consumer.start();
        //获取topic下所有的队列
        Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("test");
        //遍历消息队列
        for (MessageQueue mq : mqs) {
            log.info("消息队列信息: " + mq);
            SINGLE_MQ:
            while (true) {
                try {
                    //设置上次消费消息下标
                    PullResult pullResult = consumer.pullBlockIfNotFound(mq, null, getMessageQueueOffset(mq), 32);
                    // 保存消息下次读取的 offset
                    putMessageQueueOffset(mq, pullResult.getNextBeginOffset());
                    switch (pullResult.getPullStatus()) {
                        //根据结果状态,如果找到消息,批量消费消息
                        case FOUND:
                            List<MessageExt> messageExtList = pullResult.getMsgFoundList();
                            for (MessageExt m : messageExtList) {
                                log.info("topic:{}; getQueueId:{}; offset:{}; 消息内容:{}",m.getTopic(),m.getQueueId(),m.getQueueOffset(),new String(m.getBody()));
                            }
                            break;
                        case NO_MATCHED_MSG:
                            log.warn("没有匹配的信息");
                            break;
                        case NO_NEW_MSG:
                            log.warn("没有新的的信息");
                            break SINGLE_MQ;
                        case OFFSET_ILLEGAL:
                            log.warn("OFFSET_ILLEGAL");
                            break;
                        default:
                            break;
                    }
                } catch (Exception e) {
                    log.error("消息消费出现异常:");
                    e.printStackTrace();
                }
            }
        }
        consumer.shutdown();
    }
    //保存上次消费的消息下标,这里使用了一个全局HashMap来保存
    private static void putMessageQueueOffset(MessageQueue mq, long offset) {
        offsetTable.put(mq, offset);
    }

    //获取上次消费的消息的下表 这里可以保存在硬盘或redis中 ConsumerName-topic-queueId做key
    private static long getMessageQueueOffset(MessageQueue mq) {
        Long offset = (Long) offsetTable.get(mq);
        if (offset != null) {
            return offset;
        }
        return 0;
    }
}

4、消息落地方式

4.1 同步刷盘和异步刷盘
RocketMQ的消息是存储到磁盘上的,这样既能保证断电后恢复,又可以让存储的消息量超出内存的限制。
消息在通过Producer写入RocketMQ的时候,有两种写磁盘方式:
异步刷盘方式: 在返回写成功状态时,消息可能只是被写入了内存的PAGECACHE,写操作的返回快,
吞吐量大;当内存里的消息量积累到一定程度时,统一触发写磁盘操作,快速写入
同步刷盘方式: 在返回写成功状态时,消息已经被写入磁盘。具体流程是,消息写入内存的PAGECACHE后,立刻通知刷盘线程刷盘,然后等待刷盘完成,刷盘线程执行完成后唤醒等待的线程,返回消息写成功的状态。
4.12同步双写和异步复制
异步复制和同步双写主要是主和从的关系。消息需要实时消费的,就需要采用主从模式部署
异步复制: 比如这里有一主一从,我们发送一条消息到主节点之后,这样消息就算从producer端发送成功了,然后通过异步复制的方法将数据复制到从节点
同步双写: 比如这里有一主一从,我们发送一条消息到主节点之后,这样消息就并不算从producer端发送成功了,需要通过同步双写的方法将数据同步到从节点后, 才算数据发送成功。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章