← All tutorials

RabbitMQ — چیست، چرا و کجا

معرفی ساده‌ی صف پیام، همراه با مثال‌های عملی در دات‌نت

بنر RabbitMQ: Producer پیام را به Exchange می‌دهد و از آنجا به صف‌ها و مصرف‌کننده‌ها می‌رسد.
فرستنده منتظر مصرف‌کننده نمی‌ماند؛ صف پیام کار را جدا می‌کند.

در این آموزش چه یاد می‌گیریم؟

در این آموزش به سه پرسش اصلی درباره‌ی RabbitMQ پاسخ می‌دهیم: چیست، چرا به سیستم نیاز به واسطه‌ی پیام (message broker) داریم، و کجا — و کجا نه — باید از آن استفاده کنیم. مفاهیم پایه (تولیدکننده، صف، exchange، binding و مصرف‌کننده)، الگوهای رایج پیام‌رسانی و یک مثال عملی در .NET 8 را هم پوشش می‌دهیم.

اگر تجربه‌ی قبلی با صف پیام ندارید، جای نگرانی نیست. اگر Web API ساخته‌اید و حس کرده‌اید بعضی کارها نباید جلوی پاسخ HTTP را بگیرند، این آموزش برای شماست.

۱. RabbitMQ چیست؟

RabbitMQ یک واسطه‌ی پیام (message broker) است؛ یعنی میان‌افزار بین بخش‌های مختلف سیستم. به‌جای اینکه سرویس A مستقیماً سرویس B را صدا بزند و منتظر پاسخ بماند، سرویس A پیام را در RabbitMQ قرار می‌دهد و سرویس B هر وقت آماده بود آن را دریافت و پردازش می‌کند.

مثل اداره پست است: نامه را مستقیم به دست گیرنده نمی‌دهید و منتظر خواندنش نمی‌مانید. آن را در صندوق قرار می‌دهید، اداره پست نگه می‌دارد و مسیریابی می‌کند، و گیرنده در زمان مناسب خودش می‌خواند.

RabbitMQ پروتکل AMQP را پیاده‌سازی می‌کند. با Erlang نوشته شده، متن‌باز است و در کنار Kafka و Redis Streams یکی از پرکاربردترین واسطه‌های پیام است.

۲. چرا به واسطه‌ی پیام نیاز داریم؟

فرض کنید یک فروشگاه آنلاین دارید. با ثبت سفارش، این کارها باید انجام شود:

  • سفارش ذخیره شود
  • ایمیل تأیید ارسال شود
  • انبار مطلع شود
  • آمار به‌روزرسانی شود

اگر همه‌ی این کارها را داخل همان درخواست HTTP انجام دهید، کاربر تا پایان همه‌چیز منتظر می‌ماند. اگر سرور ایمیل کند باشد، کل فرایند پرداخت کند به‌نظر می‌رسد. بدتر از آن، اگر سرویس انبار از کار افتاده باشد، ممکن است کل سفارش با خطا مواجه شود.

با واسطه‌ی پیام، API فقط سفارش را ذخیره می‌کند و رویداد OrderPlaced را منتشر می‌کند. بقیه‌ی کارها در پس‌زمینه انجام می‌شوند. مزایا:

مزیتتوضیح
جداسازی (Decoupling)تولیدکننده نمی‌داند — و لازم نیست بداند — چه کسی پیام را مصرف می‌کند. می‌توانید مصرف‌کننده‌ی جدید اضافه کنید بدون تغییر در تولیدکننده.
ناهمگام بودن (Async)کارهای زمان‌بر (ایمیل، گزارش) در پس‌زمینه انجام می‌شوند و کاربر پاسخ سریع می‌گیرد.
تنظیم بار (Load leveling)در اوج ترافیک، صف پر می‌شود و مصرف‌کننده‌ها با سرعت خودشان آن را خالی می‌کنند؛ به‌جای از کار افتادن سرویس.
تاب‌آوری (Resilience)اگر مصرف‌کننده خاموش باشد، پیام‌ها در صف می‌مانند و پس از بازگشت، پردازش می‌شوند.
مقیاس‌پذیری (Scalability)برای پردازش سریع‌تر یک صف، مصرف‌کننده‌ی بیشتری اضافه کنید (competing consumers).

۳. مفاهیم اصلی

۳.۱ تولیدکننده (Producer) و مصرف‌کننده (Consumer)

Producer کدی است که پیام می‌فرستد. Consumer کدی است که پیام را دریافت و پردازش می‌کند. یک سرویس می‌تواند هم‌زمان هر دو نقش را داشته باشد.

۳.۲ صف (Queue)

Queue بافری است که پیام‌ها را تا زمان دریافت توسط مصرف‌کننده نگه می‌دارد. به‌صورت FIFO (اول وارد، اول خارج) کار می‌کند. پیام‌ها تا زمان تأیید (ack) در صف باقی می‌مانند.

۳.۳ Exchange و Binding

نکته‌ای که اغلب تازه‌کارها را غافلگیر می‌کند: در RabbitMQ، تولیدکننده مستقیماً به صف پیام نمی‌فرستد. پیام را به exchange می‌فرستد و exchange بر اساس قوانین binding و routing key تعیین می‌کند پیام به کدام صف(ها) برود.

Producer با یک routing key منتشر می‌کند
↓
Exchange پیام را دریافت می‌کند
↓
قوانین binding آن را به صف(های) منطبق می‌فرستند
↓
Consumer از صف می‌خواند

۳.۴ تأیید پیام (Acknowledgement)

پس از پردازش موفق، مصرف‌کننده یک ack می‌فرستد و به RabbitMQ اعلام می‌کند پیام تمام شده و می‌تواند حذف شود. اگر مصرف‌کننده قبل از ack از کار بیفتد، RabbitMQ پیام را دوباره در صف قرار می‌دهد تا گم نشود. این پایه‌ی تحویل قابل‌اعتماد است.

با auto-ack، پیام به‌محض تحویل حذف می‌شود. اگر بعد از آن مصرف‌کننده از کار بیفتد، پیام برای همیشه از دست می‌رود. برای کارهای مهم، از ack دستی استفاده کنید.

۴. انواع Exchange

نوع exchange مشخص می‌کند پیام‌ها چطور مسیریابی شوند:

نوعمنطق مسیریابیکاربرد
directrouting key باید دقیقاً با binding key یکی باشدمسیریابی بر اساس دسته، مثل order.created
fanoutrouting key را نادیده می‌گیرد — به همه‌ی صف‌های متصل کپی می‌کندپخش همگانی، مثل اطلاع‌رسانی یک رویداد به همه‌ی سرویس‌ها
topicتطبیق الگو با wildcard های * و #مسیریابی منعطف، مثل order.*.eu
headersبه‌جای routing key بر اساس مقدار header های پیام مسیریابی می‌کندقوانین پیچیده‌ای که با یک رشته کلید بیان نمی‌شوند
در topic exchange: * دقیقاً یک کلمه و # صفر یا چند کلمه را تطبیق می‌دهد. پس order.# هم order.created و هم order.created.eu را می‌گیرد.

۵. الگوهای رایج پیام‌رسانی

صف کار (Work Queue)

یک صف و چند مصرف‌کننده. RabbitMQ پیام‌ها را بین آن‌ها توزیع می‌کند — مناسب برای کارهای سنگین مثل پردازش تصویر.

انتشار / اشتراک (Publish / Subscribe)

یک fanout exchange هر پیام را به چند صف کپی می‌کند تا چند سرویس، هرکدام نسخه‌ی خودشان از همان رویداد را دریافت کنند.

مسیریابی (Routing)

یک direct یا topic exchange پیام‌ها را فقط به مصرف‌کننده‌هایی می‌فرستد که به دسته‌ی مشخصی علاقه دارند.

درخواست / پاسخ (RPC)

مصرف‌کننده به یک صف callback موقت پاسخ می‌دهد. امکان‌پذیر است، اما اگر زیاد به این الگو نیاز دارید، فراخوانی مستقیم HTTP یا gRPC معمولاً ساده‌تر است.

۶. کجا استفاده کنیم — و کجا نه

مناسب است وقتی:
  • کار پس‌زمینه دارید که نباید کاربر منتظر آن بماند (ایمیل، PDF، گزارش، تصویر بندانگشتی).
  • میکروسرویس‌ها باید از هم جدا (decoupled) بمانند.
  • ترافیک جهشی دارید و می‌خواهید بار را هموار کنید (load leveling).
  • یک رویداد باید چند واکنش مستقل را فعال کند.
  • کارهای طولانی دارید که در صورت خطا باید با اطمینان دوباره اجرا شوند.
احتمالاً گزینه‌ی مناسبی نیست وقتی:
  • به پاسخ فوری و همگام نیاز دارید (از HTTP یا gRPC استفاده کنید).
  • به بازپخش (replay) کل تاریخچه‌ی رویدادها یا استریم بسیار بزرگ نیاز دارید — Kafka مناسب‌تر است.
  • برنامه‌ی شما CRUD ساده است و کار پس‌زمینه ندارد — واسطه‌ی پیام فقط پیچیدگی اضافه می‌کند.
  • به ترتیب دقیق سراسری روی میلیون‌ها پیام نیاز دارید — با چند مصرف‌کننده همزمان، این کار سخت است.
RabbitMQ در یک خط در برابر Kafka: RabbitMQ واسطه‌ی هوشمندی است که پیام را به مصرف‌کننده‌ها push می‌کند و برای صف کار و مسیریابی مناسب است؛ Kafka یک لاگ توزیع‌شده برای استریم رویداد با throughput بالا و قابلیت replay است.

۷. تفاوت RabbitMQ و Kafka

این پرسش رایجی است. بیایید فراتر از یک جمله‌ی خلاصه برویم. این دو ابزار شبیه به‌نظر می‌رسند (هر دو پیام را بین سرویس‌ها جابه‌جا می‌کنند)، اما بر پایه‌ی ایده‌های اساساً متفاوتی ساخته شده‌اند.

تفاوت بنیادی: صف در برابر لاگ

RabbitMQ یک صف است. پیام به مصرف‌کننده تحویل می‌شود و پس از ack، حذف می‌شود. واسطه فعالانه پیام را ارسال می‌کند و پیگیری می‌کند چه کسی چه چیزی دریافت کرده. واسطه «هوشمند» است و مصرف‌کننده «ساده».

Kafka یک لاگ فقط-افزودنی (append-only) است. پیام‌ها (record) در یک لاگ مرتب و تغییرناپذیر نوشته می‌شوند و برای مدت مشخصی (مثلاً ۷ روز) باقی می‌مانند، حتی اگر قبلاً خوانده شده باشند. مصرف‌کننده‌ها recordها را pull می‌کنند و موقعیت خود (offset) را نگه می‌دارند. واسطه «ساده» است و مصرف‌کننده «هوشمند».

همین تفاوت — حذف پس از مصرف در برابر نگهداری و replay — بیشتر تفاوت‌های بعدی را توضیح می‌دهد.

مقایسه‌ی کنار هم

جنبهRabbitMQKafka
مدلصف پیام (واسطه push می‌کند)لاگ commit توزیع‌شده (مصرف‌کننده pull می‌کند)
طول عمر پیامپس از ack حذف می‌شودتا پایان بازه‌ی نگه‌داری می‌ماند؛ قابل خواندن مجدد
بازپخش تاریخچهخیر — پس از حذف، دیگر در دسترس نیستبله — با برگرداندن offset، دوباره پردازش می‌شود
مسیریابیغنی: direct، topic، fanout، headersساده: بر اساس topic و partition (منطق در مصرف‌کننده)
ترتیبدر هر صف (با competing consumers از بین می‌رود)ترتیب دقیق درون هر partition
throughputده‌ها هزار پیام در ثانیه — برای بیشتر برنامه‌ها کافیمیلیون‌ها پیام در ثانیه — برای مقیاس بسیار بزرگ
مصرف‌کننده‌هاچند مصرف‌کننده یک صف را به‌اشتراک می‌گذارند (تقسیم کار)consumer group؛ هر group کل topic را می‌خواند
تأخیر معمولبسیار کم، مناسب تک‌پیامکم، بهینه برای batch/throughput
سنگینی عملیاتیسبک‌تر؛ شروعش آسانسنگین‌تر؛ partition، broker و (اغلب) ZooKeeper/KRaft
پروتکلAMQP (همچنین MQTT، STOMP)پروتکل باینری اختصاصی خودش

تفاوت در مدیریت چند مصرف‌کننده

این بخش اغلب گیج‌کننده است. فرض کنید رویداد OrderPlaced باید به سرویس ایمیل، سرویس انبار و سرویس تحلیل برسد:

  • RabbitMQ: یک fanout exchange پیام را در سه صف جدا کپی می‌کند — یکی برای هر سرویس. سپس هر صف مستقل پردازش می‌شود.
  • Kafka: هر سه سرویس به topic orders به‌عنوان سه consumer group جدا subscribe می‌کنند. لاگ سه بار خوانده می‌شود؛ داده کپی نمی‌شود، هر group فقط offset خود را نگه می‌دارد.

برای تقسیم کار سنگین بین workerهای یک سرویس:

  • RabbitMQ: N مصرف‌کننده روی یک صف — RabbitMQ پیام‌ها را بین آن‌ها توزیع می‌کند (competing consumers).
  • Kafka: topic باید حداقل N partition داشته باشد؛ هر مصرف‌کننده در group چند partition می‌گیرد. تعداد مصرف‌کننده‌ی فعال نمی‌تواند از partition بیشتر باشد.

چه زمانی کدام را انتخاب کنیم

RabbitMQ را انتخاب کنید وقتی:
  • به صف کار نیاز دارید — «این کار را یک‌بار در پس‌زمینه انجام بده».
  • مسیریابی منعطف می‌خواهید (بر اساس نوع، پخش همگانی، قوانین header).
  • تأیید per-message و retry یا dead-letter آسان می‌خواهید.
  • مقیاس معمولی دارید (هزاران تا ده‌ها هزار پیام در ثانیه) و عملیات ساده می‌خواهید.
Kafka را انتخاب کنید وقتی:
  • به event streaming نیاز دارید — تاریخچه‌ی پایدار و قابل replay از «چه اتفاقی افتاد».
  • چند تیم مستقل باید همان استریم را بارها بخوانند.
  • به throughput بسیار بالا نیاز دارید (لاگ، clickstream، IoT، متریک).
  • به event sourcing، پردازش استریم (Kafka Streams / ksqlDB) یا replay داده در سیستم جدید نیاز دارید.
لزوماً یکی از دو تاست. بسیاری از سیستم‌ها از هر دو استفاده می‌کنند: RabbitMQ برای صف کار و درخواست‌های عملیاتی، Kafka برای ستون فقرات استریم رویداد و تحلیل. ابزار را بر اساس نیاز واقعی انتخاب کنید، نه بر اساس مد روز.

قاعده‌ی سرانگشتی: اگر می‌گویید «این کار را انجام بده» → RabbitMQ. اگر می‌گویید «ثبت کن که این اتفاق افتاد و هرکسی بتواند دوباره بخواند» → Kafka.

۸. کار عملی با دات‌نت

نصب کلاینت رسمی:

dotnet add package RabbitMQ.Client

Producer — انتشار یک پیام

using RabbitMQ.Client;
using System.Text;

var factory = new ConnectionFactory { HostName = "localhost" };
using var connection = factory.CreateConnection();
using var channel = connection.CreateModel();

// مطمئن شو صف وجود دارد (durable = با ری‌استارت broker باقی می‌ماند)
channel.QueueDeclare(
    queue: "orders",
    durable: true,
    exclusive: false,
    autoDelete: false,
    arguments: null);

var body = Encoding.UTF8.GetBytes("{ \"orderId\": 42 }");

var props = channel.CreateBasicProperties();
props.Persistent = true; // پیام با ری‌استارت broker باقی می‌ماند

channel.BasicPublish(
    exchange: "",            // exchange پیش‌فرض
    routingKey: "orders",    // مستقیم به صف "orders" می‌رود
    basicProperties: props,
    body: body);

Console.WriteLine("Sent order message");

Consumer — دریافت و پردازش

using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Text;

var factory = new ConnectionFactory { HostName = "localhost" };
using var connection = factory.CreateConnection();
using var channel = connection.CreateModel();

channel.QueueDeclare("orders", durable: true, exclusive: false,
    autoDelete: false, arguments: null);

// تا وقتی ack نکرده‌ام فقط یک پیام بده
channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false);

var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) =>
{
    var message = Encoding.UTF8.GetString(ea.Body.ToArray());
    Console.WriteLine($"Received: {message}");

    // ... کار اصلی اینجا انجام می‌شود ...

    // به RabbitMQ بگو تمام شد تا پیام را حذف کند
    channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);
};

channel.BasicConsume(queue: "orders", autoAck: false, consumer: consumer);

Console.WriteLine("Waiting for messages. Press [enter] to exit.");
Console.ReadLine();
BasicQos(prefetchCount: 1) یعنی «تا پیام فعلی را ack نکرده‌ام، پیام جدید نفرست». با چند مصرف‌کننده، بار را منصفانه توزیع می‌کند.

۹. نکات قابلیت اطمینان

  • صف durable + پیام persistent تا داده با ری‌استارت broker باقی بماند.
  • ack دستی تا کرش در میانه‌ی پردازش پیام را دوباره به صف بگذارد نه اینکه گمش کند.
  • Dead Letter Exchange (DLX) — پیام‌هایی که مدام شکست می‌خورند را به یک صف جدا «نامه‌ی مرده» بفرستید تا بررسی شوند، به‌جای حلقه‌ی بی‌پایان.
  • Consumerهای idempotent — یک پیام ممکن است بیش از یک‌بار تحویل شود؛ پس پردازش مجدد باید امن باشد (مثلاً بررسی کنید «آیا سفارش ۴۲ قبلاً پردازش شده؟»).
  • تنظیم prefetch — تا یک مصرف‌کننده هزاران پیام را یک‌جا نگیرد در حالی که بقیه بیکارند.
  • Publisher confirms — برای اطمینان از اینکه واسطه واقعاً پیام شما را دریافت کرده است.

DLX به زبان ساده (Dead Letter Exchange)

فرض کنید مصرف‌کننده‌ای دارید که سفارش‌ها را پردازش می‌کند. یک پیام معیوب می‌رسد — مثلاً داده ناقص است یا باگ دارد. مصرف‌کننده خطا می‌دهد و پیام را ack نمی‌کند. RabbitMQ فکر می‌کند «این پیام پردازش نشده» و دوباره همان را تحویل می‌دهد. باز هم خطا، باز هم تکرار... و این می‌شود حلقه‌ی بی‌پایان که هم منابع را هدر می‌دهد و هم صف را مسدود می‌کند.

DLX همین مشکل را حل می‌کند. به‌جای چرخیدن بی‌پایان پیام معیوب در صف اصلی، آن را به یک صف جداگانه می‌فرستیم تا بعداً بررسی شود. به این صف می‌گویند Dead Letter Queue (صف نامه‌های مرده) و exchangeای که پیام را به آنجا هدایت می‌کند، Dead Letter Exchange (DLX) نام دارد.

پیام معمولاً در این سه حالت «مرده» محسوب می‌شود و به DLX می‌رود:

  • مصرف‌کننده آن را reject یا nack کند (یعنی اعلام کند «نمی‌توانم پردازش کنم»).
  • پیام در صف منقضی شود (TTL یا مهلت زمانی تمام شده باشد).
  • صف پر شده باشد و جا برای پیام جدید نباشد.

یعنی جریان کار این‌طور می‌شود:

پیام به صف اصلی می‌رسد
↓
مصرف‌کننده در پردازش خطا می‌دهد و reject می‌کند
↓
RabbitMQ پیام را به DLX می‌فرستد
↓
پیام در «صف نامه‌های مرده» می‌نشیند تا بررسی شود

برای راه‌اندازی، هنگام ساخت صف اصلی مشخص کنید پیام‌های مرده به کجا بروند. مثال در دات‌نت:

// تعریف صف نامه‌های مرده و exchange مربوط به آن
channel.ExchangeDeclare("orders.dlx", ExchangeType.Fanout);
channel.QueueDeclare("orders.dead", durable: true, exclusive: false, autoDelete: false);
channel.QueueBind("orders.dead", "orders.dlx", routingKey: "");

// صف اصلی را بسازید و مشخص کنید پیام‌های مرده به DLX بروند
var args = new Dictionary<string, object>
{
    { "x-dead-letter-exchange", "orders.dlx" }
};
channel.QueueDeclare("orders", durable: true, exclusive: false,
    autoDelete: false, arguments: args);
نکته‌ی مهم: برای پیام معیوب باید با requeue: false آن را nack کنید؛ وگرنه به‌جای رفتن به DLX، دوباره به صف اصلی برمی‌گردد:
channel.BasicNack(deliveryTag: ea.DeliveryTag,
    multiple: false, requeue: false);
مزیت: پیام‌های سالم بدون مزاحمت پردازش می‌شوند. پیام‌های معیوب گم نمی‌شوند، بلکه یک‌جا جمع می‌شوند. بعداً می‌توانید صف نامه‌های مرده را بررسی کنید، علت خطا را پیدا کنید، باگ را برطرف کنید و در صورت نیاز پیام‌ها را دوباره به صف اصلی برگردانید.

۱۰. اجرای RabbitMQ

ساده‌ترین راه Docker است، با ایمیجی که رابط مدیریتی را هم دارد:

docker run -d --name rabbitmq \
  -p 5672:5672 \
  -p 15672:15672 \
  rabbitmq:3-management
پورتکاربرد
5672AMQP — برنامه‌های شما از این پورت وصل می‌شوند
15672رابط وب مدیریت

آدرس http://localhost:15672 را باز کنید (ورود پیش‌فرض: guest / guest) تا صف‌ها، نرخ پیام‌ها و مصرف‌کننده‌ها را به‌صورت زنده ببینید.


جمع‌بندی نهایی

  • RabbitMQ یک واسطه‌ی پیام است — مثل اداره پست بین سرویس‌های شما.
  • تولیدکننده‌ها به exchange می‌فرستند؛ bindingها به صف‌ها مسیریابی می‌کنند؛ مصرف‌کننده‌ها می‌خوانند و ack می‌کنند.
  • برای کار پس‌زمینه، جداسازی، تنظیم بار و پخش رویداد مناسب است.
  • برای درخواست/پاسخ فوری یا replay استریم بسیار بزرگ مناسب نیست (gRPC یا Kafka را در نظر بگیرید).
  • برای اطمینان: durable + persistent + ack دستی + DLX + idempotency.

حالا می‌دانید RabbitMQ چیست، چرا کمک می‌کند و کجا مناسب است. 🐰