受数据库的一致性处于一种动荡不定的状态.虽然,听起来很难应付,实际上这相当好管理,并且可带来ACID无法企及的更高级别的可伸缩性.
?
BASE的可用性是通过支持局部故障而不是系统全局故障来实现的.下面是一个简单的例子:如果用户分区在5个
数据库服务器上,BASE设计鼓励类似的处理方式,这样一个用户数据库的故障只会影响这台特定主机上的那20%的用户.这里不涉及任何魔法,不过,它确实可以带来更高的可感知的系统可用性.
?
因此,到目前为止,你已经将数据分解到了多个功能组中,并将最繁忙的功能组分区到了多个数据库中,如何在你的应用中应用BASE原则呢?与ACID 的典型应用场景相比,BASE需要对逻辑事务中的操作进行更加深入的分析.到底该如何进行分析呢?后续的内容将提供部分指导原则.
?
一致性模式(Consistency Patterns)
?
沿着Brewer的猜测,如果BASE在分区数据库中选择保留可用性(Availability), 那么,弱化一定程度的一致性就成为必然的选择.这通常难以决策,因为商业投资方与开发人员都倾向于认为一致性(Consistency)对应用的成功至关重要.哪怕是临时的不一致也瞒不过最终用户,因此,技术部门与产品部门都需要参与进来,以决定将一致性弱化到什么程度.
?
图-2是一个简单的概要,它阐释了BASE中一致性要考虑的事情.用户表存储用户信息,同时还包含总销售额与总购买额.这些都是运行时的统计.交易表存储每一笔交易,将买家、卖家以及交易金额关联在一起.这些是对实际使用的表进行过度简化后的结果,不过,它已经包含阐释一致性的多个方面的必要元素.
一般来说,功能组之间的一致性要比功能组内部的一致性要更加容易弱化.这个示例概要包含两个功能组:用户与交易.每当售出一个条目(的商品),交易表中就会增加一条记录,买家与卖家的计数器都会被更新.使用ACID风格的事务,SQL语句可能如图-3所示.
用户表中的总销售额的列与总购买额的列可以被认为是交易表的一份缓存(Cache).它的存在是为了提高系统的效率.有鉴于此,一致性的约束可以被弱化. 可以调整一下买家与卖家的期望设置,从而他们的运行结余(running balance)不能立即反映交易的结果.这种情况很常见,实际上,人们经常会遇到交易与运行结余之间的这种延迟(例如,ATM取款或者手机通话).
?
如何修改SQL语句来弱化一致性要取决于如何定义运行结余,如果它们只是简单的估计,也就是部分交易可以被错过不统计,SQL的修改非常简单,如图-4所示.
现在,我们已经将对用户表与交易表的更新做了解耦.两个表之间的一致性将再也无法保证.实际上,在第一个事务与第二个事务处理间隔发生故障,将导致用户表持久处于不一致的状态,不过,如果合同约定运行时汇总(running total)是估计值的话,这样做也足够了.
?
如果无法接受估计值,该怎么办呢?如何继续对用户表与交易表的更新进行解耦呢?引入一个持久消息队列来解决此问题. 有多种选择可以实现持久消息.然而,实现此消息队列的最关键的因素是,确保队列的持久化支持与数据库使用同样的资源.要实现队列在不涉及2PC的情况下按事务提交,这样做很有必要 .现在的SQL操作看上看去有点不同了,如图-5所示.
这个例子中的语法有点随意,为了阐释概念对其逻辑也做了大量的简化.通过在插入语句的同一个事务中对持久消息进行排队,可以抓取更新用户运行结余所需的信息.这个事务包含在同一个数据库实例中,因此,它不会影响系统的可用性.
?
一个独立的消息处理组件,会从队列中取出每条消息,并将此信息应用到用户表.这个例子看似解决了所有的问题,但是,还有一个问题没有解决.为了避免排队时发生2PC,消息是持久化在交易的主机上的.如果在涉及到用户主机的事务中从队列中取出消息,我们仍将遇到2PC的情景.
?
消息处理组件中的2PC的一种解决方案是什么都不做.通过将更新操作解耦到一个独立的后端(back-end)组件,可以保持面向客户的组件的可用性.业务需要或许可以接受较低的消息处理器的可用性.
?
不过,假定你的系统完全无法接受2PC.这个问题该如何解决呢?首先,你需要理解等幂概念.如果一个操作被应用一次或多次都能取得同样的结果,就被认为是等幂的.等幂操作非常有用,因为它们允许局部故障,重复执行它们不会改变系统的最终状态.
?
从等幂的角度看,所选的这个例子是有问题的.更新操作通常不等幂.这个例子中有累加账户列的操作.重复应用此操作显然会导致错误的账户余额.然而, 即使是仅仅设定一个值的更新操作也不是等幂的,因为它还涉及到操作执行的顺序.如果系统无法保证更新操作按照接收到的顺序被应用,系统的最终状态也将是不正确的.后面的内容会进一步讨论此问题.
?
在账户更新的例子中,你需要一种方式来跟踪哪些更新已经应用成功,哪些更新仍然未解决.一种技术是,使用一个表来记录已经应用的那些交易的唯一识别号.
?
图-6中展示的表会记录交易ID、更新了哪个帐号以及应用此帐号的用户ID.现在,我们的样本伪代码如图-7所示.
这个例子取决于可以窥视队列中的一条消息,并在成功处理后立即删除此消息.如有必要,可以通过两个独立的事务来处理它:消息队列上一个事务,用户数据库上一个事务.数据库操作成功提交,才提交队列操作.目前的算法可以支持局部故障,而且又能提供不依赖于2PC的事务保证.
?
如果只是关注更新的顺序的话,还有一个更加简单的技术可以确保等幂更新.我们来稍微调整一下我们的示例概要,来阐释面临的挑战以及相应的解决方案(见图-8).
假设两笔购买交易在一个很短的时间窗口内发生,我们的消息系统无法确保顺序操作.您现在面临的情况是,取决于消息被处理的顺序,last_purchase可能出现一个不正确的值.幸运的是,可以通过对SQL语句做点简单调整来解决此类更新问题, 如图-9所描述.
仅仅通过不允许last_purchase时间做逆向调整,就可以做到更新操作顺序不相关.也可以通过这种方法来保护任何更新免遭无序更新(out-of-order update).你还可以尝试使用单调递增的事务ID来取代时间.
?
消息队列的顺序
?
关于顺序消息投递,下面这个简短地附属说明可能有用.消息系统可以提供确保消息发送的顺序与接收的顺序一致的能力.不过,支持此功能可能非常昂贵,通常也没有必要,实际上,有时它也只是给出了一种虚假的安全感.
?
这里提供的例子阐释了如何弱化消息的顺序,并在最终仍然能够提供一个数据库的一致性视图.弱化消息排序所需的开销是名义上的,在大部分情况下,此开销要显著的少于在消息系统中确保消息顺序的开销.
?
进一步讲,无论互动风格如何,Web应用在语义上都是一个事件驱动的系统.客户端请求以任意顺序达到系统.每个请求所需的处理时间要求也各不相同. 整个系统的不同组件的请求调度也是不确定的,导致了消息排队的不确定.要求保持消息的顺序给出的是一种虚假的安全感.简单的事实是,不