RocketMQ-整合SpringBoot

SpringBoot整合RocketMQ

创建Maven工程,引入关键依赖:

<dependencies><dependency><groupId>org.apache.rocketmq</groupId><artifactId>rocketmq-spring-boot-starter</artifactId><version>2.2.2</version><exclusions><exclusion><groupId>org.apache.rocketmq</groupId><artifactId>rocketmq-client</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.apache.rocketmq</groupId><artifactId>rocketmq-client</artifactId><version>4.9.5</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId><version>2.5.9</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><version>2.5.9</version></dependency><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.13.2</version><scope>test</scope></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger-ui</artifactId><version>2.9.2</version></dependency><dependency><groupId>io.springfox</groupId><artifactId>springfox-swagger2</artifactId><version>2.9.2</version></dependency>
</dependencies>

启动类

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

配置文件:

rocketmq.name-server=192.168.0.128:9876
rocketmq.producer.group=springBootGroup
​
#如果这里不配,那就需要在消费者的注解中配。
#rocketmq.consumer.topic=
rocketmq.consumer.group=testGroup
server.port=9000

声明生产者,直接使用RocketMQTemplate进行消息发送。

import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.apache.rocketmq.spring.support.RocketMQHeaders;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
​
@Component
public class SpringProducer 
{@Resourceprivate RocketMQTemplate rocketMQTemplate;
​public void sendMessage(String topic,String msg){this.rocketMQTemplate.convertAndSend(topic,msg);}
}

消费者声明,所有属性通过@RocketMQMessageListener注解声明。

@Component
@RocketMQMessageListener(consumerGroup = "MyConsumerGroup", topic = "TestTopic",consumeMode= ConsumeMode.CONCURRENTLY,messageModel= MessageModel.BROADCASTING)
public class SpringConsumer implements RocketMQListener<String> 
{@Overridepublic void onMessage(String message) {System.out.println("Received message : "+ message);}
}

注意:SpringBoot框架中对消息的封装与原生API的消息封装是不一样的。

如何处理各种消息类型

1、各种基础的消息发送机制

2、一个RocketMQTemplate实例只能包含一个生产者,也就只能往一个Topic下发送消息。如果需要往另外一个Topic下发送消息,就需要通过@ExtRocketMQTemplateConfiguration()注解另外声明一个子类实例。

3、对于事务消息机制,最关键的事务监听器需要通过@RocketMQTransactionListener注解注入到Spring容器当中。在这个注解当中可以通过rocketMQTemplateBeanName属性,指向具体的RocketMQTemplate子类。

实现原理

1、Push模式

Push模式对于@RocketMQMessageListener注解的处理方式,入口在rocketmq-spring-boot-2.2.2.jar中的org.apache.rocketmq.spring.autoconfigure.ListenerContainerConfiguration类中。

ListenerContainerConfiguration类继承了Spring当中的SmartInitializingSingleton接口,当Spring容器当中所有非懒加载的实例加载完成后,就会触发他的afterSingletonsInstantiated方法进行初始化。在这个方法中会去扫描所有带有注解@RocketMQMessageListener注解的类,将他注册到内部一个Container容器当中。

public void afterSingletonsInstantiated() {Map<String, Object> beans = this.applicationContext.getBeansWithAnnotation(RocketMQMessageListener.class).entrySet().stream().filter(entry -> !ScopedProxyUtils.isScopedTarget(entry.getKey())).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
​beans.forEach(this::registerContainer);
}

这里这个Container可以认为是客户端实例的一个容器,通过这个容器来封装RocketMQ的原生API。

registerContainer关键源码:

private void registerContainer(String beanName, Object bean) {... ...//获取Bean上面的注解RocketMQMessageListener annotation = clazz.getAnnotation(RocketMQMessageListener.class);
​... ...//检查注解的配置情况validate(annotation);
​String containerBeanName = String.format("%s_%s", DefaultRocketMQListenerContainer.class.getName(),counter.incrementAndGet());GenericApplicationContext genericApplicationContext = (GenericApplicationContext) applicationContext;//将扫描到的注解转化成为Container,并注册到上下文中。genericApplicationContext.registerBean(containerBeanName, DefaultRocketMQListenerContainer.class,() -> createRocketMQListenerContainer(containerBeanName, bean, annotation));DefaultRocketMQListenerContainer container = genericApplicationContext.getBean(containerBeanName,DefaultRocketMQListenerContainer.class);//启动容器,这里就相当于是启动了消费者if (!container.isRunning()) {try {container.start();} catch (Exception e) {log.error("Started container failed. {}", container, e);throw new RuntimeException(e);}}
​log.info("Register the listener to container, listenerBeanName:{}, containerBeanName:{}", beanName, containerBeanName);
}

重要方法createRocketMQListenerContainer,里面基本看不到RocketMQ的原生API,都是在创建并维护一个DefaultRocketMQListenerContainer对象

DefaultRocketMQListenerContainer类实现了InitializingBean接口,要关注他的afterPropertiesSet方法。这是Spring提供的对象初始化的扩展机制。

public void afterPropertiesSet() throws Exception {initRocketMQPushConsumer();
​this.messageType = getMessageType();this.methodParameter = getMethodParameter();log.debug("RocketMQ messageType: {}", messageType);
}

initRocketMQPushConsumer方法里就会创建一个RocketMQ原生的DefaultMQPushConsumer消费者。

关键源码:

private void initRocketMQPushConsumer() throws MQClientException {... ...//检查并创建consumer对象if (Objects.nonNull(rpcHook)) {consumer = new DefaultMQPushConsumer(consumerGroup, rpcHook, new AllocateMessageQueueAveragely(),enableMsgTrace, this.applicationContext.getEnvironment().resolveRequiredPlaceholders(this.rocketMQMessageListener.customizedTraceTopic()));consumer.setVipChannelEnabled(false);} else {log.debug("Access-key or secret-key not configure in " + this + ".");consumer = new DefaultMQPushConsumer(consumerGroup, enableMsgTrace,this.applicationContext.getEnvironment().resolveRequiredPlaceholders(this.rocketMQMessageListener.customizedTraceTopic()));}//定制instanceNameconsumer.setInstanceName(RocketMQUtil.getInstanceName(nameServer)); ... ...//设定广播消费还是集群消费switch (messageModel) {case BROADCASTING:consumer.setMessageModel(org.apache.rocketmq.common.protocol.heartbeat.MessageModel.BROADCASTING);break;case CLUSTERING:consumer.setMessageModel(org.apache.rocketmq.common.protocol.heartbeat.MessageModel.CLUSTERING);break;default:throw new IllegalArgumentException("Property 'messageModel' was wrong.");}//维护消费者的其他属性   ...//指定Consumer的消费监听 --》在消费监听中就会去调用onMessage方法switch (consumeMode) {case ORDERLY:consumer.setMessageListener(new DefaultMessageListenerOrderly());break;case CONCURRENTLY:consumer.setMessageListener(new DefaultMessageListenerConcurrently());break;default:throw new IllegalArgumentException("Property 'consumeMode' was wrong.");}
}

2、Pull模式

Pull模式的实现其实是通过在RocketMQTemplate实例中注入一个DefaultLitePullConsumer实例来实现的。只要注入并启动了这个DefaultLitePullConsumer实例后,后续就可以通过template实例的receive方法,来调用DefaultLitePullConsumer的poll方法,主动去Pull获取消息。

初始化DefaultLitePullConsumer的代码是在rocketmq-spring-boot-2.2.2.jar包中。处理类是org.apache.rocketmq.spring.autoconfigure.RocketMQAutoConfiguration。这个配置类会配置在jar包中的spring.factories文件中,通过SpringBoot的自动装载机制加载进来。

@Bean(CONSUMER_BEAN_NAME)
@ConditionalOnMissingBean(DefaultLitePullConsumer.class)
@ConditionalOnProperty(prefix = "rocketmq", value = {"name-server", "consumer.group", "consumer.topic"}) //解析的springboot配置属性。
public DefaultLitePullConsumer defaultLitePullConsumer(RocketMQProperties rocketMQProperties)throws MQClientException {RocketMQProperties.Consumer consumerConfig = rocketMQProperties.getConsumer();String nameServer = rocketMQProperties.getNameServer();String groupName = consumerConfig.getGroup();String topicName = consumerConfig.getTopic();Assert.hasText(nameServer, "[rocketmq.name-server] must not be null");Assert.hasText(groupName, "[rocketmq.consumer.group] must not be null");Assert.hasText(topicName, "[rocketmq.consumer.topic] must not be null");
​... ...//创建消费者   DefaultLitePullConsumer litePullConsumer = RocketMQUtil.createDefaultLitePullConsumer(nameServer, accessChannel,groupName, topicName, messageModel, selectorType, selectorExpression, ak, sk, pullBatchSize, useTLS);litePullConsumer.setEnableMsgTrace(consumerConfig.isEnableMsgTrace());litePullConsumer.setCustomizedTraceTopic(consumerConfig.getCustomizedTraceTopic());litePullConsumer.setNamespace(consumerConfig.getNamespace());return litePullConsumer;
}

RocketMQUtil.createDefaultLitePullConsumer方法,就是在维护一个DefaultLitePullConsumer实例。这个实例就是RocketMQ的原生API当中提供的拉模式客户端。

实际开发中,拉模式用得比较少。RocketMQ针对拉模式也做了非常多的优化。原本提供了一个DefaultMQPullConsumer类,进行拉模式消息消费,DefaultLitePullConsumer在此基础上做了很多优化。

RocketMQ最佳实践

1、合理分配Topic、Tag

一个应用尽可能用一个Topic,而消息子类型则可以用tags来标识。tags可以由应用自由设置,只有生产者在发送消息时设置了tags,消费方在订阅消息时才可以利用tags通过broker做消息过滤:message.setTags("TagA")。

Kafka的一大问题是Topic过多,会造成Partition文件过多,影响性能。而RocketMQ中的Topic完全不会对消息转发性能有影响。但是Topic过多,还是会加大RocketMQ的元数据维护的性能消耗。所以在使用时,还是需要对Topic进行合理的分配。

使用Tag区分消息时,尽量直接使用Tag过滤,不要使用复杂的SQL过滤。因为消息过滤机制虽然可以减少网络IO,但是毕竟会加大Broker端的消息处理压力。所以,消息过滤的逻辑,还是越简单越好。

2、使用Key加快消息索引

分配好Topic和Tag之后,就需要优化Key属性了,因为Key也可以参与消息过滤。通常建议每个消息要分配一个在业务层面的唯一标识码,设置到Key属性中。这有两个方面的作用:

  • 可以配合Tag进行更精确的消息过滤。

  • RocketMQ的Broker端会为每个消息创建一个哈希索引。应用可以通过topic、key来查询某一条历史的消息内容,以及消息在集群内的处理情况。在管理控制台就可以看到。为了减少哈希索引潜在的哈希冲突问题,所以官方建议,客户端要尽量保证key的唯一性。

3、关注错误消息重试

多关注重试队列,可以及时了解消费者端的运行情况。这个队列中出现了大量的消息,就意味着消费者的运行出现了问题,要及时跟踪进行干预

RocketMQ默认允许每条消息最多重试16次,每次重试的间隔时间如下:

重试次数与上次重试的间隔时间重试次数与上次重试的间隔时间
110 秒97 分钟
230 秒108 分钟
31 分钟119 分钟
42 分钟1210 分钟
53 分钟1320 分钟
64 分钟1430 分钟
75 分钟151 小时
86 分钟162 小时

这个重试时间跟延迟消息的延迟级别是对应的。不过取的是延迟级别的后16级别。

messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h

这个重试时间可以将源码中的org.apache.rocketmq.example.quickstart.Consumer里的消息监听器返回状态改为RECONSUME_LATER测试一下。

重试次数:

如果消息重试16次后仍然失败,消息将不再投递。转为进入死信队列。

然后关于这个重试次数,RocketMQ可以进行定制。例如通过consumer.setMaxReconsumeTimes(20);将重试次数设定为20次。当定制的重试次数超过16次后,消息的重试时间间隔均为2小时。

配置覆盖:

消息最大重试次数的设置对相同GroupID下的所有Consumer实例有效。并且最后启动的Consumer会覆盖之前启动的Consumer的配置。

4、手动处理死信队列

当一条消息消费失败,RocketMQ就会自动进行消息重试。而如果消息超过最大重试次数,RocketMQ就会认为这个消息有问题。但是此时,RocketMQ不会立刻将这个有问题的消息丢弃,而会将其发送到这个消费者组对应的一种特殊队列:死信队列。

通常,一条消息进入了死信队列,意味着消息在消费处理的过程中出现了比较严重的错误,并且无法自行恢复。此时,一般需要人工去查看死信队列中的消息,对错误原因进行排查。然后对死信消息进行处理,比如转发到正常的Topic重新进行消费,或者丢弃。

死信队列的名称是%DLQ%+ConsumGroup

死信队列的特征:

  • 一个死信队列对应一个ConsumGroup,而不是对应某个消费者实例。

  • 如果一个ConsumeGroup没有产生死信队列,RocketMQ就不会为其创建相应的死信队列。

  • 一个死信队列包含了这个ConsumeGroup里的所有死信消息,而不区分该消息属于哪个Topic。

  • 死信队列中的消息不会再被消费者正常消费。

  • 死信队列的有效期跟正常消息相同。默认3天,对应broker.conf中的fileReservedTime属性。超过这个最长时间的消息都会被删除,而不管消息是否消费过。

注:默认创建出来的死信队列里面的消息是无法读取的,在控制台和消费者中都无法读取。这是因为这些默认的死信队列,他们的权限perm被设置成了2:禁读(这个权限有三种 2:禁读,4:禁写,6:可读可写)。需要手动将死信队列的权限配置成6,才能被消费(可以通过mqadmin指定或者web控制台)。

5、消费者端进行幂等控制

在MQ系统中,对于消息幂等有三种实现语义:

  • at most once 最多一次:每条消息最多只会被消费一次

  • at least once 至少一次:每条消息至少会被消费一次

  • exactly once 刚刚好一次:每条消息都只会确定的消费一次

这三种语义都有他适用的业务场景

  • at most once是最好保证的。RocketMQ中可以直接用异步发送、sendOneWay等方式就可以保证。

  • at least once这个语义,RocketMQ也有同步发送、事务消息等很多方式能够保证。

  • exactly once是MQ中最理想也是最难保证的一种语义,需要有非常精细的设计才行。RocketMQ只能保证at least once,保证不了exactly once。所以,使用RocketMQ时,需要由业务系统自行保证消息的幂等性

官网描述:

4. Are messages delivered exactly once?

RocketMQ ensures that all messages are delivered at least once. In most cases, the messages are not repeated.

但是,对于exactly once语义,阿里云上的商业版RocketMQ是明确有API支持的,至于如何实现的不得而知

消息幂等的必要性

在互联网应用中,尤其在网络不稳定的情况下,消息队列 RocketMQ 的消息有可能会出现重复,概括情况如下:

  • 发送时消息重复

    当一条消息已被成功发送到服务端并完成持久化,此时出现了网络闪断或者客户端宕机,导致服务端对客户端应答失败。 如果此时生产者意识到消息发送失败并尝试再次发送消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息。

  • 投递时消息重复

    消息消费的场景下,消息已投递到消费者并完成业务处理,当客户端给服务端反馈应答的时候网络闪断。 为了保证消息至少被消费一次,消息队列 RocketMQ 的服务端将在网络恢复后再次尝试投递之前已被处理过的消息,消费者后续会收到两条内容相同并且 Message ID 也相同的消息。

  • 负载均衡时消息重复(包括但不限于网络抖动、Broker 重启以及订阅方应用重启)

    当消息队列 RocketMQ 的 Broker 或客户端重启、扩容或缩容时,会触发 Rebalance,此时消费者可能会收到重复消息。

处理方式

从上面的分析中,我们知道,在RocketMQ中,是无法保证每个消息只被投递一次的,所以要在业务上自行来保证消息消费的幂等性。

而要处理这个问题,RocketMQ的每条消息都有一个唯一的MessageId,这个参数在多次投递的过程中是不会改变的,所以业务上可以用这个MessageId来作为判断幂等的关键依据。

但是,这个MessageId是无法保证全局唯一的,也会有冲突的情况。所以在一些对幂等性要求严格的场景,最好是使用业务上唯一的一个标识比较靠谱。例如订单ID。而这个业务标识可以使用Message的Key来进行传递。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/245096.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

基于ssm实现的工资管理系统

一、系统架构 前端&#xff1a;jsp | jquery | layui 后端&#xff1a;spring | springmvc | mybatis 环境&#xff1a;jdk1.8 | mysql | maven 二、代码及截图 三、功能介绍 01. 登录页 02. 首页 03. 用户管理-基本资料 04. 用户管理-修改密码 05. 人事管理-部门列表…

基于Python实现的滑动验证码自动识别工具源码

滑动验证码识别 今天的目标地址是字节的巨量纵横&#xff0c;目前东家是一家广告营销型的公司&#xff0c;专注于在各大平台投放信息流广告。巨量纵横为字节跳动的广告平台&#xff0c;用于管理推广账户。今天破解一下这个平台的登陆入口&#xff0c;为今后的数据爬取开个头。…

Pytorch深度强化学习1-5:详解蒙特卡洛强化学习原理

目录 0 专栏介绍1 蒙特卡洛强化学习2 策略评估原理3 策略改进原理3.1 同轨蒙特卡洛强化学习3.2 离轨蒙特卡洛强化学习 0 专栏介绍 本专栏重点介绍强化学习技术的数学原理&#xff0c;并且采用Pytorch框架对常见的强化学习算法、案例进行实现&#xff0c;帮助读者理解并快速上手…

倾斜摄影三维模型的根节点合并的优势分析

倾斜摄影三维模型的根节点合并的优势分析 根节点合并是倾斜摄影三维模型处理中的一项重要技术&#xff0c;它具有许多优势&#xff0c;可以显著提升模型数据的处理效率和渲染性能。在本文中&#xff0c;我们将对倾斜摄影三维模型的根节点合并的优势进行详细分析。 1、数据大小…

Oracle(2-8)Configuring the Database Archiving Mode

文章目录 一、基础知识1、Redo Log History2、NOARCHIVELOG Mode 非归档模式3、ARCHIVELOG Mode 归档模式4、Changing the Archiving Mode 更改归档模式![在这里插入图片描述](https://img-blog.csdnimg.cn/direct/d6a09f9a6de24de7bbcdad90b8d6b9ca.png)5、Auto and Manual Ar…

关于this和构造器的理解

1.类中的this关键字表示当前对象的引用。它可以被用于解决变量名冲突问题&#xff0c;或者在一个方法中调用类的另一个方法。如果在方法中没有明确指定要使用哪个变量&#xff0c;那么编译器就会默认使用this关键字来表示当前对象。 下面是一个输出this关键字的示例代码&#…

网工学习9-STP配置(二)

如图 1 所示&#xff0c;当前网络中存在环路&#xff0c; SwitchA 、SwitchB 、SwitchC 和 SwitchD 都运行 STP&#xff0c;通过 彼此交互信息发现网络中的环路&#xff0c;并有选择的对某个端口进行阻塞&#xff0c;最终将环形网络结构修剪成无 环路的树形网络结构&#xff…

木质家具行业分析:我国市场规模总资产达1669.19亿元

木质家具是指以天然木材和木质人造板为主要材料&#xff0c;配以其他辅料(如油漆、贴面材料、玻璃、五金配件等)制作各种家具的生产活动。 近年来实木家具越来越受到广大消费者的青睐。继板式家具、板式定制家具之后&#xff0c;板木家具与整木定制家具渐渐走进人们的视野。但目…

【EI稳定检索】第三届能源利用与自动化国际学术会议(ICEUA 2024)

第三届能源利用与自动化国际学术会议&#xff08;ICEUA 2024&#xff09; 2024 3rd International Conference on Energy Utilization and Automation (ICEUA 2024) ICEUA 2024已成功申请JPCS - Journal of Physics: Conference Series (ISSN:1742-6596)---独立出版 2024年…

使用Linux docker方式快速安装Plik并结合内网穿透实现公网访问

文章目录 1. Docker部署Plik2. 本地访问Plik3. Linux安装Cpolar4. 配置Plik公网地址5. 远程访问Plik6. 固定Plik公网地址7. 固定地址访问Plik 本文介绍如何使用Linux docker方式快速安装Plik并且结合Cpolar内网穿透工具实现远程访问&#xff0c;实现随时随地在任意设备上传或者…

Python小项目:葛兰中欧医疗基金数据分析

1 前言 2022年1月末&#xff0c;正值年前&#xff0c;股票市场持续大幅下跌&#xff0c;与之相应的基金市场也出现了近一周的持续跌势。作为一个资深的投资者&#xff0c;我承认自己曾是一名“韭菜”&#xff0c;在2021年12月初购买了中欧医疗健康混合C基金。这只基金由医疗板…

内容过滤算法:构建数字世界的守护者

目录 引言 1. 内容过滤算法概述 2. 内容过滤算法的分类 2.1 关键词过滤算法 2.2 统计模型 2.3 机器学习算法 2.4 深度学习算法 3. 内容过滤算法在实际应用中的体现 3.1 电子邮件过滤 3.2 社交媒体内容过滤 3.3 网络搜索引擎 4. 内容过滤算法的挑战与未来发展 4.1 对…