MQ的8种常用场景
场景一:异步处理
咱们先说说最常见的异步处理场景。想象一下,你有个电商系统,用户下单之后,需要做一系列的操作:扣库存、生成订单、发送通知、记录日志……如果这些操作都串在一起同步执行,那用户不得等个好几秒才能看到下单成功的提示?这体验可就不太好了。
这时候,MQ 就派上用场了。用户下单后,先把订单信息扔到 MQ 里,然后扣库存服务、生成订单服务、发送通知服务、记录日志服务各自从 MQ 里拿消息来处理。
这样一来,用户下单的操作就只需要把消息扔到 MQ 里,然后就可以立即返回给用户“下单成功”的提示了,后续的操作异步进行。
// 伪代码示例
public void placeOrder(Order order) {
// 把订单信息扔到MQ
mqClient.send("order-topic", order);
// 立即返回给用户下单成功的提示
System.out.println("下单成功!");
}
// 扣库存服务
public void deductInventory(Order order) {
// 从MQ里拿到订单信息,扣库存
mqClient.receive("order-topic", (message) -> {
Order order = (Order) message;
// 扣库存逻辑
});
}
// 其他服务类似
场景二:削峰填谷
再来说说削峰填谷。有时候,咱们的系统会遇到一些突发的流量高峰,比如秒杀活动。这时候,如果所有的请求都直接打到数据库上,那数据库估计得被压垮。
MQ 这时候就可以起到一个缓冲的作用。所有的请求先扔到 MQ 里,然后后端的服务慢慢地从 MQ 里消费请求来处理。这样,即使流量再大,也不会直接把后端服务打垮。
// 伪代码示例
public void participateInSeckill(SeckillRequest request) {
// 把请求扔到MQ
mqClient.send("seckill-topic", request);
}
// 后端处理服务
public void handleSeckillRequest(SeckillRequest request) {
// 从MQ里拿到请求,处理秒杀逻辑
mqClient.receive("seckill-topic", (message) -> {
SeckillRequest request = (SeckillRequest) message;
// 处理秒杀逻辑
});
}
场景三:系统解耦
系统解耦也是 MQ 的一大用处。比如,咱们有个用户服务,有个订单服务,还有个通知服务。用户下单后,需要通知服务发送通知给用户。如果咱们直接把通知服务的调用写在用户服务里,那将来通知服务要是换个实现方式,或者出个啥问题,用户服务岂不是也得跟着改?
这时候,咱们就可以用 MQ 来解耦。用户服务下单后,把通知的消息扔到 MQ 里,通知服务自己从 MQ 里拿消息来处理。这样,用户服务和通知服务就通过 MQ 松耦合地连接在一起了。
// 伪代码示例
public void placeOrder(Order order) {
// 下单逻辑
// ...
// 把通知消息扔到MQ
mqClient.send("notification-topic", new NotificationMessage(order.getUserId(), "您已下单成功!"));
}
// 通知服务
public void sendNotification(NotificationMessage message) {
// 从MQ里拿到通知消息,发送通知
mqClient.receive("notification-topic", (msg) -> {
NotificationMessage message = (NotificationMessage) msg;
// 发送通知逻辑
});
}
场景四:日志收集
日志收集也是 MQ 的一个常见使用场景。咱们的系统,每天都会产生大量的日志,这些日志需要集中收集起来进行分析。如果每个服务都直接把日志写到同一个地方,那不得乱套了?
这时候,咱们可以用 MQ 来收集日志。每个服务把日志消息扔到 MQ 里,然后日志收集服务从 MQ 里拿消息来处理,把日志写到该写的地方。
// 伪代码示例
public void logInfo(String log) {
// 把日志消息扔到MQ
mqClient.send("log-topic", log);
}
// 日志收集服务
public void collectLogs() {
// 从MQ里拿到日志消息,处理日志
mqClient.receive("log-topic", (message) -> {
String log = (String) message;
// 处理日志逻辑
});
}
场景五:分布式事务
分布式事务,也是个头疼的问题。不过呢,MQ 也可以在一定程度上帮忙解决。比如咱们有两个服务,服务 A 和服务 B,需要在一个事务里同时操作。这时候,咱们可以用 MQ 来实现一个最终一致性的事务。
服务 A 先操作,然后把操作结果扔到 MQ 里,服务 B 从 MQ 里拿到消息后再进行操作。如果服务 B 操作失败了,那就重试,直到成功为止。这样,虽然不能完全保证事务的原子性,但是可以保证最终的一致性。
// 伪代码示例
public void serviceAOperation() {
// 服务A的操作
// ...
// 把操作结果扔到MQ
mqClient.send("transaction-topic", "服务A操作成功");
}
// 服务B
public void serviceBOperation() {
// 从MQ里拿到服务A的操作结果,进行服务B的操作
mqClient.receive("transaction-topic", (message) -> {
String result = (String) message;
if ("服务A操作成功".equals(result)) {
// 服务B的操作
} else {
// 重试逻辑
}
});
}
场景六:消息广播
消息广播也是 MQ 的一个常用场景。比如咱们有个配置中心,当配置发生变化时,需要通知所有的服务。这时候,咱们就可以把配置变更的消息扔到 MQ 里,所有的服务从 MQ 里拿消息来处理。
// 伪代码示例
public void configChanged(String newConfig) {
// 把配置变更消息扔到MQ
mqClient.send("config-topic", newConfig);
}
// 服务A
public void handleConfigChange() {
// 从MQ里拿到配置变更消息,处理配置变更
mqClient.receive("config-topic", (message) -> {
String newConfig = (String) message;
// 处理配置变更逻辑
});
}
// 服务B、服务C类似
场景七:任务分发
任务分发也是 MQ 的一个好帮手。比如咱们有个任务处理系统,需要处理各种各样的任务。这时候,咱们可以把任务扔到 MQ 里,然后多个任务处理服务从 MQ 里拿任务来处理。
// 伪代码示例
public void submitTask(Task task) {
// 把任务扔到MQ
mqClient.send("task-topic", task);
}
// 任务处理服务A
public void processTask() {
// 从MQ里拿到任务,处理任务
mqClient.receive("task-topic", (message) -> {
Task task = (Task) message;
// 处理任务逻辑
});
}
// 任务处理服务B、服务C类似
场景八:数据流处理
最后来说说数据流处理。在大数据处理领域,MQ 也是非常重要的一个组件。比如咱们有个数据流处理系统,需要实时地处理数据。这时候,咱们可以把数据流扔到 MQ 里,然后多个数据处理服务从 MQ 里拿数据来处理。
// 伪代码示例
public void generateDataStream(DataStream dataStream) {
// 把数据流扔到MQ
mqClient.send("data-stream-topic", dataStream);
}
// 数据处理服务A
public void processDataStream() {
// 从MQ里拿到数据流,处理数据流
mqClient.receive("data-stream-topic", (message) -> {
DataStream dataStream = (DataStream) message;
// 处理数据流逻辑
});
}
// 数据处理服务B、服务C类似
消息队列还是直接使用线程池异步处理?(两者区别)
1、 处理任务的维度 不同
多线程:在一个进程中 可以有多个线程并行处理任务
MQ:把消息发送到 不同应用节点的不同进程来处理任务
2、数据可靠性不同
多线程:数据基于共享内存来交互,应用崩溃/重启 时,数据丢失
MQ:采用消息队列的持久化机制来 保证消息的可靠性
3、分布式能力方面
多线程:只能在一个进程中处理任务
MQ:具备分布式能力,高可用、高性能、高并发
线程池应用场景
- 单一进程的多线程的场景中 不需要跨进程
- 执行任务比较轻、执行时间比较短
- 并发数量有限且 可控
消息队列应用场景
- 分布式架构下 跨进程跨服务传输消息
- 对可靠性和性能 有更高要求
- 应用系统面对的流量比较大
- 涉及到 业务之间关系解耦的 一个场景
- 流量削峰
避免重复消费(保证消息幂等性)
-
方式1: 消息全局ID或者写个唯一标识(如时间戳、UUID等) :每次消费消息之前根据消息id去判断该消息是否已消费过,如果已经消费过,则不处理这条消息,否则正常消费消息,并且进行入库操作。(消息全局ID作为数据库表的主键,防止重复)
-
方式2: 利用Redis的setnx 命令:给消息分配一个全局ID,只要消费过该消息,将 < id,message>以K-V键值对形式写入redis,消费者开始消费前,先去redis中查询有没消费记录即可
-
方式3: rabbitMQ的每一个消息都有
redelivered
字段,可以获取是否是被重新投递过来的,而不是第一次投递过来的
发送消息
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 发送消息
*/
public void sendMessage() {
// 创建消费对象,并指定 全局唯一ID(这里使用UUID,也可以根据业务规则生成,只要保证全局唯一即可)
MessageProperties messageProperties = new MessageProperties ();
messageProperties.setMessageId (UUID.randomUUID ().toString ());
messageProperties.setContentType ("text/plain");
messageProperties.setContentEncoding ("utf-8");
Message message = new Message ("hello,message idempotent!".getBytes (), messageProperties);
System.out.println ("生产消息:" + message.toString ());
rabbitTemplate.convertAndSend (EXCHANGE_NAME, ROUTE_KEY, message);
}
消费消息
/**
* 消费消息
*
* @param message
* @param channel
* @throws IOException
*/
@RabbitHandler
//org.springframework.amqp.AmqpException: No method found for class [B 这个异常,并且还无限循环抛出这个异常。
//注意@RabbitListener位置,笔者踩坑,无限报上面的错,还有另外一种解决方案: 配置转换器
@RabbitListener(queues = "message_idempotent_queue")
@Transactional
public void handler(Message message, Channel channel) throws IOException {
/**
* 发送消息之前,根据消息全局ID去数据库中查询该条消息是否已经消费过,如果已经消费过,则不再进行消费。
*/
// 获取消息Id
String messageId = message.getMessageProperties ().getMessageId ();
if (StringUtils.isBlank (messageId)) {
logger.info ("获取消费ID为空!");
return;
}
MessageIdempotent messageIdempotent = null;
Optional<MessageIdempotent> list = messageIdempotentRepository.findById (messageId);
if (list.isPresent ()) {
messageIdempotent = list.get ();
}
// 如果找不到,则进行消费此消息
if (null == messageIdempotent) {
//获取消费内容
String msg = new String (message.getBody (), StandardCharsets.UTF_8);
logger.info ("-----获取生产者消息-------------------->" + "messageId:" + messageId + ",消息内容:" + msg);
//手动ACK
channel.basicAck (message.getMessageProperties ().getDeliveryTag (), false);
//存入到表中,标识该消息已消费
MessageIdempotent idempotent = new MessageIdempotent ();
idempotent.setMessageId (messageId);
idempotent.setMessageContent (msg);
messageIdempotentRepository.save (idempotent);
} else {
//如果根据消息ID(作为主键)查询出有已经消费过的消息,那么则不进行消费;
logger.error ("该消息已消费,无须重复消费!");
}
}
消息可靠性
从 三方面解决这些问题:
①生产者
②MQ
③消费者
生产者 重连
** 首先 第一种情况,就是 生产者发送消息时,出现了网络故障,导致与 MQ的连接中断。 **
spring:
rabbitmq:
connection-timeout: 1s # 设置MQ的连接超时时间
template:
retry:
enabled: true # 开启超时重试机制
initial-interval: 1000ms # 失败后的初始等待时间
multiplier: 1 # 失败后下次的等待时长倍数,下次等待时长 = initial-interval * multiplier
max-attempts: 3 # 最大重试次数
生产者消息确认
RabbitMQ提供了publisher confirm
机制来避免消息发送到MQ过程中丢失。这种机制必须给每个消息指定一个唯一ID
。消息发送到MQ以后,会返回一个结果给发送者,表示消息是否处理成功。
返回结果有两种方式:
- publisher-confirm,发送者确认
- 消息成功投递到交换机,返回ack
- 消息未投递到交换机,返回nack
- publisher-return,发送者回执
- 消息投递到交换机了,但是没有路由到队列。返回ACK,及路由失败原因
注意!
1、修改配置
修改publisher
服务中的application.yml文件,添加下面的内容:
spring:
rabbitmq:
publisher-confirm-type: correlated
publisher-returns: true
template:
mandatory: true
说明:
publish-confirm-type
:开启 publisher-confirm,这里支持两种类型:simple
:同步等待confirm结果,直到超时correlated
:异步回调,定义ConfirmCallback,MQ返回结果时会回调这个ConfirmCallback
publish-returns
:开启publish-return功能,同样是基于callback机制,不过是定义ReturnCallbacktemplate.mandatory
:定义消息路由失败时的策略。true,则调用ReturnCallback;false:则直接丢弃消息
2、定义 ReturnsCallback
每个RabbitTemplate只能
配置一个ReturnCallback, 因此需要在项目加载时配置
:
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
@Slf4j
@AllArgsConstructor
@Configuration
public class MqConfig {
private final RabbitTemplate rabbitTemplate;
@PostConstruct
public void init() {
rabbitTemplate.setReturnsCallback (new RabbitTemplate.ReturnsCallback () {
@Override
public void returnedMessage(ReturnedMessage returned) {
log.error ("触发return callback,");
log.debug ("exchange: {}", returned.getExchange ());
log.debug ("routingKey: {}", returned.getRoutingKey ());
log.debug ("message: {}", returned.getMessage ());
log.debug ("replyCode: {}", returned.getReplyCode ());
log.debug ("replyText: {}", returned.getReplyText ());
// 如果有业务需要,可以重发消息
}
});
}
}
3、定义 ConfirmCallback
/**
* ConfirmCallback 可以在发送消息时指定,因为每个业务处理 confirm成功或失败的逻辑不一定相同
*/
@Test
void testPublisherConfirm() {
// 1.创建CorrelationData
CorrelationData cd = new CorrelationData();
// 2.给Future添加ConfirmCallback
cd.getFuture().addCallback(new ListenableFutureCallback<CorrelationData.Confirm> () {
@Override
public void onFailure(Throwable ex) {
// 2.1.Future发生异常时的处理逻辑,基本不会触发
log.error("send message fail", ex);
}
@Override
public void onSuccess(CorrelationData.Confirm result) {
// 2.2.Future接收到回执的处理逻辑,参数中的result就是回执内容
if(result.isAck()){ // result.isAck(),boolean类型,true代表ack回执,false 代表 nack回执
log.debug("发送消息成功,收到 ack!");
}else{ // result.getReason(),String类型,返回nack时的异常描述
// todo 将来要重发消息
log.error("发送消息失败,收到 nack, reason : {}", result.getReason());
}
}
});
// 3.发送消息
rabbitTemplate.convertAndSend("hmall.direct", "q", "hello", cd);
}
消息持久化
生产者确认
可以确保消息投递到RabbitMQ的队列
中,但是消息发送到RabbitMQ以后,如果突然宕机,也可能导致消息丢失。
要想 确保消息在RabbitMQ中安全保存,必须开启消息持久化机制
默认发送消息是 持久化的
1、 交换机持久化
RabbitMQ中
交换机
默认是非持久化
的,mq重启后就丢失
@Bean
public DirectExchange simpleExchange(){
// 三个参数:交换机名称、是否持久化、当没有queue与其绑定时 是否自动删除autoDelete
return new DirectExchange("simple.direct", true, false);
}
2、队列持久化
RabbitMQ中
队列
默认是非持久化
的,mq重启后就丢失
@Bean
public Queue simpleQueue(){
// 使用QueueBuilder构建队列,durable就是持久化的
return QueueBuilder.durable("simple.queue").build();
}
消费者消息确认
设想这样的场景:
- 1)RabbitMQ
投递
消息给消费者 - 2)消费者获取消息后,
返回ACK
给RabbitMQ - 3)RabbitMQ删除消息
- 4)消费者宕机,消息尚未处理
这样,消息就丢失了。因此消费者返回ACK的时机非常重要
1、演示auto模式
修改
consumer
服务的application.yml文件,添加下面内容
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: auto # 开启ack
2、 演示manual模式
public void basicAck(long deliveryTag, boolean multiple):
deliveryTag 表示该消息的index(long类型的数字);
multiple 表示是否批量(true:将一次性ack所有小于deliveryTag的消息);
public void basicNack(long deliveryTag, boolean multiple, boolean requeue):告诉服务器这个消息我拒绝接收,basicNack可以一次性拒绝多个消息。
deliveryTag: 表示该消息的index(long类型的数字);
multiple: 是否批量(true:将一次性拒绝所有小于deliveryTag的消息);
requeue:指定被拒绝的消息是否重新回到队列;
public void basicReject(long deliveryTag, boolean requeue):也是用于拒绝消息,但是只能拒绝一条消息,不能同时拒绝多个消息。
deliveryTag: 表示该消息的index(long类型的数字);
requeue:指定被拒绝的消息是否重新回到队列;
acknowledge-mode: manual 改为手动签收
@RabbitListener(queues = "message_confirm_queue")
public void receiveMessage(String msg, Channel channel, Message message) throws IOException {
try {
// 这里模拟一个空指针异常,
String string = null;
string.length ();
log.info ("【Consumer01成功接收到消息】>>> {}", msg);
// 确认收到消息,只确认当前消费者的一个消息收到
channel.basicAck (message.getMessageProperties ().getDeliveryTag (), false);
} catch (Exception e) {
if (message.getMessageProperties ().getRedelivered ()) {
log.info ("【Consumer01】消息已经回滚过,拒绝接收消息 : {}", msg);
// 拒绝消息,并且不再重新进入队列
//public void basicReject(long deliveryTag, boolean requeue)
channel.basicReject (message.getMessageProperties ().getDeliveryTag (), false);
} else {
log.info ("【Consumer01】消息即将返回队列重新处理 :{}", msg);
//设置消息重新回到队列处理
// requeue表示是否重新回到队列,true重新入队
//public void basicNack(long deliveryTag, boolean multiple, boolean requeue)
channel.basicNack (message.getMessageProperties ().getDeliveryTag (), false, true);
}
e.printStackTrace ();
}
}
消费失败重试机制
1、本地重试
利用Spring的retry机制,在消费者出现异常时利用本地重试,而不是无限制的requeue到mq队列
修改
consumer
服务的application.yml文件,添加内容
spring:
rabbitmq:
listener:
simple:
retry:
enabled: true # 开启消费者失败重试
initial-interval: 1000 # 初识的失败等待时长为1秒
multiplier: 1.5 # 失败的等待时长倍数,下次等待时长 = multiplier * last-interval
max-attempts: 3 # 最大重试次数
stateless: true # true无状态;false有状态。如果业务中包含事务,这里改为false
结论:
- 开启本地重试时,消息处理过程中抛出异常,不会requeue到队列,而是在
消费者本地重试
- 重试达到最大次数后,Spring会返回ack,消息会被丢弃
2、失败策略
比较优雅的一种处理方案是RepublishMessageRecoverer,失败后将消息投递到一个指定的,专门存放异常消息的队列
,后续由人工集中处理
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.retry.MessageRecoverer;
import org.springframework.amqp.rabbit.retry.RepublishMessageRecoverer;
import org.springframework.context.annotation.Bean;
@Configuration
public class ErrorMessageConfig {
@Bean
public DirectExchange errorMessageExchange(){
return new DirectExchange("error.direct");
}
@Bean
public Queue errorQueue(){
return new Queue("error.queue", true);
}
@Bean
public Binding errorBinding(Queue errorQueue, DirectExchange errorMessageExchange){
return BindingBuilder.bind(errorQueue).to(errorMessageExchange).with("error");
//with就是指定key
}
@Bean
public MessageRecoverer republishMessageRecoverer(RabbitTemplate rabbitTemplate){
// error.direct:交换机、key:error
return new RepublishMessageRecoverer(rabbitTemplate, "error.direct", "error");
}
}
消息逆序(顺序消费)
RabbitMQ是队列存储,天然具备先进先出的特点,只要消息的发送是有序的,那么理论上接收也是有序的。不过当一个队列绑定了多个消费者时,可能出现消息轮询投递给消费者的情况,而消费者的处理顺序就无法保证
因此,要保证消息的有序性,需要做的下面几点:
- 保证消息发送的有序性
- 保证一组有序的消息都发送到同一个队列
- 保证一个队列只包含一个消费者
这样也会造成吞吐量下降,可以
在消费者内部采用多线程的方式消费
消息积压
- 消息积压问题的出现
生产者速度 > 消费者速度, 导致 大量数据没有被消费,随着数据越积越多,性能会越来越差,导致整个kafka对外提供服务的性能很差
,从而造成其他服务访问速度变慢,造成服务雪崩
- 消息积压的解决方案
- 增加
分区数
、提高每批次拉取数
//fetch.max.bytes 默认50m
properties.put(ConsumerConfig.FETCH_MAX_BYTES_CONFIG, 50 * 1024 * 1024);
//max.poll.records 默认500条
properties.put(ConsumerConfig.MAX_POLL_RECORDS_CONFIG, 500);
- 消费者中,利用 多线程,充分利用机器的性能进行消费!
- 通过业务的架构设计,提升业务层面消费的性能,算法的优化!
⼀般这个时候,只能临时紧急扩容
了,具体操作步骤和思路如下:
- 先
修复 consumer
的问题,确保其恢复消费速度,然后将现有 consumer 都停掉
- 新建⼀个
topic,partition 是原来的 10 倍
, 临时建⽴好原先 10 倍的 queue 数量
- 然后
写⼀个临时的分发数据的 consumer 程序
,这个程序部署上去消费积压的数据,消费之后不做 耗时的处理,直接均匀轮询写⼊临时建⽴好的 10 倍数量的 queue - 接着 临时征⽤ 10 倍的机器来部署 consumer,每⼀批 consumer 消费⼀个临时 queue 的数据。这种做法相当于是
临时将 queue 资源和 consumer 资源扩⼤ 10 倍,以正常的 10 倍速度来消费数据
- 等快速消费完积压数据之后,得恢复原先部署的架构,重新⽤原先的 consumer 机器来消费消息。
总结
事务消息
原理
Rocketmq 未收到rollback、commit也会 补偿回调,MQ也会有补偿机制 :checkLocalTransaction方法
让我们自己处理
TransactionListenerImpl 事务监听器
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 TransactionListenerImpl implements TransactionListener {
// 发送成功给 broker后,可以 执行本地事务
@Override
public LocalTransactionState executeLocalTransaction(Message message, Object o) {
// 执行 订单本地事务
try {
// 如果 本地事务都执行成功了,返回 commit
return LocalTransactionState.COMMIT_MESSAGE;
} catch (Exception e) {
// 本地事务 执行失败,回滚 本地事务
// 更新 broker中的消息状态为 删除
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
// 因为超时 等原因,没有返回 commit或者 rollback
@Override
public LocalTransactionState checkLocalTransaction(MessageExt messageExt) {
// 这里默认是 回滚
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
生产者:
// 接受 RocketMQ 回调的一个 监听器接口
// 会执行 订单本地事务,commit、rollback,回调查询 等逻辑
TransactionListenerImpl transactionListener = new TransactionListenerImpl ();
TransactionMQProducer producer = new TransactionMQProducer ();
ThreadPoolExecutor executorService = new ThreadPoolExecutor (
2,
5,
100,
TimeUnit.SECONDS,
new ArrayBlockingQueue<> (2000)
);
// 设置 对应的线程池,负责执行 回调请求
producer.setExecutorService (executorService);
// 开启 容错机制
producer.setSendLatencyFaultEnable (true);
// 设置 事务监听器
producer.setTransactionListener (transactionListener);
// 设置 发送失败时,最多重试几次
producer.setRetryTimesWhenSendFailed (2);
// 构建 消息体
Message message = new Message (
"PayOrderSuccessTopic",
"Tag",
"MyKey",
"Pay Success".getBytes ());
// 可以 查询发送结果
SendResult sendResult = producer.send (message, 10);
MQ的生产实践中积累的经验总结
1、灵活运用tags来过滤数据
2、基于消息key来定位消息是否丢失
要根据规则给消息设置key,这样一旦消息丢失了,我们可以去RocketMQ根据key查询该消息,进行排查
3、消息零丢失方案的补充
对之前的消息零丢失方案的补充,如果MQ集群全部挂了,那怎么办?生产者就要把消息写入本地文件或数据库暂存起来
4、提高消费者的吞吐量
增加Consumer Group的机器数量,对应的MessageQueue数量也要增加
增加Consumer单台机器的线程数量
开启消费者的批量消费功能,这样回调函数中就可以一次处理多条消息了
5、要不要消费历史消息
consumer支持 设置从哪里开始消费
MQ集群挂掉(集群崩溃设计高可用方案)
降级方案通常的思路:
发送消息到MQ代码里 去try catch捕获异常,如果你发现发送消息到MQ有异常,此时你需要进行重试
重试了,比如 超过3次还是失败,说明此时可能就是你的MQ集群彻底崩溃了
此时你必须 把这条重要的消息写入到本地存储中去,可以是写入数据库里
要不停的 尝试发送消息到MQ去
发现MQ集群恢复了,你 必须有一个后台线程可以 把之前持久化存储的消息都查询出来,然后依次 按照顺序 发送到MQ集群里去
这里要有一个很关键的注意点,就是你把消息写入存储中 暂存时,一定要保证他的顺序
,比如按照顺序一条一条的写入本地磁盘文件去
暂存消息
**流量太多:**解决⽅法就是 对线上系统扩容双段缓冲的⼤⼩,从 512kb 扩容到⼀个缓冲区 10mb。
MQ集群崩溃后,生产者要将消息写入本地硬盘或数据库中,待MQ集群恢复后,要再把这些消息重新发到MQ中。要特别注意,写入和重新发送都要按照原来的顺序
针对这种场景,我们通常都会在你发送消息到MQ的那个系统中设计高可用的降级方案
, 这个降级方案通常的思路是,你需要在你发送
消息到MQ代码里去try catch捕获异常,如果你发现发送消息到MQ有异常,此时你需要进行重试。
如果你发现连续重试了比如超过3次还是失败,说明此时可能就是你的MQ集群彻底崩溃了,此时你必须把这条重要的消息写入到本地 存储中去
,可以是写入数据库里,也可以是写入到机器的本地磁盘文件里去,或者是NoSQL存储中去,几种方式我们都做过,具体要
根据你们的具体情况来决定。
之后你要不停的尝试发送消息到MQ去,一旦发现MQ集群恢复了,你必须有一个后台线程可以把之前持久化存储的消息都查询出来,
然后依次按照顺序发送到MQ集群里去,这样才能保证你的消息不会因为MQ彻底崩溃会丢失。
这里要有一个很关键的注意点,就是你把消息写入存储中暂存时,一定要保证他的顺序
,比如按照顺序一条一条的写入本地磁盘文件去
暂存消息。
而且一旦MQ集群故障了,你后续的所有写消息的代码必须严格的按照顺序把消息写入到本地磁盘文件里去暂存,这个顺序性是要严格
保证的。
只要有这个方案在,那么哪怕你的MQ集群突然崩溃了,你的系统也是不会丢失消息的,对于一些跟金钱相关的金融系统、广告系统来
说,这种高可用的方案设计,是非常的有必要的。
RocketMQ事物消息的代码实现细节
- 发送half 事务消息
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.client.producer.TransactionSendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.common.RemotingHelper;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class TransactionProducer {
public static void main(String[] args) throws Exception {
// 这个东西就是用来接收RocketMQ回调的一个监听器接口
// 这里会实现执行订单本地事务,commit、rollback,回调查询等逻辑
TransactionListener transactionListener = new TransactionListenerImpl();
// 下面这个就是创建一个支持事务消息的Producer
// 对这个Producer还得指定要指定一个生产者分组,随便指定一个名字
TransactionMQProducer producer = new TransactionMQProducer("TestProducerGroup");
// 下面这个是指定了一个线程池,里面会包含一些线程
// 这个线程池里的线程就是用来处理RocketMQ回调你的请求的
// 如果大家对线程池不太了解
// 可以看石杉老师的《互联网Java工程师面试突击第三季》的线程池部分
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("TestThread");
return thread;
}
}
);
// 给事务消息生产者设置对应的线程池,负责执行RocketMQ回调请求
producer.setExecutorService(executorService);
// 给事务消息生产者设置对应的回调函数
producer.setTransactionListener(transactionListener);
// 启动这个事务消息生产者
producer.start();
// 构造一条订单支付成功的消息,指定Topic是谁
Message msg = new Message(
"PayOrderSuccessTopic",
"TestTag",
"TestKey",
("订单支付成功消息").getBytes(RemotingHelper.DEFAULT_CHARSET)
);
// 将消息作为half消息的模式发送出去
SendResult sendResult = producer.sendMessageInTransaction(msg, null);
// 关闭生产者(在实际应用中,通常在程序结束前关闭)
producer.shutdown();
}
}
假如half消息发送失败,或者没收到half消息响应怎么办?
我们已经看到如何发送half消息了,但是假如发送half消息失败了怎么办呢?
此时我们其实会在执行“producer.sendMessageInTransaction(msg, null)”的时候,收到一个异常,发现消息发送失败了。
所以我们可以用下面的代码去关注half消息发送失败的问题:
try {
SendResult sendResult = producer.sendMessageInTransaction(msg, null);
} catch (Exception e) {
// half消息发送失败
// 订单系统执行回滚逻辑,比如说触发支付退款,更新订单状态为“已关闭”
}
那如果一直没有收到half消息发送成功的通知呢?
针对这个问题,我们可以把发送出去的half消息放在内存里,或者写入本地磁盘文件,后台开启一个线程去检查,如果一个half消息超
过比如10分钟都没有收到响应,那就自动触发回滚逻辑。
如果half消息成功了,如何执行订单本地事务?
刚才代码里有一个TransactionListener,这个类也是我们自己定义的,如下所示:
public class TransactionListenerImpl implements TransactionListener {
// 如果half消息发送成功了
// 就会在这里回调你的这个函数,你就可以执行本地事务了
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
// 执行订单本地事务
// 接着根据本地一连串事务执行结果,去选择执行commit or rollback
try {
// 如果本地事务都执行成功了,返回commit
return LocalTransactionState.COMMIT_MESSAGE;
} catch (Exception e) {
// 本地事务执行失败,回滚所有一切执行过的操作
// 如果本地事务执行失败了,返回rollback,标记half消息无效
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
// 检查本地事务状态的方法(如果需要实现)
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
// 这里可以添加检查本地事务状态的逻辑
// 示例:假设事务成功
return LocalTransactionState.COMMIT_MESSAGE;
}
}
如果没有返回commit或者rollback,如何进行回调?
public class TransactionListenerImpl implements TransactionListener {
// 如果half消息发送成功了
// 就会在这里回调你的这个函数,你就可以执行本地事务了
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
// 执行订单本地事务
// 接着根据本地一连串事务执行结果,去选择执行commit or rollback
try {
// 如果本地事务都执行成功了,返回commit
return LocalTransactionState.COMMIT_MESSAGE;
} catch (Exception e) {
// 本地事务执行失败,回滚所有一切执行过的操作
// 如果本地事务执行失败了,返回rollback,标记half消息无效
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
// 如果因为各种原因,没有返回commit或者rollback
@Override
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
// 查询本地事务,是否执行成功了
Integer status = localTrans.get(msg.getTransactionId());
// 根据本地事务的情况去选择执行commit or rollback
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;
}
}
Kafka提高吞吐量
网络 IO
硬件资源
监控调优