目录
topic和broker与topic和messagequeue之间的关系:
一:引言
Message Queue(消息队列),从字面上理解:首先它是一个队列。FIFO的原则,即是先进先出的数据结构:队列
消息队列即是所谓的存放消息的队列
消息队列解决的不是存放消息的队列的目的,而解决的是通信问题。
消息队列解决的问题:
各服务同步通信时:
服务即是("创建订单","扣减库存")这些步骤。
比如下图中的电商订单系统为例,如果各服务之间使用同步通信,不仅耗时较久并且过程中受到网络波动的影响较大,不能保证高成功率。
分析:
(1) 用户进入点击下订单操作,然后会请求到后台服务器,后台服务器会进行一系列的操作,如:"创建订单","扣减库存"等,每一个步骤
其实都可以看作为一个微服务模块
(2) 耗时久:每一个微服务模块微服务都是同步执行的,进行完上一个微服务模块才会接着下一个。这样一层层的处理完毕之后,并且全
部成功后,才会返回"下单成功" 给用户。这样耗时是十分久的
(3) 网络波动影响较大并且不能保证高成功率 :对于网络通信来说。网络始终是不可靠的,每一个步骤对应的每一个微服务模块都可能受
到网络动荡影响而失败。只要有一个步骤执行失败,那么整个下单过程就失败,就不能保证高成功率
(4) 总结:对于用户来说:下一个单,耗时久,成功率不能保证,本来就是极差的用户体验。
如下图所示:各服务同步通信实现下单操作:
因此,我们要使用异步的通信方式对架构进行改造。
各服务异步通信时:
服务即是("创建订单","扣减库存")这些步骤。
(1)上游执行完下订单消息的发送给消息队列的业务后立即获得到下单成功的结果。用户体验好,成功率高 有保证 !
分析:用户进入并且下订单,此时不会直接把请求打到后台服务器而是发送一条Message给消息队列,此Message消息包含商品id,
商品数量,创建订单的时间等一系列商品的有关信息。当发送给消息队列的业务执行完毕后,用户会立即获得到"下单成功"的结果。其实
此时用户的下单的业务流程并没有执行完,甚至还没有执行,这就是消息队列的好处。
(2)使用异步的通信方式对微服务模块间的调用进行解耦。下游多个服务订阅到消息后各自消费。通过消息队列,屏蔽底层的通信协议,使
得解耦和并行消费消息队列中的Message得以实现。并且提高了下单的成功率 !
分析:下游的每一个下单的步骤都会对应一个微服务模块,每一个模块都会订阅消息队列中的Message消息,消息队列会把Message
按一定的规则发送给订阅者,这样各个步骤(如:"创建订单","扣减库存"等等)对应的微服务模块会互不干扰 解耦性的消费消息并且执行
下单的逻辑。即使说有一个步骤执行失败,那么也不会使得整个流程停滞不前,而是解耦性的执行 互不干扰 !
(3) 可以快速的提升系统的吞吐量。
分析:如果具有了消息队列,那么我们的后台服务器系统就无需实时的进行执行下单的流程操作。实时是什么意思?实时就是当一个
用户执行下单操作之后,后台服务器就必须此刻立即执行该请求的下单操作流程。由于具有了消息队列,可以把发送过来的消息先存放到
消息队列中,后台可以一条一条的,有规律性的处理这些请求的消息,所以自然系统的吞吐量就提升了 !
如下图所示:各服务异步通信+消息队列实现下单操作:
二:RocketMQ介绍
RocketMQ的部署架构
结合部署架构图,描述集群工作流程:
(1) 启动NameServer。NameServer就相当于是一个服务中心,类似于Zookeeper。NameServer启动后监听端口,等待Broker,
Producer,Consumer连接上来,相当于一个路由控制中心。
(2) 启动Broker ,Broker会把自己的信息注册到NameServer上面,跟所有的NameServer保持长连接,定时发送心跳包。心跳包中包含
当前Broker信息(IP+端口等)以及存储的所有Topic信息【心跳包的内容即是代表Topic与Broker之间的映射关系】。注册成功后,
’NameServer集群中就有Topic跟Broker的映射关系。
(3) 创建Topic,发消息的逻辑和收消息的逻辑都是基于Topic来实现的。创建Topic时需要指定该Topic要存储在哪些Broker上面,也可以
在发送消息时自动创建Topic。【Topic实际上是逻辑生成在NameServer这个服务中心上的,但是真正的物理存储是存储在Broker上的。
也就是说Topic与Broker之间的映射关系是保存在NameServer上的【映射关系是由第(2)中Broker与NameServer建立连接时发送的心跳包所给】】
(4) 启动Producer生产者,生产者会随机与NameServer集群中的一台进行建立一个长连接,并从NameServer中获取当前发送的Topic存
在于哪些Broker上【因为NameServer存储着Topic和Broker之间的映射关系,该映射关系的作用:因为Broker可能是一个集群,你必须确
定Topic是存在于哪一个Broker上面的】。然后轮询从队列列表中选择一个队列,然后与队列所在的Broker建立连接通道从而向Broker发
消息
(5) 启动Consumer消费者,Consumer与Producer类似,跟其中一台NameServer建立长连接,获取当前订阅的Topic存在于哪些Broker
上【因为NameServer存储着Topic和Broker之间的映射关系,该映射关系的作用:因为Broker可能是一个集群,你必须确定Topic是存在
于哪一个Broker上面的,然后直接跟Broker建立连接通道,开始消费消息。
注释:
【Producer,Consumer与NameServer建立的长连接的目的:定期的获取对应的Topic信息。这样Producer就可以给相对应Broker发送消息】
RocketMQ网络部署特点:
NameServer可以是一个几乎无状态节点,可以集群部署,节点之间无任何的信息同步。分析这句话:"无状态":意思为多个
NameServer集群节点之间是互不直到对方的存在的。因此可以易知:集群节点之间无任何的信息同步。
三: RocketMQ集群模式
为了追求更好的性能,Rocket的最佳实践方式都是在集群模式下完成的。RocketMQ官方提供了三种集群搭建方式:
(1) 2主2从异步通信方式
使用异步方式进行主从之间的数据复制,吞吐量大【因为执行效率快 迭代更新快】,但是可能会丢失消息【如图分析】
(2) 2主2从同步通信方式
使用异步方式进行主从之间的数据复制,保证消息安全投递,不会丢失,但是会影响吞吐量【因为生产者会等待主从机之间完成消息复制之后才会发送第二条消息】
(3)2主无从方式
会存在单点故障,且读的性能没有前面两种方式好。因为如果master挂掉,就会造成单点故障啦 !
还有一种:
四: 消息示例(Java代码)
1.构建Java基础环境
导入依赖:
<dependencies> <dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-client</artifactId> <version>4.4.0</version> </dependency> </dependencies>
生产者代码演示:
消费者代码演示:
public class BaseConsumer { public static void main(String[] args) throws Exception{ //1.创建消费者对象 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("my-consumer-group1") ; //2.指明NameServer地址:NameServer中存储着topic和broker之间的映射关系,它知道指定的topic都对应哪些broker consumer.setNamesrvAddr("服务器ip地址:9876"); //3.消费者订阅Topic主题:"topic"和过滤消息使用tag表达式:“*”(表示可以消费所有tag标记对应的消息) consumer.subscribe("topic","*") ; //4.创建一个监听器,当broker把消息推过来时进行调用 consumer.registerMessageListener(new MessageListenerConcurrently() { /** * @param msgs 表示消费者接收到所有消息的集合 * @param context 上下文对象 * @return */ @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { msgs.forEach(msg -> System.out.println(new String(msg.getBody())));//打印msgs集合中的每一条消息对象 return ConsumeConcurrentlyStatus.CONSUME_SUCCESS ; } }); //5.启动消费者 consumer.start(); } }
topic和broker与topic和messagequeue之间的关系:
为什么Message消息对象既在broker-a又在broker-b?queueId是什么?queueOffset偏移量是什么?
一张图解析即可:
为什么Message消息对象既在broker-a又在broker-b?queueId是什么?queueOffset偏移量是什么?
分析:
(1)创建Topic,Topic是一个逻辑上的概念但是也是具有物理存储的。
对于Topic逻辑上的概念是:创建Topic时需要指定Topic对应哪些broker【一个Topic可以对应broker集群中多个broker的】,该Topic与
broker的映射逻辑关系存储在NameServer上。
对于Topic的物理存储:对于一个Topic(如图中的MyTopic1)它的物理存储就是开辟了四个MessageQueue,默认是开启四个。该队列对
应的id是从0开始递增的。该物理存储是存储在broker集群中映射关系映射的所有broker上的。
(2) 创建完物理内存后,生产者发送消息给broker
由于一个topic可以对应多个broker,所以既可以发送给broker-a,又可以发送给broker-b
当我们选择发送给一个broker之后,该broker中又包含四个队列对象的物理内存用于真正存储Message消息对象。轮询从该broker的队
列列表中选择一个队列,然后与队列所在的broker建立连接通道从而向broker发消息
(3) 每一个broker默认开辟四个队列空间,queueid即是队列的编号,默认从0开始
queueOffset即是队列偏移量
2.简单消息示例
简单消息示例分为三种:同步消息,异步消息和单向消息
(1) 同步消息:
如果生产者发送的是同步消息,当发送后,生产者会一直阻塞等待,直到broker反馈发送成功的消息信息 生产者才会停止阻塞然后执行下面的逻辑操作
应用场景:
如重要通知信息,短信通知,短信营销系统等。(因为同步消息的安全性高,不会轻易造成消息数据丢失)
同步消息对应生产者代码:
public class SyncProducer { public static void main(String[] args) throws MQClientException, UnsupportedEncodingException, RemotingException, InterruptedException, MQBrokerException { DefaultMQProducer producer = new DefaultMQProducer("producerGroup1"); producer.setNamesrvAddr("172.16.253.101:9876"); producer.start(); for (int i = 0; i < 100; i++) { Message msg = new Message("TopicTest" /* Topic */, "TagA" /* Tag */, ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */ ); SendResult sendResult = producer.send(msg); System.out.printf("%s%n", sendResult); } System.out.println("for循环执行完(即生产者发送完消息 需等待broker对所有消息数据反馈完成 成功之后) 才可以执行该句代码"); producer.shutdown(); } }
(2) 异步消息:
如果生产者发送的是异步消息,当发送后,生产者会继续执行之后的业务逻辑,但会进行设置一个回调函数,当broker成功反馈信息给生产者之后,会触发回调函数,回调函数的逻辑即是:判断发送消息是否成功
业务场景:
异步传输一般用于响应时间敏感的业务场景(异步消息的发送是时间优先为主,安全性是考虑次要的,可能会造成消息丢失,但是系统吞吐量大大增加)
异步消息对应生产者代码:
public class AsyncProducer { public static void main(String[] args) throws Exception { DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); producer.setNamesrvAddr("172.16.253.101:9876");//NameServer中存储着topic与broker的映射关系 producer.start(); producer.setRetryTimesWhenSendAsyncFailed(0);//当发送消息失败后 就不会再重试。重试次数为0 int messageCount = 100; //countDownLatch对象为了保证messageCount条反馈都被生产者接收到 final CountDownLatch countDownLatch = new CountDownLatch(messageCount); for (int i = 0; i < messageCount; i++) { try { final int index = i; //创建消息对象,指定topic,通过NameServer中映射关系可以找到访问的是哪些broker Message msg = new Message("Jodie_topic_1023", //指定tag "TagA", "OrderID188", "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); //生产者给消费者发送消息并且搞一个回调函数,当有反馈时 会执行回调函数 producer.send(msg, new SendCallback() { @Override public void onSuccess(SendResult sendResult) { //反馈中表示成功则执行onSuccess countDownLatch.countDown();//countDownLatch次数-1 System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId()); } @Override public void onException(Throwable e) { //反馈中表示异常则执行onException countDownLatch.countDown();//countDownLatch次数-1 System.out.printf("%-10d Exception %s %n", index, e); e.printStackTrace(); } }); } catch (Exception e) { e.printStackTrace(); } } System.out.println("=============");//for循环执行完(即生产者发送完消息 无需等待反馈) 即可执行该句代码 countDownLatch.await(5, TimeUnit.SECONDS);//等待反馈最多等待5秒钟 producer.shutdown(); } }
(3) 单向消息
如果生产者发送的是单向消息,当发送后,生产者会继续执行之后的业务逻辑,但是单向消息和同步消息与异步消息区别在于无需让broker给生产者作出反馈 !当生产者发送消息给broker之后 此次发送消息的任务即结束了 !
应用场景:
生产者发送消息后不需要等待任何回复,直接进行之后的业务逻辑,单向传输用于中等可靠性的情况,例如:日志收集
(即使说单向消息是不靠谱的,既不安全,速度也不如异步消息,但是对于日志收集来说无所谓吧)
单向消息对应生产者代码:
public class OnewayProducer { public static void main(String[] args) throws Exception { DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); producer.setNamesrvAddr("172.16.253.101:9876"); producer.start(); for (int i = 0; i < 100; i++) { Message msg = new Message("TopicTest" /* Topic */, "TagA" /* Tag */, ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */ ); producer.sendOneway(msg); } System.out.println("for循环执行完(即生产者发送完消息 无需等待broker反馈) 即可执行该句代码"); Thread.sleep(5000); producer.shutdown(); } }
3.顺序消息
顺序消息指的是消费者消费消息的顺序按照发送者发送消息的顺序执行。顺序消息分为两种:局部顺序和全局顺序
局部顺序和全局顺序对比:
局部顺序:
局部消息指的是消费者消费某个topic的某个队列中的消息是顺序的。
分析:
局部顺序的意思为:针对于一个队列来说是从前往后来进行消费消息的。意思为先消费第一个队列中的部分消息后又消费第二个队列中的
消息,局部顺序不是说消费完一个队列中的所有消息才可以消费其他队列中的消息,只要对每一个队列来说是按照顺序消费的即可!
如图:只要针对于一个队列来说是从前往后消费,那么就是局部顺序消费
全局顺序:
消费者消费全部消息都是顺序的,只能通过一-个某个topic只有一个队列才能实现,这种应用场景较少,且性能较差。但是非要实现也是
可以实现的。
总结:
(1) 局部顺序消费是针对于一个队列来说是从前往后消费的。
(2) 局部顺序消费相对于全局顺序消费更加适用于开发
(3) 通常采用局部顺序消费消息数据,增加高可用性
局部顺序消费消息数据演示:
生产者:
public class OrderProducer { public static void main(String[] args) throws Exception { //Instantiate with a producer group name. MQProducer producer = new DefaultMQProducer("example_group_name"); //名字服务器NameServer的地址已经在环境变量中配置好了:NAMESRV_ADDR=172.16.253.101:9876 producer.start(); for (int i = 0; i < 10; i++) { //orderId :0-9 int orderId = i; for(int j = 0 ; j <= 5 ; j ++){ Message msg = new Message("OrderTopicTest", "order_"+orderId, "KEY" + orderId, //第四个参数是定义消息的内容:j: 0-5 ("order_"+orderId+" step " + j).getBytes(RemotingHelper.DEFAULT_CHARSET)); SendResult sendResult = producer.send(msg, new MessageQueueSelector() { @Override public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) { Integer id = (Integer) arg;//arg即是orderId //OrderTopicTest对应两个broker,一个broker四个队列,那么mqs.size()==8 // orderId:[0,9]->arg:[0,9]->id:[0.9] index=[0,9]%8==[0,7] int index = id % mqs.size(); //根据index的范围[0,7]获取八个队列中对应的队列 return mqs.get(index); } }, orderId); System.out.printf("%s%n", sendResult); } } //server shutdown producer.shutdown(); } }
消费者:
public class OrderConsumer { public static void main(String[] args) throws MQClientException { DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("example_group_name"); //名字服务器NameServer的地址已经在环境变量中配置好了:NAMESRV_ADDR=172.16.253.101:9876 consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); //订阅topic为"OrderTopicTest"对应的所有broker,“*”表示获取所有消息数据 无排除 consumer.subscribe("OrderTopicTest", "*"); //new MessageListenerOrderly():表示按局部顺序获取消息数据。 consumer.registerMessageListener(new MessageListenerOrderly() { @Override public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) { context.setAutoCommit(true); for(MessageExt msg:msgs){ System.out.println("消息内容:"+new String(msg.getBody())); } return ConsumeOrderlyStatus.SUCCESS; } }); consumer.start(); System.out.printf("Consumer Started.%n"); } }
测试:
(1)启动两个消费者和一个生产者
(2) 一共0到9 10个部分的消息
消费者1消费012389
消费者2消费4567这四类消息
乱序消费:
消费者消费消息不需要关注消息的顺序。消费者使用
4.广播消息
广播是向主题(topic)的所有订阅者发送消息。订阅同一个topic的多个消费者,能全量收到生产者发送的所有消息
之前发送的消息,当一个消费者消费过后,该消费过的消息就不可以再被其他消费者消费了,但是广播消息不同 !
生产者:
public class BroadcastProducer { public static void main(String[] args) throws Exception { DefaultMQProducer producer = new DefaultMQProducer("ProducerGroupName"); producer.start(); for (int i = 0; i < 100; i++){ Message msg = new Message("TopicTest", "TagA", "OrderID188", ("Hello world"+i).getBytes(RemotingHelper.DEFAULT_CHARSET)); SendResult sendResult = producer.send(msg); System.out.printf("%s%n", sendResult); } producer.shutdown(); } }
消费者:
public class BroadcastConsumer { public static void main(String[] args) throws Exception { DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("example_group_name"); consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET); //开启广播模式 consumer.setMessageModel(MessageModel.BROADCASTING); consumer.subscribe("TopicTest", "*"); consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { for(MessageExt msg:msgs){ System.out.println("消息内容:"+new String(msg.getBody())); } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); consumer.start(); System.out.printf("Broadcast Consumer Started.%n"); } }
细节:消费者类开启广播模式
5.延迟消息
延迟消息与普通消息的不同之处在于,它们要等到指定的时间之后才会被传递。
如图:
生产者端设置一个延迟时间的等级,生产者想要发送一个消息给broker,会存到一个死信队列中延迟等待指定的时间,等到延迟时间
之后才会被传递。
6.批量消息
批量发送消息提高了传递小消息的性能。意思即是对一大堆小量消息合到一起进行发送,批量发送也只使用一次IO 提升传递性能。
超出限制的批量信息:
官方建议批量消息的总大小不应该超过1m,实际上不应该超过4m。如果超过4m的批量消息需要进行分批处理,同时设置broker的配置
参数为4m。
总结:
如果消息大小超过4m,那么就要把消息批量发送,分批为多个批量发送,但是每一个批量不可以超过4m。这样还是提高了发送小消息的
性能。
7.过滤消息
在大多数情况下,标签是一种简单而有用的设计,可以用来选择您想要的消息
生产者:
public class TagProducer { public static void main(String[] args) throws Exception { //1.创建生产者 DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); //2.启动生产者 producer.start(); String[] tags = new String[] {"TagA", "TagB", "TagC"}; for (int i = 0; i < 15; i++) { //3.创建消息对象:定义该消息对象对应的topic主题, Message msg = new Message("TagFilterTest", //tag标签标识过滤,一个消息只可以对应一个tag,i % tags.length->[0,2] tags[i % tags.length], //消息内容 "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); //4.发送消息对象 SendResult sendResult = producer.send(msg); System.out.printf("%s%n", sendResult); } //5.关闭生产者 producer.shutdown(); } }
消费者:
public class TagConsumer { public static void main(String[] args) throws MQClientException { //1.创建生产者 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name"); //2.消费者订阅的topic主题为"TagFilterTest",指定tag标识表示过滤,只可以拿到TagA与TagC对应的Message对象 consumer.subscribe("TagFilterTest", "TagA || TagC"); //3.设置一个监听器,乱序读取 consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); consumer.start(); System.out.printf("Consumer Started.%n"); } }
消费者将收到包含TAGA或TAGB或TAGC的消息。但是限制是一条消息只能有一个tag标签,这可能不适用于复杂的场景。在这种情况下,
可以使用SQL表达式来过滤掉消息:
演示如下:
1.生产者
public class SQLProducer { public static void main(String[] args) throws Exception { DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); producer.start(); String[] tags = new String[] {"TagA", "TagB", "TagC"}; for (int i = 0; i < 15; i++) { Message msg = new Message("SqlFilterTest", tags[i % tags.length], ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) ); // 前面逻辑和单独Tag标签过滤一致 //这里多设置一个属性:"a",并且每一次设置一个对应的值。键值对形式 msg.putUserProperty("a", String.valueOf(i)); SendResult sendResult = producer.send(msg); System.out.printf("%s%n", sendResult); } producer.shutdown(); } }
2.消费者
public class SQLConsumer { public static void main(String[] args) throws MQClientException { DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name"); // Don't forget to set enablePropertyFilter=true in broker consumer.subscribe("SqlFilterTest", //使用的是SQL过滤。 //过滤1.使用Tag标签过滤一次 MessageSelector.bySql("(TAGS is not null and TAGS in ('TagA', 'TagB'))" + //过滤2.使用生产者设置的a属性进行范围过滤 "and (a is not null and a between 0 and 3)")); consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); consumer.start(); System.out.printf("Consumer Started.%n"); } }
过滤消息逻辑的执行位置?
8.事务消息
事务消息的定义:
它可以被认为是一共两阶段的提交消息实现,以确保分布式系统的最终一致性。
事务性消息确保本地事务的执行和消息的发送可以原子地执行。
【分析:原子性的意思是:如果本地事务的执行为CommitTransaction提交事务,允许消费者消费该消息,那么消息就应该从broker发送给消费者。如果本地事务的执行为RollbackTransaction回滚事务,表示该消息将被删除,不允许消费者消费该消息,那么消息就不应该从broker发送给消费者 就应该被丢弃。这就是原子性,确保本地事务的执行逻辑和消息的发送与否是一致的 !】
事务消息有三种状态:
生产者:
public class TransactionProducer { public static void main(String[] args) throws Exception { //1.创建一个事务监听器对象 具体实现是自己编写TransactionListenerImpl类的逻辑 TransactionListener transactionListener = new TransactionListenerImpl(); //2.创建生产者 TransactionMQProducer producer = new TransactionMQProducer("please_rename_unique_group_name"); //3.设置NameServer producer.setNamesrvAddr("172.16.253.101:9876"); //4.设置一个线程池 ExecutorService executorService = new ThreadPoolExecutor(2, 5, 100, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(2000), new ThreadFactory() { @Override public Thread newThread(Runnable r) { Thread thread = new Thread(r); thread.setName("client-transaction-msg-check-thread"); return thread; } }); //5.把线程池设置给生产者 producer.setExecutorService(executorService); //6.给生产者设置事务实现类 producer.setTransactionListener(transactionListener); producer.start(); String[] tags = new String[] {"TagA", "TagB", "TagC", "TagD", "TagE"}; for (int i = 0; i < 10; i++) { try { Message msg = //7.创建消息对象: //i % tags.length即是:i->[0,9] tags.length->5 [0,9]%5<==>{0,1,2,3,4}<==>[0,4] //1.设置topic为TopicTest //2.一共10个Message消息对象,五个Tag标签,每一个Tag标签分配 2个 Message对象 new Message("TopicTest", tags[i % tags.length], "KEY" + i, //3.消息内容: ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET)); //8.以事务的形式进行提交Message消息对象【提交消息的逻辑即是第6点中设置给生产者的transactionListener对象类中的实现逻辑】 SendResult sendResult = producer.sendMessageInTransaction(msg, null); System.out.printf("%s%n", sendResult); Thread.sleep(10); } catch (MQClientException | UnsupportedEncodingException e) { e.printStackTrace(); } } for (int i = 0; i < 100000; i++) { Thread.sleep(1000); } producer.shutdown(); } }
消费者:
public class TransactionConsumer { public static void main(String[] args) throws MQClientException { //1.创建消费者对象 DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("my-consumer-group1"); //2.指明nameserver的地址 consumer.setNamesrvAddr("172.16.253.101:9876"); //3.订阅主题:topic 和过滤消息用的tag表达式 consumer.subscribe("TopicTest","*"); //4.创建一个监听器,当broker把消息推过来时调用 consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { for (MessageExt msg : msgs) { // System.out.println("收到的消息:"+new String(msg.getBody())); System.out.println("收到的消息:"+msg); } return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); //5.启动消费者 consumer.start(); System.out.println("消费者已启动"); } }
事务监听器实现类:
public class TransactionListenerImpl implements TransactionListener { @Override public LocalTransactionState executeLocalTransaction(Message msg, Object arg) { String tags = msg.getTags(); if(StringUtils.contains(tags,"TagA")){ return LocalTransactionState.COMMIT_MESSAGE; }else if(StringUtils.contains(tags,"TagB")){ return LocalTransactionState.ROLLBACK_MESSAGE; }else{ return LocalTransactionState.UNKNOW; } } @Override public LocalTransactionState checkLocalTransaction(MessageExt msg) { String tags = msg.getTags(); if(StringUtils.contains(tags,"TagC")){ return LocalTransactionState.COMMIT_MESSAGE; }else if(StringUtils.contains(tags,"TagD")){ return LocalTransactionState.ROLLBACK_MESSAGE; }else{ return LocalTransactionState.UNKNOW; } } }
测试:
提出疑问:
1.生产者中:
2.事务执行逻辑:
回查机制:
对于UNKNOW标记的事务,进行回查,但是回查一般需要隔一段时间,并且回查的次数上限在15次左右,当15次还是没有回查到,那么
回查机制失效,放弃该UNKNOW标记的事务。
3.消费者中最终消费:
(1)首先立即消费到两条TagA标记的Message
(2)等待一段时间后依次消费到两条TagC标记的Message【回查机制】
事务消息消费的流程:
模拟电商 下单支付的场景:
(1) 上游 (用户即是生产者) 进行下单 但是下单成功后未进行支付,那么此时对于下单这个事务来说 就是一个half消息,是不可以直接发送
给下游 (消费者) 进行执行下单之后的逻辑的【下单之后的逻辑一般包括:增加积分,减少商品库存等等】。
(2)
如果用户进行下单成功但是未支付的话,此时事务消息在broker中的状态为UNKNOW状态,表示未知状态,但是会有一个回查检索的功
能,一般电商的检索时间间隔为5分钟,若5min还没支付,那么订单取消。若在检索时间内进行支付,那么这个事务消息在broker中转换
为Commit提交状态 然后从broker发送给消费者
如果用户直接下单失败的话,会直接把这个事务消息在broker中转换为Rollback回滚状态 然后从broker中丢弃 !
(3) 但是如果用户下单后并且支付成功的话,会直接把这个事务消息在broker中直接转换为Commit提交状态 然后从broker发送给消费者
五:SpringBoot中整合RocketMQ的一般消息
编写生产者:
1.导入依赖
<dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency>
2.配置配置文件
#应用名称 spring.application.name=rocketmq-myboot #应用程序 WEB 访问端口 server.port=8080 #配置NameServer rocketmq.name-server=”服务器ip地址“:9876 #生产者组 rocketmq.producer.group=my-boot-producer-group
3.使用IOC容器中的对象
@Component public class MyProducer { @Autowired private RocketMQTemplate rocketMQTemplate ; public void sendMessage(String topic,String message) { //生产者和消费者都会进行订阅NameServer这一服务中心 这一服务中心上具有Topic和Broker的映射关系 //因为Broker可能是一个集群,你必须确定Topic是存在于哪一个Broker上面的 ! //Topic实际上是逻辑生成在NameServer这个服务中心上的,但是真正的物理存储是存储在Broker上的 //生产者发送消息Message给到Broker中的Topic Topic中具有MessageQueue管道可以接收到Message消息对象 rocketMQTemplate.convertAndSend(topic,message) ; } }
4.测试
(1) 声明topic
(2) 定义待发送的Message对象
(3) 调用封装的MyProducer对象的sendMessage方法
@SpringBootTest public class Test01 { @Autowired private MyProducer myProducer ; @Test void testSendMessage() { String topic = "my-boot-topic" ; String message = "hello Spring rocketMQ" ; myProducer.sendMessage(topic,message) ; System.out.println("消息发送成功"); } }
编写消费者:
(1) 导入依赖
<dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency>
(2) 编写配置文件
spring.application.name=rocketmq-myboot-consumer server.port=8080 #配置NameServer rocketmq.name-server=”服务器ip地址“:9876 #消费者组 rocketmq.consumer.group=my-boot-producer-group
(3) 编写消费者类