springboot 整合 RocketMQ

一、springboot 整合 RocketMQ

1、架构图片

在这里插入图片描述

2、基本概念

(1)NameServer:

    1)消息队列中的状态服务器,集群的各个组件通过它来了解全局的信息 。类似微服务中注册中心的服务注册,发现,下线,上线的概念。
    2)热备份:NamServer可以部署多个,相互之间独立,其他角色同时向多个NameServer 机器上报状态信息。
    3)心跳机制:NameServer 中的 Broker、 Topic等状态信息不会持久存储,都是由各个角色定时上报并存储到内存中,超时不上报的话, NameServer会认为某个机器出故障不可用。

(2)Broker:RocketMQ 的核心,接收 Producer 发过来的消息、处理 Consumer 的消费消息请求、消息的持 久化存储、服务端过滤功能等 。

(3)Producer:消息的生产者,将消息投递到broker,投递消息要经历“请求-确认”机制,确保消息不会在投递过程中丢失。过程:生产者生产消息到broker,broker接受消息写入topic,之后给生产者发送确认相应,如果生产者没有收到服务端的确认或者收到失败的响应,则会重新发送消息;

(4)Consumer:消息的消费者,常用Consumer类 DefaultMQPushConsumer 收到消息后自动调用传入的处理方法来处理,实时性高 DefaultMQPullConsumer 用户自主控制 ,灵活性更高。在消费端,消费者在收到消息并完成自己的消费业务逻辑(比如,将数据保存到数据库中)后,也会给服务端发送消费成功的确认,服务端只有收到消费确认后,才认为一条消息被成功消费,否则它会给消费者重新发送这条消息,直到收到对应的消费成功确认。

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

(6)tag:topic中消息的标签,消费者消费topic中的消息时可以根据tag标签分类消费

消息模型图:
在这里插入图片描述
1、topic中队列的意义?

  • topic中队列是可配置的,多个队列相当于是负载均衡,将生产者投递到该topic中的消息分别加入各个队列中,图中为2个队列,即相当于将topic的消息承载体分为两段(即每条消息只会往每个队列里发一次,在topic中是唯一的),这样是为了提供性能,多实例并发生产和消费

  • topic中不能保证消息有序,但是在队列中消息有序

2、消费者组如何消费?

  • 同一个消费者组的消费者只能同时有一个消费者获取一个队列的消息,其他的消费者可以获取其他队列的消息。并且每个消费组自己维护在队列中读取消息的位置
  • 每个消费组都消费主题中一份完整的消息,不同消费组之间消费进度彼此不受影响,也就是说,一条消息被 Consumer Group1
    消费过,也会再给 Consumer Group2 消费。
  • topic中的消息被一个消费组消费完不会删除,应该是所有的消费者组消费完后删除
  • 对与一个消费者组来说,在同一个队列中的消费是串行的,但是多个队列加在一起就是并行消费,所以水平扩展可以提高消费性能

3、如何保证读取消息的有序性(一个消费组在一个主题下的多个队列并发消费就无法保证消息的顺序性)?

  • 后台将消息发送到同一队列中(fe:按照订单ID或者用户ID,用一致性哈希算法,计算出队列ID,指定队列ID发送,这样可以保证相同的订单/用户的消息总被发送到同一个队列上,就可以确保严格顺序了。

4、消息发送确认是否会影响效率?

  • rocket是批量确认消息的。

3、通信机制

  • Broker启动后需要完成一次将自己注册至NameServer的操作;随后每隔30s时间定时向NameServer更新Topic路由信息。
  • Producer发送消息时候,需要根据消息的Topic从本地缓存的获取路由信息。如果没有则更新路由信息会从NameServer重新拉取,同时Producer会默认每隔30s向NameServer拉取一次路由信息。
  • Consumer消费消息时候,从NameServer获取的路由信息,并再完成客户端的负载均衡后,监听指定消息队列获取消息并进行消费。

4、代码案例

版本

<dependency>
        <groupId>org.apache.rocketmq</groupId>
        <artifactId>rocketmq-client</artifactId>
        <version>4.7.0</version>
</dependency>

配置文件

rocketmq:
  # 生产者配置
  producer:
    #是否开启自动配置
    isEnable: true
    # 发送同一类消息的设置为同一个group,保证唯一
    groupName: elink-service-message-producer
    # 服务地址
    namesrvAddr: rocketmq-nameserver1:8047;rocketmq-nameserver2:8047
    # 消息最大长度 默认1024*4(4M)
    maxMessageSize: 4096
    # 发送消息超时时间,默认3000
    sendMsgTimeout: 3000
    # 发送消息失败重试次数,默认2
    retryTimesWhenSendFailed: 3
  # 消费者配置
  consumer:
    #是否开启自动配置
    isEnable: true
    # 官方建议:确保同一组中的每个消费者订阅相同的主题。
    groupName: elink-service-message-consumer
    # 服务地址,多个地址用 ';' 隔开
    namesrvAddr: rocketmq-nameserver1:8047;rocketmq-nameserver2:8047

    consumeThreadMin: 20
    consumeThreadMax: 64
    # 设置一次消费消息的条数,默认为1条
    consumeMessageBatchMaxSize: 1

Topic、Tag枚举类

@Getter
public enum MessageCodeEnum implements BaseStrEnum {


    /**
     * 消息模块主题
     */
    MESSAGE_TOPIC("elink-message","消息服务模块topic名称"),
   /**
    * 短信平台编号
    */
   NOTE_TAG("note_tag","短信标题"),
   DING_TAG("ding_tag","钉钉消息标题");


    private final String code;
    private final String msg;

    MessageCodeEnum(String code, String msg){
        this.code = code;
        this.msg = msg;
    }


}

消费者配置

@Configuration
@ConditionalOnProperty(prefix = "rocketmq.consumer", value = "isEnable", matchIfMissing = true)
@Slf4j
public class ConsumerConfig {
    @Value("${rocketmq.consumer.namesrvAddr}")
    private String namesrvAddr;
    @Value("${rocketmq.consumer.groupName}")
    private String groupName;
    @Value("${rocketmq.consumer.consumeThreadMin}")
    private int consumeThreadMin;
    @Value("${rocketmq.consumer.consumeThreadMax}")
    private int consumeThreadMax;
    @Value("${rocketmq.consumer.consumeMessageBatchMaxSize}")
    private int consumeMessageBatchMaxSize;
    @Resource
    private RocketMsgListener msgListener;

    @Bean
    public DefaultMQPushConsumer getRocketMQConsumer() {
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(groupName);
        consumer.setNamesrvAddr(namesrvAddr);
        consumer.setConsumeThreadMin(consumeThreadMin);
        consumer.setConsumeThreadMax(consumeThreadMax);
        consumer.registerMessageListener(msgListener);

        /**
         * 1. CONSUME_FROM_LAST_OFFSET:第一次启动从队列最后位置消费,后续再启动接着上次消费的进度开始消费
         * 2. CONSUME_FROM_FIRST_OFFSET:第一次启动从队列初始位置消费,后续再启动接着上次消费的进度开始消费
         * 3. CONSUME_FROM_TIMESTAMP:第一次启动从指定时间点位置消费,后续再启动接着上次消费的进度开始消费
         *  以上所说的第一次启动是指从来没有消费过的消费者,如果该消费者消费过,那么会在broker端记录该消费者的消费位置,如果该消费者挂了再启动,那么自动从上次消费的进度开始
         */
        consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);


        /**
         * CLUSTERING (集群模式) :默认模式,同一个ConsumerGroup(groupName相同)每个consumer只消费所订阅消息的一部分内容,同一个ConsumerGroup里所有的Consumer消息加起来才是所
         *  订阅topic整体,从而达到负载均衡的目的
         * BROADCASTING (广播模式) :同一个ConsumerGroup每个consumer都消费到所订阅topic所有消息,也就是一个消费会被多次分发,被多个consumer消费。
         *
         */
//        consumer.setMessageModel(MessageModel.BROADCASTING);

        consumer.setVipChannelEnabled(false);

        consumer.setConsumeMessageBatchMaxSize(consumeMessageBatchMaxSize);
        try {
            /**
             * 订阅topic,可以对指定消息进行过滤,例如:"TopicTest","tagl||tag2||tag3",*或null表示topic所有消息
             */
            consumer.subscribe(MessageCodeEnum.MESSAGE_TOPIC.getCode(), "*");
            consumer.start();
            log.info("消费者初始化成功:{}", consumer.toString());
        } catch (MQClientException e) {
            e.printStackTrace();
            log.error("消费者初始化失败:{}",e.getMessage());
        }
        return consumer;
    }
}

消息监听

@Component
@Slf4j
public class RocketMsgListener implements MessageListenerConcurrently {


    @Autowired
    private DingMessage dingMessage;
    @Autowired
    private SmsLogService smsLogService;
    @Autowired
    private NoteMessage noteMessage;

    @Override
    public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> list, ConsumeConcurrentlyContext context) {

        if (!CollectionUtils.isEmpty(list)) {
            for (MessageExt messageExt : list) {
                // 消息内容
                String body = new String(messageExt.getBody());
                log.info("接受到的消息为:{}", body);
                String tags = messageExt.getTags();
                String topic = messageExt.getTopic();
                String msgId = messageExt.getMsgId();
                String keys = messageExt.getKeys();
                int reConsume = messageExt.getReconsumeTimes();
                // 消息已经重试了3次,如果不需要再次消费,则返回成功
                if (reConsume == 3) {
                    // TODO 补偿信息
//                    smsLogService.insertLog(topic, tags, msgId, keys, body, "【" + EnumUtil.getStrMsgByCode(tags, TagsCodeEnum.class) + "】消费失败");
                    log.error("消息重试超过3次,消费失败!");
                    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
                }
                // 先判断是否订阅该topic
                if (MessageCodeEnum.MESSAGE_TOPIC.getCode().equals(topic)) {
                    boolean flag = false;
                    // 判断tag标签分类的业务,消息队列业务处理尽量单一
                    if (MessageCodeEnum.DING_TAG.getCode().equals(tags)) {
                        // 发送钉钉消息
                        flag = dingMessage.sendDingMessage(body);
                    } else if (MessageCodeEnum.NOTE_TAG.getCode().equals(tags)) {
                        // 发送短信
                        flag = noteMessage.sendNoteMessage(body);
                    } else {
                        log.info("未匹配到Tag【{}】" + tags);
                    }
                    if (!flag) {
                        return ConsumeConcurrentlyStatus.RECONSUME_LATER;
                    }
                }
            }
        }

        // 消息消费成功
//     ConsumeConcurrentlyStatus.RECONSUME_LATER broker会根据设置的messageDelayLevel发起重试,默认16次
        return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
    }

生产者配置

@Configuration
@Slf4j
public class ProducerConfig {

    @Value("${rocketmq.producer.groupName}")
    private String groupName;
    @Value("${rocketmq.producer.namesrvAddr}")
    private String namesrvAddr;
    @Value("${rocketmq.producer.maxMessageSize}")
    private Integer maxMessageSize ;
    @Value("${rocketmq.producer.sendMsgTimeout}")
    private Integer sendMsgTimeout;
    @Value("${rocketmq.producer.retryTimesWhenSendFailed}")
    private Integer retryTimesWhenSendFailed;

    @Bean
    public DefaultMQProducer getRocketMQProducer() {
        DefaultMQProducer producer;

        producer = new DefaultMQProducer(this.groupName);
        producer.setNamesrvAddr(this.namesrvAddr);
        //如果需要同一个jvm中不同的producer往不同的mq集群发送消息,需要设置不同的instanceName
        if(this.maxMessageSize!=null){
            producer.setMaxMessageSize(this.maxMessageSize);
        }
        if(this.sendMsgTimeout!=null){
            producer.setSendMsgTimeout(this.sendMsgTimeout);
        }
        //如果发送消息失败,设置重试次数,默认为2次
        if(this.retryTimesWhenSendFailed!=null){
            producer.setRetryTimesWhenSendFailed(this.retryTimesWhenSendFailed);
        }
        producer.setVipChannelEnabled(false);
        try {
            producer.start();
            log.info("生产者初始化成功:{}",producer.toString());
        } catch (MQClientException e) {
            log.error("生产者初始化失败:{}",e.getMessage());
        }
        return producer;
    }

}

消息发送

public class MessageProducer {

    @Autowired
    private DefaultMQProducer producer;

    /**
     * 同步发送消息
     * @author xch
     * @date 2020/6/5 10:19 上午
     * @param topic 主题
     * @param tag 标签
     * @param key 自定义的key,根据业务来定
     * @param value 消息的内容
     * @return org.apache.rocketmq.client.producer.SendResult
     */
    public SendResult sendMessage(String topic,String tag,String key,String value){
        String body = "topic:【"+topic+"】, tag:【"+tag+"】, key:【"+key+"】, value:【"+value+"】";
        try {
            Message msg = new Message(topic,tag,key,value.getBytes(RemotingHelper.DEFAULT_CHARSET));
            return producer.send(msg);
        } catch (UnsupportedEncodingException e) {
            log.error("消息初始化失败!body:{}",body);

        } catch (MQClientException | InterruptedException | RemotingException | MQBrokerException e) {
            log.error("消息发送失败! body:{}",body);
        }
        return null;
    }

    /**
     * 发送有序的消息
     * @author xch
     * @date 2020/6/5 10:23 上午
     * @param messagesList Message集合
     * @param messageQueueNumber 消息队列编号
     * @return org.apache.rocketmq.client.producer.SendResult
     */
    public SendResult sendOrderlyMessage(List<Message> messagesList, int messageQueueNumber) {
        SendResult result = null;
        for (Message message : messagesList) {
            try {
                result = this.producer.send(message, (list, msg, arg) -> {
                    Integer queueNumber = (Integer) arg;
                    return list.get(queueNumber);
                }, messageQueueNumber);
            } catch (MQClientException | RemotingException | MQBrokerException | InterruptedException e) {
                log.error("发送有序消息失败");
                return result;
            }
        }
        return result;
    }

}

异步消息发送

public class AsyncProducer {
    public static void main(String[] args) throws Exception {
        //Instantiate with a producer group name.
        DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
        // Specify name server addresses.
        producer.setNamesrvAddr("localhost:9876");
        //Launch the instance.
        producer.start();
        producer.setRetryTimesWhenSendAsyncFailed(0);
        for (int i = 0; i < 100; i++) {
                final int index = i;
                //Create a message instance, specifying topic, tag and message body.
                Message msg = new Message("TopicTest",
                    "TagA",
                    "OrderID188",
                    "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
                producer.send(msg, new SendCallback() {
                // 异步回调的处理
                    @Override
                    public void onSuccess(SendResult sendResult) {
                        System.out.printf("%-10d 异步发送消息成功 %s %n", index,
                            sendResult.getMsgId());
                    }
                    @Override
                    public void onException(Throwable e) {
                        System.out.printf("%-10d 异步发送消息失败 %s %n", index, e);
                        e.printStackTrace();
                    }
                });
        }
        //Shut down once the producer instance is not longer in use.
        producer.shutdown();
    }
}
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值