在nodejs中使用RabbitMQ(四)队列类型Classic、Quorum、Stream

经典队列(Classic Queues)、仲裁队列(Quorum Queues)和流队列(Stream Queues)——的特性对比表:

Feature Classic Queues Quorum Queues Stream Queues
Non-durable queues (非持久队列) Yes No No
Message replication (消息复制) No Yes No
Exclusivity (独占性) Yes No No
Per message persistence (每条消息持久化) Per message Always Always
Membership changes (成员变更) No Semi-automatic Manual
Message TTL (Time-To-Live) (消息TTL) Yes Yes Yes
Queue TTL (队列TTL) Yes Partially No
Queue length limits (队列长度限制) Yes Yes No
Keeps messages in memory (内存中保存消息) See Classic Queues Never Never
Message priority (消息优先级) Yes Yes No
Single Active Consumer (单一活跃消费者) Yes Yes Yes
Consumer exclusivity (消费者独占性) Yes No No
Consumer priority (消费者优先级) Yes Yes No
Dead letter exchanges (死信交换机) Yes Yes No
Adheres to policies (遵守策略) Yes Yes Yes
Poison message handling (毒药消息处理) No Yes No
Server-named queues (服务器命名队列) Yes No No

经典队列(Classic Queues)默认类型

1.单节点存储:Classic Queue 将消息存储在单一的队列节点上,这意味着消息会集中在一个节点的内存或磁盘上。虽然它支持持久化,但队列本身是不可分布的。

2.低延迟:对于低负载和小规模的应用,Classic Queue 提供了较低的延迟和较快的消息传输。

3.简单易用:经典队列适用于简单的应用场景,易于设置和管理。

4.性能瓶颈:当消息量非常大时,Classic Queue 可能会成为性能瓶颈,因为队列数据只存在于单个节点上,无法在集群中分布。

5.容错性差:在集群环境下,Classic Queue 不能充分利用集群的高可用性,队列和消息只能存在于单个节点上。如果该节点失败,消息可能会丢失,除非启用了持久化。

6.不适合集群场景:由于它不支持分布式存储,Classic Queue 不适合高并发、跨节点分布的集群场景。

7.适用场景:小规模应用,低并发场景。对性能要求较高,但负载较低的场景。

 

仲裁队列(Quorum Queue)

1.高可用性:Stream Queue 是 RabbitMQ 3.8 引入的新队列类型,Quorum Queue 使用 Raft 协议 来保证队列的高可用性和数据一致性。队列的数据会在集群中的多个节点上进行复制,确保即使某个节点故障,消息也能保留。

2.分布式存储:消息数据会被分布到多个节点,防止单节点成为瓶颈。

3.容错性强:支持故障转移,确保高可用性和高容错性。

4.性能开销:由于使用了 Raft 协议和数据复制,Quorum Queue 在高负载的情况下相对于 Classic Queue 可能会有更高的延迟和资源消耗。

5.延迟:消息在多个节点间复制可能会引入一些延迟,特别是在集群节点较多时。

6.适用场景:高可用性和高容错性要求高的分布式应用。集群环境中的队列,尤其是需要保证消息不丢失的场景。需要高可靠性和一致性保障的场景。

示例

producer.ts

import RabbitMQ from 'amqplib/callback_api';


function start() {
  RabbitMQ.connect("amqp://admin:admin1234@localhost:5672?heartbeat=60", function (err0, conn) {
    if (err0) {
      console.error("[AMQP]", err0.message);
      return setTimeout(start, 1000);
    }

    conn.on("error", function (err1) {
      if (err1.message !== "Connection closing") {
        console.error("[AMQP] conn error", err1.message);
      }
    });

    conn.on("close", function () {
      console.error("[AMQP] reconnecting");
      return setTimeout(start, 1000);
    });

    console.log("[AMQP] connected");

    conn.createChannel(async (err2, channel) => {
      if (err2) {
        console.error("[AMQP]", err2.message);
        return setTimeout(start, 1000);
      }

      const exchangeName = 'exchange6_quorum';
      channel.assertExchange(
        exchangeName,
        'direct',
        {
          durable: true
        },
        (err, ok) => {
          if (err) {
            console.log('exchange路由转发创建失败', err);
          } else {
            let args = ['route.key'];
            for (let i = 0; i < 20; ++i) {
              // console.log('message send!', channel.sendToQueue(
              //   queueName,
              //   Buffer.from(`发送消息,${i}${Math.ceil(Math.random() * 100000)}`),
              //   { persistent: true, correlationId: 'ooooooooooooooo' },// 消息持久化,重启后存在
              //   // (err: any, ok: Replies.Empty)=>{}
              // ));
              
              const routeKey = args[Math.floor(Math.random() * args.length)];
              console.log('消息发送是否成功', routeKey, channel.publish(
                exchangeName,
                routeKey,
                Buffer.from(`发送消息,${i}${Math.ceil(Math.random() * 100000)},${JSON.stringify(routeKey)}`),
                {
                  persistent: true,
                },
              ));
            }
          }
        }
      );
    });

    setTimeout(() => {
      conn.close();
      process.exit(0);
    }, 1000);
  });
}

start();

consumer.ts

import RabbitMQ, { type Replies } from 'amqplib/callback_api';


RabbitMQ.connect('amqp://admin:admin1234@localhost:5672', (err0, conn) => {
  if (err0) {
    console.error(err0);
    return;
  }

  conn.createChannel(function (err1, channel) {
    const queueName = 'queue4';

    channel.assertQueue(
      queueName,
      {
        durable: true,
        arguments: {
          'x-queue-type': 'quorum', // 队列类型quorum
          // 'x-expires': 60000, // 队列过期时间,60 秒没有被消费者或生产者使用会被删除
          'x-message-ttl':60000, // 消息存活 60 秒未被消费,则被删除或进入死信队列
          'x-delivery-limit': 3, // 消息最多可被传递 3 次,超出则进入死信队列或丢弃
        },
      },
    );

    console.log('[*] waiting...');

    // 一次只有一个未确认消息,防止消费者过载
    channel.prefetch(1);

    channel.bindQueue(
      queueName,
      'exchange6_quorum',
      'route.key',
      {},
      (err, ok) => {
        console.log(queueName, '队列绑定结果', err, ok);
      },
    );

    channel.consume(queueName, function (msg) {
      console.log('接收到的消息', msg?.content.toString());

      // 手动确认取消channel.ack(msg);设置noAck:false,
      // 自动确认消息noAck:true,不需要channel.ack(msg);
      try {
        if (msg) {
          channel.ack(msg);
        }
      } catch (err) {
        if (msg) {
          // 第二个参数,false拒绝当前消息
          // 第二个参数,true拒绝小于等于当前消息
          // 第三个参数,3false从队列中清除
          // 第三个参数,4true从新在队列中排队
          channel.nack(msg, false, false);
        }
        console.log(err);
      }
    }, {
      // noAck: true, // 是否自动确认消息,为true不需要调用channel.ack(msg);
      noAck: false
    }, (err: any, ok: Replies.Empty) => {
      console.log(err, ok);
    });

    // return,error事件不会把消息重新放回队列
    channel.on('return', (msg) => {
      console.error('消息发送失败:', msg);
    });

    channel.on('error', (err) => {
      console.error('通道发生错误:', err);
    });
  });

  conn.on("error", function (err1) {
    if (err1.message !== "Connection closing") {
      console.error("[AMQP] conn error", err1.message);
    }
  });

  conn.on("close", function () {
    console.error("[AMQP] reconnecting");
  });
});

consumer2.ts

import RabbitMQ, { type Replies } from 'amqplib/callback_api';


RabbitMQ.connect('amqp://admin:admin1234@localhost:5672', (err0, conn) => {
  if (err0) {
    console.error(err0);
    return;
  }

  conn.createChannel(function (err1, channel) {
    const queueName = 'queue4';

    channel.assertQueue(
      queueName,
      {
        durable: true,
        arguments: {
          'x-queue-type': 'quorum',
          
          // 队列过期时间,60 秒没有被消费者或生产者使用会被删除
          // 'x-expires': 60000, 
          
          // 消息存活 60 秒未被消费,则被删除或进入死信队列
          'x-message-ttl': 60000, 
          
          // 消息最多可被传递 3 次,超出则进入死信队列或丢弃
          'x-delivery-limit': 3, 

          // 挤压策略:当队列达到最大长度时,如何处理新消息
          // 'drop-head':丢弃队首(最旧)的消息;'drop-tail':丢弃队尾(最新)的消息;'reject-publish':拒绝新消息发布
          'x-overflow': 'drop-head',

          // 队列最大字节数:控制队列可以存储的总字节数,超过这个大小时会触发消息挤压
          // 最大消息字节数:20GB,队列中消息总字节数超过 20GB 时会进行挤压
          'x-max-length-bytes': 20_000_000_000,
        },
      },
    );

    console.log('[*] waiting...');

    // 一次只有一个未确认消息,防止消费者过载
    channel.prefetch(1);

    channel.bindQueue(
      queueName,
      'exchange6_quorum',
      'route.key',
      {},
      (err, ok) => {
        console.log(queueName, '队列绑定结果', err, ok);
      },
    );

    channel.consume(queueName, function (msg) {
      console.log('接收到的消息', msg?.content.toString());

      // 手动确认取消channel.ack(msg);设置noAck:false,
      // 自动确认消息noAck:true,不需要channel.ack(msg);
      try {
        if (msg) {
          channel.ack(msg);
        }
      } catch (err) {
        if (msg) {
          // 第二个参数,false拒绝当前消息
          // 第二个参数,true拒绝小于等于当前消息
          // 第三个参数,3false从队列中清除
          // 第三个参数,4true从新在队列中排队
          channel.nack(msg, false, false);
        }
        console.log(err);
      }
    }, {
      // noAck: true, // 是否自动确认消息,为true不需要调用channel.ack(msg);
      noAck: false
    }, (err: any, ok: Replies.Empty) => {
      console.log(err, ok);
    });

    // return,error事件不会把消息重新放回队列
    channel.on('return', (msg) => {
      console.error('消息发送失败:', msg);
    });

    channel.on('error', (err) => {
      console.error('通道发生错误:', err);
    });
  });

  conn.on("error", function (err1) {
    if (err1.message !== "Connection closing") {
      console.error("[AMQP] conn error", err1.message);
    }
  });

  conn.on("close", function () {
    console.error("[AMQP] reconnecting");
  });
});

 

当使用quorum类型队列创建多节点集群时,集群节点和队列在运行时发生变化,如对队列进行了扩展,或缩减。或者在运行时负载不均衡,需要手动重新平衡,因为多节点的队列共用相同的队列数据,需要对数据进行同步,参阅管理副本

# rebalances all quorum queues
rabbitmq-queues rebalance quorum

可以重新平衡按名称选择的队列子集:

# rebalances a subset of quorum queues
rabbitmq-queues rebalance quorum --queue-pattern "orders.*"

或特定虚拟主机集中的仲裁队列:

# rebalances a subset of quorum queues
rabbitmq-queues rebalance quorum --vhost-pattern "production.*"

修改队列:

#增加现有队列的副本到指定的节点。
rabbitmq-queues add_member [-p ]  

#删除某个队列的副本。
rabbitmq-queues delete_member [-p ]  

#增加队列副本到指定节点(并不是增加节点本身)。
rabbitmq-queues grow   [--vhost-pattern ] [--queue-pattern ]

#减少某个节点上的队列副本(并不是减少节点本身)。
rabbitmq-queues shrink  [--errors-only]

流队列(Stream Queue)

1.为大规模、高吞吐量设计:Stream Queue 是 RabbitMQ 3.9 引入的新队列类型,旨在支持大规模的消息流处理。它适用于需要高吞吐量和大量消息的应用。

2.按时间顺序消费消息:与其他队列类型不同,Stream Queue 具有顺序消费的特性,消息会按照时间戳顺序消费。

3.基于磁盘存储:消息会存储在磁盘上,具有较好的可扩展性和耐久性。消息不会立即从内存中删除,可以在磁盘中保持更长的时间,数据交由操作系统控制保存到磁盘,rabbitmq不会直接立刻保存数据到磁盘,突然断电,服务器崩溃可能会导致数据丢失。

4.高效的存储和消费:Stream Queue 适用于需要长期存储、按顺序消费的应用场景,并且它能处理大量消息,具有更高的存储效率。

5.性能开销:由于设计初衷是为了高吞吐量,Stream Queue 对资源的需求较高。尤其在内存和磁盘 I/O 的消耗上。

6.复杂性:与 Classic 和 Quorum 队列相比,Stream Queue 的使用可能需要更多的配置和管理,特别是对于消息保留和消费策略的设置。

7.使用场景:大规模消息流处理,尤其是需要持久化消息并按照时间顺序消费的场景。相比于其它类型stream类型可以挤压更多的消息。需要高吞吐量和持久化的应用,如日志收集、监控数据流、金融交易系统等。消息处理顺序和历史消息存储非常重要的场景。

 示例

producer.ts

import RabbitMQ from 'amqplib/callback_api';


function start() {
  RabbitMQ.connect("amqp://admin:admin1234@localhost:5672?heartbeat=60", function (err0, conn) {
    if (err0) {
      console.error("[AMQP]", err0.message);
      return setTimeout(start, 1000);
    }

    conn.on("error", function (err1) {
      if (err1.message !== "Connection closing") {
        console.error("[AMQP] conn error", err1.message);
      }
    });

    conn.on("close", function () {
      console.error("[AMQP] reconnecting");
      return setTimeout(start, 1000);
    });

    console.log("[AMQP] connected");

    conn.createChannel(async (err2, channel) => {
      if (err2) {
        console.error("[AMQP]", err2.message);
        return setTimeout(start, 1000);
      }

      const exchangeName = 'exchange7_stream';
      channel.assertExchange(
        exchangeName,
        'direct',
        {
          durable: true
        },
        (err, ok) => {
          if (err) {
            console.log('exchange路由转发创建失败', err);
          } else {
            let args = ['route.key'];
            for (let i = 0; i < 20; ++i) {
              // console.log('message send!', channel.sendToQueue(
              //   queueName,
              //   Buffer.from(`发送消息,${i}${Math.ceil(Math.random() * 100000)}`),
              //   { persistent: true, correlationId: 'ooooooooooooooo' },// 消息持久化,重启后存在
              //   // (err: any, ok: Replies.Empty)=>{}
              // ));
              
              const routeKey = args[Math.floor(Math.random() * args.length)];
              console.log('消息发送是否成功', routeKey, channel.publish(
                exchangeName,
                routeKey,
                Buffer.from(`发送消息,${i}${Math.ceil(Math.random() * 100000)},${JSON.stringify(routeKey)}`),
                {
                  persistent: true,
                },
              ));
            }
          }
        }
      );
    });

    setTimeout(() => {
      conn.close();
      process.exit(0);
    }, 1000);
  });
}

start();

consumer.ts

import RabbitMQ, { type Replies } from 'amqplib/callback_api';


RabbitMQ.connect('amqp://admin:admin1234@localhost:5672', (err0, conn) => {
  if (err0) {
    console.error(err0);
    return;
  }

  conn.createChannel(function (err1, channel) {
    const queueName = 'queue5';

    channel.assertQueue(queueName, {
      durable: true,  // 队列是否持久化,确保队列在 RabbitMQ 重启后仍然存在
      arguments: {
        // 指定队列类型为 Stream 类型
        // 设置队列为 Stream 类型,用于处理流式数据
        'x-queue-type': 'stream',

        // 队列能够存储最多 20GB 的消息。当队列的消息总字节数超过 20GB 时,最旧的消息会被丢弃。
        'x-max-length-bytes': 20_000_000_000,

        // 每个消息段的最大字节数:Stream 队列中的每个消息可能被分割成多个段,定义了每个段的最大大小
        // 每个消息段最大 100MB,限制了单个消息段的最大大小
        'x-stream-max-segment-size-bytes': 100_000_000,

        // 流段的最小筛选器大小:这是消息在流中的存储分段策略。控制消息在流中的存储方式和分段的粒度
        // 每个流段最小筛选器大小为 32 字节,影响消息的存储和分段方式
        'x-stream-filter-size-bytes': 32,
      },
    });

    console.log('[*] waiting...');

    // 一次只有一个未确认消息,防止消费者过载
    channel.prefetch(1);

    channel.bindQueue(
      queueName,
      'exchange7_stream',
      'route.key',
      {

      },
      (err, ok) => {
        console.log(queueName, '队列绑定结果', err, ok);
      },
    );

    channel.consume(queueName, function (msg) {
      console.log('接收到的消息', msg?.content.toString());

      // 手动确认取消channel.ack(msg);设置noAck:false,
      // 自动确认消息noAck:true,不需要channel.ack(msg);
      try {
        if (msg) {
          channel.ack(msg);
        }
      } catch (err) {
        if (msg) {
          // 第二个参数,false拒绝当前消息
          // 第二个参数,true拒绝小于等于当前消息
          // 第三个参数,3false从队列中清除
          // 第三个参数,4true从新在队列中排队
          channel.nack(msg, false, false);
        }
        console.log(err);
      }
    }, {
      // noAck: true, // 是否自动确认消息,为true不需要调用channel.ack(msg);
      noAck: false,
      arguments: {
        // offset 设置消费起始位置,可以选择以下几种方式:

        // 'last': 从流的最后一个消息开始消费,即从最新的消息开始(默认)。
        // 适用于需要只处理最新消息的场景,跳过已经处理过的消息。

        // 'first': 从流的第一个消息开始消费,即从队列头开始按顺序消费所有消息。
        // 适用于需要处理所有历史消息的场景。

        // 'next': 从队列的队头开始,跳过已经读取的消息,继续从未读取过的消息开始消费。
        // 适用于只消费未处理过的消息,适合断点恢复。

        // 'number': 从队列的队头开始,消费第几条消息(偏移量),例如 'number' 设置为 10 表示从第 10 条消息开始消费。
        // 适用于定点消费的场景,可以精确控制从哪一条消息开始消费。

        // 'timestamp': 从某个时间戳之后的消息开始消费。 
        // 适用于基于时间点的消费需求,例如从某个具体的时间开始读取流中的消息。'x-stream-offset': 'last',  
        'x-stream-offset': 'last'
      }
    }, (err: any, ok: Replies.Empty) => {
      console.log(err, ok);
    });

    // return,error事件不会把消息重新放回队列
    channel.on('return', (msg) => {
      console.error('消息发送失败:', msg);
    });

    channel.on('error', (err) => {
      console.error('通道发生错误:', err);
    });
  });

  conn.on("error", function (err1) {
    if (err1.message !== "Connection closing") {
      console.error("[AMQP] conn error", err1.message);
    }
  });

  conn.on("close", function () {
    console.error("[AMQP] reconnecting");
  });
});

 使用stream类型队列搭建集群时,如果集群节点和队列在运行时发生变化,也需要手动进行调整。

# 新增节点并扩展副本
rabbitmq-streams add_replica [-p ]  

# 节点下线前删除副本
rabbitmq-streams delete_replica [-p ]  

# 查看 Stream 状态
rabbitmq-streams stream_status [-p ] 

# 重启 Stream
rabbitmq-streams restart_stream [-p ] 

你可能感兴趣的:(nodejs,rabbitmq,node.js,后端)