Mundarija (31)
- Bu darsda
- 1. Nega bu kerak?
- 2. Ombor qoldig'i
- 2.1 Naiv kod: o'qi → tekshir → yoz
- 2.2 Kanon o'lchovi: 100 xaridor
- 2.3 Yechim 1: atomik UPDATE
- 2.4 Yechim 2: SELECT … FOR UPDATE
- 2.5 Oxirgi himoya chizig'i: CHECK
- 3. Bron: kesishgan vaqtlar
- 3.1 Nega UNIQUE yetmaydi
- 3.2 Exclusion constraint
- 3.3 Parallel bronlar
- 4. Idempotent yozish: bitta to'lov — bitta yozuv
- 4.1 Muammo
- 4.2 Javobni nima qaytaradi?
- 5. Hujumchi nigohi
- 6. Qaysi vosita qachon
- 7. Node qadami
- 8. Ko'p uchraydigan xatolar
- 8.1 "Avval tekshiraman, keyin yozaman"
- 8.2 Tranzaksiya — poyganing davosi deb o'ylash
- 8.3 rowCount ga qaramaslik
- 8.4 Bron xatosini faqat 23P01 deb kutish
- 8.5 Idempotency kalitini serverda yaratish
- 9. Mashqlar
- 1-mashq (oson): Vositani tanlang
- 2-mashq (o'rta): Kinoteatr joyi
- 3-mashq (qiyin): 50 xaridor
- 10. Real ishda
- Xulosa
- Manbalar
Race condition amaliyoti: ombor qoldig'i, bron va idempotent to'lov PostgreSQL'da
Qisqacha: Race condition (poyga holati) — natija so'rovlar qaysi tartibda kelganiga bog'liq bo'lib qolgan xato. Bazada uchta klassik ko'rinishi bor: ombordagi oxirgi mahsulotni ikki kishi sotib oladi, bir vaqt oralig'i ikki marta bron qilinadi, bitta to'lov ikki marta yoziladi. Yechimlar: atomik
UPDATE … WHERE qoldiq > 0(yokiSELECT … FOR UPDATE), exclusion constraint (EXCLUDE USING gist) va UNIQUE +ON CONFLICT DO NOTHING. Kanon o'lchovi: 100 parallel xaridor, qoldiq 10 — naiv kod 100 ta sotdi, atomik kod aniq 10 ta.
Bu darsda
- "O'qi → tekshir → yoz" naqshi parallel so'rovlarda nega buzilishini ko'rsatasiz.
- Ombor qoldig'ini atomik
UPDATEvaFOR UPDATEbilan himoyalaysiz va ularni o'lchov bilan solishtirasiz. - Bron uchun exclusion constraint yozasiz va uning xatosini (
23P01) ushlaysiz. - Idempotency kaliti bilan takror so'rovni bitta yozuvga aylantirasiz.
CHECKcheklovini oxirgi himoya chizig'i sifatida qo'yasiz.
Oldin bilishingiz kerak: Optimistic concurrency, Deadlock va serialization failure bilan ishlash, UPSERT va MERGE, Idempotency-Key.
1. Nega bu kerak?
Oldingi beshta darsda vositalarni yig'dik: izolyatsiya darajalari, MVCC, qulflar, versiya ustuni, qayta urinish. Bu dars — amaliyot. Biz uchta haqiqiy biznes muammosini olamiz va har biriga eng sodda, ishonchli vositani tanlaymiz.
Race condition (poyga holati) atamasini Asinxron naqshlar va React darslarida ko'rgansiz: ikki asinxron ish "poyga"ga tushadi va natija kim birinchi kelganiga bog'liq bo'ladi. Bazada ham xuddi shunday — faqat bu yerda poyga o'nlab serverlar va minglab foydalanuvchilar orasida.
Uchta voqea:
- Ombor. "Bahor" oshxonasi onlayn do'konida pishiriq kitobidan 10 ta qolgan. Aksiya boshlandi — 100 kishi bir soniyada "Sotib olish" tugmasini bosdi.
- Bron. Majlislar xonasi soat 10:00–12:00 ga band qilindi. Shu paytda boshqa xodim 11:00–13:00 ni band qilmoqda.
- To'lov. Malika Click orqali 35 000 so'm to'ladi. Tarmoq uzildi, ilova javob olmadi va so'rovni qayta yubordi. Pul ikki marta yechilmasligi kerak.
Uchalasida ham ilova kodi "to'g'ri" ko'rinadi. Xato faqat parallel yukda, ishlab chiqarishda chiqadi. Shuning uchun uni oldindan bilish va bazaning o'zida himoya qo'yish kerak.
2. Ombor qoldig'i
2.1 Naiv kod: o'qi → tekshir → yoz
Eng tabiiy yozilgan kod:
const { rows: [m] } = await pool.query(
"SELECT qoldiq FROM mahsulot WHERE id = 1");
if (m.qoldiq <= 0) return false; // tugagan
await pool.query("UPDATE mahsulot SET qoldiq = $1 WHERE id = 1",
[m.qoldiq - 1]);
await pool.query("INSERT INTO buyurtma (mahsulot_id) VALUES (1)");Ikki xaridor, oxirgi bitta kitob. Natijalarni biz ishga tushirdik: PostgreSQL 17.6, ikki psql sessiyasi — har biri o'z tranzaksiyasida:
| Qadam | A sessiya | B sessiya | Natija |
|---|---|---|---|
| 1 | BEGIN; va SELECT qoldiq FROM mahsulot WHERE id = 1; |
xuddi shu | ikkalasi: 1 |
| 2 | UPDATE mahsulot SET qoldiq = 0 WHERE id = 1; va INSERT INTO buyurtma … |
A "1 ta bor" — sotdi | |
| 3 | UPDATE mahsulot SET qoldiq = 0 WHERE id = 1; |
B kutadi (A ning qulfi) | |
| 4 | COMMIT; |
B ning UPDATE'i o'tadi | |
| 5 | INSERT INTO buyurtma … va COMMIT; |
nechta buyurtma? |
Transkriptda oxirgi SELECT ga qarang:
A=> SELECT qoldiq FROM mahsulot WHERE id = 1;
A qoldiq
A --------
A 1
A (1 row)
B=> SELECT qoldiq FROM mahsulot WHERE id = 1;
B qoldiq
B --------
B 1
B (1 row)
A=> UPDATE mahsulot SET qoldiq = 0 WHERE id = 1;
A UPDATE 1
A=> INSERT INTO buyurtma (mahsulot_id) VALUES (1);
A INSERT 0 1
B=> UPDATE mahsulot SET qoldiq = 0 WHERE id = 1;
A=> COMMIT;
A COMMIT
B UPDATE 1
B=> INSERT INTO buyurtma (mahsulot_id) VALUES (1);
B INSERT 0 1
B=> COMMIT;
B COMMIT
A=> SELECT (SELECT qoldiq FROM mahsulot) AS qoldiq,
A=> (SELECT count(*) FROM buyurtma) AS sotildi;
A qoldiq | sotildi
A --------+---------
A 0 | 2
A (1 row)Qoldiq 0, lekin 2 ta buyurtma. Bitta kitob ikki kishiga sotildi. B A'ning qulfini kutdi, lekin qulf bo'shagach u o'zi oldindan hisoblagan 0 ni yozdi. Bu — izolyatsiya darsidagi lost update, faqat endi oqibati — norozi mijoz.
2.2 Kanon o'lchovi: 100 xaridor
Kanon katta sinov o'tkazdi: qoldiq 10, 100 ta parallel "xaridor" (pool 20 ulanish), har usul 3 marta (t54-race.txt, PostgreSQL 18.6, Node 24.21.0):
naiv (SELECT, keyin UPDATE):
sotildi 100, qoldiq 9, "ha" 100, 633 ms
sotildi 100, qoldiq 9, "ha" 100, 539 ms
sotildi 100, qoldiq 9, "ha" 100, 580 ms
atomik UPDATE ... WHERE qoldiq > 0:
sotildi 10, qoldiq 0, "ha" 10, 226 ms
sotildi 10, qoldiq 0, "ha" 10, 149 ms
sotildi 10, qoldiq 0, "ha" 10, 211 ms
SELECT ... FOR UPDATE:
sotildi 10, qoldiq 0, "ha" 10, 454 ms
sotildi 10, qoldiq 0, "ha" 10, 476 ms
sotildi 10, qoldiq 0, "ha" 10, 449 ms- Naivqoldiq 9 qoldi100 ta
- Atomik UPDATE149–226 ms10 ta
- FOR UPDATE449–476 ms10 ta
Manba: IlmHamroh kanon o'lchovi, 2026-10-24: PostgreSQL 18.6, Node 24.21.0, pg 8.23.1, pool 20, har usul 3 marta (t54-race.txt)
Naiv kod — 100 ta sotdi (10 ta o'rniga), va qoldiq hatto 0 ga ham tushmadi — 9. Deyarli hamma xaridor 10 ni o'qidi va 9 ni yozdi. Uch urinishda ham bir xil. Bu "kamdan-kam" xato emas — yuk ostida u har doim sodir bo'ladi.
Maslahat: Kanon skriptida naiv usul tranzaksiyasiz (
pool.query— har so'rov o'zi), qolgan ikkitasi tranzaksiya ichida. Biroq naiv kodni tranzaksiyaga o'rash yordam bermaydi — yuqoridagipsqlmisolida A va B tranzaksiyada edi va baribir ikkalasi sotdi.
2.3 Yechim 1: atomik UPDATE
Tekshirish va yozishni bitta buyruqqa birlashtiramiz:
-- A va B
BEGIN;
UPDATE mahsulot SET qoldiq = qoldiq - 1
WHERE id = 1 AND qoldiq > 0;
-- A
INSERT INTO buyurtma (mahsulot_id) VALUES (1);
COMMIT;
-- B: UPDATE 0 — sotilmadi
ROLLBACK;Natija:
A=> UPDATE mahsulot SET qoldiq = qoldiq - 1
A=> WHERE id = 1 AND qoldiq > 0;
A UPDATE 1
B=> UPDATE mahsulot SET qoldiq = qoldiq - 1
B=> WHERE id = 1 AND qoldiq > 0;
A=> INSERT INTO buyurtma (mahsulot_id) VALUES (1);
A INSERT 0 1
A=> COMMIT;
A COMMIT
B UPDATE 0
B=> ROLLBACK;
B ROLLBACK
A=> SELECT (SELECT qoldiq FROM mahsulot) AS qoldiq,
A=> (SELECT count(*) FROM buyurtma) AS sotildi;
A qoldiq | sotildi
A --------+---------
A 0 | 1
A (1 row)(Boshidagi BEGIN lar tushirildi.) B qulfni kutdi, A tugagach WHERE shartini yangi qiymatda (qoldiq = 0) qayta tekshirdi — UPDATE 0. Ilova rowCount === 0 ni ko'radi va "Afsuski, tugadi" deydi. Bu Optimistic concurrency darsidagi WHERE versiya = $n bilan bir xil mexanizm: READ COMMITTED'da kutgan UPDATE shartni eng yangi versiyada qayta tekshiradi.
Node'dagi ko'rinishi:
const c = await pool.connect();
try {
await c.query("BEGIN");
const r = await c.query(
`UPDATE mahsulot SET qoldiq = qoldiq - 1
WHERE id = 1 AND qoldiq > 0`);
if (r.rowCount === 0) {
await c.query("ROLLBACK");
return false; // tugagan
}
await c.query("INSERT INTO buyurtma (mahsulot_id) VALUES (1)");
await c.query("COMMIT");
return true;
} finally {
c.release();
}UPDATE va INSERT bitta tranzaksiyada — buyurtma yozilmasa, qoldiq ham qaytadi.
2.4 Yechim 2: SELECT … FOR UPDATE
Ba'zan tekshiruv bitta WHERE ga sig'maydi: chegirma hisoblash, foydalanuvchi limiti, bir nechta jadval. Unda qatorni o'qish paytidayoq qulflaymiz (Qulflar):
await c.query("BEGIN");
const { rows: [m] } = await c.query(
"SELECT qoldiq FROM mahsulot WHERE id = 1 FOR UPDATE");
if (m.qoldiq <= 0) { await c.query("ROLLBACK"); return false; }
// ... murakkab tekshiruvlar ...
await c.query("UPDATE mahsulot SET qoldiq = $1 WHERE id = 1",
[m.qoldiq - 1]);
await c.query("INSERT INTO buyurtma (mahsulot_id) VALUES (1)");
await c.query("COMMIT");Bu yerda qoldiq - 1 ni ilova hisoblaydi — va bu xavfsiz, chunki qator qulflangan: hech kim bizdan keyin o'qib, bizdan oldin yoza olmaydi.
Kanon o'lchovida ikkalasi ham to'g'ri (10 ta), lekin FOR UPDATE taxminan 2 barobar sekinroq: 449–476 ms vs 149–226 ms. Sababi — qulf uzoqroq ushlanadi (o'qish, ilova ishi, yozish), xaridorlar navbatda turadi. Qoida: imkon bo'lsa — atomik UPDATE; shart murakkab bo'lsa — FOR UPDATE.
2.5 Oxirgi himoya chizig'i: CHECK
Atomik kodni hamma joyda yozish — intizom. Kimdir admin panelda yoki skriptda naiv kod yozib qo'yishi mumkin. Bazaning o'zi "qoldiq manfiy bo'lmaydi" deb kafolat bersin:
CREATE TABLE ombor (
id int PRIMARY KEY,
qoldiq int NOT NULL CHECK (qoldiq >= 0)
);
INSERT INTO ombor VALUES (1, 0);
UPDATE ombor SET qoldiq = qoldiq - 1 WHERE id = 1;Natija:
CREATE TABLE
INSERT 0 1
ERROR: new row for relation "ombor" violates check constraint "ombor_qoldiq_check"
DETAIL: Failing row contains (1, -1)."Yangi qator ombor_qoldiq_check cheklovini buzadi" — qoldiq −1 bo'lardi. Kod 23514. CHECK naiv kodning ortiqcha sotishini to'xtatmaydi (kanonda qoldiq 9 da qoldi — manfiyga tushmadi), lekin "minus 3 ta kitob" kabi bema'ni holatni oldini oladi. U — oxirgi chiziq, asosiy himoya emas.
Tekshirib ko'ring: Hamyondan pul yechish:
UPDATE hamyon SET balans = balans - $2 WHERE id = $1. Parallel yechishlarda balans manfiy bo'lishi mumkinmi? Qanday tuzatasiz?
Javob
Mumkin: 100 so'mlik hamyondan ikki parallel 80 so'mlik yechish — ikkalasi ham o'tadi (atomik balans - 80 lost update bermaydi, lekin chegarani tekshirmaydi) — natija −60. Tuzatish: shartni WHERE ga qo'shish — WHERE id = $1 AND balans >= $2; rowCount = 0 — "mablag' yetarli emas". Qo'shimcha — CHECK (balans >= 0) oxirgi chiziq sifatida.
3. Bron: kesishgan vaqtlar
3.1 Nega UNIQUE yetmaydi
Ikki bron 10:00, 12:00) va [11:00, 13:00) — boshlanish va tugash vaqtlari har xil, UNIQUE (xona, boshlanish) ularni o'tkazib yuboradi. "Kesishadimi" savoli — tenglik emas. Ilova kodida tekshirish (SELECT … WHERE vaqtlar kesishadi → keyin INSERT) — yana o'sha naiv "o'qi → yoz" poygasi.
3.2 Exclusion constraint
Postgres'da bunga maxsus cheklov bor — exclusion constraint (istisno cheklovi): "jadvalda ikki qator shu operatorlar bo'yicha bir vaqtda mos kelmasin". Bizda: xona teng (=) va vaqt oraliqlari kesishadi (&&) — bunday juftlik bo'lmasin.
CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE TABLE bron (
id serial PRIMARY KEY,
xona int NOT NULL,
vaqt tstzrange NOT NULL,
EXCLUDE USING gist (xona WITH =, vaqt WITH &&)
);
SET TIME ZONE 'Asia/Tashkent';
INSERT INTO bron (xona, vaqt) VALUES
(1, '[2026-10-25 10:00, 2026-10-25 12:00)'),
(1, '[2026-10-25 12:00, 2026-10-25 14:00)'),
(2, '[2026-10-25 11:00, 2026-10-25 13:00)');
INSERT INTO bron (xona, vaqt)
VALUES (1, '[2026-10-25 11:30, 2026-10-25 12:30)');Natija:
CREATE EXTENSION
CREATE TABLE
SET
INSERT 0 3
ERROR: conflicting key value violates exclusion constraint "bron_xona_vaqt_excl"
DETAIL: Key (xona, vaqt)=(1, ["2026-10-25 11:30:00+05","2026-10-25 12:30:00+05")) conflicts with existing key (xona, vaqt)=(1, ["2026-10-25 10:00:00+05","2026-10-25 12:00:00+05")).Yangi atamalar:
tstzrange— vaqt oralig'i turi (timestamptzlar oralig'i).[— boshlanish kiradi,)— tugash kirmaydi. Shuning uchun[10:00, 12:00)va[12:00, 14:00)kesishmaydi — biri tugagan lahzada ikkinchisi boshlanadi; uchala birinchi bron o'tdi. Oraliq turlarini [Massivlar, range va maxsus turlar darsida chuqur o'rganamiz.&&— "oraliqlar kesishadi" operatori.USING gist— cheklov GiST indeksi ustida ishlaydi (indeks turlarini Indeks turlari darsida ko'ramiz).btree_gistkengaytmasi GiST'ga oddiy=ni (int uchun) o'rgatadi — u Postgres bilan birga keladi.
11:30–12:30 1-xonada 10:00–12:00 bilan kesishdi — conflicting key value violates exclusion constraint ("ziddiyatli kalit qiymati istisno cheklovini buzadi"), DETAIL qaysi bron bilan to'qnashganini aytadi. Kod — 23P01 (exclusion_violation). 2-xonadagi 11:00–13:00 esa bemalol o'tdi — xona boshqa.
3.3 Parallel bronlar
Cheklov parallel holatda ham ishlaydi — u indeks darajasida tekshiriladi:
-- A
BEGIN;
INSERT INTO bron (xona, vaqt)
VALUES (3, '[2026-10-25 11:00+05, 2026-10-25 13:00+05)');
-- B
INSERT INTO bron (xona, vaqt)
VALUES (3, '[2026-10-25 12:00+05, 2026-10-25 14:00+05)');
-- A
COMMIT;Natija:
A=> BEGIN;
A BEGIN
A=> INSERT INTO bron (xona, vaqt)
A=> VALUES (3, '[2026-10-25 11:00+05, 2026-10-25 13:00+05)');
A INSERT 0 1
B=> INSERT INTO bron (xona, vaqt)
B=> VALUES (3, '[2026-10-25 12:00+05, 2026-10-25 14:00+05)');
A=> COMMIT;
A COMMIT
B ERROR: conflicting key value violates exclusion constraint "bron_xona_vaqt_excl"
B DETAIL: Key (xona, vaqt)=(3, ["2026-10-25 12:00:00+05","2026-10-25 14:00:00+05")) conflicts with existing key (xona, vaqt)=(3, ["2026-10-25 11:00:00+05","2026-10-25 13:00:00+05")).B A'ning commit qilinmagan broniga duch keldi va kutdi — A ROLLBACK qilsa, B o'tishi kerak edi. A COMMIT qildi — B 23P01 oldi.
Bir nozik joy. Biz 4 ta parallel bron INSERT ini (kanon tajribasidagi to'plam) 30 marta yurgizdik (PostgreSQL 17.6): 21 seansda faqat 23P01, lekin 9 seansda 40P01 (deadlock) ham chiqdi. Kesishgan bronlar bir-birini aylana bo'ylab kutib qolishi mumkin — kodda hech qanday FOR UPDATE bo'lmasa ham. Kanon fayli bitta seansni yozgan (unda 23P01). Xulosa: ilova ikkala kodni ham "bu vaqt band" deb tushunishi kerak — 40P01 da esa qayta urinish (Deadlock) bronni to'g'ri natijaga olib keladi: yo o'tadi, yo 23P01.
4. Idempotent yozish: bitta to'lov — bitta yozuv
4.1 Muammo
Mijoz to'lovni yubordi, server yozdi, lekin javob yo'lda yo'qoldi. Mijoz "o'tmadi" deb qayta yuboradi. Server uchun bu — yangi so'rov. Natija: ikki yozuv, pul ikki marta.
Idempotency-Key darsida HTTP tomonini ko'rdik: mijoz har amal uchun noyob kalit (Idempotency-Key sarlavhasi) yuboradi. Endi bazadagi kafolat: kalit ustunida UNIQUE cheklovi va ON CONFLICT DO NOTHING (UPSERT va MERGE).
CREATE TABLE tolov (
id serial PRIMARY KEY,
kalit uuid UNIQUE NOT NULL,
summa int NOT NULL
);
-- mijoz bir to'lovni ikki marta yubordi (tarmoq uzildi)
INSERT INTO tolov (kalit, summa)
VALUES ('6f1c2a8e-0b7d-4c1e-9a55-3b2f7e9d1a01', 35000)
ON CONFLICT (kalit) DO NOTHING
RETURNING id;
INSERT INTO tolov (kalit, summa)
VALUES ('6f1c2a8e-0b7d-4c1e-9a55-3b2f7e9d1a01', 35000)
ON CONFLICT (kalit) DO NOTHING
RETURNING id;
SELECT count(*) AS tolovlar FROM tolov;Natija:
CREATE TABLE
id
----
1
(1 row)
INSERT 0 1
id
----
(0 rows)
INSERT 0 0
tolovlar
----------
1
(1 row)Birinchisi yozildi (id = 1), ikkinchisi — INSERT 0 0, RETURNING bo'sh. Jadvalda bitta to'lov.
Parallel holatda ham shunday: kanon 5 ta bir xil so'rovni bir vaqtda yubordi — yozildi 1 | jadvalda 1 (t54-race.txt). UNIQUE indeksi parallel INSERT larni ham ketma-ket tekshiradi: ikkinchisi birinchisining commit'ini kutadi va keyin "bor ekan" deb o'tkazadi.
4.2 Javobni nima qaytaradi?
RETURNING bo'sh bo'lsa, ilova bilishi kerak: bu "takror". To'g'ri javob — birinchi so'rovning natijasi (masalan, o'sha to'lov), xato emas — mijoz shunday bo'lishini kutadi. Buning uchun takror holatda SELECT … WHERE kalit = $1 qilinadi. To'liq naqsh (kalit + so'rov hash'i, javobni saqlash, "hali bajarilmoqda") — 25-qism kanonidagi Idempotency-Key darsida.
Muhim: kalit mijozdan keladi va bir amal uchun doimiy. Server har so'rovda yangi gen_random_uuid() yaratsa — idempotentlik yo'q.
Tekshirib ko'ring: Nega "avval
SELECT … WHERE kalit = $1, topilmasaINSERT" yetarli emas?
Javob
Bu yana "o'qi → tekshir → yoz" poygasi: ikki parallel so'rov ikkalasi ham "topilmadi"ni ko'radi va ikkalasi INSERT qiladi. Faqat UNIQUE cheklovi kafolat beradi — u tekshiruv va yozishni bazaning ichida, bitta amalda bajaradi. SELECT keyin faqat "takror bo'lsa, eski javobni qaytarish" uchun kerak.
5. Hujumchi nigohi
Race condition — faqat tasodif emas, hujum usuli ham. Hujumchi bir martalik promo-kodni (masalan, "−20 000 so'm, bir marta") olib, bir soniyada 50 ta parallel so'rov yuboradi. Server kodi naiv bo'lsa: SELECT ishlatilganmi → false → chegirma berish → UPDATE ishlatildi = true. 50 so'rovning ko'pi "ishlatilmagan"ni ko'radi — chegirma 30–40 marta beriladi.
Bunday hujumlar uchun maxsus vositalar bor: so'rovlarni bitta tarmoq paketiga joylab, serverga millisekunddan kam farq bilan yetkazadi (veb xavfsizlik adabiyotida "single-packet attack" deb ataladi; kursda sinalmagan). Shuning uchun "foydalanuvchilar bunchalik tez bosmaydi" degan taxmin xavfsizlik kafolati emas.
Bu darsdagi kod qanday qarshi turadi:
- Bir martalik narsa (promo-kod, taklif havolasi, sovg'a) — atomik
UPDATE promo SET ishlatildi = true WHERE kod = $1 AND NOT ishlatildi;rowCount = 0— rad. - "Bitta foydalanuvchi — bitta marta" (bonus, ovoz berish) —
UNIQUE (foydalanuvchi_id, aksiya_id)+ON CONFLICT DO NOTHING. - Limitlar (kunlik yechish limiti, qoldiq) —
WHEREdagi shart +CHECK.
Umumiy qoida: cheklov ilova kodida emas, bazada bo'lsa — parallel so'rovlar uni aylanib o'ta olmaydi.
6. Qaysi vosita qachon
| Muammo | Vosita | Xato/belgi |
|---|---|---|
| Hisoblagich, qoldiq, balans | atomik UPDATE … WHERE shart |
rowCount = 0 |
| Murakkab "o'qi → qaror → yoz" | SELECT … FOR UPDATE |
kutish |
| Bir marta bo'ladigan narsa | UNIQUE + ON CONFLICT |
23505 yoki 0 qator |
| Kesishmaydigan oraliqlar | EXCLUDE USING gist |
23P01 |
| Foydalanuvchi tahriri | versiya ustuni |
rowCount = 0 |
| Murakkab invariantlar | SERIALIZABLE + qayta urinish | 40001 |
Oxirgi qator — izolyatsiya darsidagi "kamida bitta navbatchi" kabi qoidalar uchun, ularni bitta qator yoki cheklov bilan ifodalab bo'lmaganda.
7. Node qadami
Bu dars izolyatsiya darsidan boshlangan oltita darsning "parallel yozuvlar" qadamini yakunlaydi (holat 53). vazifalar-api da bu darsning naqshlari qayerda turibdi:
| Naqsh | vazifalar da |
Qaysi darsda qo'shildi |
|---|---|---|
| takrorni bazada taqiqlash | vazifalar_egasi_id_matn_excl — faol vazifalar orasida bir xil matn yo'q; parallel ikki POST — bittasi 23P01 → 409 |
Soft delete, Deadlock (014) |
| atomik hisoblagich | izohlar_soni trigger'da + 1/- 1 |
Denormalizatsiya |
| "o'qi → qaror → yoz" ni navbatga qo'yish | egasi qatori FOR UPDATE (#yoz) — tartib takrorlanmaydi |
Qulflar |
| foydalanuvchi tahriri | versiya + If-Match → 412 |
Optimistic concurrency |
| takror so'rov | Idempotency-Key — 25-qismdan, API qatlamida |
Idempotency-Key |
Hammasini birga tekshiradigan raqobat testi (test/pg-ombor.test.ts, holat 53; qisqartirildi):
test("raqobat (#53–#54): …", async () => {
await tozala(pool);
const o = new PgOmbori(pool, 1);
// 1) 30 parallel yarat — tartib takrorlanmaydi
const v = await Promise.all(Array.from({ length: 30 },
(_, i) => o.yarat({ matn: `p${String(i)}` })));
assert.equal(new Set(v.map((x) => x.tartib)).size, 30);
// 2) 60 aralash amal: har 5-chisi PUT (30 vazifa), qolgani PATCH
await Promise.all(Array.from({ length: 60 }, (_, i) =>
i % 5 === 0
? o.almashtirHammasi(/* 30 vazifa, bajarildi aralash */)
: o.yangila(1 + (i % 30), { bajarildi: i % 2 === 0 })));
// 3) bitta versiya bilan 10 PATCH — faqat 1 tasi o'tadi
const birVersiya = (await o.ol(1))?.versiya ?? 0;
const n = await Promise.all(Array.from({ length: 10 },
(_, i) => o.yangila(1, { matn: `t${String(i)}` },
{ versiya: birVersiya })));
assert.equal(n.filter((x) => x.tur === "ok").length, 1);
});Uch qism — uch xil poyga. (1) Parallel max(tartib) + 1 — egasi qulfi himoya qiladi. (2) PUT va PATCH teskari tartibda qulf olishi — deadlock bo'lmasligi kerak (Promise.all biror xato bilan yiqilsa, test ham yiqiladi). (3) Lost update — versiya sharti. Raqobat testlari ko'pincha "tasodifan" o'tib ketadi. Bu test esa holat 48 kodida (1) qismdayoq yiqilgan bo'lardi: o'sha kodda 50 dan atigi 8 xil tartib o'lchangan.
Kanon tekshiruvi (npm test, PostgreSQL 18.6; natijalar/tekshiruv-53.txt):
ℹ tests 83
ℹ pass 83
ℹ fail 0
ℹ skipped 083 testdan 16 tasi Postgres'ga xos (holat 48 da 81; +2: matn almashtirish va raqobat). Jonli tekshiruv (curl, 9 so'rov: ro'yxat, POST 201, takror POST 409, PATCH If-Match 200 / eski versiya 412, DELETE 204 …) — 9/9 kutilgan kodlar bilan. PUT 1 000 vazifa — 157 ms (holat 48 da 147 ms): egasi qulfi va qayta urinish o'rami deyarli narx qo'shmadi.
Holat 53 — commit (sxema fayli ham, chunki 014 shu qadamniki):
git add sql/014-matn-noyob-kechiktirilgan.sql \
src/vazifalar/pg-ombor.ts test/pg-ombor.test.ts
M="fix: yozuvlar egasi qulfi bilan bir tartibda (deadlock),"
M+=" 40P01/40001 qayta urinish"
git commit -m "$M" -m "Matn noyobligi EXCLUDE … DEFERRABLE (014)."Kanon skripti t54-race.mjs esa bu darsning o'zidagi ombor, idempotent to'lov va bronni tajriba bazasida sinagan. Bron qismining kanon chiqishi:
bron (parallel):
xona 1 [2026-10-25 10:00+05, 2026-10-25 12:00+05): ok
xona 1 [2026-10-25 11:00+05, 2026-10-25 13:00+05): 23P01 t54_bron_xona_vaqt_excl
xona 1 [2026-10-25 12:00+05, 2026-10-25 14:00+05): ok
xona 2 [2026-10-25 11:00+05, 2026-10-25 13:00+05): ok8. Ko'p uchraydigan xatolar
8.1 "Avval tekshiraman, keyin yozaman"
Har qanday SELECT → JS'da if → UPDATE/INSERT — parallel yukda buziladi (kanon: 100 ta sotildi). Shartni WHERE ga yoki cheklovga.
8.2 Tranzaksiya — poyganing davosi deb o'ylash
Naiv kod tranzaksiyada ham buziladi («Naiv kod» bo'limidagi psql misoli). Atomik UPDATE, FOR UPDATE yoki cheklov.
8.3 rowCount ga qaramaslik
UPDATE … WHERE qoldiq > 0 dan keyin buyurtmani baribir yozish. rowCount === 0 — ROLLBACK va "tugadi".
8.4 Bron xatosini faqat 23P01 deb kutish
Parallel bronda 40P01 ham keladi (biz o'lchadik: 30 dan 9 seans). Ikkalasini ham ushlash; 40P01 da qayta urinish.
8.5 Idempotency kalitini serverda yaratish
Har so'rov yangi kalit oladi — takror tanilmaydi. Kalit mijozda, amal boshida bir marta yaratiladi.
9. Mashqlar
1-mashq (oson): Vositani tanlang
Har holat uchun vosita tanlang. (a) Kinoteatrda bitta seansda bitta joy faqat bir marta sotiladi. (b) Payme hamyonidan pul yechish — balans manfiy bo'lmasin; (c) mehmonxona xonasi kunlar bo'yicha band qilinadi; (d) foydalanuvchi profilini ikki qurilmada tahrirlaydi.
Yechim
(a) UNIQUE (seans_id, joy) + ON CONFLICT DO NOTHING (2-mashq). (b) Atomik UPDATE … SET balans = balans - $2 WHERE id = $1 AND balans >= $2 + CHECK (balans >= 0). (c) EXCLUDE USING gist (xona WITH =, kunlar WITH &&) — daterange bilan. (d) versiya ustuni — Optimistic concurrency.
2-mashq (o'rta): Kinoteatr joyi
m54_chipta (seans_id, joy, xaridor) jadvalini UNIQUE (seans_id, joy) bilan yarating. Malika 7-seansda F12 ni oladi, keyin Jasur ham 7-seansda F12 ni olmoqchi bo'ladi, keyin Jasur 8-seansda F12 ni oladi. Har INSERT muvaffaqiyatli bo'lsa sotildi: <ism> qaytarsin, aks holda hech narsa. Oxirida sotilgan chiptalar ro'yxati. Eslatma: ON CONFLICT (ustunlar) DO NOTHING va RETURNING — UPSERT va MERGE, INSERT va RETURNING.
Yechim
DROP TABLE IF EXISTS m54_chipta;
CREATE TABLE m54_chipta (
id serial PRIMARY KEY,
seans_id int NOT NULL,
joy text NOT NULL,
xaridor text NOT NULL,
UNIQUE (seans_id, joy)
);
INSERT INTO m54_chipta (seans_id, joy, xaridor)
VALUES (7, 'F12', 'Malika')
ON CONFLICT (seans_id, joy) DO NOTHING
RETURNING 'sotildi: ' || xaridor;
INSERT INTO m54_chipta (seans_id, joy, xaridor)
VALUES (7, 'F12', 'Jasur')
ON CONFLICT (seans_id, joy) DO NOTHING
RETURNING 'sotildi: ' || xaridor;
INSERT INTO m54_chipta (seans_id, joy, xaridor)
VALUES (8, 'F12', 'Jasur')
ON CONFLICT (seans_id, joy) DO NOTHING
RETURNING 'sotildi: ' || xaridor;
SELECT seans_id || ' ' || joy || ' — ' || xaridor
FROM m54_chipta ORDER BY seans_id;
DROP TABLE m54_chipta;Biz ishga tushirdik (psql -X -q -A -t -f, PostgreSQL 17.6):
sotildi: Malika
sotildi: Jasur
7 F12 — Malika
8 F12 — JasurIkkinchi INSERT hech narsa qaytarmadi — joy band. RETURNING da ixtiyoriy ifoda yozish mumkin ('sotildi: ' || xaridor). Ilovada bo'sh RETURNING → "Bu joy allaqachon sotilgan, boshqasini tanlang".
3-mashq (qiyin): 50 xaridor
Kanon tajribasini kichraytirib takrorlang: qoldiq 10, 50 ta parallel xaridor (pool 20). Ikki usul: naiv (SELECT → JS'da tekshir → UPDATE) va atomik (UPDATE … WHERE qoldiq > 0 tranzaksiyada + buyurtma). Har usul uchun bitta qator: ortiqcha sotildimi (sotilgan > 10), atomikda — qoldiq ham. Eslatma: TRUNCATE — jadvalni tez tozalaydi; count(*)::int — pg count ni satr qaytaradi (node-postgres asoslari).
Yechim
import pg from "pg";
const pool = new pg.Pool({
connectionString: process.env.DATABASE_URL, max: 20 });
const kut = (ms) => new Promise((r) => setTimeout(r, ms));
await pool.query("DROP TABLE IF EXISTS m54_buyurtma, m54_mahsulot");
await pool.query(`CREATE TABLE m54_mahsulot
(id int PRIMARY KEY, qoldiq int NOT NULL)`);
await pool.query(`CREATE TABLE m54_buyurtma
(id serial PRIMARY KEY, mahsulot_id int NOT NULL)`);
async function naiv() {
const { rows: [m] } = await pool.query(
"SELECT qoldiq FROM m54_mahsulot WHERE id = 1");
if (m.qoldiq <= 0) return false;
await kut(1); // ilova ishi
await pool.query(
"UPDATE m54_mahsulot SET qoldiq = $1 WHERE id = 1",
[m.qoldiq - 1]);
await pool.query(
"INSERT INTO m54_buyurtma (mahsulot_id) VALUES (1)");
return true;
}
async function atomik() {
const c = await pool.connect();
try {
await c.query("BEGIN");
const r = await c.query(
`UPDATE m54_mahsulot SET qoldiq = qoldiq - 1
WHERE id = 1 AND qoldiq > 0`);
if (r.rowCount === 0) {
await c.query("ROLLBACK");
return false;
}
await c.query(
"INSERT INTO m54_buyurtma (mahsulot_id) VALUES (1)");
await c.query("COMMIT");
return true;
} finally {
c.release();
}
}
for (const [nom, fn] of [["naiv", naiv], ["atomik", atomik]]) {
await pool.query("TRUNCATE m54_mahsulot, m54_buyurtma");
await pool.query("INSERT INTO m54_mahsulot VALUES (1, 10)");
await Promise.all(Array.from({ length: 50 }, () => fn()));
const { rows: [h] } = await pool.query(
`SELECT (SELECT qoldiq FROM m54_mahsulot) AS qoldiq,
(SELECT count(*)::int FROM m54_buyurtma) AS sotildi`);
const ortiqcha = h.sotildi > 10 ? "ha" : "yo'q";
const qoldiq = nom === "atomik" ? `, qoldiq ${h.qoldiq}` : "";
console.log(`${nom}: ortiqcha sotildimi — ${ortiqcha}${qoldiq}`);
}
await pool.query("DROP TABLE m54_buyurtma, m54_mahsulot");
await pool.end();Biz ishga tushirdik (PostgreSQL 17.6, Node 24.21.0; 3 marta — natija bir xil):
naiv: ortiqcha sotildimi — ha
atomik: ortiqcha sotildimi — yo'q, qoldiq 0Naiv usulning aniq soni (nechta sotildi) har safar boshqacha bo'lishi mumkin — shuning uchun faqat "ortiqchami" deb chiqardik. Atomik — doim aniq 10 va 0. O'zingiz console.log(h) qilib, naiv usulda nechta sotilganini ko'ring.
10. Real ishda
- E-commerce (Uzum Market kabi do'konlar, chipta servislari): "flash sale"da qoldiq — atomik
UPDATEyoki alohida rezerv jadvali; ortiqcha sotish — klassik intervyu topshirig'i. - Bron tizimlari (mehmonxona, shifokor qabuli, majlis xonalari): exclusion constraint — Postgres'ning noyob kuchi; boshqa bazalarda buni ilova qulflari bilan qilishga to'g'ri keladi.
- To'lov integratsiyalari (Click, Payme, Stripe): callback'lar takror keladi — idempotency kaliti yoki tranzaksiya ID'si bo'yicha
UNIQUEmajburiy (29-qismdagi to'lov integratsiyasi). - Xavfsizlik testlari: race condition — bug bounty dasturlarida tez-tez topiladigan zaiflik turi.
Xulosa
- "O'qi → tekshir → yoz" parallel yukda doim buziladi: kanonda 10 ta qoldiqdan 100 ta sotildi.
- Atomik
UPDATE … WHERE qoldiq > 0— eng tez va oddiy davo (rowCount = 0— tugadi); murakkab shartda —FOR UPDATE. CHECK— oxirgi himoya chizig'i, poyganing o'zini to'xtatmaydi.- Kesishmaydigan bronlar —
EXCLUDE USING gist (xona WITH =, vaqt WITH &&), xato23P01; parallelda40P01ham bo'lishi mumkin. - Idempotent yozish — mijoz kaliti +
UNIQUE+ON CONFLICT DO NOTHING. - Cheklov bazada bo'lsa, hujumchining parallel so'rovlari uni aylanib o'ta olmaydi.
Keyingi dars: SKIP LOCKED bilan DB-asosli navbat — FOR UPDATE SKIP LOCKED bilan Redis'siz fon vazifalari navbatini quramiz.
Manbalar
- PostgreSQL 18 hujjati: Exclusion Constraints.
- PostgreSQL 18 hujjati: INSERT — ON CONFLICT Clause.
- PostgreSQL 18 hujjati: Range Types.
- PostgreSQL 18 hujjati: btree_gist.
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!