为了提供一个具体的例子(因为有一个令人惊讶的缺乏正确的例子在线):这里是如何实现“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