RocketMQ 的使用

RocketMQ 的使用_第1张图片

一、RocketMQ 简介

1.1、组件概念

NameServer:RocketMQ的注册中心,管理集群的Topic-Queue的路由配置、Broker的实时配置信息。其它模块通过NameServer提供的接口获取最新的Topic配置和路由信息。各 NameServer 都有完整的路由信息,不互相通信。

Broker:负责接收并存储消息,同时提供Push/Pull接口来将消息发送给Consumer。可以通过MessageID和MessageKey来查询消息。Borker会将自己的Topic配置信息实时同步到NameServer。 
    角色:SYNC_MASTER(同步主机)、ASYNC_MASTER(异步主机)、及SLAVE(从机)。
        消息的可靠性要较高可采用:SYNC_MASTER、SLAVE 部署方式。
        消息可靠性要求不高可采用:ASYNC_MASTER、SLAVE 部署方式。
    刷新 FlushDiskType 
        SYNC_FLUSH(同步刷新)可靠,性能低。
        ASYNC_FLUSH(异步处理)性能高,不如同步可靠。

Topic:消息主题。
    Topic Partition:分区,物理上的概念,每个Topic包含一个或多个分区。
Offset:消费位点,每个分区当前消息的总条数为最大位点MaxOffset;每个分区的起始位置为起始位点MinOffset。

Group:一组生产者或消费者,这组生产者或消费者通常生产或消费同一组消息,消息发布或订阅的逻辑一致。
     Producer:消息发布者,发送消息至Broker
     Consumer:消息订阅者,从Broker接收并消费消息。
Queue:队列,消息主题有多个队列来存消息。
Message:消息载体.
    Message Key:消息业务标识,由生产者(Producer)设置,标识某个业务逻辑。
    Message ID:消息全局唯一标识,由消息队列RocketMQ系统自动生成,标识某条消息。
    Message Tag:消息标签,进一步区分某个Topic下的消息分类。
    消息类型:普通消息、顺序消息、事务消息、延迟消息

1.2、应用概念 

集群消费:一个Group的所有Consumer平均分摊消费消息。

广播消费:一个Group的所有Consumer都会各自消费某条消息一次。

定时消息:消息到RocketMQ,推迟到某个时间,进行消费,即为定时消息。

延时消息:消息到RocketMQ,延迟一定时间后,进行消费,即为延时消息。

事务消息:类似XA的分布事务功能,RocketMQ的能达到分布式事务的最终一致。

顺序消息:按照顺序进行发布和消费的消息类型,分为全局顺序、分区顺序。

全局顺序消息:对于指定的一个Topic,所有消息按照严格的先入先出(FIFO)的顺序进行发布和消费。

分区顺序消息:对于指定的一个Topic,所有消息根据Sharding Key进行区块分区。同一个分区内的消息按照严格的FIFO顺序进行发布和消费。

消息堆积:服务端保存着大量未被消费的消息,即消息堆积。

消息过滤:RocketMQ的服务端完根据消息标签(Tag)对消息进行过滤,Consumer只接收被过滤后的消息类型。

消息轨迹:消费处理过程中,各个节点的时间、地点等数据汇聚而成的完整链路信息。

重置消费位点:以时间轴为坐标,消息持久化存储的时间范围内(默认3天),重置Consumer对已订阅的Topic的消费进度,重置后Consumer将接到设定时间点之后由Producer发送到消息队列RocketMQ服务端的消息。

死信队列:死信队列用于处理无法被正常消费的消息,存储死信消息的特殊队列称为死信队列(Dead-Letter Queue)。

1.3、应用场景 

削峰填谷:如秒杀、抢红包、企业开门红等带来较高的流量脉冲,消息队列RocketMQ可提供削峰填谷的服务来解决该问题。

异步解耦:主业务完成后,RocketMQ实现异步通信与下游业务解耦,确保主站业务的连续性

顺序收发:与先进先出FIFO(First In First Out)原理类似,消息队列RocketMQ提供的顺序消息即保证消息FIFO。

分布式事务一致性:交易系统、支付红包等场景,RocketMQ可实现系统的解耦,又可保证最终的数据一致性。

RocketMQ 的使用_第2张图片

大数据分析:RocketMQ与流式计算引擎相结合,可实现业务数据的实时分析。

分布式缓存同步:RocketMQ构建分布式缓存,实时通知数据变化。

二、RocketMQ 工作流程、使用建议 

RocketMQ 的使用_第3张图片

2.1、RocketMQ 工作流程、存储结构 、消息清理 

2.1.1、RocketMQ 工作流程 

1、NameServer 启动,Broker 启动时向 NameServer 注册。
2、生产者在发送某个主题的消息之前先从 NamerServer 获取 Broker 服务器地址列表(可能是集群),然后根据负载均衡算法从列表中选择一台 Broker 进行消息发送。
3、NameServer 与每台 Broker 服务器保持长连接,并间隔 30S 检测 Broker 是否存活,如果检测到 Broker 宕机(使用心跳机制,如果检测超过 120S),则从路由注册表中将其移除。
4、消费者在订阅某个主题的消息之前从 NamerServer 获取 Broker 服务器地址列表(可能是集群),消费者选择从 Broker 中 订阅消息,订阅 规则由 Broker 配置决定。

RocketMQ 的使用_第4张图片

2.1.2、Broker 的存储结构 

本地文件存储系统,将所有topic的消息全写入同一个文件中(commit log),保证IO写入的顺序性。
由于消息混合存储在一起,要将每个消费者组消费topic最后的偏移量记录下来,就是consumer queue(索引文件),消息在写入commit log 文件时还将偏移量信息写入consumer queue文件。
索引文件中会记录消息的物理位置、偏移量offse,消息size等,消费者消费时根据上述信息就可以从commit log文件中快速找到消息信息。

RocketMQ 的使用_第5张图片

2.1.3、可用性 

消息分布在各个broker上,一旦某个broker宕机,该broker上的消息读写会受影响。
RocketMQ提供了master/slave的结构,master宕机,slave提供消费服务,但不能写入消息,此过程对应用透明,由rocketmq内部解决。

2.1.4、消息清理 

扫描间隔:默认10秒,由broker配置参数cleanResourceInterval决定
清理时机:默认每天凌晨4点,由broker配置参数deleteWhen决定;或者磁盘空间达到阈值
文件保留时长:默认72小时,由broker配置参数fileReservedTime决定
空间阈值:当磁盘空间达到阈值时,不再接受消息,broker打印出日志,消息发送失败,阈值为固定值85%

2.2、使用建议

RocketMQ 的使用_第6张图片

RocketMQ 的使用_第7张图片

2.2.1、实践(版本5.0)

生产者 

1.Tag的使用 
一个应用尽量用一个Topic,子类型则可用tags标识。消费方订阅消息时用tags通过broker过滤,5.x SDK 可用messageBuilder.setTag(“messageTag”)。

2.Keys的使用 
每个消息在业务层面设置唯一标识到keys字段,方便定位消息丢失问题。

3.日志的打印 
消息发送成功或者失败要打印消息日志,用于业务排查问题。

4.消息发送失败处理方式 
Producer的send方法本身支持内部重试,5.x SDK的重试逻辑参考发送重试策略:

消费者 

消费过程幂等
    RocketMQ无法避免消息重复(消费者主动重发、因客户端重投机制导致的重复等),可以借助关系数据库去重。
消费速度慢的处理方式
    1.提高消费并行度
    可以通过加机器,或者在已有机器启动多个线程,5.x PushConsumer SDK 可以通过 PushConsumerBuilder.setConsumptionThreadCount();设置线程数。
    
    2.批量方式消费
    某些业务流程如果支持批量方式消费,可大程度提高消费吞吐量,5.xSDK的SimpleConsumer,每次接口调用设置批次大小,一次性拉取消费多条消息。
    
    3.重置位点跳过非重要消息
    发生消息堆积时,消费速度追不上发送速度,业务数据要求不高,可丢弃不重要的消息。用重置位点功能直接调整消费位点到指定时刻或者指定位置。
    
    4.优化每条消息消费过程

三、项目中的使用

3.1、依赖与配置

1.引入依赖


	org.apache.rocketmq
	rocketmq-spring-boot-starter
	2.2.3

2.配置参数

rocketmq:
    # 服务地址,多个用逗号分开
    name-server: 10.5.103.6:9876
    consumer:
        group: springboot_consumer_group
        # 一次拉取消息最大值,注意是拉取消息的最大值而非消费最大值
        pull-batch-size: 10
    producer:
        # 发送同一类消息的设置为同一个group,保证唯一
        group: springboot_producer_group
        # 发送消息超时时间,默认3000
        sendMessageTimeout: 10000
        # 发送消息失败重试次数,默认2
        retryTimesWhenSendFailed: 2
        # 异步消息重试此处,默认2
        retryTimesWhenSendAsyncFailed: 2
        # 消息最大长度,默认1024 * 1024 * 4(默认4M)
        maxMessageSize: 4096
        # 压缩消息阈值,默认4k(1024 * 4)
        compressMessageBodyThreshold: 4096
        # 是否在内部发送失败时重试另一个broker,默认false
        retryNextServer: false

3.其他配置 

Name Server相关配置:
        rocketmq.namesrv.domain:Name Server的域名,用于自动发现Name Server地址。

Producer相关配置:
        rocketmq.producer.group:生产者组名,用于标识一组具有相同功能的生产者。
        rocketmq.producer.send-msg-timeout:发送消息的超时时间,默认为3秒。
        rocketmq.producer.compress-msg-body-over-howmuch:消息体大于指定字节大小时启用压缩。

Consumer相关配置:
        rocketmq.consumer.group:消费者组名,用于标识一组具有相同功能的消费者。
        rocketmq.consumer.consume-thread-min:消费者线程池的最小线程数。
        rocketmq.consumer.consume-thread-max:消费者线程池的最大线程数。
        rocketmq.consumer.consume-message-batch-max-size:批量消费消息时每次拉取的最大消息数量。
        rocketmq.consumer.pull-interval:拉取消息间隔时间,默认为0,表示尽可能快地拉取消息。

Message相关配置:
        rocketmq.message.max-size:消息的最大大小,默认为4MB。
        rocketmq.message.compress-level:消息压缩级别,可选值为0(不压缩)到9(最高压缩率)。
        rocketmq.message.timeout:消息的过期时间,默认为3天。

集群模式相关配置:
        rocketmq.broker.cluster.name:Broker集群的名称。
        rocketmq.broker.cluster.slave-read-only:Slave节点是否只读,默认为true。

3.2、生产者 

import com.alibaba.fastjson.JSON;
import com.south.spring.note.User;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.client.producer.TransactionSendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.apache.rocketmq.spring.support.RocketMQHeaders;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;

import java.time.LocalDate;
import java.time.ZoneId;
import java.util.List;

@Slf4j
@Component
public class MQProducerService {

    @Value("${rocketmq.producer.send-message-timeout}")
    private Integer messageTimeOut;

    // 正常规模项目统一用一个 TOPIC
    private static final String topic = "JUN_TEST_TOPIC";

    // 标签1
    private static final String tag1 = ":tag1";

    // 标签2
    private static final String tag2 = ":tag2";

    // 标签3
    private static final String tag3 = ":tag3";

    // 标签4
    private static final String tag4 = ":tag4";

    // 标签5
    private static final String tag5 = ":tag5";
    // 直接注入使用,用于发送消息到 broker 服务器

    @Autowired
    private RocketMQTemplate rocketMQTemplate;

    /**
     * 普通发送
     */
    public void send(User user) {
        rocketMQTemplate.convertAndSend(topic + tag1, user);
//      rocketMQTemplate.send(topic + ":tag1", MessageBuilder.withPayload(user).build()); // 等价于上面一行
    }

    /**
     * 同步方法
     * syncSend() 方法会阻塞当前线程。  成功返回 SendResult 对象,包含了消息的发送状态、消息ID等信息;失败 syncSend 会抛出 MessagingException 异常。
     */
    public SendResult sendMsg(String msgBody) {
        SendResult sendResult = rocketMQTemplate.syncSend(topic+tag1, MessageBuilder.withPayload(msgBody).build());
        log.info("【sendMsg】sendResult={}", JSON.toJSONString(sendResult));
        return sendResult;
    }

    /**
     * 异步方法
     * 不会阻塞当前线程,asyncSend方法会立即返回。如果需要等待消息发送完成并处理发送结果,可以使用SendCallback回调接口。
     */
    public void sendAsyncMsg(String msgBody) {
        rocketMQTemplate.asyncSend(topic+tag1, MessageBuilder.withPayload(msgBody).build(), new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                // 处理消息发送成功逻辑
            }
            @Override
            public void onException(Throwable throwable) {
                // 处理消息发送异常逻辑
            }
        });
    }

    /**
     * 单向消息(只负责发送消息,不等应答,不关心结果)
     */
    public void sendOneWayMsg(String msgBody) {
        rocketMQTemplate.sendOneWay(topic+tag1, MessageBuilder.withPayload(msgBody).build());
    }

    /**
     * 单向顺序消息
     */
    public void sendOneWayOrderlyMsg(String msgBody,String id){
        rocketMQTemplate.sendOneWayOrderly(topic+tag2, MessageBuilder.withPayload(msgBody).build(),id);
    }

    /**
     * 同步顺序消息
     */
    public void sendSyncOneWayOrderlyMsg(String msgBody,String id){
        SendResult sendResult =  rocketMQTemplate.syncSendOrderly(topic+tag2, MessageBuilder.withPayload(msgBody).build(),id);
        log.info("【sendSyncOneWayOrderlyMsg】sendResult={}", JSON.toJSONString(sendResult));
    }

    /**
     * 异步顺序消息
     */
    public void sendAsyncOneWayOrderlyMsg(String msgBody,String id){
        rocketMQTemplate.asyncSendOrderly(topic+tag2, MessageBuilder.withPayload(msgBody).build(),id, new SendCallback() {
            @Override
            public void onSuccess(SendResult sendResult) {
                log.info(sendResult.toString());
            }

            @Override
            public void onException(Throwable e) {
                log.info(e.getMessage());
            }
        });
    }

    /**
     * 延时消息
     */
    public void syncSendDelayTimeSecondsMsg(String msgBody, int delayLevel) {
        // 秒级
        SendResult sendResult = rocketMQTemplate.syncSendDelayTimeSeconds(topic+tag3,msgBody, delayLevel);
        // 毫秒级
        // SendResult sendResult =rocketMQTemplate.syncSendDelayTimeMills(topic+tag2,msgBody, delayLevel);
        log.info("【syncSendDelayTimeSecondsMsg】sendResult={}", JSON.toJSONString(sendResult));
    }

    /**
     * 定时消息
     */
    public void syncSendDeliverTimeMillsMsg(String msgBody) {
        // 每天凌晨处理
        long time = LocalDate.now().atStartOfDay().atZone(ZoneId.systemDefault()).toInstant().toEpochMilli();
        SendResult sendResult = rocketMQTemplate.syncSendDeliverTimeMills(topic+tag3,msgBody, time);
        log.info("【syncSendDelayTimeSecondsMsg】sendResult={}", JSON.toJSONString(sendResult));
    }

    /**
     * 批量发送
     */
    public void syncSendBatchMessage(List msgs) {
        SendResult sendResult = rocketMQTemplate.syncSend(topic+tag4,msgs);
        log.info("【syncSendBatchMessage】sendResult={}", JSON.toJSONString(sendResult));
    }

    /**
     * 事务消息
     */
    public void sendMessageInTransactionMsg(String msgBody) {
        Message msgs = MessageBuilder.withPayload(JSON.toJSONString(msgBody))
                .setHeader("KEYS", msgBody)
                //设置事务ID
                .setHeader(RocketMQHeaders.TRANSACTION_ID,"KEY_"+msgBody)
                .build();
        TransactionSendResult transactionSendResult = rocketMQTemplate.sendMessageInTransaction(topic+tag5,msgs,null);
        log.info("【sendMessageInTransactionMsg】transactionSendResult={}", JSON.toJSONString(transactionSendResult));
    }
}

3.3、消费者 与 事务监听 

@Slf4j
@Component
public class MQConsumerService {

    /**
	 * topic要和生产者topic一致;consumerGroup 必须指定;selectorExpression 是tag,默认为“*”,不设置会监听所有消息。
	 */    
	@Service
    @RocketMQMessageListener(topic = "JUN_TEST_TOPIC", selectorExpression = "tag1", consumerGroup = "Con_Group_One")
    public class ConsumerSend implements RocketMQListener {
        // 监听到消息就会执行此方法
        @Override
        public void onMessage(User user) {
            log.info("监听到消息:user={}", JSON.toJSONString(user));
        }
    }
	
	// MessageExt:是一个消息接收通配符,不管发送的是String还是对象,都可接收。
    @Service
    @RocketMQMessageListener(topic = "JUN_TEST_TOPIC", selectorExpression = "tag2", consumerGroup = "Con_Group_Three")
    public class Consumer implements RocketMQListener {
        @Override
        public void onMessage(MessageExt messageExt) {
            byte[] body = messageExt.getBody();
            String msg = new String(body);
            log.info("监听到消息:msg={}", msg);
        }
    }
}


import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionListener;
import org.apache.rocketmq.spring.core.RocketMQLocalTransactionState;
import org.apache.rocketmq.spring.support.RocketMQHeaders;
import org.springframework.messaging.Message;
import org.springframework.stereotype.Component;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;


/**
 * 事物消息 Producer 事务监听器
 */
@Slf4j
@Component
@RocketMQTransactionListener()
public class TransactionListenerImpl implements RocketMQLocalTransactionListener {

    private AtomicInteger transactionIndex = new AtomicInteger(0);

    private ConcurrentHashMap localTrans = new ConcurrentHashMap();

    /**
     * 发送 prepare 消息成功此方法被回调,该方法用于执行本地事务
     * @param msg  回传的消息,利用transactionId即可获取到该消息的唯一Id
     * @param arg  调用send方法时传递的参数,当send时候若有额外的参数可以传递到send方法中,这里能获取到
     * @return     返回事务状态,COMMIT:提交  ROLLBACK:回滚  UNKNOW:回调
     */
    @Override
    public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        //事务ID
        String transId = (String) msg.getHeaders().get(RocketMQHeaders.TRANSACTION_ID);
        int value = transactionIndex.getAndIncrement();
        int status = value % 3;
        assert transId != null;
        localTrans.put(transId, status);
        if (status == 0) {
            log.info("success");
            //成功,提交事务
            return RocketMQLocalTransactionState.COMMIT;
        }
        if (status == 1) {
            log.info("failure");
            //失败,回滚事务
            return RocketMQLocalTransactionState.ROLLBACK;
        }
        log.info("unknown");
        //中间状态
        return RocketMQLocalTransactionState.UNKNOWN;
    }

    /**
     * 检查事务状态
     */
    @Override
    public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
        String transId = (String) msg.getHeaders().get(RocketMQHeaders.TRANSACTION_ID);
        RocketMQLocalTransactionState retState = RocketMQLocalTransactionState.COMMIT;
        Integer status = localTrans.get(transId);
        if (null != status) {
            switch (status) {
                case 0:
                    retState = RocketMQLocalTransactionState.COMMIT;
                    break;
                case 1:
                    retState = RocketMQLocalTransactionState.ROLLBACK;
                    break;
                case 2:
                    retState = RocketMQLocalTransactionState.UNKNOWN;
                    break;
                default: break;
            }
        }
        log.info("msgTransactionId:{},TransactionState:{},status:{}",transId,retState,status);
        return retState;
    }
}

3.4、事务 

RocketMQ 的使用_第8张图片

1、应用模块遇到要发送事务消息的场景时,先发送prepare消息给MQ。
2、prepare消息发送成功后,应用模块执行数据库事务(本地事务)。
3、根据数据库事务执行的结果,再返回Commit或Rollback给MQ。
4、如果是Commit,MQ把消息下发给Consumer端,如果是Rollback,直接删掉prepare消息。
5、第3步的执行结果如果没响应,或是超时的,启动定时任务回查事务状态(最多重试15次,超过了默认丢弃此消息),处理结果同第4步。
6、MQ消费的成功机制由MQ自己保证。

四、常见一些问题 

4.1、重复消费 

RocketMQ 的使用_第9张图片

 要根据业务场景合理的设计幂等技术方案 

4.2、顺序消费 

RocketMQ 的使用_第10张图片

MessageListenerOrderly 通过分布式锁、本地锁保证同时只有一条线程去消费一个队列。
1.broker端的分布式锁
集群模式下一个消息队列(messageQueue)同一时刻只能被同一个消费组(consumerGroup)下的某一个消费者(consumerClient)消费,
consumerClient 向 Borker 申请锁 ,成功,则拉取消息;失败,则定时任务每隔20秒会重新尝试。

2.MessageQueue 的本地锁 synchronized
消费者在处理拉取消息时,可开启多线程处理,处理消息前要对MessageQueue加锁,保证同一时刻对于同一个队列只有一个线程去消费。

3.ProcessQueue 的本地锁 consumeLock(ReentrantLock)
防止在消费消息的过程中,该消息队列因负载均衡分配给其他客户端,导致的两个客户端重复消费。

4.3、消息丢失

1.生产者发送消息,由于网络故障或broker的master节点宕机,导致消息丢失。
2.消息已经发送到RocketMQ,消息暂存内存中,服务宕机,导致消息丢失。
3.消息已经发送到RocketMQ,消费者未消费完就返回ack,此时消费者宕机,导致消息丢失。

RocketMQ 的使用_第11张图片

 

1.Producer 生产阶段
Broker 收到消息后,返回确认响应信息给 Producer。
生产者接收到返回的确认响应,代表消息在生产阶段未丢失。
2.Broker 存储阶段
Broker 默认异步刷盘(500ms)(消息持久化),修改 Broker 配置为同步刷盘(10ms刷盘一次),消息存储磁盘成功,才会返回响应。

## 默认情况为 ASYNC_FLUSH 
flushDiskType = SYNC_FLUSH 

3.消费阶段
消费者执行成功后,将返回 ConsumeConcurrentlyStatus.CONSUME_SUCCESS 状态给 Broker。Broker 未收到消费确认响应,消费者会再次拉取到该条消息,进行重试。

//注册消息监听器处理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
   @Override
    public ConsumeConcurrentlyStatus consumeMessage(List msgs, ConsumeConcurrentlyContext context){ 
     //开启子线程异步处理消息
     new Thread() {
   public void run() {
    //对消息进行处理
   }
  }.start();                                 
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }
});

4.4、死信消息 

 消费时,如果抛出了异常,会重新再次投递给该消费者。
重试次数达到默认的16次后(可通过配置文件修改)如果对应的消息还没被成功消费的话,该消息会投递到DLQ死信队列。

RocketMQ 的使用_第12张图片


可在控制台Topic列表中看到"DLQ"相关的Topic,默认命名是:%RETRY%消费组名称(重试Topic)%DLQ%消费组名称(死信Topic)。
死信队列也可以被订阅和消费,有效期与正常消息相同,均为 3 天,3 天后会被自动删除。

RocketMQ 的使用_第13张图片

4.5、集群通信与故障 

1.每个 NameServer 都保存着 Broker 集群的所有 Broker 信息,一台NameServer服务器宕机了,其它NameServer可用。
2.每个Broker节点都是主从架构,所以就算主节点宕掉了,从节点可提供服务。
3.RocketMQ以4.5版本后,RocketMQ引入了 Dleger机制 ,采用Raft协议进行主从节点的选举,实现故障自动转移。
4.RocketMQ每个Broker节点只保存整体数据的一部分,当数据量越来越大时,通过数据分散集群的模式实现水平扩展。RabbitMQ中的每个节点保存着全量数据,没法水平扩展。

RocketMQ 的使用_第14张图片

你可能感兴趣的:(rocketmq)