消息种类
- 按照发送的特点分
- 同步消息
- 异步消息
- 单向消息
- 按照使用功能特点分
- 顺序消息
- 广播模式
- 延迟消息
- 批量消息
- 过滤消息
- 事务消息
按照发送的特点分
同步消息
同步发送是指消息发送方发出数据后,会阻塞直到MQ服务方发回响应消息。应用场景:此种方式应用场景非常广泛,例如重要通知邮件、报名短信通知、营销短信系统等。
异步消息
异步发送是指发送方发出数据后,不等接收方发回响应,接着发送下个数据包的通讯方式。MQ 的异步发送,需要用户实现异步发送回调接口(SendCallback),在执行消息的异步发送时,应用不需要等待服务器响应即可直接返回,通过回调接口接收服务器响应, 并对服务器的响应结果进行处理。应用场景:异步发送一般用于链路耗时较长,对 RT 响应时间较为敏感的业务场景,例如用户视频上传后通知启动转码服务,转码完成后通知推送转码结果等。
单向消息
单向(Oneway)发送特点为只负责发送消息,不等待服务器回应且没有回调函数触发,即只发送请求不等待应答。此方式发送消息的过程耗时非常短,一般在微秒级别。应用场景:适用于某些耗时非常短,但对可靠性要求并不高的场景,例如日志收集。
按照使用功能特点分
顺序消息
当我们在说顺序时,我们在说什么?
日常思维中,顺序大部分情况会和时间关联起来,即时间的先后表示事件的顺序关系。A事件在1点发送,B事件在2点发生,则认为事件a在事件b之前发生。但是如果没有时间做参考,那么a和b之间有顺序吗?或者说怎么断定先后呢?显而易见,如果a和b有因果关系的,那么a一定发生在b之前。那么,我们在说顺序时,其实说的是:
有绝对时间参考的情况下,事件发生跟时间有关系
和时间没有参考下,一种因果关系推断出来的happening before的关系
在分布式环境中讨论顺序
同一线程上的事件顺序时确认的,可以认为他们有相同的时间作为参考
在不同线程间的顺序只能通过因果关系去推断
消息中间件的顺序消息
顺序消息(FIFO 消息)是 MQ 提供的一种严格按照顺序进行发布和消费的消息类型。顺序消息由两个部分组成:顺序发布和顺序消费。
案例:如果有100个订单,其中每个订单都有创建订单,支付订单,完成订单三个步骤。
**局部顺序:**一个Partition内所有的消息按照先进先出的顺序进行发布和消费。其中,每个订单都有创建订单,支付订单,完成订单三个步骤,这三个步骤按照这个顺序进行创建
**全局顺序:**一个Topic内所有的消息按照先进先出的顺序进行发布和消费。这100个订单第一个执行完了第二个执行继续往下执行。
但是,在单线程下可以保证的,在多线程下,若没有因果关系是无法保证顺序的。 即顺序发送在多线程发送的消息,不同线程间的消息不是顺序发布的,同一线程的消息是顺序发布的。这是需要用户自己去保障的。而对于顺序消费则需要保证哪些来自同一个发送线程的消息在消费时是按照相同的顺序被处理的。
全局顺序其实是分区顺序的一个特例,即使Topic只有一个分区(以下不在讨论全局顺序,因为全局顺序将面临性能的问题,而且绝大多数场景都不需要全局顺序)。
如何保障顺序呢
在MQ的模型中,顺序需要由3个阶段去保障:
1.消息被发送时保持顺序
2.消息被存储时保持和发送的顺序一致
3.消息被消费时保持和存储的顺序一致
发送时保持顺序意味着对于有顺序要求的消息,用户应该在同一个线程中采用同步的方式发送。存储保持和发送的顺序一致则要求在同一线程中被发送出来的消息A和B,存储时在空间上A一定在B之前。而消费保持和存储一致则要求消息A、B到达Consumer之后必须按照先A后B的顺序被处理。消费时保证顺序的简单方式就是“什么都不做”,不对收到的消息的顺序进行调整,即只要一个分区的消息只由一个线程处理即可;当然,如果a、b在一个分区中,在收到消息后也可以将他们拆分到不同线程中处理,不过要权衡一下收益
producer端
public class Producer {
public static void main(String[] args) throws UnsupportedEncodingException {
try {
DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name");
// producer.setNamesrvAddr("192.168.232.128:9876");
producer.start();
for (int i = 0; i < 10; i++) {
int orderId = i;
for(int j = 0 ; j <= 5 ; j ++){
Message msg =
new Message("OrderTopicTest", "order_"+orderId, "KEY" + orderId,
("order_"+orderId+" step " + j).getBytes(RemotingHelper.DEFAULT_CHARSET));
//MessageQueueSelector()要做的事情就是将消息路由到特定的分区
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg) {
Integer id = (Integer) arg;
int index = id % mqs.size();
return mqs.get(index);
}
}, orderId);
System.out.printf("%s%n", sendResult);
}
}
producer.shutdown();
} catch (MQClientException | RemotingException | MQBrokerException | InterruptedException e) {
e.printStackTrace();
}
}
}
consumer端
对于PushConsumer,由用户注册MessageListener来消费消息,在客户端中需要保证调用MessageListener时消息的顺序性。RocketMQ中的实现如下:
1.PullMessageService单线程的从Broker获取消息
2.PullMessageService将消息添加到ProcessQueue中(ProcessMessage是一个消息的缓存),之后提交一个消费任务到ConsumeMessageOrderService
3.ConsumeMessageOrderService多线程执行,每个线程在消费消息时需要拿到MessageQueue的锁
4.拿到锁之后从ProcessQueue中获取消息
public class Consumer {
public static void main(String[] args) throws MQClientException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name_3");
consumer.setNamesrvAddr("192.168.232.128:9876");
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
consumer.subscribe("OrderTopicTest", "*");
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.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("Consumer Started.%n");
}
}
广播模式
同一个 ConsumerGroup里的每个 Consumer都 能消费到所订阅 Topic 的全部消息,也就是一个消息会被多次分发,被多个 Consumer消费。
// 集群模式
consumer.setMessageModel(MessageModel.CLUSTERING);
// 广播模式
consumer.setMessageModel(MessageModel.BROADCASTING);
默认是集群模式,只需要将消费端改成上面的即可。
延迟消息
延迟消息是指消费者过了一个指定的时间后,才去消费这个消息。大家想象一个电商中场景,一个订单超过30分钟未支付,将自动取消。这个功能怎么实现呢?一般情况下,都是写一个定时任务,一分钟扫描一下超过30分钟未支付的订单,如果有则被取消。这种方式由于每分钟查询一下订单,一是时间不精确,二是查库效率比较低。这个场景使用RocketMQ的延迟消息最合适不过了,我们看看怎么发送延迟消息吧,
我们只是增加了一句message.setDelayTimeLevel(2);
开源版本的RocketMQ中,对延迟消息并不支持任意时间的延迟设定(商业版本中支持),而是只支
持18个固定的延迟级别,1到18分别对应messageDelayLevel=1s 5s 10s 30s 1m 2m 3m 4m 5m
6m 7m 8m 9m 10m 20m 30m 1h 2h。这从哪里看出来的?其实从rocketmq-console控制台就
能看出来。而这18个延迟级别也支持自行定义,不过一般情况下最好不要自定义修改。
批量消息
public class SimpleBatchProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("BatchProducerGroupName");
producer.start();
//If you just send messages of no more than 1MiB at a time, it is easy to use batch
//Messages of the same batch should have: same topic, same waitStoreMsgOK and no schedule support
String topic = "BatchTest";
List<Message> messages = new ArrayList<>();
messages.add(new Message(topic, "Tag", "OrderID001", "Hello world 0".getBytes()));
messages.add(new Message(topic, "Tag", "OrderID002", "Hello world 1".getBytes()));
messages.add(new Message(topic, "Tag", "OrderID003", "Hello world 2".getBytes()));
producer.send(messages);
producer.shutdown();
}
}
批量消息是指将多条消息合并成一个批量消息,一次发送出去。这样的好处是可以减少网络IO,提升吞吐量。
如果批量消息大于1MB就不要用一个批次发送,而要拆分成多个批次消息发送。也就是说,一个批次消息的大小不要超过1MB
实际使用时,这个1MB的限制可以稍微扩大点,实际最大的限制是4194304字节,大概4MB。但
是使用批量消息时,这个消息长度确实是必须考虑的一个问题。而且批量消息的使用是有一定限
制的,这些消息应该有相同的Topic,相同的waitStoreMsgOK。而且不能是延迟消息、事务消息
等。
过滤消息
在大多数情况下,可以使用Message的Tag属性来简单快速的过滤信息。
主要是看消息消费者。consumer.subscribe(“TagFilterTest”, “TagA || TagC”); 这句只订阅TagA
和TagC的消息。
TAG是RocketMQ中特有的一个消息属性。RocketMQ的最佳实践中就建议,使用RocketMQ时,
一个应用可以就用一个Topic,而应用中的不同业务就用TAG来区分。
public class TagFilterConsumer {
public static void main(String[] args) throws InterruptedException, MQClientException, IOException {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name");
consumer.subscribe("TagFilterTest", "TagA || TagC");
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");
}
}
但是,这种方式有一个很大的限制,就是一个消息只能有一个TAG,这在一些比较复杂的场景就有点不足了。 这时候,可以使用SQL表达式来对消息进行过滤。
这个模式的关键是在消费者端使用MessageSelector.bySql(String sql)返回的一个
MessageSelector。这里面的sql语句是按照SQL92标准来执行的。sql中可以使用的参数有默认的
TAGS和一个在生产者中加入的a属性。
SQL92语法:
RocketMQ只定义了一些基本语法来支持这个特性。你也可以很容易地扩展它。
数值比较,比如:>,>=,<,<=,BETWEEN,=;
字符比较,比如:=,<>,IN;
IS NULL 或者 IS NOT NULL;
逻辑符号 AND,OR,NOT;
常量支持类型为:
数值,比如:123,3.1415;
字符,比如:‘abc’,必须用单引号包裹起来;
NULL,特殊的常量
布尔值,TRUE 或 FALSE
public class SqlFilterConsumer {
public static void main(String[] args) throws Exception {
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("please_rename_unique_group_name");
// Don't forget to set enablePropertyFilter=true in broker
consumer.subscribe("SqlFilterTest",
MessageSelector.bySql("(TAGS is not null and TAGS in ('TagA', 'TagB'))" +
"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");
}
}
使用注意:只有推模式的消费者可以使用SQL过滤。拉模式是用不了的。
事务消息
首先讨论一下什么是事务消息以及支持事务消息的必要性。我们以一个转帐的场景为例来说明这个问题:Bob向Smith转账100块。
在单机环境下,执行事务的情况,大概是下面这个样子:
当用户增长到一定程度,Bob和Smith的账户及余额信息已经不在同一台服务器上了,那么上面的流程就变成了这样:
集群环境下转账事务示意图
这时候你会发现,同样是一个转账的业务,在集群环境下,耗时居然成倍的增长,这显然是不能够接受的。那我们如何来规避这个问题?
大事务 = 小事务 + 异步
将大事务拆分成多个小事务异步执行。这样基本上能够将跨机事务的执行效率优化到与单机一致。转账的事务就可以分解成如下两个小事务:
图中执行本地事务(Bob账户扣款)和发送异步消息应该保持同时成功或者失败中,也就是扣款成功了,发送消息一定要成功,如果扣款失败了,就不能再发送消息。那问题是:我们是先扣款还是先发送消息呢?
首先我们看下,先发送消息,大致的示意图如下:
事务消息:先发送消息
存在的问题是:如果消息发送成功,但是扣款失败,消费端就会消费此消息,进而向Smith账户加钱。先发消息不行,那我们就先扣款呗,大致的示意图如下:
事务消息-先扣款
存在的问题跟上面类似:如果扣款成功,发送消息失败,就会出现Bob扣钱了,但是Smith账户未加钱。
可能大家会有很多的方法来解决这个问题,比如:直接将发消息放到Bob扣款的事务中去,如果发送失败,抛出异常,事务回滚。这样的处理方式也符合“恰好”不需要解决的原则。RocketMQ支持事务消息,下面我们来看看RocketMQ是怎样来实现的。
RocketMQ实现发送事务消息
RocketMQ第一阶段发送Prepared消息时,会拿到消息的地址。第二阶段执行本地事务,第三阶段通过第一阶段拿到的消息地址去访问消息,并修改状态。细心的你可能又发现问题了,如果确认消息发送失败怎么办?rocketmq会定期扫描消息集群中的事务消息,这时发现了prepared消息,他会向消息发送至确认,bob钱到底是减了还是没有减呢?如果减了是回滚还是继续发送确认消息呢?rocketmq会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证 了消息发送与本地事务同事成功或同时失败。
那我们来看下RocketMQ源码,是不是这样来处理事务消息的。客户端发送事务消息的部分
事务消息的关键是在TransactionMQProducer中指定了一个TransactionListener事务监听器,这
个事务监听器就是事务消息的关键控制器。源码中的案例有点复杂,我这里准备了一个更清晰明
了的事务监听器示例
/public class TransactionListenerImpl implements TransactionListener {
//在提交完事务消息后执行。
//返回COMMIT_MESSAGE状态的消息会立即被消费者消费到。
//返回ROLLBACK_MESSAGE状态的消息会被丢弃。
//返回UNKNOWN状态的消息会由Broker过一段时间再来回查事务的状态。
@Override
public LocalTransactionState executeLocalTransaction(Message msg,
Object arg) {
String tags = msg.getTags();
//TagA的消息会立即被消费者消费到
if(StringUtils.contains(tags,"TagA")){
return LocalTransactionState.COMMIT_MESSAGE;
//TagB的消息会被丢弃
}else if(StringUtils.contains(tags,"TagB")){
return LocalTransactionState.ROLLBACK_MESSAGE;
//其他消息会等待Broker进行事务状态回查。
}else{
return LocalTransactionState.UNKNOW;
}
}
//在对UNKNOWN状态的消息进行状态回查时执行。返回的结果是一样的。
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
String tags = msg.getTags();
//TagC的消息过一段时间会被消费者消费到
if(StringUtils.contains(tags,"TagC")){
return LocalTransactionState.COMMIT_MESSAGE;
//TagD的消息也会在状态回查时被丢弃掉
}else if(StringUtils.contains(tags,"TagD")){
return LocalTransactionState.ROLLBACK_MESSAGE;
//剩下TagE的消息会在多次状态回查后最终丢弃
}else{
return LocalTransactionState.UNKNOW;
}
}
}
核心是发送端实现的事务监听器: RocketMQLocalTransactionListener. 该监听器提供了两个方法: executeLocalTransaction(org.springframework.messaging.Message message, Object o) 和 checkLocalTransaction(org.springframework.messaging.Message message).分别用于执行本地事务和消息反查.
1.生产者发送半消息给消息队列服务端(Broker)
2.如果半消息发送成功, 则执行本地事务.
3.根据本地事务执行结果, 向消息队列服务端发送事务提交或回滚的请求.
4.如果消息队列长时间没有收到提交或回滚的请求, RocketMQ 提供了消息反查机制用于查询本地事务的执行情况.
4.根据反查结果,向消息队列服务端发送提交事务或回滚的请求.
5.消息队列服务端收到提交的请求会将消息发送给消费者, 如果收到回滚的请求则会将消息丢弃.
上面的流程中提到两个重要的概念: 半消息和反查机制
半消息: 半消息并不是说消息内容不完整, 而是说消息内容是完整的, 只不过是暂时无法提交给消息者进行消费的消息. 此时消息的状态是"暂时不可消息".
消息反查机制: 如果出现网络问题, 或生产者故障等原因导致服务端长时间没有收到确认消息, 服务端就会发送一个请求给生产者, 查询该消息的最终状态.