Asosiy kontentga o‘tish
Innovialabs

To‘lovlar va balanslar uchun ma’lumotlar bazasini loyihalash: ledger

Chuqur4 daq

Qisqacha

Har bir pul harakatini summasi nolga teng provodkalarga ega o‘zgarmas tranzaksiya sifatida yozing, summalarni tiyinda butun son ko‘rinishida saqlang, provayder callback’larini unikal kalit orqali idempotent qiling va yechib olishdan oldin hisob qatorini bloklang.

Qisqa javob

Ishonchli to‘lov ledger’i beshta qoidaga tayanadi:

  1. Ikki tomonlama yozuv: har bir operatsiya pulni kamida ikki hisob o‘rtasida o‘tkazadi va uning provodkalari yig‘indisi nolga teng.
  2. O‘zgarmaslik: yozuvlar hech qachon yangilanmaydi va o‘chirilmaydi; xatolar yangi storno tranzaksiyasi bilan tuzatiladi.
  3. Faqat butun sonlar: summalar eng kichik birlikda — tiyin yoki sentda — bigint, hech qachon float emas.
  4. Idempotentlik: Payme, Click yoki Uzum’dan kelgan takroriy callback ikkinchi to‘lovni yarata olmaydi.
  5. Bloklash: ikkita parallel xarid bitta balansni ikkalasi ham sarflay olmaydi.

Oshirib-kamaytiriladigan oddiy users.balance ustuni bu qoidalarning birortasini ham bajarmaydi: raqam qayerdan kelganini tushuntirib ham, to‘g‘riligini isbotlab ham bo‘lmaydi.

Sxema

CREATE TABLE accounts (
  id          bigserial PRIMARY KEY,
  owner_id    bigint,
  kind        text   NOT NULL,              -- user_wallet, provider_clearing, revenue
  currency    char(3) NOT NULL DEFAULT 'UZS',
  balance     bigint NOT NULL DEFAULT 0,    -- kesh, tiyinda
  allow_negative boolean NOT NULL DEFAULT false
);

CREATE TABLE ledger_transactions (
  id              bigserial PRIMARY KEY,
  idempotency_key text NOT NULL UNIQUE,
  kind            text NOT NULL,            -- topup, purchase, refund, reversal
  created_at      timestamptz NOT NULL DEFAULT now()
);

CREATE TABLE ledger_entries (
  id             bigserial PRIMARY KEY,
  transaction_id bigint NOT NULL REFERENCES ledger_transactions(id),
  account_id     bigint NOT NULL REFERENCES accounts(id),
  amount         bigint NOT NULL CHECK (amount <> 0)  -- + oshiradi, - kamaytiradi
);

Bu yerda soddalashtirilgan ishora qoidasi ishlatilgan: musbat summa hisob balansini oshiradi, manfiy summa kamaytiradi. Har bir tranzaksiyada SUM(amount) = 0 — buni provodkalarni yozayotgan o‘sha baza tranzaksiyasida yoki kechiktirilgan trigger-cheklov bilan tekshiring.

Provayder orqali 50 000 so‘mlik to‘ldirish ikkita provodkaga aylanadi: provider_clearing hisobida −5 000 000 va foydalanuvchi hamyonida +5 000 000. Pul yo‘qdan paydo bo‘lmadi: kliring hisobi hisob-kitob vaqtigacha provayder sizga qancha qarzdorligini ko‘rsatadi. Shuning uchun bunday tizim hisoblarida allow_negative = true, foydalanuvchi hamyonlarida esa yo‘q.

Pul butun son sifatida

  • float ko‘plab o‘nli kasrlarni aniq ifodalay olmaydi va xatolar yig‘indilarda to‘planadi.
  • numeric aniq, lekin sekinroq va tiyin bo‘laklarini saqlashga undaydi.
  • Tiyindagi bigint aniq, tez va oddiy. So‘mga faqat ko‘rsatishda formatlang.

Provayderlar summalarni turli birliklarda yuborishi mumkin: ba’zilari tiyinda, ba’zilari kasr qismli so‘mda. Har bir provayder hujjatlarini tekshiring va bir marta — integratsiya chegarasida konvertatsiya qiling.

O‘zgarmas yozuvlar

  • Ilova roli uchun ledger jadvallarida UPDATE va DELETE huquqlarini olib qo‘ying yoki ularni trigger bilan bloklang.
  • Qaytarish — asl tranzaksiyaga havola qiluvchi, ko‘zgu provodkalarga ega yangi tranzaksiya.
  • Statuslar (kutilmoqda, o‘tkazildi, bekor qilindi) provodkalarga emas, to‘lov obyektiga tegishli. Ledger’ga faqat pul bilan haqiqatan sodir bo‘lgan narsa yoziladi.

Idempotent callback’lar

To‘lov provayderlari taymaut va tarmoq xatolarida so‘rovlarni takrorlaydi, bitta operatsiya bir necha marta kelishi mumkin. Kalitni provayder va uning tranzaksiya id’sidan tuzing:

INSERT INTO ledger_transactions (idempotency_key, kind)
VALUES ('payme:6720f1c2a9', 'topup')
ON CONFLICT (idempotency_key) DO NOTHING
RETURNING id;

Agar hech narsa qaytmasa, operatsiya allaqachon qayta ishlangan: uni toping va provayderga birinchi martadagi aynan o‘sha javobni yuboring. Har bir provayder protokolining o‘z bosqichlari bor (tekshirish, yaratish, o‘tkazish, bekor qilish) va har bir bosqich takrorlanishga xavfsiz bardosh berishi kerak.

Ikki marta yechib olishga qarshi bloklash

BEGIN;
SELECT balance FROM accounts WHERE id = 42 FOR UPDATE;
-- agar balance < 1500000 bo‘lsa: ROLLBACK va "mablag‘ yetarli emas" javobi
INSERT INTO ledger_transactions (idempotency_key, kind)
VALUES ('order:981', 'purchase') RETURNING id;          -- :tx
INSERT INTO ledger_entries (transaction_id, account_id, amount)
VALUES (:tx, 42, -1500000), (:tx, 7, 1500000);
UPDATE accounts SET balance = balance - 1500000 WHERE id = 42;
UPDATE accounts SET balance = balance + 1500000 WHERE id = 7;
COMMIT;
  • FOR UPDATE ikkinchi parallel xaridni birinchisi tugaguncha kutishga va keyin yangi balansni ko‘rishga majbur qiladi.
  • Deadlock’larning oldini olish uchun bir nechta hisobni qat’iy tartibda (masalan, id bo‘yicha) bloklang.
  • Daromad kabi tizim hisobi issiq qatorga aylanadi. Variantlar: uning balansini keshlamaslik va provodkalardan hisoblash yoki uni bir nechta subhisobga bo‘lish.
  • Muqobil yo‘l — SERIALIZABLE izolyatsiya darajasi va serializatsiya xatolarida qayta urinish.

Balanslar va solishtirish

Keshlangan balance — qulaylik, haqiqat manbai esa provodkalar yig‘indisi. Ularning mos kelishini muntazam tekshiring:

SELECT a.id, a.balance, COALESCE(SUM(e.amount), 0) AS from_entries
FROM accounts a LEFT JOIN ledger_entries e ON e.account_id = a.id
GROUP BY a.id HAVING a.balance <> COALESCE(SUM(e.amount), 0);

Hajm katta bo‘lsa, balanslarning davriy suratlarini saqlang va faqat yangiroq provodkalarni qo‘shing.

Solishtirish (reconciliation) ledger’ingizni provayder hisobotlari bilan taqqoslaydi: provayderning har bir tranzaksiyasiga ledger’da xuddi shu summali aynan bitta tranzaksiya mos kelishi kerak. Farqlarni “ledger’da yo‘q”, “provayderda yo‘q” va “summa mos emas” turlariga ajrating va ularni faqat yangi tuzatuvchi tranzaksiyalar bilan bartaraf eting.

Ko‘p beriladigan savollar

Prototip uchun — ha. Lekin provodkalarsiz pul qayerdan kelganiga javob bera olmaysiz, nizoni ko‘rib chiqa olmaysiz va xatoni topa olmaysiz. Ledger’ni keyinroq qo‘shish sizda yo‘q tarixni ko‘chirishni anglatadi.

Bunda yordam beramiz

O‘zingiz shug‘ullanmasdan natija olishni istasangiz — biz har kuni shu bilan shug‘ullanamiz.

Loyihani muhokama qilish

Yana o‘qing

G‘oya yoki vazifangiz bormi? Bizga aytib bering

Ish kuni davomida javob beramiz va yechim taklif qilamiz. Konsultatsiya bepul.