设为首页收藏本站

LUPA开源社区

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

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

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

摘要: 在分布式存储解决方案中谈事务一直是件很痛苦的事情,而事务也成了大部分NoSQL解决方案短板所在。近日,MongoDB公司的Antoine Girbal在其个人博客上撰文,分享了在MongoDB文档间实施鲁棒可扩展事务的5个解决方案—— ...
  在分布式存储解决方案中谈事务一直是件很痛苦的事情,而事务也成了大部分NoSQL解决方案短板所在。近日,MongoDB公司的Antoine Girbal在其个人博客上撰文,分享了在MongoDB文档间实施鲁棒可扩展事务的5个解决方案——同步字段、作业队列、二阶段提交、Log Reconciliation和版本控制。

以下为译文:

事务问题

  数据库支持数据块间的事务是有原因的。典型的场景是应用需要修改几个独立的比特时,如果只有一些而不是全部改变存储到了数据库,那么这就会出现不一致问题。因此ACID的概念是:

  • 原子性:所有的改变要么都做了,要么都没做
  • 一致性:数据保持一致性状态
  • 隔离性:其它用户看不到部分改变
  • 持久性:一旦向用户确认了事务,数据就处于安全的状态(通常存在硬盘上)

  引入NoSQL数据库后,文档间ACID事务的支持通常就取消了。许多键/值存储仍有ACID,但它只适用于单个条目,取消ACID的主要原因是其可扩展限制。如果文档横跨几个服务器,事务将会很难实施以及性能。假设事务横跨数十个服务器,一些数据库是远程的,一些是不可靠的,想象下这会变的多难,多慢!

  在单个文档等级上,MongoDB支持ACID。更准确的说,默认情况下是“ACI”,打开“j”WriteConcern选项后是ACID。Mongo有丰富的查询语言,横跨多个文档,因此人们一直在寻找多文档事务来使用他们的SQL代码。一个常见的办法是利用文档的性质:不需要很多行、很多关系,你可以将所有的东西嵌入到一个大文档中,Denormalization将带你回归事务。

  这个技术解决了从一对一关系到一对多关系的很多事务问题。这也可能使应用更简单,数据库更快,所以这是双赢。不过当数据库必须分离时,该怎么办?


减少ACID

  其实大部分应用都可以归结为:

  • 原子性:实际上你希望所有的改变都完成
  • 一致性:系统短时间不一致没关系,只要最终一致就行
  • 隔离性:缺乏隔离性导致暂时的不一致,这并不理想,但是当今线上服务时代,很多用户对此都习惯了(如用户支持:“它要花几秒传输”)。
  • 持久性:很重要,要支持。


这样问题就简化为鲁棒性、可扩性、最终一致性。

解决方案 1:字段同步

  这种解决方案的使用场景最简单,最常见:文档间有些字段需要保持“同步”。例如,你有一个用户名为“John”的用户文档,文档代表John发表过的评论。如果用户可以更换用户名,那么这个改变需要发送给所有文档,即使进程中有应用错误或数据库错误。

  为了实现这一目标,一个简单的办法是在主文档(这个情况下主文档是用户文档)中使用一个新字段(如“syncing”)。给“syncing”设置一个日期时间戳,记录用户文档的更新。

db.user.update({ _id: userId }, { $set:{ syncing: currentTime }, { rest of updates ... } })

  然后应用会修改所有的评论文档。结束后,需要移除标识:

db.user.update({ _id: userId }, {$unset: { syncing: 1 } })

  现在假设进程中出现了问题:有些评论使用的是旧用户名。不过这些地方仍然会保留标识,所以应用知道哪些进程需要重新进行。因此,你需要后台进程在指定的时间(如1小时)检查“syncing”文件是否有未完成的地方。索引应设为“sparse”,这样只有实际设置的文档需要被索引,索引量就会比较小。

db.user.ensureIndex({ syncing: 1 }, { sparse: true })

  因此,系统通常可以保持事情在短时间内同步,在系统故障的情况下,时间周期为一个小时。如果时间不重要,当探测到“syncing”标志时,应用可以轻易修复文档。


解决方案2:作业队列

  以上原理良好工作的前提是应用不需要很多内容,只依赖于通用进程(如:复制一个值)。一些事务需要执行特定变化,这些变化稍后很难识别。例如,用户文档包括一个朋友列表:

{ _id: userId, friends: [ userId1,userId2, ... ]}

  现在A和B决定成为朋友:你需要把B添加到A的列表,也需要把A添加到B的列表。如果两者没有同时发生也没有关系(只要没有引发困扰)。针对这种情况和大多数事务问题的解决方案是使用作业队列,作业队列也存储在MongoDB。一个作业文档就像这样:

{ _id: jobId, ts: timeStamp, state: "TODO", type: "ADD_FRIEND", details: { users: [ userA, userB ]} }

  或者是原始线程可以插入作业转发改变,或者是“worker”线程可以捡起工作。worker使用findAndModify()获取最原始的未加工的工作,findAndModify()是完全原子性的。操作中findAndModify()将工作标注为将被处理,同时也会表明worker name、当前时间以便于追踪。{ state: 1, ts: 1 } 上的索引使这些调用很迅速。

db.job.findAndModify({ query: { state: "TODO" }, sort: { ts: 1 }, update: { $set: { state: "PROCESSING", worker: { name: "worker1", ts: startTime } } } })

  之后worker以一种幂等的方式对双方用户文档进行修改,这些改变能应用很多次,并且有同样的效果——这很重要!为了这个目的,我们只需要使用一个$addToSet。一种更通用的替代方式是在查询端添加一个测试,检测修改是否执行了。

db.user.update({ _id: userA }, {$addToSet: { friends: userB } })

  最后一步是删除作业或标注作业完成。再保留一段时间作业是一种安全的方式,唯一的缺点是随着时间的流逝,先前的索引会变得越来越大,尽管你可以在指定域{ undone: 1 } 上使用稀疏索引,并且根据实际情况修改查询。

db.job.update({ _id: jobId }, { $set: { state: "DONE" } })

  如果进程在某一时刻故障了,作业仍然会在队列中,并标注为处理中。后台进程停止一段时间后会将作业标注为需要再次处理,然后作业会重新从头开始。


解决方案3 :二阶段提交

  二阶段提交是一个众所周知的解决方案,很多分布式系统都采用了这种解决方案。MongoDB简化了这种解决方案的实施,因为灵活的框架,我们可以将所有需要执行的数据全都放入文档中。我几年前就写过关于这种方法的文章,你可以去MongoDB Cookbook中查阅《 执行二阶段提交》(Perform Two Phase Commits)或者到MonoBD Manual中查阅《 执行二阶段提交》(Perform Two Phase Commits)。



酷毙

雷人

鲜花

鸡蛋

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

最新评论

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

返回顶部