为什么是RocketMQ & Spring Boot集成RocketMQ

为什么是RocketMQ

RocketMQ主要是针对online服务,提供可靠存储的消息中间件

持久化 & 多副本

RocketMQ可配置为 3副本、异步复制、异步刷盘,类比kafka的的replica_factor=3, acks=leader;在这种配置之下,可靠性99.99%,可用性99.95%。同时可以针对特殊场景,提供同步刷盘、同步复制的集群,提供更高的可靠性,在6副本、同步复制、同步刷盘的配置下,消息可靠性可达9个9。相应的,写入延时会升高,qps、availability会降低。RocketMQ的消息默认存储时间也可配置。

消息重试(Requeue)

kafka设计之初主要作为 log (特指类似binlog这种WAL)的传输总线,同一个partition内的有序是默认需求,这意味着消费失败的消息是不能重入的,否则消息的顺序性就被破坏了;用户需要自己打日志或者转储来处理消费失败的消息。
而在MQ领域,可能大多数场景下其实并不需要严格的顺序,对于消费失败的消息(可能是消费者下游短时间不可用、load高等),这种情况下用户希望消息能够重新投递到MQ,可以不用额外写代码处理错误的消息。RocketMQ 在非有序消费模式下,支持消息重试,类比NSQ的requeue机制,并且重试的消息具有和普通消息一样的持久化支持。

死信队列 (Dead Letter Queue)

当消息多次重试仍然消费失败,这种情况多半是消费者逻辑有问题,比如对于消息的某些字段没有兼容等;需要有地方能够转储这一批消息。RocketMQ提供了死信队列,消费者可以指定消息最大重试次数,当消息重试超过该次数,消息将会发往死信队列。待消费者的问题解决之后,可以从死信队列拉取这些消息统一处理。

延时消息

一些场景下,生产者发送消息成功后,希望delay一段时间消息再投递给消费者,比如创建一个订单之后30分钟内未支付则取消订单的场景。
NSQ支持ms级别精度的延时消息,其实现为内存里的一个priority queue,没有持久化;kafka不支持延时消息;RocketMQ支持分级的时延,比如支持10s、30s、1min、5min、10min、15min、20min、30min等多个级别时延,足够覆盖大部分场景,并且有持久化。
需要任意时延的延迟消息用户也可以支持。

写入延迟保障

RocketMQ和kafka都是顺序写盘,充分利用page cache以获得低时延和吞吐。kafka的存储模型为每个partition都由若干segment(物理文件)组成的逻辑文件,当kafka集群上的topic/partition数量多了之后,kafka性能会快速劣化,顺序写也可能会逐渐退化到随机写(简单来水就是,kafka对于单个partition是append only的,但是一块盘上可能有若干partition,多个partition整体看不是append only)(参考:如何缓解Kafka集群在有大量topic时性能快速劣化的问题?),写入延迟上涨;只能拆分集群,减少单集群上topic/partition数量。
而RocketMQ的存储模型与kafka不同,所有消息都写到一个CommitLog中即返回,再异步将offset、length信息dispatch到各Consume Queue中,所以是严格的顺序写,在topic数量很多的时候,page fault仍然维持在低水平,写入时延较为稳定。
在异步复制、异步刷盘下,写入延时的avg为10ms,pct99为20ms

基于时间回溯

比如30分钟前消费者下游故障,期间消费者全部异常,用户希望将消费进度回拨到30min前,达到”补"消息的效果。kafka需要集群版本在0.10以上才能支持,RocketMQ默认支持。

顺序/乱序消费

RocketMQ同时提供了顺序和乱序消费。 顺序的保障是,消息写到broker上的一个Consume Queue的顺序和消费者从该Consume Queue读取的顺序是一致的,多个producer并发写到一个Consume Queue的状况顺序没有保障,实际的顺序以broker收到的顺序为准。
一般有序消息都是同步发送的,也即msg n发送成功并且client收到写入成功的结果之后才会发送msg n+1,异步/并发的有序消息是个伪需求。
对于消息没有顺序要求的场景,可以使用乱序消费,并发度为消费者实例数 * 线程/协程数。

广播消费(TBD)

即同一个consumer group内的所有消费者每一个实例都能收到全量消息;可以通过每个消费者使用独立的Consumer group达到效果。

事务消息 (TBD)

事务消息指:Producer发消息和Producer的其他操作(如写DB)形成事务,如果其他操作Commit,则消息Consumer可见;如果其他操作Rollback,则消息也不会投递给Consumer。
原生的RocketMQ的java SDK支持事务消息,现阶段其他语言暂不支持,后续可能会支持。



Spring Boot集成RocketMQ

  • POM依赖
  • 生产者(子Topic区分不同消息类型)
  • 消费者(启动多个消费者,订阅不同的子Topic)

代码

https://github.com/zc-zangchao/multiple-data-source

POM依赖

        <springboot.version>2.2.0.RELEASE</springboot.version>
        <rocketmq-client.version>4.5.1</rocketmq-client.version>
        <rocketmq.version>2.0.2</rocketmq.version>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter</artifactId>
                <version>${springboot.version}</version>
            </dependency>
             <!-- rocket mq -->
            <dependency>
                <groupId>org.apache.rocketmq</groupId>
                <artifactId>rocketmq-client</artifactId>
                <version>${rocketmq-client.version}</version>
            </dependency>
            <dependency>
                <groupId>org.apache.rocketmq</groupId>
                <artifactId>rocketmq-spring-boot-starter</artifactId>
                <version>${rocketmq.version}</version>
            </dependency>

生产者

package com.springboot.demo.mq.mq;

import com.springboot.demo.mq.busi.type.EnumBusinessType;
import com.springboot.demo.mq.mq.constants.Constants;
import com.springboot.demo.mq.mq.utils.MqUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.client.producer.SendStatus;
import org.apache.rocketmq.common.message.Message;
import org.springframework.stereotype.Service;

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


/**
 * 异步消息发送
 **/
@Service
@Slf4j
public class MqProducer {

    private static final String PRODUCER_INSTANCE_NAME = Constants.MQ_PRODUCER_INSTANCE;

    private static final String MSG_KEY_CONNECT = "-";

    private static DefaultMQProducer producer;

    private String rocketMQNameServer = Constants.MQ_SERVER;

    private String producerGroupName = Constants.MQ_PRODUCER_GROUP;

    private String topic = Constants.MQ_TOPIC;

    @PostConstruct
    public void init() {
        try {
            log.info("Start mq producer begin");
            producer = new DefaultMQProducer();
            producer.setProducerGroup(producerGroupName); // 设置MQ生产者组名
            log.info("Mq.name.server {}", rocketMQNameServer);
            producer.setNamesrvAddr(rocketMQNameServer);
            producer.setInstanceName(PRODUCER_INSTANCE_NAME);
            producer.start();
            log.info("Start mq producer end");
        } catch (Exception e) {
            log.error("Mq loading error!", e);
            throw new RuntimeException("Rocket mq producer start failed");
        }
    }

    @PreDestroy
    public void stop() {
        log.info("Shutdown mq producer!");
        producer.shutdown();
    }

    /**
     * 发送不保证顺序性的消息
     *
     * @param key 消息的key(用来判重)
     * @param msg 消息的内容
     * @param delayTimeLevel 时间延迟级别
     * @return true:处理成功,不需要重发 false:处理失败, 暂不重发
     */
    public boolean sendMessage(EnumBusinessType msgTag, String key, String msg, int delayTimeLevel) {
        log.info("Send mq[topic={} msgTag={} key={} msg={} delayTimeLevel={}]", topic, msgTag, key, msg, delayTimeLevel);
        byte[] msgBytes = MqUtils.convertStrToByteArr(msg);
        boolean result = sendMessage(topic, msgTag, key, msgBytes, delayTimeLevel);
        if (!result) {
            log.error("[WARN]send mq-msg fail!, topic:{}, tag:{}, key:{}, serialize msg:{}", topic, msgTag, key, msg);
        }
        return result;
    }

    /**
     * 发送不保证顺序性的消息
     *
     * @param topic 消息的topic
     * @param msgTag 消息的tag
     * @param key 消息的key(用来判重)
     * @param msgBytes 消息的内容
     * @return true:处理成功 false:处理失败,暂不考虑重发
     */
    private boolean sendMessage(String topic, EnumBusinessType msgTag, String key, byte[] msgBytes, int delayTimeLevel) {
        String msgKey = topic + MSG_KEY_CONNECT + key;
        try {
            Message rocketMsg = new Message(topic, String.valueOf(msgTag), msgKey, msgBytes);
            rocketMsg.setDelayTimeLevel(delayTimeLevel);
            SendResult sendResult = producer.send(rocketMsg);
            if (sendResult.getSendStatus() == SendStatus.SEND_OK) {
                log.info("Send msg status success! topic:{}, tag:{}, key:{}", topic, msgTag, msgKey);
            } else {
                log.warn("Send msg status fail! status:{}, topic:{}, tag:{}, key:{}, msgID:{}", sendResult.getSendStatus(),
                        topic, msgTag, key, sendResult.getMsgId());
                return false;
            }
        } catch (Exception e) {
            log.error("Send msg occurs exception! topic:{}, tag:{}, key:{}", topic, msgTag, key, e);
            return false;
        }
        return true;
    }

}


消息类型(子Topic)

package com.springboot.demo.mq.busi.type;

public enum EnumBusinessType {
    LOGIN,

    LOGIN_OUT;
}

常量类

package com.springboot.demo.mq.mq.constants;

public final class Constants {

    public static final String MQ_SERVER = "127.0.0.1:9876";

    public static final String MQ_PRODUCER_INSTANCE = "PRODUCER_INSTANCE_DEMO";

    public static final String MQ_PRODUCER_GROUP = "PRODUCER_GROUP_DEMO";

    public static final String MQ_TOPIC = "TOPIC_DEMO";

    public static final String MQ_CONSUMER_PREFIX = "CONSUMER_DEMO_";

    private Constants() {}
}

工具类

package com.springboot.demo.mq.mq.utils;

import lombok.extern.slf4j.Slf4j;

import java.io.UnsupportedEncodingException;

@Slf4j
public final class MqUtils {

    private static final String DEFAULT_CHARSET = "UTF-8";

    private MqUtils() {
    }

    // 字符串转byte[]
    public static byte[] convertStrToByteArr(String str) {
        try {
            return str == null ? new byte[0] : str.getBytes(DEFAULT_CHARSET);
        } catch (UnsupportedEncodingException e) {
            log.error("Convert string to byte array fail, str is {}", str, e);
        }
        return new byte[0];
    }

    // byte[]转字符串
    public static String convertByteArrToStr(byte[] bytes) {
        return bytes == null ? "" : convertByteArrNotNull(bytes);
    }

    private static String convertByteArrNotNull(byte[] bytes) {
        try {
            return new String(bytes, DEFAULT_CHARSET);
        } catch (UnsupportedEncodingException e) {
            log.error("Convert byte array to string fail.", e);
        }
        return "";
    }

}

消费者

package com.springboot.demo.mq.mq;

import com.springboot.demo.mq.busi.type.EnumBusinessType;
import com.springboot.demo.mq.mq.constants.Constants;
import com.springboot.demo.mq.mq.listener.AbstractMqListener;
import com.springboot.demo.mq.mq.listener.impl.UserLoginListener;
import com.springboot.demo.mq.mq.listener.impl.UserLoginOutListener;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.MQPushConsumer;
import org.apache.rocketmq.client.exception.MQClientException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.ArrayList;
import java.util.List;


/**
 * 消息消费
 **/
@Service
@Slf4j
public class MqConsumer {

    private static List<MQPushConsumer> consumers = new ArrayList<>();

    private String rocketMQNameServer = Constants.MQ_SERVER;

    private String consumerGroupNamePrefix = Constants.MQ_CONSUMER_PREFIX;

    private String topic = Constants.MQ_TOPIC;

    /**
     * 需要确保依赖的所有的子类Listener在本类初始化之前初始化完成
     */

    @Autowired
    private UserLoginListener userLoginListener;

    @Autowired
    private UserLoginOutListener userLoginOutListener;

    @PostConstruct
    public void init() {
        try {
            log.info("Start mq consumers begin, Mq.name.server {}.", rocketMQNameServer);

            // 启动多个consumer(对应多个tag)
            for (EnumBusinessType msgType : EnumBusinessType.values()) {
                startMqConsumer(msgType);
            }

            log.info("Start mq consumers end");
        } catch (Exception e) {
            log.error("Mq consumer loading error!", e);
            throw new RuntimeException("Rocket mq consumer start failed");
        }
    }

    private void startMqConsumer(EnumBusinessType msgTag) throws MQClientException {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer();
        consumer.setConsumerGroup(consumerGroupNamePrefix + msgTag);
        consumer.setNamesrvAddr(rocketMQNameServer);

        // Subscribe topic with tag
        consumer.subscribe(topic, String.valueOf(msgTag));
        consumer.registerMessageListener(getMqListenerInstance(msgTag));

        //Launch the consumer instance.
        consumer.start();

        // add to list
        consumers.add(consumer);

        log.info("Start mq consumer success, subscribe topic: {}, tag: {}", topic, msgTag);
    }

    private AbstractMqListener getMqListenerInstance(EnumBusinessType type) {
        if (EnumBusinessType.LOGIN == type) {
            return userLoginListener;
        } else {
            return userLoginOutListener;
        }
    }

    @PreDestroy
    public void stop() {
        for (MQPushConsumer consumer : consumers) {
            consumer.shutdown();
        }
        log.info("Shutdown mq consumers!");
    }

}


Listener

package com.springboot.demo.mq.mq.listener;

import com.springboot.demo.mq.mq.constants.Constants;
import com.springboot.demo.mq.mq.utils.MqUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.common.message.MessageExt;

import java.util.List;


/**
 * 异步消息的消费
 **/
@Slf4j
public abstract class AbstractMqListener implements MessageListenerConcurrently, IReadMessage {

    private String topic = Constants.MQ_TOPIC;

    @Override
    public final ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgExtList,
                                                    ConsumeConcurrentlyContext consumeConcurrentlyContext) {
        if (msgExtList == null || msgExtList.size() == 0) {
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        }

        log.info("Receive msg ext list, size is:{}", msgExtList.size());
        boolean result = true;
        for (MessageExt msg : msgExtList) {
            try {
                if (!msg.getTopic().equals(topic)){
                    continue;
                }
                UnifiedMessage unifiedMessage = new UnifiedMessage(msg.getMsgId(), msg.getTags(), MqUtils.convertByteArrToStr(msg.getBody()));

                // 调用具体业务类处理消息,返回处理成功或失败
                result = readMessage(unifiedMessage);

                if (!result) {
                    log.warn("Read result fail, unified message: {}", unifiedMessage);
                } else {
                    log.info("Read result success, unified message: {}", unifiedMessage);
                }
            } catch (Exception ex) {
                result = false;
                log.error("Occurs exception", ex);
            }
        }
        return result ? ConsumeConcurrentlyStatus.CONSUME_SUCCESS : ConsumeConcurrentlyStatus.RECONSUME_LATER;
    }

}


接口类及模型

package com.springboot.demo.mq.mq.listener;

public interface IReadMessage {
    /**
     * 具体业务逻辑重写这个方法就可以啦
     * @param unifiedMessage 读取到的消息统一模型
     * @return 读取数据是否成功
     */
    boolean readMessage(UnifiedMessage unifiedMessage);
}

package com.springboot.demo.mq.mq.listener;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class UnifiedMessage {
    private String msgId;

    private String msgTags;

    private String msgBody;
}

实现类

package com.springboot.demo.mq.mq.listener.impl;

import com.springboot.demo.mq.mq.listener.AbstractMqListener;
import com.springboot.demo.mq.mq.listener.UnifiedMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class UserLoginListener extends AbstractMqListener {

    @Override
    public boolean readMessage(UnifiedMessage unifiedMessage) {
        log.info(unifiedMessage.getMsgBody());
        return true;
    }

}

package com.springboot.demo.mq.mq.listener.impl;

import com.springboot.demo.mq.mq.listener.AbstractMqListener;
import com.springboot.demo.mq.mq.listener.UnifiedMessage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class UserLoginOutListener extends AbstractMqListener {

    @Override
    public boolean readMessage(UnifiedMessage unifiedMessage) {
        log.info(unifiedMessage.getMsgBody());
        return true;
    }

}


参考:
RocketMQ Producer使用方法
Apache RocketMQ Quick Start
RocketMQ 常见异常处理
RocketMQ 解决 No route info of this topic 异常步骤
rocketmq No route info of this topic错误(原因版本不一致) – fastjson jar包缺失或版本不对

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值