it-swarm-eu.dev

Napsání jednoduchého bankovního schématu: Jak mám udržovat zůstatky v synchronizaci s jejich historií transakcí?

Píšu schéma pro jednoduchou bankovní databázi. Zde jsou základní specifikace:

  • Databáze bude ukládat transakce proti uživateli a měně.
  • Každý uživatel má jeden zůstatek na měnu, takže každý zůstatek je jednoduše součtem všech transakcí proti danému uživateli a měně.
  • Rovnováha nemůže být záporná.

Bankovní aplikace bude komunikovat se svou databází výhradně prostřednictvím uložených procedur.

Očekávám, že tato databáze bude přijímat stovky tisíc nových transakcí denně, stejně jako dotazy na zůstatek vyššího řádu. K tomu, abychom velmi rychle obsluhovali zůstatky, musím je předem agregovat. Zároveň musím zaručit, že zůstatek nikdy neodporuje jeho historii transakcí.

Moje možnosti jsou:

  1. Mít samostatnou tabulku balances a proveďte jednu z následujících akcí:

    1. Použijte transakce na obě tabulky transactions a balances. Pomocí logiky TRANSACTION ve vrstvě uložené procedury zajistěte, aby zůstatky a transakce byly vždy synchronizované. (Podporováno Jack .)

    2. Použijte transakce na tabulku transactions a mít spouštěč, který pro mě aktualizuje tabulku balances o částku transakce.

    3. Použít transakce do tabulky balances a mít spouštěč, který přidá novou položku do tabulky transactions s částkou transakce.

    Musím se spolehnout na bezpečnostní přístupy, abych zajistil, že mimo uložené procedury nebudou provedeny žádné změny. Jinak by například nějaký proces mohl přímo vložit transakci do tabulky transactions a pod schéma 1.3 odpovídající zůstatek by nebyl synchronizován.

  2. Mít indexované zobrazení balances, které příslušně agreguje transakce. Zůstatky jsou garantovány úložným strojem, aby zůstaly synchronizovány s jejich transakcemi, takže se nemusím spoléhat na bezpečnostní přístupy, abych to zaručil. Na druhou stranu už nemůžu vynutit zůstatky, které by byly nezáporné, protože pohledy - dokonce ani indexované pohledy - nemohou mít CHECK omezení. (Podporováno Denny .)

  3. Máte pouze tabulku transactions, ale s dalším sloupcem pro uložení zůstatku platného ihned po provedení této transakce. Nejnovější záznam transakcí pro uživatele a měnu tedy také obsahuje jejich aktuální zůstatek. (Navrhováno níže Andrew ; varianta navržená garik .)

Když jsem poprvé vyřešil tento problém, přečetl jsem tytodvě diskuze a rozhodl jsem se pro možnost 2. Pro orientaci můžete vidět implementaci holých kostí zde .

  • Navrhli jste nebo spravovali databázi, jako je tato, s profilem vysokého zatížení? Jaké bylo vaše řešení tohoto problému?

  • Myslíte si, že jsem udělal správnou volbu designu? Měl bych mít na paměti něco?

    Například vím, že změny schématu v tabulce transactions vyžadují, abych znovu vytvořil pohled balances. I když archivuji transakce, abych udržel databázi malou (např. Přesunutím někam jinam a jejich nahrazením souhrnnými transakcemi), bude muset přestavět zobrazení desítek milionů transakcí s každou aktualizací schématu pravděpodobně znamenat podstatně více prostojů na nasazení.

  • Pokud je indexované zobrazení cestou, jak mohu zaručit, že žádný zůstatek nebude záporný?


Archivace transakcí:

Dovolte mi podrobněji se zabývat archivačními transakcemi a „souhrnnými transakcemi“, které jsem zmínil výše. Zaprvé, pravidelná archivace bude v takovém systému s vysokou zátěží nezbytností. Chci zachovat soulad mezi zůstatky a jejich historií transakcí a zároveň umožnit přesun starých transakcí někam jinam. Za tímto účelem nahradím každou dávku archivovaných transakcí shrnutím jejich částek na uživatele a měnu.

Například tento seznam transakcí:

user_id    currency_id      amount    is_summary
------------------------------------------------
      3              1       10.60             0
      3              1      -55.00             0
      3              1      -12.12             0

je archivován a nahrazen tímto:

user_id    currency_id      amount    is_summary
------------------------------------------------
      3              1      -56.52             1

Tímto způsobem udržuje zůstatek s archivovanými transakcemi kompletní a konzistentní historii transakcí.

60
Nick Chammas

Nejsem obeznámen s účetnictvím, ale vyřešil jsem některé podobné problémy v prostředích typu inventáře. Do transakce ukládám běžící součty ve stejném řádku. Používám omezení, takže moje data nejsou nikdy špatná ani za vysoké souběžnosti. Poté jsem v roce 2009 napsal následující řešení: :

Výpočet běžících součtů je notoricky pomalý, ať už to děláte kurzorem nebo trojúhelníkovým spojením. Je velmi lákavé denormalizovat, ukládat běžící součty do sloupce, zejména pokud jej často vyberete. Nicméně, jako obvykle, když denormalizujete, musíte zaručit integritu vašich denormalizovaných dat. Naštěstí můžete zaručit integritu běžících součtů s omezeními - pokud jsou všechna vaše omezení důvěryhodná, jsou všechny vaše součty správné. Tímto způsobem můžete snadno zajistit, aby aktuální zůstatek (průběžné součty) nebyl nikdy záporný - vymáhání jinými metodami může být také velmi pomalé. Následující skript demonstruje techniku.

CREATE TABLE Data.Inventory(InventoryID INT NOT NULL IDENTITY,
  ItemID INT NOT NULL,
  ChangeDate DATETIME NOT NULL,
  ChangeQty INT NOT NULL,
  TotalQty INT NOT NULL,
  PreviousChangeDate DATETIME NULL,
  PreviousTotalQty INT NULL,
  CONSTRAINT PK_Inventory PRIMARY KEY(ItemID, ChangeDate),
  CONSTRAINT UNQ_Inventory UNIQUE(ItemID, ChangeDate, TotalQty),
  CONSTRAINT UNQ_Inventory_Previous_Columns 
     UNIQUE(ItemID, PreviousChangeDate, PreviousTotalQty),
  CONSTRAINT FK_Inventory_Self FOREIGN KEY(ItemID, PreviousChangeDate, PreviousTotalQty)
    REFERENCES Data.Inventory(ItemID, ChangeDate, TotalQty),
  CONSTRAINT CHK_Inventory_Valid_TotalQty CHECK(
         TotalQty >= 0 
     AND (TotalQty = COALESCE(PreviousTotalQty, 0) + ChangeQty)
  ),
  CONSTRAINT CHK_Inventory_Valid_Dates_Sequence CHECK(PreviousChangeDate < ChangeDate),
  CONSTRAINT CHK_Inventory_Valid_Previous_Columns CHECK(
        (PreviousChangeDate IS NULL AND PreviousTotalQty IS NULL)
     OR (PreviousChangeDate IS NOT NULL AND PreviousTotalQty IS NOT NULL)
  )
);

-- beginning of inventory for item 1
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
VALUES(1, '20090101', 10, 10, NULL, NULL);

-- cannot begin the inventory for the second time for the same item 1
INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)
VALUES(1, '20090102', 10, 10, NULL, NULL);


Msg 2627, Level 14, State 1, Line 10

Violation of UNIQUE KEY constraint 'UNQ_Inventory_Previous_Columns'. 
Cannot insert duplicate key in object 'Data.Inventory'.

The statement has been terminated.


-- add more
DECLARE @ChangeQty INT;
SET @ChangeQty = 5;

INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)

SELECT TOP 1 ItemID, '20090103', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

SET @ChangeQty = 3;

INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)

SELECT TOP 1 ItemID, '20090104', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

SET @ChangeQty = -4;

INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)

SELECT TOP 1 ItemID, '20090105', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

-- try to violate chronological order
SET @ChangeQty = 5;

INSERT INTO Data.Inventory(ItemID,
  ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty)

SELECT TOP 1 ItemID, '20081231', @ChangeQty, TotalQty + @ChangeQty, ChangeDate, TotalQty
  FROM Data.Inventory
  WHERE ItemID = 1
  ORDER BY ChangeDate DESC;

Msg 547, Level 16, State 0, Line 4

The INSERT statement conflicted with the CHECK constraint 
"CHK_Inventory_Valid_Dates_Sequence". 
The conflict occurred in database "Test", table "Data.Inventory".

The statement has been terminated.

SELECT ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty
FROM Data.Inventory ORDER BY ChangeDate;

ChangeDate              ChangeQty   TotalQty    PreviousChangeDate      PreviousTotalQty
----------------------- ----------- ----------- ----------------------- -----
2009-01-01 00:00:00.000 10          10          NULL                    NULL
2009-01-03 00:00:00.000 5           15          2009-01-01 00:00:00.000 10
2009-01-04 00:00:00.000 3           18          2009-01-03 00:00:00.000 15
2009-01-05 00:00:00.000 -4          14          2009-01-04 00:00:00.000 18


-- try to change a single row, all updates must fail
UPDATE Data.Inventory SET ChangeQty = ChangeQty + 2 WHERE InventoryID = 3;
UPDATE Data.Inventory SET TotalQty = TotalQty + 2 WHERE InventoryID = 3;

-- try to delete not the last row, all deletes must fail
DELETE FROM Data.Inventory WHERE InventoryID = 1;
DELETE FROM Data.Inventory WHERE InventoryID = 3;

-- the right way to update
DECLARE @IncreaseQty INT;

SET @IncreaseQty = 2;

UPDATE Data.Inventory 
SET 
     ChangeQty = ChangeQty 
   + CASE 
        WHEN ItemID = 1 AND ChangeDate = '20090103' 
        THEN @IncreaseQty 
        ELSE 0 
     END,
  TotalQty = TotalQty + @IncreaseQty,
  PreviousTotalQty = PreviousTotalQty + 
     CASE 
        WHEN ItemID = 1 AND ChangeDate = '20090103' 
        THEN 0 
        ELSE @IncreaseQty 
     END
WHERE ItemID = 1 AND ChangeDate >= '20090103';

SELECT ChangeDate,
  ChangeQty,
  TotalQty,
  PreviousChangeDate,
  PreviousTotalQty
FROM Data.Inventory ORDER BY ChangeDate;

ChangeDate              ChangeQty   TotalQty    PreviousChangeDate      PreviousTotalQty
----------------------- ----------- ----------- ----------------------- ----------------
2009-01-01 00:00:00.000 10          10          NULL                    NULL
2009-01-03 00:00:00.000 7           17          2009-01-01 00:00:00.000 10
2009-01-04 00:00:00.000 3           20          2009-01-03 00:00:00.000 17
2009-01-05 00:00:00.000 -4          16          2009-01-04 00:00:00.000 20
17
A-K

Trochu odlišný přístup (podobný vaší druhé ) možnosti, kterou je třeba zvážit, je mít pouze tabulku transakcí s definicí:

CREATE TABLE Transaction (
      UserID              INT
    , CurrencyID          INT 
    , TransactionDate     DATETIME  
    , OpeningBalance      MONEY
    , TransactionAmount   MONEY
);

Můžete také chtít ID transakce/objednávku, abyste mohli zpracovat dvě transakce se stejným datem a zlepšit vyhledávací dotaz.

Chcete-li získat aktuální zůstatek, stačí poslední záznam.

Metody získání posledního záznam :

/* For a single User/Currency */
Select TOP 1 *
FROM dbo.Transaction
WHERE UserID = 3 and CurrencyID = 1
ORDER By TransactionDate desc

/* For multiple records ie: to put into a view (which you might want to index) */
SELECT
    C.*
FROM
    (SELECT 
        *, 
        ROW_NUMBER() OVER (
           PARTITION BY UserID, CurrencyID 
           ORDER BY TransactionDate DESC
        ) AS rnBalance 
    FROM Transaction) C
WHERE
    C.rnBalance = 1
ORDER BY
    C.UserID, C.CurrencyID

Nevýhody:

  • Při vkládání transakce mimo sled (tj. K opravě problému/nesprávného počátečního zůstatku) může být nutné provést kaskádové aktualizace pro všechny následující transakce.
  • Transakce pro uživatele/měnu by musely být serializovány, aby byla zachována přesná rovnováha.

    -- Example of getting the current balance and locking the 
    -- last record for that User/Currency.
    -- This lock will be freed after the Stored Procedure completes.
    SELECT TOP 1 @OldBalance = OpeningBalance + TransactionAmount  
    FROM dbo.Transaction with (rowlock, xlock)   
    WHERE UserID = 3 and CurrencyID = 1  
    ORDER By TransactionDate DESC;
    

Pros:

  • Už nemusíte udržovat dvě oddělené tabulky ...
  • Můžete snadno ověřit zůstatek a když se zůstatek stane synchronizovaným, můžete přesně identifikovat, kdy se dostal z rána, jakmile se historie transakcí stane dokumentující.

Úpravy: Některé ukázkové dotazy týkající se získání aktuálního zůstatku a zvýraznění kon (díky @ Jack Douglas)

15
Andrew Bickerton

Nepovolit zákazníkům, aby měli zůstatek nižší než 0, je obchodní pravidlo (což by se rychle změnilo, protože poplatky za věci jako nad návrhem jsou to, jak banky vydělávají většinu svých peněz). To budete chtít zpracovat při zpracování aplikace, když budou řádky vloženy do historie transakcí. Obzvláště, jak můžete skončit s tím, že někteří zákazníci mají ochranu proti přečerpání a někteří dostávají účtované poplatky a někteří neumožňují zadávání záporných částek.

Doposud se mi líbí, kam s tím chodíte, ale pokud jde o skutečný projekt (ne školu), je třeba do obchodních pravidel vložit spoustu myšlenek atd. Jakmile máte bankovní systém nahoru a provoz není příliš prostor pro redesign, protože existují velmi specifické zákony o lidech, kteří mají přístup k jejich penězům.

14
mrdenny

Po přečtení těchto dvou diskusí jsem se rozhodl pro možnost 2

Po přečtení těchto diskusí si také nejsem jistý, proč jste se rozhodli pro [~ # ~] dri [~ # ~] řešení nad nejrozumnější z dalších možností, které nastíníte:

Použijte transakce na tabulky transakcí i zůstatků. Pomocí logiky TRANSAKCE ve vrstvě uložené procedury zajistěte, aby zůstatky a transakce byly vždy synchronizované.

Tento druh řešení má obrovské praktické výhody, pokud máte luxus omezující přístup k datům prostřednictvím transakčního rozhraní API vše. Ztratíte velmi důležitou výhodu DRI, která spočívá v tom, že integrita je zaručena databází, ale v každém modelu dostatečné složitosti budou existovat některá obchodní pravidla, která nelze pomocí DRI vymáhat .

Doporučuji používat DRI tam, kde je to možné, k prosazování obchodních pravidel, aniž byste příliš ohýbali váš model, aby to bylo možné:

I když archivuji transakce (např. Jejich přesunutím někam jinam a jejich nahrazením souhrnnými transakcemi)

Jakmile začnete uvažovat o znečišťování svého modelu, myslím, že se pohybujete do oblasti, kde je přínos DRI vyvážen obtížemi, které zavádíte. Zvažte například, že chyba ve vašem procesu archivace by teoreticky mohla způsobit vaše zlaté pravidlo (že zůstatky vždy se rovná součtu transakcí) s ticho s řešením DRI .

Zde je shrnutí výhod transakčního přístupu, jak je vidím:

  • Měli bychom to dělat stejně, pokud je to možné. Ať už si pro tento konkrétní problém vyberete jakékoli řešení, získáte větší flexibilitu při návrhu a kontrolu nad svými daty. Veškerý přístup se pak stává „transakčním“ z hlediska obchodní logiky, nikoli pouze z hlediska logiky databáze.
  • Svůj model můžete udržovat čistý
  • Můžete „prosadit“ mnohem širší škálu a složitost obchodních pravidel (s tím, že pojem „prosadit“ je volnější než s DRI)
  • Stále můžete použít DRI, kdekoli je to praktické, a poskytnout modelu robustnější základní integritu - a to může fungovat jako kontrola vaší transakční logiky
  • Většina problémů s výkonem, které vás trápí, se rozplyne
  • Zavedení nových požadavků může být mnohem snazší - například: složitá pravidla pro sporné transakce by vás mohla donutit od čistého přístupu DRI dále dolů, což znamená spoustu zbytečného úsilí
  • Rozdělení nebo archivace historických dat se stává mnohem méně riskantní a bolestivou

--Upravit

Chcete-li umožnit archivaci bez přidání složitosti nebo rizika, můžete zvolit, aby se souhrny řádků ponechaly v samostatné souhrnné tabulce, generované nepřetržitě (výpůjčky od @Andrew a @Garik)

Například pokud jsou souhrny měsíční:

  • pokaždé, když dojde k transakci (prostřednictvím vašeho API), je k dispozici odpovídající aktualizace nebo vložení do souhrnné tabulky
  • souhrnná tabulka je nikdy archivována, ale archivace transakcí je stejně jednoduchá jako smazání (nebo přetažení oddílu?)
  • každý řádek v souhrnné tabulce obsahuje „počáteční zůstatek“ a „částku“
  • na souhrnnou tabulku lze použít omezení kontroly jako „počáteční zůstatek“ + „částka“> 0 a „počáteční zůstatek“> 0
  • souhrnné řádky lze vložit do měsíční dávky, aby se usnadnilo uzamčení posledního souhrnného řádku (vždy by existoval řádek pro aktuální měsíc)

Nick.

Hlavní myšlenkou je ukládání záznamů zůstatků a transakcí do stejné tabulky. Historicky se to stalo. Takže v tomto případě můžeme získat rovnováhu pouhým vyhledáním posledního souhrnného záznamu.

 id   user_id    currency_id      amount    is_summary (or record_type)
----------------------------------------------------
  1       3              1       10.60             0
  2       3              1       10.60             1    -- summary after transaction 1
  3       3              1      -55.00             0
  4       3              1      -44.40             1    -- summary after transactions 1 and 3
  5       3              1      -12.12             0
  6       3              1      -56.52             1    -- summary after transactions 1, 3 and 5 

Lepší variantou je snížení počtu souhrnných záznamů. Můžeme mít jeden záznam zůstatku na konci (nebo začátku) dne. Jak víte, každá banka má operational day otevřít a poté zavřít provést souhrnné operace pro tento den. To nám umožňuje snadno spočítat úrok pomocí každodenního záznamu zůstatku, například:

user_id    currency_id      amount    is_summary    oper_date
--------------------------------------------------------------
      3              1       10.60             0    01/01/2011 
      3              1      -55.00             0    01/01/2011
      3              1      -44.40             1    01/01/2011 -- summary at the end of day (01/01/2011)
      3              1      -12.12             0    01/02/2011
      3              1      -56.52             1    01/02/2011 -- summary at the end of day (01/02/2011)

Štěstí.

6
garik

Na základě vašich požadavků by se volba 1 jevila jako nejlepší. I když bych měl svůj návrh, aby umožnil pouze vložení do tabulky transakcí. A mít spouštěč v tabulce transakcí, aktualizovat tabulku rovnováhy v reálném čase. K řízení přístupu k těmto tabulkám můžete použít oprávnění databáze.

V tomto přístupu je zaručeno, že zůstatek v reálném čase bude synchronizován s tabulkou transakcí. A nezáleží na tom, zda jsou použity uložené procedury nebo psql nebo jdbc. V případě potřeby můžete nechat zkontrolovat záporný zůstatek. Výkon nebude problém. Chcete-li získat rovnováhu v reálném čase, jedná se o singletonový dotaz.

Archivace tento přístup neovlivní. Můžete mít týdenní, měsíční, roční souhrnnou tabulku také v případě potřeby pro věci, jako jsou zprávy.

4
Elan Fisoc

V Oracle to můžete udělat jen pomocí tabulky transakcí s rychlým obnovitelným materializovaným pohledem, který provede agregaci pro vytvoření zůstatku. Spoušť definujete v materializovaném pohledu. Pokud je materializované zobrazení definováno jako „ON COMMIT“, efektivně zabraňuje přidávání/úpravám dat v základních tabulkách. Spoušť detekuje platná data [in] a vyvolává výjimku, pokud transakci stornuje. Hezký příklad je zde http://www.sqlsnippets.com/en/topic-12896.html

Nevím sqlserver, ale možná má podobnou možnost?

3
ik_zelf