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

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 (yoki SELECT … 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 UPDATE va FOR UPDATE bilan 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.
  • CHECK cheklovini 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:

  1. Ombor. "Bahor" oshxonasi onlayn do'konida pishiriq kitobidan 10 ta qolgan. Aksiya boshlandi — 100 kishi bir soniyada "Sotib olish" tugmasini bosdi.
  2. Bron. Majlislar xonasi soat 10:00–12:00 ga band qilindi. Shu paytda boshqa xodim 11:00–13:00 ni band qilmoqda.
  3. 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:

js
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:

text
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):

text
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
Qoldiq 10, 100 parallel xaridor: nechta sotildi
  • 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 — yuqoridagi psql misolida A va B tranzaksiyada edi va baribir ikkalasi sotdi.

2.3 Yechim 1: atomik UPDATE

Tekshirish va yozishni bitta buyruqqa birlashtiramiz:

sql
-- 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:

text
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:

js
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):

js
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:

sql
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:

text
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.

sql
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:

text
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 (timestamptz lar 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_gist kengaytmasi 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:

sql
-- 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:

text
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).

sql
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:

text
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, topilmasa INSERT" 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) — WHERE dagi 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):

ts
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):

text
ℹ tests 83
ℹ pass 83
ℹ fail 0
ℹ skipped 0

83 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):

bash
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:

text
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): ok

8. 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
sql
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):

text
sotildi: Malika
sotildi: Jasur
7 F12 — Malika
8 F12 — Jasur

Ikkinchi 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
js
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):

text
naiv: ortiqcha sotildimi — ha
atomik: ortiqcha sotildimi — yo'q, qoldiq 0

Naiv 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 UPDATE yoki 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 UNIQUE majburiy (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 &&), xato 23P01; parallelda 40P01 ham 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

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Race condition amaliyoti: ombor qoldig'i, bron va idempotent to'lov PostgreSQL'da — IlmHamroh