在后台开发中,当我们需要支持大规模的并发读写,同时具备横向扩展能力。这这时我们一般会对数据进行hash分区,从而分布到不同的服务器中,以解决写的瓶颈。同时每个写服务器会通过主从同步分布到多台服务器上面,从而实现读写分离,提高读的并发性能。
那么问题来了,一次性写多条数据,怎么保证多台服务器的一致性呢?如果数据同步存在延迟,怎么保证写后一定能读到呢?本文我们就探讨一下一致性的问题。
一致性的分类
1、我们通常把一致性分为最终一致性与强一致性,那么什么是最终一致性,什么是强一致性呢?
最终一致性(Eventually):写入数据 A 成功后,在其他副本有可能读不到 A 的最新值,但在某个时间窗口之后保证最终能读到。注意这里有限定某个时间窗口。
强一致性(Strong):数据 A 一旦写入成功,在任意副本任意时刻都能读到 A 的最新值。
从定义中我们可以看出,这种分类方法太过于笼统,不利于实际应用。故我们一般从数据的维度或者用户的维度进行分类。
2、数据维度的一致性:就是A服务器的数据与B服务器的数据是否一致。数据维度的一致性常用的有如下几种:
严格一致性,也叫线性一致性、原子一致性。在分布式系统中,如果某个进程更新了数据,那么在其他进程必须能读取到这个最新的数据。在分布式系统中如果要实现严格一致性,一般读写都需要在同一台服务器上,如果读写分布在不同的服务器上,即使使用二阶段提交,由于网络有时延,也不可能做到严格的一致性。举一个购物库存的例子,如果A客户看到评估11库存有10台,如果其他客户也都看到库存是10台,那么就满足强一致性,但是如果某个客户B看到了库存是11台,那么不满足强一致性。一个典型的应用场景就是,如果业务需要满足强一致性要求,那么会到MySQL的主库去读写。
顺序一致性是指,所有的进程以相同的顺序看到所有的修改。读操作未必能及时得到此前其他进程对同一数据的写更新,但是每个进程读到的该数据的不同值的顺序是一致的。举个例子,zk写的时候,ZAB协议只保证超过半数的进程写入成功就返回了,但是客户端去读的时候,可能是从任意的进程读取的,故某个客户端就有可能读到旧的数据库,但是ZAB可以保证按照主进程的写入顺序写入其他进程,因此客户端去读的时候是满足顺序一致性的。在比如聊天时,如果在手机端与电脑端看到的聊天内容的顺序是一致的,那么就满足顺序一致性。
因果一致性是一种弱化的顺序一致性,所有进程必须以相同的顺序看到具有潜在因果关系的写操作,不同进程可以以不同的顺序看到并发的写操作。MySQL5.7开始的数据库的主从同步,其实就是满足因果一致性。在比如一个客户先购买了某只股票,后面又卖了。那么不管在哪里看到的都是先买了某只股票,再卖了某只股票,那么就是满足因果一致性的。我们大仓风控的主从同步,就是满足因果一致性的。
3、用户维度的一致性,主要包括:
单调读一致性是指,如果一个进程读取数据项 a 的值,那么该进程对 a 执行的任何后续读操作,总是得到第一次读取的那个值或更新的值。说白了,就是不能读到新数据后,再读到比这个数据还旧的数据。比如在微博的场景中,某个明星发布了一条微博,如果某个粉丝读到了这条微博,再读到这条微博过后,重新打开APP,发现那条微博不见了(明星没有删除微博),那么就不满足单调一致性。
单调写一致性是指,一个进程对数据项 a 执行的写操作,必须在该进程对 a 执行任何后续写操作前完成。这个很容易满足,注意这里是一个进程,所有的写操作都是顺序的。
写后读一致性是指,一个进程对数据项 a 执行一次写操作的结果,总是会被该进程对 a 执行的后续读操作看见。这个比较常见,比如数据库采用 Master-Slave 结构部署时,写完 Master 数据库,如果从 Slave 读取,有可能读不到,就不满足写后读一致性了。再具体一点的例子就是,某个明星发布了一条微博,但是这个明星再次打开APP时,如果看不到自己发布的微博,那么就不满足写后读一致性。
读后写一致性是指,同一进程对数据项 a 执行的读操作之后的写操作,保证发生在与读时的版本上或者更新的版本上。比如下文介绍的Quorum机制,写的服务器的数据版本是有可能比读的时候旧的,如果写成功了,那么就不满足读后写一致性。
一致性性问题如何解决
上文提到高并发写的时候,我们通过多机器hash处理,从而实现每个机器的写并发都不会太大,但是业务一般都有都多写少的特点,故我们又通过复制多个副本,然后读操作都在副本上面实现,那么我们要如何做到读后写一致性呢?
比如 MySQL,就可以采用一个 Master,多个 Slave 的方式,所有的写都在 Master 上更新,所有的读都在 Slave 上进行,但这里存在一个问题,就是怎么保证写后读一致性?我们可以监控读写的延迟,然后设置一个最大的读写延迟的阈值,如果写后读再阈值以内,那么就去master读,如果超过了阈值,就去slave读。
前面的方法是只再主节点里面写数据,那么我们是否可以一次写多个节点呢?这时Quorum 机制(NWR 模型)就派上用场了。
Quorum 机制就是要满足公式 W+R > N。其中,W 表示必须至少写入成功的节点数,R 表示至少读取成功的节点数,N 表示总节点数。这个公式把选择权交给了业务用户,让用户来做出最终决策。那么业务端怎么知道读取出来的就是最新的数据呢?就是利用版本号,每次写数据的同时,都记录这条数据对应的版本号,读数据的时候判断版本号,版本最新的就是最终数据。这里还存在另外一个问题,那就是,如果写的时候只写入的节点<=N/2,当存在并发写操作时,版本会存在冲突,这就要求我们写数据的时候最好是遵循 W>N/2。
思考题:
1、前面介绍的只再主节点写数据,但是如果主节点挂了,就要选一个从节点作为新的主节点。那么我们如何才能保证选出来的主节点的数据不丢失呢?这时就涉及到分布式共识问题了,我后面会在分布式共识里面介绍。
2、前面介绍的通过hash到不同的服务器,那么常用的hash方法都有哪些呢?怎么做到一个工业级的hash呢?
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。