RocketMQ

一、简介

  • 阿里开源的一款的消息中间件,纯Java开发,具有高吞吐量、高可用性、适合大规模分布式系统应用的特点,性能强劲(零拷贝技术),支持海量堆积,支持指定次数和时间间隔的失败消息重发,支持consumer端tag过滤、延迟消息等,在阿里内部进行大规模使用适合在电商,互联网金融等领域使用
  • 特点
    • 支持Broker和Consumer端消息过滤0
    • 支持发布订阅模型,和点对点
    • 支持拉pull和推push两种消息模式
    • 单一队列百万消息、亿级消息堆积
    • 支持单master节点,多master节点,多master多slave节点
    • 任意一点都是高可用,水平拓展,Producer、Consumer、队列都可以分布式
    • 消息失败重试机制、支持特定level的定时消息。
    • 新版本底层采用Netty
    • 4.3.x支持分布式事务
    • 适合金融类业务,高可用性跟踪和审计功能
  • 概念
    • Producer:消息生产者
    • Producer Group:消息生产者组,发送同类消息的一个消息生产组
    • Consumer:消费者
    • Consumer Group:消费同类消息的多个实例
    • Tag:标签,子主题(二级分类)对topic的进一步细化,用于区分同一个主题下的不同业务的消息
    • Topic:主题,如订单类消息,queue是消息的物理管理单位,而topic是逻辑管理单位。一个topic下可以有多个queue,默认自动创建是4个,手动创建是8个
    • Message:消息,每个message必须指定-个topic
    • Broker:MQ程序,接收生产的消息,提供给消费者消费的程序
    • Name Server:给生产和消费者提供路由信息,提供轻量级的服务发现、路由、元数据信息,可以多个部署,互相独立(比zookeeper更轻量)
    • Offset: 偏移量,可以理解为消息进度
    • commit log: 消息存储会写在Commit log文件里面

二、源码安装RocketMQ4.x

一、基础环境搭建

  • jdk安装
    #解压
    tar -zxvf jdk-8u391-linux-x64.tar.gz
    #重命名
    mv jdk-8u391 jdk1.8
    #编辑
    vim /etc/profile
    export JAVA_HOME=/usr/local/software/jdk1.8
    export PATH=$JAVA_HOME/bin:$PATH
    export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar
    export JAVA_HOME PATH CLASSPATH
    #让配置立刻生效
    source /etc/profile 
  • maven安装
    #解压
    tar -zxvf apache-maven-3.8.8-bin.tar.gz
    #重命名
    mv apache-maven-3.8.8 maven-3.8.8
    #编辑
    vim /etc/profile
    export PATH=/usr/local/software/maven-3.8.8/bin:$PATH
    #刷新配置
    source /etc/profile

二、rocketMq源码安装

下载地址:Release Notes - Apache RocketMQ - Version 4.4.0 | RocketMQ

#解压
unzip rocketmq-all-4.4.0-source-release.zip
#进入目录mvn构建
cd rocketmq-all-4.4.0/
mvn -Prelease-all -DskipTests clean install -U
cd distribution/target/apache-rocketmq
#最终目录 rocketmq-all-4.4.0/distribution/target/apache-rocketmq
  • nameserver启动
    • nohup sh mqnamesrv &
    • 内存不足问题解决
      • 修改 bin/runserver.sh
        • JAVA_OPT="${JAVA_OPT} -server -Xms256m -Xmx256m -Xmn256m - XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
  • broker 修改配置
    • conf/broker.conf 添加一行  brokerIP1=(公网地址)
  • broker启动
    • nohup sh mqbroker -n localhost:9876 -c ../conf/broker.conf &
    • 内存不足解决
      • 修改bin/runbroker.sh
        • JAVA_OPT="${JAVA_OPT} -server -Xms528m -Xmx528m -Xmn256m"
  • 关闭
    • sh mqshutdown broker
    • sh mqshutdown namesrv

三、可视化控制台源码安装

  • 上传源码包-》解压-》进入rocketmg-console目录
  • 务必修改下面两个,再进行编译打包
    • 修改 pom.xml 版本号(官方bug), 4.4.0-SNAPSHOT---》 4.4.0
    • 修改application.xml里面的nameserver地址
  • 编译打包 mvn clean package -Dmaven.test.skip=true
  • 开发端口,默认8080
  • 进入target目录,启动java -jar rocketmg-console-ng-1.0.0.jar
    • 守护进程方式启动 nohup java -jar rocketmg-console-ng-1.0.0.jar &

四、简单案例

 pom文件

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>2.6.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <version>2.6.2</version>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.apache.rocketmq</groupId>
            <artifactId>rocketmq-client</artifactId>
            <version>4.3.0</version>
        </dependency>
@Component
public class Producer {

    public DefaultMQProducer producer;

    public static final String PRODUCER_GROUP = "PGroup";
    //集群多个;隔开 "112.124.25.207:9876;112.124.25.208:9876"
    public static final String NAME_SERVER = "112.124.25.207:9876";

    public Producer(){
        //设置生产者组
        producer = new DefaultMQProducer(PRODUCER_GROUP);
        //多个地址;隔开   例如127.0.0.1;127.0.0.2
        producer.setNamesrvAddr(NAME_SERVER);
        //启动
        try {
            this.producer.start();
        } catch (MQClientException e) {
            e.printStackTrace();
        }
    }


    public void shutdown(){
        this.producer.shutdown();
    }


    public DefaultMQProducer getProducer(){
        return this.producer;
    }

}
@Component
public class Consumer {
    private DefaultMQPushConsumer pushConsumer;

    //集群多个;隔开 "112.124.25.207:9876;112.124.25.208:9876"
    public static final String NAME_SERVER = "112.124.25.207:9876";
    public static final String GROUP = "CGroup";
    public static final String TOPIC = "test";

    public Consumer(){
        //设置消费者组
        this.pushConsumer = new DefaultMQPushConsumer(GROUP);
        //设置nameserver地址
        pushConsumer.setNamesrvAddr(NAME_SERVER);
        //设置消费偏移量offset
        pushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        //订阅主题,*代表所有tag标签
        try {
            pushConsumer.subscribe(TOPIC,"*");
        } catch (MQClientException e) {
            e.printStackTrace();
        }
        //监听函数
        pushConsumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                try {
                    Message msg = msgs.get(0);
                    System.out.printf("%s Receive New Messages: %s %n",
                            Thread.currentThread().getName(), new String(msgs.get(0).getBody()));
                    String topic = msg.getTopic();
                    String body = new String(msg.getBody(), "utf-8");
                    String tags = msg.getTags();
                    String keys = msg.getKeys();
                    System.out.println("topic=" + topic + ", tags=" + tags + ",keys=" + keys + ", msg=" + body);
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                }
            }
        });
        //启动
        try {
            pushConsumer.start();
        } catch (MQClientException e) {
            e.printStackTrace();
        }
    }
}
@RequestMapping("mq")
@RestController
public class MQController {

    @Autowired
    private Producer producer;

    public static final String TOPIC = "test";

    @GetMapping("sendMsg")
    public String sendMsg(@RequestParam("msg") String msg)
            throws InterruptedException, RemotingException,
            MQClientException, MQBrokerException {
        Message message = new Message(TOPIC, "tag_1", "only_key", msg.getBytes());
        SendResult result = producer.getProducer().send(message);
        System.out.println(result);
        return "success";
    }

}

一定到开放端口 9876、10909、10911、8080 

  • 代码报错一
    • org.apache.rocketmq.remoting.exception.RemotingTooMuchRequestException: sendDefaultImpl call timeout
    • 原因:阿里云存在多网卡,rocketmg都会根据当前网卡选择一个IP使用,当你的机器有多块网卡时,很有可能会有问题。比如,我遇到的问题是我机器上有两个IP,一个公网IP,一个私网IP,因此需要配置broker.conf 指定当前的公网ip,然后重新启动broker新增配置:conf/broker.conf (属性名称brokerIp1=broker所在的公网ip地址)新增这个配置:brokerIP1=120.76.62.13
      • 启动命令:nohup sh bin/mgbroker -n localhost:9876-c ./conf/broker.conf &
  • 代码报错二
    • MQClientException: No route info of this topic, Test1
    • nameserver端口未开放,未手动创基主题
  • 控制台看不了数据
    • 未开放10909端口

三、集群架构

一、集群模式

  • 单节点
    • 优点:本地开发测试,配置简单,同步刷盘消息一条都不会丢
    • 缺点:不可靠,如果宕机,会导致服务不可用
  • 主从(异步、同步双写)======》推荐
    • 优点:同步双写消息不丢失,异步复制存在少量丢失,主节点宕机,从节点可以对外提供消息的消费,但是不支持写入
    • 缺点:主备有短暂消息延迟,毫秒级,目前不支持自动切换,需要脚本或者其他程序进行检测然后进行停止broker,重启让从节点成为主节点
  • 双主:
    • 优点:配置简单,可以靠配置RAID磁盘阵列保证消息可靠,异步刷盘丢失少量消息
    • 缺点: master机器宕机期间,未被消费的消息在机器恢复之前不可消费,实时性会受到影响
  • 双主双从,多主多从模式(异步复制)======》推荐
    • 优点:磁盘损坏,消息丢失的非常少,消息实时性不会受影响,Master 宕机后,消费者仍然可以从Slave消费
    • 缺点:主备有短暂消息延迟,毫秒级,如果Master宕机,磁盘损坏情况,会丢失少量消息
  • 双主双从,多主多从模式(同步双写)======》推荐
    • 优点:同步双写方式,主备都写成功,向应用才返回成功,服务可用性与数据可用性都非常
    • 缺点:性能比异步复制模式略低,主宕机后,备机不能自动切换为

二、主从搭建 

  • 机器列表
    • server1    192.168.200.129  主
    • server2    192.168.200.130  从
  • 修改两台机器的nameserver 并启动
    vim runserver.sh
    JAVA_OPT="${JAVA_OPT} -server -Xms528m -Xmx528m -Xmn256m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
    vim runbroker.sh
    JAVA_OPT="${JAVA_OPT} -server -Xms528m -Xmx528m -Xmn256m"
    #启动 nameserver
    nohup sh bin/mqnamesrv &
    #全路径
    #/usr/local/software/rocketmq/distribution/target/apache-rocketmq
  • 修改两台机器的broker配置并启动 
  • #主节点
    vim conf/2m-2s-async/broker-a.properties
    namesrvAddr=192.168.200.129:9876;192.168.200.130:9876
    brokerClusterName=TestCluster
    brokerName=broker-a
    #主从配置  0为主节点,非0为从节点
    brokerId=0
    deleteWhen=04
    fileReservedTime=48
    #broker的角色   ASYNC_MASTER异步复制主  SYNC_MASTER同步复制主   SLAVE从
    brokerRole=ASYNC_MASTER
    #ASYNC_FLUSH异步刷盘   SYNC_FLUSH同步刷盘
    flushDiskType=ASYNC_FLUSH
    #启动命令
    nohup sh bin/mqbroker -c conf/2m-2s-async/broker-a.properties &
    #从节点
    vim conf/2m-2s-async/broker-a-s.properties
    namesrvAddr=192.168.200.129:9876;192.168.200.130:9876
    brokerClusterName=TestCluster
    brokerName=broker-a
    #主从配置  0为主节点,非0为从节点
    brokerId=1
    deleteWhen=04
    fileReservedTime=48
    #ASYNC_MASTER异步复制主  SYNC_MASTER同步复制主  SLAVE从
    brokerRole=SLAVE
    #ASYNC_FLUSH异步刷盘   SYNC_FLUSH同步刷盘
    flushDiskType=ASYNC_FLUSH
    #启动命令
    nohup sh bin/mqbroker -c conf/2m-2s-async/broker-a-s.properties &
  • 控制台 
  • #application.properties修改nameserver
    #添加 rocketmq.config.namesrvAddr=192.168.200.129:9876;192.168.200.130:9876
    mvn install -Dmaven.test.skip=true
    java -jar rocketmq-console-ng-1.0.0.jar

三、双主双从搭建 

  • 机器列表
    • server1    192.168.200.129  部署 maser a、nameserver
    • server2    192.168.200.130  部署 slave a-s、nameserver 
    • server3    192.168.200.131  部署 maser b
    • server4    192.168.200.132  部署 slave b-s
  •  修改 129、131两台机器runserver,修改四台机器runbroker
  • vim runserver.sh
    JAVA_OPT="${JAVA_OPT} -server -Xms528m -Xmx528m -Xmn256m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"
    vim runbroker.sh
    JAVA_OPT="${JAVA_OPT} -server -Xms528m -Xmx528m -Xmn256m"
    #启动 nameserver
    nohup sh bin/mqnamesrv &
    #全路径
    #/usr/local/software/rocketmq/distribution/target/apache-rocketmq
  • 修改四台机器broker配置 
#主节点 a
vim conf/2m-2s-sync/broker-a.properties
namesrvAddr=192.168.200.129:9876;192.168.200.131:9876
brokerClusterName=TestCluster
brokerName=broker-a
#主从配置  0为主节点,非0为从节点
brokerId=0
deleteWhen=04
fileReservedTime=48
#broker的角色   ASYNC_MASTER异步复制主  SYNC_MASTER同步复制主   SLAVE从
brokerRole=SYNC_MASTER
#ASYNC_FLUSH异步刷盘   SYNC_FLUSH同步刷盘
flushDiskType=ASYNC_FLUSH
#启动命令
nohup sh bin/mqbroker -c conf/2m-2s-sync/broker-a.properties &



#从节点 a-s
vim conf/2m-2s-sync/broker-a-s.properties
namesrvAddr=192.168.200.129:9876;192.168.200.131:9876
brokerClusterName=TestCluster
brokerName=broker-a
#主从配置  0为主节点,非0为从节点
brokerId=1
deleteWhen=04
fileReservedTime=48
#broker的角色   ASYNC_MASTER异步复制主  SYNC_MASTER同步复制主   SLAVE从
brokerRole=SYNC_MASTER
#ASYNC_FLUSH异步刷盘   SYNC_FLUSH同步刷盘
flushDiskType=ASYNC_FLUSH
#启动命令
nohup sh bin/mqbroker -c conf/2m-2s-sync/broker-a-s.properties &


#主节点 b
vim conf/2m-2s-sync/broker-b.properties
namesrvAddr=192.168.200.129:9876;192.168.200.131:9876
brokerClusterName=TestCluster
brokerName=broker-b
#主从配置  0为主节点,非0为从节点
brokerId=0
deleteWhen=04
fileReservedTime=48
#broker的角色   ASYNC_MASTER异步复制主  SYNC_MASTER同步复制主   SLAVE从
brokerRole=SYNC_MASTER
#ASYNC_FLUSH异步刷盘   SYNC_FLUSH同步刷盘
flushDiskType=ASYNC_FLUSH
#启动命令
nohup sh bin/mqbroker -c conf/2m-2s-sync/broker-b.properties &



#从节点 b-s
vim conf/2m-2s-sync/broker-a-s.properties
namesrvAddr=192.168.200.129:9876;192.168.200.131:9876
brokerClusterName=TestCluster
brokerName=broker-b
#主从配置  0为主节点,非0为从节点
brokerId=1
deleteWhen=04
fileReservedTime=48
#broker的角色   ASYNC_MASTER异步复制主  SYNC_MASTER同步复制主   SLAVE从
brokerRole=SYNC_MASTER
#ASYNC_FLUSH异步刷盘   SYNC_FLUSH同步刷盘
flushDiskType=ASYNC_FLUSH
#启动命令
nohup sh bin/mqbroker -c conf/2m-2s-sync/broker-b-s.properties &
  •  控制台
#application.properties修改nameserver
#添加 rocketmq.config.namesrvAddr=192.168.200.129:9876;192.168.200.131:9876
mvn install -Dmaven.test.skip=true
java -jar rocketmq-console-ng-1.0.0.jar

 

四、核心知识

一、生产者

一、核心配置

  • compressMsgBodyOverHowmuch:消息超过默认字节4096后进行压缩
  • retryTimesWhenSendFailed:失败重发次数maxMessageSize:最大消息配置,默认128k
  • topicQueueNums:主题下面的队列数量,默认是4autoCreateTopicEnable:是否自动创建主题Topic,开发建议为true,生产要为false
  • defaultTopicQueueNums:自动创建服务器不存在的topic,默认创建的队列数
  • autoCreateSubscriptionGroup:是否允许 Broker 自动创建订阅组,建议线下开发开启,线上关闭
  • brokerClusterName:集群名称
  • brokerld:0表示Master主节点 大于0表示从节点brokerlP1:Broker服务地址
  • brokerRole : broker角色 ASYNC MASTER/ SYNC MASTER/ SLAVE
  • deleteWhen:每天执行删除过期文件的时间,默认每天凌晨4点
  • flushDiskType :刷盘策略,默认为 ASYNC_FLUSH(异步刷盘),另外是SYNC FLUSH(同步刷盘)
  • listenPort: Broker监听的端口号
  • mapedFileSizeCommitLog:单个conmmitlog文件大小,默认是1GB
  • mapedFileSizeConsumeQueue:ConsumeQueue每个文件默认存30W条,可以根据项目调整
  • storePathRootDir:存储消息以及一些配置信息的根目录默认为用户的${HOME//storestorePath
  • CommitLog:commitlog存储目录默认为${storePathRootDir//commitlogstorePathIndex:消息索引存储路径
  • syncFlushTimeout:同步刷盘超时时间
  • diskMaxUsedSpaceRatio :检测可用的磁盘空间大小,超过后会写入报错

二、消息发送状态

  • FLUSH DISK TIMEOUT
    • 没有在规定时间内完成刷盘(刷盘策略需要为SYNC FLUSH 才会出这个错误)
  • FLUSH SLAVE TIMEOUT
    • 主从模式下,broker是SYNC MASTER,没有在规定时间内完成主从同步
  • SLAVE NOT AVAILABLE
    • 从模式下,broker是SYNC MASTER,但是没有找到被配置成Slave的Broker
  • SEND_OK
    • 发送成功,没有发生上面的三种问题

三、消息发送重试 

  • 生产者Producer重试(异步和SendOneWay下配置无效)
    • 消息重投(保证数据的高可靠性),本身内部支持重试,默认次数是2。
    • 如果网络情况比较差,或者跨集群则建改多几次。
    • 对象设置 producer.setRetryTimesWhenSendFailed(3);

四、异步发送

 producer.getProducer().send(message, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                //发送成功,响应信息
            }

            @Override
            public void onException(Throwable e) {
                //异常处理,补偿操作
            }

五、多场景发送

  • SYNC
    • 应用场景:重要通知邮件、报名短信通知、营销短信系统等
  • ASYNC:异步
    • 应用场景:对RT时间敏感,可以支持更高的并发,回调成功触发相对应的业务,比如注册成功。后通知积分系统发放优惠券
  • ONEWAY:无需要等待响应
    • 官方文档:https://rocketmg.apache.org/docs/simple-example/
    • 使用场景:主要是日志收集,适用于某些耗时非常短,但对可靠性要求并不高的场景,也就是。LogServer, 只负责发送消息,不等待服务器回应且没有回调函数触发,即只发送请求不等待应答

六、延迟消息 

  • 什么是延迟消息:
    • Producer 将消息发送到消息队列 RocketMQ 服务端,但并不期望这条消息立马投递,而是推。迟到在当前时间点之后的某一个时间投递到 Consumer 进行消费,该消息即定时消息,目前支持固定精度的消息
    • 代码:rocketmg-store>MessageStoreConfig.java 属性 messageDelayLevel
      • "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h".
      • 使用message.setDelayTimeLevel(xxx)
      • //xxx是级别,1表示配置里面的第一个级别,2表示第二个级别
      • 定时消息:目前rocketmg开源版本还不支持,商业版本则有,两者使用场景类似
  • 使用场景
    • 通过消息触发一些定时任务,比如在某一固定时间点向用户发送提醒消息
    • 消息生产和消费有时间窗口要求:比如在天猫电商交易中超时未支付关闭订单的场景,在订0单创建时会发送一条 延时消息。这条消息将会在 30 分钟以后投递给消费者,消费者收到此消息后需要判断对应的订单是否已完成支付。如支付未完成,则关闭订单。如已完成支付则忽略

七、MessageQueueSelector 

  • 应用场景:顺序消息,分摊负载
  • 默认topic下的queue数量是4,可以配置
  • 支持同步,异步发送指定的MessageQueue
  • 选择的queue数量必须小于配置的,否则会出错
producer.send(message, new MessageQueueSelector(){
    //arg 就是传的参数 就是0
     select(List<MessageQueue> mqs, Message msg, Object arg){
         //自定义算法最终选择队列
         Integer queueNum = (Integer)arg;
         //获取队列,下标0开始
         return mqs.get(queueNum);
     }
}, 0)

二、消费者

一、核心配置

  • consumeFromWhere配置(某些情况失效:参考https://blog.csdn.net/a417930422/article/details/83585397)
    • CONSUME FROM_FIRST OFFSET: 初次从消息队列头部开始消费,即历史消息(还储存。在broker的)全部消费一遍,后续再启动接着上次消费的进度开始消费
    • CONSUME FROM_LAST OFFSET:默认策略,初次从该队列最尾开始消费,即跳过历史消息,后续再启动接着上次消费的进度开始消费
    • CONSUME FROM_TIMESTAMP:从某个时间点开始消费,默认是半个小时以前,后续再启。动接着上次消费的进度开始消费
  • allocateMessageQueueStrategy:负载均衡策略算法,即消费者分配到queue的算法,默认值是
  • allocateMessageQueueAveragely即取模平均分配offsetStore:消息消费进度存储器
  • offsetStore 有两个策略:
    • LocalFileOffsetStore 和 RemoteBrokerOffsetStor 广播模式
    • 默认使用LocalFileOffsetStore
    • 集群模式默认使用RemoteBrokerOffsetStore
  • consumeThreadMin 最小消费线程池数量
  • consumeThreadMax 最大消费线程池数量
  • pullBatchSize:消费者去broker拉取消息时,一次拉取多少条。可选配置
  • consumeMessageBatchMaxSize:单次消费时一次性消费多少条消息,批量消费接口才有用,可选配置
  • messageModel:消费者消费模式
    • CLUSTERING--默认是集群模式
    • CLUSTERINGBROADCASTING--广播模式

 二、tag标签-消息过滤

  • 一个Message只有一个Tag,tag是二级分类
    • 订单:数码类订单、食品类订单
  • 过滤分为Broker端和Consumer端过滤
    • Broker端过滤,减少了无用的消息的进行网络传输,增加了broker的负担
    • Consumer端过滤,完全可以根据业务需求进行实习,但是增加了很多无用的消息传输。
  • 一般是监听*,或者指定 tag,‖运算,SLQ92,FilterServer等;
    • tag性能高,逻辑简单
    • SQL92 性能差点,支持复杂逻辑(只支持PushConsumer中使用)MessageSelector.bySql
      • 语法:>,<=,IS NULL, AND,OR, NOT 等,sql where后续的语法即可(大部分)
  • 注意:消费者订阅关系要一致,不然会消费混乱,甚至消息丢失
    • 订阅关系一致:订阅关系由 Topic和 Tag 组成,同一个 group name,订阅的 topic和tag 必须是一样的
  • 在Broker 端进行MessageTag过滤,遍历message queue存储的 message tag和 订阅传递的tag 的hashcode不一样则跳过,符合的则传输给Consumer,在consumer queue存储的是对应的hashcode,对比也是通过hashcode对比; Consumer收到过滤消息后也会进行匹配操作,但是是对比真实的message tag而不是hashcode
    • consume queue存储使用hashcode定长,节约空间
    • 过滤中不访问commit log,可以高效过滤
    • 如果存在hash冲突,Consumer端可以进行再次确认
  • 如果想使用多个Tag,可以使用sql表达式,但是不建议,单一职责,多个队列 
  • 常见错误
    • The broker does not support consumer to filter message by SQL92
    • 解决 添加配置:enablePropertyFilter=true

三、消费重试

  • 消费端重试
    • 原因:消息处理异常、broker端到consumer端各种问题,如网络原因闪断,消费处理失败ACK返回失败等等问题。
  • 注意:
    • 重试间隔时间配置,默认每条消息最多重试 16 次
    • messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
  • 超过重试次数人工补偿
    • 消费端去重
    • 一条消息无论重试多少次,这些重试消息的 Message lD,key 不会改变
    • 消费重试只针对集群消费方式生效;广播方式不提供失败重试特性,即消费失败后,失败消息不再重试,继续消费新的消息

 

pushConsumer.registerMessageListener(new MessageListenerOrderly() {
            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                MessageExt msg = msgs.get(0);
                //消费重试次数
                int reconsumeTimes = msg.getReconsumeTimes();
                try {
                    System.out.printf("%s Receive New Messages: %s %n",
                            Thread.currentThread().getName(), new String(msgs.get(0).getBody()));
                    String topic = msg.getTopic();
                    String body = new String(msg.getBody(), "utf-8");
                    String tags = msg.getTags();
                    String keys = msg.getKeys();
                    System.out.println("topic=" + topic + ", tags=" + tags + ",keys=" + keys + ", msg=" + body);
                    return ConsumeOrderlyStatus.SUCCESS;
                } catch (UnsupportedEncodingException e) {
                    if(reconsumeTimes >= 2){
                        //重复消费两次就不在重新消费,直接人工处理
                        return ConsumeOrderlyStatus.SUCCESS;
                    }
                    e.printStackTrace();
                    return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
                }
            }
        });

四、顺序消费

  • MessageListenerOrderly
    • Consumer会平均分配queue的数量
    • 并不是简单禁止并发处理,而是为每个ConsumerQuene加个锁,消费每个消息前,需要获得这个消息所在的Queue的锁,这样同个时间,同个Queue的消息不被并发消费,但是不同Queue的消息可以并发处理
  • 扩展思维:为什么高并发情况下ConcurrentHashMap比HashTable和HashMap更高效且线程安全?
    • 提示:分段锁Segment
  • 代码
pushConsumer.registerMessageListener(new MessageListenerOrderly() {
            @Override
            public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
                try {
                    Message msg = msgs.get(0);
                    System.out.printf("%s Receive New Messages: %s %n",
                            Thread.currentThread().getName(), new String(msgs.get(0).getBody()));
                    String topic = msg.getTopic();
                    String body = new String(msg.getBody(), "utf-8");
                    String tags = msg.getTags();
                    String keys = msg.getKeys();
                    System.out.println("topic=" + topic + ", tags=" + tags + ",keys=" + keys + ", msg=" + body);
                    return ConsumeOrderlyStatus.SUCCESS;
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                    return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
                }
            }
        });

五、消费模式

  • Push和Pull优缺点分析
    • Push
      • 实时性高;但增加服务端负载,消费端能力不同,如果Push推送过快,消费端会出现很多问题
    • Pull
      • 消费者从Server端拉取消息,主动权在消费者端,可控性好;但 间隔时间不好设置,间隔太短,则空请求,浪费资源;间隔时间太长,则消息不能及时处理
    • 长轮询
      • Client请求Server端也就是Broker的时候, Broker会保持当前连接一段时间默认是15s,如果这段时间内有消息到达,则立刻返回给Consumer.没消息的话超过15s,则返回空,再进行重新请求;
      • 主动权在Consumer中,Broker即使有大量的消息 也不会主动提送Consumer,缺点:服务端需要保持Consumer的请求,会占用资源,需要客户端连接数可控否则会一堆连接
  • PushConsumer本质是长轮训
    • 系统收到消息后自动处理消息和offset,如果有新的Consumer加入会自动做负载均衡
    • 在broker端可以通过longPollingEnable=true来开启长轮询
    • 消费端代码:DefaultMQPushConsumerlmpl->pullMessage->PullCallback
    • 服务端代码:broker.longpolling
    • 虽然是push,但是代码里面大量使用了pul,是因为使用长轮训方式达到Push效果,既有pull有的,又有Push的实时性
    • 优雅关闭:主要是释放资源和保存Offset,调用shutdown(即可,参考 @PostConstruct@PreDestroy
  • PullConsumer需要自己维护Offset(参考官方例子)
    • 官方例子路径:org.apache.rocketmg.example.simple.PullConsumer
    • 获取MessageQueue遍历
    • 客户维护Offset,需用用户本地存储Offset,存储内存、磁盘、数据库等
    • 处理不同状态的消息 FOUND、NO NEW MSG、OFFSET ILLRGL.CNO MATCHED MSG、4种状态
    • 灵活性高可控性强,但是编码复杂度会高
    • 优雅关闭:主要是释放资源和保存Offset,需用程序自己保存好Offset,特别是异常处理的时候

五、offset、commitlog

一、offset偏移量

  • 什么是offset
    • message queue是无限长的数组,一条消息进来下标就会涨1,下标就是offset,消息在某个。MessageQueue里的位置,通过offset的值可以定位到这条消息,或者指示Consumer从这条消息开始向后处理
    • message queue中的maxOffset表示消息的最大offset,maxOffset并不是最新的那条消息的。offset,而是最新消息的offset+1,minOffset则是现存在的最小offset
    • fileReserveTime=48 默认消息存储48小时后,消费会被物理地从磁盘删除,message queue的min offset也就对应增长。所以比minOffset还要小的那些消息已经不在broker上了,就无法被消费
  • 类型(父类是OffsetStore):
    • 本地文件类型
      • DefaultMQPushConsumer的BROADCASTING模式,各个Consumer没有互相干扰,使用LoclaFileOffsetStore,把Offset存储在本地
    • Broker代存储类型
      • DefaultMQPushConsumer的CLUSTERING模式,由Broker端存储和控制Offset的值使用RemoteBrokerOffsetStore
  • 有什么用
    • 主要是记录消息的偏移量,有多个消费者进行消费
    • 集群模式下采用RemoteBrokerOffsetStore,broker控制offset的值
    • 广播模式下采用LocalFileOffsetStore,消费端存储
  • 建议采用pushConsumer,RocketMQ自动维护OffsetStore,如果用另外一种pullConsumer需要自己进行维护OffsetStore

二、commitlog消息存储

  • 消息存储是由ConsumeQueue和CommitLog配合完成
    • ConsumeQueue:是逻辑队列,CommitLog是真正存储消息文件的,存储的是指向物理存储的地址
    • Topic下的每个message queue都有对应的ConsumeQueue文件,内容也会被持久化到磁盘默认地址:store/consumequeue/ftopicName}/fqueueid}/fileName
    • 什么是CommitLog:
      • 消息文件的存储地址
      • 生成规则:
        • 每个文件的默认1G =1024*1024*1024,commitlog的文件名fileName,名字长度为20位,左边补零,剩余为起始偏移量;比如00000000000000000000代表了第一个文件,起始偏移量为0,文件大小为1G=1073 741824Byte;当这个文件满了,第二个文件名字为00000000001073741824,起始偏移量为1073741824,消息存储的时候会顺序写入文件,当文件满了则写入下一个文件判断消息存储在哪个CommitLog上
        • 例如 1073742827 为物理偏移量,则其对应的相对偏移量为 1003=1073742827-1073741824,并且该偏移量位于第二个 CommitLog
    • Broker里面一个Topic
      • 里面有多个MesssageQueue
      • 每个MessageQueue对应一个ConsumeQueue
      • ConsumeQueue里面记录的是消息在CommitLog里面的物理存储地址

六、分布式事务消息

一、事务消息

  • RocketMQ事务消息
    •  RocketMQ 提供分布事务功能,通过 RocketMQ 事务消息能达到分布式事务的最终一致
  • 半消息Half Message
    • 暂不能投递的消息(暂不能消费),Producer已经将消息成功发送到了Broker端,但是服务端未。收到生产者对该消息的二次确认,此时该消息被标记成“暂不能投递”状态,处于该种状态下的消息即半消息
  • 消息回查:
    • 由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,消息队列。RocketMQ 服务端通过扫描发现某条消息长期处于“半消息”时,需要主动向消息生产者询问该消息的最终状态(Commit 或是 Rollback),该过程即消息回查
  • 整体交互流程
    • Producer向broker端发送消息
    • 服务端将消息持久化成功之后,向发送方 ACK 确认消息已经发送成功,此时消息为半消息
    • 发送方开始执行本地事务逻辑
    • 发送方根据本地事务执行结果向服务端提交二次确认(Commit 或是 Rollback),服务端收。到 Commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;服务端收到 Rollback状态则删除半消息,订阅方将不会接受该消息
    • 在断网或者是应用重启的特殊情况下,上述步骤4提交的二次确认最终未到达服务端,经过固定时间后服务端将对该消息发起消息回查
    • 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果
    • 发送方根据检查得到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤4对半消息进行操作
  • RocketMQ事务消息的状态
    • COMMIT MESSAGE: 提交事务消息,消费者可以消费此消息
    • ROLLBACK MESSAGE:回滚事务消息,消息会在broker中删除,消费者不能消费
    • UNKNOW:Broker需要回查确认消息的状态
  • 关于事务消息的消费
    • 事务消息consumer端的消费方式和普通消息是一样的,RocketMQ能保证消息能被consumer收到(消息重试等机制,最后也存在consumer消费失败的情况,这种情况出现的概率极低

 二、代码实现

#生产者

package com.wsl.rocket.jms;

import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
import org.springframework.stereotype.Component;

import java.util.concurrent.*;

@Component
public class TransactionProducer {

    public static final String PRODUCER_GROUP = "TGroup";
    public static final String NAME_SERVER = "112.124.25.207:9876";
    TransactionMQProducer producer = null;
    //事务消息实现
    TransactionListener transactionListener = new TransactionListenerImpl();

    ExecutorService executorService = new ThreadPoolExecutor(2,
            5,
            100,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<Runnable>(2000),
            new ThreadFactory() {
                @Override
                public Thread newThread(Runnable r) {
                    Thread thread = new Thread(r);
                    thread.setName("client-transaction-msg-check-thread");
                    return thread;
                }
            });


    public TransactionProducer(){
        producer = new TransactionMQProducer(PRODUCER_GROUP);
        producer.setNamesrvAddr(NAME_SERVER);
        producer.setTransactionListener(transactionListener);
        producer.setExecutorService(executorService);
        //启动
        try {
            this.producer.start();
        } catch (MQClientException e) {
            e.printStackTrace();
        }
    }


    public void shutdown(){
        this.producer.shutdown();
    }


    public TransactionMQProducer getProducer(){
        return this.producer;
    }

}


#事务监听实现
public class TransactionListenerImpl implements TransactionListener{


    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        System.out.println("=========执行本地事务========");
        String body = new String(msg.getBody());
        String key = msg.getKeys();
        String transactionId = msg.getTransactionId();
        System.out.println("transactionId="+transactionId+",key="+key+",body="+body);
        //做对应的事务操作 todo

        //模拟事务不同状态
        int status = Integer.parseInt(arg.toString());
        if(status == 1){
            //提交事务
            return LocalTransactionState.COMMIT_MESSAGE;
        }
        if(status == 2){
            //回滚事务
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
        if(status == 3){
            //未知的,broker会回调
            return LocalTransactionState.UNKNOW;
        }
        return LocalTransactionState.UNKNOW;
    }

    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        System.out.println("===========broker事务查询回调==============");
        String body = new String(msg.getBody());
        String key = msg.getKeys();
        String transactionId = msg.getTransactionId();
        System.out.println("transactionId="+transactionId+",key="+key+",body="+body);
        //本地查询事务执行结果 todo
        return LocalTransactionState.COMMIT_MESSAGE;
    }
}
#消费者
package com.wsl.rocket.jms;

import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.*;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
import org.springframework.stereotype.Component;

import java.io.UnsupportedEncodingException;
import java.util.List;


@Component
public class Consumer {
    private DefaultMQPushConsumer pushConsumer;


    public static final String NAME_SERVER = "112.124.25.207:9876";
    public static final String GROUP = "CGroup";
    public static final String TOPIC = "test";

    public Consumer(){
        //设置消费者组
        this.pushConsumer = new DefaultMQPushConsumer(GROUP);
        //设置nameserver地址
        pushConsumer.setNamesrvAddr(NAME_SERVER);
        //设置消费偏移量offset
        pushConsumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
        //订阅主题,*代表所有tag标签
        try {
            pushConsumer.subscribe(TOPIC,"*");
        } catch (MQClientException e) {
            e.printStackTrace();
        }
        //监听函数
        pushConsumer.registerMessageListener(new MessageListenerConcurrently() {
            @Override
            public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
                try {
                    Message msg = msgs.get(0);
                    System.out.printf("%s Receive New Messages: %s %n",
                            Thread.currentThread().getName(), new String(msgs.get(0).getBody()));
                    String topic = msg.getTopic();
                    String body = new String(msg.getBody(), "utf-8");
                    String tags = msg.getTags();
                    String keys = msg.getKeys();
                    System.out.println("topic=" + topic + ", tags=" + tags + ",keys=" + keys + ", msg=" + body);
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                } catch (UnsupportedEncodingException e) {
                    e.printStackTrace();
                    return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                }
            }
        });
        //启动
        try {
            pushConsumer.start();
        } catch (MQClientException e) {
            e.printStackTrace();
        }
    }
}
#controller
package com.wsl.rocket.controller;

import com.wsl.rocket.jms.TransactionProducer;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.TransactionSendResult;
import org.apache.rocketmq.common.message.Message;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("trmq")
@RestController
public class TransactionMQController {

    @Autowired
    private TransactionProducer producer;

    public static final String TOPIC = "test";

    @GetMapping("sendMsg")
    public String sendMsg(@RequestParam("msg") String msg,
                          @RequestParam("otherParam") String otherParam)
            throws MQClientException {
        Message message = new Message(TOPIC, "tag_1","only_key", msg.getBytes());
        //发送事务消息  otherParam控制事务消息的提交还是回滚
        TransactionSendResult transactionSendResult =
                producer.getProducer().sendMessageInTransaction(message, otherParam);
        System.out.println(transactionSendResult);
        return "success";
    }
}

七、难点解决

一、消息重复

  • RocketMQ不保证消息不重复,如果你的业务需要保证严格的不重复消息,需要你自己在业务端去重
  • 接口幂等性保障,消费端处理业务消息要保持幂等性
  • 方案
    • redis setNx() / incr()
    • 数据库唯一索引
    • 代码做相关业务幂等操作

二、可靠传输

  • producer端
    • 不采用oneway发送,使用同步或者异步方式发送,做好重试,但是重试的Messagekey必须唯一
    • 投递的日志需要保存,关键字段,投递时间、投递状态、重试次数、请求体、响应体
  • broker端
    • 双主双从架构,NameServer需要多节点。
    • 同步双写、异步刷盘 (同步刷盘则可靠性更高,但是性能差点,根据业务选择)
  • consumer端
    • 消息消费务必保留日志,即消息的元数据和消息体
    • 消费端务必做好幂等性处理
  • 投递到broker端后
    • 机器断电重启:异步刷盘,消息丢失;同步刷盘消息不丢失
    • 硬件故障:可能存在丢失,看队列架构。

三、消息堆积

  • 临时topic队列扩容,并提高消费者能力,但是如果增加Consumer数量,但是堆积的topic里面的message queue数量固定,过多的consumer不能分配到message queue
  • 编写临时处理分发程序,从旧topic快速读取到临时新topic中,新topic的queue数量扩容多倍,然后再启动更多consumer进行在临时新的topic里消费

四、为啥rocketMq性能高

  • MQ架构配置
    • 顺序写,随机读,零拷贝
    • 同步刷盘SYNC_FLUSH和异步刷盘ASYNC_FLUSH,通过flushDiskType配置
    • 同步复制和异步复制,通过brokerRole配置,ASYNC MASTER,SYNC MASTER, SLAVE
    • 推荐同步复制(双写),异步刷盘
  • 发送端高可用
    • 双主双从架构:创建Topic对应的时候,MessageQueue创建在多个Broker上即相同的Broker名称,不同的brokerid(即主从模式);当一个Master不可用时,组内其他的Master仍然可用。
    • 但是机器资源不足的时候,需要手工把slave转成master,目前不支持自动转换,可用shell处理
  • 消费高可用
    • 主从架构:Broker角色,Master提供读写,Slave只支持读
    • Consumer不用配置,当Master不可用或者繁忙的时候,Consumer会自动切换到Slave节点进行能读取
  • 提高消息的消费能力
    • 并行消费
      • 增加多个节点
      • 增加单个Consumer的并行度,修改consumerThreadMin和consumerThreadMax
      • 批量消费,设置Consumer的consumerMessageBatchMaxSize,默认是1,如果为N,则消息多的时候,每次收到的消息为N条
    • 择 Linux Ext4 文件系统,Ext4 文件系统删除 1G 大小的文件通常耗时小于 50ms,而 Ext3文件系统耗时需要 1s,删除文件时磁盘IO 压力极大,IO 操作超时
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值