RocketMQ思维导图,不看会后悔哟
Mysql思维导图分享
上面思维导图可去gongzhonghao回复:扣扣号,获取联系方式后找我免费获得可编辑版本。 后面会继续分享其他思维导图,包括Redis、JVM、并发编程、RocketMQ、RabbtiMQ、Kafka、spring、Zookeeper、Dubbo等等 |
消息的发送方式有三种:单向发送(oneway)、同步发送、异步发送。
消息的发送过程如下:
生产者发送请求到MQ服务器
MQ服务器处理请求
MQ服务器向生产者返回应答
oneway发送了不等待应答,即在实现层面仅仅将数据写入 socket缓冲区,此过程耗时通常在微秒级。
所以对可靠性要求并不高,例如日志收集类应用可以采用oneway形式调用。
public void sendOneway(Message msg) throws MQClientException, RemotingException, InterruptedException {
this.defaultMQProducerImpl.sendOneway(msg);
}
单向发送没有重试机制哟!
同步发送是发送后会在等到响应之后才算是发送完。
public SendResult send(Message msg) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
return this.defaultMQProducerImpl.send(msg);
}
同步发送可查看返回结果。send消息方法只要不抛异常,就代表发送成功。发送成功会有多种状态,在SendResult里定义。各种状态说明如下:
SEND_OK
消息发送成功。要注意的是消息发送成功也不意味着它是可靠的。要确保不会丢失任何消息,还应启用同步Master服务器或同步刷盘,即SYNC_MASTER或SYNC_FLUSH。
FLUSH_DISK_TIMEOUT
此状态说明消息虽已经进入MQ服务器队列(内存),但是服务器刷盘超时。
因为消息存储配置参数中可以设置刷盘方式和同步刷盘时间长度,如果Broker服务器设置了刷盘方式为同步刷盘,即FlushDiskType=SYNC_FLUSH(默认为异步刷盘方式),当Broker服务器 未在同步刷盘时间内(默认为5s)完成刷盘,则将返回该状态——刷盘超时。
因为还在内存中,所以此时如果MQ服务器宕机,消息会丢失。
FLUSH_SLAVE_TIMEOUT
消息发送成功,但是MQ服务器同步到Slave时超时。此时消息已经进入服务器队列,只有服务器宕机,消息才会丢失。如果Broker服务器的角色是同步Master,即SYNC_MASTER(默认是异步Master即ASYNC_MASTER),并且从Broker服务器未在同步刷盘时间(默认为5秒)内完成与主服务器的同步,则将返回该状态——数据同步到Slave服务器超时。
SLAVE_NOT_AVAILABLE
消息发送成功,但是此时Slave不可用。如果Broker服务器的角色是同步Master,即SYNC_MASTER(默认是异步Master服务器即ASYNC_MASTER),但没有配置slave Broker服务器,则将返回该状态——无Slave服务器可用。
异步发送是指发送后不会等等MQ服务返回本次消息发送的结果,只需提供一个回调函数供MQ服务器异步回调生产者。
public void send(Message msg, SendCallback sendCallback) throws MQClientException, RemotingException, InterruptedException {
this.defaultMQProducerImpl.send(msg, sendCallback);
}
异步发送提高了发送效率,但可能会给MQ服务器带来压力,所以异步发送时RocketMQ对消息发送进行了并发控制(默认65535),通过参数clientAsyncSemaphoreValue配置。
异步发送虽然也可以通过DefaultMQProducer#retryTimesWhenSendAsyncFailed属性来控制发消息的发生重试次数,但是重试的调用入口是在收到MQ服务器的响应包之后才进行的,如果出现网络异常、网络超时等未收到响应包的情况将不会重试。
像日志这种不是太重要的,首选当然是“单向发送”。当然,如果该日志较为重要还是选择别的方式。
一致性要求高的(多数),最好是在发消息前将消息存在本地库,然后专门有一个字段存储当前消息的最新状态。如果有失败,可通过定时任务去兜底重新发送。此时无论选择哪种发送都不是太重要了。因为会有定时任务重新发消息,所以消费者记得去重判断;其实生产者不重发,RocketMQ的设计里也会有可能导致同一条消息被多次消费,所以去重判断是必须的,除非系统没有这个要求。
其实具体怎么选还是要看自己项目的整体设计的,我举一个我在项目中的设计。“消费者端”只允许重新消费两次,两次之后就记录下来,并告诉MQ服务器消费成功了;在“生产者端”,每条消息发送之前都落库了,消费者消费成功了会将库里该条数据的状态进行变更。还加了一个定时,十分钟前状态还未消费成功的,就会重发一次消息。
这种设计下,即使选了异步发送,我们收到了异步回调是成功的消息,但是消费者可能会因为超过重试次数而让此条消息被丢掉了。所以此时选异步发送的SendCallback 对系统来讲也没什么太大的意义。所以此时即使选择“单向发送”也是可以的。
单向发送无重试,其它两重试逻辑如下:
1.默认最多重试2次。
2.同步模式发送失败,则轮询到下一个Broker;异步模式发送失败,则只会在当前Broker进行重试。这个方法的总耗时时间不超过sendMsgTimeout设置的值,默认10s。
如果向broker发送消息产生超时异常,则不会再重试。
生产者端可能会误以为发送失败而将同一条消息发两遍,这就会导致同一条消息可能会有两个不同的msgId(消息ID),所以同一条消息可能会存在两个不同的msgId。
所以我们的消息一定要有一个我们自己能区别的唯一标识,比如任务ID、订单ID。每次发送消息时都带着订单ID,那么即使同一条消息多次发送,在消息者端也能通过订单ID去判断是否已经处理过了。
1.增加消费者
同一 ConsumerGroup 下,通过增加消费者数量来提高消费速度(注:超过订阅队列数的消费者无效)
2.批量消费
通过设置 consumer的 consumeMessageBatchMaxSize 这个参数,默认是 1,即一次只消费一条消息
3.扔掉消息
当消息因处理不过来堆积到一定阈值时,可以扔掉后面的消息,即直接返回成功。直接返回成功,MQ服务就认为处理成功了。示例如下:
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> messages, ConsumeConcurrentlyContext context) {
long offset = messages.get(0).getQueueOffset();
String maxOffset = messages.get(0).getProperty(MessageConst.PROPERTY_MAX_OFFSET);
long diff = Long.parseLong(maxOffset) - offset;
if (diff > 10000) {
// 消息堆积后直接返回成功,即不处理该消息了
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
// bug师姐 假装业务逻辑.....
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
4.优化耗时代码
消费速度变慢,除了量本身很大以为,可以检查下消费者代码是不是有可以优化的地方。比如常见单条操作数据库改成批量、单条远程调用改成批量操作等等。
5.提高消费者的并发线程数
配置消费者的消费并行线程,通过修改参数 consumeThreadMin、consumeThreadMax实现,默认最大是64,最小是20:
public DefaultMQPushConsumer(String consumerGroup, RPCHook rpcHook, AllocateMessageQueueStrategy allocateMessageQueueStrategy) {
this.messageModel = MessageModel.CLUSTERING;
this.consumeFromWhere = ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET;
this.consumeTimestamp = UtilAll.timeMillisToHumanString3(System.currentTimeMillis() - 1800000L);
this.subscription = new HashMap();
this.consumeThreadMin = 20;
this.consumeThreadMax = 64;
this.adjustThreadPoolNumsThreshold = 100000L;
this.consumeConcurrentlyMaxSpan = 2000;
this.pullThresholdForQueue = 1000;
this.pullInterval = 0L;
this.consumeMessageBatchMaxSize = 1;
this.pullBatchSize = 32;
this.postSubscriptionWhenPull = false;
this.unitMode = false;
this.consumerGroup = consumerGroup;
this.allocateMessageQueueStrategy = allocateMessageQueueStrategy;
this.defaultMQPushConsumerImpl = new DefaultMQPushConsumerImpl(this, rpcHook);
}
从DefaultMQPushConsumerImpl里的checkConfig源码可看出,最大并行线程数上限是1000.
private void checkConfig() throws MQClientException {
if (!orderly && !concurrently) {
throw new MQClientException("messageListener must be instanceof MessageListenerOrderly or MessageListenerConcurrently" + FAQUrl.suggestTodo("https://github.com/alibaba/RocketMQ/issues/41"), (Throwable)null);
} else if (this.defaultMQPushConsumer.getConsumeThreadMin() >= 1 && this.defaultMQPushConsumer.getConsumeThreadMin() <= 1000 && this.defaultMQPushConsumer.getConsumeThreadMin() <= this.defaultMQPushConsumer.getConsumeThreadMax()) {
if (this.defaultMQPushConsumer.getConsumeThreadMax() >= 1 && this.defaultMQPushConsumer.getConsumeThreadMax() <= 1000) {
if (this.defaultMQPushConsumer.getConsumeConcurrentlyMaxSpan() >= 1 && this.defaultMQPushConsumer.getConsumeConcurrentlyMaxSpan() <= 65535) {
if (this.defaultMQPushConsumer.getPullThresholdForQueue() >= 1 && this.defaultMQPushConsumer.getPullThresholdForQueue() <= 65535) {
if (this.defaultMQPushConsumer.getPullInterval() >= 0L && this.defaultMQPushConsumer.getPullInterval() <= 65535L) {
if (this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize() >= 1 && this.defaultMQPushConsumer.getConsumeMessageBatchMaxSize() <= 1024) {
if (this.defaultMQPushConsumer.getPullBatchSize() < 1 || this.defaultMQPushConsumer.getPullBatchSize() > 1024) {
throw new MQClientException("pullBatchSize Out of range [1, 1024]" + FAQUrl.suggestTodo("https://github.com/alibaba/RocketMQ/issues/41"), (Throwable)null);
}
} else {
throw new MQClientException("consumeMessageBatchMaxSize Out of range [1, 1024]" + FAQUrl.suggestTodo("https://github.com/alibaba/RocketMQ/issues/41"), (Throwable)null);
}
} else {
throw new MQClientException("pullInterval Out of range [0, 65535]" + FAQUrl.suggestTodo("https://github.com/alibaba/RocketMQ/issues/41"), (Throwable)null);
}
} else {
throw new MQClientException("pullThresholdForQueue Out of range [1, 65535]" + FAQUrl.suggestTodo("https://github.com/alibaba/RocketMQ/issues/41"), (Throwable)null);
}
} else {
throw new MQClientException("consumeConcurrentlyMaxSpan Out of range [1, 65535]" + FAQUrl.suggestTodo("https://github.com/alibaba/RocketMQ/issues/41"), (Throwable)null);
}
} else {
throw new MQClientException("consumeThreadMax Out of range [1, 1000]" + FAQUrl.suggestTodo("https://github.com/alibaba/RocketMQ/issues/41"), (Throwable)null);
}
} else {
throw new MQClientException("consumeThreadMin Out of range [1, 1000]" + FAQUrl.suggestTodo("https://github.com/alibaba/RocketMQ/issues/41"), (Throwable)null);
}
}
当建立新的消费者组时,可以指定消费已经存在于 Broker 中的历史消息
CONSUME_FROM_LAST_OFFSET :将会忽略历史消息,并消费之后生成的任何消息。
CONSUME_FROM_FIRST_OFFSET :将会消费每个存在于 Broker 中的信息。
CONSUME_FROM_TIMESTAMP :消费在指定时间戳后产生的消息。
几年前用rabbitmq的时候遇到一个问题:消费端有个bug导致消费不成,结果一直重试,当每条消息都要重试,而且还不停的有新的消息过来。消费不成功倒影响不大,但是产生了大量的无用日志。
大量重试的原因就是当时没有设置重试次数导致的。试想一想,如果消费者是因为超时导致的,结果大量重试就会形成洪泛攻击,最终把服务打垮。
所以最好的做法是,每条消息设置一个重试上限,达到重试上限之后,将数据记录下来做特殊处理。代码思路如下:
public ConsumeConcurrentlyStatus consumeMessage(MessageExt message, ConsumeConcurrentlyContext context) {
try {
// bug师姐 假装这里是很复杂的消费业务逻辑.....
System.out.println(context.getAckIndex());
} catch (Exception e) {
if (message.getReconsumeTimes() < this.getRetryLimitTimes()) {
message.setReconsumeTimes(message.getReconsumeTimes() + 1);
return ConsumeConcurrentlyStatus.RECONSUME_LATER;
}
this.savleMysql(message);// 超过重试次数处理的伪代码
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;// 失败也返回成功
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
public int getRetryLimitTimes() {
return 2;
}