异步复制的最终一致性问题
全球分布式数据库因为地理距离较远(上万公里),网络通信延迟一般在100ms级别,所以只能采取异步复制的方案.采取异步复制方案,那就决定了最终数据被复制的时效性无法得到保证,例如正常情况仅仅比网络延迟多几毫秒(100ms+).但坏情况时,例如,因为网络线路不好,数据可能要花费数秒甚至数分钟才能同步.这就导致了非常恼人的用户体验.
考虑这样的场景:某网络游戏平台的用户A在中国,而用户B是他曾经的邻居,目前在美国.某日,用户A将游戏中的道具转给了用户B,A在游戏中看到了明确的操作成功的提示,而且刷新也确认道具已经转交。但是B一直查询不到该道具。
这就是经典的异步复制(最终一致性)导致的问题。首先,我们把结构简化,整个系统只有两个数据库,分别在中国和美国,还有两个用户,A和B分别在中国和美国。A和B虽然通过游戏平台的业务逻辑代码访问数据,但我们简化,让他们直接访问数据,让他们可以直接看到数据库的数据。
方案1:用户读回源
这个方案很直观,既然A是在中国做的操作,写的是中国的数据库,那么B也应该访问中国的数据,而不是美国的数据库.如果这样做,绝对不会出现不一致的情况.也就是说,单点存储没有一致性问题.
但是, B在什么情况下应该回源访问中国的数据库呢?如果他每一次查看背包,都要访问中国的数据库,那么他会觉得游戏平台的服务非常”慢”.而平台在美国部署的数据库没有起作用,因为部署美国数据库的一个初衷就是让美国的用户访问速度”快”起来.
方案2:用户写多处
这个方案也很直观,用户A不仅仅写中国的数据库,还主动连接美国的数据库写,双写.所以,只要写成功, B在美国就能立即看到,因为已经写了美国的数据库啊…
这个方案也有问题,第一个问题是慢的问题, A要连接美国的数据库写数据,显然会很慢.但是,没办法,要么读慢,要么写慢,一致性只能用通信来保证,而通信又受光速所限,逃不掉.
第二个问题是,如果写中国数据库成功,而写美国数据成功,那么用户应该认为写操作了还是没成功呢?这个问题没有唯一解,只能用户自己决定.如果用户认为成功了,那么就会出现开头例子所说的不一致的问题.如果认为没成功,那么用户只能不断重试.哈哈,这就是经典的CAP理论,想要一致性,就必须要放弃严格的一致性.不过,我们开头举的例子是追求一致性的,所以,结论是:不成功,用户重试.
方案3:数据库强一致性读写
前面的两种方案,不仅有缺陷,重要的是,把责任全推给用户.其实,数据库本身可以做很多事,如果数据库做了这些事,用户就能省功夫.所以,我们看看数据库系统能做什么.
如果把中国和美国两个数据库共同组成一个raft复制组(集群),这样的话,似乎能解决问题?等等, raft具有单一的固定leader,所以,如果leader在中国,美国用户肯定要回源到中国写数据.但是,我们可以提供配套的SDK,用户不需要关心自己有没有回源.
读操作也要回源,无论用raft还是paxos,都是基于通信,所谓通信,就是回源.不过,可以优化回源的数据量,例如, raft就可以通过ReadIndex技术只回源binlog序号,减少了通信数据量.
这个方案无论是写还是读,都很慢,方案不好.这也违反了”小范围同步复制(强一致),大范围异步复制(最终一致)”的原则.
方案4:数据库提供同步(sync)原语
“小范围同步复制(强一致),大范围异步复制(最终一致)”,这个原则是真理,不能违反.所以,数据库自己不能解决全部问题,还是需要用户一起配合.
借鉴Memory Barrier思想,数据库可以提供一些同步(sync)原语,以确保数据能同步到期望的地方.数据同步仍然由数据库来做,脏活累活由数据库来做,用户只需要提出请求.
数据库提供sync_write原语,用户A在中国写入数据库之后,请求数据库sync_write.数据库收到该请求,立即获取本地的binlog序号,然后向美国查询binlog序号,如果美国的序号比中国的序号小,说明之前的写入操作可能还没有同步到美国,等待,然后继续轮询.直到确认同步状态后,再返回响应给用户A.这时,用户A再私下通过微信告诉B去查看游戏背包, B一定会立即看到更新.
数据库提供sync_read原语,用户B在美国读取数据前,先请求数据库sync_read.数据库收到该请求,立即向中国查询最新的binlog序号,然后和自己本地的序号比较.如果美国的序号比中国的序号小,那么就 等待,然后继续比较一次.直至确认美国的序号等于或者大于刚才查询到的中国的序号,这才返回响应. B拿到响应后,再去查看游戏背包,一定会看到更新.注意:查询中国的序号只做一次.
只要用户A sync_write,或者用户B sync_read,就能满足一致性,不需要两人同时做.而其它的正常请求,不需要这两个同步原语,不会有速度慢的问题.