Manuel ack'ler ve publisher confirm'lerle RabbitMQ "at-least-once" teslimatı mümkün kılar, ama exactly-once teslimatı garanti etmez. Bu yüzden .NET'te güvenilir mesaj işlemenin birkaç katmanı vardır: durable kuyruklar ve persistent mesajlar, publisher confirm'ler, sınırlı bir prefetch ile manuel ack'ler, mükerrer mesajları güvenle atlayan bir consumer, bir dead-letter exchange üzerinden gecikmeli yeniden denemeler, zehirli (poison) mesajlar için bir parking kuyruğu ve hem veritabanına yazmanız hem de bir mesaj yayımlamanız gerektiğinde outbox deseni. Kod örnekleri RabbitMQ.Client 7'nin asenkron API'sini kullanıyor.
Bir mesaj nerede kaybolabilir?
Bir mesaj üç yerde risk altındadır: publisher ile broker arasında, broker'ın kendi içinde ve broker ile consumer arasında. Broker'dan başlayalım: kuyruk durable, mesaj persistent olmalıdır. İkisi birbirinden bağımsızdır; transient bir mesaj, durable bir kuyrukta dursa bile node yeniden başladığında silinir. RabbitMQ.Client 7'de bir mesajı persistent yapmak için BasicProperties içinde Persistent değerini true yapın. Quorum kuyruğu her zaman durable'dır, birkaç node'a replike edilir ve teslimat modundan bağımsız olarak tüm mesajlarını persistent tutar; bu rehberdeki örnekler bu kuyruk tipini kullanıyor.
Publisher tarafında, publisher confirm'ler etkin değilse tamamlanmış bir BasicPublishAsync yalnızca istemcinin mesajı gönderdiği anlamına gelir, broker'ın onu kabul ettiği anlamına gelmez. RabbitMQ.Client 7'de confirm'leri channel'ı oluştururken CreateChannelOptions ile açarsınız. publisherConfirmationTrackingEnabled de etkinse BasicPublishAsync üzerindeki bir await, broker'ın confirm'i gelene kadar tamamlanmaz. Broker mesajı nack ederse ya da mandatory: true ile yayımlanan bir mesaj hiçbir kuyruğa ulaşmayıp geri dönerse bir PublishException fırlatılır. Durable bir kuyruktaki persistent bir mesaj için confirm, mesajın diske yazıldığı anlamına gelir; quorum kuyruğunda ise replikaların çoğunluğunun onu kabul ettiği anlamına gelir. mandatory için varsayılan değer false'tur ve hiçbir kuyruğa ulaşmayan bir mesaj, bir alternate exchange yapılandırılmamışsa sessizce atılır.
Bağlantı confirm gelmeden koparsa publisher mesajın ulaşıp ulaşmadığını bilemez ve tek güvenli seçenek onu yeniden yayımlamaktır. Mükerrer mesajların ilk kaynağı budur.
Manuel ack, autoAck ve prefetch
autoAck modunda broker bir mesajı gönderdiği anda teslim edilmiş sayar; süreç işlemenin ortasında başarısız olursa ya da bağlantı koparsa mesaj gider. Önemli mesajlar için BasicConsumeAsync metodunu autoAck: false ile çağırın ve BasicAckAsync çağrısını yalnızca sonuç veritabanına kaydedildikten sonra gönderin. Ack'lenmemiş her teslimat, channel ya da bağlantı kapandığında otomatik olarak kuyruğa geri döner.
BasicQosAsync, bir consumer'ın aynı anda tutabileceği onaylanmamış mesaj sayısını belirler; global: false ile RabbitMQ bu sınırı her consumer'a ayrı ayrı uygular. Sıfır değeri sınır yok demektir ve dokümantasyon, ack'siz ve sınırsız tüketmenin broker node'unda bellek kullanımını artırdığı konusunda uyarır. Dokümantasyona göre 100 ile 300 arasındaki değerler genellikle en iyi throughput'u verir; 1 ise throughput'u belirgin şekilde düşüren en temkinli seçimdir. Doğru sayıyı gerçek yük altında ölçerek bulun.
İki varsayılan daha önemlidir. Ack timeout varsayılan olarak 30 dakikadır. Bir teslimat bu süre içinde ack'lenmezse, broker sürümüne ve kuyruk tipine bağlı olarak ya channel bir PRECONDITION_FAILED hatasıyla kapatılır ya da (4.3'ten beri quorum kuyruklarında) consumer iptal edilir; her iki durumda da ack'lenmemiş teslimatlar kuyruğa geri döner. RabbitMQ.Client 7'de ConsumerDispatchConcurrency de varsayılan olarak 1'dir; yani bir channel'ın handler'ları birer birer çalışır. Daha yüksek bir değerle işleme paralel yürür, ama mesajların işlenme sırası artık garanti edilmez ve handler'ın thread-safe olması gerekir.
Mükerrer mesajlar neden gelir?
Manuel ack'lerle teslimat at-least-once'tır ve mükerrer mesajlar sistemin normal davranışı olarak ele alınmalıdır. Üç yaygın senaryo:
- Consumer veritabanı transaction'ını commit etti, ama ack'i broker'a ulaşmadan başarısız oldu.
- Publisher confirm'i almadı ve mesajı yeniden yayımladı; oysa ilk kopya kuyruğa çoktan ulaşmıştı.
- Outbox desenindeki relay mesajı gönderdi, ama satırı gönderildi olarak işaretlemeden önce durdu.
Redelivered bayrağına güvenemezsiniz. RabbitMQ dokümantasyonu onu yalnızca mesajın daha önce görülmüş olabileceğine dair bir ipucu olarak görür ve ikinci senaryoda bu bayrak hiç ayarlanmaz, çünkü broker açısından ikinci kopya yeni bir mesajdır. Dokümantasyonun kendi tavsiyesi, consumer'ları idempotent olacak şekilde tasarlamaktır.
İlk seçenek işlemin kendisini idempotent yapmaktır: "42 numaralı siparişi ödendi olarak işaretle" defalarca çalışabilir, ama "stoğa 100 birim ekle" çalışamaz. Doğası gereği idempotent olmayan işlemler için ikinci seçenek, birincil anahtarı MessageId olan ve ana değişikliği kaydeden aynı transaction içinde doldurulan bir işlenmiş mesajlar tablosudur. Bu ancak producer mesajı oluştururken MessageId değerini ayarlarsa ve her yeniden yayımlamada korursa çalışır; her yayımlama yeni bir Guid üretirse tablo işe yaramaz.
public sealed class OrderPlacedHandler(BillingDbContext db)
{
public async Task HandleAsync(string messageId, OrderPlaced order, CancellationToken ct)
{
if (await db.ProcessedMessages.AnyAsync(p => p.MessageId == messageId, ct))
return;
var invoice = Invoice.CreateFor(order);
db.Invoices.Add(invoice);
db.ProcessedMessages.Add(new ProcessedMessage(messageId, DateTime.UtcNow));
db.OutboxMessages.Add(OutboxMessage.Create("invoice.created", new InvoiceCreated(invoice.Id)));
try
{
await db.SaveChangesAsync(ct);
}
catch (DbUpdateException ex) when (ex.InnerException is SqlException { Number: 2627 or 2601 })
{
// Same MessageId already saved by a concurrent delivery: treat as processed.
}
}
}
Üç ekleme de tek bir SaveChangesAsync çağrısında gerçekleşir ve EF Core varsayılan olarak tek bir SaveChanges çağrısının tüm değişikliklerini tek bir transaction içinde uygular; dolayısıyla fatura, mesaj kaydı ve outbox satırı birlikte yazılır ya da hiç yazılmaz. AnyAsync yalnızca ucuz bir kısayoldur; asıl garanti birincil anahtardan gelir. İki consumer aynı mesajı eşzamanlı olarak işlerse ikinci kayıt 2627 veya 2601 hatasıyla (birincil anahtar ya da benzersiz indeks ihlali) başarısız olur. Boş catch kasıtlıdır: ikinci transaction tamamen geri alınmıştır, ilki bu mesajı kaydetmiştir ve handler hata vermeden döner; böylece consumer mesajı ack'ler. Bu transaction'daki başka tabloların da benzersiz kısıtları varsa, gerçek bir hatanın gizlenmemesi için kısıt adını da kontrol edin. Ve her mesaj için yeni bir DbContext oluşturun, çünkü başarısız bir SaveChanges çağrısından sonra eklenen entity'ler hâlâ o DbContext tarafından izlenir.
Gecikmeli yeniden denemeler: bir DLX ve bir bekleme kuyruğu
Bir hataya verilebilecek en basit tepki requeue: true ile BasicNackAsync çağırmaktır, ama kuyruğa geri konan mesaj hemen yeniden teslim edilebilir. Hatanın nedeni hemen ortadan kalkmıyorsa, örneğin veritabanı birkaç dakika erişilemez durumdaysa, RabbitMQ dokümantasyonunun ağ bant genişliği ve CPU açısından pahalı diye tanımladığı bir teslim ve geri koyma döngüsü yaşarsınız. Denemelerin arasında bir boşluk olmalıdır.
Desteklenen tüm sürümlerde çalışan klasik desen bir dead-letter exchange (DLX) ve bir TTL'den oluşur. İş kuyruğu reddedilen mesajları kendi DLX'ine verir; bu exchange de consumer'ı olmayan, sabit bir TTL'ye sahip ve kendi DLX'i olarak orijinal exchange'i kullanan bir bekleme kuyruğuna bağlıdır. İşleme başarısız olduğunda consumer mesajı requeue: false ile reddeder; mesaj TTL dolana kadar bekleme kuyruğunda durur ve sonra iş kuyruğuna döner. Aşağıdaki kod bir BackgroundService sınıfının ExecuteAsync metodunun başlangıcıdır; kullanıcı adını ve parolayı koda yazmak yerine yapılandırmadan okuyun.
var factory = new ConnectionFactory
{
HostName = "rabbitmq.internal",
ClientProvidedName = "billing-worker"
};
await using var connection = await factory.CreateConnectionAsync(stoppingToken);
await using var channel = await connection.CreateChannelAsync(cancellationToken: stoppingToken);
await channel.ExchangeDeclareAsync("orders", ExchangeType.Direct, durable: true);
await channel.ExchangeDeclareAsync("orders.retry", ExchangeType.Fanout, durable: true);
await channel.QueueDeclareAsync("orders.work", durable: true, exclusive: false, autoDelete: false,
arguments: new Dictionary<string, object?>
{
["x-queue-type"] = "quorum",
["x-dead-letter-exchange"] = "orders.retry"
});
await channel.QueueBindAsync("orders.work", "orders", "order.placed");
await channel.QueueDeclareAsync("orders.wait", durable: true, exclusive: false, autoDelete: false,
arguments: new Dictionary<string, object?>
{
["x-queue-type"] = "quorum",
["x-message-ttl"] = 30_000,
["x-dead-letter-exchange"] = "orders"
});
await channel.QueueBindAsync("orders.wait", "orders.retry", "");
await channel.QueueDeclareAsync("orders.parking", durable: true, exclusive: false, autoDelete: false,
arguments: new Dictionary<string, object?> { ["x-queue-type"] = "quorum" });
Bekleme kuyruğunda x-dead-letter-routing-key yoktur; bu yüzden mesaj orijinal routing key'i olan order.placed ile orders exchange'ine, oradan da orders.work kuyruğuna geri döner. RabbitMQ dead-letter döngülerini tespit eder, ama bir mesajı yalnızca döngünün hiçbir yerinde reddetme olmadıysa atar; burada her tur consumer'ın bir reddiyle başlar. orders.parking kuyruğunun ne otomatik bir consumer'ı ne de TTL'si vardır; bu yüzden mesajlar incelenmek üzere orada kalır. Birkaç nokta daha:
- TTL'yi tek tek mesajlara değil, kuyruğa koyun. Süresi dolmuş bir mesaj ancak kuyruğun başına ulaştığında dead-letter edilir; mesaj başına ayrı TTL ile süresi dolmuş mesajlar, süresi henüz dolmamış olanların arkasında takılı kalabilir. Kademeli gecikmeler için farklı TTL'lere sahip birkaç bekleme kuyruğu oluşturun ve mesajı doğru kuyruğa consumer'ın kendisi yayımlasın.
- RabbitMQ dokümantasyonu DLX ve TTL için x-argument'ler yerine policy'leri önerir, çünkü x-argument'ler uygulamayı yeniden deploy etmeden değiştirilemez, bir policy ise her an değiştirilebilir. Yukarıdaki kodda okunabilirlik için x-argument'leri kullandım; kuyruk tanımlanırken ayarlanması zorunlu olan yalnızca
x-queue-type'tır. - Varsayılan olarak dead-lettering, dahili publisher confirm'ler olmadan gerçekleşir (at-most-once stratejisi) ve bir cluster'da bir mesaj bu yolda kaybolabilir. Quorum kuyruğunda bir policy içinde
dead-letter-strategydeğeriniat-least-once,overflowdeğerini dereject-publisholarak ayarlamak, daha fazla bellek ve CPU pahasına bu yolu at-least-once yapar. - RabbitMQ 4.3'ten beri quorum kuyruklarında doğrusal gecikmeli, yerleşik bir yeniden deneme vardır:
x-delayed-retry-type,x-delayed-retry-minvex-delayed-retry-maxargümanlarıyla (ya da bir policy'de x- öneki olmayan karşılık gelen anahtarlarla), consumer'ın kuyruğa geri verdiği bir mesaj yeniden teslim edilmeden önce bekletilir ve gecikme başarısız teslimat sayısıyla birlikte artar. Broker sürümünüz izin veriyorsa bu özellik bekleme kuyruğunun yerini alabilir. - Delayed message exchange eklentisinin deposundaki README'ye göre RabbitMQ ekibi artık bu eklentinin bakımını yapmıyor; yeni tasarımları onun üzerine kurmayın.
Zehirli mesajlar ve parking kuyruğu
Zehirli (poison) bir mesaj hiçbir zaman başarıyla işlenmez: formatı bozuktur, var olmayan bir kaydı işaret eder ya da handler'daki bir bug'a takılır. Onu sonsuza kadar yeniden denemek yalnızca kaynak israf eder ve sorunu gizler. Denemelerin bir üst sınırı olmalı; bu sınırdan sonra mesaj, birinin bakması için bir parking kuyruğuna gitmelidir.
Quorum kuyruklarının yerleşik bir güvenlik ağı da vardır. RabbitMQ 4.0'dan beri varsayılan delivery-limit 20'dir ve başarısız teslimat sayısı x-delivery-count header'ında taşınır. Sınırı aşan bir mesaj, bir DLX yapılandırılmışsa dead-letter edilir, yapılandırılmamışsa atılır; başka bir deyişle DLX'i olmayan bir quorum kuyruğu zehirli mesajları sessizce siler. 4.3'ten beri bu sayaç yalnızca basic.reject ya da kapanan bir channel veya bağlantı gibi gerçek başarısızlıklarda artar ve basic.nack çağrılarını saymaz. Dokümantasyona göre sınırı -1 ile devre dışı bırakmak önerilmez.
Bu tasarımda deneme sayısı x-death header'ından gelir: mesaj orders.work kuyruğundan her dead-letter edildiğinde RabbitMQ bu header'da o kuyruk ve neden için sayacı artırır. Consumer işlemeden önce sayıyı okur ve üst sınıra ulaşılmışsa mesajı çalıştırmadan parking'e gönderir. Okunamayan ya da MessageId değeri olmayan bir mesaj da doğrudan oraya gider. Bu parça aynı metodun devamıdır:
const int MaxAttempts = 5;
await using var parking = await connection.CreateChannelAsync(
new CreateChannelOptions(publisherConfirmationsEnabled: true, publisherConfirmationTrackingEnabled: true));
await channel.BasicQosAsync(prefetchSize: 0, prefetchCount: 20, global: false);
var consumer = new AsyncEventingBasicConsumer(channel);
consumer.ReceivedAsync += async (_, ea) =>
{
var id = ea.BasicProperties.MessageId;
OrderPlaced? order = null;
try { order = JsonSerializer.Deserialize<OrderPlaced>(ea.Body.Span); }
catch (JsonException) { }
if (id is null || order is null || DeathCount(ea.BasicProperties, "orders.work") >= MaxAttempts)
{
var props = new BasicProperties(ea.BasicProperties) { Persistent = true };
await parking.BasicPublishAsync("", "orders.parking", mandatory: true,
basicProperties: props, body: ea.Body.ToArray());
await channel.BasicAckAsync(ea.DeliveryTag, multiple: false);
return;
}
try
{
await using var scope = scopes.CreateAsyncScope();
var handler = scope.ServiceProvider.GetRequiredService<OrderPlacedHandler>();
await handler.HandleAsync(id, order, stoppingToken);
await channel.BasicAckAsync(ea.DeliveryTag, multiple: false);
}
catch (Exception)
{
await channel.BasicNackAsync(ea.DeliveryTag, multiple: false, requeue: false);
}
};
await channel.BasicConsumeAsync("orders.work", autoAck: false, consumer: consumer);
await Task.Delay(Timeout.Infinite, stoppingToken);
static long DeathCount(IReadOnlyBasicProperties props, string queue)
{
if (props.Headers is null || !props.Headers.TryGetValue("x-death", out var raw)
|| raw is not IList<object> deaths)
return 0;
long total = 0;
foreach (var d in deaths.OfType<IDictionary<string, object>>())
if (d.TryGetValue("queue", out var q) && q is byte[] name && Encoding.UTF8.GetString(name) == queue)
total += Convert.ToInt64(d["count"]);
return total;
}
Parking'e yayımlama, publisher confirm'li ayrı bir channel'da yapılır; böylece orijinal mesaj ancak broker kopyayı onayladıktan sonra ack'lenir. Süreç bu ikisi arasında başarısız olursa mesaj iki kez park edilir; bu zararsızdır. scopes, inject edilmiş IServiceScopeFactory nesnesidir. DeathCount iş kuyruğu için tüm nedenleri toplar; böylece delivery-limit sınırına takılarak bekleme kuyruğuna ulaşan bir mesaj da sayılır. Gövde ToArray ile kopyalanır, çünkü istemci dokümantasyonuna göre Body belleğini kopyalamadan ReceivedAsync dışında kullanmak güvenli değildir.
MaxAttempts = 5 ile sürekli başarısız olan bir mesaj beş kez işlenir ve altıncı teslimatta çalıştırılmadan park edilir. Parking kuyruğunun uzunluğuna bir alarm koyun; kimsenin kontrol etmediği bir parking kuyruğu pratikte mesajları sessizce atmakla aynıdır.
Outbox: veritabanına kaydetmek ve mesaj yayımlamak birlikte
Bir servisin bir fatura kaydettiğini ve ayrıca bir InvoiceCreated olayı yayımlaması gerektiğini varsayalım. SQL Server ve RabbitMQ ortak bir transaction paylaşmaz. Önce kaydederseniz ve süreç yayımlamadan önce başarısız olursa fatura olaysız kalır; önce yayımlarsanız ve kayıt başarısız olursa var olmayan bir veri hakkında bir olay duyurmuş olursunuz.
Transactional outbox deseninde mesaj doğrudan yayımlanmaz; ana değişiklikle aynı transaction içinde bir outbox tablosuna yazılır. OrderPlacedHandler sınıfının OutboxMessages.Add ile yaptığı da tam olarak buydu. Genellikle relay olarak adlandırılan ayrı bir süreç gönderilmemiş satırları okur, publisher confirm'lerle yayımlar ve ardından gönderildi olarak işaretler. Böylece bir mesaj ancak transaction'ı commit edildikten sonra yayımlanır. Aşağıdaki kod, ayrı bir BackgroundService içindeki ExecuteAsync metodunun gövdesidir:
await using var channel = await connection.CreateChannelAsync(
new CreateChannelOptions(publisherConfirmationsEnabled: true, publisherConfirmationTrackingEnabled: true),
stoppingToken);
while (!stoppingToken.IsCancellationRequested)
{
await using var scope = scopes.CreateAsyncScope();
var db = scope.ServiceProvider.GetRequiredService<BillingDbContext>();
var batch = await db.OutboxMessages
.Where(m => m.SentAt == null)
.OrderBy(m => m.CreatedAt)
.Take(100)
.ToListAsync(stoppingToken);
foreach (var m in batch)
{
var props = new BasicProperties
{
MessageId = m.Id.ToString(),
Type = m.Type,
ContentType = "application/json",
Persistent = true
};
await channel.BasicPublishAsync("billing", m.Type, mandatory: true,
basicProperties: props, body: m.Payload, cancellationToken: stoppingToken);
m.SentAt = DateTime.UtcNow;
await db.SaveChangesAsync(stoppingToken);
}
if (batch.Count == 0)
await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
}
Burada connection ve scopes DI'dan gelir. Bu relay her mesajı tek tek yayımlar ve confirm'ini bekler. Resmî RabbitMQ tutorial'ı batch'ler hâlinde yayımlamanın daha hızlı olduğunu gösteriyor, ama bir şeyler ters gittiğinde hangi mesajın ulaşmadığını anlamak daha zordur. Outbox satırının ID'si MessageId olarak kullanılır; relay yayımladıktan sonra ve SaveChangesAsync çağrısından önce durursa, yeniden başladıktan sonra aynı mesajı tekrar gönderir ve consumer işlenmiş mesajlar tablosu sayesinde ikinci kopyayı atlar. Bir outbox'ın idempotent bir consumer olmadan eksik kalmasının nedeni budur. Gerçek kodda PublishException ve bağlantı hatalarını yakalayın, loglayın ve kısa bir aradan sonra devam edin.
Servisin birkaç instance'ı çalışıyorsa ya relay'i yalnızca biri çalıştırır ya da satırlar okunurken kilitlenir. SQL Server'daki yaygın yaklaşım, bir transaction içinde UPDLOCK ve READPAST hint'leriyle okumaktır; SQL Server dokümantasyonunun kendisi READPAST'i esas olarak bir tablo üzerinde iş kuyrukları uygulamanın bir yolu olarak sunar. EF Core'da bu hint'lerin raw SQL ile (FromSql) yazılması gerekir ve READ_COMMITTED_SNAPSHOT açıksa READ COMMITTED düzeyinde READPAST yalnızca READCOMMITTEDLOCK ile birlikte kabul edilir.
Gönderilmemiş satırlar üzerindeki filtrelenmiş bir indeks relay'in sorgusunu ucuz tutar (bkz. SQL Server performansı rehberi); gönderilmiş satırların da periyodik olarak silinmesi ya da arşivlenmesi gerekir. Bu parçaları kendiniz kurmak istemiyorsanız MassTransit ve NServiceBus yeniden denemeleri, outbox'ı ve mükerrer mesaj tespitini hazır olarak sunar; lisans koşulları farklıdır ve seçmeden önce bunları kontrol etmelisiniz.
Production öncesi kontrol listesi
Gozine2'de RabbitMQ, .NET 6, SQL Server ve Redis'in yanında yığınımızın bir parçasıydı. Aşağıdaki liste bu rehberi özetliyor; yeni bir consumer'ı devreye almadan önce üzerinden geçebilirsiniz:
- Bir quorum kuyruğu ya da en azından durable bir kuyruk;
Persistent = trueolan mesajlar; önemli mesajlar için publisher confirm'ler vemandatory: true. autoAck: false, yalnızca commit'ten sonra ack ve sınırlı bir prefetch.- Ack timeout'undan (varsayılan 30 dakika) önce biten işleme.
- Producer tarafından ayarlanan, değişmeyen bir
MessageIdve aynı transaction içinde yazılan bir işlenmiş mesajlar tablosu. - Gecikmeli yeniden denemeler, denemeler için bir üst sınır ve uzunluğuna alarm konmuş bir parking kuyruğu.
delivery-limitmesajları sessizce atmasın diye her quorum kuyruğu için bir DLX.- Kaydetme ve yayımlamanın birlikte olması gereken her yerde bir outbox.
Her sistemin bir broker'a ihtiyacı yoktur. Mesajların bir süreç yeniden başlatmasından sağ çıkması gerekmiyorsa süreç içi bir kuyruk daha basittir; izlenme süresi pipeline'ında 202 ve 503 yanıtlarıyla sınırlı, bellek içi bir Channel kullandım. Böyle bir yolu tasarlamak ya da gözden geçirmek için yardım istiyorsanız Benimle çalışın sayfası nasıl yardımcı olabileceğimi anlatıyor.
Sık sorulan sorular
RabbitMQ exactly-once teslimatı garanti eder mi?
Hayır. Durable kuyruklar ve persistent mesajlar, publisher confirm'ler ve manuel ack'ler size at-least-once teslimat sağlar; mükerrer mesajlar yine de gelebilir. Exactly-once bir etkinin uygulamanın kendisinde kurulması gerekir; örneğin ana değişiklikle aynı transaction içinde yazılan, işlenmiş MessageId'lerden oluşan bir tabloyla.
autoAck mı, manuel ack mı?
autoAck ile bir mesaj gönderildiği anda teslim edilmiş sayılır ve consumer işlemenin ortasında başarısız olursa mesaj kaybolur. Önemli mesajlar için ack'i sonuç kaydedildikten sonra manuel olarak gönderin ve onaylanmamış mesajların sayısını BasicQosAsync ile sınırlayın.
Neden requeue: true ile yeniden denememeli?
Kuyruğa geri konan mesaj hemen yeniden teslim edilebilir ve hatanın nedeni düzelene kadar pahalı bir teslim ve geri koyma döngüsü yaşarsınız. Mesajı requeue: false ile TTL'li bir bekleme kuyruğuna reddetmek ya da RabbitMQ 4.3 ve sonrasında, geri dönen bir mesajı yeniden teslim etmeden önce bekleten quorum kuyruğunun kendi gecikmeli yeniden deneme özelliğini etkinleştirmek daha iyidir.
Quorum kuyruğunda delivery-limit ne işe yarar?
RabbitMQ 4.0'dan beri varsayılan değeri 20'dir ve teslimatı bundan fazla kez başarısız olan bir mesaj, bir DLX yapılandırılmışsa dead-letter edilir, yapılandırılmamışsa atılır. 4.3'ten beri yalnızca basic.reject ya da kopan bir bağlantı gibi gerçek başarısızlıklar sayılır, basic.nack sayılmaz. Dokümantasyonun her quorum kuyruğu için bir dead-letter yapılandırması önermesinin nedeni budur.
Outbox deseni hangi sorunu çözer?
Hem veritabanına veri kaydetmeniz hem de bir mesaj yayımlamanız gerektiğinde outbox, mesajı aynı transaction içinde bir tabloya yazar ve ayrı bir süreç onu yayımlar. Mesaj yalnızca transaction commit edilirse yayımlanır, ama birden fazla kez gelebilir; bu yüzden consumer'ın idempotent olması gerekir.
Mahdi Molavi