在生產環境中由於一些不明原因,導致 rabbitmq 重啓,在 RabbitMQ 重啓期間生產者消息投遞失敗,導致消息丟失,需要手動處理和恢復。如何才能進行 RabbitMQ 的消息可靠投遞呢? 特別是在這樣比較極端的情況,RabbitMQ 集羣不可用的時候,無法投遞的消息該如何處理呢?
消息回退(mandatory = true)
確認機制方案
如圖所示生產者將消息發給broker,以下考慮兩個情況
- 交換機不存在或交換機命名寫錯了,即交換機收不到消息如何告訴生產者(.Net版本此種情況不存在,交換機寫錯了會直接報錯)
- RoutingKey寫錯了,隊列收不到消息如何告訴生產者,即消息回退(以下演示這種情況)
代碼架構圖
RabbitmqUntils配置代碼新增方法GetConfirmAdvancedQueue()
/// <summary>
/// 發佈確認高級
/// </summary>
/// <returns></returns>
public static IModel GetConfirmAdvancedQueue()
{
/*
1. 聲身confirm.exchange
2. 聲明confirm.queue
3. 聲明confirm.routingkey
4. 綁定隊列與交換機
*/
var channel = RabbitmqUntils.GetChannel();
channel.ExchangeDeclare(Confirm_Exchange,"direct",false,false,null);
channel.QueueDeclare(Confirm_Queue, false, false, false, null);
channel.QueueBind(Confirm_Queue,Confirm_Exchange,Confirm_Routingkey);
return channel;
}
生產者代碼
在僅開啓了生產者確認機制的情況下,交換機接收到消息後,會直接給消息生產者發送確認消息,如
果發現該消息不可路由,那麼消息會被直接丟棄,此時生產者是不知道消息被丟棄這個事件的。那麼如何
讓無法被路由的消息幫我想辦法處理一下?最起碼通知我一聲,好自己處理。通過設置mandatory參
數可以在當消息傳遞過程中不可達目的地時將消息返回給生產者
using rabbitmq.common;
using RabbitMQ.Client;
using System.Text;
namespace PublishConfirm.Producer
{
public class Program
{
static void Main(string[] args)
{
Console.WriteLine("請輸入要發送的消息:");
string message = Console.ReadLine();
if (string.IsNullOrEmpty(message))
{
while (true)
{
Console.WriteLine("請輸入要發送的消息: {0}", message);
}
}
else
{
using var channel = RabbitmqUntils.GetConfirmAdvancedQueue();
while (true)
{
SendMsg(message, channel);
message = Console.ReadLine();
}
}
Console.ReadKey();
}
public static void SendMsg(string msg,IModel channel)
{
var body = Encoding.UTF8.GetBytes(msg);
channel.ConfirmSelect();// 開啓發布確認
//演示Routingkey配置錯誤
//如果發佈了帶有“mandatory” = true 標誌集的消息,但無法傳遞,代理將其返回給發送客戶端( channel.BasicReturn)。
channel.BasicPublish(RabbitmqUntils.Confirm_Exchange, RabbitmqUntils.Confirm_Routingkey+"111", mandatory:true, null, body);
// 監聽確認的消息
channel.BasicAcks += (sender, e) =>
{
Console.WriteLine($"交換機已收到:{e.DeliveryTag}");
};
//監聽回退的消息
channel.BasicReturn += ((sender, e) =>
{
var body = e.Body.ToArray();
var msg = System.Text.Encoding.UTF8.GetString(body);
Console.WriteLine($"監聽回退的消息{msg};RoutingKey:{e.RoutingKey};退回原因:{e.ReplyText}");
});
}
}
}
測試效果
備份交換機
有了mandatory參數和回退消息,我們獲得了對無法投遞消息的感知能力,有機會在生產者的消息無法被投遞時發現並處理。但有時候,我們並不知道該如何處理這些無法路由的消息,最多打個日誌,然後觸發報警,再來手動處理。而通過日誌來處理這些無法路由的消息是很不優雅的做法,特別是當生產者所在的服務有多臺機器的時候,手動複製日誌會更加麻煩而且容易出錯。而且設置mandatory參數會增加生產者的複雜性,需要添加處理這些被退回的消息的邏輯。如果既不想丟失消息,又不想增加生產者的複雜性,該怎麼做呢?前面在設置死信隊列中,我們提到,可以爲隊列設置死信交換機來存儲那些處理失敗的消息,可是這些不可路由消息根本沒有機會進入到隊列,因此無法使用死信隊列來保存消息。在RabbitMQ中,有一種備份交換機的機制存在,可以很好的應對這個問題。什麼是備份交換機呢?備份交換機可以理解爲RabbitMQ中交換機的“備胎”,當我們爲某一個交換機聲明一個對應的備份交換機時,就是爲它創建一個備胎,當交換機接收到一條不可路由消息時,將會把這條消息轉發到備份交換機中,由備份交換機來進行轉發和處理,通常備份交換機的類型爲Fanout,這樣就能把所有消息都投遞到與其綁定的隊列中,然後我們在備份交換機下綁定一個隊列,這樣所有那些原交換機無法被路由的消息,就會都進入這個隊列了。當然,我們還可以建立一個報警隊列,用獨立的消費者來進行監測和報警。
代碼架構圖
RabbitmqUntils.GetConfirmAdvancedQueue()方法增加代碼
/// <summary>
/// 發佈確認高級
/// </summary>
/// <returns></returns>
public static IModel GetConfirmAdvancedQueue()
{
/*
1. 聲明confirm.exchange type = direct
2. 聲明confirm.queue
3. 聲明confirm.routingkey
4. 綁定隊列與交換機
*/
var channel = RabbitmqUntils.GetChannel();
var arguments = new Dictionary<string, object>();
arguments.Add("alternate-exchange",Back_Exchange);
channel.ExchangeDeclare(Confirm_Exchange,"direct",true,false, arguments);
channel.QueueDeclare(Confirm_Queue, false, false, false, null);
channel.QueueBind(Confirm_Queue,Confirm_Exchange,Confirm_Routingkey);
/*
1. 聲明backup.exchange type = fanout
2. 聲明backup.queue,warning.queue
4. 綁定隊列與交換機
5. 配置確認交換機(Confirm_Exchange)轉發到備份交換機(Back_Exchange)
*/
channel.ExchangeDeclare(Back_Exchange, "fanout", true, false, null);
channel.QueueDeclare(Back_Queue,false,false, false, null);
channel.QueueDeclare(Warning_Queue,false,false, false, null);
channel.QueueBind(Back_Queue, Back_Exchange, "");
channel.QueueBind(Warning_Queue, Back_Exchange, "");
return channel;
}
生產者代碼
using rabbitmq.common;
using RabbitMQ.Client;
using System.Text;
namespace PublishConfirm.Producer
{
public class Program
{
static void Main(string[] args)
{
Console.WriteLine("請輸入要發送的消息:");
string message = Console.ReadLine();
if (string.IsNullOrEmpty(message))
{
while (true)
{
Console.WriteLine("請輸入要發送的消息: {0}", message);
}
}
else
{
var channel = RabbitmqUntils.GetConfirmAdvancedQueue();
while (true)
{
SendMsg(message, channel);
message = Console.ReadLine();
}
}
Console.ReadKey();
}
public static void SendMsg(string msg,IModel channel)
{
var body1 = Encoding.UTF8.GetBytes("路由正常_"+msg);
var body2 = Encoding.UTF8.GetBytes("路由不正常_" + msg);
channel.ConfirmSelect();// 開啓發布確認
channel.BasicPublish(RabbitmqUntils.Confirm_Exchange, RabbitmqUntils.Confirm_Routingkey, mandatory: true, null, body1);
//演示Routingkey配置錯誤
//如果發佈了帶有“mandatory” = true 標誌集的消息,但無法傳遞,代理將其返回給發送客戶端( channel.BasicReturn)。
channel.BasicPublish(RabbitmqUntils.Confirm_Exchange, RabbitmqUntils.Confirm_Routingkey+"111", mandatory:true, null, body2);
// 監聽確認的消息
channel.BasicAcks += (sender, e) =>
{
Console.WriteLine($"交換機已收到:{e.DeliveryTag}");
};
//監聽回退的消息
channel.BasicReturn += ((sender, e) =>
{
var body = e.Body.ToArray();
var msg = System.Text.Encoding.UTF8.GetString(body);
Console.WriteLine($"監聽回退的消息{msg};RoutingKey:{e.RoutingKey};退回原因:{e.ReplyText}");
});
}
}
}
消費者者代碼
using rabbitmq.common;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Text;
namespace PublishConfirm.Consumer
{
public class Program
{
static void Main(string[] args)
{
if (args[0] == "client1")
{
ConfirmConsumer();
} else if (args[0] == "client2")
{
BackupConsumer();
}
else if (args[0] == "client3")
{
WarningConsumer();
}
Console.ReadKey();
}
public static void ConfirmConsumer()
{
Console.WriteLine("ConfirmConsumer開始接受消息:");
var channel = RabbitmqUntils.GetConfirmAdvancedQueue();
var consumer = new EventingBasicConsumer(channel);
channel.BasicConsume(RabbitmqUntils.Confirm_Queue, false, consumer);
consumer.Received += ((sender, e) =>
{
var body = e.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
Console.WriteLine($"ConfirmConsumer{DateTime.Now} 接收消息:{message}");
channel.BasicAck(e.DeliveryTag, false);
});
}
public static void BackupConsumer()
{
Console.WriteLine("BackupConsumer開始接受消息:");
var channel = RabbitmqUntils.GetConfirmAdvancedQueue();
var consumer = new EventingBasicConsumer(channel);
channel.BasicConsume(RabbitmqUntils.Back_Queue, false, consumer);
consumer.Received += ((sender, e) =>
{
var body = e.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
Console.WriteLine($"BackupConsumer{DateTime.Now} 接收消息:{message}");
channel.BasicAck(e.DeliveryTag, false);
});
}
public static void WarningConsumer()
{
Console.WriteLine("WarningConsumer開始接受消息:");
var channel = RabbitmqUntils.GetConfirmAdvancedQueue();
var consumer = new EventingBasicConsumer(channel);
channel.BasicConsume(RabbitmqUntils.Warning_Queue, false, consumer);
consumer.Received += ((sender, e) =>
{
var body = e.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
Console.WriteLine($"WarningConsumer{DateTime.Now} 接收消息:{message}");
channel.BasicAck(e.DeliveryTag, false);
});
}
}
}
測試效果
注意:測試時我們需要把原來的**confirm.queue**
刪除,因爲我們修改了屬性,否則會報錯
mandatory參數與備份交換機可以一起使用的時候,如果兩者同時開啓,經過上面結果顯示答案是備份交換機優先級高。