Nega «yechishdan oldin tekshirish» ishlamaydi
Bu fintech mahsulotga keyinchalik qo‘shib bo‘lmaydigan uchta tugundan biri (qolgan ikkitasi — o‘zgarmas jurnal va solishtirish; umumiy manzara fintech mahsulot yaratish qo‘llanmasida). Batafsil ko‘rib chiqamiz. Mijoz yechish so‘rovini yubordi. Server uni qabul qildi, amalni o‘tkazdi — va shu payt aloqa uzildi. Mijoz javob olmadi. U yechish sodir bo‘ldimi yoki yo‘qmi bilmaydi va barcha qoidalar bo‘yicha so‘rovni takrorlashi shart: POST qo‘shimcha kelishuvsiz idempotent emas (RFC 9110).
Birinchi bo‘lib xayolga keladigan fikr — yechishdan oldin o‘xshash amal yo‘qligini tekshirish: bir xil summa, bir xil oluvchi, oxirgi besh daqiqa. Bu ikki sababga ko‘ra ishlamaydi. Birinchidan, odam do‘stiga bir xil summani ikki marta o‘tkazishga to‘liq haqli. Ikkinchidan, tekshiruv bilan qo‘shish orasida vaqt o‘tadi, va ikkita parallel so‘rov tekshiruvdan ikkalasi ham o‘tadi.
Idempotentlik kaliti: uni kim yaratadi va nimaga ishora qiladi
Kalitni mijoz — ilova yoki frontend — foydalanuvchi niyat shakllantirgan paytda yaratadi. Server emas: server takrorni yangi niyatdan ajrata olmaydi. Va so‘rov yuborilgan paytda emas, shakl ochilgan paytda: aks holda takroriy bosish yangi kalit yaratadi va butun konstruksiya buziladi.
- Bitta kalit — foydalanuvchining bitta niyati. O‘tkazma shaklini ochdi — kalit oldi; «yuborish» ni uch marta bosdi — kalit o‘sha.
- Kalit tasodifiy bo‘lishi kerak (UUID), summa va vaqtdan hosil qilingan emas: ketma-ket ikkita bir xil o‘tkazma — qonuniy stsenariy.
- Kalit cheklangan vaqt yashaydi. Bir sutka — oqilona muddat: u har qanday takrorni qoplaydi va jadvalni abadiy omborga aylantirmaydi.
Bu bazada qanday ko‘rinadi
Minimal sxema — bitta jadval. Unda uchta narsa muhim: idempotentlik kaliti bo‘yicha birlamchi kalit, so‘rov izi va saqlangan javob.
create table payment_request (
idempotency_key uuid primary key,
account_id bigint not null,
request_hash text not null,
status text not null
check (status in ('in_progress', 'succeeded', 'failed')),
response jsonb,
operation_id bigint references operation(id),
created_at timestamptz not null default now()
);
-- Amallar jurnali: faqat qo'shish. Bekor qilish — teskari ishorali yangi
-- yozuv, update emas va, albatta, delete emas.
create table operation (
id bigserial primary key,
account_id bigint not null,
amount numeric(20, 4) not null,
kind text not null,
reverses bigint references operation(id),
created_at timestamptz not null default now()
);Balans bu yerda ustun emas, hisob bo‘yicha sum(amount). Balans kimdir yangilaydigan saqlanadigan qiymatga aylanishi bilanoq, ikkinchi haqiqat manbai va «nega amallar yig‘indisi balansga to‘g‘ri kelmayapti» degan savol paydo bo‘ladi, unga yaxshi javob yo‘q.
Ishlov beruvchi: amallar tartibi muhim
Poygadan himoya — bu unikal kalit bilan insert, insert dan oldingi select emas. Qo‘shishdan oldingi tekshiruv qutqarmaydi: ikkita parallel so‘rov undan ikkalasi ham o‘tadi, chunki oralarida vaqt bor. Baza buni biz uchun hal qila oladi — unikal indeks atomar.
async function charge(req: ChargeRequest) {
const hash = sha256(canonicalJson(req.body));
// Kalitni band qilishga urinamiz. Agar u band bo'lsa — bu takror.
const claimed = await db.insertIgnoreConflict('payment_request', {
idempotency_key: req.key,
account_id: req.accountId,
request_hash: hash,
status: 'in_progress',
});
if (!claimed) {
const prev = await db.get('payment_request', req.key);
// Bir xil kalit, boshqa tana — mijoz xato qildi yoki summani
// almashtirmoqchi.
if (prev.request_hash !== hash) throw new Conflict('key_reused');
// Birinchi so'rov hali ishda: keyinroq takrorlashni so'raymiz,
// lekin ikkinchi marta o'tkazmaymiz.
if (prev.status === 'in_progress') throw new Retry('in_progress');
// Tugagan so'rovning takrori — YANGI emas, O'SHA javobni qaytaramiz.
return prev.response;
}
const result = await db.transaction(async (tx) => {
const op = await tx.insert('operation', {
account_id: req.accountId,
amount: -req.body.amount,
kind: 'charge',
});
// Provayderga murojaat BU YERDA emas: tranzaksiya ichidagi tarmoq
// chaqiruvi — bu pul allaqachon ketgan holda bazada ortga qaytishni
// olish usuli. Vazifani o'sha tranzaksiya bilan chiquvchi navbatga
// qo'yamiz.
await tx.insert('outbox', { type: 'provider.charge', operation_id: op.id });
return { operationId: op.id };
});
await db.update('payment_request', req.key, {
status: 'succeeded',
response: result,
operation_id: result.operationId,
});
return result;
}onConflict bo‘lgan satr: poygani bizning kodimiz emas, baza hal qiladi.Solishtirish: siz ko‘zda tutmagan xatolarni topadigan narsa
Idempotentlik dublikatlardan himoya qiladi, lekin farqlardan emas: provayder «qabul qilindi» deb javob bergandan keyin amalni rad etgan bo‘lishi mumkin, komissiya alohida satr bilan kelgan bo‘lishi mumkin, qaytarish sizning tizimingizdan tashqarida o‘tgan bo‘lishi mumkin. Bu haqda bilishning yagona yo‘li — o‘z jurnalingizni provayder ko‘chirmasi bilan har kuni taqqoslash.
- 01
Sutkalik ko‘chirmani olamiz
Provayder qanday bersa, shundayligicha, va har qanday ishlovdan oldin xom holda saqlaymiz. Solishtirish natijasi savol tug‘dirganda asl nusxa kerak bo‘ladi.
- 02
Amal identifikatori bo‘yicha moslashtiramiz
Summa va vaqt bo‘yicha emas: summalarning mos kelishi amallarning mos kelishi emas. Provayder identifikatori sizning amalingiz yonida boshidanoq saqlanishi kerak.
- 03
Farqlarni turlarga ajratamiz
Bizda bor, ularda yo‘q. Ularda bor, bizda yo‘q. Ikkalasida ham bor, lekin summalar boshqa. Uchinchi tur — eng yoqimsizi va eng ma’lumot beruvchisi.
- 04
Xat emas, vazifa ochamiz
Mas’ul va muddatsiz farq bir haftadan keyin o‘qilmay qo‘yiladigan kundalik bildirishnomaga aylanadi.
Idempotentlik nima bermaydi
U «kamida bir marta» yetkazishni «roppa-rosa bir marta» ga aylantirmaydi — taqsimlangan tizimda bu imkonsiz. U qayta ishlashni xavfsiz qiladi, bu esa boshqa va’da: tizim so‘rovni baribir ikki marta oladi, shunchaki ikkinchi marta hech nima sodir bo‘lmaydi. Pul atrofida quradigan hamma narsangiz har qanday xabar bir martadan ko‘p kelishidan kelib chiqishi kerak.
Ko‘p so‘raladigan savollar
So‘rovlar jadvalisiz unikal indeksning o‘zi yetarlimi?
Unikal indeks ikkinchi amaldan himoya qiladi, lekin mijoz birinchisining natijasi o‘rniga xato oladi — va «allaqachon o‘tdi» ni «o‘tmadi» dan ajrata olmaydi. So‘rovlar jadvalining ma’nosi faqat himoyada emas, takroriy so‘rovga birinchisi olgan javobning aynan o‘zini qaytarishda ham.
Idempotentlik kalitlarini qancha saqlash kerak?
Bir sutka amalda har qanday takrorni qoplaydi: undan uzoqroq mijoz takrorlamaydi. Muddatdan oshgan yozuvlar jadval bo‘yicha o‘chiriladi, aks holda jadval abadiy o‘sadi. Amalning o‘zi esa hisob talab qilgancha saqlanadi — bu turli jadvallar uchun turli muddatlar.
Birinchi so‘rov hali bajarilayotgan bo‘lsa nima qaytarish kerak?
409 kodi, tushunarli tana va keyinroq takrorlash ko‘rsatmasi bilan. 200 qaytarib bo‘lmaydi: mijoz amal o‘tdi deb hisoblaydi va foydalanuvchiga hali sodir bo‘lmagan muvaffaqiyatni ko‘rsatadi. Ishlov beruvchi ichida tugashini kutish ham kerak emas — shunday qilib bloklangan ulanishlar navbatini olasiz.
Bu faqat to‘lovlarda keraksmi?
Yo‘q. Tashqi ta’sirga ega har qanday harakat — xat yuborish, bonus hisoblash, buyurtma yaratish, hujjat chop etish — xuddi shu usuldan yutadi. To‘lovlarda xato narxi shunchaki eng yuqori, shuning uchun u yerda bu haqda birinchi bo‘lib o‘ylashadi.
Manbalar
Maqoladagi da’volarni tekshirish mumkin: quyida qayta hikoya emas, birlamchi manbalar.
- 01RFC 9110 — HTTP Semantics, idempotent metodlar — nega POST takrorlar haqida alohida kelishuvni talab qiladi
- 02Stripe API — Idempotent requests — kalit yashash muddati va kalit mos, tana farqli bo‘lgandagi xatti-harakat
- 03Transactional outbox pattern — provayderga tranzaksiya ichidan murojaat qilmaslik uchun
- 04PostgreSQL — Transaction Isolation — nega «tekshirib qo‘shish» atomar amal emas
Maqolalarni loyihalarda ishlayotgan muhandislar yozadi — lekin ism bilan imzolanmaydi. Sabab saytda mijoz logotiplari yo‘qligi bilan bir xil: deyarli barcha loyihalar NDA yoki white-label ostida boradi, va to‘lov yadrosi haqidagi maqola muallifi mijozni logotipdan kam bo‘lmagan darajada ko‘rsatadi. Ism o‘rniga matn uchun qoidalar bilan javob beramiz — bundan mustasno: o‘z ochiq kodimiz haqidagi maqolalar muallif ismi bilan imzolanadi.
Qanday yozamiz va nimani tekshiramiz