神一样的CAP理论被应用在何方?
对于开发或设计分布式系统的架构师工程师来说,CAP 是必须要掌握的理论。
图片来自 Pexels
But:这个文章的重点并不是讨论 CAP 理论和细节,重点是说说 CAP 在微服务中的开发怎么起到一个指引作用,会通过几个微服务开发的例子说明,尽量的去贴近开发。
CAP 定理又被称为布鲁尔定理,是加州大学计算机科学家埃里克·布鲁尔提出来的猜想,后来被证明成为分布式计算领域公认的定理。
不过布鲁尔在出来 CAP 的时候并没有对 CAP 三者(Consistency,Availability,Partition tolerance)进行详细的定义,所以在网上也出现了不少对 CAP 不同解读的声音。
CAP 定理
在一个分布式系统中(指互相连接并共享数据的节点集合)中,当涉及到读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(Partition Tolerance)三者中的两个,另外一个必须被牺牲。
这个版本的 CAP 理论在探讨分布式系统,更加强调两点是互联和共享数据,其实也是理清楚了第一个版本中三选二的一些缺陷。
分布式系统不一定都存在互联和共享数据,例如 Memcached 集群相互间就没有存在连接和共享数据。
所以 Memcached 集群这类的分布式系统并不在 CAP 理论讨论的范围,而像 MySQL 集群就是互联和数据共享复制,因此 MySQL 集群是属于 CAP 理论讨论的对象。
一致性(Consistency)
一致性意思就是写操作之后进行读操作无论在哪个节点都需要返回写操作的值。
可用性(Availability)
非故障的节点在合理的时间内返回合理的响应。
分区容错性(Partition Tolerance)
当网络出现分区后,系统依然能够继续旅行社职责。
在分布式的环境下,网络无法做到 100% 可靠,有可能出现故障,因此分区是一个必须的选项。
如果选择了 CA 而放弃了 P,若发生分区现象,为了保证 C,系统需要禁止写入,此时就与 A 发生冲突;如果是为了保证 A,则会出现正常的分区可以写入数据,有故障的分区不能写入数据,则与 C 就冲突了。
因此分布式系统理论上不可能选择 CA 架构,而必须选择 CP 或 AP 架构。
分布式事务 BASE 理论
BASE 理论是对 CAP 的延伸和补充,是对 CAP 中的 AP 方案的一个补充,即使在选择 AP 方案的情况下,如何更好的最终达到 C。
BASE 是基本可用,柔性状态,最终一致性三个短语的缩写,核心的思想是即使无法做到强一致性,但应用可以采用适合的方式达到最终一致性。
CAP 在服务中实际的应用例子
理解貌似讲多了,项目的 CAP 可以参考下李运华的《从零开始学架构》的书里面的 21,22 章,比较详细的描绘了 CAP 的理论细节和 CAP 的版本演化过程。
服务注册中心,是选择 CA 还是选择 CP?
服务注册中心解决的问题
在讨论 CAP 之前先明确下服务注册中心主要是解决什么问题:
服务注册:实例将自身服务信息注册到注册中心,这部分信息包括服务的主机 IP 和服务的 Port,以及暴露服务自身状态和访问协议信息等。
服务发现:实例请求注册中心所依赖的服务信息,服务实例通过注册中心,获取到注册到其中的服务实例的信息,通过这些信息去请求它们提供的服务。
Dubbo 的 Zookeeper
Spring Cloud 的 Eureka,Consul
RocketMQ 的 nameServer
HDFS 的 nameNode
Zookeeper 选择 CP
Eureka 选择 AP
ZK 和 Eureka 的数据一致性问题
相对 ZK 来说剔除了 Leader 节点选取和事务日志机制,这样更有利于维护和保证 Eureka 在运行的健壮性。
也有可能存在一些本应该被删除而没被删除的脏数据。
服务注册应该选择 AP 还是 CP
分布式锁,是选择 CA 还是选择 CP?
基于数据库实现分布式锁
基于 Redis 实现分布式锁
基于 Zookeeper 实现分布式锁
基于数据库实现分布式锁
构建表结构:
利用表的 UNIQUE KEY idx_lock(method_lock)作为唯一主键,当进行上锁时进行 Insert 动作,数据库成功录入则以为上锁成功,当数据库报出 Duplicate entry 则表示无法获取该锁。
基于 Redis 实现分布式锁
实现方式:
setnx key value Expire_time
获取到锁 返回 1 , 获取失败 返回 0
为了解决数据库锁的无主从切换的问题,可以选择 Redis 集群,或者是 Sentinel 哨兵模式,实现主从故障转移,当 Master 节点出现故障,哨兵会从 Slave 中选取节点,重新变成新的 Master 节点。
这个时候,一旦主挂掉或者网络抖动等各种原因,可能会切换到“从”节点,这个时候可能会导致两个业务线程同时获取得两把锁。
业务线程 -1 向主节点请求锁
业务线程 -1 获取锁
业务线程 -1 获取到锁并开始执行业务
这个时候 Redis 刚生成的锁在主从之间还未进行同步
Redis 这时候主节点挂掉了
Redis 的从节点升级为主节点
业务线程 -2 想新的主节点请求锁
业务线程 -2 获取到新的主节点返回的锁
业务线程 -2 获取到锁开始执行业务
这个时候业务线程 -1 和业务线程 -2 同时在执行任务
基于 Zookeeper 实现分布式锁
首先 ZK 的模式是 CP 模型,也就是说,当 ZK 锁提供给我们进行访问的时候,在 ZK 集群中能确保这把锁在 ZK 的每一个节点都存在。
①ZK 锁实现的原理
有序节点:当在一个父目录下如 /lock 下创建 有序节点,节点会按照严格的先后顺序创建出自节点 lock000001,lock000002,lock0000003,以此类推,有序节点能严格保证各个自节点按照排序命名生成。
临时节点:客户端建立了一个临时节点,在客户端的会话结束或会话超时,Zookepper 会自动删除该节点 ID。
事件监听:在读取数据时,我们可以对节点设置监听,当节点的数据发生变化(1 节点创建 2 节点删除 3 节点数据变成 4 自节点变成)时,Zookeeper 会通知客户端。
业务线程 -1,业务线程 -2 分别向 ZK 的 /lock 目录下,申请创建有序的临时节点。
业务线程 -1 抢到 /lock0001 的文件,也就是在整个目录下最小序的节点,也就是线程 -1 获取到了锁。
业务线程 -2 只能抢到 /lock0002 的文件,并不是最小序的节点,线程 2 未能获取锁。
业务线程 -1 与 lock0001 建立了连接,并维持了心跳,维持的心跳也就是这把锁的租期。
当业务线程 -1 完成了业务,将释放掉与 ZK 的连接,也就是释放了这把锁。
②ZK 分布式锁的代码实现
ZK 官方提供的客户端并不支持分布式锁的直接实现,我们需要自己写代码去利用 ZK 的这几个特性去进行实现。
究竟该用 CP 还是 AP 的分布式锁
分布式事务,是怎么从 ACID 解脱,投身 CAP/BASE
如果我们追求数据的一致性而忽略可用性这个在微服务中肯定是行不通的,如果我们追求可用性而忽略一致性,那么在一些重要的数据(例如支付,金额)肯定出现漏洞百出,这个也是无法接受。所以我们既要一致性,也要可用性。
实现最终一致性
BASE 模型
分布式事务
两阶段提交(2PC)
补偿事务(TCC)
本地消息表
MQ 事务消息
①两阶段提交(2PC)
事务管理器要求每个涉及到事务的数据库预提交(Precommit)此操作,并反映是否可以提交。
事务协调器要求每个数据库提交数据,或者回滚数据。
②补偿事务(TCC)
TCC 是服务化的两阶段编程模型,每个业务服务都必须实现 Try,Confirm,Cancel 三个方法,这三个方式可以对应到 SQL 事务中 Lock,Commit,Rollback。
一个下订单,生成订单扣库存的例子:
接下来看看,我们的下单扣减库存的流程怎么加入 TCC:
在 Confirm 的时候,会使用在 Try 预留的资源,在 TCC 事务机制中认为,如果在 Try 阶段能正常预留的资源,那么在 Confirm 一定能完整的提交。
实现可以参考:
https://github.com/changmingxie/tcc-transaction
③本地消息表
本地消息表这个方案最初是 eBay 提出的,eBay 的完整方案:
https://queue.acm.org/detail.cfm?id=1394128
本地消息表这种实现方式应该是业界使用最多的,其核心思想是将分布式事务拆分成本地事务进行处理。
当我们去创建订单的时候,我们新增一个本地消息表,把创建订单和扣减库存写入到本地消息表,放在同一个事务(依靠数据库本地事务保证一致性)。
配置一个定时任务去轮询这个本地事务表,扫描这个本地事务表,把没有发送出去的消息,发送给库存服务,当库存服务收到消息后,会进行减库存,并写入服务器的事务表,更新事务表的状态。
库存服务器通过定时任务或直接通知订单服务,订单服务在本地消息表更新状态。
④MQ 事务
RocketMQ 中实现了分布式事务,实际上是对本地消息表的一个封装,将本地消息表移动到了 MQ 内部。
整体交互流程如下图所示:
RocketMQ 选择同步/异步刷盘,同步/异步复制,背后的 CP 和 AP 思考
同步刷盘/异步刷盘
异步刷盘:消息快速写入到内存的 Pagecache,就立马返回写成功状态,当内存的消息累计到一定程度的时候,会触发统一的写磁盘操作。这种方式可以保证大吞吐量,但也存在着消息可能未存入磁盘丢失的风险。
同步刷盘:消息快速写入内存的 Pagecahe,立刻通知刷盘线程进行刷盘,等待刷盘完成之后,唤醒等待的线程,返回消息写成功的状态。
同步复制/异步复制
同步复制:是等 Master 和 Slave 均写成功后才反馈给客户端写成功状态。
异步复制:是只要 Master 写成功即可反馈给客户端写成功状态。
值得一提的是 Lazy+Primary/Copy 的复制协议在实际生产环境中是非常实用的。
总结
可以这么说,只要是分布式,只要是集群都面临着 AP 或者 CP 的选择,但你很贪心的时候,既要一致性又要可用性,那只能对一致性作出一点妥协,也就是引入了 BASE 理论,在业务允许的情况下实现最终一致性。
作者:陈于喆
简介:十余年的开发和架构经验,国内较早一批微服务开发实施者。曾任职国内互联网公司网易和唯品会高级研发工程师,后在创业公司担任技术总监/架构师,目前在洋葱集团任职技术研发副总监。
编辑:陶家龙、孙淑娟
征稿:有投稿、寻求报道意向技术人请联络 editor@51cto.com
精彩文章推荐: