Powrót do strony głównej

Redis kolejka Lua: kolejność partycji i pula workerów

Artykuł opisuje implementację kolejki zadań na Redis z skryptami Lua dla gwarancji porządku w partycjach przy wspólnej puli workerów. Obsługa odroczonych wiadomości, retry, benchmarki wydajności. Nadaje się dla wysokoobciążonych serwisów bez Kafka.

Skrypty Lua w Redis: kolejka z kolejnością i opóźnieniem
Advertisement 728x90

Kolejka Redis w Lua: Gwarantowany porządek w partycjach ze wspólną pulą workerów

W systemach o wysokim obciążeniu przetwarzanie wiadomości wymaga ścisłej kolejności wewnątrz logicznych partycji przy setkach tysięcy kluczy. Ograniczona pula workerów musi równomiernie rozkładać obciążenie, bez blokowania przez ciężkie partycje. Obsługa ponownych prób i zadań odroczonych jest realizowana przez Redis ze skryptami Lua w bibliotece smart-redis-queue.

Rozwiązanie wykorzystuje atomowość Lua do operacji na ZSET, HASH i STRING bez wyścigów. Klucze są tworzone z prefiksem queue:{queueName}:. Partycje uporządkowane (kod z prefiksem !) są przetwarzane wyłącznie z blokadą, zwykłe — równolegle.

Porównanie z Kafka i RabbitMQ

Kafka zapewnia porządek w partycjach tematów i skalowanie grup konsumentów, ale ustala przypisanie partycji do konsumentów. Dynamiczna pula workerów, pobierająca zadania z dowolnych gotowych partycji, wymaga innej semantyki. TTL w Kafka nie zastępuje precyzyjnego odłożenia do czasu T.

Google AdInline article slot

RabbitMQ obsługuje TTL i DLQ, ale ścisła kolejność przy wielu konsumentach jest naruszana przez round-robin. Oddzielne kolejki na miliony kluczy nie skalują się.

Redis + Lua daje lekki wariant: jedno round-trip na operację atomową, bez klastra i skomplikowanego monitoringu.

Struktura kluczy

| Klucz | Struktura | Przeznaczenie |

Google AdInline article slot

|------|-----------|------------|

| queue:{name}:partitions | SET | Aktywne partycje |

| queue:{name}:partition:{code}:{priority} | ZSET | Kolejka zadań według priorytetu, score = czas dostępności (ms) |

Google AdInline article slot

| queue:{name}:partition:{code}:priorities | ZSET | Poziomy priorytetów |

| queue:{name}:payload:{taskId} | STRING | Ładunek |

| queue:{name}:partition:{taskId} | STRING | Kod partycji |

| queue:{name}:consumer:{id}:tasks | HASH | Zadania w trakcie przetwarzania |

| queue:{name}:partition:{code}:lock | STRING | Blokada partycji uporządkowanej |

| queue:{name}:partition:{code}:block | STRING (TTL) | Tymczasowa blokada (rate limit) |

Cykl życia zadania

  • Publish: Idempotentne dodanie z SET NX, zapis do ZSET z score = scheduled (ms). Partycja dodawana do SET.
local ok = redis.call('SET', payloadKey, payload, 'NX')
if ok then
    redis.call('ZADD', partitionQueueKey, scheduled, taskId)
    redis.call('SADD', partitionsKey, partitionCode)
end
  • Get: Heartbeat, iteracja po partycjach, sprawdzenie lock/block dla uporządkowanych. Pobieranie według malejących priorytetów (ZREVRANGE), głowa ZSET jeśli scheduled <= now. Przejście do HASH konsumenta.
  • Ack: Usunięcie z HASH, payload, kluczy. Zdjęcie lock jeśli licznik uporządkowanych = 0.
  • Reject: Zwrot do ZSET z nowym score. Dla uporządkowanych — priorytet +1, reject_seq dla tie-break. Consume odrzuca całą partię partycji uporządkowanej przy reject pierwszego zadania.

Benchmarki wydajności

Testy na Go 1.22+, Redis 5, Intel i7-13700H.

Publikacja (zadań/s):

  • 1 zadanie: 17–23 tys.
  • Partia 10: 80–92 tys.
  • Partia 100: 100–117 tys.

Partiowanie zwiększa przepustowość dzięki jednemu wywołaniu Lua.

Pobieranie przez jednego workera (zadań/s):

| Prefetch | ns/op | Zadania/s |

|----------|--------|---------|

| 1 | 80–103 µs | 9.7–12.5 tys. |

| 5 | 57–67 µs | 15–17.5 tys. |

Równolegle:

  • 2 workery: 28–30 tys./s
  • 8 workerów: 23–26 tys./s

Skalowanie nieliniowe z powodu konkurencji w Redis.

Obsługa awarii

  • Heartbeat z TTL ~120 s.
  • Żywy konsument skanuje martwe przez distributed lock.
  • Zadania z HASH martwego wracają do kolejki (uporządkowane z +priorytetem).

Rate limit: RejectWithDelay ustawia block z TTL.

Rekomendacja: krok priorytetów 100+ dla uniknięcia kolizji z inkrementami reject.

Co jest ważne

  • Atomowość Lua zapewnia brak wyścigów przy przenoszeniu zadań między ZSET i HASH.
  • Partycje uporządkowane zachowują kolejność przez wyłączny lock i logikę reject.
  • Partiowanie w Publish/Get podnosi przepustowość do 100+ tys. zadań/s.
  • Automatyczne odzyskiwanie po upadku workerów bez utraty zadań.
  • Nadaje się do scenariuszy z milionami partycji bez overhead klastra.

— Editorial Team

Advertisement 728x90

Czytaj dalej