Mundarija (31)
- Bu darsda
- 1. Nega bu kerak?
- 2. Muammo: ishchilar bir-biriga xalaqit beradi
- 2.1 Ish jadvali
- 2.2 Uch xil "ish olish"
- 3. SKIP LOCKED amalda
- 3.1 Ikki ishchi
- 3.2 Ishchi yiqilsa
- 4. Ikkinchi dizayn: "band qilib olish" va qorovul
- 4.1 Tranzaksiya ichida ishlashning narxi
- 4.2 Holat bilan band qilish
- 4.3 Xato va qayta urinish
- 4.4 Qorovul: osilib qolgan ishlar
- 4.5 Qaysi dizayn?
- 5. Ishchini uyg'otish: polling va NOTIFY
- 6. Tayyor kutubxonalar va Redis'ga qachon o'tish
- 7. Node qadami: kanon tajribasi, loyihaga kirmaydi
- 8. Ko'p uchraydigan xatolar
- 8.1 Qulfsiz "ish olish"
- 8.2 SKIP LOCKED siz FOR UPDATE
- 8.3 ORDER BY siz
- 8.4 Uzoq ishni tranzaksiya ichida bajarish
- 8.5 Ishni idempotent qilmaslik
- 8.6 Bajarilgan ishlarni abadiy saqlash
- 9. Mashqlar
- 1-mashq (oson): Kim qaysi ishni oladi?
- 2-mashq (o'rta): Partiya bilan olish
- 3-mashq (qiyin): To'rt ishchi
- 10. Real ishda
- Xulosa
- Manbalar
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'sizFOR UPDATE— 68 ish/s; qulfsizSELECT— 3 101 ish ikki marta bajarildi.
Bu darsda
- Fon vazifalari navbati nima uchun kerakligini va uni bazada qanday saqlashni bilasiz.
FOR UPDATE SKIP LOCKEDbilan 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:
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:
- Qulfsiz:
SELECT id … WHERE holat = 'kutmoqda' ORDER BY id LIMIT 1. Ikki ishchi bir vaqtda so'rasa, ikkalasi ham bitta ishni oladi. FOR UPDATE: birinchi ishchi ishni qulflaydi. Ikkinchisi — o'sha birinchi qatorni so'raydi va qulf bo'shashini kutadi. Ishchilar navbatga tizilib, bittadan ishlaydi.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):
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- 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:
-- 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:
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 |
-- 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:
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
COMMITdan 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:
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:
CREATE TABLE
CREATE INDEX
INSERT 0 2Yangi 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:
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:
id | tur | payload | urinish
----+---------+-------------------+---------
1 | eslatma | {"vazifa_id": 17} | 1
(1 row)
UPDATE 1Bu 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:
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:
id | holat | urinish | kutish
----+----------+---------+----------
1 | kutmoqda | 1 | 00:00:02
(1 row)
UPDATE 1Ish 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:
-- 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:
UPDATE 1
id | tur | urinish
----+-------+---------
2 | email | 1
(1 row)
UPDATE 1Qorovul 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/NOTIFYbilan 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):
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
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):
olindi: 1
olindi: 2
olindi: 3
bajarilmoqda: 3
kutmoqda: 2Partiya 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
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):
bajarildi 40, takror 0
ishlagan ishchilar 4, qoldi 0Endi 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 LOCKEDdan 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 keyinxato. - 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
- PostgreSQL 18 hujjati: SELECT — The Locking Clause (SKIP LOCKED).
- PostgreSQL 18 hujjati: NOTIFY.
- pg-boss: github.com/timgit/pg-boss (hujjat bo'yicha, sinalmagan).
- graphile-worker: worker.graphile.org (hujjat bo'yicha, sinalmagan).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!