爲了提供一個具體的例子(因爲有一個令人驚訝的缺乏正確的例子在線):這裏是如何實現「atomic bank balance transfer」 CouchDB中(從同一主題我的博客文章在很大程度上覆制:http://blog.codekills.net/2014/03/13/atomic-bank-balance-transfer-with-couchdb/)
首先,問題的簡短回顧:如何能在銀行系統,它允許 錢在帳戶之間轉移被設計成有可能離開無效的或無意義的餘額無種族 條件?
這個問題有幾個部分:
第一個:事務日誌。 {"account": "Dave", "balance": 100}
- - 而不是在一個單一的 紀錄或文件存儲的賬戶餘額的賬戶的餘額 由所有的貸方和借方總結該帳戶計算。 這些貸方和借方都存儲在一個事務日誌,它可能看起來 是這樣的:
{"from": "Dave", "to": "Alex", "amount": 50}
{"from": "Alex", "to": "Jane", "amount": 25}
與CouchDB的地圖,減少功能來計算餘額可能看起來 是這樣的:
POST /transactions/balances
{
"map": function(txn) {
emit(txn.from, txn.amount * -1);
emit(txn.to, txn.amount);
},
"reduce": function(keys, values) {
return sum(values);
}
}
爲了完整起見,這裏是平衡的列表:
GET /transactions/balances
{
"rows": [
{
"key" : "Alex",
"value" : 25
},
{
"key" : "Dave",
"value" : -50
},
{
"key" : "Jane",
"value" : 25
}
],
...
}
但這休假顯而易見的問題是:如何處理錯誤?如果 有人試圖讓轉帳大於餘額,會發生什麼情況?
使用CouchDB(以及類似的數據庫),這種業務邏輯和錯誤 處理必須在應用程序級別實現。天真,這樣的功能 可能是這樣的:
def transfer(from_acct, to_acct, amount):
txn_id = db.post("transactions", {"from": from_acct, "to": to_acct, "amount": amount})
if db.get("transactions/balances") < 0:
db.delete("transactions/" + txn_id)
raise InsufficientFunds()
但要注意的是,如果將交易 並檢查更新的餘額數據庫將處於不一致 狀態留給之間的應用程序崩潰:發送者可能是隻剩下一個負平衡,並與 的錢,以前不存在的收件人:
// Initial balances: Alex: 25, Jane: 25
db.post("transactions", {"from": "Alex", "To": "Jane", "amount": 50}
// Current balances: Alex: -25, Jane: 75
如何這個問題能解決?
爲了確保系統永遠處於不一致的狀態,需要被添加到每一筆交易的 兩條信息:
交易的創建(保證的時間,有一個strict total ordering的交易)和
狀態 - 交易是否成功。
還有將需要兩種觀點 - 一個返回賬戶的可用 平衡(即,所有的「成功」交易的總和),而另一個 回報的最古老的「待定」的交易:轉讓
POST /transactions/balance-available
{
"map": function(txn) {
if (txn.status == "successful") {
emit(txn.from, txn.amount * -1);
emit(txn.to, txn.amount);
}
},
"reduce": function(keys, values) {
return sum(values);
}
}
POST /transactions/oldest-pending
{
"map": function(txn) {
if (txn.status == "pending") {
emit(txn._id, txn);
}
},
"reduce": function(keys, values) {
var oldest = values[0];
values.forEach(function(txn) {
if (txn.timestamp < oldest) {
oldest = txn;
}
});
return oldest;
}
}
列表現在看起來是這樣的:
{"from": "Alex", "to": "Dave", "amount": 100, "timestamp": 50, "status": "successful"}
{"from": "Dave", "to": "Jane", "amount": 200, "timestamp": 60, "status": "pending"}
接下來,應用程序將需要有可爲了解決 交易通過檢查每一等待交易的功能,以驗證它是 有效,然後更新其狀態從「待定」要麼「成功」或 「拒絕」:
def resolve_transactions(target_timestamp):
""" Resolves all transactions up to and including the transaction
with timestamp `target_timestamp`. """
while True:
# Get the oldest transaction which is still pending
txn = db.get("transactions/oldest-pending")
if txn.timestamp > target_timestamp:
# Stop once all of the transactions up until the one we're
# interested in have been resolved.
break
# Then check to see if that transaction is valid
if db.get("transactions/available-balance", id=txn.from) >= txn.amount:
status = "successful"
else:
status = "rejected"
# Then update the status of that transaction. Note that CouchDB
# will check the "_rev" field, only performing the update if the
# transaction hasn't already been updated.
txn.status = status
couch.put(txn)
最後,爲正確地應用程序代碼進行傳輸:
def transfer(from_acct, to_acct, amount):
timestamp = time.time()
txn = db.post("transactions", {
"from": from_acct,
"to": to_acct,
"amount": amount,
"status": "pending",
"timestamp": timestamp,
})
resolve_transactions(timestamp)
txn = couch.get("transactions/" + txn._id)
if txn_status == "rejected":
raise InsufficientFunds()
有兩點要注意:
爲了簡潔起見,這個特定的實現假定CouchDB的map-reduce中有一定量的 原子性。更新代碼,以便它不依賴於 ,該假設作爲練習留給讀者。
主/複製或CouchDB的文檔同步尚未考慮到 的考慮。主/主複製和同步使這個問題 明顯更加困難。
在真實的系統中,使用time()
可能會導致衝突,所以使用 有點熵的東西可能是一個好主意;也許"%s-%s" %(time(), uuid())
,或在排序中使用文檔的_id
。 包括時間不是絕對必要的,但它有助於在多個請求幾乎同時進入時保持邏輯 。
CouchDB不是鍵值,它是一個文檔存儲。 – OrangeDog 2017-02-17 10:26:42