在金融科技等需要即時處理海量交易資料的領域,資料模型的設計直接決定了系統的效能與擴展性。本文將以一個常見的挑戰——「儲存與查詢大量信用卡交易資料」——為例,深入探討如何利用 Aerospike 的特性,設計出一個既能滿足儲存需求,又能實現高效能查詢的資料模型。
挑戰:無限增長的交易記錄
一個常見的錯誤是將一個信用卡帳戶的所有交易,都儲存在該帳戶的單一記錄 (Record) 中。這種做法會導致記錄大小無限增長,很快就會超過 Aerospike 的記錄大小上限 (8 MiB),並且每次新增交易都需要重寫整個龐大的記錄,對效能和儲存裝置都是一場災難。
解決方案:分桶儲存 (Bucketing)
一個更優雅且高效的解決方案是分桶儲存。我們不再將所有交易都放在一個記錄裡,而是將它們按時間維度進行「分桶」,每個桶儲存在一個獨立的記錄中。
- 主鍵 (PK) 設計: 我們可以設計一個複合主鍵,例如
信用卡PAN + 日期。這樣,一張信用卡每天的交易都會被儲存在一個新的、獨立的記錄中。"Pan-123456789:20230401""Pan-123456789:20230402"
交易資料模型設計
在確定了分桶策略後,我們需要決定如何在每個「日交易記錄」中儲存當天的多筆交易。這裡有兩種主流方案:
方案 A:使用 Map 結構
我們可以建立一個名為 txns 的 Bin,其型別為 Map。Map 的 Key 是交易發生的精確時間戳,Value 則是另一個包含交易詳情(如金額、描述、ID)的 Map。
// Bin "txns"
{
1680113831954: {"amount": 100.00, "desc": "咖啡", "txnId": "t1"},
1699422631954: {"amount": 5.00, "desc": "冰淇淋", "txnId": "t2"},
1704423453345: {"amount": 27.50, "desc": "披薩", "txnId": "t3"}
}
- 優點: 結構清晰,可讀性好,易於理解。
- 缺點: 欄位名 (
amount,desc,txnId) 在每一筆交易中都會重複儲存,造成一定的空間浪費。
方案 B:使用 List 結構 (更優化)
為了極致的空間效率,我們可以將每筆交易的詳情儲存為一個固定順序的 List,而不是 Map。
// Bin "txns"
{
1680113831954: [100.00, "咖啡", "t1"],
1699422631954: [5.00, "冰淇淋", "t2"],
1704423453345: [27.50, "披薩", "t3"]
}
- 優點: 極大地減少了儲存空間和網路傳輸量,因為欄位名不再被重複儲存。
- 缺點: 可讀性稍差,應用程式需要根據約定好的順序(例如,索引 0 是金額,索引 1 是描述)來解析資料。
對於高效能場景,方案 B (List 結構) 通常是更佳的選擇。
高效查詢實踐
1. 批次讀取 (Batch Read)
場景: 詐欺偵測系統需要一次性獲取某張信用卡過去 30 天的所有交易。
// 1. 計算出過去 30 天對應的 Key
long dayOffset = calculateDaysSinceEpoch(new Date());
Key[] keys = new Key[30];
for (int i = 0; i < 30; i++) {
keys[i] = new Key("finance", "transactions", "Pan-123456789:" + (dayOffset - i));
}
// 2. 使用 client.get() 批次讀取 30 個記錄
Record[] dailyTxnRecords = client.get(null, keys);
// 3. 在客戶端合併處理這 30 天的交易資料
// ...
這個操作將 30 次獨立的資料庫請求合併為一次網路往返,極大地提升了查詢效率。
圖表解說:批次讀取交易記錄流程
此循序圖展示了客戶端如何計算多個 Key,並透過一次批次讀取請求從 Aerospike 獲取多天的交易資料。
@startuml
!theme _none_
skinparam dpi auto
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam minClassWidth 100
skinparam defaultFontSize 16
title 批次讀取交易記錄循序圖
participant "Client App" as App
participant "Aerospike Client" as Client
participant "Aerospike Server" as Server
App -> App : 計算過去 30 天的 Keys
App -> Client : client.get(null, keys[])
Client -> Server : 傳送批次讀取請求 (含 30 個 Keys)
Server -> Server : 平行查詢 30 筆記錄
Server --> Client : 回應 Records[] (包含 30 筆記錄的結果)
Client --> App : 回傳 Records[]
App -> App : 合併與處理所有交易資料
@enduml2. 範圍查詢 (Range Query)
場景: 查詢某一天內,特定時間範圍或特定金額範圍的交易。
// 查詢 9:00 到 10:00 之間的所有交易
Record record = client.operate(null, key,
MapOperation.getByKeyRange(
"txns",
Value.get(startTime), // 9:00 的時間戳
Value.get(endTime), // 10:00 的時間戳
MapReturnType.KEY_VALUE
)
);
這個操作將過濾邏輯下推到伺服器端執行,只回傳符合條件的資料,避免了將整天的交易資料都傳輸到客戶端的開銷。
處理物件關聯:客戶與帳戶
在真實世界中,一個客戶 (Customer) 可能擁有多個帳戶 (Account),這是一個典型的多對多關係。
- 最佳實踐: 雙向連結。
- 在
Customer記錄中,建立一個名為accountIds的 List Bin,儲存該客戶擁有的所有帳戶 ID。 - 在
Account記錄中,建立一個名為customerIds的 List Bin,儲存關聯到此帳戶的所有客戶 ID。
- 在
圖表解說:客戶-帳戶多對多關係模型
此類別圖展示了如何透過在各自記錄中儲存對方 ID 的列表,來實現客戶與帳戶之間的雙向關聯。
@startuml
!theme _none_
skinparam dpi auto
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam minClassWidth 100
skinparam defaultFontSize 16
title 客戶-帳戶多對多關係模型
class Customer {
+ custId: String
+ name: String
+ accountIds: List<String>
}
class Account {
+ accountId: String
+ balance: Double
+ customerIds: List<String>
}
Customer "1" -- "0..*" Account : (owns)
@enduml這種設計使得從任何一方都能高效地查詢到其關聯的所有物件。例如,要找到一個客戶的所有帳戶,只需讀取該客戶記錄,獲取 accountIds 列表,然後再批次讀取這些帳戶 ID 對應的記錄即可。