在前面三篇文章中,介绍了关于分布式系统中数据一致性的问题,这一篇主要介绍CAP定理以及自己对CAP定理的了解。
CAP定理是2000年,由 Eric Brewer 提出来的
Brewer认为在分布式的环境下设计和部署系统时,有3个核心的需求,以一种特殊的关系存在。这里的分布式系统说的是在物理上分布的系统,比如我们常见的web系统。
这3个核心的需求是:Consistency,Availability和Partition Tolerance,赋予了该理论另外一个名字 - CAP。
Consistency:一致性,这个和数据库ACID的一致性类似,但这里关注的所有数据节点上的数据一致性和正确性,而数据库的ACID关注的是在在一个事务内,对数据的一些约束。
Availability:可用性,关注的在某个结点的数据是否可用,可以认为某一个节点的系统是否可用,通信故障除外。
Partition Tolerance:分区容忍性,是否可以对数据进行分区。这是考虑到性能和可伸缩性。
为什么不能完全保证这个三点了,个人觉得主要是因为一旦进行分区了,就说明了必须节点之间必须进行通信,涉及到通信,就无法确保在有限的时间内完成指定的行文,如果要求两个操作之间要完整的进行,因为涉及到通信,肯定存在某一个时刻只完成一部分的业务操作,在通信完成的这一段时间内,数据就是不一致性的。如果要求保证一致性,那么就必须在通信完成这一段时间内保护数据,使得任何访问这些数据的操作不可用。
如果想保证一致性和可用性,那么数据就不能够分区。一个简单的理解就是所有的数据就必须存放在一个数据库里面,不能进行数据库拆分。这个对于大数据量,高并发的互联网应用来说,是不可接受的。
我们可以拿一个简单的例子来说明:假设一个购物系统,卖家A和卖家B做了一笔交易100元,交易成功了,买家把钱给卖家。
这里面存在两张表的数据:Trade表Account表 ,涉及到三条数据Trade(100),Account A ,Account B
假设 trade表和account表在一个数据库,那么只需要使用数据库的事务,就可以保证一致性,同时不会影响可用性。但是随着交易量越来越大,我们可以考虑按照业务分库,把交易库和account库单独分开,这样就涉及到trade库和account库进行通信,也就是存在了分区,那么我们就不可能同时保证可用性和一致性。
我们假设初始状态
trade(buyer,seller,tradeNo,status) = trade(A,B,20121001,I)
account(accountNo,balance) = account(A,300)
account(accountNo,balance) = account(B,10)
在理想情况下,我们期望的状态是
trade(buyer,seller,tradeNo,status) = trade(A,B,20121001,S)
account(accountNo,balance) = account(A,200)
account(accountNo,balance) = account(B,110)
但是考虑到一些异常情况
假设在trade(20121001,S)更新完成之前,帐户A进行扣款之后,帐户A进行了另外一笔300款钱的交易,把钱消费了,那么就存在一个状态
trade(buyer,seller,tradeNo,status) = trade(A,B,20121001,S)
account(accountNo,balance) = account(A,0)
account(accountNo,balance) = account(B,10)
产生了数据不一致的状态
由于这个涉及到资金上的问题,对资金要求比较高,我们必须保证一致性,那么怎么办,只能在进行trade(A,B,20121001)交易的时候,对于任何A的后续交易请求trade(A,X,X),必须等到A完成之后,才能够进行处理,也就是说在进行trade(A,B,20121001)的时候,Account(A)的数据是不可用的。
任何架构师在设计分布式的系统的时候,都必须在这三者之间进行取舍。首先就是是否选择分区,由于在一个数据分区内,根据数据库的ACID特性,是可以保证一致性的,不会存在可用性和一致性的问题,唯一需要考虑的就是性能问题。对于可用性和一致性,大多数应用就必须保证可用性,毕竟是互联网应用,牺牲了可用性,相当于间接的影响了用户体验,而唯一可以考虑就是一致性了。
牺牲一致性
对于牺牲一致性的情况最多的就是缓存和数据库的数据同步问题,我们把缓存看做一个数据分区节点,数据库看作另外一个节点,这两个节点之间的数据在任何时刻都无法保证一致性的。在web2.0这样的业务,开心网来举例子,访问一个用户的信息的时候,可以先访问缓存的数据,但是如果用户修改了自己的一些信息,首先修改的是数据库,然后在通知缓存进行更新,这段期间内就会导致的数据不一致,用户可能访问的是一个过期的缓存,而不是最新的数据。但是由于这些业务对一致性的要求比较低,不会带来太大的影响。
异常错误检测和补偿
还有一种牺牲一致性的方法就是通过一种错误补偿机制来进行,可以拿上面购物的例子来说,假设我们把业务逻辑顺序调整一下,先扣买家钱,然后更新交易状态,在把钱打给卖家
我们假设初始状态
account(accountNo,balance) = account(A,300)
account(accountNo,balance) = account(B,10)
trade(buyer,seller,tradeNo,status) = trade(A,B,20121001,I)
那么有可能出现
account(accountNo,balance) = account(A,200)
trade(buyer,seller,tradeNo,status) = trade(A,B,20121001,S)
account(accountNo,balance) = account(B,10)
那么就出现了A扣款成功,交易状态也成功了,但是钱没有打给B,这个时候可以通过一个时候的异常恢复机制,把钱打给B,最终的情况保证了一致性,在一定时间内数据可能是不一致的,但是不会影响太大。
两阶段提交协议
当然,还有一种方式就是我另外一篇文章里面《X/Open DTP-分布式事务模型》里面说的,但是再第一阶段和第二阶段之间,数据也可不能是一致性的,也可能出现同样的情况导致异常。而且DTP的分布式事务模型 限制太多,例如必须有实现其功能的相关的容器支持,并且资源管理器也必须实现了XA规范。限制比较多。
国外有的架构师有两种方案去解决CAP的限制,但是也是比较适合特定的业务,而没有通用的解决方案,
探知分区->分区内操作->事后补偿
就是上面介绍的异常检测恢复机制,这种机制其实还是有限制,
首先对于分区检测操作,不同的业务涉及到的分区操作可能不一样
分区内操作限制:不同的业务对应的约束不一致
事后补偿:由于业务约束不一样,补偿方式也不一样。
所以这只能作为一种思想,不能做一个通用的解决方案
还有就是ebay使用的BASE 思想((basically available, soft state, eventually consistent),这个比较有意思,有时间在写吧。