Mundarija (29)
- Bu darsda
- 1. Nega bu kerak?
- 2. Deadlock
- 2.1 Ikki sessiyada
- 2.2 deadlock_timeout va kim "qurbon" bo'ladi
- 3. Deadlock'ning oldini olish: bir xil tartib
- 4. Serialization failure: 40001
- 5. Qayta urinish funksiyasi (retry wrapper)
- 5.1 To'rt qoida
- 5.2 Mantiqni bazasiz sinaymiz
- 5.3 pg bilan: kanon wrapper'i
- 5.4 Yon ta'sirlar — tranzaksiyadan tashqarida
- 6. Kutishni cheklash: timeout'lar
- 7. Node qadami
- 7.1 Qulf tartibi va qayta urinish
- 7.2 Topilma: matnlarni almashtiradigan PUT
- 8. Ko'p uchraydigan xatolar
- 8.1 Faqat oxirgi so'rovni qaytarish
- 8.2 Hamma xatoni qayta urinish
- 8.3 Cheksiz qayta urinish
- 8.4 Tranzaksiya ichida tashqi ish
- 8.5 Deadlock'ni deadlock_timeout bilan "tuzatish"
- 9. Mashqlar
- 1-mashq (oson): Qayta urinasizmi?
- 2-mashq (o'rta): Pauzalar jadvali
- 3-mashq (qiyin): Deadlock va uning davosi Node'da
- 10. Real ishda
- Xulosa
- Manbalar
PostgreSQL deadlock va serialization failure: 40P01, 40001 va qayta urinish
Qisqacha: Deadlock — ikki tranzaksiya bir-birining qulfini kutib, abadiy qotib qolishi. Postgres buni 1 soniyada (
deadlock_timeout) aniqlaydi va bittasini40P01 deadlock detectedbilan bekor qiladi. Serialization failure (40001) — REPEATABLE READ yoki SERIALIZABLE'da "parallel o'zgarish tufayli natijangiz noto'g'ri bo'lardi". Ikkalasi ham normal xato: ilova butun tranzaksiyani boshidan, pauza bilan (exponential backoff + jitter) qayta urinishi kerak. Deadlock'ning eng yaxshi davosi esa — qulflarni hamma joyda bir xil tartibda olish.
Bu darsda
- Deadlock qanday paydo bo'lishini ikki sessiyada ko'rsatasiz va xato matnini o'qiysiz.
- Qulflarni bir xil tartibda olib, deadlock'ning oldini olasiz.
40001va40P01ni boshqa xatolardan ajratib, qayta urinish funksiyasini (retry wrapper) yozasiz.- Exponential backoff va jitter nima uchun kerakligini tushuntirasiz.
lock_timeout,statement_timeoutva ularning SQLSTATE kodlarini bilasiz.
Oldin bilishingiz kerak: Qulflar: row, table va advisory, Read phenomena va izolyatsiya darajalari, Tranzaksiya asoslari.
1. Nega bu kerak?
Oldingi darslarda ikki xil "kutilgan" xatoni ko'rdik. Izolyatsiya darsida SERIALIZABLE tranzaksiya 40001 bilan yiqildi va Postgres ochiq aytdi: HINT: The transaction might succeed if retried. — "qayta urinsangiz, o'tishi mumkin". Qulflar darsida esa ikki yozuvchi bir-birini kutdi.
Endi tasavvur qiling: Malika Jasurga 100 so'm o'tkazyapti, aynan shu soniyada Jasur Malikaga 50 so'm. Birinchi o'tkazma avval Malika hisobini qulflaydi, keyin Jasurnikini. Ikkinchisi — teskari: avval Jasur, keyin Malika. Ikkalasi ham birinchi qulfni oldi va ikkinchisini kutyapti. Har biri ikkinchisi "bo'shatishini" kutadi — hech qachon bo'shatmaydi.
Hayotiy o'xshatish: tor ko'chada ikki mashina yuzma-yuz kelib qoldi. Har biri "u orqaga yursin" deb kutadi. Kimdir bitta mashinaga "orqaga yur" demasa, ular kechgacha turadi. Postgres — o'sha "kimdir": u bitta tranzaksiyani bekor qiladi, ikkinchisi o'tib ketadi. Bekor qilingan haydovchi esa (ilova) bir oz kutib, qayta urinishi kerak.
Bu darsning asosiy fikri: ba'zi xatolar — dasturdagi xato emas, parallel ishning tabiiy natijasi. Ularni ushlash va to'g'ri qayta urinish — har jiddiy backend'ning majburiy qismi.
2. Deadlock
2.1 Ikki sessiyada
hisob jadvali — izolyatsiya darsidagi, balanslar 1 000 dan. A: Malika → Jasur 100. B: Jasur → Malika 50. Natijalarni biz ishga tushirdik: PostgreSQL 17.6, ikki psql sessiyasi.
| Qadam | A sessiya (Malika → Jasur) | B sessiya (Jasur → Malika) | Natija |
|---|---|---|---|
| 1 | SHOW deadlock_timeout;, BEGIN; |
BEGIN; |
ikkalasi tranzaksiyada |
| 2 | UPDATE hisob SET balans = balans - 100 WHERE id = 1; |
A Malikani qulfladi | |
| 3 | UPDATE hisob SET balans = balans - 50 WHERE id = 2; |
B Jasurni qulfladi | |
| 4 | UPDATE hisob SET balans = balans + 100 WHERE id = 2; |
A kutadi (Jasur B da) | |
| 5 | UPDATE hisob SET balans = balans + 50 WHERE id = 1; |
B kutadi (Malika A da) — aylana | |
| 6 | COMMIT; |
COMMIT; |
kim o'tdi? |
Transkriptda A ning xatosi qachon chiqqaniga va DETAIL qatorlariga qarang:
A=> SHOW deadlock_timeout;
A deadlock_timeout
A ------------------
A 1s
A (1 row)
A=> BEGIN;
A BEGIN
B=> BEGIN;
B BEGIN
A=> UPDATE hisob SET balans = balans - 100 WHERE id = 1;
A UPDATE 1
B=> UPDATE hisob SET balans = balans - 50 WHERE id = 2;
B UPDATE 1
A=> UPDATE hisob SET balans = balans + 100 WHERE id = 2;
B=> UPDATE hisob SET balans = balans + 50 WHERE id = 1;
B UPDATE 1
A ERROR: deadlock detected
A DETAIL: Process 8356 waits for ShareLock on transaction 1073; blocked by process 25936.
A Process 25936 waits for ShareLock on transaction 1072; blocked by process 8356.
A HINT: See server log for query details.
A CONTEXT: while updating tuple (0,2) in relation "hisob"
A=> COMMIT;
A ROLLBACK
B=> COMMIT;
B COMMIT
A=> SELECT id, balans FROM hisob ORDER BY id;
A id | balans
A ----+--------
A 1 | 1050
A 2 | 950
A (2 rows)Nima bo'ldi:
- A Malikani (
id = 1), B Jasurni (id = 2) qulfladi. - A Jasurni so'radi — B ushlab turibdi, A kutadi.
- B Malikani so'radi — A ushlab turibdi. Aylana yopildi: A → B → A.
- Taxminan 1 soniyadan keyin Postgres aylanani topdi va A ni bekor qildi:
ERROR: deadlock detected("o'zaro qotib qolish aniqlandi"). - A bekor bo'lgach uning qulflari bo'shadi va B'ning
UPDATEi darhol o'tdi (B UPDATE 1xato qatoridan oldin chiqdi). - A'ning
COMMITiROLLBACKga aylandi. Natija: faqat B'ning o'tkazmasi — Malika 1 050, Jasur 950. Jami 2 000 — pul yo'qolmadi, lekin A'ning o'tkazmasi bajarilmadi.
DETAIL ni o'qiymiz: "8356 jarayoni 1073-tranzaksiyaga ShareLock kutyapti, uni 25936 to'sib turibdi. 25936 esa 1072 ni kutyapti, uni 8356 to'sib turibdi". Bu — Qulflar darsidagi transactionid ShareLock: kutuvchi qatorni emas, tranzaksiya tugashini kutadi. HINT aytadi: so'rov matnlari server log'ida (ular boshqa foydalanuvchilarning ma'lumotini o'z ichiga olishi mumkin, shuning uchun mijozga yuborilmaydi).
Kod — 40P01 (deadlock_detected). Kanon xuddi shu tajribani Node'da qilgan: deadlock 1 003 ms da aniqlangan, bitta tranzaksiya 40P01, ikkinchisi ok (t53-deadlock.txt).
flowchart LR
A["A tranzaksiya<br/>ushlab turibdi: Malika"] -->|kutyapti: Jasur| B["B tranzaksiya<br/>ushlab turibdi: Jasur"]
B -->|kutyapti: Malika| A2.2 deadlock_timeout va kim "qurbon" bo'ladi
Postgres deadlock'ni har lahzada tekshirmaydi — bu qimmat. Tranzaksiya qulfni deadlock_timeout (sukut — 1 s) kutgandan keyingina u aylanani qidiradi. Qaysi tranzaksiya bekor qilinishi kafolatlanmagan — odatda tekshiruvni boshlagan (ko'proq kutgan) jarayon. Shuning uchun ilova ikkala tomonda ham 40P01 ga tayyor bo'lishi kerak.
deadlock_timeout ni kamaytirish deadlock'ni yo'qotmaydi — faqat tezroq aniqlaydi va oddiy qisqa kutishlarda ham keraksiz tekshiruvlar qo'shadi. Odatda sukutda qoldiriladi.
Tekshirib ko'ring: Deadlock'dan keyin A'ning tranzaksiyasida birinchi
UPDATE(Malikadan 100 ayirish) muvaffaqiyatli bo'lgan edi. Malika balansida bu 100 qoldimi?
Javob
Yo'q. Deadlock xatosi butun tranzaksiyani buzadi, COMMIT esa ROLLBACK bo'ladi — A'ning ikkala o'zgarishi ham bekor. Natijadagi 1 050 — faqat B'ning +50 i. Shuning uchun deadlock'da pul "yarim yo'lda" qolmaydi — atomiklik saqlanadi. Lekin A'ning o'tkazmasi umuman bajarilmadi: ilova uni qayta urinmasa, Malika "o'tkazdim" deb o'ylaydi, Jasur esa pul olmaydi.
3. Deadlock'ning oldini olish: bir xil tartib
Deadlock faqat qulflar turli tartibda olinganda bo'ladi. Hamma tranzaksiya qulflarni bir xil tartibda olsa — masalan, id o'sish tartibida — aylana yopila olmaydi: ikkinchisi birinchi qulfdayoq kutadi.
-- A va B
BEGIN;
-- A: Malika → Jasur; avval ikkala hisobni id bo'yicha qulflaydi
SELECT id FROM hisob WHERE id IN (1, 2) ORDER BY id FOR UPDATE;
-- B: Jasur → Malika; xuddi shu tartibda
SELECT id FROM hisob WHERE id IN (2, 1) ORDER BY id FOR UPDATE;
-- A
UPDATE hisob SET balans = balans - 100 WHERE id = 1;
UPDATE hisob SET balans = balans + 100 WHERE id = 2;
COMMIT;
-- B
UPDATE hisob SET balans = balans - 50 WHERE id = 2;
UPDATE hisob SET balans = balans + 50 WHERE id = 1;
COMMIT;Natija:
A=> SELECT id FROM hisob WHERE id IN (1, 2) ORDER BY id FOR UPDATE;
A id
A ----
A 1
A 2
A (2 rows)
B=> SELECT id FROM hisob WHERE id IN (2, 1) ORDER BY id FOR UPDATE;
A=> UPDATE hisob SET balans = balans - 100 WHERE id = 1;
A UPDATE 1
A=> UPDATE hisob SET balans = balans + 100 WHERE id = 2;
A UPDATE 1
A=> COMMIT;
A COMMIT
B id
B ----
B 1
B 2
B (2 rows)
B=> UPDATE hisob SET balans = balans - 50 WHERE id = 2;
B UPDATE 1
B=> UPDATE hisob SET balans = balans + 50 WHERE id = 1;
B UPDATE 1
B=> COMMIT;
B COMMIT
A=> SELECT id, balans FROM hisob ORDER BY id;
A id | balans
A ----+--------
A 1 | 950
A 2 | 1050
A (2 rows)(Boshidagi ikki BEGIN tushirildi.) B o'z FOR UPDATEida birinchi qatordayoq kutdi va A tugagach davom etdi. Ikkala o'tkazma ham bajarildi: 1 000 − 100 + 50 = 950, 1 000 + 100 − 50 = 1 050. Deadlock yo'q.
E'tibor bering: B IN (2, 1) deb yozdi, lekin ORDER BY id qulflarni baribir 1, 2 tartibida oldi. Tartibni so'rov emas, ORDER BY belgilaydi.
Amaliy qoidalar:
- Bir nechta qatorni o'zgartirishdan oldin ularni
SELECT … ORDER BY id FOR UPDATEbilan bir xil tartibda qulflang. - Ilova kodida ham tartib: "avval kichik id li hisob" —
Math.min(kimdan, kimga). - Jadvallar tartibi: bir tranzaksiya avval
vazifalar, keyinizohlarga yozsa, ikkinchisi teskari yozmasin. - Tranzaksiyani qisqa tuting — qulf qancha qisqa ushlansa, to'qnashuv ehtimoli shuncha kam.
UPDATE … WHERE id IN (…) ning o'zi qatorlarni qaysi tartibda qulflashini kafolatlamaydi — u reja (plan) ga bog'liq. Ko'p qatorli UPDATE lar parallel bo'lsa, ham deadlock bo'lishi mumkin. Yechim yana o'sha: avval SELECT … ORDER BY … FOR UPDATE.
Tartib har doim ham yetarli emas. Trigger'lar va tashqi kalitlar ham qulf oladi (Qulflar darsidagi counter cache misoli) — ularning tartibi har doim ko'rinmaydi. Biz buni o'lchadik (PostgreSQL 17.6): exclusion cheklovli bron jadvaliga 4 ta parallel INSERT — 30 seansdan 9 tasida kamida bitta 40P01 chiqdi, garchi kodda hech qanday FOR UPDATE yo'q edi (Race condition'larni hal qilish). Shuning uchun tartib — birinchi himoya, qayta urinish — ikkinchisi.
4. Serialization failure: 40001
Izolyatsiya darsida bu xatoni ikki marta ko'rdik:
- REPEATABLE READ'da lost update o'rniga:
could not serialize access due to concurrent update. - SERIALIZABLE'da write skew o'rniga:
could not serialize access due to read/write dependencies among transactions.
Ikkalasining kodi — 40001 (serialization_failure). Bu xato "sizning tranzaksiyangiz noto'g'ri natija berishi mumkin edi, shuning uchun bekor qilindi" degani. Deadlock'dan farqi: u qulf aylanasi tufayli emas, ma'lumot to'qnashuvi tufayli.
SERIALIZABLE'da 40001 tez-tez chiqadi. Kanon o'lchadi: 50 ta parallel o'tkazma (har biri balansni o'qib, keyin yozadi), SERIALIZABLE, qayta urinishsiz — 45 tasi 40001 bilan yiqildi, faqat 5 tasi o'tdi (t53-deadlock.txt). Pul yo'qolmadi (jami 2 000), lekin 45 foydalanuvchi "xato" ko'rdi.
SQL standartida 40 sinfi — "Transaction Rollback": shu sinfdagi har xato tranzaksiya bekor qilinganini bildiradi. Amalda qayta urinish uchun ikkitasi muhim: 40001 va 40P01.
5. Qayta urinish funksiyasi (retry wrapper)
5.1 To'rt qoida
- Faqat kerakli kodlar:
40001va40P01.23505(takror kalit),23514(CHECK) — qayta urinish yordam bermaydi, ular darhol yuqoriga uzatiladi. - Butun tranzaksiya — boshidan:
BEGIN, hammaSELECTlar, hisob-kitob,UPDATElar,COMMIT. Faqat yiqilgan so'rovni qaytarish — xato: tranzaksiya allaqachon bekor, eskiSELECTnatijalari esa eskirgan. - Chegara: masalan, 5–10 urinish. Undan keyin xatoni foydalanuvchiga ko'rsatish (HTTP'da 503 yoki 409).
- Pauza va tasodif: urinishlar orasida kutish, har safar ikki barobar uzunroq (exponential backoff) va tasodifiy (jitter).
Nega jitter? Ikki tranzaksiya to'qnashdi, ikkalasi 10 ms kutib, bir vaqtda qayta urindi — yana to'qnashdi. Tasodifiy pauza ularni vaqt bo'yicha "sochib yuboradi". "Full jitter" — pauza 0 dan asos × 2ⁱ gacha tasodifiy son. Bu g'oyani Asinxron naqshlar bilan tarmoq so'rovlarida ham ko'rgansiz — bazada ham xuddi shunday.
5.2 Mantiqni bazasiz sinaymiz
Avval qayta urinish mantig'ini bazasiz yozamiz — taqlid qilingan xato bilan. Bu blokni saytda ishga tushirish mumkin:
const QAYTA = new Set(["40001", "40P01"]);
const kut = (ms) => new Promise((r) => setTimeout(r, ms));
async function qaytaUrin(ish, { urinish = 5, asos = 10 } = {}) {
for (let i = 1; ; i++) {
try {
return await ish(i);
} catch (e) {
if (!QAYTA.has(e.code) || i >= urinish) throw e;
console.log(`${i}-urinish: ${e.code} — qayta urinamiz`);
await kut(Math.random() * asos * 2 ** i); // full jitter
}
}
}
function pgXatosi(code, xabar) {
return Object.assign(new Error(xabar), { code });
}
const natija = await qaytaUrin(async (i) => {
if (i <= 2) throw pgXatosi("40001", "could not serialize access");
return `o'tkazma bajarildi (${i}-urinishda)`;
});
console.log(natija);
try {
await qaytaUrin(async () => {
throw pgXatosi("23505", "duplicate key value");
});
} catch (e) {
console.log(`qayta urinilmadi: ${e.code} ${e.message}`);
}Konsolda:
1-urinish: 40001 — qayta urinamiz
2-urinish: 40001 — qayta urinamiz
o'tkazma bajarildi (3-urinishda)
qayta urinilmadi: 23505 duplicate key valueish — tranzaksiyaning butun tanasi (funksiya), shuning uchun har urinishda u boshidan bajariladi. 23505 birinchi urinishdayoq yuqoriga chiqdi — qayta urinish ma'nosiz edi.
5.3 pg bilan: kanon wrapper'i
Haqiqiy versiyada har urinish — pool'dan yangi client, BEGIN, ish, COMMIT; xatoda ROLLBACK va release. Kanon kodi (t53-deadlock.mjs):
const QAYTA = new Set(["40001", "40P01"]);
async function qaytaUrin(p, fn, { urinish = 10, asos = 5 } = {}) {
for (let i = 1; ; i++) {
const c = await p.connect();
try {
await c.query("BEGIN ISOLATION LEVEL SERIALIZABLE");
const r = await fn(c);
await c.query("COMMIT");
return { r, urinish: i };
} catch (e) {
await c.query("ROLLBACK").catch(() => {});
if (!QAYTA.has(e.code) || i >= urinish) throw e;
await kut(Math.random() * asos * 2 ** i); // "full jitter"
} finally { c.release(); }
}
}Muhim joylar:
COMMIThamtryichida: SERIALIZABLE xatosiCOMMITpaytida chiqishi mumkin (izolyatsiya darsidagi write skew).ROLLBACKdagi.catch(() => {})— ulanish uzilgan bo'lsa,ROLLBACKning o'zi xato beradi; u asl xatoni yashirmasligi kerak.finally { c.release() }— har yo'lda client pool'ga qaytadi (Tranzaksiya asoslari).
Kanon natijasi (50 parallel o'tkazma, SERIALIZABLE, pool 10; PostgreSQL 18.6, Node 24.21.0):
2) 50 parallel o'tkazma, SERIALIZABLE, retry'siz: 40001 bilan yiqilgan 45, urinishlar 5, balans 950 / 1050 (jami 2000), 102 ms
2) 50 parallel o'tkazma, SERIALIZABLE, retry BILAN: 40001 bilan yiqilgan 0, urinishlar 197, balans 500 / 1500 (jami 2000), 796 ms- Retry'siz: bajarildi5 ta
- Retry'siz: 4000145 ta
- Retry bilan: bajarildi197 urinish, 796 ms50 ta
- Retry bilan: 400010 ta
Manba: IlmHamroh kanon o'lchovi, 2026-10-24: PostgreSQL 18.6, Node 24.21.0, pg 8.23.1 (t53-deadlock.txt)
Retry bilan hammasi o'tdi: 50 ta o'tkazma uchun 197 urinish (o'rtacha ~4), vaqt 102 ms dan 796 ms ga oshdi. Bu SERIALIZABLE'ning narxi: to'g'rilik kafolatlangan, lekin to'qnashuv ko'p bo'lsa — vaqt va qayta ish. Balans 500 / 1 500 — 50 × 10 so'm haqiqatan bir hisobdan ikkinchisiga o'tdi.
5.4 Yon ta'sirlar — tranzaksiyadan tashqarida
Qayta urinish tranzaksiya ichidagi narsalarni qayta bajaradi. Agar ichida email yuborilgan yoki tashqi to'lov API'si chaqirilgan bo'lsa — ular ham ikki-uch marta bajariladi, va ROLLBACK ularni qaytara olmaydi. Qoida: tranzaksiya ichida — faqat baza ishi. Tashqi ish — COMMIT dan keyin (yoki navbat orqali — SKIP LOCKED navbat).
Tekshirib ko'ring: Qayta urinish funksiyasi
40001dan keyin pauzasiz darhol qayta urinadi. 50 parallel so'rovda nima bo'lishi mumkin?
Javob
To'qnashgan tranzaksiyalar deyarli bir vaqtda qayta boshlanadi va yana to'qnashadi — urinishlar soni keskin oshadi, ba'zilari chegaraga yetib, baribir yiqiladi. Baza ham ortiqcha yuk oladi. Exponential backoff kutishni uzaytiradi, jitter esa urinishlarni vaqt bo'yicha tarqatadi — to'qnashuvlar kamayadi.
6. Kutishni cheklash: timeout'lar
Qayta urinish "yiqilgan" tranzaksiya uchun. Lekin tranzaksiya yiqilmasdan uzoq kutib qolsa-chi? Uch sozlama:
lock_timeout— qulfni ko'pi bilan shuncha kutadi (Qulflar darsida ko'rdik). Xato55P03.statement_timeout— bitta so'rov ko'pi bilan shuncha davom etadi (qulf kutish ham, hisoblash ham).idle_in_transaction_session_timeout— tranzaksiya ochiq, lekin hech narsa qilmayapti — shuncha vaqtdan keyin ulanish uziladi.pool.query('BEGIN')tuzog'idan qolgan ulanishlarga qarshi.
SET statement_timeout = '100ms';
SELECT pg_sleep(1);
RESET statement_timeout;Natija:
A=> SET statement_timeout = '100ms';
A SET
A=> SELECT pg_sleep(1);
A ERROR: canceling statement due to statement timeout
A=> RESET statement_timeout;
A RESET"So'rov vaqti tugagani uchun bekor qilindi". Kod — 57014 (query_canceled). Kanon ham shu kodni olgan.
Qaysi xatoga ilova qanday javob beradi — bitta jadvalda:
| SQLSTATE | Nomi | Ilova nima qiladi |
|---|---|---|
40001 |
serialization_failure | butun tranzaksiyani qayta urinish |
40P01 |
deadlock_detected | butun tranzaksiyani qayta urinish |
55P03 |
lock_not_available | "band" — keyinroq yoki qayta urinish |
57014 |
query_canceled | so'rovni optimallash; 503/504 |
23505 |
unique_violation | qayta urinmaslik; 409 |
23P01 |
exclusion_violation | qayta urinmaslik; 409 (vazifalar dagi takror matn, holat 53 dan) |
55P03 ni qayta urinish mumkin, lekin ehtiyot bo'ling: qulf uzoq ushlanayotgan bo'lsa, qayta urinish faqat kutishni uzaytiradi. 57014 ni qayta urinish odatda foydasiz — so'rov o'zi sekin.
statement_timeout ni butun ilova uchun ham qo'yish mumkin: ulanish satrida (?options=-c%20statement_timeout%3D5000) yoki rol uchun ALTER ROLE … SET statement_timeout = '5s'. Node'da AbortSignal bilan so'rovni klient tomonida kutishni to'xtatish — serverdagi so'rovni to'xtatmaydi; server tomonidagi chegara — aynan statement_timeout.
7. Node qadami
vazifalar-api da bu darsning ikkala saboqi ham ishlatildi — va yo'lda bitta kutilmagan topilma chiqdi.
7.1 Qulf tartibi va qayta urinish
Qulflar darsining «Node qadami» da PgOmbori dagi deadlock'lar ko'rindi: 200 aralash amalda 138 va 88 ta 40P01 (holat 48). Sabab — «Deadlock» bo'limidagi manzara: PATCH va PUT ikki qatorni teskari tartibda qulflardi. Birinchi davo — bir xil tartib (hamma yozuv avval egasi qatorini qulflaydi). Ikkinchisi — qayta urinish: baribir 40P01/40001 kelsa (masalan, bazaga to'g'ridan SQL yozgan boshqa jarayon bilan), butun tranzaksiya qaytadan. Ikkalasi bitta yordamchida — #yoz (holat 53):
const QAYTA_URINISH = 5;
async #yoz<T>(fn: (c: PoolClient) => Promise<T>): Promise<T> {
for (let urinish = 1; ; urinish++) {
try {
return await this.#tranzaksiyada(async (c) => {
await c.query(
"SELECT 1 FROM foydalanuvchilar WHERE id = $1 FOR UPDATE",
[this.#egasi]);
return fn(c);
});
} catch (x) {
const kod = x instanceof pg.DatabaseError ? x.code : undefined;
const qayta = kod === "40P01" || kod === "40001";
if (!qayta || urinish >= QAYTA_URINISH) throw x;
await new Promise((r) =>
setTimeout(r, Math.random() * 10 * 2 ** urinish));
}
}
}(Asl kodda shart bitta qatorda; o'qish uchun qayta o'zgaruvchisiga ajratildi.) «Qayta urinish funksiyasi» bo'limidagi to'rt qoidaning hammasi shu yerda: faqat ikki kod, butun tranzaksiya (fn boshidan), chegara 5, full jitter. Kanon wrapper'idan bitta farq: ROLLBACK xatosi yutilmaydi — #tranzaksiyada buzilgan ulanishni release(xato) bilan yopadi (Tranzaksiya asoslari).
Natija (olchov/raqobat-olchov.ts, PostgreSQL 18.6, ikki seans):
(3) 200 aralash amal (40 PUT + 160 PATCH): ok 200, xatolar {}, 2982 ms
(2-seans) (3) 200 aralash amal (40 PUT + 160 PATCH): ok 200, xatolar {}, 2176 msNima bo'ldi? 200 dan 200 — xatosiz (holat 48 da 62 va 112 edi). Qayta urinish hisobotda alohida ko'rinmaydi: qulf tartibi deadlock'ning o'zini yo'qotdi, retry — sug'urta.
7.2 Topilma: matnlarni almashtiradigan PUT
Raqobat testi yozilayotganda yana bir xato chiqdi. PUT /v1/vazifalar ikki vazifaning matnini almashtirsa (1-vazifa "Non" → "Sut", 2-vazifa "Sut" → "Non"), bitta UPDATE … FROM unnest ham 23505 berdi — garchi yakuniy holatda takror yo'q bo'lsa ham. Biz buni kichik misolda takrorladik (PostgreSQL 17.6, holat 48 sxemasi — noyoblik hali partial unique indeks):
UPDATE vazifalar v SET matn = u.matn
FROM (VALUES (1, 'Sut olish #bozor'), (2, 'Non olish #bozor'))
AS u(id, matn)
WHERE v.id = u.id;ERROR: duplicate key value violates unique constraint "vazifalar_egasi_id_matn_key"
DETAIL: Key (egasi_id, lower(matn))=(1, sut olish #bozor) already exists.(Oldin 1- va 2-vazifa matnlari "Non olish #bozor" va "Sut olish #bozor" qilingan edi.) Sabab hujjatda yozilgan: DEFERRABLE bo'lmagan noyoblik har qator yozilganda tekshiriladi, buyruq oxirida emas. 1-qator "Sut" ga o'zgargan lahzada 2-qator hali "Sut" — to'qnashuv.
Yechim — tekshiruvni buyruq oxiriga surish: DEFERRABLE. Lekin partial va ifodali (lower(matn)) indeksni DEFERRABLE qilib bo'lmaydi — bu faqat cheklovlarga (UNIQUE, PRIMARY KEY, EXCLUDE) mumkin. Kanon teng ma'noli exclusion constraint ni tanladi — sql/014-matn-noyob-kechiktirilgan.sql:
DROP INDEX vazifalar_egasi_id_matn_key;
ALTER TABLE vazifalar ADD CONSTRAINT vazifalar_egasi_id_matn_excl
EXCLUDE USING btree (egasi_id WITH =, lower(matn) WITH =)
WHERE (ochirilgan_vaqt IS NULL)
DEFERRABLE INITIALLY IMMEDIATE;EXCLUDE … WITH = — "ikki faol qatorda egasi_id ham, lower(matn) ham teng bo'lmasin". Bu noyob indeks bilan bir xil qoida (Race condition'larni hal qilish darsida EXCLUDE ni bron uchun && bilan ko'ramiz). INITIALLY IMMEDIATE — har buyruq oxirida tekshiriladi (kerak bo'lsa SET CONSTRAINTS … DEFERRED bilan COMMIT'gacha surish mumkin). Holat 53 sxemasida o'sha almashtirish (biz ishga tushirdik, PostgreSQL 17.6):
UPDATE vazifalar v SET matn = u.matn
FROM (VALUES (1, 'Non olish #bozor'), (2, 'Sut olish #bozor'))
AS u(id, matn)
WHERE v.id = u.id;
UPDATE vazifalar v SET matn = u.matn
FROM (VALUES (1, 'Sut olish #bozor'), (2, 'Non olish #bozor'))
AS u(id, matn)
WHERE v.id = u.id;
SELECT id, matn FROM vazifalar WHERE id IN (1, 2) ORDER BY id;UPDATE 2
UPDATE 2
id | matn
----+------------------
1 | Sut olish #bozor
2 | Non olish #bozor
(2 rows)Ikkinchi UPDATE — almashtirish — o'tdi. Narxi: takror matn xatosining kodi o'zgardi — 23505 o'rniga 23P01 (exclusion_violation), xabar: conflicting key value violates exclusion constraint "vazifalar_egasi_id_matn_excl". Ombordagi takrorXatosi ikkala kodni ham 409 takror-matn ga aylantiradi — API mijozi farqni sezmaydi. Shu sababli 26-qismning SQL darslarida (holat 40 gacha) takror matn 23505 beradi, API darslarida esa 23P01.
Bu qadam ikkita yangi test bilan tugadi: "PUT ikki vazifa matnini almashtiradi" va raqobat testi (amaliyot darsida). Holat 53 ning commit'i — Amaliyot: race condition'larni hal qilish darsida.
Kanonning t53-deadlock.mjs tajribasi (yuqoridagi bo'limlarning Node varianti) chiqishining boshi:
1) deadlock (1003 ms; deadlock_timeout 1s):
A: XATO 40P01 deadlock detected
B: ok
tartib bilan (ORDER BY id FOR UPDATE): B ok8. Ko'p uchraydigan xatolar
8.1 Faqat oxirgi so'rovni qaytarish
catch ichida yiqilgan UPDATE ni qayta yuborish. Tranzaksiya bekor — ERROR: current transaction is aborted, commands ignored until end of transaction block. ROLLBACK, keyin butun funksiya boshidan.
8.2 Hamma xatoni qayta urinish
23505, 23P01 yoki 23514 ni 10 marta qayta urinish — 10 marta bir xil xato va foydalanuvchi 10 barobar kutadi. Faqat 40001, 40P01 (va ongli ravishda 55P03).
8.3 Cheksiz qayta urinish
Yuqori yukda tranzaksiya hech qachon o'tmasligi mumkin. Chegara (5–10) va oxirida aniq xato.
8.4 Tranzaksiya ichida tashqi ish
Retry email'ni uch marta yuboradi. Tashqi ish — COMMIT dan keyin.
8.5 Deadlock'ni deadlock_timeout bilan "tuzatish"
Sozlama faqat aniqlash tezligini o'zgartiradi. Qulf tartibi + qisqa tranzaksiya + qayta urinish.
9. Mashqlar
1-mashq (oson): Qayta urinasizmi?
Har xato uchun ayting: qayta urinish kerakmi? (a) 40P01 deadlock detected; (b) 23P01 conflicting key value violates exclusion constraint "vazifalar_egasi_id_matn_excl"; (c) 40001 could not serialize access due to concurrent update; (d) 23514 … violates check constraint "vazifalar_matn_check"; (e) 57014 canceling statement due to statement timeout.
Yechim
(a) Ha — butun tranzaksiya. (b) Yo'q — bu vazifa allaqachon bor; foydalanuvchiga "Bu vazifa ro'yxatda allaqachon bor" (409). (c) Ha. (d) Yo'q — ma'lumot noto'g'ri (masalan, 100 belgidan uzun); 400/422. (e) Odatda yo'q — so'rov sekin; uni tezlashtirish kerak (indekslar — keyingi darslarda).
2-mashq (o'rta): Pauzalar jadvali
pauza(urinish, { asos, tavan, tasodif }) funksiyasini yozing: full jitter — Math.floor(tasodif() * min(tavan, asos × 2^urinish)). Sukut: asos = 10, tavan = 1000, tasodif = Math.random. Sinash uchun tasodif ni o'zingiz bering (() => 0.999999 — eng katta pauza, () => 0.5 — o'rtasi) va 1–8 urinish uchun jadval chiqaring. tavan nima uchun kerak? Eslatma: ** — darajaga ko'tarish, Math.min, Math.floor — JS asoslaridan.
Yechim
function pauza(urinish, sozlama = {}) {
const { asos = 10, tavan = 1000, tasodif = Math.random } = sozlama;
const chegara = Math.min(tavan, asos * 2 ** urinish);
return Math.floor(tasodif() * chegara);
}
for (let i = 1; i <= 8; i++) {
const eng = pauza(i, { tasodif: () => 0.999999 });
const yarim = pauza(i, { tasodif: () => 0.5 });
console.log(`${i}-urinish: 0…${eng} ms, o'rtasi ${yarim} ms`);
}Konsolda:
1-urinish: 0…19 ms, o'rtasi 10 ms
2-urinish: 0…39 ms, o'rtasi 20 ms
3-urinish: 0…79 ms, o'rtasi 40 ms
4-urinish: 0…159 ms, o'rtasi 80 ms
5-urinish: 0…319 ms, o'rtasi 160 ms
6-urinish: 0…639 ms, o'rtasi 320 ms
7-urinish: 0…999 ms, o'rtasi 500 ms
8-urinish: 0…999 ms, o'rtasi 500 mstavan (ship) bo'lmasa, 10-urinishda pauza 10 soniyagacha, 15-da — 5 daqiqagacha o'sardi. 7-urinishdan boshlab chegara 1 000 ms da to'xtadi. tasodif ni parametr qilish — funksiyani sinash (test) mumkin qiladi: haqiqiy Math.random bilan natija har safar boshqacha.
3-mashq (qiyin): Deadlock va uning davosi Node'da
otkaz(kimdan, kimga, summa, tartibla) funksiyasini yozing: tranzaksiyada kimdan dan ayiradi, 100 ms kutadi, kimga ga qo'shadi. tartibla = true bo'lsa, avval ikkala qatorni ORDER BY id FOR UPDATE bilan qulflaydi. Ikki o'tkazmani (1→2 va 2→1) parallel ishga tushiring — avval tartibla = false, keyin true. Har safar nechta ok, qaysi xato kodlari va jami balansni chiqaring. Eslatma: = ANY($1) va massiv parametri — Parametrli so'rovlar.
Yechim
import pg from "pg";
const url = process.env.DATABASE_URL;
const pool = new pg.Pool({ connectionString: url, max: 4 });
const kut = (ms) => new Promise((r) => setTimeout(r, ms));
await pool.query("DROP TABLE IF EXISTS m53_hisob");
await pool.query(`CREATE TABLE m53_hisob
(id int PRIMARY KEY, balans int NOT NULL)`);
await pool.query("INSERT INTO m53_hisob VALUES (1, 1000), (2, 1000)");
// kimdan -> kimga summa;
// tartibla = true — qulflar id bo'yicha olinadi
async function otkaz(kimdan, kimga, summa, tartibla) {
const c = await pool.connect();
try {
await c.query("BEGIN");
if (tartibla) {
await c.query(
`SELECT id FROM m53_hisob WHERE id = ANY($1)
ORDER BY id FOR UPDATE`, [[kimdan, kimga]]);
}
await c.query(
"UPDATE m53_hisob SET balans = balans - $2 WHERE id = $1",
[kimdan, summa]);
await kut(100); // ikkinchi o'tkazma ham birinchi qulfini olsin
await c.query(
"UPDATE m53_hisob SET balans = balans + $2 WHERE id = $1",
[kimga, summa]);
await c.query("COMMIT");
return "ok";
} catch (e) {
await c.query("ROLLBACK");
return e.code;
} finally {
c.release();
}
}
for (const tartibla of [false, true]) {
const natija = await Promise.all([
otkaz(1, 2, 100, tartibla),
otkaz(2, 1, 50, tartibla),
]);
const xato = natija.filter((n) => n !== "ok");
const { rows } = await pool.query(
"SELECT sum(balans)::int AS jami FROM m53_hisob");
const ok = natija.length - xato.length;
console.log(`tartibla=${tartibla}: ok ${ok},` +
` xato [${xato.join(", ")}], jami ${rows[0].jami}`);
}
await pool.query("DROP TABLE m53_hisob");
await pool.end();Biz ishga tushirdik (PostgreSQL 17.6, Node 24.21.0, pg 8.23.1):
tartibla=false: ok 1, xato [40P01], jami 2000
tartibla=true: ok 2, xato [], jami 2000Tartibsiz — bittasi 40P01, jami saqlangan (atomiklik), lekin bitta o'tkazma bajarilmadi. Tartib bilan — ikkalasi ham o'tdi. Keyingi qadam sifatida otkaz ni «Mantiqni bazasiz sinaymiz» bo'limidagi qaytaUrin bilan o'rab ko'ring — tartibsiz holatda ham ikkalasi bajariladi (bittasi ikkinchi urinishda).
10. Real ishda
- Bank va to'lov tizimlari: o'tkazmalarda qulf tartibi ("kichik hisob raqami birinchi") — klassik qoida.
- ORM va kutubxonalar: ko'p ORM'larda tayyor retry yo'q — o'zingiz yozasiz; Prisma'ning interaktiv tranzaksiyalarida ham
P2034(write conflict/deadlock) xatosini ushlab qayta urinish tavsiya etiladi (27-qism). - Monitoring:
log_lock_waits = on—deadlock_timeoutdan uzoq kutishlar log'ga yoziladi;pg_stat_database.deadlocks— deadlock'lar soni. - Intervyu: "Deadlock nima va qanday oldini olasiz?", "Serialization failure'ga ilova qanday javob beradi?", "Jitter nima uchun kerak?".
Xulosa
- Deadlock — qulflarning aylanasi; Postgres
deadlock_timeout(1 s) dan keyin bittasini40P01bilan bekor qiladi. - Oldini olish: qulflarni hamma joyda bir xil tartibda olish (
ORDER BY id FOR UPDATE) va qisqa tranzaksiya. 40001— RR/SERIALIZABLE'da ma'lumot to'qnashuvi; kanon: retry'siz 50 dan 45 tasi yiqildi.- Qayta urinish: faqat
40001/40P01, butun tranzaksiya boshidan, chegara bilan, exponential backoff + jitter. lock_timeout(55P03),statement_timeout(57014),idle_in_transaction_session_timeout— cheksiz kutishga qarshi.
Keyingi dars: Amaliyot: race condition'larni hal qilish — ombor qoldig'i, bron va idempotent to'lov: uch klassik muammo va o'lchangan yechimlar.
Manbalar
- PostgreSQL 18 hujjati: Explicit Locking — Deadlocks.
- PostgreSQL 18 hujjati: Serialization Failure Handling.
- PostgreSQL 18 hujjati: PostgreSQL Error Codes.
- AWS Architecture Blog: "Exponential Backoff And Jitter" (full jitter g'oyasi).
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!