一:RocketMQ 部署架构
1、RocketMQ的角色介绍
Producer:消息的发送者ProducerGroup:同一类producer的集合Consumer:消息接收者;PushConsumer : broker主动推送消息给消费端PullConsumer :消费端主动拉取消息ConsumerGroup:同一类consumer的集合Broker:暂存和传输消息; 一个broker中可以存储多个topic(一个topic也可以分片在多个broker上)NameServer:管理Broker;Topic:区分消息的种类;一个发送者可以发送消息给一个或者多个Topic;一个消息的接收者可以订阅一个或者多个Topic消息Message Queue:相当于是Topic的分区,一个topic下可以挂载多个queue,消息真实的物理存储结构;广播消息: 所有消费者都会消费该消息,哪怕是同一个ConsumerGroup的消费者集群消费:一条消息只会被同一个ConsumerGroup下的一个消费者消费,如果是多条消息,会均衡消费

NameServer:无状态节点,可集群部署,节点间无信息同步。Broker:mater:slave一对多,通过指定相同的BrokerName建立关系。
BrokerId为0为master,非0为slave,slave中只有为1的那个可参与读负载。
每个broker与nameServer中所有节点建立长连接,定时注册topic信息上去。
Producer: 与nameServer中随机一个节点建立长连接,定期获取topic路由信息,并与提供topic服务的master建立长连接,定时发送心跳。 无状态,可集群部署。consumer:同producer,另外也会与slave建立长连接和发送心跳。
可以从master或slave订阅消息,如果是master,则master会根据最大偏移量和拉取偏移量对比,(判断是否读取老消息,老消息已持久化,会产生IO),以及slave是否可读等,建议下一次从master还是slave拉取。
如上图示:
- 在broker集群中,每个broker是可以存储多个topic的,当然,每个topic也是可以分散到不同的broker上。
- 在一个broker的每个topic分片下,有多个messageQueue(数量是创建topic时候指定的)。
- 那么每一个consumerGroup中的所有consumer,会轮询消费这些messageQueue(集群消费模式如图2)。 所以,consumer的数量不能超过messageQueue的数量
- 启动NameServer,监听端口,等待broker、producer、consumer的连接, 相当于一个路由控制中心。
- broker启动与NameServer建立连接,定时发送心跳包(包含broker信息:ip、port、存储的所有topic信息)。
- 手动创建topic,指明要存储在那些broker上(也可以发送消息时候创建)
- producer发消息,启动时先与一台NameServer建立长连接,并获取到要发送的topic在哪些broker,轮询选择一个队列,与所在的broker建立长连接并发送消息。
- consumer启动与producer类似。
Tag:统一topic下区分不同类型消息的
顺序消息: 消费一类消息时,可以保证按发送的顺序依次消费,比如一个订单的创建,支付,完成三条消息。 不同的订单之间可并行
普通顺序消息 : 顺序消息的一种,正常情况保证顺序,如果broker宕机队列数发生变化,哈希取模定位队列也会发生变化,造成错乱。
严格顺序消息:一个broker挂机,全都不能使用,可以保证顺序性。 但是可用性差
消息过滤: consumer根据tag标签来消费自己想要的消息,类似rabbitmq中路由键一样。
回溯消费: broker提供的机制,consumer可重新消费一定时间内,已经消费过的消息
事务消息: 可将本地事务和发送消息的操作定义到全局事务中,保证同时成功或失败
定时消息: rocket中自带了一个topic:SCHEDULE_TOPIC_XXXX,该topic下绑定了代表不同定时时间的queue。 如果发送定时消息,会先发到该topic,并根据具体时间路由都对应的queue中,并在达到合适的时间后发送到业务topic中。
默认时间有18个等级,delayTimeLevel:1-18(18个queue,queueId=level-1): 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h。
- level == 0,消息为非延迟消息
- 1<=level<=maxLevel,消息延迟特定时间,例如level==1,延迟1s
- level > maxLevel,则level== maxLevel,例如level==20,延迟2h
消息重试:
消费失败通常有两种原因,1:消息本身反序列化失败,即使立即重试也不会成功。所以最好提供一种定时重试机制,不要频繁重试。 2、下游程序原因,比如db连接不可用,可以让应用sleep一段时间,再消费下一条消息,减轻broker重试消息的压力。
消息重投:
retryTimesWhenSendFailed:
同步消息重投,设置重投次数,默认2。
重投不会选择上一次失败的broker,最大保证成功。
如果超过重试次数,抛出异常由客户端自己保证消息不丢失。
RemotingException、 MQClientException和部分MQBrokerException时会重投
retryTimesWhenSendAsyncFailed:异步发送失败重试次数,异步重试不会选择其他broker, 仅在同一个broker上做重试,不保证消息不丢。
retryAnotherBrokerWhenNotStoreOK:消息刷盘(主或备)超时或slave不可用(返回状态非SEND_OK),是否尝试发送到其他broker,默认false。十分重要消息可以开启。
流量控制:
1) 生产者流控:因broker处理能力达到瓶颈
2) 消费者流控,因为消费能力达到瓶颈
死信队列: 消费失败且超过重试次数的消息,会发送到死信队列
1)、同步发送消息
import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.remoting.common.RemotingHelper; import org.apache.rocketmq.remoting.exception.RemotingException; import java.io.UnsupportedEncodingException; public class MyProducer { public static void main(String[] args) throws UnsupportedEncodingException, InterruptedException, RemotingException, MQClientException, MQBrokerException { // 在实例化生产者的同时,指定了生产组名称 DefaultMQProducer producer = new DefaultMQProducer("myproducer_grp_01"); // 指定NameServer的地址 producer.setNamesrvAddr("node1:9876"); // 对生产者进行初始化,然后就可以使用了 producer.start(); // 创建消息,第一个参数是主题名称,第二个参数是消息内容 Message message = new Message("tp_demo_01", "hello lagou 01".getBytes(RemotingHelper.DEFAULT_CHARSET)); // 发送消息 final SendResult result = producer.send(message); System.out.println(result); // 关闭生产者 producer.shutdown(); } }
2)、异步发送消息
import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.client.producer.DefaultMQProducer; import org.apache.rocketmq.client.producer.SendCallback; import org.apache.rocketmq.client.producer.SendResult; import org.apache.rocketmq.common.message.Message; import org.apache.rocketmq.remoting.exception.RemotingException; import java.io.UnsupportedEncodingException; public class MyAsyncProducer { public static void main(String[] args) throws MQClientException, UnsupportedEncodingException, RemotingException, InterruptedException { // 实例化生产者,并指定生产组名称 DefaultMQProducer producer = new DefaultMQProducer("producer_grp_01"); // 指定nameserver的地址 producer.setNamesrvAddr("node1:9876"); // 初始化生产者 producer.start(); for (int i = 0; i < 100; i++) { Message message = new Message("tp_demo_02", ("hello lagou " + i).getBytes("utf-8")); // 消息的异步发送 producer.send(message, new SendCallback() { @Override public void onSuccess(SendResult sendResult) { System.out.println("发送成功:" + sendResult); } @Override public void onException(Throwable throwable) { System.out.println("发送失败:" + throwable.getMessage()); } }); } // 由于是异步发送消息,上面循环结束之后,消息可能还没收到broker的响应 // 如果不sleep一会儿,就报错 Thread.sleep(10_000); // 关闭生产者 producer.shutdown(); } }
3)、消费者拉取消息
import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer; import org.apache.rocketmq.client.consumer.PullResult; import org.apache.rocketmq.client.exception.MQBrokerException; import org.apache.rocketmq.client.exception.MQClientException; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageQueue; import org.apache.rocketmq.remoting.exception.RemotingException; import java.io.UnsupportedEncodingException; import java.util.List; import java.util.Set; /** * 拉取消息的消费者 */ public class MyPullConsumer { public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException, UnsupportedEncodingException { // 拉取消息的消费者实例化,同时指定消费组名称 DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("consumer_grp_01"); // 设置nameserver的地址 consumer.setNamesrvAddr("node1:9876"); // 对消费者进行初始化,然后就可以使用了 consumer.start(); // 获取指定主题的消息队列集合 final Set<MessageQueue> messageQueues = consumer.fetchSubscribeMessageQueues("tp_demo_01"); // 遍历该主题的各个消息队列,进行消费 for (MessageQueue messageQueue : messageQueues) { // 第一个参数是MessageQueue对象,代表了当前主题的一个消息队列 // 第二个参数是一个表达式,对接收的消息按照tag进行过滤 // 支持"tag1 || tag2 || tag3"或者 "*"类型的写法;null或者"*"表示不对 消息进行tag过滤 // 第三个参数是消息的偏移量,从这里开始消费 // 第四个参数表示每次最多拉取多少条消息 final PullResult result = consumer.pull(messageQueue, "*", 0, 10); // 打印消息队列的信息 System.out.println("message******queue******" + messageQueue); // 获取从指定消息队列中拉取到的消息 final List<MessageExt> msgFoundList = result.getMsgFoundList(); if (msgFoundList == null) continue; for (MessageExt messageExt : msgFoundList) { System.out.println(messageExt); System.out.println(new String(messageExt.getBody(), "utf- 8")); } } // 关闭消费者 consumer.shutdown(); } }
4)、主动推送消息给消费者
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer; 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.client.exception.MQClientException; import org.apache.rocketmq.common.message.MessageExt; import org.apache.rocketmq.common.message.MessageQueue; import java.io.UnsupportedEncodingException; import java.util.List; /** * 推送消息的消费者 */ public class MyPushConsumer { public static void main(String[] args) throws MQClientException, InterruptedException { // 实例化推送消息消费者的对象,同时指定消费组名称 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_grp_02"); // 指定nameserver的地址 consumer.setNamesrvAddr("node1:9876"); // 订阅主题 consumer.subscribe("tp_demo_02", "*"); // 添加消息监听器,一旦有消息推送过来,就进行消费 consumer.setMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { final MessageQueue messageQueue = context.getMessageQueue(); System.out.println(messageQueue); for (MessageExt msg : msgs) { try { System.out.println(new String(msg.getBody(), "utf- 8")); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } } // 消息消费成功 return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; // 消息消费失败 // return ConsumeConcurrentlyStatus.RECONSUME_LATER; } }); // 初始化消费者,之后开始消费消息 consumer.start(); // 此处只是示例,生产中除非运维关掉,否则不应停掉,长服务 // Thread.sleep(30_000); // 关闭消费者 // consumer.shutdown(); } }
使用原生api后,再来看看与spring整合
生产者:
1)添加依赖
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.1.RELEASE</version> </parent> <properties> <rocketmq-spring-boot-starter-version>2.0.3</rocketmq-spring-boot- starter-version> </properties> <dependencies> <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-spring-boot-starter</artifactId> <version>${rocketmq-spring-boot-starter-version}</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.6</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
2)配置文件
# application.propertiesrocketmq.name-server=192.168.80.121:9876;192.168.80.122:9876rocketmq.producer.group=my-group
3) 测试类
@RunWith(SpringRunner.class) @SpringBootTest(classes = {MQSpringBootApplication.class}) public class ProducerTest { @Autowired private RocketMQTemplate rocketMQTemplate; @Test public void test1(){ rocketMQTemplate.convertAndSend("springboot-mq","hello springbootrocketmq"); } }
1) 依赖和配置文件同生产者
2) 消息监听器
@Slf4j @Component @RocketMQMessageListener(topic = "springboot-mq",consumerGroup = "springboot-mq-consumer-1") public class Consumer implements RocketMQListener<String> { @Override public void onMessage(String message) { log.info("Receive message:"+message); } }