依赖:
org.apache.rocketmq
rocketmq-client
4.4.0
消息发送者步骤分析
1.创建消息生产者producer,并制定生产者组名
2.指定Nameserver地址
3.启动producer
4.创建消息对象,指定主题Topic、Tag和消息体
5.发送消息
6.关闭生产者producer
消息消费者步骤分析
1.创建消费者Consumer,制定消费者组名
2.指定Nameserver地址
3.订阅主题Topic和Tag
4.设置回调函数,处理消息
5.启动消费者consumer
这种可靠性同步地发送方式使用的比较广泛,比如:重要的消息通知,短信通知。
/**
* 发送同步消息
*/
public class SyncProducer {
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
//1.创建消息生产者producer,并制定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("group1");
//2.指定Nameserver地址,集群地址用逗号隔开
producer.setNamesrvAddr("192.168.59.131:9876");
//3.启动producer
producer.start();
for (int i = 0; i < 10; i++) {
//4.创建消息对象,指定主题Topic、Tag和消息体
/**
* 参数一:消息主题Topic
* 参数二:消息Tag
* 参数三:消息内容
*/
Message message = new Message("base", "Tag1", ("hello world" + i).getBytes());
//5.发送消息
SendResult send = producer.send(message);
System.out.println(send.toString());
//线程睡1秒
TimeUnit.MILLISECONDS.sleep(1);
}
//6.关闭生产者producer
producer.shutdown();
}
}
异步消息通常用在对响应时间敏感的业务场景,即发送端不能容忍长时间地等待Broker
的响应。
/**
* 发送异步消息
*/
public class AsyncProducer {
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
//1.创建消息生产者producer,并制定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("group1");
//2.指定Nameserver地址,集群地址用逗号隔开
producer.setNamesrvAddr("192.168.59.131:9876");
//3.启动producer
producer.start();
//使用减法计数器防止主线程结束了,异步线程还没执行完
final CountDownLatch countDownLatch = new CountDownLatch(10);
for (int i = 0; i < 10; i++) {
//4.创建消息对象,指定主题Topic、Tag和消息体
/**
* 参数一:消息主题Topic
* 参数二:消息Tag
* 参数三:消息内容
*/
Message message = new Message("base", "Tag2", ("hello world" + i).getBytes());
//5.发送异步消息
producer.send(message, new SendCallback() {
/**
* 发送成功回调函数
*/
public void onSuccess(SendResult sendResult) {
//减法计数器减一
countDownLatch.countDown();
System.out.println("发送结果:"+sendResult);
}
/**
* 发送失败回调函数
*/
public void onException(Throwable throwable) {
//减法计数器减一
countDownLatch.countDown();
System.out.println("发送异常:"+throwable);
}
});
}
//延迟主线程的执行时间,避免主线程结束了,异步线程还没执行完,出现No route info of this topic的异常
//或者使用CountDownLatch解决
// Thread.sleep(3000);
//必要的任务执行完,等待计数器归零,才向下执行
countDownLatch.await();
//6.关闭生产者producer
producer.shutdown();
}
}
这种方式主要用在不特别关心发送结果的场景,例如:日志发送。
没有返回值的,是不知道有没有发送成功,除非使用消费者去消费
/**
* 单向发送消息
*/
public class OnewayProducer {
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
//1.创建消息生产者producer,并制定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("group1");
//2.指定Nameserver地址,集群地址用逗号隔开
producer.setNamesrvAddr("192.168.59.131:9876");
//3.启动producer
producer.start();
for (int i = 0; i < 10; i++) {
//4.创建消息对象,指定主题Topic、Tag和消息体
/**
* 参数一:消息主题Topic
* 参数二:消息Tag
* 参数三:消息内容
*/
Message message = new Message("base", "Tag3", ("hello world,单向消息" + i).getBytes());
//5.发送单向消息,无返回值
producer.sendOneway(message);
//线程睡1秒
TimeUnit.MILLISECONDS.sleep(1);
}
//6.关闭生产者producer
producer.shutdown();
}
}
因为之前测试同步,异步,单向消息发送时,已经往队列里添加了消息,现在测试消费者,直接去队列里消费即可,消费了消息,要发送者重新发送消息,才能继续消费。
/**
* 消息的接受者
* 1.创建消费者Consumer,制定消费者组名
* 2.指定Nameserver地址
* 3.订阅主题Topic和Tag
* 4.设置回调函数,处理消息
* 5.启动消费者consumer
*/
public class Consumer {
private static void run() throws MQClientException {
//1.创建消费者Consumer,制定消费者组名 推模式Broker将消息推给消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
//2.指定Nameserver地址
consumer.setNamesrvAddr("192.168.59.131:9876");
//3.订阅主题Topic和Tag
consumer.subscribe("base","Tag1");
//4.设置回调函数,处理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
//接受消息内容
public ConsumeConcurrentlyStatus consumeMessage(List list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
System.out.println(new String(messageExt.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//5.启动消费者consumer
consumer.start();
}
public static void main(String[] args) throws MQClientException {
run();
}
}
消费者采用负载均衡方式消费消息,多个消费者共同消费队列消息,每个消费者处理的消息不同。默认的消费模式。
启动上面的消费者三个,然后启动同步消息提供者。
提供者发送十条消息,看看十条消息是不是分散到三个消费者中消费。
确实如此。
消费者采用广播的方式消费消息,每个消费者消费的消息都是相同的
/**
* 广播模式
*/
private static void run1() throws MQClientException {
//1.创建消费者Consumer,制定消费者组名 推模式Broker将消息推给消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
//2.指定Nameserver地址
consumer.setNamesrvAddr("192.168.59.131:9876");
//3.订阅主题Topic和Tag
consumer.subscribe("base","Tag1");
//设置消费模式:广播模式
consumer.setMessageModel(MessageModel.BROADCASTING);
//4.设置回调函数,处理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
//接受消息内容
public ConsumeConcurrentlyStatus consumeMessage(List list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
System.out.println(new String(messageExt.getBody()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//5.启动消费者consumer
consumer.start();
}
public static void main(String[] args) throws MQClientException {
// run();
//广播模式
run1();
}
启动两个消费者,然后启动同步消息提供者。
提供者发送十条消息:
消费者1:
消费者2:
每个消费者消费的消息都是相同的!
消息有序指的是可以按照消息的发送顺序来消费(FIFO)。RocketMQ可以严格的保证消息有序,可以分为分区有序或者全局有序。
在默认的情况下消息发送会采取Round Robin
轮询方式把消息发送到不同的queue
(分区队列);而消费消息的时候以多线程的方式从多个queue
上拉取消息,这种情况发送和消费是不能保证顺序。
但是如果控制发送的顺序消息只依次发送到同一个queue
中,消费的时候只从这个queue
上依次拉取,则就保证了顺序。
局部消息顺序:
一个topic在一个broker上创建队列数量defaultTopicQueueNums=8 默认为8
当发送和消费参与的queue只有一个,则是全局有序;
如果多个queue参与,则为分区有序,即相对每个queue,消息都是有序的。
一般来讲,我们只需要保证局部的顺序消息就可以了,说白了就是同一个业务相关的消息,发到同一个队列中就可以了,消费的时候,取队列的消息的时候,采用一个线程的方式去取同一个队列的消息。
下面用订单进行分区有序的示例。一个订单的顺序流程是:创建、付款、推送、完成。订单号相同的消息会被先后发送到同一个队列中,消费时,同一个OrderId获取到的肯定是同一个队列。
构造消息集合:
/**
* 订单的步骤
*/
public class OrderStep {
private long orderId;
private String desc;
public long getOrderId() {
return orderId;
}
public void setOrderId(long orderId) {
this.orderId = orderId;
}
public String getDesc() {
return desc;
}
public void setDesc(String desc) {
this.desc = desc;
}
@Override
public String toString() {
return "OrderStep{" +
"orderId=" + orderId +
", desc='" + desc + '\'' +
'}';
}
/**
* 生成模拟订单数据 3个订单
*/
public static List buildOrders() {
//1039 :创建 付款 推送 完成 取余3
//1065 :创建 付款 完成 取余1
//7235 :创建 付款 完成 取余3
List orderList = new ArrayList();
OrderStep orderDemo = new OrderStep();
orderDemo.setOrderId(1039L);
orderDemo.setDesc("创建");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(1065L);
orderDemo.setDesc("创建");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(1039L);
orderDemo.setDesc("付款");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(7235L);
orderDemo.setDesc("创建");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(1065L);
orderDemo.setDesc("付款");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(7235L);
orderDemo.setDesc("付款");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(1065L);
orderDemo.setDesc("完成");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(1039L);
orderDemo.setDesc("推送");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(7235L);
orderDemo.setDesc("完成");
orderList.add(orderDemo);
orderDemo = new OrderStep();
orderDemo.setOrderId(1039L);
orderDemo.setDesc("完成");
orderList.add(orderDemo);
return orderList;
}
}
生产者:
public class Producer {
private static void run() throws MQClientException, InterruptedException, RemotingException, MQBrokerException {
//1.创建消息生产者producer,并制定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("group1");
//2.指定Nameserver地址,集群地址用逗号隔开
producer.setNamesrvAddr("192.168.59.131:9876");
//3.启动producer
producer.start();
//构建订单步骤集合
List orderSteps = OrderStep.buildOrders();
for (int i = 0; i < orderSteps.size(); i++) {
/**
* 4.创建消息对象,指定主题Topic、Tag和消息体
* 参数一:消息主题Topic
* 参数二:消息Tag
* 参数三:消息key
* 参数四:消息内容
*/
Message message = new Message("OrderTopic", "order", "i"+i,orderSteps.get(i).toString().getBytes());
/**
* 5.发送消息
* 参数一:消息对象
* 参数二:消息队列选择器
* 参数三:选择队列的业务标识(订单id)
*/
SendResult sendResult = producer.send(message, new MessageQueueSelector() {
/**
* 选择队列
* @param list 队列集合 默认四个队列,要修改的话broker配置文件defaultTopicQueueNums=x
* @param message 消息对象
* @param o 业务表示的参数
* @return
*/
@Override
public MessageQueue select(List list, Message message, Object o) {
Long orderId = (Long) o;
//取余,选取队列
long l = orderId % list.size();
return list.get((int) l);
}
}, orderSteps.get(i).getOrderId());
System.out.println("发送结果:"+sendResult);
}
//6.关闭生产者producer
producer.shutdown();
}
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
run();
}
}
public class Consumer {
private static void run() throws MQClientException {
//1.创建消费者Consumer,制定消费者组名 推模式Broker将消息推给消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
//2.指定Nameserver地址
consumer.setNamesrvAddr("192.168.59.131:9876");
//3.订阅主题Topic和Tag *表示这个主题下的所有消息都消费
consumer.subscribe("OrderTopic","*");
//4.设置回调函数,处理消息
//MessageListenerOrderly在进行消息消费的时候,对于一个队列的消息用一个线程处理
consumer.registerMessageListener(new MessageListenerOrderly(){
@Override
public ConsumeOrderlyStatus consumeMessage(List list, ConsumeOrderlyContext consumeOrderlyContext) {
for (MessageExt messageExt : list) {
System.out.println("线程名:"+Thread.currentThread().getName()+"消费消息:"+new String(messageExt.getBody()));
}
return ConsumeOrderlyStatus.SUCCESS;
}
});
//5.启动消费者consumer
consumer.start();
System.out.println("消费者启动---------------------------");
}
public static void main(String[] args) throws MQClientException {
run();
}
}
启动生产者
订单1065消息集合的消费顺序:创建->付款->完成 由线程2按顺序单独消费完
1039:创建->付款->推送->完成 由线程1按顺序单独消费完
7235:创建->付款->完成 由线程1按顺序单独消费完
提供者再次发送消息时,发现订单的消息消费顺序没变,但是由多个线程消费完
订单1065消息集合的消费顺序:创建->付款->完成 由线程4,7,9共同消费完
1039:创建->付款->推送->完成
7235:创建->付款->完成
开多个消费者来测试:也是能按顺序消费的,而且是负载均衡的模式去消费
比如电商里,提交了一个订单就可以发送一个延时消息,1h后去检查这个订单的状态,如果还是未付款就取消订单释放库存。
生产者:其实就加一句代码message.setDelayTimeLevel(2);
/**
* 发送同步消息
*/
public class SyncProducer {
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
//1.创建消息生产者producer,并制定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("group1");
//2.指定Nameserver地址,集群地址用逗号隔开
producer.setNamesrvAddr("192.168.59.131:9876");
//3.启动producer
producer.start();
for (int i = 0; i < 10; i++) {
//4.创建消息对象,指定主题Topic、Tag和消息体
/**
* 参数一:消息主题Topic
* 参数二:消息Tag
* 参数三:消息内容
*/
Message message = new Message("DelayTopic", "tag1", ("hello world" + i).getBytes());
//设置延迟时间,延迟等级2
message.setDelayTimeLevel(2);
//5.发送消息
SendResult send = producer.send(message);
System.out.println(send.toString());
}
//6.关闭生产者producer
producer.shutdown();
}
}
消费者:
public class DelayConsumer {
private static void run() throws MQClientException {
//1.创建消费者Consumer,制定消费者组名 推模式Broker将消息推给消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
//2.指定Nameserver地址
consumer.setNamesrvAddr("192.168.59.131:9876");
//3.订阅主题Topic和Tag
consumer.subscribe("DelayTopic","tag1");
//4.设置回调函数,处理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
//接受消息内容
@Override
public ConsumeConcurrentlyStatus consumeMessage(List list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
System.out.println("消息ID:"+messageExt.getMsgId()+" 延迟时间:"+(System.currentTimeMillis()-messageExt.getStoreTimestamp()));
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//5.启动消费者consumer
consumer.start();
System.out.println("消费者启动");
}
public static void main(String[] args) throws MQClientException {
run();
}
}
使用限制:
现在RocketMq
并不支持任意时间的延时,需要设置几个固定的延时等级,从1s到2h分别对应着等级1到18
// org/apache/rocketmq/store/config/MessageStoreConfig.java
private String messageDelayLevel = "1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h";
批量发送消息能显著提高传递小消息的性能。限制是这些批量消息应该有相同的topic
,相同的waitStoreMsgOK
,而且不能是延时消息。此外,这一批消息的总大小不应超过4MB
。
生产者:把消息放到集合里,直接send,不用for循环一个一个send
/**
* 发送同步消息
*/
public class BatchProducer {
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
//1.创建消息生产者producer,并制定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("group1");
//2.指定Nameserver地址,集群地址用逗号隔开
producer.setNamesrvAddr("192.168.59.131:9876");
//3.启动producer
producer.start();
//4.批量发送消息
List messageList=new ArrayList();
Message message1=new Message("base","tag1",("hello"+1).getBytes());
Message message2=new Message("base","tag1",("hello"+2).getBytes());
Message message3=new Message("base","tag1",("hello"+3).getBytes());
messageList.add(message1);
messageList.add(message2);
messageList.add(message3);
SendResult sendResult = producer.send(messageList);
System.out.println("发送结果:"+sendResult);
//6.关闭生产者producer
producer.shutdown();
}
}
消费者:
public class DatchConsumer {
private static void run() throws MQClientException {
//1.创建消费者Consumer,制定消费者组名 推模式Broker将消息推给消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
//2.指定Nameserver地址
consumer.setNamesrvAddr("192.168.59.131:9876");
//3.订阅主题Topic和Tag
consumer.subscribe("base","tag1");
//4.设置回调函数,处理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
//接受消息内容
@Override
public ConsumeConcurrentlyStatus consumeMessage(List list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
System.out.println(messageExt);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//5.启动消费者consumer
consumer.start();
System.out.println("消费者启动");
}
public static void main(String[] args) throws MQClientException {
run();
}
}
如果消息的总长度可能大于4MB时,这时候最好把消息进行分割:
public class ListSplitter implements Iterator> {
private final int SIZE_LIMIT = 1024 * 1024 * 4;
private final List messages;
private int currIndex;
public ListSplitter(List messages) {
this.messages = messages;
}
@Override
public boolean hasNext() {
return currIndex < messages.size();
}
@Override
public List next() {
int nextIndex = currIndex;
int totalSize = 0;
for (; nextIndex < messages.size(); nextIndex++) {
Message message = messages.get(nextIndex);
int tmpSize = message.getTopic().length() + message.getBody().length;
Map properties = message.getProperties();
for (Map.Entry entry : properties.entrySet()) {
tmpSize += entry.getKey().length() + entry.getValue().length();
}
tmpSize = tmpSize + 20; // 增加日志的开销20字节
if (tmpSize > SIZE_LIMIT) {
//单个消息超过了最大的限制
//忽略,否则会阻塞分裂的进程
if (nextIndex - currIndex == 0) {
//假如下一个子列表没有元素,则添加这个子列表然后退出循环,否则只是退出循环
nextIndex++;
}
break;
}
if (tmpSize + totalSize > SIZE_LIMIT) {
break;
} else {
totalSize += tmpSize;
}
}
List subList = messages.subList(currIndex, nextIndex);
currIndex = nextIndex;
return subList;
}
}
//把大的消息分裂成若干个小的消息
ListSplitter splitter = new ListSplitter(messages);
while (splitter.hasNext()) {
try {
List listItem = splitter.next();
producer.send(listItem);
} catch (Exception e) {
e.printStackTrace();
//处理error
}
}
在大多数情况下,TAG
是一个简单而有用的设计,其可以来选择您想要的消息。例如:
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("CID_EXAMPLE");
//*表示这个主题下的所有消息都消费
consumer.subscribe("TOPIC", "TAGA || TAGB || TAGC");
消费者将接收包含TAGA
或TAGB
或TAGC
的消息。
还可以使用SQL
表达式筛选消息。SQL
特性可以通过发送消息时的属性来进行计算。在RocketMQ
定义的语法下,可以实现一些简单的逻辑。下面是一个例子:
------------
| message |
|----------| a > 5 AND b = 'abc'
| a = 10 | --------------------> Gotten
| b = 'abc'|
| c = true |
------------
------------
| message |
|----------| a > 5 AND b = 'abc'
| a = 1 | --------------------> Missed
| b = 'abc'|
| c = true |
------------
RocketMQ
只定义了一些基本语法来支持这个特性。你也可以很容易地扩展它。
常量支持类型为:
只有使用push
模式的消费者才能用使用SQL92
标准的sql
语句,接口如下:
public void subscribe(finalString topic, final MessageSelector messageSelector)
消息生产者:
public class SqlProducer {
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
//1.创建消息生产者producer,并制定生产者组名
DefaultMQProducer producer = new DefaultMQProducer("group1");
//2.指定Nameserver地址,集群地址用逗号隔开
producer.setNamesrvAddr("192.168.59.131:9876");
//3.启动producer
producer.start();
for (int i = 0; i < 10; i++) {
//4.创建消息对象,指定主题Topic、Tag和消息体
/**
* 参数一:消息主题Topic
* 参数二:消息Tag
* 参数三:消息内容
*/
Message message = new Message("FilterTagTopic", "Tag1", ("hello world" + i).getBytes());
//给消息设置属性
message.putUserProperty("i",String.valueOf(i));
//5.发送消息
SendResult send = producer.send(message);
System.out.println(send.toString());
}
//6.关闭生产者producer
producer.shutdown();
}
}
消息消费者:
public class SqlConsumer {
private static void run() throws MQClientException {
//1.创建消费者Consumer,制定消费者组名 推模式Broker将消息推给消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group1");
//2.指定Nameserver地址
consumer.setNamesrvAddr("192.168.59.131:9876");
//3.订阅主题Topic和Tag
consumer.subscribe("FilterTagTopic", MessageSelector.bySql("i > 3"));
//4.设置回调函数,处理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
//接受消息内容
@Override
public ConsumeConcurrentlyStatus consumeMessage(List list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
System.out.println(messageExt);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//5.启动消费者consumer
consumer.start();
System.out.println("消费者启动");
}
public static void main(String[] args) throws MQClientException {
run();
}
}
RocketMQ
消息的发送分成2个阶段:Prepare阶段和确认阶段
。
prepare
阶段
HalfMsg
到消息中间件,消息中间件会为这个HalfMsg
生成一个全局唯一标识,生产者可以持有标识,以便下一阶段找到这个HalfMsg
;注意:消费者无法立刻消费HalfMsg
,生产者可以对HalfMsg
进行Commit
或者Rollback
来终结事务。只有当Commit
了HalfMsg
后,消费者才能消费到这条消息。
确认阶段
Commit
消息(包含之前HalfMsg
的唯一标识),中间件修改HalfMsg
的状态为【已提交】,然后通知消费者执行事务;Rollback
消息(包含之前HalfMsg
的唯一标识),中间件修改HalfMsg
的状态为【已取消】。消息中间件会定期去向生产者询问,是否可以Commit
或者Rollback
那些由于错误没有被终结的HalfMsg
,以此来结束它们的生命周期,以达成事务最终的一致。之所以需要这个询问机制,是因为生产者可能提交完本地事务,还没来得及对HalfMsg
进行Commit
或者Rollback
,就挂掉了,这样就会处于一种不一致状态。
ACK
机制
消费者消费完消息后,可能因为自身异常,导致业务执行失败,此时就必须要能够重复消费消息。RocketMQ
提供了ACK
机制,即RocketMQ
只有收到服务消费者的ack message
后才认为消费成功。
所以,服务消费者可以在自身业务员逻辑执行成功后,向RocketMQ
发送ack message
,保证消费逻辑执行成功。
生产者:
与之前不同的地方
得用事务生产者TransactionMQProducer
;
还得设置事务监听器setTransactionListener
;
发送事务消息;
public class TranProducer {
public static void main(String[] args) throws MQClientException, RemotingException, InterruptedException, MQBrokerException {
//1.创建消息生产者producer,并制定生产者组名
TransactionMQProducer producer = new TransactionMQProducer("group2");
//2.指定Nameserver地址,集群地址用逗号隔开
producer.setNamesrvAddr("192.168.59.131:9876");
//设置事务监听器
producer.setTransactionListener(new TransactionListener() {
/**
* 生产者在该方法中执行本地的事务
* @param message
* @param o
* @return
*/
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
if (StringUtils.equals("tag1",message.getTags())){
return LocalTransactionState.COMMIT_MESSAGE;
}else if (StringUtils.equals("tag2",message.getTags())){
return LocalTransactionState.ROLLBACK_MESSAGE;
}else if (StringUtils.equals("tag3",message.getTags())){
//即不提交事务也不回滚事务,因为询问机制,mq会回查是否可以Commit或者Rollback那些由于错误没有被终结的HalfMsg
return LocalTransactionState.UNKNOW;
}
return LocalTransactionState.UNKNOW;
}
/**
* 该方法是mq进行消息事务状态回查
* @param messageExt
* @return
*/
@Override
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
System.out.println("消息的tag:"+messageExt.getTags());
return LocalTransactionState.COMMIT_MESSAGE;
}
});
//3.启动producer
producer.start();
System.out.println("生产者启动");
String[] tag={"tag1","tag2","tag3"};
for (int i = 0; i < 3; i++) {
//4.创建消息对象,指定主题Topic、Tag和消息体
/**
* 参数一:消息主题Topic
* 参数二:消息Tag
* 参数三:消息内容
*/
Message message = new Message("TransactionTopic", tag[i], ("hello world" + i).getBytes());
//5.发送事务消息
//第二个参数,在进行事务控制的时候,可以将事务应用到某一个消息上,
//也可以应用到producer上,该producer发送的消息都会进行事务控制
//null表示应用到producer
SendResult send = producer.sendMessageInTransaction(message,null);
System.out.println(send.toString());
//线程睡1秒
TimeUnit.MILLISECONDS.sleep(1);
}
//6.关闭生产者producer
//由于mq要回查生产者,所以不要停了生产者
// producer.shutdown();
}
}
消费者:与之前没啥不同
public class TranConsumer {
private static void run() throws MQClientException {
//1.创建消费者Consumer,制定消费者组名 推模式Broker将消息推给消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("group2");
//2.指定Nameserver地址
consumer.setNamesrvAddr("192.168.59.131:9876");
//3.订阅主题Topic和Tag
consumer.subscribe("TransactionTopic","*");
//4.设置回调函数,处理消息
consumer.registerMessageListener(new MessageListenerConcurrently() {
//接受消息内容
@Override
public ConsumeConcurrentlyStatus consumeMessage(List list, ConsumeConcurrentlyContext consumeConcurrentlyContext) {
for (MessageExt messageExt : list) {
System.out.println(messageExt.getTags());
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//5.启动消费者consumer
consumer.start();
System.out.println("消费者启动");
}
public static void main(String[] args) throws MQClientException {
run();
}
}
测试:
生产者成功发送三条消息到mq
,但是tag2
的消息被回滚了,tag3
的消息先是即不回滚也不提交,然后被mq
进行消息事务状态回查时,给提交了。所以猜测消费者能消费tag1
和tag3
的消息。
消费者: