对于开发或设计分布式系统的架构师工程师来讲,CAP是必需要掌握的理论。mysql
(but:这个文章的重点并非讨论CAP理论和细节,重点是说说CAP在微服务中的开发怎么起到一个指引做用,会经过几个微服务开发的例子说说明,尽可能的去贴近开发)git
CAP定理又被成为布鲁尔定理,是加州大学计算机科学家埃里克·布鲁尔提出来的猜测,后来被证实成为分布式计算领域公认的定理。不过布鲁尔在出来CAP的时候并无对CAP三者(Consistency,Availability,Partition tolerance)进行详细的定义,因此在网上也出现了很多对CAP不一样解读的声音。github
CAP定理在发展中存在过两个版本,咱们以第二个版本为准redis
在一个分布式系统中(指互相链接并共享数据的节点集合)中,当涉及到读写操做时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(Partition Tolerance)三者中的两个,另一个必须被牺牲。算法
这个版本的CAP理论在探讨分布式系统,更增强调两点是互联和共享数据,其实也是理清楚了第一个版本中三选二的一些缺陷,分布式系统不必定都存在互联和共享数据,例如memcached集群相互间就没有存在链接和共享数据,因此memcached集群这类的分布式系统并不在CAP理论讨论的范围,而想Mysql集群就是互联和数据共享复制,所以mysql集群式属于CAP理论讨论的对象。spring
一致性意思就是写操做以后进行读操做不管在哪一个节点都须要返回写操做的值sql
非故障的节点在合理的时间内返回合理的响应数据库
当网络出现分区后,系统依然可以继续旅行社职责bash
在分布式的环境下,网络没法作到100%可靠,有可能出现故障,所以分区是一个必须的选项,若是选择了CA而放弃了P,若发生分区现象,为了保证C,系统须要禁止写入,此时就与A发生冲突,若是是为了保证A,则会出现正常的分区能够写入数据,有故障的分区不能写入数据,则与C就冲突了。所以分布式系统理论上不可能选择CA架构,而必须选择CP或AP架构。服务器
BASE理论是对CAP的延伸和补充,是对CAP中的AP方案的一个补充,即便在选择AP方案的状况下,如何更好的最终达到C。
BASE是基本可用,柔性状态,最终一致性三个短语的缩写,核心的思想是即便没法作到强一致性,但应用能够采用适合的方式达到最终一致性。
理解貌似讲多了,项目的CAP能够参考下李运华的《从零开始学架构》的书,里面的21,22章比较详细的描绘了CAP的理论细节和CAP的版本演化过程。
这里着重的讲解的是神同样的CAP在咱们的微服务中怎么去指导和应用起来,大概会举几个平时常见的例子
在讨论CAP以前先明确下服务注册中心主要是解决什么问题:一个是服务注册,一个是服务发现。
服务注册:实例将自身服务信息注册到注册中心,这部分信息包括服务的主机IP和服务的Port,以及暴露服务自身状态和访问协议信息等。
服务发现:实例请求注册中心所依赖的服务信息,服务实例经过注册中心,获取到注册到其中的服务实例的信息,经过这些信息去请求它们提供的服务。
目前做为注册中心的一些组件大体有:dubbo的zookeeper,springcloud的eureka,consul,rocketMq的nameServer,hdfs的nameNode。目前微服务主流是dubbo和springcloud,使用最可能是zookeeper和eureka,咱们就来看看应该根据CAP理论应该怎么去选择注册中心。(springcloud也能够用zk,不过不是主流不讨论)。
zookeep保证CP,即任什么时候刻对zookeeper的访问请求能获得一致性的数据结果,同时系统对网络分割具有容错性,可是它不能保证每次服务的可用性。从实际状况来分析,在使用zookeeper获取服务列表时,若是zk正在选举或者zk集群中半数以上的机器不可用,那么将没法获取数据。因此说,zk不能保证服务可用性。
eureka保证AP,eureka在设计时优先保证可用性,每个节点都是平等的,一部分节点挂掉不会影响到正常节点的工做,不会出现相似zk的选举leader的过程,客户端发现向某个节点注册或链接失败,会自动切换到其余的节点,只要有一台eureka存在,就能够保证整个服务处在可用状态,只不过有可能这个服务上的信息并非最新的信息。
先要明确一点,eureka的建立初心就是为一个注册中心,可是zk更可能是做为分布式协调服务的存在,只不过由于它的特性被dubbo赋予了注册中心,它的职责更可能是保证数据(配置数据,状态数据)在管辖下的全部服务之间保持一致,全部这个就不难理解为什么zk被设计成CP而不是AP,zk最核心的算法ZAB,就是为了解决分布式系统下数据在多个服务之间一致同步的问题。
更深层的缘由,zookeeper是按照CP原则构建,也就是说它必须保持每个节点的数据都保持一致,若是zookeeper下节点断开或者集群中出现网络分割(例如交换机的子网间不能互访),那么zk会将它们从本身的管理范围中剔除,外界不能访问这些节点,即便这些节点是健康的能够提供正常的服务,因此致使这些节点请求都会丢失。
而eureka则彻底没有这方面的顾虑,它的节点都是相对独立,不须要考虑数据一致性的问题,这个应该是eureka的诞生就是为了注册中心而设计,相对zk来讲剔除了leader节点选取和事务日志极致,这样更有利于维护和保证eureka在运行的健壮性。
再来看看,数据不一致性在注册服务中中会给eureka带来什么问题,无非就是某一个节点被注册的服务多,某个节点注册的服务少,在某一个瞬间可能致使某些ip节点被调用数少,某些ip节点调用数少的问题。也有可能存在一些本应该被删除而没被删除的脏数据。
对于服务注册来讲,针对同一个服务,即便注册中心的不一样节点保存的服务注册信息不相同,也并不会形成灾难性的后果,对于服务消费者来讲,能消费才是最重要的,就算拿到的数据不是最新的数据,消费者自己也能够进行尝试失败重试。总比为了追求数据的一致性而获取不到实例信息整个服务不可用要好。
因此,对于服务注册来讲,可用性比数据一致性更加的重要,选择AP。
这里实现分布式锁的方式选取了三种:
构建表结构
利用表的 UNIQUE KEY idx_lock
(method_lock
) 做为惟一主键,当进行上锁时进行insert动做,数据库成功录入则觉得上锁成功,当数据库报出 Duplicate entry 则表示没法获取该锁。
不过这种方式对于单主却没法自动切换主从的mysql来讲,基本就没法现实P分区容错性,(Mysql自动主从切换在目前并无十分完美的解决方案)。能够说这种方式强依赖于数据库的可用性,数据库写操做是一个单点,一旦数据库挂掉,就致使锁的不可用。这种方式基本不在CAP的一个讨论范围。
redis单线程串行处理自然就是解决串行化问题,用来解决分布式锁是再适合不过。
实现方式:
setnx key value Expire_time
获取到锁 返回 1 , 获取失败 返回 0
复制代码
为了解决数据库锁的无主从切换的问题,能够选择redis集群,或者是 sentinel 哨兵模式,实现主从故障转移,当master节点出现故障,哨兵会从slave中选取节点,从新变成新的master节点。
哨兵模式故障转移是由sentinel集群进行监控判断,当maser出现异常即复制停止,从新推选新slave成为master,sentinel在从新进行选举并不在乎主从数据是否复制完毕具有一致性。
因此redis的复制模式是属于AP的模式。保证可用性,在主从复制中“主”有数据,可是可能“从”尚未数据,这个时候,一旦主挂掉或者网络抖动等各类缘由,可能会切换到“从”节点,这个时候可能会致使两个业务县城同时获取得两把锁
这个过程以下:
上述的问题其实并非redis的缺陷,只是redis采用了AP模型,它自己没法确保咱们对一致性的要求。redis官方推荐redlock算法来保证,问题是redlock至少须要三个redis主从实例来实现,维护成本比较高,至关于redlock使用三个redis集群实现了本身的另外一套一致性算法,比较繁琐,在业界也使用得比较少。
能不能使用redis做为分布式锁,这个自己就不是redis的问题,仍是取决于业务场景,咱们先要本身确认咱们的场景是适合 AP 仍是 CP , 若是在社交发帖等场景下,咱们并无很是强的事务一致性问题,redis提供给咱们高性能的AP模型是很是适合的,但若是是交易类型,对数据一致性很是敏感的场景,咱们可能要寻在一种更加适合的 CP 模型
刚刚也分析过,redis其实没法确保数据的一致性,先来看zookeeper是否合适做为咱们须要的分布式锁,首先zk的模式是CP模型,也就是说,当zk锁提供给咱们进行访问的时候,在zk集群中能确保这把锁在zk的每个节点都存在。
(这个其实是zk的leader经过二阶段提交写请求来保证的,这个也是zk的集群规模大了的一个瓶颈点)
说zk的锁问题以前先看看zookeeper中几个特性,这几个特性构建了zk的一把分布式锁
特性:
当在一个父目录下如 /lock 下建立 有序节点,节点会按照严格的前后顺序建立出自节点 lock000001,lock000002,lock0000003,以此类推,有序节点能严格保证各个自节点按照排序命名生成。
客户端创建了一个临时节点,在客户端的会话结束或会话超时,zookepper会自动删除该解ID那。
在读取数据时,咱们能够对节点设置监听,当节点的数据发生变化(1 节点建立 2 节点删除 3 节点数据变成 4 自节点变成)时,zookeeper会通知客户端。
结合这几个特色,来看下zk是怎么组合分布式锁。
zk官方提供的客户端并不支持分布式锁的直接实现,咱们须要本身写代码去利用zk的这几个特性去进行实现。
首先得了解清楚咱们使用分布式锁的场景,为什么使用分布式锁,用它来帮咱们解决什么问题,先聊场景后聊分布式锁的技术选型。
不管是redis,zk,例如redis的AP模型会限制不少使用场景,但它却拥有了几者中最高的性能,zookeeper的分布式锁要比redis可靠不少,但他繁琐的实现机制致使了它的性能不如redis,并且zk会随着集群的扩大而性能更加降低。
简单来讲,先了解业务场景,后进行技术选型。
若是说到事务,ACID是传统数据库经常使用的设计理念,追求强一致性模型,关系数据库的ACID模型拥有高一致性+可用性,因此很难进行分区,因此在微服务中ACID已是没法支持,咱们仍是回到CAP去寻求解决方案,不过根据上面的讨论,CAP定理中,要么只能CP,要么只能AP,若是咱们追求数据的一致性而忽略可用性这个在微服务中确定是行不通的,若是咱们追求可用性而忽略一致性,那么在一些重要的数据(例如支付,金额)确定出现漏洞百出,这个也是没法接受。因此咱们既要一致性,也要可用性。
都要是没法实现的,但咱们能不能在一致性上做出一些妥协,不追求强一致性,转而追求最终一致性,因此引入BASE理论,在分布式事务中,BASE最重要是为CAP提出了最终一致性的解决方案,BASE强调牺牲高一致性,从而获取肯用性,数据容许在一段时间内不一致,只要保证最终一致性就能够了。
弱一致性:系统不能保证后续访问返回更新的值。须要在一些条件知足以后,更新的值才能返回。从更新操做开始,到系统保证任何观察者老是看到更新的值的这期间被称为不一致窗口。
最终一致性:这是弱一致性的特殊形式;存储系统保证若是没有对某个对象的新更新操做,最终全部的访问将返回这个对象的最后更新的值。
BASE模型是传统ACID模型的反面,不一样与ACID,BASE强调牺牲高一致性,从而得到可用性,数据容许在一段时间内的不一致,只要保证最终一致就能够了。
BASE模型反ACID模型,彻底不一样ACID模型,牺牲高一致性,得到可用性或可靠性: Basically Available基本可用。支持分区失败(e.g. sharding碎片划分数据库) Soft state软状态 状态能够有一段时间不一样步,异步。 Eventually consistent最终一致,最终数据是一致的就能够了,而不是时时一致。
在分布式系统中,要实现分布式事务,无外乎几种解决方案。方案各有不一样,不过其实都是遵循BASE理论,是最终一致性模型。
其实还有一个数据库的XA事务,不过目前在真正的互联网中实际的应用基本不多,两阶段提交就是使用XA原理。
在 XA 协议中分为两阶段:
说一下,为什么在互联网的系统中没被改造过的两阶段提交基本不多被业界应用,最最大的缺点就是同步阻塞问题,在资源准备就绪以后,资源管理器中的资源就一直处于阻塞,直到提交完成以后,才进行资源释放。这个在互联网高并发大数据的今天,两阶段的提交是不能知足如今互联网的发展。
还有就是两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能,例如:
好比在第二阶段中,假设协调者发出了事务 Commit 的通知,可是由于网络问题该通知仅被一部分参与者所收到并执行了 Commit 操做,其他的参与者则由于没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。
TCC是服务化的两阶段变成模型,每一个业务服务都必须实现 try,confirm,calcel三个方法,这三个方式能够对应到SQL事务中Lock,Commit,Rollback。
相比两阶段提交,TCC解决了几个问题
同步阻塞,引入了超时机制,超时后进行补偿,并不会像两阶段提交锁定了整个资源,将资源转换为业务逻辑形式,粒度变小。 由于有了补偿机制,能够由业务活动管理器进行控制,保证数据一致性。
1). try阶段
try只是一个初步的操做,进行初步的确认,它的主要职责是完成全部业务的检查,预留业务资源
2). confirm阶段
confirm是在try阶段检查执行完毕后,继续执行的确认操做,必须知足幂等性操做,若是confirm中执行失败,会有事务协调器触发不断的执行,直到知足为止
3). cancel是取消执行,在try没经过并释放掉try阶段预留的资源,也必须知足幂等性,跟confirm同样有可能被不断执行
一个下订单,生成订单扣库存的例子:
接下来看看,咱们的下单扣减库存的流程怎么加入TCC
在try的时候,会让库存服务预留n个库存给这个订单使用,让订单服务产生一个“未确认”订单,同时产生这两个预留的资源, 在confirm的时候,会使用在try预留的资源,在TCC事务机制中认为,若是在try阶段能正常预留的资源,那么在confirm必定能完整的提交
在try的时候,有任务一方为执行失败,则会执行cancel的接口操做,将在try阶段预留的资源进行释放。
这个并非重点要论tcc事务是怎么实现,重点仍是讨论分布式事务在CAP+BASE理论的应用。实现能够参考:github.com/changmingxi…
本地消息表这个方案最初是 eBay 提出的,eBay 的完整方案 queue.acm.org/detail.cfm?…
本地消息表这种实现方式应该是业界使用最多的,其核心思想是将分布式事务拆分红本地事务进行处理。
对于本地消息队列来讲,核心就是将大事务转变为小事务,仍是用上面下订单扣库存的例子说说明
这里须注意的是,对于一些扫描发送未成功的任务,会进行从新发送,因此必须保证接口的幂等性。
本地消息队列是BASE理论,是最终一致性模型,适用对一致性要求不高的状况。
RocketMq在4.3版本已经正式宣布支持分布式事务,在选择Rokcetmq作分布式事务请务必选择4.3以上的版本。
RocketMQ中实现了分布式事务,其实是对本地消息表的一个封装,将本地消息表移动到了MQ内部。
事务消息做为一种异步确保型事务, 将两个事务分支经过 MQ 进行异步解耦,RocketMQ 事务消息的设计流程一样借鉴了两阶段提交理论,总体交互流程以下图所示:
MQ事务是对本地消息表的一层封装,将本地消息表移动到了MQ内部,因此也是基于BASE理论,是最终一致性模式,对强一致性要求不那么高的事务适用,同时MQ事务将整个流程异步化了,也很是适合在高并发状况下使用。
虽然同步刷盘/异步刷盘,同步/异步复制,并无对cAP直接的应用,但在配置的过程当中也同样涉及到可用性和一致性的考虑
RocketMQ的消息是能够作到持久化的,数据会持久化到磁盘,RocketMQ为了提升性能,尽量保证磁盘的顺序写入,消息在Producer写入RocketMq的时候,有两种写入磁盘方式:
一个broker组有Master和Slave,消息须要从Master复制到Slave上,因此有同步和异步两种复制方式。
异步复制的优势是能够提升响应速度,但牺牲了一致性 ,通常实现该类协议的算法须要增长额外的补偿机制。同步复制的优势是能够保证一致性(通常经过两阶段提交协议),可是开销较大,可用性很差(参见CAP定理),带来了更多的冲突和死锁等问题。值得一提的是Lazy+Primary/Copy的复制协议在实际生产环境中是很是实用的。
RocketMQ的设置要结合业务场景,合理设置刷盘方式和主从复制方式,尤为是SYNC_FLUSH方式,因为频繁的触发写磁盘动做,会明显下降性能。一般状况下,应该把Master和Slave设置成ASYNC_FLUSH的刷盘方式,主从之间配置成SYNC_MASTER的复制方式,这样即便有一台机器出故障,仍然能够保证数据不丢。
在微服务的构建中,永远都逃离不了CAP理论,由于网络永远不稳定,硬件总会老化,软件会可能出现bug,因此分区容错性在微服务中是躲不过的命题,能够这么说,只要是分布式,只要是集群都面临着AP或者CP的选择,但你很贪心的时候,既要一致性又要可用性,那只能对一致性做出一点妥协,也就是引入了BASE理论,在业务容许的状况下实现最终一致性。
到底是选AP仍是选CP,真的在于对业务的了解,例如金钱,库存相关会优先考虑CP模型,例如社区发帖相关能够优先选择AP模型,这个说白了其实基于对业务的了解是一个选择和妥协的过程。