强一致性、顺序一致性、弱一致性和共识

强一致性、顺序一致性、弱一致性和共识
原创chao2016 发布于2018-07-21 21:57:37 阅读数 12831 收藏
展开



  1. 一致性(Consistency)
    1.1 强一致性(Strict Consistency)
    原子一致性(Atomic Consistency)
    线性一致性(Linearizable Consistency)
    1.2 顺序一致性(Sequential Consistency)
    1.3 弱一致性
    最终一致性

  2. 共识(Consensus)

  3. 一致性(Consistency)
    一致性(Consistency)是指多副本(Replications)问题中的数据一致性。可以分为强一致性、顺序一致性与弱一致性。



1.1 强一致性(Strict Consistency)
也称为:



原子一致性(Atomic Consistency)
线性一致性(Linearizable Consistency)
两个要求:



任何一次读都能读到某个数据的最近一次写的数据。
系统中的所有进程,看到的操作顺序,都和全局时钟下的顺序一致。
简言之,在任意时刻,所有节点中的数据是一样的。



例如,对于关系型数据库,要求更新过的数据能被后续的访问都能看到,这是强一致性。



1.2 顺序一致性(Sequential Consistency)
the result of any execution is the same as if the operations of all the processors were executed in some sequential order, and the operations of each individual processor appear in this sequence in the order specified by its program. - - Lamport



两个要求:



任何一次读都能读到某个数据的最近一次写的数据。
系统的所有进程的顺序一致,而且是合理的。即不需要和全局时钟下的顺序一致,错的话一起错,对的话一起对。

1.3 弱一致性
数据更新后,如果能容忍后续的访问只能访问到部分或者全部访问不到,则是弱一致性。



最终一致性就属于弱一致性。



最终一致性
不保证在任意时刻任意节点上的同一份数据都是相同的,但是随着时间的迁移,不同节点上的同一份数据总是在向趋同的方向变化。



简单说,就是在一段时间后,节点间的数据会最终达到一致状态。



最终一致性根据更新数据后各进程访问到数据的时间和方式的不同,又可以区分为:



因果一致性(Casual Consistency)。如果进程A通知进程B它已更新了一个数据项,那么进程B的后续访问将返回更新后的值,且一次写入将保证取代前一次写入。与进程A无因果关系的进程C的访问,遵守一般的最终一致性规则。
“读己之所写(read-your-writes)”一致性。当进程A自己更新一个数据项之后,它总是访问到更新过的值,绝不会看到旧值。这是因果一致性模型的一个特例。
会话(Session)一致性。这是上一个模型的实用版本,它把访问存储系统的进程放到会话的上下文中。只要会话还存在,系统就保证“读己之所写”一致性。如果由于某些失败情形令会话终止,就要建立新的会话,而且系统的保证不会延续到新的会话。
单调(Monotonic)读一致性。如果进程已经看到过数据对象的某个值,那么任何后续访问都不会返回在那个值之前的值。
单调写一致性。系统保证来自同一个进程的写操作顺序执行。要是系统不能保证这种程度的一致性,就非常难以编程了。



  1. 共识(Consensus)
    共识问题中所有的节点要最终达成共识,由于最终目标是所有节点都要达成一致,所以根本不存在一致性强弱之分。



例如,Paxos是共识(Consensus)算法而不是强一致性(Consistency)协议。共识算法没有一致性级别的区分。



强一致性:系统中的某个数据被成功更新后,后续任何对该数据的读取操作都将得到更新后的值;



弱一致性:系统中的某个数据被更新后,后续对该数据的读取操作可能得到更新后的值,也可能是更改前的值。但经过“不一致时间窗口”这段时间后,后续对该数据的读取都是更新后的值;



最终一致性:是弱一致性的特殊形式,存储系统保证在没有新的更新的条件下,最终所有的访问都是最后更新的值。



一、业务场景



业务场景为,购买商品的过程要对余额进行查询与修改,大致的业务流程如下:



(1)从数据库查询用户现有余额 SELECT money FROM t_yue WHERE uid=$uid,不妨设查询出来的$old_money=100元



(2)业务层实施业务逻辑,比如购买一个80元的商品,并且打九折



if($old_money> 800.9) $new_money=$old_money-800.9=28



(3)将数据库中的余额进行修改 UPDAtE t_yue SET money=$new_money WHERE uid=$uid



在并发量低的情况下,这个流程没有任何问题,原有金额100元,购买了80元的九折商品(72元),剩余28元。



二、潜在的问题



在分布式环境中,如果并发量很大,这种“查询+修改”的业务很容易出现数据不一致。极限情况下,可能出现这样的异常流程:



(1)业务1和业务2同时查询余额,是100元



(2)业务1和业务2进行逻辑计算,算出各自业务的余额,假设业务1算出的余额是28元,业务2算出的余额是38元



(3)业务1对数据库中的余额先进行修改,设置成28元。



业务2对数据库中的余额后进行修改,设置成38元。



此时异常出现了,原有金额100元,业务1扣除了72元,业务2扣除了62元,最后剩余38元。



三、问题原因



高并发环境下,对同一个数据的并发读(两边都读出余额是100)与并发写(一个写回28,一个写回38)导致的数据一致性问题。



四、原因分析



业务1的写回:原有金额100,这是一个初始状态,写回金额28,理论上只有在原有金额为100的时候才允许写回成功,这一步没问题。



业务2的写回:的原有金额10038100的时候才允许写回成功,可实际上,这个时候数据库中的金额已经变为28了,这一步的写操作不应该成功。



五、简易解决方案



在set写回的时候,加上初始状态的条件compare,只有初始状态不变时,才允许set写回成功,这正是大家常说的“Compare And Set”(CAS),是一种常见的降低读写锁冲突,保证数据一致性的方法。



六、业务的升级



业务线使用CAS解决高并发时数据一致性问题,只需要在进行set操作时,compare一下初始值,如果初始值变换,不允许set成功。



对于上文中的业务场景,只需要将“UPDAtEt_yue SET money=$new_money WHERE uid=$uid”升级为



UPDAtE t_yue SETmoney=$new_money WHERE uid=$uid AND money=$old_money”即可。



并发操作发生时:



业务1执行 => UPDAtE t_yue SET money=28 WHERE uid=$uid AND money=100



业务2执行SET money=38 WHERE uid=$uid



【这两个操作同时进行时,只能有一个执行成功】。



七、怎么判断哪个执行成功,哪个执行失败



set操作,其实无所谓成功或者失败,业务能通过affect rows得知哪个修改没有成功:



执行成功的业务,为1



执行失败的业务,为0



八、总结



高并发“查询并修改”的场景,可以用CAS(Compare and Set)的方式解决数据一致性问题。对应到业务,即在set的时候,加上初始条件的比对。



1.给出CAP理论的文章,链接地址:http://www.mysqlops.com/2011/03/31/nosql-cap.html2.关于一致性,分为:强一致性、弱一致性、最终一致性,所以说一致性是肯定存在的,只是什么程度而已;3.数据库的一致性肯定要分集中式数据库 还是 分布式数据库,若是集中式数据库的一致性也是可以设置的,但是集中式与分布式数据库的一致性就是天囊之别。4.数据库的作用主要不是为了实现一致性,而是为了资源共享,然后再引入很多新问题,比如你所提到的数据一致性,那么就会借助锁等解决技巧。



ConcurrentHashMap的迭代器是强一致性的迭代器还是弱一致性的迭代器



而在迭代时,ConcurrentHashMap使用了不同于传统集合的快速失败迭代器(见之前的文章《JAVA API备忘—集合》)的另一种迭代方式,我们称为弱一致迭代器。在这种迭代方式中,当iterator被创建后集合再发生改变就不再是抛出ConcurrentModificationException,取而代之的是在改变时new新的数据从而不影响原有的数据,iterator完成后再将头指针替换为新的数据,这样iterator线程可以使用原来老的数据,而写线程也可以并发的完成改变,更重要的,这保证了多个线程并发执行的连续性和扩展性,是性能提升的关键。


Category storage