Redis ile güvenilir bir iş kuyruğu, bekleyen iş kimliklerini bir listede tutar ve her işçi bir işi BLMOVE ile tek atomik adımda bekleyenler listesinden "işleniyor" listesine taşır. İşin ayrıntıları (yük, durum, deneme sayısı, sonuç) bir hash'te durur; iş bitince kimlik işleniyor listesinden LREM ile çıkarılır ve hash'e süre konur. İşçi yarıda çökerse iş işleniyor listesinde kalır ve periyodik çalışan bir geri alma (reclaim) adımı, görünürlük zaman aşımını geçen işleri yeniden bekleyenler listesine koyar. Sonuç, en az bir kez teslim garantisi veren bir kuyruktur.
Bu yazı Redis belgelerindeki "Redis job queue" kullanım senaryosunu ve onun redis-py, node-redis ve Predis örneklerini temel alır. Liste komutlarının kendisi list komutları yazısında, hash komutları hash yazısında, tüketici gruplu stream'ler ise consumer group yazısında anlatılır; burada bu parçaların bir kuyrukta nasıl birleştiğine odaklanıyoruz.
İş kuyruğu neden gerekir, sorun neden zor?
E-posta göndermek, ödeme işlemek, görsel dönüştürmek veya webhook çağırmak gibi işleri kullanıcı isteğinin içinde eşzamanlı yapmak, yük altında istekleri bekletir ve gecikmeyi artırır. Belge ayrıca tek bir kullanıcı eyleminin çoğu zaman birden çok aşağı akış işini tetiklediğini, ortak bir kuyruk olmadan her servisin diğerine noktadan noktaya bağlanmak zorunda kaldığını ve bunun ölçek büyüdükçe kırılganlaştığını anlatır.
Belgenin saydığı kolay çözümlerin her birinin bir bedeli vardır. Süreç içi basit kuyruklar çökmede işleri kaybeder ve işi birden çok işçiye dağıtamaz. Veritabanında bekleyen satırları yoklamak ana veritabanına sürekli yük bindirir ve aynı satır için yarışan işçiler arasında devir yarışları yaratır. Ayrı bir mesaj aracısı (RabbitMQ, Kafka, SQS) eklemek ise kurulacak, izlenecek ve parası ödenecek bir altyapı parçası daha demektir. Belgeye göre işe yarar bir kuyruğun üç şeye ihtiyacı vardır: en az bir kez teslim, kuyruktan işçiye atomik devir ve yarıda çöken işçilerin işlerini geri almanın bir yolu. Bunlara ek olarak iş başına durum, deneme sayısı ve sonuç kayıtlarını sonsuza kadar birikmeden tutabilmek gerekir.
Veri modeli
Belgedeki örnek her işi rastgele onaltılık bir kimlikle tanımlar ve dört liste ile bir hash kullanır:
queue:jobs:pending (list) bekleyen is kimlikleri, en eskisi sagda
queue:jobs:processing (list) alinmis ama tamamlanmamis isler
queue:jobs:completed (list) son basarili isler (LTRIM ile sinirli gecmis)
queue:jobs:failed (list) kalici olarak basarisiz isler
queue:jobs:job:{id} (hash) is basina meta veri
queue:jobs:events (pubsub) tamamlanma bildirimleriHash; kimliği, JSON olarak saklanan yükü, durumu (pending, processing, completed, failed), deneme sayısını, kuyruğa giriş, alınma ve tamamlanma zamanlarını, her alımda yeniden üretilen claim_token değerini, sonucu ve son hata mesajını taşır. Yükün JSON olarak saklanması, her alanı hash alanına bölmek zorunda kalmadan iç içe yapılar taşıyabilmek içindir. Anahtarların hepsinin queue:jobs: önekiyle başlaması da bilinçli bir tercihtir; belge her kuyruk için açıkça ayrılmış bir önek kullanmayı, böylece kuyruğu incelemenin ve gerektiğinde uygulama verisine dokunmadan temizlemenin kolaylaşacağını söyler.
Akış adım adım

- Uygulama işi kuyruğa koyar: yardımcı sınıf meta veri hash'ini yazar ve iş kimliğini
LPUSHile bekleyenler listesinin soluna ekler. - İşçi iş ister:
BLMOVEbekleyenler listesinin sağından bir kimlik alır ve atomik olarak işleniyor listesinin soluna koyar; ardından hash'e yeni birclaim_tokenve alınma zamanı yazılır, deneme sayısı bir artırılır. - İşçi işi çalıştırır ve sonuca göre tamamla veya başarısız say adımını çağırır.
- Tamamlama, kimliği işleniyor listesinden çıkarır, sonucu yazar, kimliği tamamlananlar geçmişine ekler; geçmiş
LTRIMile kısaltılır, hash'eEXPIREkonur. - Başarısızlıkta deneme hakkı kaldıysa iş bekleyenlere geri döner, kalmadıysa başarısızlar listesine taşınır.
- Periyodik geri alma, işleniyor listesinde alınma zamanı görünürlük zaman aşımından eski olan işleri bekleyenlere geri koyar.
Bu tasarımın ana fikri LMOVE komut sayfasındaki "güvenilir kuyruk" deseninden gelir. Sayfa, üreticinin listeye ekleyip tüketicinin RPOP veya BRPOP ile çektiği basit kuyruğun güvenilir olmadığını, çünkü tüketici mesajı aldıktan hemen sonra ama işlemeden önce çökerse mesajın kaybolacağını anlatır. LMOVE veya engelleyici sürümü BLMOVE mesajı alırken aynı anda bir işleniyor listesine koyar; tüketici işi bitirince LREM ile kaldırır, başka bir istemci de bu listede çok uzun kalan öğeleri yeniden kuyruğa iter.
LMOVE ve BLMOVE nasıl davranır?

LMOVE, kaynak listenin bir ucundan öğe alıp hedef listenin bir ucuna koyar ve taşınan öğeyi döndürür. Komut sayfasındaki örnek iki yönü de gösterir:
> RPUSH mylist "one"
(integer) 1
> RPUSH mylist "two"
(integer) 2
> RPUSH mylist "three"
(integer) 3
> LMOVE mylist myotherlist RIGHT LEFT
"three"
> LMOVE mylist myotherlist LEFT RIGHT
"one"
> LRANGE mylist 0 -1
1) "two"
> LRANGE myotherlist 0 -1
1) "three"
2) "one"BLMOVE aynı işi yapar, ancak kaynak liste boşsa bağlantıyı başka bir istemci listeye bir öğe ekleyene kadar veya zaman aşımı dolana kadar bekletir. Zaman aşımı saniye cinsinden bir ondalık sayıdır ve 0 sonsuza kadar bekleme anlamına gelir. Zaman aşımı dolarsa yanıt nil olur. MULTI/EXEC bloğu veya Lua betiği içinde BLMOVE engellemez, LMOVE gibi davranır. Komut, artık kullanımdan kaldırılmış BRPOPLPUSH'un yerini alır ve BLMOVE ... RIGHT LEFT ona eşdeğerdir. Belgelere göre BLMOVE Redis 6.2 veya sonrasını gerektirir; daha eski sunucularda BRPOPLPUSH kullanılır.
LREM ise belirli bir değere eşit öğeleri siler: sayı pozitifse baştan sona, negatifse sondan başa doğru o kadar öğe, 0 ise eşleşen tüm öğeler silinir; dönüş değeri silinen öğe sayısıdır. Kuyrukta her iş kimliği benzersiz olduğu için işleniyor listesinden tek bir öğe çıkarılır.
redis-py ile işi almak
Belgedeki Python örneğinin claim yöntemi akışın kalbidir. İşçi blmove ile bekler, bir kimlik gelirse yeni bir sahiplik belirteci üretir ve hash güncellemesini, deneme sayacını ve hash'in okunmasını tek bir pipeline içinde gönderir:
def claim(self, timeout_ms: int = 1000) -> Optional[ClaimedJob]:
timeout_s = max(timeout_ms / 1000.0, 0.1)
job_id = self.redis.blmove(self.pending_key, self.processing_key, timeout_s, src="RIGHT", dest="LEFT")
if job_id is None:
return None
token = secrets.token_hex(8)
now_ms = self._now_ms()
meta_key = self._meta_key(job_id)
pipe = self.redis.pipeline()
pipe.hset(meta_key, mapping={
"status": "processing",
"claimed_at_ms": now_ms,
"claim_token": token,
})
pipe.hincrby(meta_key, "attempts", 1)
pipe.hgetall(meta_key)
_, _, meta = pipe.execute()
return ClaimedJob(job_id, json.loads(meta["payload"]), int(meta["attempts"]), token)claim_token işçinin bu deneme için sahiplik kanıtıdır. Sonraki her durum değişikliği (tamamlama, başarısızlık) işleniyor listesine dokunmadan önce bu belirteci kontrol eder. Böylece görünürlük zaman aşımını aşacak kadar takılıp sonra uyanan bir işçi, işi o arada almış olan yeni işçinin işine karışamaz. Bu fikir dağıtık kilit yazısındaki rastgele değerle kilidi imzalama mantığının aynısıdır.
Pipeline kullanımı ağ gidiş-dönüşlerini azaltmak içindir; pipeline'ın ne yaptığı ve ne yapmadığı pipelining yazısında anlatılır. Kuyruğa ekleme adımı da aynı yöntemi kullanır: meta veri hash'i ve LPUSH tek pipeline'da gönderilir.
Tamamlama, başarısızlık ve geri alma neden Lua ile?
Tamamlama birden çok adımdan oluşur: belirteci kontrol et, kimliği işleniyor listesinden çıkar, sonucu ve durumu yaz, geçmişe ekle, geçmişi kısalt, hash'e süre koy. Bu adımlar ayrı komutlar olarak gönderilirse araya geri alma işlemi girebilir. Belgedeki örnek bu yüzden tamamlama, başarısızlık ve geri alma akışlarını Lua betikleriyle (EVALSHA ile) çalıştırır; böylece her akış işleniyor listesi ve meta veri hash'i üzerinde atomik çalışır. Tamamlama betiği önce belirteci kontrol eder ve işçi artık işin sahibi değilse 0 döndürür. Lua betiklerinin atomikliği ve EVALSHA için Lua script yazısına bakın.
Başarısızlıkta deneme sayısı her alımda artırıldığı için üç kez başarısız olan bir iş başarısızlar listesine attempts = 3 ve son hata mesajıyla taşınır. Geri alma betiği ayrıca dar bir yarışı da ele alır: BLMOVE ile alınma zamanını yazma arasında çöken bir işçinin işi henüz alınma zamanına sahip değildir. Belgedeki betik bu işleri, kuyruğa giriş zamanını yedek sayaç olarak kullanıp görünürlük süresinin iki katından sonra geri alır; böylece hiçbir iş sonsuza kadar asılı kalmaz.
İş tamamlandığında yardımcı sınıf queue:jobs:events kanalına PUBLISH ile bir bildirim gönderir; işi gönderen taraf yoklama yapmadan haberdar olabilir. Pub/Sub'ın en fazla bir kez teslim ettiğini, yani dinleyen yokken gönderilen bildirimin kaybolacağını unutmayın; bildirim burada yalnızca bir kolaylıktır, kuyruğun güvenilirliği listelerden gelir. Ayrıntılar Pub/Sub yazısında.
Node.js ve PHP örneklerindeki farklar
Node.js ve PHP örnekleri aynı tasarımı uygular, ancak belgeler istemciye özgü birkaç noktayı vurgular. node-redis sayfasına göre BRPOPLPUSH node-redis v5'te kaldırıldı; onun yerine BLMOVE kullanılmalıdır. Aynı sayfa node-redis'in komutları tek bir TCP bağlantısı üzerinden kendiliğinden pipeline'ladığını, bu yüzden çoğu iş yükü için süreç başına tek bir istemci oluşturup her yerde onu kullanmayı önerir. İkinci bir bağlantıya yalnızca uzun zaman aşımlı bir BLMOVE veya SUBSCRIBE gibi engelleyici bir çağrıyı ayırmak için ihtiyaç duyulur; örnekteki kısa 500 milisaniyelik zaman aşımıyla tek istemci yeterlidir. Node.js tek iş parçacıklı olduğu için örnekteki her "işçi" bir işletim sistemi iş parçacığı değil, olay döngüsünde çalışan eşzamansız bir döngüdür.
Predis sayfası ise Predis'in BLMOVE'u doğrudan sunduğunu ve kesirli saniye zaman aşımı kabul ettiğini, bu yüzden örneğin zaman aşımını tam saniyeye yuvarlamak yerine en az 0,1 saniye tuttuğunu belirtir. PHP örneği geri alma döngüsünü systemd altında çalışan küçük bir sonsuz döngü olarak önerir. İstemci kütüphanelerinin kendisi için redis-py, node-redis ve Predis yazılarına bakın.
Üretimde dikkat edilecekler

Görünürlük zaman aşımını en kötü iş süresine göre seçin
Görünürlük zaman aşımı en uzun gerçek iş süresinden, pay bırakarak, uzun olmalıdır. Çok kısa seçilirse yavaş bir işi çalıştıran sağlıklı bir işçinin işi geri alma devreye girdiğinde ikinci kez çalıştırılır. Çok uzun seçilirse gerçek bir çökmenin fark edilmesi gecikir. Belge çoğu üretim kurulumunun kuyruk başına, iş gecikmesinin 99. yüzdeliğine göre ayarlanmış bir değer kullandığını ve örnek olarak e-posta için 2 dakika, video dönüştürme için 30 dakika verdiğini aktarır.
Geri almayı bir zamanlayıcıyla çalıştırın
Belgedeki demo geri almayı yalnızca düğmeye basıldığında yapar. Üretimde geri alma, hızlı kuyruklar için birkaç saniyede bir, yavaş kuyruklar için dakikada bir çalışan periyodik bir görevden veya her işçinin iş beklemeden önce çağırdığı bir adımdan yürütülmelidir. Belgenin vurguladığı gibi iki yol da işe yarar; yeter ki biri bu taramayı çalıştırsın.
Geçmişi sınırlayın ve deneme sayısını işe göre ayarlayın
Örnek son 50 tamamlanan ve 50 başarısız iş kimliğini LTRIM ile tutar. Denetim için daha uzun geçmiş gerekiyorsa belge tamamlanma olaylarını ayrı bir Redis Stream'e veya harici bir depoya yazmayı, kuyruk içi geçmişi kısa tutmayı önerir. Varsayılan max_attempts = 3 ağ zaman aşımı gibi geçici hatalar için makuldür; ancak bir ödeme çekimi gibi eş etkili (idempotent) olmayan dış sistemlerle konuşan işler ya uygulama düzeyinde eş etkililik anahtarları ya da çok daha düşük bir deneme sayısı gerektirir. En az bir kez teslimin doğal sonucu budur: bir iş nadiren de olsa iki kez çalışabilir.
Kuyruğu redis-cli ile incelemek
Kuyruk yalnızca listeler ve hash'lerden oluştuğu için durumunu doğrudan redis-cli ile görebilirsiniz. Belgedeki komutlar:
# Kac bekleyen is var?
redis-cli LLEN queue:jobs:pending
# Siradaki 5 isi gorun
redis-cli LRANGE queue:jobs:pending -5 -1
# Bir isin meta verisini okuyun
redis-cli HGETALL queue:jobs:job:9a4f0d1c
# Su anda kac is isleniyor?
redis-cli LLEN queue:jobs:processingLRANGE ... -5 -1 listenin sağ ucundaki son beş öğeyi verir; işler sağdan alındığı için bunlar sıradaki işlerdir. Belge aynı bölümde kuyruğun tüm anahtarlarını --scan --pattern ile bulup silen bir komut da gösterir ve bunun işleri sileceği konusunda uyarır.
Öncelik, gecikmeli iş ve fan-out
Belge, FIFO ve LIFO kuyruklarının yanında öncelikli ve gecikmeli kuyrukların da çekirdek Redis veri yapılarıyla kurulabileceğini söyler. Gecikmeli çalıştırma ve öncelik için sorted set önerilir: puan olarak çalışma zamanı damgası veya öncelik kullanılır ve hazır işler ZRANGEBYSCORE ile (ya da ZRANGE ... BYSCORE ile) okunur. Bir taslak olarak, çalışma zamanı milisaniye cinsinden puan olacak şekilde işi ekler, zamanı gelenleri okur ve ZREM ile kendinize alırsınız:
ZADD queue:jobs:delayed 1715441060000 9a4f0d1c
ZRANGE queue:jobs:delayed -inf 1715441000000 BYSCORE LIMIT 0 10
ZREM queue:jobs:delayed 9a4f0d1cZREM gerçekten silinen üye sayısını döndürdüğü için aynı kimliği iki işçi aynı anda almaya çalışırsa yalnızca 1 yanıtı alan işçi işi bekleyenler listesine aktarmalıdır; 0 alan işçi işi başkasının aldığını anlar. Sorted set komutlarının ayrıntısı için sorted set yazısına bakın. Tek bir kullanıcı eylemini birbirinden bağımsız ilerleyen birden çok işçi havuzuna dağıtmak istiyorsanız belge Stream'leri ve tüketici gruplarını önerir; bu durumda her grup kendi ilerlemesini tutar.
Son olarak hazır çözümleri de hatırlayın. Belge Redis üzerinde güvenilir kuyruk desenlerini uygulayan kütüphaneleri sayar: Python için Celery, RQ ve Dramatiq; Ruby için Sidekiq ve Resque; Node.js için Bull ve BullMQ; Go için Asynq; Java için Redisson ve Spring Batch; .NET için Hangfire. Kendi kuyruğunuzu yazmak, bu desenleri anlamanın en iyi yoludur; üretimde ise olgun bir kütüphane çoğu zaman daha az sürpriz demektir.
Sık Sorulan Sorular
Basit LPUSH ve BRPOP neden yetmez?
BRPOP öğeyi listeden çıkarır; işçi öğeyi aldıktan sonra ama işi bitirmeden çökerse iş kaybolur. BLMOVE ise öğeyi işleniyor listesine taşıdığı için çöken işçinin işi orada kalır ve geri alınabilir.
Bir iş iki kez çalışabilir mi?
Evet. Tasarım en az bir kez teslim sağlar. Takılan bir işçinin işi geri alınıp başka işçiye verildiğinde ilk işçi de işi bitirmiş olabilir; belirteç kontrolü yalnızca durum güncellemelerinin karışmasını engeller, dış dünyadaki yan etkileri değil. Bu yüzden işlerinizi mümkün olduğunca eş etkili tasarlayın.
List mi Stream mi kullanmalıyım?
Tek bir işçi havuzunun paylaştığı basit bir kuyruk için listeler yeterlidir ve belgedeki örnek bunu kullanır. Aynı olayı birden çok bağımsız işçi grubuna dağıtmak, daha zengin geçmiş tutmak veya bekleyen mesajları tüketici bazında izlemek istiyorsanız Streams ve tüketici grupları daha uygun olur.
BLMOVE hangi sürümü gerektirir?
Belgelere göre Redis 6.2 veya sonrası. Daha eski sunucularda aynı desen BRPOPLPUSH ile kurulur; ancak bu komut kullanımdan kaldırılmıştır ve node-redis v5 onu artık sunmaz.
Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.
İlgili içerikler: Redis öğrenme rehberi · List komutları · Streams consumer group · Redis Lua script · Redis dağıtık kilit · redis-py kullanımı