RocketMQ消息队列整理

106 阅读13分钟

消息队列梳理

一、系统中为什么使用消息队列

消息队列使用的场景主要的核心有三个:解耦、异步、削峰

解耦

  • 比方、之前项目中我负责模块遇到这样一个场景。A兼职系统用户报名兼职,会调用B商家报名单系统查看商家的报名单(招聘道具)是否充足,如果不足需要通知A兼职服务(mysql)C兼职搜索引擎(ES)系统暂停该商家下所有的招聘进行中的兼职,同时需要推送系统D将商家招聘道具不足进行推送、和短信系统E通知给商家手机上。最后将数据同步给CRM销售系统F进行同步。
  • 目前我来梳理下这个场景。A系统查询给B系统,B系统同步回调A、C、D、E、F五个系统,都是通过接口调用发送。现在会发生这样场景 新增G系统也需要这个数据?那如果D系统现在不需要了呢?B系统负责人几乎崩溃...... 同样也无法维护迭代的成本很大

动图1

  • 在上面这个场景中,B系统与其他的系统严重耦合,B系统反馈商家报名单不足比较的信息,很多系统都需要B系统将这个信息同步过去。B系统需要关注A、C、D、E、F这五个系统挂了怎么办。需要做事物补偿或者重发,比较复杂了。

  • 如果使用MQ,B系统产生一条数据,发送到MQ里面去,对应的系统需要数据自己去MQ里面消费。如果新系统E需要通知,直接从MQ里面消费即可;如果某个系统不需要这条数据了,就取消对MQ消息的消费即可。这样下来,B系统不需要去考虑要给谁发送数据,不需要维护这个代码,也不需要考虑人家是否调用成功、失败超时等情况。

动图1

  • 总结: 通过一个 MQ,Pub/Sub 发布订阅消息这么一个模型,B系统就跟其它系统彻底解耦了

异步

  • 在互联网中,对于用户直接的操作,要求都是在400ms以内处理完成,这样对用户的几乎是无感知的。
  • 使用MQ,那么A系统联系发送2条消息到MQ队列中,假设耗时5ms,A系统从接受一个请求到返回响应给用户,总时长是3+5=8ms,对于用户而言,就好像只点了一个按钮,8ms以后就直接返回了,第一反应就是这个APP或者网站做的真好 哈哈!!(ps: 反正我平时就是这么评价App的)

削峰

  • 正常情况下, 系统比较稳当。并发量并不是特别大,但是如果做活动或者第三方引流(支付宝引流), 每秒并发请求量会增到8k+, 但是系统存储是直接基于MYSQL的,大量的请求涌入到Mysql,每秒钟对MYSQL执行8K条SQL.
  • 一般情况下mysql扛到每秒2K并发量就到达瓶颈了。如果请求量直接打到8K,可能直接MYSQL挂掉了,导致整个系统崩溃。
  • 但是一般到了中午12:00~14:00,就达到一个低峰期。请求量比较少对系统几乎没有任何压力.

动图1

  • 使用MQ,当每秒8K个请求写入MQ,系统每秒最多处理2K个请求,主要是在Mysql瓶颈上面。系统从MQ中慢慢拉取请求,每秒就能拉取2k个请求。就算在高峰时候,系统也绝对不会挂掉。 MQ每秒钟5k个请求进来,其中2k个请求出去,积压的请求在高峰期过去后,系统会快速将积压的消息给解决掉。

动图1

  • 引入MQ后业务系统处理能力有限,将压力转嫁到MQ,消息堆积到MQ后可以慢慢进行消费. 动图1

二、消息队列有优缺点

优点

在特定场景下的好处,解耦、异步、削峰

缺点

系统的可用性降低

系统引入的外部依赖越多,越容易挂掉。本来B系统调用 CDEFG 四个系统的接口就好了,加个 MQ 进来,万一MQ挂了,整套系统崩溃。需要保证消息队列的高可用,在引入MQ之后是需要考虑的。

系统复杂度提高

MQ进来,需要保证消息没有重复消费?怎么处理消息丢失的情况?怎么保证消息传递的顺序性等等问题。

一致性问题

B系统处理完了直接返回成功了,以为这个请求就成功了;但是问题是,要是CDEFG四个系统那里,CDE三个系统写库成功了,结果FG系统写库失败了,这数据就不一致了。

三、RocketMQ基本概念

消息模型 (Message Model)

  • RocketMQ主要由 Producer、Broker、Consumer 三部分组成,其中Producer 负责生产消息,Consumer 负责消费消息,Broker 负责存储消息。Broker 在实际部署过程中对应一台服务器,每个Broker可以存储多个Topic的消息,每个Topic的消息也可以分片存储于不同的 Broker。Message Queue 用于存储消息的物理地址,每个Topic中的消息地址存储于多个 Message Queue 中。ConsumerGroup 由多个Consumer 实例构成。

主题 (Topic)

  • 表示一类消息的集合,每个主题包含若干条消息,每条消息只能属于一个主题,是RocketMQ进行消息订阅的基本单位。

广播消费 (Broadcasting)

  • 广播消费模式下,相同Consumer Group的每个Consumer实例都接收全量的消息。

普通顺序消息 (Normal Ordered Message)

  • 普通顺序消费模式下,消费者通过同一个消息队列( Topic 分区,称作 Message Queue) 收到的消息是有顺序的,不同消息队列收到的消息则可能是无顺序的。

严格顺序消息 (Strictly Ordered Message)

  • 严格顺序消息模式下,消费者收到的所有消息均是有顺序的。

消息 (Messsge)

  • 消息系统所传输信息的物理载体,生产和消费数据的最小单位,每条消息必须属于一个主题。RocketMQ中每个消息拥有唯一的Message ID,且可以携带具有业务标识的Key。系统提供了通过Message ID和Key查询消息的功能。

四、RocketMQ结构设计

架构图

动图1

说明

RocketMQ架构主要分为四个部分:

Producer

  • 消息发布的角色,支持分布式集群方式部署。Producer通过MQ的负载均衡模块选择相应的Broker集群队列进行消息投递,投递的过程支持快速失败并且低延迟。

Consumer

  • 消息消费的角色,支持分布式集群方式部署。支持以push推,pull拉两种模式对消息进行消费。同时也支持集群方式和广播方式的消费,提供实时消息订阅机制,满足大多数用户需求。

NameServer

  • NameServicer是一个非常简单的Topic路由注册中心,其角色相当于Dubbo中的zookeeper(SpringCloud中的Eureka), 支持Broker的动态注册与发现。主要包括两个功能: Broker管理,NameService接受Broker集群的注册信息并且保存下来作为路由信息的基本数据。然后提供心跳检测机制,检查Broker是否还存活;路由信息管理,每个NameService将保存关于Broker集群的整个路由信息和用于客户端查询的队列信息。然后Producer和Conumser通过NameServer就可以知道整个Broker集群的路由信息,从而进行消息的投递和消费。NameServer通常也是集群的方式部署,各实例间相互不进行信息通讯。Broker是向每一台NameServer注册自己的路由信息,所以每一个NameServer实例上面都保存一份完整的路由信息。当某个NameServer因某种原因下线了,Broker仍然可以向其它NameServer同步其路由信息,Producer, Consumer仍然可以动态感知Broker的路由的信息。

BrokerServer

Broker主要负责消息的存储、投递和查询以及服务高可用保证,为了实现这些功能,Broker包含了以下几个重要子模块。

  • Remoting Module:整个Broker的实体,负责处理来自clients端的请求。
  • Remoting Module:整个Broker的实体,负责处理来自clients端的请求。
  • Client Manager:负责管理客户端(Producer/Consumer)和维护Consumer的Topic订阅信息
  • Store Service:提供方便简单的API接口处理消息存储到物理硬盘和查询功能。
  • HA Service:高可用服务,提供Master Broker 和 Slave Broker之间的数据同步功能。
  • Index Service:根据特定的Message key对投递到Broker的消息进行索引服务,以提供消息的快速查询。

动图1

五、利用docker安装RocketMQ

安装namesrv

我们可以去docker官方网站,查找对应的rocketmq的镜像的地址。 hub.docker.com/r/rocketmqi…

查询有哪些可用的rocketmq

docker search rocketmq

拉取镜像

docker pull rocketmqinc/rocketmq

启动namesrv服务(新建文件路径/opt/mydata/rocketmq)

## /opt/mydata/rocketmq 目录根据实际服务器中创建文件存放的日志和数据的地方

docker run -d -p 9876:9876 -v /opt/mydata/rocketmq/logs:/root/logs -v /opt/mydata/rocketmq/data:/root/store --name rmqnamesrv -e "MAX_POSSIBLE_HEAP=100000000" 09bbc30a03b6 sh mqnamesrv

安装broker

创建配置文件 broker.conf

特别注意 ⚠️ brokerIP1,此处的坑比较大,设置的时候需要特别小心,对于mac电脑设置broker节点所在服务器的ip地址、物理ip,不能用127.0.0.1、localhost、docker内网ip 。可利用ifconfig的命令查看自己的电脑的服务的ip

  1 # 所属集群名称,如果节点较多可以配置多个
  2 brokerClusterName = DefaultCluster

  3 #broker名称,master和slave使用相同的名称,表明他们的主从关系
  4 brokerName = broker-a

  5 #0表示Master,大于0表示不同的slave
  6 brokerId = 0

  7 #表示几点做消息删除动作,默认是凌晨4点
  8 deleteWhen = 04

  9 #在磁盘上保留消息的时长,单位是小时
 10 fileReservedTime = 48

 11 #有三个值:SYNC_MASTER,ASYNC_MASTER,SLAVE;同步和异步表示Master和Slave之间同步数据的机制;
 12 brokerRole = ASYNC_MASTER

 13 #刷盘策略,取值为:ASYNC_FLUSH,SYNC_FLUSH表示同步刷盘和异步刷盘;SYNC_FLUSH消息写入磁盘后才返回成功
 14 状态,ASYNC_FLUSH不需要;
 15 flushDiskType = ASYNC_FLUSH

 16 # 设置broker节点所在服务器的ip地址、物理ip,不能用127.0.0.1、localhost、docker内网ip 
 17 brokerIP1 = 192.168.123.127                               

启动broker容器

docker run -d  -p 10911:10911 -p 10909:10909   -v /opt/mydata/rocketmq/logs:/root/logs -v /opt/mydata/rocketmq/data:/root/store  -v /opt/mydata/rocketmq/conf/broker.conf:/opt/rocketmq/conf/broker.conf  --name rmqbroker --link rmqnamesrv:namesrv -e "NAMESRV_ADDR=namesrv:9876" -e "JAVA_OPTS=-Duser.home=/opt" -e "JAVA_OPT_EXT=-server -Xms1024m -Xmx1024m"  rocketmqinc/rocketmq sh mqbroker -c /opt/rocketmq/conf/broker.conf

安装控制台 rocket-console

拉取rocket-console镜像

docker pull styletang/rocketmq-console-ng

运行rocket-console镜像

docker run -e "JAVA_OPTS=-Drocketmq.config.namesrvAddr=192.168.123.127:9876 -Drocketmq.config.isVIPChannel=false" -p 9993:8080 -t styletang/rocketmq-console-ng

暴露端口号:9876、8080

rocketmq控制台访问

gif动态图

动图1

六、SpringBoot引入RocketMq

引入maven库

    <dependency>
        <groupId>org.apache.rocketmq</groupId>
        <artifactId>rocketmq-spring-boot-starter</artifactId>
        <version>2.1.1</version>
    </dependency>
    <dependency>
        <groupId>org.apache.rocketmq</groupId>
        <artifactId>rocketmq-client</artifactId>
        <version>4.8.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.rocketmq</groupId>
        <artifactId>rocketmq-common</artifactId>
        <version>4.8.0</version>
    </dependency>

封装配置文件

package com.java.xval.val.common.config;

import com.java.xval.val.common.RocketMqHelper;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;


@ConditionalOnWebApplication
@Configuration(proxyBeanMethods = false)
public class RocketMqAutoConfiguration {

    @Bean
    public RocketMqHelper rocketMqHelper() {
        return new RocketMqHelper();
    }
}

配置启动的注解

package com.java.xval.val.common.constraints;

import com.java.xval.val.common.config.RocketMqAutoConfiguration;
import org.springframework.context.annotation.Import;

import java.lang.annotation.*;

/**
 * 开启RocketMq注解
 */
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Import({RocketMqAutoConfiguration.class})
public @interface EnableHyhRocketMq {
}

在SpringBoot启动类上添加注解启动

package com.java.xval.val;

import com.java.xval.val.common.constraints.EnableHyhRocketMq;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@EnableHyhRocketMq
public class ValApplication {

    public static void main(String[] args) {
        SpringApplication.run(ValApplication.class, args);
    }

}

封装RocketMq工具类

package com.java.xval.val.common;

import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.messaging.Message;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.annotation.Resource;


/**
 * 描述:
 * 〈RocketMq封装工具类〉
 *
 * @author fangyang
 * @since 2021-10-20
 */
public class RocketMqHelper {

    /**
     * 日志
     */
    private static final Logger LOG = LoggerFactory.getLogger(RocketMqHelper.class);

    /**
     * rocketmq模板注入
     */
    @Resource
    private RocketMQTemplate rocketMQTemplate;

    @PostConstruct
    public void init() {
        LOG.info("RocketMq开始实例化");
    }

    /**
     * 发送异步消息
     *
     * @param topic   消息Topic
     * @param message 消息实体
     */
    public void asyncSend(Enum topic, Message<?> message) {
        asyncSend(topic.name(), message, getDefaultSendCallBack());
    }


    /**
     * 发送异步消息
     *
     * @param topic        消息Topic
     * @param message      消息实体
     * @param sendCallback 回调函数
     */
    public void asyncSend(Enum topic, Message<?> message, SendCallback sendCallback) {
        asyncSend(topic.name(), message, sendCallback);
    }

    /**
     * 发送异步消息
     *
     * @param topic   消息Topic
     * @param message 消息实体
     */
    public void asyncSend(String topic, Message<?> message) {
        rocketMQTemplate.asyncSend(topic, message, getDefaultSendCallBack());
    }

    /**
     * 发送异步消息
     *
     * @param topic        消息Topic
     * @param message      消息实体
     * @param sendCallback 回调函数
     */
    public void asyncSend(String topic, Message<?> message, SendCallback sendCallback) {
        rocketMQTemplate.asyncSend(topic, message, sendCallback);
    }

    /**
     * 发送异步消息
     *
     * @param topic        消息Topic
     * @param message      消息实体
     * @param sendCallback 回调函数
     * @param timeout      超时时间
     */
    public void asyncSend(String topic, Message<?> message, SendCallback sendCallback, long timeout) {
        rocketMQTemplate.asyncSend(topic, message, sendCallback, timeout);
    }

    /**
     * 发送异步消息
     *
     * @param topic        消息Topic
     * @param message      消息实体
     * @param sendCallback 回调函数
     * @param timeout      超时时间
     * @param delayLevel   延迟消息的级别
     */
    public void asyncSend(String topic, Message<?> message, SendCallback sendCallback, long timeout, int delayLevel) {
        rocketMQTemplate.asyncSend(topic, message, sendCallback, timeout, delayLevel);
    }

    /**
     * 发送顺序消息
     *
     * @param message 消息实体
     * @param topic   消息Topic
     * @param hashKey 为了保证到同一个队列中,将消息发送到orderTopic主题上
     *                他的hash值计算发送到哪一个队列,用的是同一个值,那么他们的hash一样就可以保证发送到同一个队列里
     */
    public void syncSendOrderly(Enum topic, Message<?> message, String hashKey) {
        syncSendOrderly(topic.name(), message, hashKey);
    }


    /**
     * 发送顺序消息
     *
     * @param message 消息实体
     * @param topic   消息Topic
     * @param hashKey 为了保证到同一个队列中,将消息发送到orderTopic主题上
     *                他的hash值计算发送到哪一个队列,用的是同一个值,那么他们的hash一样就可以保证发送到同一个队列里
     */
    public void syncSendOrderly(String topic, Message<?> message, String hashKey) {
        LOG.info("发送顺序消息,topic:" + topic + ",hashKey:" + hashKey);
        rocketMQTemplate.syncSendOrderly(topic, message, hashKey);
    }

    /**
     * 发送顺序消息
     *
     * @param message 消息实体
     * @param topic   消息Topic
     * @param hashKey 为了保证到同一个队列中,将消息发送到orderTopic主题上
     *                他的hash值计算发送到哪一个队列,用的是同一个值,那么他们的hash一样就可以保证发送到同一个队列里
     * @param timeout 延时时间
     */
    public void syncSendOrderly(String topic, Message<?> message, String hashKey, long timeout) {
        LOG.info("发送顺序消息,topic:" + topic + ",hashKey:" + hashKey + ",timeout:" + timeout);
        rocketMQTemplate.syncSendOrderly(topic, message, hashKey, timeout);
    }

    /**
     * 默认CallBack函数
     *
     * @return SendCallback
     */
    private SendCallback getDefaultSendCallBack() {
        return new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                LOG.info("---发送MQ成功---");
            }

            @Override
            public void onException(Throwable throwable) {
                LOG.error("---发送MQ失败---" + throwable.getMessage(), throwable.getMessage());
            }
        };
    }


    @PreDestroy
    public void destroy() {
        LOG.info("---RocketMq注销---");
    }

}

添加配置文件

#rocketmq配置
rocketmq:
  name-server: localhost:9876
  # 生产者配置
  producer:
    isOnOff: on
    # 发送同一类消息的设置为同一个group,保证唯一
    group: hyh-rocketmq-group
    groupName: hyh-rocketmq-group
    # 服务地址
    namesrvAddr: localhost:9876
    # 消息最大长度 默认1024*4(4M)
    maxMessageSize: 4096
    # 发送消息超时时间,默认3000
    sendMsgTimeout: 3000
    # 发送消息失败重试次数,默认2
    retryTimesWhenSendFailed: 2

测试功能需求

消息发送测试代码如下(示例)

package com.java.xval.val.controller;


import com.java.xval.val.common.RocketMqHelper;
import com.java.xval.val.common.api.CommonResult;
import com.java.xval.val.model.Person;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
@RequestMapping(value = "/message/send")
public class RocketMqController {

    @Resource
    private RocketMqHelper rocketMqHelper;

    private static final Integer NUM = 10;

    /**
     * 发送异步消息
     *
     * @return the CommonResult
     */
    @RequestMapping(value = "/asyncSend", method = RequestMethod.GET)
    public CommonResult<String> asyncSend() {
        Person person = new Person();
        person.setName("Java开发");
        person.setAge(25);
        rocketMqHelper.asyncSend("PERSON_ADD", MessageBuilder.withPayload(person).build());
        return CommonResult.success("发送异步消息发送成功");
    }


    /**
     * 发送同步有序的消息.
     *
     * @return CommonResult
     */
    @RequestMapping(value = "/syncSendOrderly", method = RequestMethod.GET)
    public CommonResult<String> syncSendOrderly() {
        String message = "orderly message: ";
        for (int i = 0; i < NUM; i++) {
            // 模拟有序发送消息
            rocketMqHelper.syncSendOrderly("topic-orderly", MessageBuilder.withPayload(message + i).build(), "select_queue_key");
        }
        return CommonResult.success("同步有序消息发送成功");

    }
}

消息监听代码如下(示例)

异步消息接收代码
package com.java.xval.val.common.listener;

import com.java.xval.val.model.Person;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;


@Slf4j
@Component
@RocketMQMessageListener(consumerGroup = "${rocketmq.producer.groupName}", topic = "PERSON_ADD")
public class PersonMqListener implements RocketMQListener<Person> {

    @Override
    public void onMessage(Person person) {
        log.info("#PersonMqListener接收到消息,开始消费..name:" + person.getName() + ",age:" + person.getAge());
    }
}
同步有序的消息代码
package com.java.xval.val.common.listener;


import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.ConsumeMode;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.stereotype.Component;

@Slf4j
@Component
@RocketMQMessageListener(topic = "topic-orderly", consumerGroup = "orderly-consumer-group", consumeMode = ConsumeMode.ORDERLY)
public class OrderMqListener implements RocketMQListener<String> {

    @Override
    public void onMessage(String s) {
        log.info("#OrderMqListener接收到消息,开始消费.message={}:", s);
    }
}

请求示例图