IlmHamroh
JavaScript Full-stack/26-qism. SQL va PostgreSQL55/64-dars19 daqiqa
Mundarija (31)

PostgreSQL navbat: FOR UPDATE SKIP LOCKED bilan Redis'siz fon vazifalari

Qisqacha: Fon vazifalari (eslatma yuborish, email, hisobot) uchun alohida navbat serveri shart emas — kichik va o'rta loyihada Postgres jadvalining o'zi navbat bo'la oladi. Kalit — SELECT … FOR UPDATE SKIP LOCKED: har ishchi bitta bo'sh ishni qulflab oladi, band ishlarni esa kutmasdan o'tkazib yuboradi. Kanon o'lchovi (8 ishchi, 2 000 ish): SKIP LOCKED — 460 ish/s va 0 takror; SKIP'siz FOR UPDATE — 68 ish/s; qulfsiz SELECT — 3 101 ish ikki marta bajarildi.

Bu darsda

  • Fon vazifalari navbati nima uchun kerakligini va uni bazada qanday saqlashni bilasiz.
  • FOR UPDATE SKIP LOCKED bilan bir nechta ishchi bir-birini kutmasdan va takrorlamasdan ishlashini ko'rsatasiz.
  • Ikki dizaynni — "tranzaksiya ichida ishlash" va "band qilib olish + qorovul" — solishtirasiz.
  • Qayta urinish (urinish, keyingi_urinish, exponential backoff) va "xato" holatini qurasiz.
  • pg-boss, graphile-worker va Redis/BullMQ'ga qachon o'tish haqida qaror qila olasiz.

Oldin bilishingiz kerak: Qulflar: row, table va advisory, Deadlock va serialization failure bilan ishlash, LISTEN/NOTIFY.

1. Nega bu kerak?

vazifalar ilovasiga yangi imkoniyat qo'shmoqchimiz: muddati yaqinlashgan vazifa uchun foydalanuvchiga Telegram orqali eslatma yuborish. Telegram'ga so'rov 200 ms dan 3 soniyagacha davom etadi, ba'zan xato beradi (429 Too Many Requests). Buni HTTP so'rov ichida qilsak, foydalanuvchi kutib qoladi; xato bo'lsa — eslatma yo'qoladi.

Yechim — fon vazifasi (background job): "eslatma yuborish kerak" degan yozuvni navbatga qo'yamiz va darhol javob qaytaramiz. Alohida jarayon — ishchi (worker) — navbatdan ishlarni olib, bajaradi, xato bo'lsa keyinroq qayta urinadi.

Navbat qayerda turadi? Ko'p qo'llanmalar darhol Redis va BullMQ ni taklif qiladi. Lekin bizda allaqachon Postgres bor. Uning afzalligi katta: ishni navbatga qo'yish asosiy ma'lumot bilan bitta tranzaksiyada bo'ladi. Vazifa saqlandi — eslatma ishi ham saqlandi; tranzaksiya bekor bo'ldi — ikkalasi ham yo'q. Alohida navbat serverida bu kafolatni olish ancha murakkab.

Hayotiy o'xshatish: oshxonadagi buyurtmalar taxtasi. Ofitsiant buyurtmani taxtaga ilib qo'yadi. Har oshpaz taxtadan birinchi hech kim olmagan varaqni oladi, ustiga ismini yozadi va tayyorlaydi. Boshqa oshpaz qo'li band varaqni kutib turmaydi — keyingisini oladi. Bu — SKIP LOCKED.

2. Muammo: ishchilar bir-biriga xalaqit beradi

2.1 Ish jadvali

Eng sodda navbat — oddiy jadval:

sql
CREATE TABLE ish (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  tur text NOT NULL,
  payload jsonb NOT NULL,
  holat text NOT NULL DEFAULT 'kutmoqda'
);
INSERT INTO ish (tur, payload)
SELECT 'eslatma', jsonb_build_object('vazifa_id', g)
FROM generate_series(1, 5) g;

payload — ishning ma'lumoti (qaysi vazifa, kimga). jsonb — har tur o'z maydonlarini saqlay oladi (Ma'lumot turlari).

2.2 Uch xil "ish olish"

Ishchi sikli: tranzaksiya ochadi → bitta ishni oladi → bajaradi → holat = 'bajarildi' → COMMIT. Savol — ishni qanday olish:

  1. Qulfsiz: SELECT id … WHERE holat = 'kutmoqda' ORDER BY id LIMIT 1. Ikki ishchi bir vaqtda so'rasa, ikkalasi ham bitta ishni oladi.
  2. FOR UPDATE: birinchi ishchi ishni qulflaydi. Ikkinchisi — o'sha birinchi qatorni so'raydi va qulf bo'shashini kutadi. Ishchilar navbatga tizilib, bittadan ishlaydi.
  3. FOR UPDATE SKIP LOCKED: ikkinchi ishchi qulflangan qatorni o'tkazib, keyingi bo'sh ishni oladi.

Kanon uchalasini o'lchadi: 2 000 ish, 8 ishchi (har biri o'z ulanishi), har ishni "bajarish" 2 ms (t55-navbat.txt, PostgreSQL 18.6, Node 24.21.0). Har qatorda uchta narsaga qarang: vaqt, "ikki marta bajarilgan" soni va "ishchilar" ro'yxati (har ishchi nechta ish qildi):

text
SKIP LOCKED           : 4344 ms, 460 ish/s; bajarishlar 2000, ikki marta bajarilgan 0; ishchilar: 245 252 251 253 252 250 247 250
FOR UPDATE (SKIP'siz) : 29454 ms, 68 ish/s; bajarishlar 2000, ikki marta bajarilgan 0; ishchilar: 520 334 273 388 147 338
qulfsiz SELECT        : 13978 ms, 143 ish/s; bajarishlar 4008, ikki marta bajarilgan 3101; ishchilar: 110 113 139 110 115 107 109 104 1093
8 ishchi, 2 000 ish: tezlik (ish/s)
  • SKIP LOCKEDtakror 0460 ish/s
  • FOR UPDATEtakror 0, ishchilar kutadi68 ish/s
  • Qulfsiz SELECT3 101 ish ikki marta!143 ish/s

Manba: IlmHamroh kanon o'lchovi, 2026-10-24: PostgreSQL 18.6, Node 24.21.0, pg 8.23.1, 8 ishchi, ish 2 ms (t55-navbat.txt)

Nima bo'ldi? Qatorlarni o'qiymiz:

  • SKIP LOCKED — 4.3 soniya, takror yo'q, ish 8 ishchiga deyarli teng bo'lindi (245–253 tadan).
  • FOR UPDATE — to'g'ri (takror 0), lekin 7 barobar sekin: 29.5 soniya. Ishchilar bir-birini kutdi. "ishchilar" qatorida faqat 6 ta son — 8 tadan ikkitasi umuman ish ololmagan.
  • Qulfsiz — eng xavfli: 2 000 ishdan 3 101 marta ikkinchi bajarish (kanon skripti 4 000 dan keyin to'xtatgan — xavfsizlik chegarasi). Eslatma foydalanuvchiga ikki-uch marta keladi; to'lov bo'lsa — pul ikki marta yechiladi.

3. SKIP LOCKED amalda

3.1 Ikki ishchi

Ikki terminal oching — ikkalasida psql. A va B — ikki ishchi. Avval reja (vaqt yuqoridan pastga):

Qadam A sessiya B sessiya Natija
1 BEGIN; SELECT … FOR UPDATE SKIP LOCKED — A 1-ishni oladi va qulflaydi
2 (ishlayapti) BEGIN; xuddi shu SELECT B 1-ishni o'tkazib, darhol 2-ishni oladi
3 UPDATE … id = 1; COMMIT — 1-ish bajarildi, qulf bo'shadi
4 — UPDATE … id = 2; COMMIT 2-ish bajarildi

Natijalarni biz ishga tushirdik: PostgreSQL 17.6, ikki psql sessiyasi. Natijada har qator boshidagi A yoki B — qaysi terminal ekanini bildiradi; id ustuniga qarang:

sql
-- A va B (ikki ishchi)
BEGIN;
SELECT id, payload FROM ish
WHERE holat = 'kutmoqda'
ORDER BY id LIMIT 1
FOR UPDATE SKIP LOCKED;
-- A
UPDATE ish SET holat = 'bajarildi' WHERE id = 1;
COMMIT;
-- B
UPDATE ish SET holat = 'bajarildi' WHERE id = 2;
COMMIT;

Natija:

text
A=> SELECT id, payload FROM ish
A=> WHERE holat = 'kutmoqda'
A=> ORDER BY id LIMIT 1
A=> FOR UPDATE SKIP LOCKED;
A    id |     payload
A   ----+------------------
A     1 | {"vazifa_id": 1}
A   (1 row)
B=> SELECT id, payload FROM ish
B=> WHERE holat = 'kutmoqda'
B=> ORDER BY id LIMIT 1
B=> FOR UPDATE SKIP LOCKED;
B    id |     payload
B   ----+------------------
B     2 | {"vazifa_id": 2}
B   (1 row)
A=> UPDATE ish SET holat = 'bajarildi' WHERE id = 1;
A   UPDATE 1
A=> COMMIT;
A   COMMIT
B=> UPDATE ish SET holat = 'bajarildi' WHERE id = 2;
B   UPDATE 1
B=> COMMIT;
B   COMMIT
A=> SELECT holat, count(*) FROM ish GROUP BY holat ORDER BY holat;
A      holat   | count
A   -----------+-------
A    bajarildi |     2
A    kutmoqda  |     3
A   (2 rows)

(BEGIN qatorlari tushirildi.) A 1-ishni oldi va qulfladi. B xuddi shu so'rovni berdi — 1-ish qulflangan, SKIP LOCKED uni o'tkazib yubordi va B darhol 2-ishni oldi. Hech kim kutmadi, hech kim takrorlamadi.

3.2 Ishchi yiqilsa

Tranzaksiya ichida ishlashning kuchli tomoni: ishchi jarayoni o'lsa (xato, server qayta ishga tushdi), uning ulanishi uziladi va Postgres tranzaksiyani avtomatik bekor qiladi. Qulf bo'shaydi, ish yana "kutmoqda". Buni ROLLBACK bilan taqlid qilamiz (hamma ishlar yana kutmoqda). Endi uchta terminal:

Qadam A sessiya B sessiya C sessiya
1 BEGIN; ish olish → 1-ish qulflandi — —
2 (ishlayapti) BEGIN; ish olish → 2-ish —
3 ROLLBACK ("yiqildi") → 1-ish qulfi bo'shadi (ishlayapti) —
4 — (ishlayapti) ish olish → 1-ish
sql
-- A va B
BEGIN;
SELECT id FROM ish WHERE holat = 'kutmoqda'
ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED;
-- A "yiqildi"
ROLLBACK;
-- C (uchinchi ishchi)
SELECT id FROM ish WHERE holat = 'kutmoqda'
ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED;

Natija:

text
A=> SELECT id FROM ish WHERE holat = 'kutmoqda'
A=> ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED;
A    id
A   ----
A     1
A   (1 row)
B=> SELECT id FROM ish WHERE holat = 'kutmoqda'
B=> ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED;
B    id
B   ----
B     2
B   (1 row)
A=> ROLLBACK;
A   ROLLBACK
C=> SELECT id FROM ish WHERE holat = 'kutmoqda'
C=> ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED;
C    id
C   ----
C     1
C   (1 row)

A yiqilgach, 1-ish C ga o'tdi. Hech narsa yo'qolmadi va hech qanday "tozalash" kodi kerak bo'lmadi.

Tekshirib ko'ring: Ishchi Telegram'ga xabarni muvaffaqiyatli yubordi, lekin COMMIT dan oldin elektr o'chdi. Nima bo'ladi?

Javob

Tranzaksiya bekor bo'ladi — ish yana kutmoqda, boshqa ishchi uni oladi va xabarni ikkinchi marta yuboradi. Navbat "kamida bir marta" (at-least-once) kafolatini beradi, "aynan bir marta" emas. Shuning uchun ishning o'zi idempotent bo'lishi kerak (Race condition'larni hal qilish darsidagi idempotency kaliti — masalan, "bu eslatma yuborilganmi" yozuvi). Bu har qanday navbatga — BullMQ'ga ham — tegishli.

4. Ikkinchi dizayn: "band qilib olish" va qorovul

4.1 Tranzaksiya ichida ishlashning narxi

Yuqoridagi dizaynda ish bajarilayotgan butun vaqt tranzaksiya ochiq. Ish 2 ms bo'lsa — muammo yo'q. 30 soniya (katta PDF hisobot) yoki 5 daqiqa bo'lsa:

  • har ishchi bir ulanishni band qiladi (Connection pooling);
  • uzoq ochiq tranzaksiyalar VACUUM'ni to'xtatadi (MVCC);
  • tashqi API'ni tranzaksiya ichida chaqirish — Qulflar darsidagi "qulf ostida uzoq ish" xatosi.

4.2 Holat bilan band qilish

Muqobil: ishni qisqa tranzaksiyada "band" deb belgilaymiz va darhol COMMIT qilamiz. Ish tranzaksiyadan tashqarida bajariladi, oxirida natija yana qisqa tranzaksiya bilan yoziladi. Jadval boyroq:

sql
CREATE TABLE navbat (
  id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  tur text NOT NULL,
  payload jsonb NOT NULL,
  holat text NOT NULL DEFAULT 'kutmoqda' CHECK (holat IN
    ('kutmoqda', 'bajarilmoqda', 'bajarildi', 'xato')),
  urinish int NOT NULL DEFAULT 0,
  keyingi_urinish timestamptz NOT NULL DEFAULT now(),
  band_vaqti timestamptz,
  oxirgi_xato text
);
CREATE INDEX navbat_tayyor_idx ON navbat (keyingi_urinish)
  WHERE holat = 'kutmoqda';
INSERT INTO navbat (tur, payload) VALUES
  ('eslatma', '{"vazifa_id": 17}'),
  ('email', '{"foydalanuvchi_id": 2}');

Natija:

text
CREATE TABLE
CREATE INDEX
INSERT 0 2

Yangi ustunlar:

  • urinish — necha marta olingan.
  • keyingi_urinish — ish qachondan boshlab "tayyor". Xatodan keyin uni kelajakka suramiz.
  • band_vaqti — ishchi qachon olgan. Ishchi o'lsa, shu bo'yicha topamiz.
  • navbat_tayyor_idx — partial indeks (faqat kutayotgan ishlar): ishchining so'rovi tez bo'ladi, bajarilgan millionlab yozuvlar indeksga kirmaydi. Partial indekslarni Partial, expression va covering indekslar darsida chuqur ko'ramiz.

Ishni olish — bitta buyruq: ichki SELECT … FOR UPDATE SKIP LOCKED ishni topadi va qulflaydi, tashqi UPDATE uni band qiladi:

sql
UPDATE navbat
SET holat = 'bajarilmoqda', urinish = urinish + 1,
    band_vaqti = now()
WHERE id = (
  SELECT id FROM navbat
  WHERE holat = 'kutmoqda' AND keyingi_urinish <= now()
  ORDER BY keyingi_urinish, id
  LIMIT 1
  FOR UPDATE SKIP LOCKED
)
RETURNING id, tur, payload, urinish;

Natija:

text
 id |   tur   |      payload      | urinish
----+---------+-------------------+---------
  1 | eslatma | {"vazifa_id": 17} |       1
(1 row)

UPDATE 1

Bu buyruq autocommit'da (o'zi tranzaksiya) — qulf millisekund ushlanadi. Ishchi payload ni oldi va endi bemalol, tranzaksiyasiz Telegram'ga yuboradi.

4.3 Xato va qayta urinish

Telegram 429 qaytardi. Ishni qaytaramiz: urinish ga qarab pauza — 2, 4, 8 soniya (exponential backoff, Deadlock va serialization failure darsidagi g'oya), 3 urinishdan keyin — xato:

sql
UPDATE navbat
SET holat = CASE WHEN urinish >= 3 THEN 'xato' ELSE 'kutmoqda' END,
    keyingi_urinish = now() + make_interval(secs => 2 ^ urinish),
    band_vaqti = NULL,
    oxirgi_xato = 'Telegram: 429 Too Many Requests'
WHERE id = 1
RETURNING id, holat, urinish,
          keyingi_urinish - now() AS kutish;

Natija:

text
 id |  holat   | urinish |  kutish
----+----------+---------+----------
  1 | kutmoqda |       1 | 00:00:02
(1 row)

UPDATE 1

Ish yana kutmoqda, lekin 2 soniyadan keyin "tayyor" bo'ladi — ishchi so'rovidagi keyingi_urinish <= now() sharti uni shu vaqtgacha ko'rmaydi. xato holatidagi ishlarni ishchi umuman olmaydi — ularni odam ko'radi (admin panel, alert). Bunday "o'lik ishlar" joyi ko'pincha dead letter queue deyiladi.

Kanon ham xuddi shu sxemani sinagan (2^urinish soniya, 3 dan keyin xato): t55-navbat.txt da 1- va 2-ishlar urinish 1, keyinroq: true, 3-ish tegilmagan.

4.4 Qorovul: osilib qolgan ishlar

Endi muammo: ishchi ishni band qildi va... o'ldi. Tranzaksiya allaqachon yopilgan — Postgres hech narsani qaytarmaydi. Ish abadiy bajarilmoqda da qoladi. Yechim — davriy qorovul (reaper) so'rovi:

sql
-- 2-ish 10 daqiqa oldin olindi, ishchi esa "yiqildi" (taqlid)
UPDATE navbat
SET holat = 'bajarilmoqda', urinish = urinish + 1,
    band_vaqti = now() - interval '10 minutes'
WHERE id = 2;
-- "qorovul": 5 daqiqadan uzoq band ishlarni navbatga qaytaradi
UPDATE navbat
SET holat = 'kutmoqda', band_vaqti = NULL
WHERE holat = 'bajarilmoqda'
  AND band_vaqti < now() - interval '5 minutes'
RETURNING id, tur, urinish;

Natija:

text
UPDATE 1
 id |  tur  | urinish
----+-------+---------
  2 | email |       1
(1 row)

UPDATE 1

Qorovul har daqiqada (cron yoki ishchining o'zi) ishga tushadi. 5 daqiqa — "eng uzun ish" dan katta bo'lishi kerak, aks holda tirik ishchining ishi ham qaytarilib, ikki marta bajariladi.

4.5 Qaysi dizayn?

flowchart LR
  K["kutmoqda"] -->|"ishchi oldi"| B["bajarilmoqda"]
  B -->|"muvaffaqiyat"| D["bajarildi"]
  B -->|"xato, urinish < 3"| K
  B -->|"xato, urinish >= 3"| X["xato"]
  B -->|"qorovul: band_vaqti eski"| K
Tranzaksiya ichida Band qilish + qorovul
Ishchi o'lsa avtomatik qaytadi qorovul qaytaradi
Uzoq ishlar yomon (ochiq tranzaksiya) yaxshi
Murakkablik kam ko'proq
Mos joy qisqa ishlar (< 1 s) tashqi API, uzoq ishlar

Kanon tajribasi birinchi dizaynni o'lchagan (ish 2 ms). Tayyor kutubxonalar odatda ikkinchisini ishlatadi.

5. Ishchini uyg'otish: polling va NOTIFY

Ishchi navbat bo'sh bo'lsa nima qiladi? Eng sodda — polling: bir oz uxlaydi (masalan, 1 soniya) va qayta so'raydi. Bu kechikish beradi (ish 1 soniyagacha kutadi) va bo'sh so'rovlar qiladi.

Yaxshiroq: ish qo'shilganda NOTIFY yuborish, ishchi esa LISTEN bilan kutish (LISTEN/NOTIFY). Kanon u yerda ko'rgan muhim fakt: tranzaksiya ichidagi NOTIFY faqat COMMIT da yetkaziladi — demak ishchi uyg'onganda ish allaqachon ko'rinadi. Lekin NOTIFY yo'qolishi mumkin (ishchi o'sha paytda ulanmagan bo'lsa) — shuning uchun amalda ikkalasi birga: NOTIFY tez uyg'otadi, siyrak polling (masalan, 30 soniyada) hech narsa qolib ketmasligini kafolatlaydi.

6. Tayyor kutubxonalar va Redis'ga qachon o'tish

O'zingiz yozgan navbat — o'rganish uchun ajoyib. Ishlab chiqarishda ko'pincha tayyor kutubxona olinadi. Ikkalasi ham Postgres'da SKIP LOCKED ga asoslangan (hujjatlari bo'yicha, 2026-10; kursda sinalmagan):

  • pg-boss — Node.js uchun: navbatlar, qayta urinish, kechiktirilgan va cron ishlar, muddati o'tgan ishlarni qaytarish (yuqoridagi qorovul vazifasi).
  • graphile-worker — Node.js uchun: ish qo'shish SQL funksiyasi orqali (trigger'dan ham), LISTEN/NOTIFY bilan uyg'onadi.

Qachon Postgres navbati yetarli:

  • sekundiga o'nlab–yuzlab ish (kanon: 460 ish/s, 8 ishchi, kichik ish);
  • ishni ma'lumot bilan bitta tranzaksiyada qo'shish muhim;
  • alohida infratuzilma (Redis serveri, uning zaxirasi, monitoringi) qo'shishni istamaysiz.

Qachon Redis/BullMQ (yoki RabbitMQ, Kafka) ga o'tish haqida o'ylash kerak:

  • sekundiga minglab ish — navbat jadvali eng "issiq" jadvalga aylanadi, bloat va VACUUM bosimi oshadi (MVCC — har ish kamida 2–3 yangi qator versiyasi);
  • murakkab imkoniyatlar: ustuvorliklar, tezlik cheklovi (rate limit), ish oqimlari (flow), ko'p xizmatlar orasida xabar almashish;
  • navbat bazaning yukini sezilarli oshira boshladi.

Redis va BullMQ — keyingi qismlarda (Nega navbat kerak — 29-qism). Hozircha muhim xulosa: ko'p loyihalar uchun "avval Postgres navbati, kerak bo'lsa — keyin BullMQ" — sog'lom yo'l.

7. Node qadami: kanon tajribasi, loyihaga kirmaydi

vazifalar sxemasiga navbat jadvali qo'shilmadi — 25-qism API'sida fon vazifasi (eslatma) yo'q. API'ni Postgres'ga ulaganimizda ham navbat kerak bo'lmadi: bu naqsh 29-qismda — Telegram eslatmalar boti bilan — ishga tushadi. Kanon t55-navbat.mjs da birinchi dizaynni o'lchagan. Ishchi siklining asosiy qismi (kanondan; SQL qatorlarga bo'lingan):

js
const c = await mijoz();
for (;;) {
  await c.query("BEGIN");
  const { rows: [ish] } = await c.query(
    `SELECT id FROM t55_ish
     WHERE holat = 'kutmoqda' AND keyingi_urinish <= now()
     ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED`);
  if (!ish) { await c.query("COMMIT"); break; }
  await kut(2);    // ishni bajarish (masalan, Telegram'ga yuborish)
  await c.query(
    `UPDATE t55_ish SET holat = 'bajarildi', bajaruvchi = $2,
       urinish = urinish + 1 WHERE id = $1`, [ish.id, n]);
  await c.query("COMMIT");
}

Har ishchi — o'z Client i (pool emas): u siklda doimiy ishlaydi va ulanishni boshqalar bilan bo'lishmaydi. Navbat bo'shasa (!ish) — sikldan chiqadi; haqiqiy ishchi esa uxlab, qayta so'raydi.

8. Ko'p uchraydigan xatolar

8.1 Qulfsiz "ish olish"

Kanon: 3 101 takror bajarish. FOR UPDATE SKIP LOCKED.

8.2 SKIP LOCKED siz FOR UPDATE

To'g'ri, lekin ishchilar navbatga tiziladi (68 vs 460 ish/s). SKIP LOCKED.

8.3 ORDER BY siz

LIMIT 1 "tasodifiy" ishni oladi — eski ishlar cheksiz kutishi mumkin. ORDER BY keyingi_urinish, id (yoki ustuvorlik bo'yicha).

8.4 Uzoq ishni tranzaksiya ichida bajarish

Ulanishlar tugaydi, VACUUM to'xtaydi. "Band qilish + qorovul" dizayni.

8.5 Ishni idempotent qilmaslik

Navbat "kamida bir marta" beradi. Ish ikki marta bajarilsa ham zarar bo'lmasin (idempotency kaliti, "yuborilgan" belgisi).

8.6 Bajarilgan ishlarni abadiy saqlash

Jadval o'sadi, indekslar shishadi. Eski bajarildi larni davriy o'chirish yoki arxivga ko'chirish (va partial indeks — faqat kutmoqda).

9. Mashqlar

1-mashq (oson): Kim qaysi ishni oladi?

Navbatda 1–5 ishlar kutmoqda. A ishchi tranzaksiyada 1-ishni FOR UPDATE SKIP LOCKED bilan oldi va hali ishlayapti. B ishchi … LIMIT 1 FOR UPDATE SKIP LOCKED qiladi. C esa … LIMIT 1 FOR UPDATE (SKIP'siz). B va C nima oladi?

Yechim

B — darhol 2-ishni (1 qulflangan, o'tkazib yuboriladi). C — 1-qatorni so'raydi va A tugashini kutadi. A COMMIT qilgach, C uyg'onadi, WHERE holat = 'kutmoqda' ni yangi versiyada qayta tekshiradi — 1-ish endi bajarildi, shart yolg'on. Bunda C LIMIT 1 uchun 0 qator olishi mumkin, garchi navbatda boshqa ishlar bo'lsa ham. SKIP'siz FOR UPDATE ning yana bir kamchiligi shu.

2-mashq (o'rta): Partiya bilan olish

Ishchi har safar bitta emas, 3 tagacha ish olsin (tarmoq aylanishlarini tejash uchun). m55_navbat jadvaliga 5 ta ish qo'shing va bitta UPDATE … WHERE id IN (SELECT … LIMIT 3 FOR UPDATE SKIP LOCKED) RETURNING bilan 3 tasini bajarilmoqda qiling. Oxirida holatlar bo'yicha son chiqarilsin. Eslatma: IN (subquery) — Subquery va LATERAL.

Yechim
sql
DROP TABLE IF EXISTS m55_navbat;
CREATE TABLE m55_navbat (
  id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  tur text NOT NULL,
  holat text NOT NULL DEFAULT 'kutmoqda'
);
INSERT INTO m55_navbat (tur)
SELECT 'eslatma' FROM generate_series(1, 5);

UPDATE m55_navbat SET holat = 'bajarilmoqda'
WHERE id IN (
  SELECT id FROM m55_navbat
  WHERE holat = 'kutmoqda'
  ORDER BY id
  LIMIT 3
  FOR UPDATE SKIP LOCKED
)
RETURNING 'olindi: ' || id;

SELECT holat || ': ' || count(*)
FROM m55_navbat GROUP BY holat ORDER BY holat;
DROP TABLE m55_navbat;

Biz ishga tushirdik (psql -X -q -A -t -f, PostgreSQL 17.6):

text
olindi: 1
olindi: 2
olindi: 3
bajarilmoqda: 3
kutmoqda: 2

Partiya bilan olish tezlikni oshiradi, lekin ishchi yiqilsa uchala ish ham qorovulni kutadi. Partiya hajmi — ishchi bir "nafasda" bajara oladigan miqdor.

3-mashq (qiyin): To'rt ishchi

Node'da 40 ta ishli jadval va 4 ta ishchi yozing. Har ishchi — o'z pg.Client i, siklda: BEGIN → SELECT … FOR UPDATE SKIP LOCKED → 5 ms "ish" → UPDATE holat = 'bajarildi', bajaruvchi = raqam → COMMIT; ish qolmasa — chiqadi. Oxirida: nechta ish bajarildi, nechtasi takror, nechta ishchi ishladi, nechta ish qoldi. Eslatma: count(*) FILTER (WHERE …), count(DISTINCT …) — Agregatlar, GROUP BY va HAVING; takrorni new Set bilan sanash.

Yechim
js
import pg from "pg";

const url = process.env.DATABASE_URL;
const kut = (ms) => new Promise((r) => setTimeout(r, ms));
const admin = new pg.Client({ connectionString: url });
await admin.connect();
await admin.query("DROP TABLE IF EXISTS m55_ish");
await admin.query(`CREATE TABLE m55_ish (
  id int GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  holat text NOT NULL DEFAULT 'kutmoqda',
  bajaruvchi int)`);
await admin.query(`INSERT INTO m55_ish (holat)
  SELECT 'kutmoqda' FROM generate_series(1, 40)`);

const bajarilgan = [];
async function ishchi(raqam) {
  const c = new pg.Client({ connectionString: url });
  await c.connect();
  for (;;) {
    await c.query("BEGIN");
    const { rows: [ish] } = await c.query(
      `SELECT id FROM m55_ish WHERE holat = 'kutmoqda'
       ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED`);
    if (!ish) {
      await c.query("COMMIT");
      break;
    }
    await kut(5); // ishni bajarish
    await c.query(
      `UPDATE m55_ish SET holat = 'bajarildi', bajaruvchi = $2
       WHERE id = $1`, [ish.id, raqam]);
    await c.query("COMMIT");
    bajarilgan.push(ish.id);
  }
  await c.end();
}

await Promise.all([1, 2, 3, 4].map(ishchi));
const takror = bajarilgan.length - new Set(bajarilgan).size;
const { rows } = await admin.query(
  `SELECT count(DISTINCT bajaruvchi)::int AS ishchilar,
          count(*) FILTER (WHERE holat = 'kutmoqda')::int AS qoldi
   FROM m55_ish`);
console.log(`bajarildi ${bajarilgan.length}, takror ${takror}`);
const { ishchilar, qoldi } = rows[0];
console.log(`ishlagan ishchilar ${ishchilar}, qoldi ${qoldi}`);
await admin.query("DROP TABLE m55_ish");
await admin.end();

Biz ishga tushirdik (PostgreSQL 17.6, Node 24.21.0, pg 8.23.1; ikki marta — bir xil):

text
bajarildi 40, takror 0
ishlagan ishchilar 4, qoldi 0

Endi FOR UPDATE SKIP LOCKED dan SKIP LOCKED ni olib tashlab, vaqtni (performance.now()) o'lchang; keyin FOR UPDATE ni ham olib tashlang va takror ga qarang — kanon natijalarining kichik nusxasini olasiz.

10. Real ishda

  • SaaS va startaplar: email, push, hisobot, webhook'lar — ko'pincha pg-boss/graphile-worker yoki shunga o'xshash o'z navbati bilan, Redis'siz.
  • "Outbox" naqshi: ma'lumot va "yuborilishi kerak bo'lgan xabar" bitta tranzaksiyada yoziladi, ishchi keyin yuboradi — mikroservislarda keng tarqalgan; asosi aynan shu darsdagi jadval.
  • Navbat tizimlari ichida: ko'p navbat va ish rejalashtiruvchi tizimlar Postgres'ni saqlash joyi sifatida qo'llab-quvvatlaydi va SKIP LOCKED dan foydalanadi.
  • Intervyu: "Bazada navbatni qanday qurasiz?", "SKIP LOCKED nima qiladi?", "At-least-once va exactly-once farqi?".

Xulosa

  • Fon vazifalari navbati Postgres jadvali bo'la oladi — afzalligi: ish ma'lumot bilan bitta tranzaksiyada qo'shiladi.
  • FOR UPDATE SKIP LOCKED — ishchilar band ishni o'tkazib yuboradi: kanonda 460 ish/s, takror 0.
  • Qulfsiz olish — takror bajarish (3 101 ta); SKIP'siz FOR UPDATE — 7 barobar sekin.
  • Qisqa ishlar — tranzaksiya ichida; uzoq ishlar — "band qilish" + band_vaqti + qorovul.
  • Qayta urinish: urinish, keyingi_urinish = now() + 2^n, chegaradan keyin xato.
  • Navbat "kamida bir marta" beradi — ishning o'zi idempotent bo'lsin.

Keyingi dars: Indeks nima: B-tree ichidan — so'rovlarni tezlashtirish bo'limi boshlanadi: nega 2 mln qatorli jadvalda qidiruv 150 ms dan 0.08 ms ga tushadi.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
PostgreSQL navbat: FOR UPDATE SKIP LOCKED bilan Redis'siz fon vazifalari — IlmHamroh