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

Optimistic concurrency: versiya ustuni bilan "kimdir o'zgartirib qo'ydi"ni aniqlash

Qisqacha: Optimistic concurrency (optimistik qulf) — qatorni tahrirlash paytida qulflamaymiz, faqat yozayotganda tekshiramiz: "men o'qigan versiya hali ham joyidami?". Har qatorda versiya ustuni bor, har o'zgarishda u +1 bo'ladi. Yozish — UPDATE … WHERE id = $1 AND versiya = $2: 0 qator o'zgarsa, demak kimdir sizdan oldin tahrirlagan — foydalanuvchiga xabar beriladi (HTTP'da 412 yoki 409). Bu "oxirgi yozgan yutadi" xatosini jim yo'qotishdan ochiq to'qnashuvga aylantiradi.

Bu darsda

  • "Oxirgi yozgan yutadi" (lost update) muammosini ikki brauzer oynasi misolida ko'rsatasiz.
  • versiya ustuni va WHERE versiya = $n bilan optimistik tekshiruv yozasiz.
  • rowCount = 0 ning ikki ma'nosini (topilmadi yoki konflikt) ajratasiz.
  • Nega bu tekshiruv parallel so'rovlarda ham ishonchli ekanini tushuntirasiz.
  • Optimistik va pessimistik (FOR UPDATE) usullardan qaysi birini qachon tanlashni bilasiz.

Oldin bilishingiz kerak: Qulflar: row, table va advisory, Read phenomena va izolyatsiya darajalari, HTTP caching va shartli so'rovlar.

1. Nega bu kerak?

Aziz vazifalar ilovasini ikki joyda ochib qo'ygan: telefonda va noutbukda. Ikkalasida ham "Portfolio saytiga blog yozish" vazifasi ko'rinib turibdi.

  1. Noutbukda u matnni tahrirlaydi: "Portfolio saytiga 2 ta blog yozish" — saqlaydi.
  2. Telefonda (sahifa yangilanmagan, eski matn ko'rinib turibdi) vazifani "bajarildi" deb belgilaydi. Ilova butun vazifani — eski matni bilan — serverga yuboradi.

Bazada nima qoladi? Buni psql da ikki sessiya bilan taqlid qilamiz. A — noutbuk, B — telefon. Natijalarni biz ishga tushirdik: PostgreSQL 17.6, vazifalar kanon bazasi (holat 40) nusxasida:

sql
-- A va B: vazifani ko'rsatish
SELECT id, matn, bajarildi, versiya FROM vazifalar WHERE id = 17;
-- A: matnni tahrirladi
UPDATE vazifalar
SET matn = 'Portfolio saytiga 2 ta blog yozish #portfolio'
WHERE id = 17;
-- B: eski forma bilan "bajarildi"
UPDATE vazifalar
SET matn = 'Portfolio saytiga blog yozish #portfolio',
    bajarildi = true, bajarilgan_vaqt = now()
WHERE id = 17;
-- natija
SELECT matn, bajarildi, versiya FROM vazifalar WHERE id = 17;

Natija (oxirgi SELECT):

text
                   matn                   | bajarildi | versiya
------------------------------------------+-----------+---------
 Portfolio saytiga blog yozish #portfolio | t         |       3
(1 row)

"2 ta" yo'qoldi. Noutbukdagi tahrir hech qanday xatosiz o'chib ketdi — telefon uni bilmagan holda ustidan yozdi. Bu — izolyatsiya darsidagi lost update, faqat endi u tranzaksiya ichida emas, foydalanuvchi vaqtida sodir bo'ldi: o'qish va yozish orasida bir necha daqiqa.

Bu yerda izolyatsiya darajasi yordam bermaydi. A va B ning o'qish va yozishi alohida HTTP so'rovlar, alohida tranzaksiyalar. Orada daqiqalar o'tadi — tranzaksiyani shuncha vaqt ochiq ushlab bo'lmaydi.

Hayotiy o'xshatish: umumiy Google hujjatini emas, bitta qog'oz shaklni ikki xodim to'ldiryapti. Har birida nusxa. Kimdir nusxasini kotibaga birinchi topshirdi. Ikkinchi xodim keyin keladi — kotiba shakl burchagidagi raqamga qaraydi: "sizning nusxangiz №1, arxivda esa allaqachon №2 bor. Avval yangisini o'qing". Bu raqam — versiya.

2. Ikki yondashuv: pessimistik va optimistik

Qulflar darsidagi SELECT … FOR UPDATE — pessimistik usul: "to'qnashuv bo'ladi deb kutaman, shuning uchun oldindan qulflayman". Formani ochish = qatorni qulflash.

Muammosi: foydalanuvchi formani 10 daqiqa ochiq qoldirishi mumkin. Shu vaqt davomida:

  • tranzaksiya va ulanish band (pool tez tugaydi — Connection pooling);
  • qator qulflangan — boshqa hamma kutadi;
  • MVCC bo'yicha ochiq tranzaksiya VACUUM'ni to'xtatadi;
  • HTTP so'rovlari orasida tranzaksiyani umuman ushlab bo'lmaydi — har so'rov alohida.

Optimistik usul: "to'qnashuv kamdan-kam bo'ladi — qulflamayman, faqat yozishda tekshiraman". O'qish paytida hech narsa band qilinmaydi. Yozishda bitta qo'shimcha shart.

Pessimistik (FOR UPDATE) Optimistik (versiya)
Qachon tekshiradi o'qishda qulflaydi yozishda solishtiradi
To'qnashuvda ikkinchisi kutadi ikkinchisi xato oladi
Uzoq "o'ylash" vaqti yaramaydi muammo emas
Mos joy qisqa, ko'p to'qnashuvli ish foydalanuvchi tahriri, API

3. versiya ustuni

3.1 Kanon sxemasida

vazifalar jadvalida bu ustun allaqachon bor — u Amaliyot: real loyiha sxemasi darsida (011-yakuniy.sql) aynan shu dars uchun qo'shilgan:

sql
ALTER TABLE vazifalar
  ADD COLUMN versiya integer NOT NULL DEFAULT 1;

CREATE OR REPLACE FUNCTION yangilangan_vaqtni_qoy() RETURNS trigger
LANGUAGE plpgsql AS $$
BEGIN
  NEW.yangilangan_vaqt := now();
  NEW.versiya := OLD.versiya + 1;   -- ilova versiyani o'zi oshirmaydi
  RETURN NEW;
END $$;

(Kanon faylidan; birinchi qator ikkiga bo'lingan.) Trigger'ni Soft delete, timestamps va tarix darsida ko'rgan edik — u yangilangan_vaqt ni qo'yardi. 011 unga bitta qator qo'shdi: har mazmunli tahrirda versiya +1. Trigger UPDATE OF matn, bajarildi, ustuvorlik, muddat, ota_id, ochirilgan_vaqt da ishlaydi — ya'ni izohlar_soni o'zgarsa, versiya oshmaydi (izoh qo'shish vazifani "tahrirlash" emas).

Nega versiyani trigger oshiradi, ilova emas? Chunki bazaga yozadigan har kod (API, admin skript, migratsiya) uni unutishi mumkin. Trigger bilan kafolat bazada. Darsning boshidagi misolda ham versiya 1 dan 3 ga o'sdi — ikki UPDATE, lekin hech kim uni tekshirmadi.

3.2 Tekshiruv bilan yozish

Endi xuddi shu voqea, lekin har UPDATE o'qilgan versiyani shart qilib qo'yadi:

sql
-- A va B o'qidi: versiya = 1
-- A
UPDATE vazifalar
SET matn = 'Portfolio saytiga 2 ta blog yozish #portfolio'
WHERE id = 17 AND versiya = 1
RETURNING versiya;
-- B
UPDATE vazifalar
SET bajarildi = true, bajarilgan_vaqt = now()
WHERE id = 17 AND versiya = 1
RETURNING versiya;

Natija:

text
A=> UPDATE vazifalar
A=> SET matn = 'Portfolio saytiga 2 ta blog yozish #portfolio'
A=> WHERE id = 17 AND versiya = 1
A=> RETURNING versiya;
A    versiya
A   ---------
A          2
A   (1 row)
A   UPDATE 1
B=> UPDATE vazifalar
B=> SET bajarildi = true, bajarilgan_vaqt = now()
B=> WHERE id = 17 AND versiya = 1
B=> RETURNING versiya;
B    versiya
B   ---------
B   (0 rows)
B   UPDATE 0

A yozdi, yangi versiya 2 (trigger oshirdi, RETURNING uni qaytardi). B — UPDATE 0: versiya = 1 sharti bilan qator topilmadi. B'ning eski ma'lumotga asoslangan tahriri yozilmadi. A'ning "2 ta" si saqlandi.

Endi B (telefon) nima qiladi? Yangi holatni o'qiydi va foydalanuvchiga ko'rsatadi: "Vazifa boshqa joyda o'zgartirilgan. Yangi matn: …". Foydalanuvchi qayta tasdiqlasa — yangi versiya bilan yuboradi:

sql
-- B
SELECT versiya FROM vazifalar WHERE id = 17;
UPDATE vazifalar
SET bajarildi = true, bajarilgan_vaqt = now()
WHERE id = 17 AND versiya = 2
RETURNING matn, bajarildi, versiya;

Natija:

text
B=> SELECT versiya FROM vazifalar WHERE id = 17;
B    versiya
B   ---------
B          2
B   (1 row)
B=> UPDATE vazifalar
B=> SET bajarildi = true, bajarilgan_vaqt = now()
B=> WHERE id = 17 AND versiya = 2
B=> RETURNING matn, bajarildi, versiya;
B                        matn                      | bajarildi | versiya
B   -----------------------------------------------+-----------+---------
B    Portfolio saytiga 2 ta blog yozish #portfolio | t         |       3
B   (1 row)
B   UPDATE 1

Ikkala tahrir ham saqlandi: matnda "2 ta", bajarildi = t, versiya 3.

Yana bir farqqa e'tibor bering: B ikkinchi marta faqat o'zgargan maydonni (bajarildi) yubordi, butun obyektni emas. Bu ham lost update'ni kamaytiradi (HTTP'da PATCH, PUT emas — 25-qism). Lekin to'liq himoya — faqat versiya bilan: ikki kishi bitta maydonni o'zgartirsa, PATCH ham yordam bermaydi.

sequenceDiagram
  participant A as Noutbuk
  participant DB as Postgres
  participant B as Telefon
  A->>DB: o'qidi (versiya 1)
  B->>DB: o'qidi (versiya 1)
  A->>DB: UPDATE ... versiya = 1
  DB-->>A: UPDATE 1, versiya 2
  B->>DB: UPDATE ... versiya = 1
  DB-->>B: UPDATE 0 — konflikt
  B->>DB: qayta o'qidi (versiya 2)
  B->>DB: UPDATE ... versiya = 2
  DB-->>B: UPDATE 1, versiya 3

Tekshirib ko'ring: Ilova UPDATE … SET versiya = $2 + 1 WHERE id = $1 deb yozsa (versiyani o'zi hisoblab, lekin WHERE da tekshirmasdan), konflikt aniqlanadimi?

Javob

Yo'q. Versiya faqat WHERE da ishlaydi. SET dagi qiymat hech narsani tekshirmaydi: ikkala oyna ham versiya = 2 yozadi va ikkalasi ham "muvaffaqiyatli" bo'ladi — lost update qaytdi. Bundan tashqari, kanon sxemasida versiyani trigger qo'yadi va ilova yozgan qiymat baribir almashtiriladi.

4. rowCount = 0 ning ikki ma'nosi

UPDATE … WHERE id = $1 AND versiya = $2 0 qator qaytarsa, ikki sabab bo'lishi mumkin:

  1. Vazifa yo'q (o'chirilgan yoki hech qachon bo'lmagan) — HTTP'da 404 Not Found.
  2. Vazifa bor, versiya boshqa — konflikt: 412 Precondition Failed yoki 409 Conflict (pastda).

UPDATE ning o'zi ularni ajratmaydi. Eng sodda yo'l — 0 qatordan keyin ikkinchi so'rov: SELECT versiya FROM vazifalar WHERE id = $1. Qator yo'q — 404; bor — konflikt, va siz joriy versiyani ham bilasiz (javobga qo'shish uchun).

Kanon (vazifalar) da yana bir holat bor — yumshoq o'chirilgan vazifa (ochirilgan_vaqt IS NOT NULL). Kanon tajribasidagi repository funksiyasi uni ham shartga qo'shgan (AND ochirilgan_vaqt IS NULL) — Savatdagi vazifani tahrirlab bo'lmaydi.

Ikki so'rov o'rniga bitta so'rov bilan ham ajratish mumkin — data-modifying CTE bilan (CTE (WITH)). Bu 2-mashqda.

Diqqat: 0 qatordan keyingi SELECT va UPDATE bitta lahzada emas — orada vazifa o'chib ketishi mumkin. Bu xavfli emas: bu SELECT faqat xato turini tanlash uchun. Ma'lumot baribir buzilmaydi, chunki yozish WHERE versiya = $2 bilan himoyalangan.

5. Parallel yozuvlarda ham ishonchlimi?

Shubha tug'ilishi tabiiy: ikki UPDATE aynan bir vaqtda kelsa-chi? Ikkalasi ham versiya = 3 ni ko'rib, ikkalasi ham yozib yubormaydimi?

Tekshiramiz. A tranzaksiya ichida yozadi va COMMIT ni kechiktiradi. B o'sha versiya bilan yozishga urinadi:

Qadam A sessiya B sessiya Natija
1 BEGIN; va UPDATE … SET ustuvorlik = 'yuqori' WHERE id = 17 AND versiya = 3 RETURNING versiya; A: versiya 4, COMMIT yo'q
2 UPDATE … SET ustuvorlik = 'past' WHERE id = 17 AND versiya = 3 RETURNING versiya; B kutadi (qator qulfi)
3 COMMIT; B uyg'onadi — nechta qator?

Transkriptda B ning javobi A ning COMMIT idan keyin chiqqaniga qarang:

text
A=> BEGIN;
A   BEGIN
A=> UPDATE vazifalar SET ustuvorlik = 'yuqori'
A=> WHERE id = 17 AND versiya = 3
A=> RETURNING versiya;
A    versiya
A   ---------
A          4
A   (1 row)
A   UPDATE 1
B=> UPDATE vazifalar SET ustuvorlik = 'past'
B=> WHERE id = 17 AND versiya = 3
B=> RETURNING versiya;
A=> COMMIT;
A   COMMIT
B    versiya
B   ---------
B   (0 rows)
B   UPDATE 0

B qator qulfini kutdi (A uni yozayotgan edi). A COMMIT qilgach, B uyg'ondi va — muhim joy — WHERE shartini qatorning yangi versiyasida qayta tekshirdi: endi versiya = 4, shart yolg'on → UPDATE 0. Bu READ COMMITTED'ning xatti-harakati: qulf kutgan UPDATE eng yangi versiyani qayta o'qiydi (izolyatsiya darsidagi atomik balans + 20 ham shu sababli to'g'ri ishlagan edi).

Demak UPDATE … WHERE versiya = $n — atomik "solishtir va almashtir" (compare-and-swap). Kanon buni katta sinovda tekshirgan: 20 ta parallel tahrir, hammasi bir xil versiya bilan — 1 muvaffaqiyat, 19 konflikt (t52-optimistik.txt).

REPEATABLE READ'da B UPDATE 0 o'rniga 40001 could not serialize access due to concurrent update oladi — natija bir xil (yozilmadi), faqat xato turi boshqa.

6. HTTP bilan bog'lash: ETag va If-Match

HTTP caching va shartli so'rovlar darsida ETag va If-Match sarlavhalarini ko'rgan edik. Optimistik qulf ular bilan tabiiy bog'lanadi:

  1. GET /v1/vazifalar/17 — javobda ETag: "3" (versiya).
  2. Mijoz tahrirni yuboradi: PATCH /v1/vazifalar/17 + If-Match: "3".
  3. Server UPDATE … WHERE id = 17 AND versiya = 3 qiladi.
  4. 1 qator — 200 OK, yangi ETag: "4". 0 qator va vazifa bor — 412 Precondition Failed: "siz qo'ygan shart bajarilmadi". 0 qator va vazifa yo'q — 404.

412 — If-Match sarlavhasi uchun standart javob (RFC 9110). 25-qism kanoni aynan shunday qilgan: If-Match mos kelmasa — 412, kod: versiya-mos-emas, javobda joriy ETag.

Ba'zi API'lar versiyani sarlavhada emas, so'rov tanasida oladi ({ "matn": "…", "versiya": 3 }). U holda odatda 409 Conflict qaytariladi — "resursning joriy holati bilan ziddiyat". Ikkalasi ham to'g'ri, faqat bitta API ichida bittasini tanlang. Kanon B tajribasi 409 deb yozgan; 25-qism API'si If-Match bilan ishlaydi — u yerda 412.

Kanon tajribasidagi ETag shakli W/"1-3" (id va versiya) — faqat g'oya edi. 25-qism kanoni kuchli "<versiya>" ni tanlagan: If-Match kuchli solishtirish talab qiladi, W/ li ETag u yerda doim 412 beradi (RFC 9110).

7. Node'da: repository funksiyasi

Kanon tajribasi (t52-optimistik.mjs) — repository darajasidagi funksiya; SQL qatorlarga bo'lingan:

js
async function tahrirla(id, kutilganVersiya, ozgarish) {
  const r = await p.query(
    `UPDATE vazifalar
     SET matn = coalesce($3, matn),
         bajarildi = coalesce($4, bajarildi),
         bajarilgan_vaqt = CASE WHEN coalesce($4, bajarildi)
           THEN coalesce(bajarilgan_vaqt, now()) END
     WHERE id = $1 AND versiya = $2 AND ochirilgan_vaqt IS NULL
     RETURNING id, matn, bajarildi, versiya`,
    [id, kutilganVersiya, ozgarish.matn ?? null,
     ozgarish.bajarildi ?? null]);
  return r.rows[0] ?? null;
}

Nimaga qarang:

  • coalesce($3, matn) — maydon yuborilmagan bo'lsa (null), eski qiymat qoladi. Bu PATCH semantikasi.
  • bajarilgan_vaqt — CHECK bajarildi = (bajarilgan_vaqt IS NOT NULL) (ALTER TABLE darsidagi cheklov) buzilmasligi uchun birga hisoblanadi.
  • null qaytsa — chaqiruvchi konflikt yoki topilmadi ekanini aniqlaydi («rowCount = 0 ning ikki ma'nosi» bo'limi).

Kanon chiqishi (PostgreSQL 18.6, Node 24.21.0, pg 8.23.1):

text
ikkala oyna o'qidi: {
  id: '17',
  matn: 'Portfolio saytiga blog yozish #portfolio',
  bajarildi: false,
  versiya: 1
}
oyna 1 matnni o'zgartiradi: {
  id: '17',
  matn: 'Portfolio saytiga 2 ta blog yozish #portfolio',
  bajarildi: false,
  versiya: 2
}
oyna 2 (eski versiya 1) bajarildi qiladi: 0 qator → KONFLIKT (API: 409 Conflict, yangi holatni qayta o'qing)
oyna 2 qayta o'qidi va qayta urindi: {
  id: '17',
  matn: 'Portfolio saytiga 2 ta blog yozish #portfolio',
  bajarildi: true,
  versiya: 3
}
20 parallel tahrir (versiya 2): muvaffaqiyatli 1, konflikt 19; yakuniy versiya 3
ETag: W/"1-3" — If-Match bilan kelgan versiya UPDATE ... WHERE versiya = $2 ga beriladi

id: '17' — satr: bigint ni pg satr qaytaradi; versiya: 1 — son (integer). node-postgres asoslari darsida id uchun xavfsiz tur parseri ko'rsatilgan.

7.1 Node qadami: vazifalar-api da versiya sharti

25-qism API'si optimistik qulfni allaqachon biladi: GET /v1/vazifalar/:id javobida ETag: "<versiya>", PATCH va DELETE esa If-Match ni qabul qiladi. Xotira ombori versiyani JS'da solishtirardi. Postgres ombori (PgOmbori, birinchi variant — holat 44) shu shartni SQL'ning WHERE iga ko'chirdi. Uch qatlam:

Qatlam Fayl Vazifasi
marshrut marshrut.ts, shartli.ts If-Match: "3" → { versiya: 3 }; sarlavha yo'q yoki * → shartsiz
xizmat xizmat.ts natija turi → HTTP: topilmadi → 404, konflikt → 412 + joriy ETag
ombor pg-ombor.ts UPDATE … WHERE … AND versiya = $5; 0 qator bo'lsa — #nega

Ombor metodi (holat 53 dagi yakuniy shakli; qisqartirildi):

ts
async yangila(id: number, ozgarish: VazifaOzgarishi, shart?: Shart) {
  return this.#yoz(async (c) => {
    const [q] = (await c.query<VazifaQatori>(
      `UPDATE vazifalar AS v
       SET matn = coalesce($3, matn),
           bajarildi = coalesce($4, bajarildi)
       WHERE id = $1 AND egasi_id = $2
         AND ochirilgan_vaqt IS NULL
         AND ($5::int IS NULL OR versiya = $5)
       RETURNING ${QATOR}`,
      [id, this.#egasi, ozgarish.matn ?? null,
       ozgarish.bajarildi ?? null, shart?.versiya ?? null])).rows;
    if (q === undefined) return this.#nega(c, id);
    await this.#versiyaniOshir(c);
    return { tur: "ok", qiymat: vazifa(q) };
  });
}

Darsdagi tahrirla dan uch farq:

  • ($5::int IS NULL OR versiya = $5) — If-Match kelmasa, $5 = null va shart o'chadi (shartsiz PATCH); kelsa — compare-and-swap. ::int — null parametrning turini Postgres'ga aytadi.
  • egasi_id = $2 — boshqa foydalanuvchining vazifasini tahrirlab bo'lmaydi; u "topilmadi" bo'ladi (auth — 28-qism; hozircha egasi EGASI_ID dan).
  • #nega(c, id) — «rowCount = 0 ning ikki ma'nosi» bo'limidagi ikkinchi SELECT: vazifa bormi? Bor — { tur: "konflikt", joriy }, yo'q — { tur: "topilmadi" }. Holat 53 da u o'sha tranzaksiyada bajariladi — #yoz bergan c bilan (Qulflar darsining «Node qadami»).

Jonli tekshiruv — kanon serveri SAQLASH=postgres bilan, curl (natijalar/tekshiruv-53.txt). Avval If-Match: "1" bilan 9-vazifani bajarildi qilamiz, keyin xuddi shu eski versiya bilan yana:

text
$ curl -X PATCH -H Content-Type: application/merge-patch+json -H If-Match: "1" -d {"bajarildi":true} http://127.0.0.1:4653/v1/vazifalar/9
HTTP/1.1 200 OK
ETag: "2"

$ curl -X PATCH -H Content-Type: application/merge-patch+json -H If-Match: "1" -d {"bajarildi":false} http://127.0.0.1:4653/v1/vazifalar/9
HTTP/1.1 412 Precondition Failed
ETag: "2"
Content-Type: application/problem+json; charset=utf-8

(Chiqish qisqartirildi: boshqa sarlavhalar va javob tanasi olib tashlandi.) Nima bo'ldi? Birinchi PATCH versiyani 2 ga ko'tardi — javobda yangi ETag: "2". Ikkinchisi eskirgan "1" bilan keldi: WHERE versiya = 1 0 qator, #nega "vazifa bor" dedi — 412. Javob tanasi (Problem Details) shunday: "detail":"Vazifa 9 boshqa so'rov bilan o'zgargan (joriy versiya 2) — qayta o'qing". Mijoz joriy ETag ni sarlavhadan olib, qayta o'qiydi.

Kanonning API o'lchovida ham o'sha 20 parallel tahrir bor (olchov/raqobat-olchov.ts, holat 53, PgOmbori orqali): 20 parallel yangila (versiya 1): ok 1, konflikt 19, yakuniy versiya 2 — darsdagi kanon tajribasi bilan bir xil. Holat 53 ning commit'i — Amaliyot: race condition'larni hal qilish darsida.

8. Muqobil "versiyalar" va qachon nima

Versiya o'rniga nima ishlatsa bo'ladi?

  • yangilangan_vaqt — ko'p loyihalar shuni ishlatadi. Kamchiligi: bir mikrosoniyada ikki yozuv (yoki soat orqaga ketsa) — farq sezilmaydi; solishtirishda formatlash (millisekund yo'qolishi) xatolar beradi. Butun son ishonchliroq.
  • Tizim ustuni xmin — MVCC darsidagi "versiyani yaratgan tranzaksiya" har UPDATE'da o'zgaradi. Ba'zi .NET kutubxonalari uni versiya sifatida ishlatadi (hujjat bo'yicha; kursda sinalmagan). Kamchiligi: izoh qo'shilganda ham o'zgaradi (izohlar_soni UPDATE'i) — keraksiz konfliktlar; ichki tafsilotga bog'lanish.
  • Butun yozuvning hash'i — ustun qo'shmasdan; lekin har tekshiruvda hisoblash kerak.

Qachon optimistik emas? To'qnashuv tez-tez bo'lsa. Masalan, chiptalar sotuvi: 1 000 kishi bir vaqtda oxirgi 10 joyni oladi — optimistik usulda 990 tasi konflikt oladi va qayta urinadi, qayta-qayta. Bunday joyda atomik UPDATE … SET qoldiq = qoldiq - 1 WHERE qoldiq > 0 yoki pessimistik qulf yaxshiroq. Buni keyingi amaliyot darsida o'lchaymiz.

Holat Tavsiya
Foydalanuvchi formani tahrirlaydi optimistik (versiya)
Hisoblagich, qoldiq, balans atomik UPDATE
Qisqa "o'qi → qaror → yoz" ko'p to'qnashuv bilan pessimistik FOR UPDATE

Tekshirib ko'ring: Vazifaga izoh qo'shildi (izohlar_soni +1). Shu paytda boshqa oynada vazifa matni tahrirlanmoqda (versiya 5 bilan). Tahrir konflikt oladimi?

Javob

Yo'q. Kanon trigger'i versiyani faqat mazmunli ustunlar (matn, bajarildi, ustuvorlik, muddat, ota_id, ochirilgan_vaqt) o'zgarganda oshiradi, izohlar_soni esa ro'yxatda yo'q (kanon review'i ham tekshirgan: izoh versiyani oshirmaydi). Versiya 5 da qoladi — tahrir o'tadi. Agar versiya o'rniga xmin ishlatilganda edi — konflikt bo'lardi, garchi hech kim matnga tegmagan bo'lsa ham.

9. Ko'p uchraydigan xatolar

9.1 rowCount ni tekshirmaslik

await pool.query("UPDATE … WHERE versiya = $2") — natijaga qaramasdan "Saqlandi!" deyish. Konflikt jim yo'qoladi — xuddi versiyasiz holatdagidek. rowCount === 0 → konflikt/topilmadi tarmog'i.

9.2 Versiyani faqat SET da ishlatish

«Tekshirib ko'ring» savolidagi xato. Shart — WHERE da.

9.3 Mijozga versiyani bermaslik

Mijoz versiyani bilmasa, uni qaytarib yubora olmaydi. Har GET javobida versiya (tanada yoki ETag da), har yozishda — qaytariladi.

9.4 Konfliktda avtomatik qayta yozish

"Versiya mos kelmadi — mayli, yangi versiya bilan qayta yozib qo'yaman" — bu yana "oxirgi yozgan yutadi". Konfliktda foydalanuvchiga yangi holatni ko'rsating yoki o'zgarishlarni maydon bo'yicha birlashtiring (faqat bir-biriga tegmaydigan maydonlar). Avtomatik qayta urinish — faqat tranzaksiya xatolari (40001) uchun, va u ham butun "o'qi → hisobla → yoz" ni qaytadan bajaradi.

9.5 Versiyani ilova oshirishi

Admin skript versiya ni oshirishni unutsa, keyingi tahrirlar eski versiya bilan o'tib ketadi. Kanondagidek — trigger.

10. Mashqlar

1-mashq (oson): Natijani ayting

Vazifa 17 ning versiyasi 7. Uchta so'rov ketma-ket (har biri autocommit) keladi: (1) … WHERE id = 17 AND versiya = 7, (2) … WHERE id = 17 AND versiya = 7, (3) … WHERE id = 17 AND versiya = 8. Har biri nechta qatorni o'zgartiradi? Oxirida versiya nechchi?

Yechim

(1) — 1 qator, versiya 8 bo'ladi (trigger). (2) — 0 qator: versiya allaqachon 8 — konflikt. (3) — 1 qator, versiya 9. Oxirida — 9. Ikkinchi so'rov yuborgan mijoz yangi holatni o'qib, 9 bilan qayta urinishi kerak.

2-mashq (o'rta): Bitta so'rovda uch natija

m52_vazifa (id, matn, versiya) jadvalini yarating. PREPARE tahrir(int, int, text) — bitta so'rov: data-modifying CTE ichida UPDATE … WHERE id = $1 AND versiya = $2 RETURNING versiya, asosiy SELECT esa natijani qaytarsin: ok, versiya N, topilmadi yoki konflikt, joriy versiya N. To'rt chaqiruv bilan sinang. Eslatma: CTE va EXISTS — CTE (WITH), Subquery; PREPARE nom(turlar) AS … — serverda nomlangan so'rov, $1, $2 — uning parametrlari (Parametrli so'rovlar va SQL injection); EXECUTE nom(qiymatlar) uni bajaradi.

Yechim
sql
DROP TABLE IF EXISTS m52_vazifa;
CREATE TABLE m52_vazifa (
  id int PRIMARY KEY,
  matn text NOT NULL,
  versiya int NOT NULL DEFAULT 1
);
INSERT INTO m52_vazifa (id, matn) VALUES (1, 'Non olish');

PREPARE tahrir(int, int, text) AS
WITH y AS (
  UPDATE m52_vazifa SET matn = $3, versiya = versiya + 1
  WHERE id = $1 AND versiya = $2
  RETURNING versiya
)
SELECT CASE
  WHEN EXISTS (SELECT 1 FROM y)
    THEN 'ok, versiya ' || (SELECT versiya FROM y)
  WHEN NOT EXISTS (SELECT 1 FROM m52_vazifa WHERE id = $1)
    THEN 'topilmadi'
  ELSE 'konflikt, joriy versiya '
       || (SELECT versiya FROM m52_vazifa WHERE id = $1)
END AS natija;

EXECUTE tahrir(1, 1, 'Non va sut olish');
EXECUTE tahrir(1, 1, 'Non va qatiq olish');
EXECUTE tahrir(99, 1, 'Yo''q vazifa');
EXECUTE tahrir(1, 2, 'Non va qatiq olish');
SELECT matn || ' (v' || versiya || ')' FROM m52_vazifa;
DROP TABLE m52_vazifa;

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

text
ok, versiya 2
konflikt, joriy versiya 2
topilmadi
ok, versiya 3
Non va qatiq olish (v3)

Bu yerda trigger yo'q, shuning uchun versiya = versiya + 1 ni SET ga yozdik — tekshiruv baribir WHERE da. Diqqat: asosiy SELECT jadvalni UPDATE oldidagi snapshot bilan ko'radi (CTE darsidagi "CTE'dagi UPDATE asosiy so'rovga ko'rinmaydi" topilmasi). Shuning uchun muvaffaqiyatli holatda yangi versiyani jadvaldan emas, y dan olamiz. Konflikt tarmog'ida esa jadvaldagi qiymat — haqiqiy joriy versiya, chunki UPDATE hech narsani o'zgartirmagan.

3-mashq (qiyin): Natija turi bilan repository

Node'da yangila(id, versiya, ozgarish) funksiyasini yozing. U 25-qism kanonidagi YozishNatijasi shaklini qaytarsin: { tur: "ok", qiymat }, { tur: "topilmadi" } yoki { tur: "konflikt", joriy }. Uch holatni sinang, keyin 20 ta parallel tahrirni (hammasi versiya 2 bilan) yuborib, nechta ok va konflikt bo'lganini sanang. Eslatma: coalesce — NULL mantiqi; Promise.all va Array.from({ length }) — JS asinxron darslaridan.

Yechim
js
import pg from "pg";

const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL, max: 20 });
await pool.query("DROP TABLE IF EXISTS m52_vazifa");
await pool.query(`CREATE TABLE m52_vazifa (
  id int PRIMARY KEY, matn text NOT NULL,
  bajarildi boolean NOT NULL DEFAULT false,
  versiya int NOT NULL DEFAULT 1)`);
await pool.query(
  "INSERT INTO m52_vazifa (id, matn) VALUES (1, 'Non olish')");

async function yangila(id, versiya, ozgarish) {
  const r = await pool.query(
    `UPDATE m52_vazifa
     SET matn = coalesce($3, matn),
         bajarildi = coalesce($4, bajarildi),
         versiya = versiya + 1
     WHERE id = $1 AND versiya = $2
     RETURNING id, matn, bajarildi, versiya`,
    [id, versiya, ozgarish.matn ?? null,
     ozgarish.bajarildi ?? null]);
  if (r.rowCount === 1) return { tur: "ok", qiymat: r.rows[0] };
  // 0 qator: yo vazifa yo'q, yo versiya eskirgan — qaysi biri?
  const j = await pool.query(
    "SELECT versiya FROM m52_vazifa WHERE id = $1", [id]);
  if (j.rowCount === 0) return { tur: "topilmadi" };
  return { tur: "konflikt", joriy: j.rows[0].versiya };
}

console.log(await yangila(1, 1, { matn: "Non va sut olish" }));
console.log(await yangila(1, 1, { bajarildi: true }));
console.log(await yangila(7, 1, { bajarildi: true }));

// 20 ta parallel tahrir, hammasi versiya 2 ni ko'rgan
const natijalar = await Promise.all(
  Array.from({ length: 20 }, (_, i) =>
    yangila(1, 2, { matn: `Tahrir ${i}` })));
const sana = (tur) => natijalar.filter((n) => n.tur === tur).length;
console.log(`ok: ${sana("ok")}, konflikt: ${sana("konflikt")}`);
const oxir = await pool.query("SELECT versiya FROM m52_vazifa");
console.log(`yakuniy versiya: ${oxir.rows[0].versiya}`);
await pool.query("DROP TABLE m52_vazifa");
await pool.end();

Biz ishga tushirdik (PostgreSQL 17.6, Node 24.21.0, pg 8.23.1):

text
{
  tur: 'ok',
  qiymat: { id: 1, matn: 'Non va sut olish', bajarildi: false, versiya: 2 }
}
{ tur: 'konflikt', joriy: 2 }
{ tur: 'topilmadi' }
ok: 1, konflikt: 19
yakuniy versiya: 3

20 dan faqat bittasi o'tdi — kanon natijasi bilan bir xil. Natija turi (tur) HTTP'ni bilmaydi: API qatlami uni 200 / 404 / 412 ga aylantiradi. Repository'ning vazifasi — faqat "nima bo'ldi"ni aniq aytish. id bu jadvalda int — shuning uchun son; kanon vazifalar da bigint — satr bo'lardi.

11. Real ishda

  • Hamkorlikdagi tahrir: CRM, admin panellar, Notion/Jira kabi tizimlar "Bu yozuv boshqa foydalanuvchi tomonidan o'zgartirildi" oynasini aynan shu usul bilan ko'rsatadi.
  • ORM'lar: Prisma'da update({ where: { id, versiya } }) naqshi, TypeORM'da @VersionColumn, Hibernate'da @Version — hammasi WHERE versiya = ? (27-qism).
  • REST API: ETag + If-Match → 412 — GitHub, Google Cloud API'lari shu standartdan foydalanadi.
  • Intervyu: "Optimistic va pessimistic locking farqi?", "Lost update'dan qanday himoyalanasiz?" — deyarli har backend intervyusida.

Xulosa

  • Foydalanuvchi vaqtidagi lost update'ni izolyatsiya darajasi tuzatmaydi — o'qish va yozish alohida so'rovlarda.
  • Optimistik usul: versiya ustuni + UPDATE … WHERE id = $1 AND versiya = $2; 0 qator — kimdir oldin yozgan.
  • Kanon sxemasida versiyani trigger oshiradi; izoh qo'shish versiyani o'zgartirmaydi.
  • rowCount = 0 — topilmadi yoki konflikt; ikkinchi SELECT (yoki CTE) ajratadi.
  • READ COMMITTED'da WHERE versiya = $n atomik: kutgan UPDATE shartni yangi versiyada qayta tekshiradi (20 parallel → 1 ok).
  • HTTP'da: ETag: "versiya" + If-Match → 412; versiya tanada bo'lsa — ko'pincha 409.

Keyingi dars: Deadlock va serialization failure bilan ishlash — ba'zi xatolar normal: 40P01 va 40001 ni ushlab, tranzaksiyani to'g'ri qayta urinishni o'rganamiz.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Optimistic concurrency: versiya ustuni bilan "kimdir o'zgartirib qo'ydi"ni aniqlash — IlmHamroh