it-swarm.asia

Menulis skema bank sederhana: Bagaimana saya harus menjaga keseimbangan saldo saya dengan riwayat transaksi mereka?

Saya menulis skema untuk database bank sederhana. Berikut spesifikasi dasar:

  • Basis data akan menyimpan transaksi terhadap pengguna dan mata uang.
  • Setiap pengguna memiliki satu saldo per mata uang, sehingga setiap saldo hanyalah jumlah dari semua transaksi terhadap pengguna dan mata uang tertentu.
  • Saldo tidak boleh negatif.

Aplikasi bank akan berkomunikasi dengan database secara eksklusif melalui prosedur tersimpan.

Saya berharap database ini menerima ratusan ribu transaksi baru per hari, serta menyeimbangkan kueri pada tingkat yang lebih tinggi. Untuk menayangkan saldo dengan sangat cepat, saya perlu melakukan pra-agregat. Pada saat yang sama, saya perlu menjamin bahwa saldo tidak pernah bertentangan dengan riwayat transaksi.

Pilihan saya adalah:

  1. Miliki tabel balances yang terpisah dan lakukan salah satu dari yang berikut:

    1. Terapkan transaksi ke tabel transactions dan balances. Gunakan logika TRANSACTION di lapisan prosedur tersimpan saya untuk memastikan bahwa saldo dan transaksi selalu sinkron. (Didukung oleh Jack .)

    2. Terapkan transaksi ke tabel transactions dan miliki pemicu yang memperbarui tabel balances untuk saya dengan jumlah transaksi.

    3. Terapkan transaksi ke tabel balances dan memiliki pemicu yang menambahkan entri baru di tabel transactions untuk saya dengan jumlah transaksi.

    Saya harus mengandalkan pendekatan berbasis keamanan untuk memastikan tidak ada perubahan yang dapat dilakukan di luar prosedur yang tersimpan. Kalau tidak, misalnya, beberapa proses dapat secara langsung memasukkan transaksi ke dalam tabel transactions dan di bawah skema 1.3 saldo yang relevan tidak sinkron.

  2. Miliki balances tampilan yang diindeks yang mengagregasi transaksi dengan tepat. Saldo dijamin oleh mesin penyimpanan untuk tetap selaras dengan transaksi mereka, jadi saya tidak perlu bergantung pada pendekatan berbasis keamanan untuk menjamin ini. Di sisi lain, saya tidak bisa memaksakan saldo menjadi non-negatif lagi karena pandangan - bahkan tampilan yang diindeks - tidak dapat memiliki batasan CHECK. (Didukung oleh Denny .)

  3. Hanya memiliki tabel transactions tetapi dengan kolom tambahan untuk menyimpan saldo efektif tepat setelah transaksi itu dijalankan. Dengan demikian, catatan transaksi terbaru untuk pengguna dan mata uang juga mengandung saldo mereka saat ini. (Disarankan di bawah oleh Andrew ; varian yang diajukan oleh garik .)

Ketika saya pertama kali mengatasi masalah ini, saya membaca inidua diskusi dan memutuskan opsi 2. Untuk referensi, Anda dapat melihat implementasi telanjang itu di sini .

  • Sudahkah Anda merancang atau mengelola basis data seperti ini dengan profil beban tinggi? Apa solusi Anda untuk masalah ini?

  • Apakah Anda pikir saya sudah membuat pilihan desain yang tepat? Apakah ada sesuatu yang harus saya ingat?

    Sebagai contoh, saya tahu perubahan skema pada tabel transactions akan mengharuskan saya membangun kembali tampilan balances. Bahkan jika saya mengarsipkan transaksi untuk menjaga basis data kecil (mis. Dengan memindahkannya ke tempat lain dan menggantinya dengan transaksi ringkasan), harus membangun kembali pandangan dari puluhan juta transaksi dengan setiap pembaruan skema mungkin akan berarti lebih banyak downtime per penyebaran.

  • Jika tampilan yang diindeks adalah cara untuk pergi, bagaimana saya bisa menjamin bahwa tidak ada saldo negatif?


Pengarsipan transaksi:

Izinkan saya menguraikan sedikit tentang pengarsipan transaksi dan "ringkasan transaksi" yang saya sebutkan di atas. Pertama, pengarsipan teratur akan menjadi kebutuhan dalam sistem beban tinggi seperti ini. Saya ingin menjaga konsistensi antara saldo dan riwayat transaksi mereka sambil membiarkan transaksi lama dipindahkan di tempat lain. Untuk melakukan ini, saya akan mengganti setiap kumpulan transaksi yang diarsipkan dengan ringkasan jumlah mereka per pengguna dan mata uang.

Jadi, misalnya, daftar transaksi ini:

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

diarsipkan dan diganti dengan ini:

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

Dengan cara ini, keseimbangan dengan transaksi yang diarsipkan mempertahankan riwayat transaksi yang lengkap dan konsisten.

60
Nick Chammas

Saya tidak terbiasa dengan akuntansi, tetapi saya memecahkan beberapa masalah serupa di lingkungan tipe inventaris. Saya menyimpan total yang berjalan di baris yang sama dengan transaksi. Saya menggunakan batasan, sehingga data saya tidak pernah salah bahkan di bawah konkurensi tinggi. Saya telah menulis solusi berikut pada 2009: :

Menghitung jumlah total yang berjalan sangat lambat, apakah Anda melakukannya dengan kursor atau gabungan segitiga. Sangat menggoda untuk melakukan denormalkan, untuk menyimpan total yang berjalan dalam kolom, terutama jika Anda sering memilihnya. Namun, seperti biasa saat Anda melakukan denormalkan, Anda perlu menjamin integritas data yang didenormalisasi Anda. Untungnya, Anda dapat menjamin integritas menjalankan total dengan kendala - selama semua kendala Anda tepercaya, semua total menjalankan Anda benar. Juga dengan cara ini Anda dapat dengan mudah memastikan bahwa saldo saat ini (total berjalan) tidak pernah negatif - menegakkan dengan metode lain juga bisa sangat lambat. Script berikut menunjukkan tekniknya.

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

Pendekatan yang sedikit berbeda (mirip dengan opsi Anda ke-2 ) untuk dipertimbangkan adalah dengan hanya memiliki tabel transaksi, dengan definisi:

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

Anda juga mungkin menginginkan ID transaksi/Pesanan, sehingga Anda dapat menangani dua transaksi dengan tanggal yang sama dan meningkatkan permintaan pengambilan Anda.

Untuk mendapatkan saldo saat ini, yang perlu Anda dapatkan adalah catatan terakhir.

Metode untuk mendapatkan catatan terakhir :

/* 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

Kekurangan:

  • Saat memasukkan transaksi di luar urutan (yaitu: untuk memperbaiki masalah/saldo awal yang salah), Anda mungkin perlu membuat pembaruan untuk semua transaksi berikutnya.
  • Transaksi untuk Pengguna/Mata Uang perlu diserialisasi untuk menjaga keseimbangan yang akurat.

    -- 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;
    

Kelebihan:

  • Anda tidak lagi harus mempertahankan dua tabel terpisah ...
  • Anda dapat dengan mudah memvalidasi saldo, dan ketika saldo tidak sinkron, Anda dapat mengidentifikasi secara tepat kapan saldo habis saat histori transaksi menjadi dokumentasi sendiri.

Edit: Beberapa contoh pertanyaan tentang pengambilan saldo saat ini dan untuk menyorot con (Terima kasih @Jack Douglas)

15
Andrew Bickerton

Tidak mengizinkan pelanggan untuk memiliki saldo kurang dari 0 adalah aturan bisnis (yang akan berubah dengan cepat karena biaya untuk hal-hal seperti over draft adalah bagaimana bank menghasilkan sebagian besar uang mereka). Anda akan ingin menangani ini dalam pemrosesan aplikasi ketika baris dimasukkan ke dalam riwayat transaksi. Terutama karena Anda mungkin berakhir dengan beberapa pelanggan memiliki perlindungan cerukan dan beberapa mendapatkan biaya yang dikenakan dan beberapa tidak memungkinkan jumlah negatif dimasukkan.

Sejauh ini saya suka ke mana Anda akan pergi dengan ini, tetapi jika ini untuk proyek yang sebenarnya (bukan sekolah) perlu ada banyak pemikiran dimasukkan ke dalam aturan bisnis, dll. Begitu Anda memiliki sistem perbankan naik dan menjalankan tidak ada banyak ruang untuk mendesain ulang karena ada undang-undang yang sangat spesifik tentang orang yang memiliki akses ke uang mereka.

14
mrdenny

Setelah membaca dua diskusi ini, saya memutuskan opsi 2

Setelah membaca diskusi itu juga, saya tidak yakin mengapa Anda memutuskan pada DRI solusi yang paling masuk akal dari pilihan lain yang Anda uraikan:

Terapkan transaksi ke tabel transaksi dan saldo. Gunakan logika TRANSACTION di lapisan prosedur tersimpan saya untuk memastikan bahwa saldo dan transaksi selalu sinkron.

Solusi semacam ini memiliki manfaat praktis yang sangat besar jika Anda memiliki kemewahan membatasi semua akses ke data melalui API transaksional Anda. Anda kehilangan manfaat DRI yang sangat penting, yaitu integritas dijamin oleh basis data, tetapi dalam model kompleksitas apa pun yang cukup akan ada beberapa aturan bisnis yang tidak dapat ditegakkan oleh DRI .

Saya akan menyarankan menggunakan DRI jika memungkinkan untuk menegakkan aturan bisnis tanpa terlalu banyak menekuk model Anda untuk memungkinkannya:

Meskipun saya mengarsipkan transaksi (mis. Dengan memindahkannya ke tempat lain dan menggantinya dengan transaksi ringkasan)

Segera setelah Anda mulai mempertimbangkan mencemari model Anda seperti ini, saya pikir Anda pindah ke daerah di mana manfaat DRI lebih besar daripada kesulitan yang Anda hadapi. Pertimbangkan misalnya bahwa bug dalam proses pengarsipan Anda secara teori dapat menyebabkan aturan emas Anda (yang menyeimbangkan selalu sama dengan jumlah transaksi) menjadi hancurkan secara diam-diam dengan solusi DRI .

Berikut ini ringkasan keuntungan dari pendekatan transaksional seperti yang saya lihat:

  • Kita harus tetap melakukan ini jika memungkinkan. Solusi apa pun yang Anda pilih untuk masalah khusus ini, itu memberi Anda lebih banyak fleksibilitas desain dan kontrol atas data Anda. Semua akses kemudian menjadi "transaksional" dalam hal logika bisnis, bukan hanya dalam hal logika database.
  • Anda dapat menjaga model tetap rapi
  • Anda dapat "menegakkan" rentang yang jauh lebih luas dan kompleksitas aturan bisnis (mencatat bahwa konsep "menegakkan" lebih longgar daripada dengan DRI)
  • Anda masih dapat menggunakan DRI di mana pun praktis untuk memberikan model integritas mendasar yang lebih kuat - dan ini dapat bertindak sebagai pemeriksaan pada logika transaksional Anda
  • Sebagian besar masalah kinerja yang mengganggu Anda akan mencair
  • Memperkenalkan persyaratan baru bisa jauh lebih mudah - misalnya: aturan kompleks untuk transaksi yang disengketakan mungkin memaksa Anda menjauh dari pendekatan DRI murni yang jauh di depan, yang berarti banyak usaha yang sia-sia
  • Partisi atau pengarsipan data historis menjadi jauh lebih tidak berisiko dan menyakitkan

--edit

Untuk memungkinkan pengarsipan tanpa menambah kerumitan atau risiko, Anda dapat memilih untuk menyimpan baris ringkasan di tabel ringkasan terpisah, dihasilkan terus menerus (meminjam dari @Andrew dan @Garik)

Misalnya, jika ringkasannya bulanan:

  • setiap kali ada transaksi (melalui API Anda) ada pembaruan yang sesuai atau masukkan ke tabel ringkasan
  • tabel ringkasan adalah tidak pernah diarsipkan, tetapi transaksi pengarsipan menjadi sesederhana penghapusan (atau jatuhkan partisi?)
  • setiap baris dalam tabel ringkasan termasuk 'saldo awal' dan 'jumlah'
  • periksa batasan seperti 'saldo awal' + 'jumlah'> 0 dan 'saldo awal'> 0 dapat diterapkan ke tabel ringkasan
  • baris ringkasan dapat dimasukkan dalam batch bulanan untuk membuat mengunci baris ringkasan terbaru lebih mudah (akan selalu ada baris untuk bulan ini)

Nick.

Gagasan utama adalah menyimpan saldo dan catatan transaksi dalam tabel yang sama. Itu terjadi secara historis, saya pikir. Jadi dalam hal ini kita bisa mendapatkan keseimbangan hanya dengan menemukan catatan ringkasan terakhir.

 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 

Varian yang lebih baik adalah mengurangi jumlah catatan ringkasan. Kami dapat memiliki satu catatan saldo di akhir (dan/atau mulai) hari itu. Seperti yang Anda tahu setiap bank memiliki operational day untuk membuka dan menutupnya untuk melakukan beberapa operasi ringkasan untuk hari ini. Ini memungkinkan kita untuk menghitung dengan mudah bunga dengan menggunakan catatan saldo setiap hari, misalnya:

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)

Keberuntungan.

6
garik

Berdasarkan kebutuhan Anda, opsi 1 akan muncul yang terbaik. Meskipun saya ingin desain saya hanya memungkinkan memasukkan ke dalam tabel transaksi. Dan memiliki pemicu pada tabel transaksi, untuk memperbarui tabel saldo real time. Anda bisa menggunakan izin basis data untuk mengontrol akses ke tabel ini.

Dalam pendekatan ini, saldo waktu nyata dijamin akan disinkronkan dengan tabel transaksi. Dan tidak masalah jika prosedur tersimpan atau psql atau jdbc digunakan. Anda dapat memeriksa saldo negatif Anda jika diperlukan. Kinerja tidak akan menjadi masalah. Untuk mendapatkan keseimbangan waktu nyata, ini adalah permintaan tunggal.

Pengarsipan tidak akan memengaruhi pendekatan ini. Anda dapat memiliki tabel ringkasan mingguan, bulanan, tahunan juga jika diperlukan untuk hal-hal seperti laporan.

4
Elan Fisoc

Di Oracle, Anda bisa melakukan ini hanya dengan menggunakan tabel transaksi dengan Tampilan Material yang cepat dan dapat di-refresh, yang melakukan agregasi untuk membentuk saldo. Anda menentukan pemicu pada Tampilan Terwujud. Jika Tampilan Terwujud didefinisikan dengan 'ON COMMIT', itu secara efektif mencegah menambah/memodifikasi data dalam tabel dasar. Pemicu mendeteksi data yang valid dan memunculkan pengecualian, di mana ia mengembalikan transaksi. Contoh yang bagus ada di sini http://www.sqlsnippets.com/en/topic-12896.html

Saya tidak tahu sqlserver tetapi mungkin memiliki opsi serupa?

3
ik_zelf