对于消息队列的通信模型,目前市面上主流的消息队列产品主要有两种形式,一种按照队列的数据结构设计出来的“队列模型”,另一则是在其基础上演化出来的“发布-订阅模型”,而RocketMQ采用的是发布订阅的方式。
发布订阅,看成是生产者-消费者模型的一种形式就可以了。消息发送给到订阅了这个消息的消费者,那么势必系统需要知道生产者和消费者之间的关系。RocketMQ提供了Broker这个组件做为消息中间层,处理消息投递过程中的一些事务,例如:存储、转发。为了维护他们之间的关系,RocketMQ又引入了NamerServer提供类似注册中心的功能,只是此时注册的是所有Broker,生产者和消费者通过NamerServer找到对应消息投递和接收的Broker,整体的一个流程如下:
RocketMQ在设计上是参考kafka实现的,所以整体上就跟kafka的很像。
RocketMQ对于消息提供了很多用法,包括:同步消息、异步消息、单向发送、顺序消息、延时消息、批量消息、过滤消息、事务消息等。添加依赖:
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.3.0</version>
</dependency>
项目启动时init生成者信息:
@Component
public class RProducer {
@Value("${rocketmq.namesrvAddr}")
private String namesrvAddr;
@Value("${rocketmq.retryTimesWhenSendFailed:3}")
private int retryTimesWhenSendFailed;
private DefaultMQProducer producer;
@PostConstruct
private void init() throws MQClientException {
producer = new DefaultMQProducer("P_DEFAULT");
producer.setNamesrvAddr(namesrvAddr);
producer.setUnitName("P_NAME");
producer.setSendLatencyFaultEnable(true);
producer.setRetryTimesWhenSendFailed(retryTimesWhenSendFailed);
producer.setSendMsgTimeout(60000);
producer.start();
}
@PreDestroy
public void shutdown() {
if (producer != null) {
producer.shutdown();
}
}
}
同步消息关注的是数据的可靠性,对于发送的数据,RocketMQ会在发送消息的同时将消息写入到磁盘,最后返回发送状态。可以应用于重要的消息通知,短信通知等。
public SendResult send(String topic, String tag, String info) throws Exception {
boolean failed = false;
do {
try {
Message message = new Message(topic, tag, info.getBytes());
return producer.send(message);
}catch (Exception e){
failed = true;
TimeUnit.SECONDS.sleep(3);
}
}while (failed);
return null;
}
异步消息通常用在对响应时间比较敏感,同时又不需要及时的关注消息传递结果的场景,比如优惠券发放,秒杀等。
public void sendSync(String topic, String tag, String info) throws Exception{
Message message = new Message(topic, tag, info.getBytes());
producer.send(message, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {}
@Override
public void onException(Throwable e) {}
});
}
这种方式主要用在不特别关注消息传递结果的场景,例如日志发送。
public void sendOneWay(String topic, String tag, String info) throws Exception{
Message message = new Message(topic, tag, info.getBytes());
producer.sendOneway(message);
}
RocketMQ默认是采用轮询的方式将消息发送到不同的队列里的,消费者拿到的数据其实是不能保证消息的有序性的。但如果控制消息都往同一个队列里发,或者是同一类的消息往同一个队列里发,也就可以实现消息全局有序或者局部有序。主要可以用于一些对顺序性有要求的数据,比如一个订单,假如流程是创建、付款、通知、完成,按照订单号维度,把相同的订单放到同一个队列里,那样就能保证对于一条订单来说,数据消费是有序的。
Message message = new Message(topic, tag, info.getBytes());
SendResult sendResult = producer.send(message, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message mess, Object arg) {
//根据订单id选择发送的队列
int index = (int) arg % mqs.size();
return mqs.get(index);
}
}, id);
初始化消息的时候可以配置消息延迟发送的等级。RocketMQ默认有18个延迟等级:“1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h”。
Message message = new Message(topic, tag, info.getBytes());
// 延迟等级为2,延迟5s发送
message.setDelayTimeLevel(2);
RocketMQ允许将一批数据组合成一个List一次性发送出去,减少不必要的网络传输,但需要注意发送数据量的大小。
public SendResult sendList(List<Message> messages) throws Exception{
boolean failed = false;
do {
try {
return producer.send(messages);
}catch (Exception e){
failed = true;
TimeUnit.SECONDS.sleep(3);
}
}while (failed);
return null;
}
构造Message的时候,需要指定发送的topic(主题)和tags,topic用于匹配消息的订阅,而tags主要用于消息的过滤。消费者可以通过订阅指定的tags,从而获取想要的内容。
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("C_DEFAULT");
// *表示不过滤,多个tags用 “||”隔开
consumer.subscribe("TOPIC_TEST", "*");
也可以在message中设置一些属性,在消费者通过一些基本语法实现过滤,比如:接收id>100的可以这样实现:
// 生产者
Message message = new Message(topic, tag, info.getBytes());
message.putUserProperty("id", val);
// 消费者
consumer = new DefaultMQPushConsumer("C_DEFAULT");
consumer.subscribe("TOPIC_TEST", MessageSelector.bySql("id > 10"));
提供的语法有:
数值比较,比如:>,>=,<,<=,BETWEEN,=
字符比较,比如:=,<>,IN,IS NULL 或者 IS NOT NULL
逻辑符号,比如:AND,OR,NOT
RocketMQ的事务消息是基于2pc进行设计的,共有三种状态,提交状态、回滚状态、中间状态,具体可以查看TransactionStatus的定义。事务提交之前,消息会先进行预发送,等到本地事务执行完后再根据结果决定发送消息还是回滚消息,从而实现分布式事务。
// 创建事务消息生产者
@Component
public class RTProduct {
@Value("${rocketmq.namesrvAddr}")
private String namesrvAddr;
@Value("${rocketmq.retryTimesWhenSendFailed:3}")
private int retryTimesWhenSendFailed;
private TransactionMQProducer producer;
@PostConstruct
private void init() throws MQClientException {
producer = new TransactionMQProducer("R_T_DEFAULT");
producer.setNamesrvAddr(namesrvAddr);
producer.setUnitName("P_NAME");
// 设置监听的线程
ExecutorService executorService = new ThreadPoolExecutor(2, 5, 20, 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;
}
});
producer.setExecutorService(executorService);
// 绑定事务监听
producer.setTransactionListener(new RTListener());
producer.setRetryTimesWhenSendFailed(retryTimesWhenSendFailed);
producer.start();
}
}
// 实现事务监听
public class RTListener implements TransactionListener {
private ConcurrentHashMap<String, Integer> localTrans = new ConcurrentHashMap<>();
@Override
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
Integer status = localTrans.get(messageExt.getTransactionId());
if (null != status) {
switch (status) {
case 0:
return LocalTransactionState.UNKNOW;
case 1:
return LocalTransactionState.COMMIT_MESSAGE;
case 2:
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
return LocalTransactionState.COMMIT_MESSAGE;
}
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
/*
* TODO 执行本地事务
**/
localTrans.put(message.getTransactionId(), 1);
return LocalTransactionState.UNKNOW;
}
}
RocketMQ消息的消费主要有2种模式,一种是消息队列主动推给消费者(push),一种是消费者定时去消息队列拉取消息(pull),不过推模式是RocketMQ本身在拉模式上做的封装,所以主要关注拉模式就可以了,同时消费过程中也要考虑消费有序、消息幂等、消息堆积等情况,后续再单独写一篇分享。感兴趣的可以关注我,一起学习!