看到一篇讲CAP神一样的CAP理论被应用在何方 很不错的文章,这里也记录下,方便查阅。

CAP 是什么

一致性(Consistency)

一致性意思就是写操作之后进行读操作无论在哪个节点都需要返回写操作的值

可用性(Availability)

非故障的节点在合理的时间内返回合理的响应

分区容错性(Partition Tolerance)

当网络出现分区后(也是部分节点出现故障),系统依然能够继续服务

大部分人解释这一定律时,常常简单的表述为:“一致性、可用性、分区容忍性三者你只能同时达到其中两个,不可能同时达到”。实际上这是一个非常具有误导性质的说法。

因为在分布式的环境下,服务绝对无法做到100%可靠,一定会出现故障,因此分区是一个必须的选项,如果选择了CA而放弃了P,若发生分区现象,为了保证C,系统需要禁止写入,此时就与A发生冲突,如果是为了保证A,则会出现正常的分区可以写入数据,有故障的分区不能写入数据,则与C就冲突了。因此分布式系统理论上不可能选择CA架构,而必须选择CPAP架构。

选择的关键在于当前的业务场景,没有定论,比如对于需要确保强一致性的场景如银行一般会选择保证 CP

CAP 在服务中实际的应用例子

服务注册中心解决的问题

先明确下服务注册中心主要是解决什么问题:

  1. 服务注册
  2. 服务发现

服务注册:服务端将自身信息注册到注册中心,这部分信息包括 IP 和 Port ,以及暴露服务自身状态和访问协议信息等。

服务发现:新注册的服务端信息能够及时的被其他调用者发现。不管是服务新增和服务删减都能实现自动发现。

目前作为注册中心的一些组件大致有:zookeeperetcdeurekaconsulrocketMqnameServerhdfsnameNode。目前微服务主流是dubbospringcloud,使用最多是zookeepereureka

zookeeper选择CP

zookeeper保证CP,即任何时刻对zookeeper的访问请求能得到一致性的数据结果,同时系统对网络分割具备容错性,但是它不能保证每次服务的可用性。从实际情况来分析,在使用zookeeper获取服务列表时,如果zk正在选举或者zk集群中半数以上的机器不可用,那么将无法获取数据。所以说,zk不能保证服务可用性。

eureka选择AP

eureka保证APeureka在设计时优先保证可用性,每一个节点都是平等的,一部分节点挂掉不会影响到正常节点的工作,不会出现类似zk的选举leader的过程,客户端发现向某个节点注册或连接失败,会自动切换到其他的节点,只要有一台eureka存在,就可以保证整个服务处在可用状态,只不过有可能这个服务上的信息并不是最新的信息。

服务注册应该选择AP还是CP

对于服务注册来说,针对同一个服务,即使注册中心的不同节点保存的服务注册信息不相同,也并不会造成灾难性的后果,对于服务消费者来说,能消费才是最重要的,就算拿到的数据不是最新的数据,消费者本身也可以进行尝试失败重试。总比为了追求数据的一致性而获取不到实例信息整个服务不可用要好。

所以,对于服务注册来说,可用性比数据一致性更加的重要,选择AP

分布式锁,是选择AP还是选择CP ?

这里实现分布式锁的方式选取了三种:

  • 基于数据库实现分布式锁
  • 基于redis实现分布式锁
  • 基于zookeeper实现分布式锁

基于数据库实现分布式锁

利用表的 UNIQUE KEY特性,当进行上锁时进行insert动作,数据库成功录入则以为上锁成功,当数据库报出 Duplicate entry 则表示无法获取该锁。

不过这种方式对于单主却无法自动切换主从的mysql来说,基本就无法现实P分区容错性,(Mysql自动主从切换在目前并没有十分完美的解决方案)。可以说这种方式强依赖于数据库的可用性,数据库写操作是一个单点,一旦数据库挂掉,就导致锁的不可用。这种方式基本不在CAP的一个讨论范围。

基于redis实现分布式锁

redis单线程串行处理天然就是解决串行化问题,用来解决分布式锁是再适合不过。

实现方式:

setnx key value Expire_time
set key value [EX seconds][PX milliseconds][NX|XX]

事实上这类琐最大的缺点就是它加锁时只作用在一个Redis节点上,即使Redis通过sentinel保证高可用,如果这个master节点由于某些原因发生了主从切换(如主挂掉,或者网络抖动等原因),那么就会出现锁丢失的情况:

  1. 在Redis的master节点上拿到了锁;
  2. 但是这个加锁的key还没有同步到slave节点;
  3. master故障,发生故障转移,slave节点升级为master节点;
  4. 导致锁丢失。

所以 redis 是采用了 AP 方式,保证可用性,但无法确保一致性。

redis官方推荐redlock算法来保证,问题是redlock至少需要三个redis实例(完全互相独立,不存在主从复制或者其他集群协调机制),维护成本比较高。

能否使用redis作为分布式锁?

能不能使用redis作为分布式锁,这个本身就不是redis的问题,还是取决于业务场景,我们先要自己确认我们的场景是适合 AP 还是 CP , 如果在社交发帖等场景下,我们并没有非常强的事务一致性问题,redis提供给我们高性能的AP模型是非常适合的,但如果是交易类型,对数据一致性非常敏感的场景,我们可能要寻在一种更加适合的 CP 模型

基于zookeeper实现分布式锁

首先zk的模式是CP模型,也就是说,当zk锁提供给我们进行访问的时候,在zk集群中能确保这把锁在zk的每一个节点都存在。

zk锁实现的原理

说zk的锁问题之前先看看zookeeper中几个特性,这几个特性构建了zk的一把分布式锁

–>1. 有序节点

当在一个父目录下如 /lock 下创建 有序节点,节点会按照严格的先后顺序创建出自节点 lock000001,lock000002,lock0000003,以此类推,有序节点能严格保证各个自节点按照排序命名生成。

–>2. 临时节点

客户端建立了一个临时节点,在客户端的会话结束或会话超时,zookepper会自动删除该解ID那。

–>3. 事件监听

在读取数据时,我们可以对节点设置监听,当节点的数据发生变化(1 节点创建 2 节点删除 3 节点数据变成 4 自节点变成)时,zookeeper会通知客户端。

结合这几个特点,来看下zk是怎么组合分布式锁

  1. 业务线程-1 业务线程-2 分别向zk的/lock目录下,申请创建有序的临时节点
  2. 业务线程-1 抢到/lock0001 的文件,也就是在整个目录下最小序的节点,也就是线程-1获取到了锁
  3. 业务线程-2 只能抢到/lock0002的文件,并不是最小序的节点,线程2未能获取锁
  4. 业务线程-1 与 lock0001 建立了连接,并维持了心跳,维持的心跳也就是这把锁的租期
  5. 当业务线程-1 完成了业务,将释放掉与zk的连接,也就是释放了这把锁

究竟该用CP还是AP的分布式锁

首先得了解清楚我们使用分布式锁的场景,为何使用分布式锁,用它来帮我们解决什么问题,先聊场景后聊分布式锁的技术选型。 无论是redis,zk,例如redisAP模型会限制很多使用场景,但它却拥有了几者中最高的性能,zookeeper的分布式锁要比redis可靠很多,但他繁琐的实现机制导致了它的性能不如redis,而且zk会随着集群的扩大而性能更加下降。 简单来说,先了解业务场景,后进行技术选型。

分布式事务,是怎么从ACID解脱,投身CAP/BASE

如果说到事务,ACID是传统数据库常用的设计理念,追求强一致性模型,关系数据库的ACID模型拥有高一致性+可用性,所以很难进行分区,所以在微服务中ACID已经是无法支持,我们还是回到CAP去寻求解决方案,不过根据上面的讨论,CAP定理中,要么只能CP,要么只能AP,如果我们追求数据的一致性而忽略可用性这个在微服务中肯定是行不通的,如果我们追求可用性而忽略一致性,那么在一些重要的数据(例如支付,金额)肯定出现漏洞百出,这个也是无法接受。所以我们既要一致性,也要可用性。

都要是无法实现的,但我们能不能在一致性上作出一些妥协,不追求强一致性,转而追求最终一致性,所以引入BASE理论,在分布式事务中,BASE最重要是为CAP提出了最终一致性的解决方案,BASE强调牺牲高一致性,从而获取肯用性,数据允许在一段时间内不一致,只要保证最终一致性就可以了。

BASE模型

BASE模型是传统ACID模型的反面,不同与ACIDBASE强调牺牲高一致性,从而获得可用性,数据允许在一段时间内的不一致,只要保证最终一致就可以了。

BASE理论:

  • Basically Available 基本可用。
  • Soft state软状态即这个状态只是一个中间状态,状态可以有一段时间不同步,异步。
  • Eventually consistent最终一致,最终数据是一致的就可以了,而不是时时一致。

实现最终一致性

弱一致性:系统不能保证后续访问返回更新的值。需要在一些条件满足之后,更新的值才能返回。从更新操作开始,到系统保证任何观察者总是看到更新的值的这期间被称为不一致窗口。

最终一致性:这是弱一致性的特殊形式;存储系统保证如果没有对某个对象的新更新操作,最终所有的访问将返回这个对象的最后更新的值。

分布式事务

在分布式系统中,要实现分布式事务,无外乎几种解决方案。方案各有不同,不过其实都是遵循BASE理论,是最终一致性模型。

  • 两阶段提交(2PC)
  • 补偿事务(TCC)
  • 本地消息表
  • MQ事务消息

两阶段提交(2PC)

其实还有一个数据库的XA事务,不过目前在真正的互联网中实际的应用基本很少,两阶段提交就是使用XA原理。

在 XA 协议中分为两阶段:

  1. 事务管理器要求每个涉及到事务的数据库预提交(precommit,每一个参与者节点会各自执行与事务有关的数据更新,写入Undo LogRedo Log)此操作,并反映是否可以提交。
  2. 事务协调器要求每个数据库提交数据,或者回滚数据。

说一下,为何在互联网的系统中没被改造过的两阶段提交基本很少被业界应用,最最大的缺点就是同步阻塞问题,在资源准备就绪之后,资源管理器中的资源就一直处于阻塞,直到提交完成之后,才进行资源释放。这个在互联网高并发大数据的今天,两阶段的提交是不能满足现在互联网的发展。

还有就是两阶段提交协议虽然为分布式数据强一致性所设计,但仍然存在数据不一致性的可能,例如:

比如在第二阶段中,假设协调者发出了事务 Commit 的通知,但是因为网络问题该通知仅被一部分参与者所收到并执行了 Commit 操作,其余的参与者则因为没有收到通知一直处于阻塞状态,这时候就产生了数据的不一致性。

补偿事务(TCC)

TCC是服务化的两阶段变成模型,每个业务服务都必须实现 try,confirm,cancel三个方法,这三个方式可以对应到SQL事务中Lock,Commit,Rollback

相比两阶段提交,TCC解决了几个问题

同步阻塞,引入了超时机制,超时后进行补偿,并不会像两阶段提交锁定了整个资源,将资源转换为业务逻辑形式,粒度变小。 因为有了补偿机制,可以由业务活动管理器进行控制,保证数据一致性。

–>1. try阶段

try只是一个初步的操作,进行初步的确认,它的主要职责是完成所有业务的检查,预留业务资源

–>2. comfirm阶段

confirm是在try阶段检查执行完毕后,继续执行的确认操作,必须满足幂等性操作,如果confirm中执行失败,会有事务协调器触发不断的执行,直到满足为止

–>3. comfirm阶段

cancel是取消执行,在try没通过并释放掉try阶段预留的资源,也必须满足幂等性,跟confirm一样有可能被不断执行

本地消息表

本地消息表这个方案最初是 eBay 提出的,其核心思想是将分布式事务拆分成本地事务进行处理。

对于本地消息队列来说,核心就是将大事务转变为小事务,用订单和库存的例子说明

  1. 当我们去创建订单的时候,我们新增一个本地消息表,把创建订单和扣减库存写入到本地消息表,放在同一个事务(依靠数据库本地事务保证一致性)
  2. 配置一个定时任务去轮训这个本地事务表,扫描这个本地事务表,把没有发送出去的消息,发送给库存服务,当库存服务收到消息后,会进行减库存,并写入服务器的事务表,更新事务表的状态。
  3. 库存服务器通过定时任务或直接通知订单服务,订单服务在本地消息表更新状态。

这里须注意的是,对于一些扫描发送未成功的任务,会进行重新发送,所以必须保证接口的幂等性。

MQ事务

RocketMQ中实现了分布式事务,实际上是对本地消息表的一个封装,将本地消息表移动到了MQ内部。

总结

在微服务的构建中,永远都逃离不了CAP理论,因为网络永远不稳定,硬件总会老化,软件会可能出现bug,所以分区容错性在微服务中是躲不过的命题,可以这么说,只要是分布式,只要是集群都面临着AP或者CP的选择,但你很贪心的时候,既要一致性又要可用性,那只能对一致性作出一点妥协,也就是引入了BASE理论,在业务允许的情况下实现最终一致性。 究竟是选AP还是选CP,真的在于对业务的了解,例如金钱,库存相关会优先考虑CP模型,例如社区发帖相关可以优先选择AP模型,这个说白了其实基于对业务的了解是一个选择和妥协的过程。