我需要做事务(开始、提交或回滚)、锁定(选择更新)。我怎样才能在文档模型数据库中做到这一点?
编辑:
情况是这样的:
- 我想经营一个拍卖网站。
- 我也认为如何直接购买。
- 在直接购买中,我必须减少项目记录中的数量字段,但前提是数量大于零。这就是为什么我需要锁和事务。
- 我不知道如何在没有锁和/或事务的情况下解决这个问题。
我可以用 CouchDB 解决这个问题吗?
不,CouchDB 使用“乐观并发”模型。用最简单的术语来说,这只是意味着您将文档版本与更新一起发送,如果当前文档版本与您发送的内容不匹配,CouchDB 将拒绝更改。
看起来很简单,真的。您可以为 CouchDB 重构许多基于常规事务的场景。不过,在学习 CouchDB 时,您确实需要放弃您的 RDBMS 领域知识。从更高的层次解决问题是有帮助的,而不是试图将 Couch 塑造成一个基于 SQL 的世界。
跟踪库存
您概述的问题主要是库存问题。如果您有一个描述项目的文档,并且它包含一个“可用数量”字段,您可以处理这样的并发问题:
_rev
CouchDB 发送的属性_rev
使用属性将更新的文档发回_rev
匹配当前存储的号码,完成!_rev
不匹配时),检索最新的文档版本在这种情况下,需要考虑两种可能的故障情况。如果最新文档版本的数量为 0,则您可以像在 RDBMS 中一样处理它,并提醒用户他们实际上无法购买他们想要购买的东西。如果最新文档版本的数量大于 0,您只需使用更新的数据重复操作,然后从头开始。这迫使您比 RDBMS 做更多的工作,并且如果有频繁的、冲突的更新,可能会有点烦人。
现在,我刚刚给出的答案假定您将在 CouchDB 中以与在 RDBMS 中相同的方式做事。我可能会以不同的方式处理这个问题:
我将从包含所有描述符数据(名称、图片、描述、价格等)的“主产品”文档开始。然后我会为每个特定实例添加一个“库存单”文档,其中包含 和product_key
字段claimed_by
。如果您要销售一种锤子模型,并且有 20 个要出售,那么您可能有带有 、 等键的文档hammer-1
来hammer-2
表示每个可用的锤子。
然后,我将创建一个视图,为我提供可用锤子的列表,并使用 reduce 函数让我看到“总数”。这些完全是即兴的,但应该让您了解工作视图的外观。
地图
function(doc)
{
if (doc.type == 'inventory_ticket' && doc.claimed_by == null ) {
emit(doc.product_key, { 'inventory_ticket' :doc.id, '_rev' : doc._rev });
}
}
这为我提供了按产品密钥列出的可用“门票”列表。当有人想买一把锤子时,我可以抓住其中的一组,然后迭代发送更新(使用id
and _rev
)直到我成功申领一个(之前申领的票会导致更新错误)。
减少
function (keys, values, combine) {
return values.length;
}
这个 reduce 函数只返回无人认领的物品总数inventory_ticket
,因此您可以知道有多少“锤子”可供购买。
注意事项
对于您提出的特定问题,这个解决方案代表了大约 3.5 分钟的总思考时间。可能有更好的方法来做到这一点!也就是说,它确实大大减少了冲突更新,并减少了使用新更新来响应冲突的需要。在此模型下,您不会有多个用户尝试更改主要产品条目中的数据。在最坏的情况下,您将有多个用户试图索取一张票,如果您从您的视图中抓取了其中的几个,您只需转到下一张票并再试一次。
扩展库尔特先生的答案。对于很多场景,您不需要按顺序兑换股票。您可以从剩余的票中随机选择,而不是选择第一张票。给定大量票证和大量并发请求,与每个人都试图获得第一个票证相比,这些票证的争用将大大减少。
restfull 事务的设计模式是在系统中创建一个“张力”。对于银行账户交易的流行示例用例,您必须确保更新两个相关账户的总数:
张力扫描应该在所有“张力文件”的后端过程中完成,以保持系统中的张力时间短。在上面的示例中,当第一个帐户已更新但第二个帐户尚未更新时,将出现短时间预期的不一致。如果您的 Couchdb 是分布式的,则必须以与处理最终一致性相同的方式考虑这一点。
另一种可能的实现完全避免了事务的需要:只需存储张力文档并通过评估每个涉及的张力文档来评估系统的状态。在上面的示例中,这意味着一个帐户的总额仅被确定为涉及该帐户的交易文档中的总和值。在 Couchdb 中,您可以很好地将其建模为 map/reduce 视图。
不,CouchDB 通常不适合事务性应用程序,因为它不支持集群/复制环境中的原子操作。
CouchDB 牺牲了事务能力以支持可扩展性。为了进行原子操作,您需要一个中央协调系统,这限制了您的可扩展性。
如果您可以保证您只有一个 CouchDB 实例,或者修改特定文档的每个人都连接到同一个 CouchDB 实例,那么您可以使用冲突检测系统使用上述方法创建一种原子性,但如果您稍后扩展到集群或者使用像 Cloudant 这样的托管服务,它会崩溃,你必须重做系统的那部分。
所以,我的建议是使用 CouchDB 以外的其他东西作为您的帐户余额,这样会容易得多。
作为对 OP 问题的回应,Couch 可能不是这里的最佳选择。使用视图是跟踪库存的好方法,但钳制到 0 或多或少是不可能的。当您阅读视图结果时,问题是竞争条件,决定您可以使用“hammer-1”项目,然后编写一个文档来使用它。问题是,如果视图的结果是 > 0 个锤子-1,则没有原子方法可以仅编写文档以使用锤子。如果 100 个用户同时查询视图并看到 1 个锤子-1,他们都可以编写一个文档来使用锤子 1,从而产生 -99 个锤子-1。在实践中,竞争条件会相当小——如果您的数据库运行的是 localhost,则非常小。但是,一旦您扩展并拥有一个异地数据库服务器或集群,问题就会变得更加明显。
MrKurt 回复的更新(可能只是过时了,或者他可能不知道某些 CouchDB 功能)
视图是处理 CouchDB 中的余额/库存等事情的好方法。
您不需要在视图中发出 docid 和 rev。当您检索查看结果时,您可以免费获得这两个。发出它们——尤其是像字典这样冗长的格式——只会让你的视图不必要地变大。
用于跟踪库存余额的简单视图应该看起来更像这样(我也想不到)
function( doc )
{
if( doc.InventoryChange != undefined ) {
for( product_key in doc.InventoryChange ) {
emit( product_key, 1 );
}
}
}
而reduce函数就更简单了
_sum
这使用了一个内置的 reduce 函数,它只是将所有行的值与匹配的键相加。
在此视图中,任何文档都可以有一个成员“InventoryChange”,该成员将 product_key 映射到它们的总库存的变化。IE。
{
"_id": "abc123",
"InventoryChange": {
"hammer_1234": 10,
"saw_4321": 25
}
}
将添加 10 个hammer_1234 和 25 个 saw_4321。
{
"_id": "def456",
"InventoryChange": {
"hammer_1234": -5
}
}
会从库存中烧掉 5 把锤子。
使用此模型,您永远不会更新任何数据,只会追加。这意味着没有更新冲突的机会。更新数据的所有事务问题都消失了 :)
这个模型的另一个好处是数据库中的任何文档都可以从库存中添加和减去项目。这些文档中可以包含各种其他数据。您可能有一个“装运”文档,其中包含有关接收日期和时间、仓库、接收员工等的大量数据,只要该文档定义了 InventoryChange,它就会更新库存。就像“Sale”文档和“DamagedItem”文档等一样。查看每个文档,他们读得很清楚。视图处理所有艰苦的工作。
实际上,你可以在某种程度上。查看HTTP 文档 API并向下滚动到标题“使用单个请求修改多个文档”。
基本上,您可以在对URI /{dbname}/_bulk_docs的单个发布请求中创建/更新/删除一堆文档,它们要么全部成功,要么全部失败。不过,该文件确实警告说,这种行为将来可能会改变。
编辑:正如预测的那样,从 0.9 版开始,批量文档不再以这种方式工作。
只需使用 SQlite 一种轻量级的事务解决方案,当事务成功完成后复制它,并在 SQLite 中标记它已复制
SQLite 表
txn_id , txn_attribute1, txn_attribute2,......,txn_status
dhwdhwu$sg1 x y added/replicated
您也可以删除复制成功的事务。