经典队列(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 |
1.单节点存储:Classic Queue 将消息存储在单一的队列节点上,这意味着消息会集中在一个节点的内存或磁盘上。虽然它支持持久化,但队列本身是不可分布的。
2.低延迟:对于低负载和小规模的应用,Classic Queue 提供了较低的延迟和较快的消息传输。
3.简单易用:经典队列适用于简单的应用场景,易于设置和管理。
4.性能瓶颈:当消息量非常大时,Classic Queue 可能会成为性能瓶颈,因为队列数据只存在于单个节点上,无法在集群中分布。
5.容错性差:在集群环境下,Classic Queue 不能充分利用集群的高可用性,队列和消息只能存在于单个节点上。如果该节点失败,消息可能会丢失,除非启用了持久化。
6.不适合集群场景:由于它不支持分布式存储,Classic Queue 不适合高并发、跨节点分布的集群场景。
7.适用场景:小规模应用,低并发场景。对性能要求较高,但负载较低的场景。
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]
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 ]