用 RabbitMQ 的死信佇列來做定時任務

牛牛_sean發表於2018-10-06

在開發中做定時任務是一個非常常見的業務場景,在程式碼層面 Node.js 可以用 setTimeout、setInerval 這種基礎語法或用 node-schedule 這些類似的庫來達到部分目的,在第三方服務上可以用 Redis 的 Keyspace Notification 或 Linux 自身的 crontab 來做定時任務。RabbitMQ 作為一個訊息中介軟體,使用其死信佇列也可以達到做定時任務的目的。

用 RabbitMQ 的死信佇列來做定時任務

本文以 Node.js 作為演示語言,操作 RabbitMQ 使用的是 amqplib

死信佇列

RabbitMQ 中有一種交換器叫 DLX,全稱為 Dead-Letter-Exchange,可以稱之為死信交換器。當訊息在一個佇列中變成死信(dead message)之後,它會被重新傳送到另外一個交換器中,這個交換器就是 DLX,繫結在 DLX 上的佇列就稱之為死信佇列。 訊息變成死信一般是以下幾種情況:

  • 訊息被拒絕,並且設定 requeue 引數為 false
  • 訊息過期
  • 佇列達到最大長度

DLX 也是一個正常的交換器,和一般的交換器沒有區別,它能在任何佇列上被指定,實際上就是設定某個佇列的屬性。當這個佇列存在死信時,RabbitMQ 就會自動地將這個訊息重新發布到設定的 DLX 上去,進而被路由到另一個佇列,即死信佇列。要為某個佇列新增 DLX,需要在建立這個佇列的時候設定其deadLetterExchangedeadLetterRoutingKey 引數,deadLetterRoutingKey 引數可選,表示為 DLX 指定的路由鍵,如果沒有特殊指定,則使用原佇列的路由鍵。

const amqp = require('amqplib');

const myNormalEx = 'my_normal_exchange';
const myNormalQueue = 'my_normal_queue';
const myDeadLetterEx = 'my_dead_letter_exchange';
const myDeadLetterRoutingKey = 'my_dead_letter_routing_key';
let connection, channel;
amqp.connect('amqp://localhost')
  .then((conn) => {
    connection = conn;
    return conn.createChannel();
  })
  .then((ch) => {
    channel = ch;
    ch.assertExchange(myNormalEx, 'direct', { durable: false });
    return ch.assertQueue(myNormalQueue, {
      exclusive: false,
      deadLetterExchange: myDeadLetterEx,
      deadLetterRoutingKey: myDeadLetterRoutingKey,
    });
  })
  .then((ok) => {
    channel.bindQueue(ok.queue, myNormalEx);
    channel.sendToQueue(ok.queue, Buffer.from('hello'));
    setTimeout(function () { connection.close(); process.exit(0) }, 500);
  })
  .catch(console.error);

複製程式碼

上面的程式碼先宣告瞭一個交換器 myNormalEx, 然後宣告瞭一個佇列 myNormalQueue,在宣告該佇列的時候通過設定其 deadLetterExchange 引數,為其新增了一個 DLX。所以當佇列 myNormalQueue 中有訊息成為死信後就會被髮布到 myDeadLetterEx 中去。

過期時間(TTL)

在 RabbbitMQ 中,可以對訊息和佇列設定過期時間。當通過佇列屬性設定過期時間時,佇列中所有訊息都有相同的過期時間。當對訊息設定單獨的過期時間時,每條訊息的 TTL 可以不同。如果兩種方法一起使用,則訊息的 TTL 以兩者之間較小的那個數值為準。訊息在佇列中的生存時間一旦超過設定的 TTL 值時,就會變成“死信”(Dead Message),消費者將無法再接收到該訊息。

針對每條訊息設定 TTL 是在傳送訊息的時候設定 expiration 引數,單位為毫秒。

const amqp = require('amqplib');

const myNormalEx = 'my_normal_exchange';
const myNormalQueue = 'my_normal_queue';
const myDeadLetterEx = 'my_dead_letter_exchange';
const myDeadLetterRoutingKey = 'my_dead_letter_routing_key';
let connection, channel;
amqp.connect('amqp://localhost')
  .then((conn) => {
    connection = conn;
    return conn.createChannel();
  })
  .then((ch) => {
    channel = ch;
    ch.assertExchange(myNormalEx, 'direct', { durable: false });
    return ch.assertQueue(myNormalQueue, {
      exclusive: false,
      deadLetterExchange: myDeadLetterEx,
      deadLetterRoutingKey: myDeadLetterRoutingKey,
    });
})
  .then((ok) => {
    channel.bindQueue(ok.queue, myNormalEx);
    channel.sendToQueue(ok.queue, Buffer.from('hello'), { expiration: '4000'});
    setTimeout(function () { connection.close(); process.exit(0) }, 500);
  })
  .catch(console.error);

複製程式碼

上面的程式碼在向佇列傳送訊息的時候,通過傳遞 { expiration: '4000'} 將這條訊息的過期時間設為了4秒,對訊息設定4秒鐘過期,這條訊息並不一定就會在4秒鐘後被丟棄或進入死信,只有當這條訊息到達隊首即將被消費時才會判斷其是否過期,若未過期就會被消費者消費,若已過期就會被刪除或者成為死信。

定時任務

因為佇列中的訊息過期後會成為死信,而死信又會被髮布到該訊息所在的佇列的 DLX 上去,所以通過為訊息設定過期時間,然後再消費該訊息所在佇列的 DLX 所繫結的佇列,從而來達到定時處理一個任務的目的。 簡單的講就是當有一個佇列 queue1,其 DLX 為 deadEx1,deadEx1 繫結了一個佇列 deadQueue1,當佇列 queue1 中有一條訊息因過期成為死信時,就會被髮布到 deadEx1 中去,通過消費佇列 deadQueue1 中的訊息,也就相當於消費的是 queue1 中的因過期產生的死信訊息。

用 RabbitMQ 的死信佇列來做定時任務

消費死信佇列的程式碼如下:

const amqp = require('amqplib');

const myDeadLetterEx = 'my_dead_letter_exchange';
const myDeadLetterQueue = 'my_dead_letter_queue';
const myDeadLetterRoutingKey = 'my_dead_letter_routing_key';
let channel;
amqp.connect('amqp://localhost')
.then((conn) => {
  return conn.createChannel();
})
.then((ch) => {
  channel = ch;
  ch.assertExchange(myDeadLetterEx, 'direct', { durable: false });
  return ch.assertQueue(myDeadLetterQueue, { exclusive: false });
})
.then((ok) => {
  channel.bindQueue(ok.queue, myDeadLetterEx, myDeadLetterRoutingKey);
  channel.consume(ok.queue, (msg) => {
    console.log(" [x] %s: '%s'", msg.fields.routingKey, msg.content.toString());
  }, { noAck: true})
})
.catch(console.error);
複製程式碼

這裡需要注意的是,如果宣告的 myDeadLetterEx 是 direct 型別,那麼在為其繫結佇列的時候一定要指定 BindingKey,即這裡的 myDeadLetterRoutingKey,如果不指定 Bindingkey,則需要將 myDeadLetterEx 宣告為 fanout 型別。

相關文章