Mundarija (31)
- Bu darsda
- 1. Nega bu kerak?
- 2. Qator qulflari: SELECT … FOR UPDATE
- 2.1 Birinchi tajriba
- 2.2 pg_locks ichida
- 2.3 To'rtta qator qulfi rejimi
- 3. FOR SHARE va tashqi kalitlar
- 3.1 O'qiydiganlar bir-birini to'smaydi
- 3.2 Tashqi kalit nega FOR KEY SHARE oladi
- 3.3 Kanon topilmasi: counter cache qulfi
- 4. Kutishni boshqarish: NOWAIT, lock_timeout, SKIP LOCKED
- 5. Jadval qulflari va "lock queue"
- 5.1 Har buyruq jadvalga qulf oladi
- 5.2 Lock queue: bitta ALTER butun saytni to'xtatadi
- 6. Advisory lock: ilova ma'nosidagi qulf
- 6.1 Qatorsiz qulf
- 6.2 Sessiya va tranzaksiya qulflari
- 7. Node qadami
- 8. Ko'p uchraydigan xatolar
- 8.1 Tranzaksiyasiz FOR UPDATE
- 8.2 Qulf ostida uzoq ish
- 8.3 Migratsiyani lock_timeout siz yurgizish
- 8.4 Sessiya advisory lock'ini pool bilan ishlatish
- 8.5 SKIP LOCKED ni hisobotda ishlatish
- 9. Mashqlar
- 1-mashq (oson): Kim kutadi?
- 2-mashq (o'rta): Bir vaqtda bitta import
- 3-mashq (qiyin): Kim to'sib turibdi?
- 10. Real ishda
- Xulosa
- Manbalar
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 UPDATEbilan qatorni o'qish paytidayoq qulflab olasiz: "men buni o'zgartiraman, boshqalar kutsin".NOWAITvalock_timeoutkutishni cheklaydi,SKIP LOCKEDband qatorni o'tkazib yuboradi. Jadval darajasidaALTER TABLEhatto oddiySELECTni 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 SHAREfarqini va ularni kim olishini bilasiz.NOWAIT,lock_timeoutvaSKIP LOCKEDbilan kutishni boshqarasiz.- Jadval qulf rejimlarini va "lock queue" tuzog'ini tushuntirasiz.
pg_advisory_xact_lockbilan ilova darajasidagi qulf qurasiz.pg_stat_activity,pg_locksvapg_blocking_pidsbilan "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
vazifalarkanon bazasida (holat 40). Natijalarni biz o'zimiz ishga tushirdik: PostgreSQL 17.6, uchta haqiqiypsqlsessiyasi (A, B, C) — kanon (PostgreSQL 18.6) xuddi shu tajribalarni Node'da bajargan (t51-qulflar.txt), natijalar mos.pidraqamlari 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:
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 1Uch narsani ko'ring:
- B'ning oddiy
SELECTi darhol javob oldi.FOR UPDATEo'quvchilarni to'smaydi. - B'ning
UPDATEi javobsiz qoldi. C buni ko'rsatdi: B (23692)18552ni kutyapti — bu A. A esaidle in transaction: tranzaksiya ochiq, lekin hozir hech narsa bajarmayapti. - A
COMMITqilgach, B'ningUPDATE 1i 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:
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:
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) jadvaldaRowShareLockushlab turibdi — buSELECT … FOR UPDATEning jadval darajasidagi izi. Plus o'z tranzaksiya raqamigaExclusiveLock— har yozuvchi tranzaksiya shunday "men tirikman" belgisini oladi. - B (
23692) jadvaldaRowExclusiveLockoldi (harUPDATE/INSERT/DELETEshuni oladi), navbatdagi birinchi o'rinni egalladi (tuple ExclusiveLock) va A'ning tranzaksiya raqamigaShareLockso'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 UPDATEqilib turibdi. BSELECT … FOR SHAREqiladi. B kutadimi? Agar AFOR SHAREqilgan 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
-- 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:
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:
-- 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:
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 ROLLBACKOddiy 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:
-- 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:
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 ROLLBACKBu 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:
-- 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:
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, darholcould not obtain lock on row in relation "vazifalar"("vazifalar jadvalidagi qatorga qulf olib bo'lmadi"). Kod55P03(lock_not_available). Kanonda bu 6 ms da qaytgan.lock_timeout— ko'pi bilan shuncha kutadi, keyincanceling statement due to lock timeout("qulf kutish vaqti tugagani uchun buyruq bekor qilindi"). Kod ham55P03. Kanonda: 500 ms chegara — 505 ms da xato. U har qanday qulfga ta'sir qiladi —FOR UPDATEga ham, oddiyUPDATEga ham,ALTER TABLEga ham.SKIP LOCKED— band qatorlarni jimgina o'tkazib yuboradi:1qulflangan — natijada2,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:
NOWAITvalock_timeoutxatosi 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 INDEXyozishni to'xtatadi (o'qish davom etadi). Ishlab turgan jadvalda —CREATE INDEX CONCURRENTLY(Qaysi ustunga va qanday tartibda indeks darsida). ALTER TABLEning ko'p turlariACCESS EXCLUSIVEoladi — u turganda hech kim, hatto oddiySELECTham 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:
-- 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:
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 TABLEC'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.
-- 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:
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_locksda qulfadvisoryturi bilan ko'rinadi:classid = 42,objid = 1.- A
COMMITqilgach, (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):
(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 ms40P01 — 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):
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
yaratbirinchisiningCOMMITini kutadi vamax(tartib) + 1ni 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):
(1) 50 parallel yarat: har xil tartib 50/50, eng katta 50
(3) 200 aralash amal (40 PUT + 160 PATCH): ok 200, xatolar {}, 2982 msNima 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):
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
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):
Aziz (1-oyna): import tugadi
Aziz (2-oyna): import allaqachon ketmoqda
Dilnoza: import tugadi
Aziz (keyinroq): import tugadiDilnoza'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
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):
B kutyapti, to'sib turgan: A
oxirgi holat: bekorpg_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 UPDATEyoki atomikUPDATE— 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) vaSKIP LOCKED— cheksiz kutishning uch muqobili.ALTER TABLEACCESS EXCLUSIVEoladi; navbatdagi ALTER orqasida hamma so'rovlar to'planadi — migratsiyadan oldinlock_timeout.- Advisory lock — ilova ma'nosidagi qulf;
pg_advisory_xact_lockni 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
- PostgreSQL 18 hujjati: Explicit Locking.
- PostgreSQL 18 hujjati: SELECT — The Locking Clause.
- PostgreSQL 18 hujjati: Advisory Lock Functions.
- PostgreSQL 18 hujjati: pg_locks.
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!