分布式一致性问题:就是解决不同机器上的服务数据一致性,该服务可以是普通服务之间的数据同步,可以是不同的数据备份,可以是缓存和主存之间的等。
还需要了解一下:分布式数据一致性能不能完全解决,答案是能,但是这样就需要所有的读写完全隔离,按照顺序来实现事务的读和写,那这样分布式的Available就没有意义了!所有的操作都在排队,Available还有什么意义?!而且集群的点越多,排队时间就久,这就是完全失去意义,也是矛盾的。这也就是CAP理论所描述的问题!
CAP理论解释了为了保持绝对的一致性,那么需要整个集群的读写操作都要事务话,而且事务要排队,就失去了高可用的意义。所以就又有了扩展理论也就是BASE理论(basically Available[基本可用],soft state 软状态或中间状态,Eventual Consistency最终一致性),也就是一致性是存在中间过程的不一致,追求最终一致性,而不是强一致性。要求读和写之间存在事务的非绝对隔离,或者同步之间存在过程的不一致。
下面两个例子:对于HDFS的实现,读写数据都在主节点(主节点建立向从节点推拉数据的队列),从节点(DataNode)只是存储不同文件分片和分片的备份,这是很保守的分布式设计,这样的分布式只是实现了分布式存储,没有实现分布式读写的能力。
对于kafka,kafka在创建不同的topic的时候,启动一次选举,选举leader节点,也就是不同的topic存在不同的leader节点(所以每一个topic的leader节点分布在不同集群机器上)。topic-leader节点创建分区,将不同topic通过partioner分到不同的机器上,同时不同的partion有多个备份点。读写数据的时候,都是从topic-leader开始的,数据写的时候,producer将数据写到topic-leader内存中,leader发给对应的partion节点上去固化,同时该partion的备份点也是主动向topic-leader拉取数据来做备份。数据读的时候,consumer先去topic-leader请求数据,然后leader去对应的partion节点读数据到leader内存中,发给consumer.所以kafka的备份repica就是只是备份而已。
对于分布式实现高性能读和写,一般采用读写分离,而且集群对外暴露节点一般为主节点。写的时候,一般采用写到主节点,由主节点向其他分区节点同步数据。读数据的时候,必然也有转发的过程,客户请求落到主节点(或者集群对外节点),然后该节点到分区节点或者备份节点拿数据回应给客户。redis分布式读写分离的能力就是这样实现的。
对于分布式节点上写数据,如何同步到其他节点,直接去分发就是?!一般不这么做,一般是检测其他节点是否正常,有一个提醒的过程(防止出现其他节点出现堵塞问题,或者状态异常问题);这就是接下来说的设计模式:常用的设计模式或者方案就是XA分布式事务协议(X/Open Agency提出的分布式事务管理协议)两提交/三提交,或者Paxos,Raft算法,现有节点的precommit或者(canCommmit->precommit)undo log,然后协调者(一般是主节点)发出统一的指令后,各自节点开始固化数据。没有precommit返回ACK的节点,主节点记录该节点暂时出现问题,只要过半返回ACK即可。(简单说就是TCC Try-Confirm-Cancel模式)
在每一个节点执行读和写,就是事务;当然在分布式上数据的分发同步也是事务的一部分,有了事务的概念我们就要提到ACID理论(A:Atomicity C:consistency I:Isolution D:Durability)[原子性、一致性、隔离性和持久性];一个事务我们要求它是一个完整的个体,是一个操作的集合,中间不应该被终止,所以要求它具备原子性;一个事务中间有需要操作的step,如果其中有一个step操作失败了,那么之前执行的多个step应该执行rollback操作,保证操作的一致性;一个原子操作需要之间进行隔离,相互不能干涉;最终执行的原子操作要持久化的改变了数据,时间永久性。既然一个事务中间是多个step的集合,而且做到原子性和一致性,中间必然有undo log或缓存去执行逆操作,或者补救、重试操作(当然分布式节点之间的同步方案也是这样的)。
了解了数据同步之间的事务的概念和主流的设计模式之后,我们发现日志或者缓存作为修改的记录描述很关键,可以为回滚和重试,补救等做好一致性的关键备案。那么在事务并发操作中,我们上面也说了不是完全让事务排队的,并发操作会不会有什么问题?很简单我们举一个例子,一个事务在实行写操作,但是只是到缓存或者没有完全的commit,另外一个事务去读,读回来是1,是没有提交的数据,马上再去读那么就是commit之后的2.所以这就有事务并发的问题!
事务并发问题通常是大家说的,1.脏读 2.不可重复读 3.幻读
1.脏读:就是上面例子说的,读了没有提交的,提交之后数值才是对的
2.不可重复读:读了没有提交的,再去读一遍读了已经提交了,前后不一致;尼玛这不是和脏读一样吗??!这是大家传统区分的,我个人认为是一个意思;还区分个毛,确实他妈的不是一个意思嘛!
3.幻读:读了未提交之前的,查回来2条数据,然后再去读就是提交之后的,查询回来三条数据。这尼玛还是一个意思,为非就是插入数据,不是对同一条数据的修改而已。所以这几个分类就是放屁。
所以可以看出来事务需要隔离呀,咋隔离?完全互斥,这样效率太低,所以对于事物我们分为几类隔离级别或者类型。
1.读未提交:就是可以读没有提交的事务,这个就是完全没有限制。
2.读已提交:就是读一定要是事务提交之后,就是读的时候,要是有写事务,会被锁定,等提交之后,再去执行读事务。
3.可重复读:就是屏蔽了重复度出现的不一致问题,这尼玛和第二条不是一个意思嘛?!!个人感觉又是吹牛逼的扯淡的论述,“读已提交:是提交了去读,不是后面还有另外的事务马上又提交了,所以都两次还是不一样呀。” “可重复读:只要后面有连续的数据在提交,那么读都等到所有提交结束再去读。” 尼玛还这样分,扯淡!
4.序列化:就是所有的事务都排序隔离,绝对的队列执行,这个绝对的,但是很耗性能,意义不是很大。
上面说了事务的事情,事务是一堆的step集合,他们之间要做到隔离,完全隔离又不能实用,不完全隔离之间总是在某一个step上出现问题,导致不一致性的问题。那么只能在某些step管理上或者数据读写上做上标记,并发事务的操作我们去识别和规避不一致性问题,这就引入到MVCC概念。mvcc(Muti-Versin Concurrency Control)多版本并发控制,在mysql存储上就使用了这个概念:1.记录数据版本号version code 2.记录的事务ID,且事务id是递增的 3.每一条数据加入隐含的资源 有索引id trx_id,还有roll_pointer回滚指针,该指针指向版本链表和数据的可读视图read_view. 4.Read view 记录每一个事务修改的数值链表,再加上标记位(可见性)来实现读取可见性。那么我们在缓存向DB曾固化的时候,也可以这么实现呢!read_view这个玩意的原理就是在一个事务里面做了一个指针,指针指向要查的数据地址,只要修改读的时候就取回来,回滚了那么也一样读回来,多次提交修改了再去读;但是要是插入数据,这就完了,要借助段锁了,数据段加锁,或者这个间隙段加锁。所以事务的MVCC是一个思想,在事务里面怎么更好控制,减少整个事务的控制隔离。MVCC中数据锁和段锁一起使用才可以解决所谓的“幻读”的问题。
话题转到上面的分布式数据分发的设计模式,不要这些模式(什么两提交,三提交),直接区分发,记录分发修改成功的节点不也一样吗,感觉在性能上区别不大。确实是这样,直接去分发,记录分发修改成功的点即可,不成功的再去重试。太复杂的分发模式反而影响性能。分布式数据一致性问题,就在数据分发出去,每个节点都会得到一个写数据的事务,然后节点还在固化数据,这时候另外的事务需要查数据。如果在分布式上让事务等待,那么高可用就没有意义了,如果记录到缓存的数据,直接返回给要查询的,但是这需要做一层结合缓存的处理的逻辑服务,要是查询的范围大于刚刚修改的数据而缓存的数据,这层缓存服务不得不查询一下,对缓存中重复的行做一下替换,这样使得整个查询的集合是数据一致的。
不过有些场景为了追求 每一个节点数据必须强一致性,比如数据库的备份节点,消息队列缓存节点等,我们还不得不详细讨论下XA协议。XA是一个分布式事务管理的模型,有事务管理中心,资源管理者。事务管理者来向其他资源点分发事务,且驱动事务接收者执行操作事务。XA协议(2PC/3PC)定义的是强一致性,长事务,整个环节有事务锁,其实有锁死的风险,而且单点要是有故障,整个事务都会回滚,完全卡住。在这边方面比较推崇TCC,TCC每一个步骤是一个事务,一定程度上防止长事务风险。
接下来事务直接提交给某一个节点,那么就是具体节点的事务可能存在并发问题了,因为可能刚刚的其他事务还没有结束。所以分布式事务就转移到单节点事务并发的问题了(分布式事务其实就是单节点的事务并发问题)。我们先拿mysql数据库事务做一下简单的分析:
事务A的查询结果锁定在一个Read_View中,即使有另外的事务已经改变了数据 。这样设计是为了解决一个事务中两次读的问题,不过是否很合理具体看场景吧。
通过在select中追加 lock in share mode,来增加间隙锁(段锁) ,那么在查询区间内就不允许去修改了,这样可以使得事务之间很好的隔离,这也是MVCC提供的一个隔离方式。
这个为什么会出现,按理说和第一张图的场景一样的? 场景是一样,但是关键在于上面的insert插入数据之后,read_view还是指向刚开始的查询结果视图,但是经过对刚刚插入的数据进行update之后,会使得read_view刷新数据(或者可以理解为read_view重建)所以导致“幻读”的出现。
分布式一致性的问题:这个问题其实可以分成两个问题分支的;1)分布式各节点数据的一致性问题; 2)分布式事务一致性问题。但是节点数据一致性可以依托分布式事务强制实现,所以两个问题有些人认为应该就是一个问题,就是分布式事务一致性问题。其实这样理解也有些问题,不同节点数据一致性,其实我们可以依托缓存同步机制可以很大程度上解决数据一致性,这是两个思路的问题。对于分布式事务,但是又不能让分布式事务串行化(这样导致分布式系统可用性极差),所以就让分布式事务之间不完全隔离。而且至少让每一个分布式事务是分阶段的(XA协议),然后再去要求每一个阶段是有一定的隔离性。隔离是相对而言的,所以数据的一致性也不是绝对的,这就是分布式系统普遍问题,到单个节点又存在事务并发处理问题。
对于我们开头部分提出:分布式读写的能力,同时又要保证数据的一致性,很好的方案就是读写分离。主节点接收写的数据并分发到不同分区节点和备份节点;读取的时候从备份节点/分区节点读取分发给客户请求。但是这中间要保证读和写的一致性,对于kafka的实现在CA中做了一些细节上的中和。就是每一个备份点会被动态统计与partion的数据差距,来实现动态replica可用与不可用(ISR)。只要备份点的数据和partion差距小到一定范围就认为一致性的。传统上我们还有WNR方案,只要写的副本数或者可读副本数大于集群数N的一半就认为达到集群一致性。追求每个节点严格上的数据一致性这是很不现实的,这也是A会因为C而丧失。对于具体到每一个节点,会因为集群的读写在某一个节点上出现并发读写,那么上面mysql中MVCC的设计就是避免严格的事务互斥,而折衷的方案。所以在分布式上我们为了在每一个节点上能追求一定的高可用而出现一些折衷方案,在具体某一个节点上我们也有防止完全互斥的折衷方案,来尽力实现一定的高可用性。
一般在主从数据库一致性方案中,我们加一层缓存,缓存中存储我们binlog日志,然后缓存定时将binlog同步到从节点或者备份节点。对于读写分离节点(读和写是分离的节点并发提供数据库服务),这个要求就对数据一致性很高了,落盘的数据很可能立马就会被请求查询。那么方案是:我们在缓存存储原数据,然后设置很小的定时间隔(500ms)去同步到数据库只读节点,然后只读节点落盘时间假如耗时500ms,那么缓存整个同步到只读节点的耗时就是1.0s。那么我们需要设置缓存过期策略时间是1s; 然后每一次去查询数据的时候,需要到缓存中查看是否有该数据(有则命中),那么只读节点数据还没有完成落盘操作。所以接着我们需要去写节点去读取数据, 如果没有命中,那么我们就去只读节点读取数据。
为什么实现读写分离呢,是因为在写的过程中,需要锁定表段,对于删除可能需要锁定整个表的数据。读写分离就是为了读和写之间有频繁的间歇轮流操作,那么就会导致不同请求的写和读之间数据库事务排队的现象。但是这个读写分离由于中间添加了缓存判断就会降低性能,那么缓存这一层我们可以在服务本地去实现缓存。
读写分离的业务场景:订单服务不断的接收用户的订单提交以及订单数据的修改,同时仓储或者供应链服务又需要不断查询订单的服务列表去配货或者不同区域的货物信息流转。
对于mysql 还有同步的插件,安装插件配置从节点的数据库,那么插件会定时同步binlog日志到从节点。
参考链接:看一遍就理解:MVCC原理详解 - 知乎