前言
上次分享介绍了Java整合RocketMQ-client的方式,并且简单的了解了生产者生产数据,消费者消费数据的整个流程,这次,我们详细的了解一下生产者和几种消息类别
一、生产者概述
发送消息的一方被称为生产者,我们先了解下几个基础概念
生产者组:一个逻辑概念,在使用生产者实例的时候需要指定一个组名。一个生产者组可以生产多个Topic的消息。
生产者实例:一个生产者组部署了多个进程,每个进程都可以称为一个生产者实例。
Topic:主题名字,一个Topic由若干Queue组成。
RocketMQ 客户端中的生产者有两个独立实现类:
- org.apache.rocketmq.client.producer.DefaultMQProducer
- org.apache.rocketmq.client.producer.TransactionMQProducer。
前者用于生产普通消息、顺序消息、单向消息、批量消息、延迟消息,后者主要
用于生产事务消息
二、RocketMQ五种消息类型
1.普通消息
普通消息也称为并发消息,和传统的队列相比,并发消息没有顺序, 但是生产消费都是并行进行的,单机性能可达十万级别的TPS。
再上一章中介绍的方式就是普通消息,这里就不再重复示例,忘记的小伙伴可以移步复习一下
2.有序消息
与Kafka中的分区类似,把一个Topic消息分为多个分区“保存”和消费,在一个分区内的消息就是传统的队列,遵循FIFO(先进先出)原则。 简单的理解就是把需要按序发送的信息放在同一个队列中。
//创建DefaultMQProducer消息生产者对象
DefaultMQProducer producer = new DefaultMQProducer("TestProducerGroup");
//设置NameServer
producer.setNamesrvAddr("192.168.2.5:9876");
//设置NameServer节点地址,多个节点间用分号分割
try {
//与NameServer建立长连接
producer.start();
//设置个集合,集合信息中是要发送订单消息、支付消息、物流消息。要保证相同订单号的在同一个队列
List<JSONObject> orderInfos = new ArrayList<>();
JSONObject orderInfo1 = new JSONObject();
orderInfo1.put("orderId",3347);
orderInfo1.put("info","我是订单消息");
orderInfos.add(orderInfo1);
JSONObject orderInfo2 = new JSONObject();
orderInfo2.put("orderId",3348);
orderInfo2.put("info","我是订单消息");
orderInfos.add(orderInfo2);
JSONObject orderInfo3 = new JSONObject();
orderInfo3.put("orderId",3347);
orderInfo3.put("info","我是订单支付消息");
orderInfos.add(orderInfo3);
JSONObject orderInfo4 = new JSONObject();
orderInfo4.put("orderId",3348);
orderInfo4.put("info","我是订单支付消息");
orderInfos.add(orderInfo4);
JSONObject orderInfo5 = new JSONObject();
orderInfo5.put("orderId",3347);
orderInfo5.put("info","我是订单物流消息");
orderInfos.add(orderInfo5);
for (JSONObject orderInfo : orderInfos) {
int orderId = orderInfo.getInteger("orderId");
Message msg = new Message("TopicOrder", "orderinfo", orderId+"",
orderInfo.toJSONString().getBytes());
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
//附加参数 producer.send方法中的第三个参数
int id = (Integer) arg;
System.out.println("*******************orderId:"+id+"***********************");
//和队列总数取余,来决定选择哪个队列,因为相同的订单ID取余的结果是一样的,这样就能保证相同的订单,我们选择的是同一个队列
int index = id % mqs.size();
MessageQueue messageQueue = mqs.get(index);
System.out.println("队列:"+messageQueue);
return messageQueue;
}
}, orderId);
}
}catch (Exception e){
e.printStackTrace();
}finally {
try {
producer.shutdown();
} catch (Exception e) {
}
}
执行之后,我们可以看到以下打印消息:
从信息中,我们不难看出,相同ID的信息,我们是通过同一个队列发送的。
3.延迟消息
消息发送后,消费者要在一定时间后,或者指定某个时间点才可以消费。在没有延迟消息时,基本的做法是基于定时计划任务调度,定时发送消息。在RocketMQ中只需要在发送消息时设置延迟级别即可实现。
Apache RocketMQ 一共支持18个等级的延迟投递,具体时间如下:
由于只有固定的18个延迟级别,所以通常我们很少使用这种方式,大部分会通过定时任务去执行,而不是通过延迟消息,所以我们只需要了解一下,写法和普通消息基本一样
//创建DefaultMQProducer消息生产者对象
DefaultMQProducer producer = new DefaultMQProducer("TestProducerGroup");
//设置NameServer
producer.setNamesrvAddr("192.168.2.5:9876");
//设置NameServer节点地址,多个节点间用分号分割
try {
//与NameServer建立长连接
producer.start();
JSONObject json = new JSONObject();
json.put("orderId", "8888");
json.put("desc", "这是延迟队列测试");
//数据正文
String data = json.toJSONString();
Message message = new Message("TopicOrder", "PAY_TAG", data.getBytes());
//1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h
message.setDelayTimeLevel(3);//代表延迟10S发送
//发送消息,获取发送结果
SendResult result = producer.send(message);
//将发送结果对象打印在控制台
System.out.println("发送时间:"+ DateFormatUtils.format(new Date(), "yyyy-MM-dd HH:mm:ss"));
System.out.println("消息已发送:MsgId:" + result.getMsgId() + ",发送状态:"
+ result.getSendStatus());
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
producer.shutdown();
} catch (Exception e) {
}
}
通过上一节写的示例接受消息并打印时间,可以看到下面的信息
发送方:
接收方:
4.批量消息
在对吞吐率有一定要求的情况下,Apache RocketMQ可以将一些消息聚成一批以后进行发送,可以增加吞吐率,并减少API和网络调用次数。
//创建DefaultMQProducer消息生产者对象
DefaultMQProducer producer = new DefaultMQProducer("TestProducerGroup");
//设置NameServer
producer.setNamesrvAddr("192.168.2.5:9876");
//设置NameServer节点地址,多个节点间用分号分割
try {
//与NameServer建立长连接
producer.start();
String topic = "TopicOrder";
List<Message> messageList = new ArrayList<>();
for (int i = 1; i < 10; i++) {
JSONObject json = new JSONObject();
json.put("orderId", "8888");
json.put("desc", "测试批量发送");
//数据正文
String data = json.toJSONString();
Message message = new Message(topic, "PAY_TAG", data.getBytes());
messageList.add(message);
}
//发送消息,获取发送结果
SendResult result = producer.send(messageList);
//将发送结果对象打印在控制台
System.out.println("消息已发送:MsgId:" + result.getMsgId() + ",发送状态:"
+ result.getSendStatus());
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
producer.shutdown();
} catch (Exception e) {
}
}
注意:这里调用非常简单,将消息打包成 Collection<Message> msgs 传入方法中即可,需要注意的是批量消息的大小不能超过 1MiB(否则需要自行分割),其次同一批 batch 中 topic 必须相同。
5.事务消息(重要)
主要涉及分布式事务,即需要保证在多个操作同时成功或者同时失败时,消费者才能消费消息。RocketMQ通过发送Half消息、处理本地事务、提交(Commit)消息或者回滚(Rollback)消息实现分布式事务。
事务消息发送分为两个阶段。
第一阶段会发送一个半事务消息,半事务消息是指暂不能投递的消息,生产者已经成功地将消息发送到了 Broker,但是Broker 未收到生产者对该消息的二次确认,此时该消息被标记成“暂不能投递”状态,如果发送成功则执行本地事务,并根据本地事务执行成功与否,向 Broker 半事务消息状态(commit或者rollback),半事务消息只有 commit 状态才会真正向下游投递。
如果由于网络闪断、生产者应用重启等原因,导致某条事务消息的二次确认丢失,Broker 端会通过扫描发现某条消息长期处于“半事务消息”时,需要主动向消息生产者询问该消息的最终状态(Commit或是Rollback)。这样最终保证了本地事务执行成功,下游就能收到消息,本地事务执行失败,下游就收不到消息。总而保证了上下游数据的一致性。
整个事务消息的详细交互流程如下图所示:
例子
发送两个订单信息,一个订单号是6666,一个订单号是8888
6666订单用于测试信息二次确认返回失败,mq信息不会被消费
8888用于测试二次确认失败,broker回调查询成功,mq信息被消费的情况
实现方法
1.新增监听器实现TransactionListener接口,重写方法,一个用来发送半事务二次确认消息,一个用来处理回调查询事务状态
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
public class OrderTransactionListenerImpl implements TransactionListener {
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
String key = message.getKeys();
if("6666".equals(key)){
System.out.println("接收到orderId:"+key +"的信息");
System.out.println("模拟本地执行方法失败的情况");
//执行本地订单插入方法报错,执行回滚
return LocalTransactionState.ROLLBACK_MESSAGE;
}else if ("8888".equals(key)){
System.out.println("接收到orderId:"+key +"的信息");
System.out.println("模拟本地方法成功,但是未成功发送确认方法");
//执行本地方法成功,发送MQ提交确认信息
return LocalTransactionState.UNKNOW;
}
return null;
}
@Override
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
String key = messageExt.getKeys();
if("6666".equals(key)){
System.out.println("这是回调方法,订单号是:"+key);
return LocalTransactionState.ROLLBACK_MESSAGE;
}else if ("8888".equals(key)){
System.out.println("这是回调方法,订单号是:"+key);
System.out.println("订单号:"+key+"的回查成功");
//执行本地方法成功,发送MQ提交确认信息
return LocalTransactionState.COMMIT_MESSAGE;
}
return null;
}
}
2.编写producer
//使用TransactionMQProducer事务生产者创建
TransactionMQProducer producer = new
TransactionMQProducer("transaction_producer_group");
//从NameServer获取配置数据
producer.setNamesrvAddr("192.168.2.5:9876");
//定义线程池用于回查本地事务状态
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("check-thread");
return thread;
}
});
//将生产者与线程池绑定
producer.setExecutorService(executorService);
//绑定事务监听器,用于执行代码
TransactionListener transactionListener = new OrderTransactionListenerImpl();
producer.setTransactionListener(transactionListener);
//启动生产者
try {
producer.start();
//定义两条记录模拟事务回滚
List<JSONObject> orderInfos = new ArrayList<>();
// 6666的订单模拟发送MQ消息成功,但是本地方法(比如订单插入数据库失败)失败需要回滚的情况
JSONObject orderInfo1 = new JSONObject();
orderInfo1.put("orderId", 6666);
orderInfo1.put("info", "发送MQ消息成功,本地方法失败");
orderInfos.add(orderInfo1);
// 8888的订单用来模拟,MQ发送成功,本地方法执行成功,
// 但是通知MQ确认消息的时候失败,然后又通过MQ回查本地事务又成功的情况
JSONObject orderInfo2 = new JSONObject();
orderInfo2.put("orderId", 8888);
orderInfo2.put("info", "MQ发送成功,本地方法执行成功,再次发送MQ提交信息失败,MQ回查本地事务情况又成功");
orderInfos.add(orderInfo2);
for (JSONObject orderInfo : orderInfos) {
Message message = new Message("TopicOrder", "PAY_TAG",orderInfo.getString("orderId"), orderInfo.toJSONString().getBytes());
TransactionSendResult result = producer.sendMessageInTransaction(message, null);
System.out.println("订单号:"+orderInfo.getString("orderId")+"的第一次MQ信息发送状态:"+result.getSendStatus());
}
//用于等待broker的回调
for (int i = 1; i < 100000; i++) {
Thread.sleep(1000);
System.out.println(i+"秒");
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
producer.shutdown();
} catch (Exception e) {
}
}
然后我们看一下控制台信息:
生产者:
消费者:
可以看出,订单6666因为二次确认失败,信息无法被消费,而8888虽然二次确认失败,但是通过回调查询还是成功的。所以8888成功被消费。
三、总结
普通消息:性能最好,但是消息的生产和消费是无序的。适用于大部分场景
有序消息:容易出现单点问题,如果broker宕机,会导致发送失败,有序消息场景适用
延迟消息:支持延迟特性,使用非常方便,但是灵活性较低,不能根据任何时间延迟,适用于非精确、延迟级别不多的场景
事务消息:RocketMQ是生产者事务,只有生产者参与,如果消费者处理失败则事务失败,适用于简单事务处理的场景。