原子交易中的关键价值的商店
-
11-09-2019 - |
题
请原谅中的任何错误的术语。特别是,我使用的关系数据库中的条款。
还有一些持续性的关键价值的商店,其中包括 CouchDB 和 Cassandra, 沿着大量的其他项目。
一个典型的说法是对他们,他们一般不允许原子交易跨越多个行或表。我不知道,如果没有一般的做法将会解决这个问题。
例如采取这一情况的设定银行账户。我们怎么转移资金从一个银行帐户转到另一个?如果每个银行账户是一排,我们希望更新两种行为的一部分,相同的交易,减少的价值和增加值在另一个。
一个明显的方法是有一个单独的表格,其中描述了交易。然后,移动的金钱从一个银行帐户转到另一个包含简单地插入一个新列入本表。我们不存储的当前余额的两个银行账户,而不是依靠综上所有适当的行交易的表格。这是很容易想象,这将有太多的工作,然而;银行可能会有数以百万计的交易每天和一个单独的银行帐户可能迅速有几千的交易'与它相关联。
一个数字(所有的?) 关键价值的商店将'滚回'的行动,如果潜在的数据已经改变,因为你最后一次抓住它。可能这可能是使用模拟原子交易,然后,然后您可以指出某一特定领域是锁着的。有一些明显的问题,这种做法。
任何其他想法吗?这完全是可能的,我的方法是根本错误的和我们尚未包裹着我的大脑,围绕新的思维方式。
解决方案
如果,把你的例子,你想更新原子中的单的文件(行中关系的术语),你可以在CouchDB中这样做的价值。当您尝试提交更改,如果因为你读它的其他竞争客户端已经更新了相同的文档,你会得到一个冲突错误。然后,您将需要阅读的新值,更新并重新尝试提交。有一个不确定的(可能是无限的,如果有一个的很多的争夺)的次数,你可能不得不重复这个过程,但你都保证在数据库中的文档与原子更新的余额,如果您提交不失败的。
如果您需要更新两个余额(即,从一个帐户转移到一个其他),那么就需要使用一个单独的事务文件(有效另一个表,其中行是交易),其存储量和两个账户(在输入和输出)。这是一种常见的做法记账,顺便说一句。由于CouchDB的计算的观点仅在需要时,它实际上仍是非常有效的计算从该列表中该帐户的交易帐户的电流量。在CouchDB中,将使用该发射的帐户号码作为密钥和交易金额(正传入阴性,传出)的映射函数。您减少功能只会值相加每个键,发射相同的密钥和总和。然后可以使用一个视图与组=真来获得帐户余额,通过帐户号码为关键字。
其他提示
CouchDB不适用于事务系统,因为它不支持锁定和原子操作。
为了完成一个银行转账,你必须做几件事:
- 验证的交易,确保有足够的资金来源的帐户,这两个帐户是开放的,不锁,并在良好的信誉,等等
- 减少余额的源帐户
- 增加的平衡的目标帐户
如果变化是在任何这些步骤的平衡状态的账户、交易可能成为无效之后提交的,这是一个大问题中的一个系统的这种。
甚至如果您使用的方法上所建议插入"转让"记录和使用地图/减少图计算出最后帐户余额,你有没有办法确保你不透支来源的帐户,因为那里仍然是一个之间的竞争条件检查源账户平衡,并插入的事务在两个交易可能同时之后添加检查的平衡。
所以...这是错误的工具。CouchDB可能是最擅长很多事情,但这是什么,它真的不能这样做。
编辑:这可能是值得注意的实际银行在现实世界中使用最终一致性。如果你透支您的银行帐户进行足够长你透支的费用。如果你是非常好,你甚至可能能够取钱从两个不同的自动取款机在几乎同一时间和你透支的帐户,因为有一个竞争条件,以检查的平衡问题的钱,并且记录的交易。当你沉检查进入你的帐户他们撞的平衡,但实际上这些资金的一段时间",只是在情况下"来源的帐户并不真正具有足够的钱。
提供一个具体的例子(因为那里是一个令人惊讶的缺乏正确的例子在线):这是如何实现一个"原子银行余额转移"在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
这怎么可能是固定的?
为确保该系统是从不在状态不一致,两片 信息需要增加每个交易:
时间的交易是创建(确保有一个 严格 总序 交易),
一个状况—是否不该交易是成功的。
还需要两个意见—一个其返回的一个帐户是可用的, 平衡(即,所有的"成功的"交易),和另一个 返回的最古老的"未决的"交易:
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的地图减少。新代码,所以它不依赖于 这种假设是保留为行使读者。
主/主复制或CouchDB的文件同步没有被采纳 审议。主/主复制和同步使这个问题 明显地更加困难。
在一个真正的系统,使用
time()
可能会导致冲突,因此使用 一些与多一点的熵可能是一个很好的想法;也许"%s-%s" %(time(), uuid())
, 或使用的文档_id
在排序。包括时间并不是严格必要的,但它有助于维持一个逻辑 如果多个请求在大约同一时间。
的BerkeleyDB和LMDB均为键值存储有用于ACID事务的支持。在BDB txns是可选的,而仅LMDB操作事务。
一个典型的说法是对他们,他们一般不允许原子交易跨越多个行或表。我不知道,如果没有一般的做法将会解决这个问题。
很多现代化的数据储存不支持原子多关键的更新(交易)的框但是他们中的大多数提供原始让你建立酸客户端的交易。
如果数据储存支持的每个关键的线性一致性和比较和交换或试验和设置的操作那么这足以实现可串行交易。例如,这种方法的使用 谷歌的过滤器 在 CockroachDB 数据库。
在我的博客是我创造的 一步步骤的可视化的序列化交碎片的客户端的交易, 描述主要的使用情况,并提供链接到变的算法。我希望它会帮助你理解如何实现它们的数据存储。
其中数据存储,支持每个关键的线性一致性和CAS是:
- Cassandra有轻型交易
- Riak一致的水桶
- RethinkDB
- 动物园管理员
- Etdc
- HBase
- DynamoDB
- MongoDB
顺便说一句,如果你没有读提交的隔离水平,那么很有意义,采取一看 坡道的交易 由彼得*Bailis.他们也可以实施相同的一组数据存储。