MQ消息队列

        微服务开发中经常会使用消息队列进行跨服务通信。在一个典型场景中,服务A执行一个业务逻辑,需要保存数据库,然后通知服务B执行相应的业务逻辑。在这种场景下,我们需要考虑如何发送消息。

        

 基于消息队列,生产者发送消息到消费者消费消息的过程:

  1. 生产者发送消息

  2. MQ收到消息并将数据持久化,在存储中新增一条记录

  3. 返回ACK给生产者

  4. MQ 推送消息给对应的消费者,等待消费者返回ACK

  5. 如果消费者在指定时间内成功返回ACK,那么MQ则认为消费成功,执行第6步删除消息,如果MQ在指定时间内没有收到ACK,则认为消息消费失败,会重新推送消息,重复执行第4、5、6步操作。

  6. 删除消息。

        

1 基础版

        首先,我们可能会考虑将数据库操作和消息发送放在同一个事务中,以下是伪代码示例:

@Transactional  
public void saveWithMessage(BusinessDO businessDO){ 
 String id = IdUtils.nextId();
 businessDO.setId(id);
    xxxRepository.save(businessDO);  
    
    BusinessMessage businessMessage = new BusinessMessage();  
    businessMessage.setKey(id);  
    SendResult send = rocketMQTemplate.syncSend("test-topic", sendMessage);
}

        在这段代码里通过@Transactional注解将数据库的操作以及发送消息放到一个事务中,如果数据库的保存或者消息发送失败,则回滚事务。

1.1 缺陷  

1.1.1 数据不一致

        这种消息发送方式无法保证数据的最终一致性。

        假设数据库处理成功,消息消费成功,但是MQ由于某些原因处理超时,导致ACK确认失败,此时整个事务回滚,结果出现数据不一致问题。

        这种数据不一致的问题在RPC调用的场景下也经常出现,其根本的原因在于:远程调用,结果最终可能为成功、失败、超时;而对于超时的情况,处理方最终的结果可能是成功,也可能是失败,调用方是无法知晓的。

1.1.2 事务未提交

        其次,使用以上方式还存在另一个问题,即消费者在处理消息时可能读不到刚刚保存的数据,即消费者消费速度快于事务提交的速度。

举例:

假设服务B需要通过消息中的数据ID获取服务A数据库保存的数据。这种情况下,数据库操作与消息发送在同一事务中。可能出现服务B在处理消息时,服务A事务还未提交,导致服务B获取的数据是空数据,无法执行相应业务逻辑。

1.1.3 适用场景

        尽管这种发送方式存在上述两个问题,但在某些场景下仍然适用。例如,消费者在处理时不依赖生产者的数据,且对数据一致性要求不高,这种情况下消息发送和数据库保存可以不在同一个事务中。

2 进阶版

        为解决事务未提交问题,我们可以确保事务提交后再发送消息。在SpringBoot项目中,有两种常见解决方案。

2.1 事务同步器

        基于事务同步器的方法:

@Transactional  
public void saveWithMessage(BusinessDO businessDO){  
    xxxRepository.save(businessDO);  
    
    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {  
        @Override  
        public void afterCommit() {  
            BusinessMessage businessMessage = new BusinessMessage();  
            businessMessage.setXXX();  
            rocketMQTemplate.syncSend("test-topic", sendMessage);  
        }  
    });  
}

  TransactionSynchronizationManager.registerSynchronization 是 Spring 框架中用于注册事务同步的方法。通过这个方法,你可以在事务提交、回滚或完成时执行一些额外的逻辑。

        在上述代码中,使用了afterCommit方法,在事务成功提交后执行发送消息操作,确保在数据库操作成功且事务稳定的情况下发送消息。

2.2 消息监听器

        另一种方法是基于ApplicationEventPublisher,在保存数据库操作后发布一个事件,并通过@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)注解监听事件。这样可以确保消息在数据库事务提交之后再发送。

@Transactional  
public void saveWithMessage(BusinessDO businessDO){  
    xxxRepository.save(businessDO);  

 eventPublisher.publishEvent(new UserCreatedEvent(registerUser));
}

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) 
public void handleUserRegisteredEvent(UserCreatedEvent userCreatedEvent) {  
    rocketMQTemplate.syncSend("test-topic", sendMessage);  
}

        这里需要说明一下:在默认情况下Spring的事件监听机制并不是异步的,而是同步的将代码进行解耦,@TransactionalEventListener也是通过同步的方式,但是加入了回调的方式来解决,这样就能够控制事务进行Commited、Rollback时才进行事件的处理,来达到事务同步的目的。

2.3 适用范围

        通过以上方式,相较于基础版,可以确保消息在事务提交后发送,解决了消费者读取空数据的问题。但仍然无法保证数据的一致性,适用于对数据一致性要求不高的场景。

3 本地消息表+补偿重试

        如果需要保证最终一致性而非强一致性,可以采用本地消息表+补偿重试的方式来发送消息。

这种方式的执行原理如下:

  • 在执行业务操作的同时,在本地消息表中插入一条状态为待发送的记录,业务数据的记录与消息记录必须在同一个事务中完成,这是此方案的核心原则。由于消息表与业务表在同一个库中,事务可以通过数据库来保证。

  • 事务提交后发送消息,如果消息发送成功,则将消息状态标记为发送成功或删除消息

  • 在生产者服务中会创建一个定时任务,定时从消息表中检索待发送的消息重新发送。

  • 对于消费者消费失败,则依赖MQ本身的重试机制来完成,保证数据的最终一致性。

        核心代码如下:

@Transactional  
public void saveWithMessage(BusinessDO businessDO){  
    TransactionMessage transactionMessage = new TransactionMessage();  
    transactionMessage.setStaus(MessageStatus.WAITING_SEND);  
    transactionMessage.setMessageKey(businessDO.getId());  
    ...  
  
    xxxRepository.save(businessDO);  
    messageRepository.save(TransactionMessage);  
    TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {  
        @Override  
        public void afterCommit() {  
            messageService.sendMessage(transactionMessage,businessDO);  
        }  
    });  
}  

public void sendMessage(TransactionMessage transactionMessage,BusinessDO businessDO){  
    BusinessMessage businessMessage = new BusinessMessage();  
    businessMessage.setXXX();  
    try{  
        rocketMQTemplate.syncSend("test-topic", sendMessage);  
        transactionMessage.setStatus(MessageStatus.SUCCESS);  
        messageRepository.update(transactionMessage);  
    }catch (Exception e){  
        // 执行失败的业务逻辑  
    }  
  
}

3.1 问题

        虽然这种方式能够保证消息在事务提交后发送,且能够保证最终一致性,但仍然存在一些缺陷:

        首先,需要额外的消息表,增加了系统复杂度。(针对此问题,我们又可以将该功能单独提取出来,做成一个消息服务来统一处理)

        其次,通过定时任务轮询消息表,对于处理成功但ACK超时的数据会重新发送消息,这对下游系统产生了强烈的幂等性保障要求,消费者的处理逻辑必须做好幂等控制。

4 基于事务消息发送

        目前,RocketMQ是主流MQ中唯一一个支持事务消息的,如果你们项目恰好使用的是RocketMQ,可以采用事务消息来发送。

5 总结

        本文详细介绍了微服务开发中常用的4种消息发送方式。对于那些对数据一致性要求不高的场景,可以选择使用进阶版的消息发送方式。而对于需要保证最终一致性的情况,推荐采用事务消息和本地消息表的方式进行消息发送。

### MQ 消息队列的使用教程与解决方案 #### 一、常见 MQ 消息队列产品的选择 在现代分布式系统中,消息队列(Message Queue, MQ)是一个重要的组件。常见的 MQ 消息队列产品包括 RabbitMQ、Kafka、ZeroMQ 和 ActiveMQ 等[^1]。每种产品都有其特定的优势和适用场景: - **RabbitMQ**: 基于 AMQP 协议实现的消息中间件,适合用于复杂的路由规则和高可靠性需求的应用场景。 - **Kafka**: 高吞吐量的日志收集和流数据处理平台,适用于大数据分析和实时数据管道构建。 - **ZeroMQ**: 提供轻量级的消息传递库,支持多种模式下的高性能通信。 - **ActiveMQ**: 功能全面的企业级消息中间件,兼容 JMS 标准。 #### 二、MQ 的基础功能及其优势 MQ 的核心功能在于提供可靠的消息通信机制。具体来说,它可以完成以下几个方面的工作[^2]: - **消息通信**: 客户端可以通过生产者将消息发送至 MQ 并由消费者获取这些消息。 - **异步处理**: 生产者无需等待消费者的响应即可继续执行其他操作,从而提升系统的整体性能。 - **应用解耦**: 不同模块之间通过消息队列进行交互,减少了直接依赖关系。 - **削峰填谷**: 当流量激增时,利用消息队列缓冲请求,避免下游服务因瞬时压力过大而导致崩溃。 #### 三、典型应用场景解析 以下是几种典型的 MQ 使用场景以及对应的解决思路[^3][^4]: ##### 场景 1: 秒杀活动中的订单处理 对于电商网站上的秒杀活动而言,短时间内可能会涌入大量用户提交订单请求。如果直接让数据库承受如此高的访问频率,则极有可能造成系统瘫痪。此时可采用如下策略: 1. 将用户的下单请求先存入消息队列; 2. 如果队列长度超出预设阈值,则拒绝新增加的请求或者引导客户查看错误提示页; 3. 后台工作线程按固定速率依次取出待处理的任务并进一步验证库存状态等逻辑后再决定是否真正创建该笔交易记录。 ```python import pika def send_order_to_queue(order_id): connection = pika.BlockingConnection(pika.ConnectionParameters('localhost')) channel = connection.channel() queue_name = 'seckill_orders' max_length = 1000 arguments={ 'x-max-length':max_length, 'x-overflow':'reject-publish', } channel.queue_declare(queue=queue_name,durable=True,arguments=arguments) message=str(order_id) channel.basic_publish(exchange='',routing_key=queue_name,body=message) print(f"[x] Sent {message}") connection.close() send_order_to_queue(1234567890) ``` ##### 场景 2: 日志采集与监控 企业内部往往存在多个独立运行的服务实例,它们各自产生的日志文件难以统一管理。借助 Kafka 可以搭建一套集中式的日志管理系统: 1. 所有的微服务都将自身的运行状况信息推送到指定主题上; 2. 数据分析师可以从订阅渠道读取原始资料进而开展探索性研究项目比如异常检测预警等等。 --- ###
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

IT_LOSER

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值