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

PostgreSQL qulflari: SELECT FOR UPDATE, jadval qulflari va advisory lock

Qisqacha: MVCC o'quvchi va yozuvchini ajratadi. Lekin ikki yozuvchi bitta qatorda baribir to'qnashadi: ikkinchisi birinchisining COMMITini kutadi. SELECT … FOR UPDATE bilan qatorni o'qish paytidayoq qulflab olasiz: "men buni o'zgartiraman, boshqalar kutsin". NOWAIT va lock_timeout kutishni cheklaydi, SKIP LOCKED band qatorni o'tkazib yuboradi. Jadval darajasida ALTER TABLE hatto oddiy SELECT ni ham to'xtatadi. Advisory lock esa — jadvalga bog'lanmagan, ilova ma'nosidagi qulf ("bu foydalanuvchining importi bir vaqtda bitta").

Bu darsda

  • FOR UPDATE, FOR NO KEY UPDATE, FOR SHARE, FOR KEY SHARE farqini va ularni kim olishini bilasiz.
  • NOWAIT, lock_timeout va SKIP LOCKED bilan kutishni boshqarasiz.
  • Jadval qulf rejimlarini va "lock queue" tuzog'ini tushuntirasiz.
  • pg_advisory_xact_lock bilan ilova darajasidagi qulf qurasiz.
  • pg_stat_activity, pg_locks va pg_blocking_pids bilan "kim kimni to'sib turibdi" savoliga javob topasiz.

Oldin bilishingiz kerak: Read phenomena va izolyatsiya darajalari, MVCC: Postgres ichida versiyalar, Tranzaksiya asoslari.

1. Nega bu kerak?

MVCC darsida o'quvchi hech qachon kutmasligini ko'rdik: u o'z snapshot'idagi versiyani oladi. Lekin ikki tranzaksiya bitta qatorni o'zgartirmoqchi bo'lsa-chi? Ikkita "hozirgi" versiya bo'lishi mumkin emas. Shuning uchun Postgres ikkinchi yozuvchini kutishga majbur qiladi — buni izolyatsiya darsida ko'rdik: B'ning UPDATEi A'ning COMMITigacha javobsiz turdi.

Bu avtomatik qulf. Ba'zan esa qulfni o'zimiz, oldindan olishimiz kerak. Klassik holat — "tekshir, keyin bajar": ombordagi oxirgi bitta mahsulot. Ikki xaridor bir vaqtda SELECT qoldiq qilib, 1 ni ko'radi va ikkalasi ham sotib oladi. O'qish paytida hech kim hech narsani qulflamadi — MVCC o'quvchini to'xtatmaydi.

Hayotiy o'xshatish: kutubxonadagi kitob. Hamma uni javonda ko'ra oladi (oddiy SELECT). Lekin uni o'qish uchun uyga olib ketmoqchi bo'lsangiz, kutubxonachi kartochkaga nomingizni yozadi (FOR UPDATE). Boshqa birov ham olmoqchi bo'lsa — navbatda kutadi, yoki "band ekan, boshqasini olaman" deydi (SKIP LOCKED), yoki darhol ketadi (NOWAIT). Kutubxona remont qilinsa esa (ALTER TABLE) — eshik yopiladi, hatto qarab turish ham mumkin emas.

Maslahat: Bu darsning misollari vazifalar kanon bazasida (holat 40). Natijalarni biz o'zimiz ishga tushirdik: PostgreSQL 17.6, uchta haqiqiy psql sessiyasi (A, B, C) — kanon (PostgreSQL 18.6) xuddi shu tajribalarni Node'da bajargan (t51-qulflar.txt), natijalar mos. pid raqamlari sizda boshqacha bo'ladi.

2. Qator qulflari: SELECT … FOR UPDATE

2.1 Birinchi tajriba

A vazifani "o'zgartirish uchun" o'qiydi. B uni oddiy o'qiydi, keyin o'zgartirmoqchi bo'ladi. Uchinchi terminal C — kuzatuvchi:

Qadam A sessiya B sessiya (4-qadam — C) Natija
1 BEGIN; va SELECT id, matn FROM vazifalar WHERE id = 1 FOR UPDATE; A qatorni qulfladi
2 B: SELECT id, matn FROM vazifalar WHERE id = 1; B kutadimi?
3 B: UPDATE vazifalar SET ustuvorlik = 'yuqori' WHERE id = 1; B kutadimi?
4 C: pg_stat_activity so'rovi (transkriptda to'liq) kim kimni kutyapti
5 COMMIT; B ning UPDATE'i tugaydi

Transkriptda B ning ikki buyrug'iga javob qachon chiqqaniga va C ning kimni_kutadi ustuniga qarang:

text
A=> BEGIN;
A   BEGIN
A=> SELECT id, matn FROM vazifalar WHERE id = 1 FOR UPDATE;
A    id |                       matn
A   ----+--------------------------------------------------
A     1 | JavaScript kursining 26-qismini boshlash #o'qish
A   (1 row)
B=> SELECT id, matn FROM vazifalar WHERE id = 1;
B    id |                       matn
B   ----+--------------------------------------------------
B     1 | JavaScript kursining 26-qismini boshlash #o'qish
B   (1 row)
B=> UPDATE vazifalar SET ustuvorlik = 'yuqori' WHERE id = 1;
C=> SELECT pid, pg_blocking_pids(pid) AS kimni_kutadi, state,
C=>        left(query, 30) AS sorov
C=> FROM pg_stat_activity
C=> WHERE datname = 'vazifalar' AND pid <> pg_backend_pid();
C     pid  | kimni_kutadi |        state        |             sorov
C   -------+--------------+---------------------+--------------------------------
C    18552 | {}           | idle in transaction | SELECT id, matn FROM vazifalar
C    23692 | {18552}      | active              | UPDATE vazifalar SET ustuvorli
C   (2 rows)
A=> COMMIT;
A   COMMIT
B   UPDATE 1

Uch narsani ko'ring:

  1. B'ning oddiy SELECTi darhol javob oldi. FOR UPDATE o'quvchilarni to'smaydi.
  2. B'ning UPDATEi javobsiz qoldi. C buni ko'rsatdi: B (23692) 18552 ni kutyapti — bu A. A esa idle in transaction: tranzaksiya ochiq, lekin hozir hech narsa bajarmayapti.
  3. A COMMIT qilgach, B'ning UPDATE 1 i chiqdi.

pg_stat_activity — serverdagi har ulanish haqida bitta qator: kim, qaysi bazada, qaysi holatda, qaysi so'rovni bajaryapti. pg_blocking_pids(pid) — shu jarayonni to'sib turgan jarayonlar ro'yxati. Bu ikkalasi — "baza qotib qoldi" holatidagi birinchi qurol.

FOR UPDATE siz ham B kutgan bo'lardi — agar A qatorni UPDATE qilganida. FOR UPDATE ning farqi: A hali hech narsani o'zgartirmagan, faqat niyat bildirdi. Endi A qaror qilish uchun bemalol vaqt oladi — qiymat uning ko'z o'ngida o'zgarmaydi.

2.2 pg_locks ichida

O'sha paytda C qulflar jadvaliga ham qaradi:

sql
SELECT pid, locktype, mode, granted
FROM pg_locks
WHERE locktype IN ('relation', 'tuple', 'transactionid')
  AND (relation IS NULL OR relation = 'vazifalar'::regclass)
  AND pid <> pg_backend_pid()
ORDER BY pid, locktype;

Natija:

text
  pid  |   locktype    |       mode       | granted
-------+---------------+------------------+---------
 18552 | relation      | RowShareLock     | t
 18552 | transactionid | ExclusiveLock    | t
 23692 | relation      | RowExclusiveLock | t
 23692 | transactionid | ShareLock        | f
 23692 | tuple         | ExclusiveLock    | t
(5 rows)

Qatorlarni o'qiymiz:

  • A (18552) jadvalda RowShareLock ushlab turibdi — bu SELECT … FOR UPDATE ning jadval darajasidagi izi. Plus o'z tranzaksiya raqamiga ExclusiveLock — har yozuvchi tranzaksiya shunday "men tirikman" belgisini oladi.
  • B (23692) jadvalda RowExclusiveLock oldi (har UPDATE/INSERT/DELETE shuni oladi), navbatdagi birinchi o'rinni egalladi (tuple ExclusiveLock) va A'ning tranzaksiya raqamiga ShareLock so'radi — granted = f, ya'ni kutmoqda.

Qiziq tomoni: qator qulfining o'zi pg_locks da ko'rinmaydi. Postgres uni qator versiyasining sarlavhasiga yozadi (xmax ga — tanish ustun). Shuning uchun million qatorni qulflash xotirada million yozuv yaratmaydi. Kutuvchi esa "qator bo'shasin" deb emas, "A'ning tranzaksiyasi tugasin" deb kutadi — transactionid ShareLock.

2.3 To'rtta qator qulfi rejimi

FOR UPDATE — eng kuchlisi. Postgres'da to'rtta rejim bor:

Rejim Kim oladi Qaysilari bilan to'qnashadi
FOR UPDATE DELETE, kalit ustunni o'zgartiruvchi UPDATE hammasi bilan
FOR NO KEY UPDATE oddiy UPDATE (kalit o'zgarmasa) FOR SHARE va undan kuchlilar
FOR SHARE qo'lda: "o'qiyman, hech kim o'zgartirmasin" FOR NO KEY UPDATE, FOR UPDATE
FOR KEY SHARE tashqi kalit tekshiruvi faqat FOR UPDATE

"Kalit" — bu yerda unique indeksi bor ustun (PK, UNIQUE). Bu bo'linish nima uchun kerak, keyingi bo'limda ko'ramiz.

Tekshirib ko'ring: A SELECT … FOR UPDATE qilib turibdi. B SELECT … FOR SHARE qiladi. B kutadimi? Agar A FOR SHARE qilgan bo'lsa-chi?

Javob

Birinchi holatda — kutadi: FOR UPDATE hamma rejim bilan to'qnashadi. Ikkinchi holatda — kutmaydi: ikki FOR SHARE bir-biriga xalaqit bermaydi (ikkalasi ham faqat o'qiydi). Lekin ikkalasi ham shu qatorni UPDATE qilmoqchi bo'lgan har kimni to'xtatadi.

3. FOR SHARE va tashqi kalitlar

3.1 O'qiydiganlar bir-birini to'smaydi

sql
-- A
BEGIN;
SELECT id FROM vazifalar WHERE id = 1 FOR SHARE;
-- B
SELECT id FROM vazifalar WHERE id = 1 FOR SHARE;
SET lock_timeout = '300ms';
UPDATE vazifalar SET ustuvorlik = 'orta' WHERE id = 1;
-- A
ROLLBACK;

Natija:

text
B=> SELECT id FROM vazifalar WHERE id = 1 FOR SHARE;
B    id
B   ----
B     1
B   (1 row)
B=> SET lock_timeout = '300ms';
B   SET
B=> UPDATE vazifalar SET ustuvorlik = 'orta' WHERE id = 1;
B   ERROR:  canceling statement due to lock timeout
B   CONTEXT:  while locking tuple (0,42) in relation "vazifalar"

(A'ning BEGIN va FOR SHARE qatorlarini tushirdik.) Ikkinchi FOR SHARE darhol o'tdi. UPDATE esa A'ni kutdi. Biz kutishni 300 ms bilan chekladik (lock_timeout, keyingi bo'limda) — shuning uchun u xato bilan to'xtadi. CONTEXT qatori qaysi qator versiyasida ((0,42) — tanish ctid!) kutganini aytadi.

3.2 Tashqi kalit nega FOR KEY SHARE oladi

Kimdir vazifaga teg qo'shmoqda — vazifa_teglari ga qator yozadi. Tashqi kalit (foreign key) tekshiradi: "vazifa 2 bormi?" va tranzaksiya tugaguncha vazifa 2 o'chib ketmasligini kafolatlashi kerak. Buning uchun u vazifa qatoriga eng yengil qulfni — FOR KEY SHARE ni qo'yadi:

sql
-- A
BEGIN;
INSERT INTO vazifa_teglari (vazifa_id, teg_id) VALUES (2, 4);
-- B
SET lock_timeout = '300ms';
UPDATE vazifalar SET ustuvorlik = 'yuqori' WHERE id = 2;
DELETE FROM vazifalar WHERE id = 2;
-- A
ROLLBACK;

Natija:

text
A=> BEGIN;
A   BEGIN
A=> INSERT INTO vazifa_teglari (vazifa_id, teg_id) VALUES (2, 4);
A   INSERT 0 1
B=> SET lock_timeout = '300ms';
B   SET
B=> UPDATE vazifalar SET ustuvorlik = 'yuqori' WHERE id = 2;
B   UPDATE 1
B=> DELETE FROM vazifalar WHERE id = 2;
B   ERROR:  canceling statement due to lock timeout
B   CONTEXT:  while deleting tuple (0,44) in relation "vazifalar"
A=> ROLLBACK;
A   ROLLBACK

Oddiy UPDATE (kalit o'zgarmaydi, FOR NO KEY UPDATE) — o'tdi. DELETE (FOR UPDATE) — kutdi. Aynan shu uchun rejimlar to'rtta: teg qo'shayotgan foydalanuvchi vazifani tahrirlayotgan boshqa foydalanuvchini to'xtatmasligi kerak. Postgres 9.3 gacha FK FOR SHARE olardi va bunday tahrirlar ham kutardi.

3.3 Kanon topilmasi: counter cache qulfi

Endi xuddi shu tajriba — teg o'rniga izoh bilan:

sql
-- A
BEGIN;
INSERT INTO izohlar (vazifa_id, muallif_id, matn)
VALUES (2, 1, 'Qachon tugatamiz?');
-- B
SET lock_timeout = '300ms';
UPDATE vazifalar SET bajarildi = true, bajarilgan_vaqt = now()
WHERE id = 2;
-- A
ROLLBACK;

Natija:

text
A=> BEGIN;
A   BEGIN
A=> INSERT INTO izohlar (vazifa_id, muallif_id, matn)
A=> VALUES (2, 1, 'Qachon tugatamiz?');
A   INSERT 0 1
B=> SET lock_timeout = '300ms';
B   SET
B=> UPDATE vazifalar SET bajarildi = true, bajarilgan_vaqt = now()
B=> WHERE id = 2;
B   ERROR:  canceling statement due to lock timeout
B   CONTEXT:  while locking tuple (0,55) in relation "vazifalar"
A=> ROLLBACK;
A   ROLLBACK

Bu safar oddiy UPDATE ham kutdi. Nega? FK emas — u FOR KEY SHARE oladi va UPDATE ga xalaqit bermaydi (teg misolida ko'rdik). Sababi — izohlar_soni_trg trigger'i (Denormalizatsiya va hisoblangan maydonlar). U izoh qo'shilganda vazifa qatorini UPDATE qiladi (izohlar_soni + 1) va shu bilan vazifaga FOR NO KEY UPDATE qulfini qo'yadi. Endi izoh tranzaksiyasi tugamaguncha hech kim vazifani tahrirlay olmaydi.

Diqqat: CONTEXT dagi ctid (0,55) — bu vazifa 2 ning yangi versiyasi. Trigger qatorni yangilagani shundan ham ko'rinib turibdi.

Kanon buni o'zi topgan (t51-qulflar.txt) — bu counter cache'ning narxi: izoh tranzaksiyasi qancha qisqa bo'lsa, shuncha yaxshi. Uzoq izoh tranzaksiyasi (masalan, izoh bilan birga fayl yuklash tranzaksiya ichida) = vazifa qulflangan.

4. Kutishni boshqarish: NOWAIT, lock_timeout, SKIP LOCKED

Sukut bo'yicha qulf kutish cheksiz. Foydalanuvchi sahifasi aylanib turadi, ulanish band. Uch vosita bor:

sql
-- A
BEGIN;
SELECT id FROM vazifalar WHERE id = 1 FOR UPDATE;
-- B
SELECT id FROM vazifalar WHERE id = 1 FOR UPDATE NOWAIT;
SET lock_timeout = '500ms';
UPDATE vazifalar SET ustuvorlik = 'past' WHERE id = 1;
SELECT id FROM vazifalar
WHERE id IN (1, 2, 3)
ORDER BY id
FOR UPDATE SKIP LOCKED;
-- A
ROLLBACK;

Natija:

text
B=> SELECT id FROM vazifalar WHERE id = 1 FOR UPDATE NOWAIT;
B   ERROR:  could not obtain lock on row in relation "vazifalar"
B=> SET lock_timeout = '500ms';
B   SET
B=> UPDATE vazifalar SET ustuvorlik = 'past' WHERE id = 1;
B   ERROR:  canceling statement due to lock timeout
B   CONTEXT:  while locking tuple (0,42) in relation "vazifalar"
B=> SELECT id FROM vazifalar
B=> WHERE id IN (1, 2, 3)
B=> ORDER BY id
B=> FOR UPDATE SKIP LOCKED;
B    id
B   ----
B     2
B     3
B   (2 rows)
  • NOWAIT — umuman kutmaydi: qulf band bo'lsa, darhol could not obtain lock on row in relation "vazifalar" ("vazifalar jadvalidagi qatorga qulf olib bo'lmadi"). Kod 55P03 (lock_not_available). Kanonda bu 6 ms da qaytgan.
  • lock_timeout — ko'pi bilan shuncha kutadi, keyin canceling statement due to lock timeout ("qulf kutish vaqti tugagani uchun buyruq bekor qilindi"). Kod ham 55P03. Kanonda: 500 ms chegara — 505 ms da xato. U har qanday qulfga ta'sir qiladi — FOR UPDATE ga ham, oddiy UPDATE ga ham, ALTER TABLE ga ham.
  • SKIP LOCKED — band qatorlarni jimgina o'tkazib yuboradi: 1 qulflangan — natijada 2, 3. Bu natija "haqiqiy holat" emas, "hozir bo'sh bo'lganlar". Shu sabab u hisobot uchun yaramaydi, lekin navbat uchun ajoyib: o'nta ishchi bir-birini kutmasdan har biri o'z ishini oladi. Bunga SKIP LOCKED bilan navbat darsi to'liq bag'ishlangan.

SET lock_timeout — sessiya oxirigacha amal qiladi. Faqat bitta tranzaksiya uchun — SET LOCAL lock_timeout = '500ms' (tranzaksiya ichida).

Diqqat: NOWAIT va lock_timeout xatosi oddiy xato — tranzaksiya ichida bo'lsa, tranzaksiya buziladi (current transaction is aborted). Ilova uni ushlab, foydalanuvchiga "hozir band, birozdan keyin urinib ko'ring" deyishi yoki qayta urinishi kerak.

5. Jadval qulflari va "lock queue"

5.1 Har buyruq jadvalga qulf oladi

Har SQL buyrug'i jadvalning o'ziga ham qulf qo'yadi. Ko'pchiligi bir-biriga xalaqit bermaydi:

Rejim Kim oladi Kim bilan to'qnashadi
ACCESS SHARE SELECT faqat ACCESS EXCLUSIVE
ROW SHARE SELECT … FOR UPDATE/SHARE EXCLUSIVE, ACCESS EXCLUSIVE
ROW EXCLUSIVE INSERT, UPDATE, DELETE SHARE va undan kuchlilar
SHARE CREATE INDEX (oddiy) yozuvchilar (ROW EXCLUSIVE)
ACCESS EXCLUSIVE ALTER TABLE, DROP, TRUNCATE, VACUUM FULL hammasi bilan

Jadvalda sakkizta rejim bor; bu yerda amalda eng ko'p uchraydigan beshtasi. To'liq jadval — hujjatning "Explicit Locking" bo'limida.

Ikki amaliy xulosa:

  • Oddiy CREATE INDEX yozishni to'xtatadi (o'qish davom etadi). Ishlab turgan jadvalda — CREATE INDEX CONCURRENTLY (Qaysi ustunga va qanday tartibda indeks darsida).
  • ALTER TABLE ning ko'p turlari ACCESS EXCLUSIVE oladi — u turganda hech kim, hatto oddiy SELECT ham ishlay olmaydi. ALTER TABLE darsida "ADD COLUMN … DEFAULT 'faol' 4.5 ms" deb o'lchagan edik — tez. Lekin u qulfni olishi uchun avval kutishi mumkin.

Qo'lda jadval qulfi ham olish mumkin: LOCK TABLE vazifalar IN SHARE MODE; (tranzaksiya ichida). Amalda kamdan-kam kerak bo'ladi.

5.2 Lock queue: bitta ALTER butun saytni to'xtatadi

Eng xavfli holat. A — oddiy uzoq so'rov (yoki ochiq qolgan tranzaksiya). B — migratsiya (ALTER TABLE). C — saytning oddiy foydalanuvchisi:

sql
-- A
BEGIN;
SELECT count(*) FROM vazifalar;
-- B
SET lock_timeout = '2s';
ALTER TABLE vazifalar ADD COLUMN eslatma text;
-- C
SET lock_timeout = '500ms';
SELECT count(*) FROM vazifalar;
-- A
COMMIT;

Natija:

text
A=> BEGIN;
A   BEGIN
A=> SELECT count(*) FROM vazifalar;
A    count
A   -------
A       48
A   (1 row)
B=> SET lock_timeout = '2s';
B   SET
B=> ALTER TABLE vazifalar ADD COLUMN eslatma text;
C=> SET lock_timeout = '500ms';
C   SET
C=> SELECT count(*) FROM vazifalar;
C   ERROR:  canceling statement due to lock timeout
C   LINE 1: SELECT count(*) FROM vazifalar;
C                                ^
A=> COMMIT;
A   COMMIT
B   ALTER TABLE

C'ning oddiy SELECTi ham kutib qoldi (biz uni 500 ms da to'xtatdik). Nega? A ACCESS SHARE ushlab turibdi. B ACCESS EXCLUSIVE uchun navbatga turdi. C'ning ACCESS SHARE i A bilan to'qnashmaydi — lekin u B'dan keyin keldi va navbatda B'ning orqasida turadi. Postgres navbatni "sakrab o'tishga" ruxsat bermaydi — aks holda B hech qachon qulf ololmasdi.

flowchart LR
  A["A: SELECT<br/>ACCESS SHARE — ushlab turibdi"] --> B["B: ALTER TABLE<br/>ACCESS EXCLUSIVE — kutyapti"]
  B --> C["C: oddiy SELECT<br/>kutyapti (B orqasida)"]
  B --> D["D, E, F...<br/>hammasi kutyapti"]

Ishlab chiqarishda bu shunday ko'rinadi: kimdir migratsiya ishga tushirdi, shu paytda bitta uzoq hisobot ishlab turgan edi. Migratsiya kutyapti — va uning orqasida saytning hamma so'rovlari to'planyapti. Bir necha soniyada ulanishlar hovuzi tugaydi, sayt "yiqiladi". Kanon ham buni o'lchagan: ALTER navbatda turganda yangi oddiy SELECT 200 ms chegarada 55P03 bilan to'xtagan.

Qoida: migratsiyadan oldin SET lock_timeout (masalan, '2s'). Qulf tez olinmasa, migratsiya o'zi yiqilsin va keyinroq qayta urinilsin — sayt to'xtamasin. Migratsiya vositalari (27-qism) buni ko'pincha avtomatik qiladi.

Tekshirib ko'ring: Kechasi 2 da ALTER TABLE vazifalar ADD COLUMN … ishga tushirildi va 10 daqiqa "osilib" turdi, sayt ishlamadi. Nima sabab bo'lishi mumkin va qanday tekshirasiz?

Javob

ALTER biror ochiq tranzaksiyaning ACCESS SHARE qulfini kutgan (ko'pincha idle in transaction ulanish yoki uzoq hisobot), orqasida esa hamma so'rovlar navbatga turgan. Tekshirish: pg_stat_activity dan pg_blocking_pids(pid) bo'sh bo'lmagan qatorlarni oling — ALTER kimni kutayotgani ko'rinadi. Oldini olish: SET lock_timeout va idle_in_transaction_session_timeout.

6. Advisory lock: ilova ma'nosidagi qulf

6.1 Qatorsiz qulf

Ba'zan qulflanadigan narsa jadval qatori emas. Masalan: "Aziz bir vaqtda faqat bitta CSV import qila oladi". Import yangi qatorlar yaratadi — qulflash uchun mavjud qator yo'q.

Advisory lock ("maslahat" qulfi) — Postgres'ga ixtiyoriy son (yoki ikki son) beriladi, u shu son uchun qulf ushlaydi. Son nimani anglatishini faqat ilova biladi. Postgres uni jadvalga bog'lamaydi va o'zi hech qachon olmaydi — "maslahat" degani shu: hamma ishtirokchi qoidaga rioya qilsa ishlaydi.

sql
-- A
BEGIN;
SELECT pg_advisory_xact_lock(42, 1);
-- B
SELECT pg_try_advisory_xact_lock(42, 1) AS oldi_1,
       pg_try_advisory_xact_lock(42, 2) AS oldi_2;
-- C
SELECT pid, locktype, classid, objid, mode, granted
FROM pg_locks WHERE locktype = 'advisory';
-- A
COMMIT;
-- B
SELECT pg_try_advisory_xact_lock(42, 1) AS oldi_1;

Natija:

text
A=> BEGIN;
A   BEGIN
A=> SELECT pg_advisory_xact_lock(42, 1);
A    pg_advisory_xact_lock
A   -----------------------
A   (1 row)
B=> SELECT pg_try_advisory_xact_lock(42, 1) AS oldi_1,
B=>        pg_try_advisory_xact_lock(42, 2) AS oldi_2;
B    oldi_1 | oldi_2
B   --------+--------
B    f      | t
B   (1 row)
C=> SELECT pid, locktype, classid, objid, mode, granted
C=> FROM pg_locks WHERE locktype = 'advisory';
C     pid  | locktype | classid | objid |     mode      | granted
C   -------+----------+---------+-------+---------------+---------
C    24876 | advisory |      42 |     1 | ExclusiveLock | t
C   (1 row)
A=> COMMIT;
A   COMMIT
B=> SELECT pg_try_advisory_xact_lock(42, 1) AS oldi_1;
B    oldi_1
B   --------
B    t
B   (1 row)
  • pg_advisory_xact_lock(42, 1) — (42, 1) juftligiga qulf; band bo'lsa kutadi. Tranzaksiya tugaganda o'zi bo'shaydi (xact — transaction).
  • pg_try_advisory_xact_lock — kutmaydi: oldi — t, band — f. B (42, 1) ni ololmadi, (42, 2) ni oldi — boshqa juftlik.
  • pg_locks da qulf advisory turi bilan ko'rinadi: classid = 42, objid = 1.
  • A COMMIT qilgach, (42, 1) bo'shadi.

Ikki sonli shakl qulay: birinchi son — "qaysi turdagi ish" (42 — import), ikkinchisi — "kim uchun" (foydalanuvchi id). Bitta bigint li shakli ham bor: pg_advisory_xact_lock(12345).

6.2 Sessiya va tranzaksiya qulflari

_xact_ siz variantlar (pg_advisory_lock, pg_advisory_unlock) — sessiya darajasida: tranzaksiya tugaganda bo'shamaydi, faqat unlock yoki ulanish yopilganda. Ular xavfli: ulanishlar hovuzida ulanish keyingi so'rovga qaytadi — qulf esa unda qolib ketadi. Tashqi pooler'larning transaction rejimida esa sessiya qulfi umuman ishonchsiz (PgBouncer va tashqi pooler'lar). Qoida: pg_advisory_xact_lock ni afzal ko'ring.

Tekshirib ko'ring: Cron har daqiqada "kunlik hisobot" skriptini ishga tushiradi. Ba'zan skript 2 daqiqa ishlaydi va ikki nusxa bir-birining ustiga yozadi. Advisory lock bilan qanday hal qilasiz?

Javob

Skript boshida tranzaksiya ochib SELECT pg_try_advisory_xact_lock(<hisobot_soni>) qiladi. f qaytsa — oldingi nusxa hali ishlayapti. Skript darhol chiqib ketadi. t bo'lsa — ishni bajaradi va COMMIT da qulf o'zi bo'shaydi. try varianti muhim: kutadigan variant ikkinchi nusxani navbatda ushlab qoladi va ish baribir ikki marta bajariladi.

7. Node qadami

Izolyatsiya darsining «Node qadami» da muammo ko'rindi: 50 ta parallel yarat — atigi 8 xil tartib. Kanon xuddi shu o'lchovda yana bir narsani topdi (olchov/raqobat-olchov.ts, holat 48, PostgreSQL 18.6, ikki seans):

text
(3) 200 aralash amal (40 PUT + 160 PATCH): ok 62, xatolar {"40P01":138}, 82593 ms
(2-seans) (3) 200 aralash amal (40 PUT + 160 PATCH): ok 112, xatolar {"40P01":88}, 36085 ms

40P01 — deadlock: ikki tranzaksiya bir-birining qulfini kutib qoldi (batafsil — Deadlock va serialization failure darsida). Sababi qulflar tartibida edi. PATCH (bitta vazifani tahrirlash) avval vazifa qatorini, keyin egasining ro'yxat versiyasini (foydalanuvchilar qatorini) qulflardi. PUT (butun ro'yxat) esa aksincha: avval egasini, keyin vazifalarni.

Yechim — bu darsning FOR UPDATE i (holat 53). PgOmbori dagi har yozuv endi bitta yordamchi #yoz orqali o'tadi. U tranzaksiya ochadi va birinchi ish — egasining qatorini qulflaydi (qisqartirildi):

ts
async #yoz<T>(fn: (c: PoolClient) => Promise<T>): Promise<T> {
  return this.#tranzaksiyada(async (c) => {
    // bir egasining hamma yozuvi — navbat bilan, bir xil tartibda
    await c.query(
      "SELECT 1 FROM foydalanuvchilar WHERE id = $1 FOR UPDATE",
      [this.#egasi]);
    return fn(c);   // keyin vazifa(lar) va ro'yxat versiyasi
  });
}

yarat, yangila, ochir, almashtirHammasi — hammasi this.#yoz(async (c) => { … }) ichida. Natija ikki xil:

  • Tartib bir xil: hamma yozuv avval egasini, keyin vazifalarni qulflaydi — teskari tartib yo'q, deadlock'ning asosiy sababi yo'qoldi.
  • Bir egasining yozuvlari ketma-ket: ikkinchi yarat birinchisining COMMIT ini kutadi va max(tartib) + 1 ni to'g'ri hisoblaydi. Turli foydalanuvchilar esa bir-birini kutmaydi — qulf faqat o'z qatorida.

Xuddi shu o'lchov holat 53 da (natijalar/raqobat-53.txt, 1-seans):

text
(1) 50 parallel yarat: har xil tartib 50/50, eng katta 50
(3) 200 aralash amal (40 PUT + 160 PATCH): ok 200, xatolar {}, 2982 ms

Nima bo'ldi? Tartib 8/50 dan 50/50 ga, deadlock 138 tadan 0 ga; 200 amal 82.6 s o'rniga 3.0 s da tugadi (2-seansda 2.2 s). Narxi: bitta yozuv endi 5 ta tarmoq aylanishi (BEGIN, qulf, yozuv, ro'yxat versiyasi, COMMIT). Bu #yoz ning qayta urinish qismi — Deadlock va serialization failure darsida; holat 53 commit'i — Amaliyot: race condition'larni hal qilish darsida.

«Kanon topilmasi: counter cache qulfi» bo'limidagi saboq ham shu yerda amal qiladi: egasi qulfi ostida faqat baza ishi bajariladi, tashqi ishlar (fayl, xabar yuborish) — tranzaksiyadan tashqarida.

Qulf tajribalarining o'zi kanonda t51-qulflar.mjs da uch pg.Client bilan takrorlangan. Chiqishning bir qismi (PostgreSQL 18.6, t51-qulflar.txt):

text
2) B FOR UPDATE NOWAIT: 6 ms, XATO 55P03 could not obtain lock on row in relation "vazifalar"
   B lock_timeout 500ms: 505 ms, XATO 55P03 canceling statement due to lock timeout
3) B FOR UPDATE SKIP LOCKED (id 1–3): [ '2', '3' ]
5) A ochiq tranzaksiyada SELECT qilgan; B ALTER kutmoqda; K (yangi oddiy SELECT): 212 ms, XATO 55P03 canceling statement due to lock timeout

[ '2', '3' ] — id lar satr: pg bigint ni satr qaytaradi (node-postgres asoslari).

8. Ko'p uchraydigan xatolar

8.1 Tranzaksiyasiz FOR UPDATE

Autocommit'da SELECT … FOR UPDATE qulfni oladi va darhol bo'shatadi — buyruq tugashi bilan tranzaksiya ham tugaydi. BEGIN → FOR UPDATE → o'zgartirish → COMMIT, hammasi bitta client'da.

8.2 Qulf ostida uzoq ish

FOR UPDATE qilib, keyin tashqi API'ga so'rov yuborish yoki foydalanuvchi javobini kutish. Shu vaqt ichida qator — va MVCC ga ko'ra VACUUM ham — kutadi. Qulf ostida faqat baza ishi; tashqi ish — oldin yoki keyin.

8.3 Migratsiyani lock_timeout siz yurgizish

«Lock queue» bo'limidagi voqea. SET lock_timeout = '2s' va yiqilsa — qayta urinish.

8.4 Sessiya advisory lock'ini pool bilan ishlatish

pg_advisory_lock olindi, unlock xatoda chaqirilmadi — ulanish pool'ga qaytdi, qulf osilib qoldi. pg_advisory_xact_lock.

8.5 SKIP LOCKED ni hisobotda ishlatish

Natija "band bo'lmagan qatorlar" — har safar boshqacha va to'liq emas. SKIP LOCKED — faqat "ish olish" naqshlari uchun.

9. Mashqlar

1-mashq (oson): Kim kutadi?

Har juftlikda B kutadimi? A tranzaksiyasi ochiq. (a) A: SELECT … FOR UPDATE (qator 5); B: oddiy SELECT (qator 5). (b) A: UPDATE (qator 5, oddiy ustun); B: INSERT INTO izohlar … vazifa_id = 5 — faqat FK nuqtai nazaridan (trigger'ni hisobga olmang). (c) A: oddiy SELECT (jadval); B: ALTER TABLE … ADD COLUMN.

Yechim

(a) Yo'q — FOR UPDATE o'quvchilarni to'smaydi (MVCC). (b) Yo'q — oddiy UPDATE FOR NO KEY UPDATE oladi, FK esa FOR KEY SHARE; ular to'qnashmaydi («Tashqi kalit nega FOR KEY SHARE oladi» bo'limi). Lekin vazifalar da izohlar_soni_trg B'ni ham vazifa qatorini yangilashga majbur qiladi — haqiqiy kanon bazasida B kutadi. (c) Ha — ALTER TABLE ACCESS EXCLUSIVE istaydi, u ACCESS SHARE bilan to'qnashadi.

2-mashq (o'rta): Bir vaqtda bitta import

Node'da importQil(foydalanuvchiId, nom) funksiyasini yozing: tranzaksiya ochadi va pg_try_advisory_xact_lock(26, foydalanuvchiId) oladi. Ololmasa — "<nom>: import allaqachon ketmoqda", olsa — 200 ms "ishlaydi" va "<nom>: import tugadi". Aziz uchun ikki oyna (ikkinchisi 50 ms keyin), Dilnoza uchun bitta — parallel. Oxirida Aziz yana bir marta. Eslatma: pool'dan bitta client — pool.connect() va finally { release() } (Tranzaksiya asoslari).

Yechim
js
import pg from "pg";

const url = process.env.DATABASE_URL;
const pool = new pg.Pool({ connectionString: url, max: 5 });
const IMPORT = 26; // ilova ichidagi "import" qulflari guruhi
const kut = (ms) => new Promise((r) => setTimeout(r, ms));

async function importQil(foydalanuvchiId, nom) {
  const c = await pool.connect();
  try {
    await c.query("BEGIN");
    const { rows } = await c.query(
      "SELECT pg_try_advisory_xact_lock($1, $2) AS oldi",
      [IMPORT, foydalanuvchiId]);
    if (!rows[0].oldi) {
      await c.query("ROLLBACK");
      return `${nom}: import allaqachon ketmoqda`;
    }
    await kut(200); // import ishi
    await c.query("COMMIT"); // qulf shu yerda bo'shaydi
    return `${nom}: import tugadi`;
  } finally {
    c.release();
  }
}

const natijalar = await Promise.all([
  importQil(1, "Aziz (1-oyna)"),
  kut(50).then(() => importQil(1, "Aziz (2-oyna)")),
  kut(50).then(() => importQil(2, "Dilnoza")),
]);
for (const n of natijalar) console.log(n);
console.log(await importQil(1, "Aziz (keyinroq)"));
await pool.end();

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

text
Aziz (1-oyna): import tugadi
Aziz (2-oyna): import allaqachon ketmoqda
Dilnoza: import tugadi
Aziz (keyinroq): import tugadi

Dilnoza'ning importi to'silmadi — ikkinchi son (foydalanuvchi id) boshqa. Ikkinchi oyna kutmadi — try varianti. Qulfni hech qayerda qo'lda bo'shatmadik: COMMIT/ROLLBACK buni qiladi. Haqiqiy importda kut(200) o'rnida INSERT lar bo'ladi — xuddi shu tranzaksiyada.

3-mashq (qiyin): Kim to'sib turibdi?

Ikki ulanish (A, B) va kuzatuvchi K. A buyurtma qatorini UPDATE qiladi va tranzaksiyani ochiq qoldiradi. B shu qatorni UPDATE qilmoqchi bo'ladi (kutadi). K pg_stat_activity va pg_blocking_pids orqali kim kutayotganini topib, B kutyapti, to'sib turgan: A deb chiqarsin (pid'larni nomga aylantiring). Keyin A COMMIT, B tugaydi, oxirgi holat chiqarilsin. Eslatma: kutadigan so'rovni darhol await qilmang (izolyatsiya darsidagi Node misoli); cardinality(massiv) — massiv uzunligi.

Yechim
js
import pg from "pg";

const url = process.env.DATABASE_URL;
const ulan = async () => {
  const c = new pg.Client({ connectionString: url });
  await c.connect();
  return c;
};
const A = await ulan(), B = await ulan(), K = await ulan();
const pid = async (c) =>
  (await c.query("SELECT pg_backend_pid() AS p")).rows[0].p;
const nomlar = new Map([[await pid(A), "A"], [await pid(B), "B"]]);

await A.query("DROP TABLE IF EXISTS m51_buyurtma");
await A.query(`CREATE TABLE m51_buyurtma
  (id int PRIMARY KEY, holat text NOT NULL)`);
await A.query("INSERT INTO m51_buyurtma VALUES (1, 'yangi')");

await A.query("BEGIN");
await A.query(
  "UPDATE m51_buyurtma SET holat = 'tayyor' WHERE id = 1");
const bKutadi = B.query(
  "UPDATE m51_buyurtma SET holat = 'bekor' WHERE id = 1");
await new Promise((r) => setTimeout(r, 200));

const { rows } = await K.query(
  `SELECT pid, pg_blocking_pids(pid) AS tosuvchilar
   FROM pg_stat_activity
   WHERE cardinality(pg_blocking_pids(pid)) > 0`);
for (const r of rows) {
  const kim = nomlar.get(r.pid) ?? "?";
  const kimlar = r.tosuvchilar.map((p) => nomlar.get(p) ?? "?");
  console.log(`${kim} kutyapti, to'sib turgan: ${kimlar.join(", ")}`);
}
await A.query("COMMIT");
await bKutadi;
const h = await K.query(
  "SELECT holat FROM m51_buyurtma WHERE id = 1");
console.log(`oxirgi holat: ${h.rows[0].holat}`);
await A.query("DROP TABLE m51_buyurtma");
await A.end(); await B.end(); await K.end();

Biz ishga tushirdik (PostgreSQL 17.6):

text
B kutyapti, to'sib turgan: A
oxirgi holat: bekor

pg_backend_pid() — har ulanishning server tomonidagi jarayon raqami; uni oldindan so'rab, nomga bog'ladik. Oxirgi holat bekor — B A'dan keyin yozdi va A'ning tayyor ini ustidan yozdi. Bu yerda bu to'g'ri xatti-harakatmi? Biznes qoidasiga bog'liq: "tayyor" buyurtmani bekor qilish mumkinmi? Bunday hollarda Optimistic concurrency yoki WHERE holat = 'yangi' sharti kerak.

10. Real ishda

  • Ombor, bron, to'lov: "oxirgi joy"ni sotishda FOR UPDATE yoki atomik UPDATE — Race condition'larni hal qilish amaliyotida.
  • Fon vazifalari navbati: FOR UPDATE SKIP LOCKED — pg-boss, graphile-worker kabi kutubxonalarning yuragi (SKIP LOCKED navbat).
  • Migratsiyalar: lock_timeout + qayta urinish — katta jamoalarning majburiy qoidasi; Prisma/Drizzle migratsiyalarida ham shu muammo bor (27-qism).
  • Favqulodda holat: pg_stat_activity + pg_blocking_pids + pg_cancel_backend — Monitoring va favqulodda vaziyatlar.
  • Intervyu: "SELECT FOR UPDATE nima qiladi?", "NOWAIT va SKIP LOCKED farqi?", "Nega ALTER TABLE saytni to'xtatdi?".

Xulosa

  • O'quvchilar hech qachon qulf kutmaydi; ikki yozuvchi bitta qatorda — kutadi.
  • FOR UPDATE — "o'zgartiraman" niyati bilan o'qish; to'rt rejim tufayli FK tekshiruvi oddiy tahrirni to'smaydi.
  • NOWAIT, lock_timeout (55P03) va SKIP LOCKED — cheksiz kutishning uch muqobili.
  • ALTER TABLE ACCESS EXCLUSIVE oladi; navbatdagi ALTER orqasida hamma so'rovlar to'planadi — migratsiyadan oldin lock_timeout.
  • Advisory lock — ilova ma'nosidagi qulf; pg_advisory_xact_lock ni afzal ko'ring.
  • Kanon topilmasi: counter cache trigger'i izoh tranzaksiyasi davomida vazifani qulflaydi.

Keyingi dars: Optimistic concurrency — qulf ushlamasdan, versiya ustuni bilan "kimdir o'zgartirib qo'ydi" holatini aniqlash.

Manbalar

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
PostgreSQL qulflari: SELECT FOR UPDATE, jadval qulflari va advisory lock — IlmHamroh