工作隊列(又稱任務隊列)的主要思想是避免立即執行資源密集型任務,而不得不等待它完成。相反我們安排任務在之後執行。我們把任務封裝爲消息並將其發送到隊列。在後臺運行的工作進程將彈出任務並最終執行作業。當有多個工作線程時,這些工作線程將一起處理這些任務。
輪詢分發消息
封裝獲取Channel代碼
using RabbitMQ.Client;
using System.Data.SqlTypes;
namespace rabbitmq.common
{
/// <summary>
/// 工具類
/// </summary>
public class RabbitmqUntils
{
/// <summary>
/// 對列名稱
/// </summary>
public static string QueueName { get; set; } = "test_hello";
public static string WorkQueueName { get; set; } = "test_WorkQueue";
/// <summary>
/// 得到一個Channel 作爲輕量級的Connection極大減少了操作系統建立TCPconnection的開銷
/// </summary>
/// <returns></returns>
public static IModel GetChannel()
{
//創建一個連接工廠
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.HostName = "localhost";
connectionFactory.UserName = "guest";
connectionFactory.Password = "guest";
var connection = connectionFactory.CreateConnection();
var channel = connection.CreateModel();
return channel;
}
}
}
生產者代碼
using rabbitmq.common;
using RabbitMQ.Client;
using System.Text;
namespace _02.WorkQueue.Producer
{
public class Program
{
static void Main(string[] args)
{
using var channel = RabbitmqUntils.GetChannel();
/*
*生成一個隊列
*1.隊列名稱
*2.隊列裏面的消息是否持久化默認消息存儲在內存中
*3.該隊列是否只供一個消費者進行消費是否進行共享true可以多個消費者消費
*4.是否自動刪除最後一個消費者端開連接以後該隊列是否自動刪除true自動刪除*
*5.其他參數
*/
channel.QueueDeclare(RabbitmqUntils.WorkQueueName, false, false, false, null);//創建一個消息隊列
Console.WriteLine("請輸入要發送的消息:");
string message = Console.ReadLine();
if (string.IsNullOrEmpty(message))
{
while (true)
{
Console.WriteLine("請輸入要發送的消息: {0}", message);
}
}
else
{
while (true)
{
var body = Encoding.UTF8.GetBytes(message);
/*
* 發送一個消息
* 1.發送到那個交換機
* 2.路由的 key 是哪個
* 3.其他的參數信息
* 4.發送消息的消息體
*/
channel.BasicPublish(exchange: "", RabbitmqUntils.WorkQueueName, null, body); //開始傳遞
Console.WriteLine("已發送: {0}", message);
Console.WriteLine("請輸入要發送的消息:");
message = Console.ReadLine();
}
}
Console.ReadKey();
}
}
}
消費者代碼
using rabbitmq.common;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Text;
namespace _02.WorkQueue.Consumer1
{
public class Program
{
static void Main(string[] args)
{
if (args == null || args.Length == 0)
{
Console.WriteLine("請輸入客戶端名稱:");
string clientName = Console.ReadLine();
while (string.IsNullOrEmpty(clientName))
{
Console.WriteLine("請輸入客戶端名稱:");
}
args = new string[clientName.Length];
args[0] = clientName;
}
Console.WriteLine($"{args[0]}:等待消費消息");
using var channel = RabbitmqUntils.GetChannel();
// 創建隊列/交換機(如隊列/交換機已存在的情況可不用再次創建/此創建爲:確保先開啓消費者,生產者未創建隊列/交換機而引發報錯)
channel.QueueDeclare(RabbitmqUntils.WorkQueueName, false, false, false, null);
// 事件對象
var consumer = new EventingBasicConsumer(channel);
/*
* 消費者消費消息
* 1.消費哪個隊列
* 2.消費成功之後是否要自動應答 true 代表自動應答 false 手動應答
* 3.消費者未成功消費的回調
*/
channel.BasicConsume(RabbitmqUntils.WorkQueueName, false, consumer);
// 接收消息回調
consumer.Received += (sender, e) =>
{
var body = e.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
Console.WriteLine("接收消息: {0}", message);
};
// 取消消息回調
consumer.Registered += (sender, e) =>
{
var body = e.ConsumerTags.ToArray();
Console.WriteLine("取消消息: {0}", body);
};
Console.WriteLine($"{args[0]}:消費消息完成!");
Console.ReadKey();
}
}
}
測試效果
消息應答
消費者完成一個任務可能需要一段時間,如果其中一個消費者處理一個長的任務並僅只完成 了部分突然它掛掉了,會發生什麼情況。RabbitMQ 一旦向消費者傳遞了一條消息,便立即將該消 息標記爲刪除。在這種情況下,突然有個消費者掛掉了,我們將丟失正在處理的消息。以及後續 發送給該消費這的消息,因爲它無法接收到。 爲了保證消息在發送過程中不丟失,rabbitmq 引入消息應答機制,消息應答就是:消費者在接收 到消息並且處理該消息之後,告訴 rabbitmq 它已經處理了,rabbitmq 可以把該消息刪除了。
自動應答
消息發送後立即被認爲已經傳送成功,這種模式需要在高吞吐量和數據傳輸安全性方面做權 衡,因爲這種模式如果消息在接收到之前,消費者那邊出現連接或者 channel 關閉,那麼消息就丟失 了,當然另一方面這種模式消費者那邊可以傳遞過載的消息,沒有對傳遞的消息數量進行限制,當 然這樣有可能使得消費者這邊由於接收太多還來不及處理的消息,導致這些消息的積壓,最終使 得內存耗盡,最終這些消費者線程被操作系統殺死,所以這種模式僅適用在消費者可以高效並以 某種速率能夠處理這些消息的情況下使用。
手動應答
手動應答的三個方法
- 用於肯定應答 RabbitMQ 已知道該消息並且成功的處理消息,可以將其丟棄了
- 用於否定應答
- 用於否定應答 與 Channel.BasicNack 相比少一個 multiple 參數不處理該消息了直接拒絕,可以將其丟棄了
multiple 參數
true 代表批量應答 channel 上未應答的消息
比如說 channel 上有傳送 tag 的消息 5,6,7,8 當前 tag 是8 那麼此時
5-8 的這些還未應答的消息都會被確認收到消息應答
false 同上面相比
只會應答 tag=8 的消息 5,6,7 這三個消息依然不會被確認收到消息應答
消息重新入隊列
如果消費者由於某些原因失去連接(其通道已關閉,連接已關閉或 TCP 連接丟失),導致消息未發送 ACK 確認,RabbitMQ 將瞭解到消息未完全處理,並將對其重新排隊。如果此時其他消費者可以處理,它將很快將其重新分發給另一個消費者。這樣,即使某個消費者偶爾死亡,也可以確保不會丟失任何消息。
手動應答的關鍵代碼
手動應答測試效果
消息重新入隊列測試效果
Consumer1 休眠 1s
Consumer1 休眠 10s
發送消息 11,22,33,44
Consumer1 接受消息 11,33
Consumer2 接受消息 22 此時殺死 Consumer2進程可看到消息44被Consumer1消費
持久化
保障當 RabbitMQ 服務停掉以後消 息生產者發送過來的消息不丟失。默認情況下 RabbitMQ 退出或由於某種原因崩潰時,它忽視隊列 和消息,除非告知它不要這樣做。確保消息不會丟失需要做兩件事:我們需要將隊列和消息都標 記爲持久化
隊列持久化
申明隊列時設置參數durable:true
注意:可能會遇到的錯誤
如果之前聲明的隊列不是持久化的,需要把原先隊列先刪除,或者重新創建一個持久化的隊列,不然就會出現錯誤
消息持久化
不公平分發
最開始RabbitMQ分發消息採用的是輪詢分發,但是有兩個消費者在處理任務,其中有個消費者 1 處理任務的速度非常快,而另外一個消費者 2處理速度卻很慢,這個時候我們還是採用輪訓分發的化就會到這處理速度快的這個消費者很大一部分時間處於空閒狀態,而處理慢的那個消費者一直在幹活,這種分配方式在這種情況下其實就不太好,但是RabbitMQ 並不知道這種情況它依然很公平的進行分發
Consumer1,Consumer2 都配置如下代碼
預取值
限制此緩衝區的大小,以避免緩衝區裏面無限制的未確認消息問題
該值定義通道上允許的未確認消息的最大數量
雖然自動應答傳輸消息速率是最佳的,但是,在這種情況下已傳遞但尚未處理的消息的數量也會增加,從而增加了消費者的 RAM 消耗(隨機存取存儲器)
配置 prefetchCount > 1 即可