微服务之消息队列

         在微服务架构中,服务之间的通信至关重要。而消息队列 (Message Queue, MQ) 作为一种异步通 信机制,能够有效解耦服务,提高系统的可扩展性、可靠性和最终一致性。


1. 微服务为什么要使用消息队列?

在微服务架构中,服务之间通常通过同步调用 (如 REST API) 进行通信。然而,同步调用存在以下问题:

  • 耦合度高: 服务之间直接依赖,任何一个服务出现故障都会影响其他服务。

  • 性能瓶颈: 同步调用会阻塞线程,当调用链路过长时,会导致系统性能下降。

  • 可扩展性差: 增加新的服务需要修改现有服务的代码,扩展性较差。

消息队列通过异步通信机制,能够有效解决上述问题:

  • 解耦服务: 服务之间通过消息队列进行通信,无需直接依赖,降低了耦合度。

  • 提高性能: 异步通信不会阻塞线程,提高了系统的吞吐量和响应速度。

  • 增强可扩展性: 新的服务可以订阅感兴趣的消息,无需修改现有服务的代码,易于扩展。

2. RocketMQ /Kafka/RabbitMQ比较

特性 RocketMQ Kafka RabbitMQ
架构设计 分布式架构,NameServer + Broker + Producer + Consumer 分布式架构,Zookeeper + Broker + Producer + Consumer 基于 AMQP 协议,Broker + Exchange + Queue + Producer + Consumer
设计目标 高吞吐、低延迟、高可靠性,支持事务消息、顺序消息等 超高吞吐、低延迟,适合日志收集、流处理等大数据场景 灵活的路由、消息确认机制,适合企业级应用和复杂消息路由场景
性能 单机吞吐量 10 万级 TPS,延迟毫秒级 单机吞吐量百万级 TPS,延迟毫秒级 单机吞吐量万级 TPS,延迟较低
可靠性 支持消息持久化、消息重试、消息轨迹,保证消息不丢失 支持消息持久化、多副本机制,保证消息不丢失 支持消息持久化、ACK 机制,保证消息不丢失
功能特性 支持顺序消息、事务消息、延迟消息、消息过滤、消息轨迹等 支持分区、副本、流处理,功能相对单一 支持灵活的路由规则、消息确认、优先级队列、死信队列等
消息模型 基于 Topic 的发布订阅模型,支持 Pull 和 Push 模式 基于 Topic 的分区模型,支持 Pull 模式 基于 Exchange 和 Queue 的路由模型,支持 Push 模式
消息顺序 支持严格的消息顺序(分区顺序) 支持分区内的消息顺序 不支持全局消息顺序,单队列内有序
事务支持 支持分布式事务消息(半消息机制) 不支持事务消息,但可以通过流处理实现类似功能 支持事务消息,但性能较差
延迟消息 支持延迟消息(固定级别) 不支持延迟消息,可通过外部实现 支持延迟消息(插件实现)
消息堆积能力 支持海量消息堆积,性能稳定 支持海量消息堆积,性能稳定 消息堆积能力较弱,性能下降明显
扩展性 支持水平扩展,NameServer 无状态,Broker 可动态扩展 支持水平扩展,Zookeeper 管理集群状态,Broker 可动态扩展 支持集群扩展,但扩展性相对较弱
运维复杂度 中等,需要部署 NameServer 和 Broker,配置较复杂 较高,需要部署 Zookeeper 和 Broker,运维成本较高 较低,部署简单,易于管理
社区生态 阿里巴巴开源,中文社区活跃,文档丰富 Apache 顶级项目,全球社区活跃,生态丰富 开源,社区活跃,文档丰富,插件生态完善
适用场景 电商、金融、物联网等高并发、高可靠性场景 日志收集、实时流处理、大数据分析等超高吞吐场景 企业级应用、复杂消息路由、低延迟场景
优点 高性能、高可靠、功能丰富,适合大规模分布式系统 超高吞吐、低延迟,适合大数据场景 灵活的路由、消息确认机制,易于使用
缺点 学习成本较高,配置复杂 功能相对单一,运维成本高 性能和吞吐量较低,不适合超大规模场景
  • RabbitMQ 在吞吐量方面虽然稍逊于 Kafka 和 RocketMQ ,但是由于它基于 erlang 开发,所以并发能力很强,性能极其好,延时很低,达到微秒级。如果业务场景对并发量要求不是太高(十万级、百万级),那这几个消息队列中,RabbitMQ 一定是你的首选。如果是大数据领域的实时计算、日志采集等场景,用 Kafka 是业内标准的.

  • RocketMQ 阿里出品,Java 系开源项目,源代码我们可以直接阅读,然后可以定制自己公司的MQ,并且RocketMQ 有阿里巴巴的实际业务场景的实战考验。RocketMQ 社区活跃度相对较为一般,不过也还可以,文档相对来说简单一些.

  • kafka 的特点其实很明显,就是仅仅提供较少的核心功能,但是提供超高的吞吐量,ms 级的延迟,极高的可用性以及可靠性,而且分布式可以任意扩展。同时 kafka 最好是支撑较少的 topic 数量即可,保证其超高吞吐量。kafka 唯一的一点劣势是有可能消息重复消费,那么对数据准确性会造成极其轻微的影响,在大数据领域中以及日志采集中,这点轻微影响可以忽略这个特性天然适合大数据实时计算以及日志收集

3. 使用

3.1 rocketmq使用

3.1.1 环境要求
  • JDK 1.8+

  • Maven(

  • Linux/Unix 环境(推荐)或 Windows(开发测试)

3.1.2. 下载与安装
# 下载 RocketMQ
wget https://archive.apache.org/dist/rocketmq/4.9.4/rocketmq-all-4.9.4-bin-release.zip
unzip rocketmq-all-4.9.4-bin-release.zip
cd rocketmq-4.9.4
3.1.3. 启动 NameServer
# 启动 NameServer
nohup sh bin/mqnamesrv &
# 查看日志
tail -f ~/logs/rocketmqlogs/namesrv.log
3.1.4. 启动 Broker
# 修改 Broker 配置(可选)
vi conf/broker.conf
# 添加配置(例如设置 NameServer 地址)
namesrvAddr=localhost:9876

# 启动 Broker
nohup sh bin/mqbroker -n localhost:9876 &
# 查看日志
tail -f ~/logs/rocketmqlogs/broker.log
3.1.5. 验证服务状态
# 查看 Broker 状态
sh bin/mqadmin clusterList -n localhost:9876
3.1.6. 集群部署(高可用)
  • 多 NameServer:部署多个 NameServer 实例,Broker 配置所有 NameServer 地址。

  • Broker 主从:通过配置文件设置 brokerRole(SYNC_MASTER/ SLAVE)和 brokerId(0 表示 Master,非 0 表示 Slave)。


3.1.7. Java 客户端使用
1. 添加 Maven 依赖

    org.apache.rocketmq
    rocketmq-client
    4.9.4
2. 生产者示例
public class ProducerDemo {
    public static void main(String[] args) throws Exception {
        // 1. 创建生产者实例
        DefaultMQProducer producer = new DefaultMQProducer("producer_group");
        // 2. 设置 NameServer 地址
        producer.setNamesrvAddr("localhost:9876");
        // 3. 启动生产者
        producer.start();

        // 4. 创建消息
        Message msg = new Message("test_topic", "tagA", "Hello RocketMQ".getBytes());
        
        // 5. 发送消息(同步方式)
        SendResult result = producer.send(msg);
        System.out.println("消息发送结果:" + result);
        
        // 6. 关闭生产者
        producer.shutdown();
    }
}
3. 消费者示例
public class ConsumerDemo {
    public static void main(String[] args) throws Exception {
        // 1. 创建消费者实例
        DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("consumer_group");
        // 2. 设置 NameServer 地址
        consumer.setNamesrvAddr("localhost:9876");
        // 3. 订阅 Topic 和 Tag(* 表示所有 Tag)
        consumer.subscribe("test_topic", "*");
        
        // 4. 注册消息监听器
        consumer.registerMessageListener((MessageListenerConcurrently) (msgs, context) -> {
            for (MessageExt msg : msgs) {
                System.out.println("收到消息: " + new String(msg.getBody()));
            }
            return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
        });
        
        // 5. 启动消费者
        consumer.start();
        System.out.println("消费者已启动");
    }
}
4. 关键配置参数
  • 生产者

    • setSendMsgTimeout(int timeout):发送超时时间。

    • setRetryTimesWhenSendFailed(int retryTimes):失败重试次数。

  • 消费者

    • setConsumeThreadMin(int min):最小消费线程数。

    • setConsumeFromWhere(ConsumeFromWhere consumeFromWhere):消费起始位置(如 CONSUME_FROM_LAST_OFFSET)。

3.2 kafka使用

3.2.1 环境准备
  • Java:Kafka 需要 Java 环境,建议使用 JDK 8 或更高版本。

  • Zookeeper:Kafka 依赖 Zookeeper 进行集群管理。

3.2.2 下载 Kafka

从 Apache Kafka 官网 下载最新版本的 Kafka。

wget https://downloads.apache.org/kafka/3.6.0/kafka_2.13-3.6.0.tgz
tar -xzf kafka_2.13-3.6.0.tgz
cd kafka_2.13-3.6.0
3.2..3 启动 Zookeeper

Kafka 依赖 Zookeeper,首先需要启动 Zookeeper。

bin/zookeeper-server-start.sh config/zookeeper.properties
3.2..4 启动 Kafka Broker

启动 Kafka Broker。

bin/kafka-server-start.sh config/server.properties
3.2..5 创建 Topic

创建一个名为 test-topic 的 Topic。

bin/kafka-topics.sh --create --topic test-topic --bootstrap-server localhost:9092 --partitions 1 --replication-factor 1
3.2.6 生产者和消费者测试

启动一个生产者并向 test-topic 发送消息。

bin/kafka-console-producer.sh --topic test-topic --bootstrap-server localhost:9092

启动一个消费者并消费 test-topic 中的消息。

ka-console-consumer.sh --topic test-topic --bootstrap-server localhost:9092 --from-beginning
3.2.7. Java 客户端使用
1 添加依赖

在 Maven 项目中添加 Kafka 客户端依赖。


    org.apache.kafka
    kafka-clients
    3.6.0
2 生产者示例

以下是一个简单的 Kafka 生产者示例。

import org.apache.kafka.clients.producer.*;

import java.util.Properties;

public class KafkaProducerExample {
    public static void main(String[] args) {
        Properties props = new Properties();
        props.put("bootstrap.servers", "localhost:9092");
        props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
        props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

        Producer producer = new KafkaProducer<>(props);

        for (int i = 0; i < 10; i++) {
            ProducerRecord record = new ProducerRecord<>("test-topic", "key-" + i, "value-" + i);
            producer.send(record, (metadata, exception) -> {
                if (exception == null) {
                    System.out.println("Message sent to topic " + metadata.topic() + " partition " + metadata.partition() + " offset " + metadata.offset());
                } else {
                    exception.printStackTrace();
                }
            });
        }

        producer.close();
    }
}
3 消费者示例

以下是一个简单的 Kafka 消费者示例。

import org.apache.kafka.clients.consumer.*;
import org.apache.kafka.common.TopicPartition;

import java.time.Duration;
import java.util.Collections;
import java.util.Properties;

public class KafkaConsumerExample {
    public static void main(String[] args) {
        Properties props = new Properties();
        props.put("bootstrap.servers", "localhost:9092");
        props.put("group.id", "test-group");
        props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
        props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");

        Consumer consumer = new KafkaConsumer<>(props);
        consumer.subscribe(Collections.singletonList("test-topic"));

        while (true) {
            ConsumerRecords records = consumer.poll(Duration.ofMillis(100));
            for (ConsumerRecord record : records) {
                System.out.printf("Consumed message with key %s and value %s%n", record.key(), record.value());
            }
        }
    }
}

4. 问题

4.1 消息会堆积吗?什么时候清理过期的消息?

如果消息出现堆积可能有三种情况:

    1.消息的生产者生产消息过快。

    2.broker发生了阻塞。

    3.消息的消费者消费消息过慢。

   可以设置过期时间,不设置不会过期.过期数据根据策略进行删除,不在被消费

  • RocketMQ 默认保留消息并持续存储,不会自动清理过期消息,需要手动进行清理。(4.5.0的版本也可以设置过期)
  • Kafka 保留所有消息,但可以设置消息的过期时间,过期的消息会在日志压缩和段删除过程中被清理。

4.2 消息会重复消费吗?

  1. 络异常和延迟:在分布式环境下,网络可能出现异常或延迟,导致消息在传输过程中发生重试或重复传递。
  2. 生产者重试机制:当生产者发送消息时,如果没有及时收到确认响应,可能会触发重试机制,导致消息被重复发送。
  3. 消费者应答超时:消费者在处理消息时,如果由于某种原因没有及时向Broker发送消费确认(ack),Broker会认为该消息处理失败,并重新将其投递给其他消费者进行处理。
  4. 消费者崩溃与重启:如果消费者在处理消息期间发生崩溃或重启,可能会导致其消费进度丢失或回滚到之前的状态,从而导致消息被再次消费。
  5. 消息重复发送:在某些情况下,生产者可能会重复发送相同的消息,例如由于业务逻辑错误、重试策略设置不当等。

解决: 

  1. 息去重:生产者可以在发送消息时设置唯一标识码(MessageKey),Broker会根据这个标识码进行去重判断。如果相同的MessageKey的消息已经被消费过,则会被Broker判定为重复消息,不会再次投递给消费者。
  2. 消费端幂等性处理:消费者在处理消息时,需要保证其逻辑具备幂等性。即无论收到多少次重复消息,最终的处理结果都是一致的。通过幂等性处理,即使重复消费了同一条消息,也能确保最终的业务状态正确。
  3. 消费进度保存与容错:RocketMQ会记录每个消费者组的消费进度,保存在Broker端。当消费者处理完一条消息后,会将消费进度更新到Broker上。如果由于某种原因导致消费者中断或重新启动,RocketMQ能够自动恢复消费进度,避免重复消费。
  4. 消耗模型选择:在RocketMQ中,可以选择Push模式或Pull模式进行消息消费。Pull模式下,消费者能够主动控制消息的拉取和处理,可以更灵活地处理消息重复消费的情况

4.3 RockerMQ如何保证消息不丢失?

  • producer端:可以采取send() 同步发消息,发送结果时同步感知的,发送失败后可以重试,设置重试次数,默认3次。
  • broker端:可以将刷盘策略改为同步刷盘,默认情况下时异步刷盘。
  • consumer端:完全消费正常后在进行手动ack确认。

4.4 .RocketMQ消息堆积如何处理?

    首先要找到消息堆积的原因,是producer太多,还是consumer太少,还是消息消费速度异常,正常的话,可以上线更多的consumer临时解决消息堆积问题。

4.5 如何让RockerMQ的消息保证顺序消费?

queue是典型的FIFO,天然顺序,多个queue同时消费时无法绝对保证消息的有序性,所以同一个topic,同一个queue 发消息的时候,一个线程去发送消息,消费的时候一个线程去消费一个queue里的消息。

你可能感兴趣的:(微服务,java,架构)