使用RabbitMq原生實現延遲佇列

百年俊少發表於2024-11-19

使用RabbitMQ的TTL(Time-To-Live)特性和死信交換機(Dead Letter Exchange, DLX)來實現延遲投遞是一種間接的方法。在這個方法中,訊息首先被髮送到一個具有TTL設定的佇列。當訊息在佇列中存活的時間超過TTL時,它會被丟棄到配置的死信交換機,然後你可以在該交換機上配置路由規則將訊息轉發到目標佇列

發訊息的程式碼:

var factory = new ConnectionFactory
{
    HostName = RabbitMqAccount.Host,
    Port = RabbitMqAccount.Port,
    UserName = RabbitMqAccount.UserName,
    Password = RabbitMqAccount.Password
};
var connection = factory.CreateConnection();    // 建立連線物件
var channel = connection.CreateModel();

// 宣告正常交換機 要先啟服務端,再啟客戶端
channel.ExchangeDeclare(ExchangeNameStr.normalExchange, "direct");
foreach (var item in users)
{    
    string msg = JsonConvert.SerializeObject(item);
    Random rng = new Random(Guid.NewGuid().GetHashCode());
    int time = rng.Next(1 * 60, 60 * 60) * 1000;    
    SendDelayedMessage(channel, msg,time);
}

void SendDelayedMessage(IModel channel, string message, int delayMilliseconds)
{
    var properties = channel.CreateBasicProperties();
    properties.Expiration = delayMilliseconds.ToString();

    // 傳送訊息到正常佇列
    channel.BasicPublish(ExchangeNameStr.normalExchange, ExchangeNameStr.routingKey, false, properties, Encoding.UTF8.GetBytes(message));
    logComm.Debug($"[x] Sent {message} with delay:" + properties.Expiration);
}

消費者的程式碼:

var factory = new ConnectionFactory
{
    HostName = RabbitMqAccount.Host,
    Port = RabbitMqAccount.Port,
    UserName = RabbitMqAccount.UserName,
    Password = RabbitMqAccount.Password
};
var connection = factory.CreateConnection();    // 建立連線物件
var channel = connection.CreateModel();

// 宣告死信交換機
channel.ExchangeDeclare(ExchangeNameStr.dlxExchange, "direct");
// 宣告死信佇列
var dlxArgs = new Dictionary<string, object>
{
    {"x-dead-letter-exchange", ""}, // 訊息從死信佇列消費後不再進入其它佇列
};
channel.QueueDeclare(ExchangeNameStr.dlxQueue, true, false, false, dlxArgs);
channel.QueueBind(ExchangeNameStr.dlxQueue, ExchangeNameStr.dlxExchange, ExchangeNameStr.routingKey);

// 宣告正常交換機
channel.ExchangeDeclare(ExchangeNameStr.normalExchange, "direct");
// 宣告正常佇列
var normalArgs = new Dictionary<string, object>
{
    {"x-dead-letter-exchange", ExchangeNameStr.dlxExchange} // 死信交換機
};
channel.QueueDeclare(ExchangeNameStr.normalQueue, true, false, false, normalArgs);
channel.QueueBind(ExchangeNameStr.normalQueue, ExchangeNameStr.normalExchange, ExchangeNameStr.routingKey);

ConsumeDelayedMessages(channel);
void ConsumeDelayedMessages(IModel channel)
{
    var consumer = new EventingBasicConsumer(channel);
    consumer.Received += async (model, ea) =>
    {
        var body = ea.Body.ToArray();
        var message = Encoding.UTF8.GetString(body);
        User user = JsonConvert.DeserializeObject<User>(message);

        if (user != null)
        {
            logComm.Debug($"[x] Received {user.ToSelfString()} from DLX.");           
        }
    };
    channel.BasicConsume(ExchangeNameStr.dlxQueue, true, consumer);
}

正常互動機裡的訊息不消費,過期後進入死信交換機,消費端只消費死信交換機隊裡裡的訊息。

public class ExchangeNameStr
{
    public static readonly string normalExchange = "SJSD-DelayedExchange";
    public static readonly string normalQueue = "SJSD_Delayed_Queue";
    public static readonly string dlxExchange = "SJSD-DLXExchange";
    public static readonly string dlxQueue = "SJSD_Dlx_Queue";
    public static readonly string routingKey = "ActivityData";
}
/// <summary>
/// RabbitMq的地址和賬號
/// </summary>
public class RabbitMqAccount
{
    public static readonly string Host = "127.0.0.1";
    public static readonly int Port = 5672;
    public static readonly string UserName = "UserName";
    public static readonly string Password = "Password";
}

相關文章