MartinFowler告诉你大数据架构师必备的NoSQL技能-版本戳(上)

2018-04-03 15:37:19 浏览数 (1)

-许多NoSQL数据库的批评者老说NoSQL数据库不支持事务。

事务是一个有用的工具,他可以帮助编程者解决一致性的问题。然而,NoSQL的推崇者并不担心这个问题,原因就是面向聚合的NoSQL数据库是支持在一个聚合内的原子更新的。也就是支持聚合内事务。这种聚合的设计思路就是为了使得数据被组织成一个自然的更新的单元。

即便如此,在你决定用什么数据库的时候也应该把是否支持事务这个问题考虑进来。

然而,有一点必须要记住,那就是事务也是有局限性的。即使在一个支持事务的系统中,我们依然需要通过人为的干涉来处理一些更新,而且通常这些更新也没法运行在同一个事务中,因为他们会hold住一个事务很长时间不放,事务打开时间过长也是不好的。

对于这样的情况,我们又该如何处理呢?那就是引入版本戳version stamps)来解决这个问题——这个版本戳在其他的场景也很好用,尤其是当我们需要从“单服务器分布模型”(single-server distribution model)迁移到多服务器时,更是如此。

Business and SystemTransactions 业务事务和系统事务

在没有事务的情况下也要支持更新一致性的需求实际上也已经是很多系统中非常普遍的功能,即使这些系统是建立在事务数据库之上的。

当人们说起事务的时候,通常指的是“业务事务”或者叫“商业事务”(business transactions)。一个“业务事务”是这样的,比如说,用户浏览了产品的目录,然后选择了一瓶价格合适的Talisker威士忌,然后填写好信用卡信息,然后确认订单。然而所有的这一切通常都不会发生在同一个由数据库提供的系统事务(system transaction)中,因为这将会把数据库的某些元素锁住,当这个用户在寻找自己的信用卡的时候被他的同事叫去一块去吃午饭以后,数据库的那些元素将会被一直锁起来。

通常应用程序只在处理完用户交互操作之后才开始“系统事务”,这样的话,锁就只被持有很短的时间。但是问题是,当需要计算和决策的时候,也许数据已经被改动了。比如,价格表上威士忌的价格也许已经变了,或者有人把客户的地址改了,或者已经把运费改了。

处理这个问题宽泛的技术叫做:离线并发技术(offline concurrency)[Fowler PoEAA]。这个技术在NoSQL上也管用。其中一个比较有用的方法是使用“乐观离线锁”( Optimistic Offline Lock)[Fowler PoEAA],它是“条件更新”(conditional update)的一种形式,就是客户端操作中会重新读取这次业务事务中相关联的信息,然后检查之前最近的那次读取和最新的读取数据是否一致,有没有改变,如果没有改变,那么就将数据显示给用户。

实现这个技术有个很不错的方法就是确保让数据库中的记录都包含某种形式的版本戳(version stamp):其实就是个字段,每当数据库的底层数据被修改时,同时也更新版本戳这个字段的值。这样的话,当你读取数据的时候,你就携带一个版本戳,这样的话,当你写入数据的时候,你就可以检查版本是不是已经被修改了。

在你使用http协议更新资源时,也会涉及到这种技术。其中一种实现方式就是使用“etag”,“e标签”,无论何时你获取到一个资源(resource),服务器总是会在返回的时候在头部(header)中携带“etag”。这个etag是一个没有实际意义的透明的字符串,就是表示资源的版本。如果你之后更新那个资源,那么你就可以通过提供你最近一次在GET中获取到的那个etag的方式进行条件更新(conditional update)。如果服务器上的资源已经被修改,那么这个etags将不会匹配,并且服务器也会拒绝这次更新,服务器会返回412(Precondition Failed 先决条件未满足)错误。

有的数据库也提供了类似的条件更新的机制,就是允许你确保自己的更新不是基于旧数据。你可以自己对这个进行检查,不过要确保在更新和读取资源的过程中,没有其他线程修改这个资源。

(有时候这种做法也被称作“compare-and-set”(CAS)操作,这个名字是从处理器的“CAS操作”中借鉴过来的。二者的区别就在于,处理器中的CAS是在set值之前比较值本身,而数据库的条件更新是比较版本戳的值。)

有很多种不同的方式来构造一个版本戳。

(1) 、计数器。

你可以使用计数器,每更新一次资源就递增。计算器很有用,因为我们很容易的就看出一个版本相比其他的是不是较新的,最新的。另外,这个生成版本的事情必须由一个服务器(server)来做,而且也需要一个单一的master来保证各个计数器不能重复。

(2)、GUID

另外一种方法就是创建一个GUID,这个id是个很大的随机数并且是唯一的。这个值是由日期、硬件信息、以及其他的随机出现的资源的组合后构建的一个值。GUID的一个优势就是他可以被任何人生成并且不会重复。缺点就是它太大并且没法直接比较版本的陈旧与否。

(3)、hash码

第三种方法就是生成该资源内容的hash码。只要这个hash的键足够大,那么这个内容的hash值就可以是全剧唯一的,就像GUID一样,而且还可以被任何人生成;这种方法的优点在于hash码的内容是确定的——只要是同一个资源数据,那么任何节点都将生成同样内容的hash码。然而,就像GUID一样,hash码也是不能直接比较出数据的新旧与否,而且也比较冗长。

(4)、时间戳

第四种做法就是使用最近一次更新的时间戳。就像计数器(counter)一样,他们也相当的短小并且可以直接比较出数据的新旧,然而,这种方法相比计数器的一个好处是我们不再需要一个主节点(master)来专门生成版本戳。多个机器都可以和能够生成时间戳——但是前提是,他们必须得始终保持同步。如果有一个节点出现了“坏时钟”(bad clock),那么就会导致各种数据损毁(data corruptions)现象——如果你在一毫秒内就发生很多次更新的话,那么使用毫秒精度的时间戳就不适合了。

(5)、融合构造

你可以把这几种不同的版本戳方案的优点融合起来,通过使用几种做法来生成一个复合的版本戳。比如,CouchDB就是结合了计数器和hash码来生成版本戳。大多数情况下,我们通过版本戳就可以比较出资源的新旧了,万一遇到两个节点同时更新数据,这时候也许会出现计数相同但内容hash码不同的情况,我们也可以轻松的发现冲突。

除了避免冲突,版本戳也有助于维持“会话一致性”(这个我们之前的文中说到过这个《NoSQL-ReadConsistency-读取一致性》)

0 人点赞