消息发送
普通消息
生产者多次循环发送该消息,broker收到消息顺序会不一致,因为convertAndSend并没有等上一条信息发送完并收到响应再发送下一条消息
@GetMapping("/send")
public String send() {
rocketMQTemplate.convertAndSend("topic","普通消息");
return "发送成功";
}
同步消息
producer向 broker 发送消息后同步等待, 直到broker 服务器返回发送结果
@GetMapping("/sync")
public String sync() {
SendResult sendResult = rocketMQTemplate.syncSend("topic:sync", "同步消息");
if (sendResult.getSendStatus() == SendStatus.SEND_OK) {
return "同步发送成功";
}
return "同步发送失败";
}
SendStatus中有四种状态
状态 | 意义 |
---|---|
SendStatus.SEND_OK | 消息发送成功,需要将消息刷盘和消息复制到 slave 节点变为同步才是真正发送成功 |
SendStatus. FLUSH_DISK_TIMEOUT | 消息发送成功但消息刷盘超时 |
SendStatus.FLUSH_SLAVE_TIMEOUT | 消息发送成功但是消息同步到slave节点超时 |
SendStatus.SLAVE_NOT_AVAILABLE | 消息发送成功但是broker的slave节点不可用 |
配置消息刷盘同步,配置消息复制到slave为同步,将flushDiskType刷盘方式设为同步刷盘,将brokerRole角色设为同步双写
/conf/broker.conf
flushDiskType=SYNC_FLUSH #ASYNC_FLUSH 异步刷盘; SYNC_FLUSH 同步刷盘
brokerRole=SYNC_MASTER # Broker的角色: ASYNC_MASTER 异步复制; SYNC_MASTER 同步双写; SLAVE 努力
异步消息
producer向 broker 发送消息注册回调方法,调用 API 后立即返回,消息发送成功或失败的回调任务在一个新的线程中执行
@GetMapping("/async")
public String async() {
rocketMQTemplate.asyncSend("topic:async", "异步消息", new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println("异步消息发送成功");
}
@Override
public void onException(Throwable throwable) {
System.out.println("异步消息发送失败");
}
});
return "发送异步消息";
}
topic:async中的topic是主题,async是tag,用于区分一个主题下的不同tag,之间用冒号隔开,在写消费者时,topic写主题,selectorExpression写tag的表达式,支持 * || 这些匹配逻辑
@Component
@RocketMQMessageListener(topic = "topic",
selectorExpression = "async",consumerGroup = "my-consumer-group")
public class Consumer implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
System.out.println(message);
}
}
单向消息
producer向 broker 发送消息,执行 API 时直接返回,不等待broker 服务器的结果 ,也不注册回调函数
@GetMapping("/oneWay")
public String oneWay() {
rocketMQTemplate.sendOneWay("oneWay", "单向消息");
return "单向消息发送成功";
}
顺序消息
RocketMQ的一个主题topic下会有多个消息队列,使用setMessageQueueSelector将信息发送指定的消息队列,如list.get(1)就是指定1这个队列,生产环境可以将id根据队列数list.size()取模,则如果发送多次消息,这多次消息都在该队列中排队即有序,前面完成了有序存储,后面需要消费者有序消费
@RequestMapping("/orderly")
public String orderly() {
rocketMQTemplate.setMessageQueueSelector(new MessageQueueSelector() {
@Override
public MessageQueue select(List<MessageQueue> list, org.apache.rocketmq.common.message.Message message, Object o) {
return list.get(1);
}
});
rocketMQTemplate.syncSendOrderly("orderly", "顺序消息", "123");
return "发送顺序消息";
}
消费者有序消费需要在注解上加上consumeMode = ConsumeMode.ORDERLY,表示有序消费,consumeThreadMax控制消费者线程为1,否则就算消息在一个队列有序存储也不会有序
@Component
@RocketMQMessageListener(topic = "topic",selectorExpression = "*",
consumerGroup = "my-consumer-group",
consumeMode = ConsumeMode.ORDERLY,
consumeThreadMax = 1)
public class Consumer implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
System.out.println(message);
}
}
总结一下,顺序消息需要一个生产者往一个队列中生产消息,消费该队列的消费者只能有一个,且需要是顺序消费模式,且消费线程为1
批量消息
其实就算发送一个消息列表,将多个消息聚合成一个列表,然后通过一次同步发送发送出去,但是每次消息发送会有长度限制,普通和顺序消息最长4 MB,所以如果需要发送的消息太多,需要分片
@RequestMapping("/batch")
public String pushBatchMessage() {
// 将消息聚合在一起,再同步发送
List<Message> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add( MessageBuilder.withPayload("批量消息"+i)
.setHeader(RocketMQHeaders.KEYS, i)
.build());
}
rocketMQTemplate.syncSend("topic:batch", list);
return "发送批量消息";
}
sql过滤
生产者代码,可以看到其实sql过滤也是使用同步发送,只不过在构建消息添加了一个字段value,就可以消费者可以根据该value选择性消费
@RequestMapping("/sql")
public String pushSqlMessage() {
List<Message> list = new ArrayList<>();
for (int i = 0; i < 10; i++) {
list.add( MessageBuilder.withPayload("sql过滤消息"+i)
.setHeader(RocketMQHeaders.KEYS, i)
.setHeader("value", i)
.build());
}
rocketMQTemplate.syncSend("sql", list);
return "sql过滤消息";
}
消费者代码
使用selectorType = SelectorType.SQL92,必须修改配置文件,否则启动会报错, SelectorType.SQL92是启动sql过滤,默认的SelectorType.Tag,即通过标签选择消息
@Component
@RocketMQMessageListener(topic = "sql",
selectorType = SelectorType.SQL92,
selectorExpression = "value > 4",
consumerGroup = "my-consumer-group"
)
public class Consumer implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
System.out.println(message);
}
}
修改配置文件,修改配置文件后要根据配置文件启动,如sh ./bin/mqbroker -c ./conf/broker.conf ,-c参数就是根据配置文件启动
/conf/broker.conf
autoCreateTopicEnable=true #是否允许 Broker 自动创建Topic
autoCreateSubscriptionGroup=true #是否允许 Broker 自动创建订阅组
namesrvAddr=127.0.0.1:9876 #nameServer地址
enablePropertyFilter=true #允许sql过滤
事务消息
事务消息就是,先发送个信息给broker,表明发送了消息,然后执行本地事务,如果本地事务成功执行了,则发送commit给broker,则该消息可以被消费者消费了,如果发送的是rollback,则该消息不会被消费
@RequestMapping("/transaction")
public String transaction() {
Message<String> message = MessageBuilder.withPayload("事务消息")
.setHeader(RocketMQHeaders.KEYS, 1)
.setHeader("money", 10)
.setHeader(RocketMQHeaders.TRANSACTION_ID, 100)
.build();
TransactionSendResult transactionSendResult = rocketMQTemplate.sendMessageInTransaction("transaction", message, null);
return "事务消息";
}
该代码也是写在消息生产者中,executeLocalTransaction即执行本地事务的代码,如果返回的是RocketMQLocalTransactionState.COMMIT,可以在消费者看到消息被消费,如果执行的是RocketMQLocalTransactionState.ROLLBACK可以看到消息不会被消费
@RocketMQTransactionListener
public class TransactionListener implements RocketMQLocalTransactionListener {
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message message, Object o) {
System.out.println("运行executeLocalTransaction");
return RocketMQLocalTransactionState.COMMIT;
// return RocketMQLocalTransactionState.ROLLBACK;
}
@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message message) {
System.out.println("checkLocalTransaction");
return RocketMQLocalTransactionState.COMMIT;
}
}
重复消费问题
@Component
@RocketMQMessageListener(topic = "topic",selectorExpression = "*",
consumerGroup = "my-consumer-group",
consumeMode = ConsumeMode.ORDERLY,
messageModel = MessageModel.CLUSTERING)
public class Consumer implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
System.out.println(message);
}
}
配置messageModel,messageModel有两种模式BROADCASTING 和 CLUSTERING,
- 在BROADCASTING模式下所有注册的消费者都会消费
- 在CLUSTERING模式下,如果一个topic被多个consumerGroup消费,也会重复消费
- 在CLUSTERING模式下,同一个consumerGroup消费者组,一个队列只会分配给一个消费者,但是在消费者上线和下线时,会重新负载均衡,更换队列对应的消费者。一个队列所对应的新的消费者要获取之前消费的offset,此时之前的消费者可能已经消费了一条消息,但是并没有把offset提交给broker,那么新的消费者可能会重新消费一次。
- orderly是前一个消费者先解锁,后一个消费者加锁再消费的模式,比起concurrently要严格了,但是加锁的线程和提交offset的线程不是同一个,所以还是会出现极端情况下的重复消费
- orderly模式是一批中有一条消费失败,一批统一重新消费,直到达到最大消费次数的限制,发送到死信队列。而concurrently情况下,在返回成功(CONSUME_SUCCESS)的前提下,有个ackIndex可以分隔成功和失败的消息,失败的、没有消费的消息发送到retry队列,不会造成重复消费,而如果返回的是(RECONSUME_LATER),仍然是和orderly一样的同批次全部重新消费
- 消费者pullRequest发出去,如果长时间收不到请求,是会被取出来重新再放入队列再请求一次的,所以也是会重复拉取消息的
综上所述,rocketMQ是无法避免消息重复消费的,只能使用幂等方案
防止消息丢失
生产者防止消息丢失
- 同步发送信息,同步发送后会返回状态码,根据状态码决定是否执行消息重发逻辑
状态 | 意义 |
---|---|
SendStatus.SEND_OK | 消息发送成功,需要将消息刷盘和消息复制到 slave 节点变为同步才是真正发送成功 |
SendStatus. FLUSH_DISK_TIMEOUT | 消息发送成功但消息刷盘超时 |
SendStatus.FLUSH_SLAVE_TIMEOUT | 消息发送成功但是消息同步到slave节点超时 |
SendStatus.SLAVE_NOT_AVAILABLE | 消息发送成功但是broker的slave节点不可用 |
- 异步发送,可以在onException中执行消息重发逻辑
@GetMapping("/async")
public String async() {
rocketMQTemplate.asyncSend("topic:async", "异步消息", new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println("异步消息发送成功");
}
@Override
public void onException(Throwable throwable) {
System.out.println("异步消息发送失败");
}
});
return "发送异步消息";
}
- 使用事务消息,Producer先发送half消息,Broker会把消息写入队列后给Producer返回成功,Producer再执行本地事务,成功后给Broker发送commit命令,失败则发送rollback。通过这种确认机制也可以防止消息丢失
- 使用消息索引,在构建消息时会有一个KEYS,可以发送消息后,生产者根据该KEYS去查询该消息,使用消费者的queryMessage方法
@GetMapping("/sync")
public String sync() throws MQClientException, InterruptedException {
Message<String> message = MessageBuilder.withPayload("同步消息")
.setHeader(RocketMQHeaders.KEYS, "key")
.setHeader("money", 10)
.build();
rocketMQTemplate.syncSend("topic:sync", message);
DefaultMQProducer producer = rocketMQTemplate.getProducer();
producer.queryMessage("topic", "key", 1, 10, 100);
return "同步消息";
}
public QueryResult queryMessage(String topic, String key, int maxNum, long begin, long end) throws MQClientException, InterruptedException {
return this.defaultMQProducerImpl.queryMessage(this.withNamespace(topic), key, maxNum, begin, end);
}
broker消息存储者防止消息丢失
- 将刷盘策略设为同步刷盘
策略名 | 作用 |
---|---|
异步刷盘 | 默认。消息写入 CommitLog 时,并不会直接写入磁盘,而是先写入 PageCache 缓存后返回成功,然后用后台线程异步把消息刷入磁盘。异步刷盘提高了消息吞吐量,但是可能会有消息丢失的情况,比如断点导致机器停机,PageCache 中没来得及刷盘的消息就会丢失 |
同步刷盘 | 消息写入内存后,立刻请求刷盘线程进行刷盘,如果消息未在约定的时间内刷盘成功,就返回 FLUSH_DISK_TIMEOUT,Producer 收到这个响应后,可以进行重试。同步刷盘策略保证了消息的可靠性,同时降低了吞吐量,增加了延迟 |
- 将主从复制策略改为同步复制,部署broker集群时为一主多从,且主从复制默认是异步的,master 收到消息后,不等 slave 节点复制消息就直接给 Producer 返回成功,这样如果 master 宕机了,进行主备切换后就会有消息丢失,因为当原先的主机重新启动,它已经变为从机了,而此时的主机(也就是原先从机)不会对此时从机的数据进行同步,因为主从复制都是把数据从主复制到从,而不是从从复制到主
消费者防止消息丢失
- 一般我们消费需要进行确认机制,即使用try-catch,成功了则将ACK发回给broker,出异常了则在catch块中发送失败状态码给broker。但是在使用注解实现消费者时,是看不到这些的,但是其实是封装了这种逻辑的,在消息消费出异常会进行重新消费
@Component
@RocketMQMessageListener(topic = "topic",
selectorExpression = "async",consumerGroup = "my-consumer-group")
public class Consumer implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
System.out.println(message);
}
}
- 在DefaultRocketMQListenerContainer类中有两个内部类,DefaultMessageListenerOrderly和DefaultMessageListenerConcurrently,它们分别实现接口MessageListenerOrderly 和 MessageListenerConcurrently,可以看到这两个接口的consumeMessage方法都是有一个枚举类的返回值的,使用try-catch块,在成功时返回成功状态码,在异常时返回异常状态码。这两种实现类就对应着注解@RocketMQMessageListener的属性onsumeMode消费模式中的两个选项ConsumeMode.CONCURRENTLY 并行处理,ConsumeMode.ORDERLY 按顺序处理
public class DefaultMessageListenerOrderly implements MessageListenerOrderly {
public DefaultMessageListenerOrderly() {
}
public ConsumeOrderlyStatus consumeMessage(List<MessageExt> msgs, ConsumeOrderlyContext context) {
Iterator var3 = msgs.iterator();
while(var3.hasNext()) {
MessageExt messageExt = (MessageExt)var3.next();
DefaultRocketMQListenerContainer.log.debug("received msg: {}", messageExt);
try {
long now = System.currentTimeMillis();
DefaultRocketMQListenerContainer.this.handleMessage(messageExt);
long costTime = System.currentTimeMillis() - now;
DefaultRocketMQListenerContainer.log.info("consume {} cost: {} ms", messageExt.getMsgId(), costTime);
} catch (Exception var9) {
DefaultRocketMQListenerContainer.log.warn("consume message failed. messageExt:{}", messageExt, var9);
context.setSuspendCurrentQueueTimeMillis(DefaultRocketMQListenerContainer.this.suspendCurrentQueueTimeMillis);
return ConsumeOrderlyStatus.SUSPEND_CURRENT_QUEUE_A_MOMENT;
}
}
return ConsumeOrderlyStatus.SUCCESS;
}
}
public class DefaultMessageListenerConcurrently implements MessageListenerConcurrently {
public DefaultMessageListenerConcurrently() {
}
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
Iterator var3 = msgs.iterator();
while(var3.hasNext()) {
MessageExt messageExt = (MessageExt)var3.next();
DefaultRocketMQListenerContainer.log.debug("received msg: {}", messageExt);
try {
long now = System.currentTimeMillis();
DefaultRocketMQListenerContainer.this.handleMessage(messageExt);
long costTime = System.currentTimeMillis() - now;
DefaultRocketMQListenerContainer.log.debug("consume {} cost: {} ms", messageExt.getMsgId(), costTime);
} catch (Exception var9) {
DefaultRocketMQListenerContainer.log.warn("consume message failed. messageExt:{}, error:{}", messageExt, var9);
context.setDelayLevelWhenNextConsume(DefaultRocketMQListenerContainer.this.delayLevelWhenNextConsume);
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
}