您可以捐助,支持我们的公益事业。

1元 10元 50元





认证码:  验证码,看不清楚?请点击刷新验证码 必填



  求知 文章 文库 Lib 视频 iPerson 课程 认证 咨询 工具 讲座 Modeler   Code  
会员   
 
   
 
 
     
   
 订阅
  捐助
五个解决方案让MongoDB拥有RDBMS的鲁棒性事务
 
作者  Antoine Girbal 火龙果软件  发布于 2014-08-11
   次浏览      
 

在现实世界中,有很多场景都会用到事务这个特性,这也是NoSQL横行下传统RDBMS仍占统治地位的原因,近日MongoDB公司Antoine分享了5个解决方案,用以解决MongoDB文档间的事务问题。

在分布式存储解决方案中谈事务一直是件很痛苦的事情,而事务也成了大部分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”设置一个日期时间戳,记录用户文档的更新。

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

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

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

解决方案2:作业队列

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

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

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

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

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

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

解决方案3:二阶段提交

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

解决方案4:Log Reconciliation

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

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

几个重点:

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

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

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

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

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

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

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

解决方案5:版本控制

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

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

总结

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

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

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

   
次浏览       
相关文章

基于EA的数据库建模
数据流建模(EA指南)
“数据湖”:概念、特征、架构与案例
在线商城数据库系统设计 思路+效果
 
相关文档

Greenplum数据库基础培训
MySQL5.1性能优化方案
某电商数据中台架构实践
MySQL高扩展架构设计
相关课程

数据治理、数据架构及数据标准
MongoDB实战课程
并发、大容量、高性能数据库设计与优化
PostgreSQL数据库实战培训
最新活动计划
LLM大模型应用与项目构建 12-26[特惠]
QT应用开发 11-21[线上]
C++高级编程 11-27[北京]
业务建模&领域驱动设计 11-15[北京]
用户研究与用户建模 11-21[北京]
SysML和EA进行系统设计建模 11-28[北京]

MySQL索引背后的数据结构
MySQL性能调优与架构设计
SQL Server数据库备份与恢复
让数据库飞起来 10大DB2优化
oracle的临时表空间写满磁盘
数据库的跨平台设计
更多...   


并发、大容量、高性能数据库
高级数据库架构设计师
Hadoop原理与实践
Oracle 数据仓库
数据仓库和数据挖掘
Oracle数据库开发与管理


GE 区块链技术与实现培训
航天科工某子公司 Nodejs高级应用开发
中盛益华 卓越管理者必须具备的五项能力
某信息技术公司 Python培训
某博彩IT系统厂商 易用性测试与评估
中国邮储银行 测试成熟度模型集成(TMMI)
中物院 产品经理与产品管理
更多...