设为首页收藏本站

LUPA开源社区

 找回密码
 注册
文章 帖子 博客
LUPA开源社区 首页 业界资讯 技术文摘 查看内容

五个解决方案让MongoDB拥有RDBMS的鲁棒性事务

2014-8-8 09:48| 发布者: joejoe0332| 查看: 32582| 评论: 0|原作者: Antoine Girbal|来自: 个人博客

摘要: 在分布式存储解决方案中谈事务一直是件很痛苦的事情,而事务也成了大部分NoSQL解决方案短板所在。近日,MongoDB公司的Antoine Girbal在其个人博客上撰文,分享了在MongoDB文档间实施鲁棒可扩展事务的5个解决方案—— ...


解决方案4: Log Reconciliation

  很多财务系统常用的解决方案是 log reconciliation。这种解决方案将事务写作简单的日志,这避免了复杂性和潜在的故障。然后从上次良好状态以来所有的变化推测当前账户的状态。在极端情况下,你可以清空账户,然后通过实施从第一天以来所有的变化重建账户……这听起来很恐怖,但是可行。账户文件需要一个“缓存”来提高速度,还需要一个seqId,seqId计算如下:

{ _id: accountId, cache: { balance:10000, seqId: 115 } }

  执行事务时,一个典型的财务系统会给事务写一个条目,会给与事务有关的账户写一个“账户变化”条目。这个方法需要进一步的写保证,“作业队列”解决方案可以实现写保证,事务中所有的作业在所有账户更改写入前都会保持不变。不过有了MongoDB,我们可以写一个包括事务和账户更改的文档。这个文档应该嵌入tx集合,如下:

{ _id: ObjectId, ts: timestamp , proc: "UNCOMMITTED", state: "VALID", changes: [ { account: 1234, type: "withdraw", value: -100, seqId: 801, cachedBal: null }, { account: 2345, type: "deposit", value: 100, seqId: 203, cachedBal: null } ] }


几个重点:

  • 步骤:事务从“UNCOMMITTED” 状态开始,变为“COMMITTED”,此时涉及这些账户的所有先前事务也会变为 “COMMITTED” ,这表明这个事务也可以用作“anchor”来进行平衡计算。
  • 状态:状态可能是 “VALID”、“CANCELLED等。如果不是VALID,即使是“COMMITTED”,平衡计算也会忽略事务。
  • seqId:这是账户的独有的seqId,这个seqId给账户更改一个确定的顺序。
  • cachedBal:账户的缓存平衡。如果事务时“COMMITTED”状态,那么缓存平衡(如果设置了)是一个有效值。
  • 注意我们在 { changes.account: 1, changes.seqId: 1 }上使用一个独特的索引。reconciliation需要这个索引来提速,一个账户也不会有seqId副本。


  关键是确保即使事务没有按顺序发生,缓存平衡也可以安全的计算/取消,还有就是事务状态可能改变。因此我们每个账户使用一个seqId,这确保了账户更改按确定的顺序发生,可以避免复杂的锁。在写事务前,应用首先通过简单地查询推断每个账户的下一个sqlId:

db.tx.find({ "changes.account": 1234 }, { "changes.$.seqId": 1 }).sort({ "changes.seqId": -1 }).limit(1)


  然后每个sqlId都本地增长,然后写作事务的一部分。如果另一个线程也可能同时包括同样的seqId,独特的索引会确保写失败,线程会进行重试直到顺利完成任务。另一种方法是在账户集中保存一个当前seqId,然后用 findAndModify()获得下一个seqId,这通常会比较慢,除非你对账户有很多争用。注意如果因为某种原因事务没有写时,seqId可能会被跳过去,不过只有没有副本情况下才会成为。


  下面我们谈谈reconciliation的基础。后台进程确保所有未提交的事务都会继续进行。只有所有账户的低seqId的事务都提交后一个事务才会被标注为提交。事务被标记为提交后就会变成不可变的。下面来谈谈好的方面:获得账户平衡。首先我们获得好的平衡,我们可以通过索引进行查询:

db.tx.find({ "changes.account": 1234, proc: "COMMITTED" }, { "changes.$": 1 }).sort({ "changes.seqId": -1 }).limit(1)


  我们通过较大seqId的事务获得所有将发生的更改:

db.tx.find({ "changes.account": 1234, "changes.seqId": { $gt: lastGoodSeqId } }, { "changes.$": 1 }).sort({ "changes.seqId": 1 })


  我们可以使用这些解决展示即将发生的损耗。如果我们只想简单的了解将来的平衡点在哪,我们可以让MongoDB收集所有变更展示总数:

db.tx.aggregate([{ $match: { "changes.account": 1234, "changes.seqId": { $gt: lastGoodSeqId }, state: "VALID" }}, 
{ $unwind: "changes" }, 
{ $match: { "account": 1234 }}, 
{ $group: { _id: "total", total: { $sum: "$value" } }}])


  为了确保系统快速、计算量小,后台工作者要确保所有的事务都达到提交状态,平衡得到缓存。理想情况下一个事务是不可逆的,取而代之的是提交一个逆向事务来实施事务。不过只要所有的进一步事务状态和缓存都是正确设置的,取消是可行的。


解决方案5:版本控制

  有时变得很复杂,以至于不能再JSON中表示,这些变更可能涉及很多有着复杂关系的文件(如树结构)。如果仅是部分变化(如破坏树)将会很混乱,这种情况下我们需要隔离。获取隔离性的一种方式是插入有着高版本号的新文档,取代对现有文档的更新。可以通过同日志和解同样的技术很容易、很安全的获得新版本号。通常{ itemId: 1, version: 1}上有一个独特的索引。

  嵌入文档的应用从子文档开始,到主文档结束(如根节点)。当获取数据时,应用检查主文档的版本号,忽略高于版本号高于此版本号的文档。未完成的事务可以保持原状,可以忽略,可以清楚。


总结

  综上所述,我们提供了在文档间实施鲁棒可扩展事物的五种解决方案:

  • 同步标志:最适用于仅从主文档复制数据的情况
  • 作业队列:比较通用,适用于95%的情况,大部分系统至少需要一个作业队列
  • 二阶段提交:这种技术确保每个实体都有为保持一致性状态所需的所有信息
  • Log Reconciliation:最鲁棒的技术,最适用于财务系统
  • 版本控制:提供了隔离性,适用于复杂的结构


  此外,我们还提到了很多次MongoDB最终将支持真正的原子性和文档间的隔离事务。这已经作为分区的一部分了,但目前还只是内部的……只有文档在同一分区时这一特性才可能实现,否则我们将回到不可扩展的SQL世界。


原文链接: How to Implement Robust and Scalable Transactions Across Documents with MongoDB(编译/蔡仁君 责编/仲浩)



酷毙

雷人

鲜花

鸡蛋

漂亮
  • 快毕业了,没工作经验,
    找份工作好难啊?
    赶紧去人才芯片公司磨练吧!!

最新评论

关于LUPA|人才芯片工程|人才招聘|LUPA认证|LUPA教育|LUPA开源社区 ( 浙B2-20090187 浙公网安备 33010602006705号   

返回顶部