翻译内容:
NoSQL Distilled 第五章 Consistency 一致性
作者简介:
本节摘要:
一致性向来是分布式的一大问题。公众号importsource在之前发布了《更新一致性》的内容,本文我们来讨论一致性中的读取一致性。
5.2. ReadConsistency 读取一致性
接前面《更新一致性》
现在我们的数据库已经支持了更新一致性。但这还不够,这并不能确保数据的reader们发到server上的请求(request)都能得到一致的响应(response)。我们来举个例子,假设现在我们要读取一个订单,这个订单包含了商品信息(line item)和运费(shipping charge)。这个运费是根据订单中的商品信息计算出来的。如果我们添加了一个商品,我们这时候就需要再次计算并更新运费。如果在关系型数据库的话,这个运费和商品信息将会分别放在两个表中。这时候如果Martin向订单中添加了一个商品,然后Pramod就去读取商品和运费,然后Martin更新运费,这就会存在数据不一致的风险。就像图5.1中显示的那样,Pramod读取操作是间于Martin的两次写入中间,这就造成了一种叫做读取不一致( inconsistent read)或者叫读写不一致(read-write conflict)的情况。
图5.1 违反逻辑一致性的读写冲突
我们把上面这种类型的一致性也叫做“逻辑一致性”( logical consistency)。(译者曰:此时你是否想起了产品经理的一句话:“你这个不符合逻辑啊”,oh ,no)好,我们继续,这个逻辑一致性什么意思呢?就是:就是要确保不同的数据放到一块是符合逻辑的。为了避免这种逻辑上不一致的读写冲突(read- write conflict),关系数据库支持“事务”这一概念。通过“事务”就可以把Martin的两次写操作放在一个事务里,这样就可以确保Pramod要么读取到更新之前的两条数据要么读到更新后的两条数据。这样就不会出现逻辑不一致的问题了。
我们经常会听到NoSQL数据库是不支持事务的,并且也无法保证一致性。这种说法大多是不正确的。因为它忽略了很多重要的细节。这里首先要说明的一点是,不支持事务这个说法仅限于一部分NoSQL数据库,尤指面向聚合的数据库。与之相反,你比如“图数据库”就和关系型数据库一样也支持ACID事务。
另外,面向聚合的数据库是支持事务的,只是仅限于聚合内(a single aggregate)。这意味我们在一个聚合内是能保证逻辑一致性的,但跨聚合的逻辑一致性就无法保证了。所以在上面的这个例子,如果我们把订单(order)、运费(delivery charge)以及商品清单放在一个订单聚合中就可以避免之前的“逻辑不一致”问题了。
当然了,我们不可能把所有数据都放在一个聚合里。所以,一旦涉及到多个聚合的更新,肯定会留下一段时间空档导致我们的读取是不一致的。这个不一致的时间段我们称之为“不一致窗口”( inconsistency window)。NoSQL的不一致窗口很短暂:有数据显示,Amazon在文档中声称他们自己的SimpleDB的不一致窗口通常在1秒之内。
上面这个例子说的这种读取的“逻辑不一致”问题,是一个比较典型的例子,你在几乎任何一本有关数据库编程的书里都会看到这种经典案例。而一旦你在自己的数据库中引入了“复制”(replication)这个技术,那么你将会遇到一种全新的不一致类型。让我们再举下之前在“复制”中提到的那个例子,订宾馆的例子。假设酒店现在只剩最后一套房,Martin和Cindy夫妇正在考虑预定这个房间,但是他们还没确定要不要订,于是他们就打越洋电话开始讨论这件事情,因为Martin在伦敦,而Cindy在波士顿。在这个时候呢,在印度孟买的Pramod也想订这套房,并且他就果断的把这套房给订了。这时候数据库就需要把这个更新操作同步到其他的副本上,然而这个更新同步操作到达波士顿的时间要早于伦敦。当夫妇二人打完电话后再次打开网页看房间情况的时候,Cindy看到的是房间被订,而Martin看到的则是这个房间还有。像这种读取不一致的情况,我们叫做“复制一致性”(replication consistency):从不同的副本中读取相同的数据获得相同的值。(see 图 5.2)
图5.2 举例说明“复制不一致”问题
当然了,最终这些更新都会到达每个副本,Martin最终也将会看到这个房间已经被人预订了。我们通常把这种情形称为:最终一致性(eventually consistent),什么意思呢?就是这种不一致的情况只是暂时的,最终所有的节点上都会变成同样的值。这种数据过期我们叫做“陈旧”(stale)的数据,其实从某种意义上,缓存(cache)也是一种复制(replication),特别是那种“主从复制分布模型”( master-slave distribution model)的情况。(公众号importsource在之前发布过有关《主从复制》的内容)
尽管这个复制一致性和逻辑一致性是两码事,不过复制过程中的这个“不一致窗口”太长的话,也会加剧“逻辑不一致”。在主节点上的短时间的两个不同的update操作,也许会有几毫秒的“不一致窗口”,但由于网络原因的延迟,这意味着在从节点上的这种“不一致窗口”将会持续更长。
一致性并不是说就是非常粗暴的针对整个应用程序。我们通常可以对单个特定的请求指定一致性的级别(level)。这样的话,我们就可以在大部分时候没什么大问题的情况下,使用一个比较弱的一致性。而当有必要时,则可使用一致性级别比较高的请求。
不一致窗口的存在意味着不同的人在同一时间看到了不同的数据。如果Martin和Cindy正在通过打越洋电话讨论订房间的事情,那么这种不一致会让他们困惑。如果是单独一个人订房间的话,他就不知道这个不一致的问题,自然这个不一致暂时也不是什么大问题。但是,即使是用户单独一人在进行操作时,不一致窗口也会给用户带来困惑。比如下面这个例子,假设你想对一个博客发表评论。在把刚想好的内容打成文字时,就算“不一致窗口”持续好几分钟,也很少有人会担心。通常系统会通过集群和负载均衡的方式把进来的请求分摊到不同的节点上去,但这存在问题:也许你的一条评论添加请求是由一个节点处理的,然后你刷新你的浏览器,这个刷新请求又被路由到了另外一个节点上,而这个节点还没有接收到刚刚那个更新—于是这个用户刷新后就看不到自己的评论了。
像这种情况,有一种解决思路会让你容忍这种“不一致窗口”,甚至是很长时间,作为用户来讲,你只需要“读取我刚才的写入一致性”read-your-writes consistency)就够了,什么意思呢?一旦你做了更新,随后你就会读到自己的更新后的数据。这个怎么实现呢?在拥有“最终一致性”的系统中,可以提供一种“会话一致性”(session consistency):就是在用户会话内保证“读取我刚才的写入一致性”(read-your-writes consistency)。但有时候会话因为某些原因终止或者用户通过不同的电脑同时去访问同一个系统而导致失去这种“会话一致性”。但这种情况在实际操作时是比较少见的。
这里有几个可以实现“会话一致性”(session consistency)的技术。一个常见的也是最简单的方法就是提供一个“黏性会话”(sticky session),也就是绑定到某个节点的会话(这种特性也被称作叫做“会话亲和力”(session affinity))。这种“黏性会话”可以确保只要某节点具备“读取我刚才的写入一致性”(read-your-writes consistency),那么和这个节点绑定的会话就都有这个特性了。“黏性会话”的缺点就是它会降低负载均衡器(load balancer)的效能。
另外一种实现“会话一致性”的方式是使用版本戳(这个我们会在第六章的详细讲到)以及同数据库的每次交互过程中都必须含有会话所见的最新的那个版本戳。服务器节点也要保证每次响应请求之前,它所含有的更新数据包含此版本戳。
使用“黏性会话”和“主从复制”来保证“会话一致性”时,如果想把读取操作指派给从节点来改善读取性能,同时仍然想将写入操作指派给主节点的话,就比较难办了。解决这个问题的一种方法是,将写入请求先发给从节点,由它负责转发给主节点,并同时保持客户端的“会话一致性”。另一种方法是,在执行写入操作时临时切换到主节点,并且在从节点尚未收到更新数据的这段时间内,把读取操作都交由主节点处理。
我们这里是在数据库的语境下讨论“复制一致性”问题的。然而,我们在为应用程序做整体设计时,这个问题同样是非常重要的因素。哪怕是在一个简单的数据库系统中,也会经常出现用户查看数据,思考其内容,并更新数据的情况。在用户和数据库的交互过程中,通常不要把事务一直开着,因为实际使用中,当用户更新数据库时,可能真的会发生冲突,这种情况就要使用“离线锁”一类的方法了[Fowler PoEAA]。