定义

C:Consistency(一致性)
A:Avaliable(可用性)
P:Partition Tolerate(分区容忍性)

理论:任何一个分布式系统,不能同时满足上述的三个特性。

分类

CA系统:所有单机系统
AP系统:zookeeper、redis、kafka
CP系统:hadoop、mysql

理解

  1. 单机系统由于数据都存在同一个数据库,所以通过RDB自带的事务功能即可以实现CA特性。
  2. 由于分布式系统的数据分散在各个子系统,且他们之间进行数据交换,但由于网络传输的不可靠性,所以分布式系统天然满足P。
  3. 所以理论上描述的不能同时满足三个,其实也可以说是不能同时满足可用性和一致性。

场景

用户发起请求,该请求需要A和B两个子系统协作完成,且A和B存在数据分区。如果A和B之间的网络异常,那么如果A完成分区内的处理后调用B系统,此时B系统无法响应,那么A系统有两种选择:

  • 第一种是:等,如果调用超时则继续发起请求后直到网络恢复。
  • 第二种是:调用超时后抛出异常放弃等待,并返回用户成功。

可用性

第一种会导致线程挂起,假设这种请求很多,那么系统的的处理线程池被耗尽,从而导致系统无法响应任何请求,造成可用性问题。(服务维度)

  1. 如何保障系统可用性?
    1. 导致系统不可用的两个原因:服务器宕机、系统性能不足。
    2. 对应的解决方案是:防单点,系统拆分。
  2. 怎么备份?
    1. 一个系统通常来讲分为两个要素:服务和数据
    2. 服务的高可用很简单,因为服务本身是无状态的,所以可以从两个角度来看。
      1. 从运维角度,集群化部署(防单机单点,比如服务器宕机)、多机房部署(防机房单点,比如光缆被挖断)、异地部署(防城市单点,比如城市地震)等方案既可以提高性能,又能互为热备份,一举两得。
      2. 从业务角度,防止服务单点,比如场景A:运营后台有运营人员大量上传大文件hang住了线程,导致处理C端业务的线程不足,系统响应时长飙高,再比如场景B:信息流业务由于代码BUG大量报错触发限流保护,导致会员业务的流量下跌。这些都是单服务系统的弊端,通过微服务拆分,可以有效避免不同业务之间的影响,提高整个系统的SLA。
      3. 两者是可以并行设计的,也就是现在业内大量采用的微服务化+集群+多机房+异地部署方式。
    3. 数据备份更加复杂, 因为数据是有状态的。
      1. 那什么叫有状态,什么叫无状态呢?简单来讲就是多一个你或少一个,不会破坏整个系统的完整性。同理,集群里多一台服务器少一台服务器不会对系统产生影响,但是多一份或少一分数据则会造成系统数据异常。
      2. 我们以最简单的mysql单库存储数据为例,通过子线程不断pull主库的binlog在备库进行replay实现数据备份,存在的问题是
        1. 主备存在延时同步的问题,一旦主库宕机,备库存在延时段内的数据丢失。
        2. 备库如果作为冷备,存在主库存在性能瓶颈,备注存在资源浪费,如果作为只读热备,存在延时段内的数据不一致。
      3. 双写主库,一个请求同时写入两个库,可以保证数据一致,但带来新的问题。
        1. 同步双写且保证(分布式)事务,存在性能问题。
        2. 同步双写不保证事务,存在性能问题,且数据弱一致。
        3. 异步事务双写(事务消息,如rocketmq),数据最终一致。
        4. 异步双写(普通消息如kafka),数据弱一致性。性能优于c。
        5. 作为互联网应用,db作为系统的最底层的性能一定要得到保障,故个人认为ab方案不考虑,cd方案基于系统对数据一致性级别的要求来考虑进行选择。
      4. 上述的备份方案是用于解决单点问题的,和服务一样,性能问题就依赖于分布式(由于是有状态的,所以数据库部署没有集群的概念),也就是以前经常提到的大表纵向或横向拆分的问题。
        1. 为什么横向纵向拆分能提高性能?因为单库里的表少了,单表里的行数少了,单行里的字段少了,这意味着mysql的索引需要遍历的数据少了,同时mysql的buffer-pool里面的可以缓存的数据换出少了命中率提高了,这些变化既能减少操作系统的磁盘寻址时间消耗,也能减少cpu算力的消耗。
        2. 数据纵向拆分,就是微服务化拆分天然实现了该要求。
        3. 数据横向拆分,就是现在业内采用的分布式数据库,比如网易杭研的DDB,阿里的OB,一般实现方式是通过软件的方式,将多个mysql实例节点进行组合(单个节点的高可用依然是上述方式)。
          1. 写入时通过基于数据id进行取模散列到各个节点上,所以每个表都会有一个sharding-id。
          2. 读取时如果带上sharding-id查询,则可以直接获取到对应的mysql节点,性能较好,如果不存在sharding-id,则遍历所有节点,收集到所有节点的数据,在内存中通过软件进行结果组装再返回,性能较差。

一致性

第二种会让用户以为请求完成,如果用户后续的操作会查询或基于B系统的数据,那么B系统会因为没有数据而导致服务异常,从而造成一致性问题。(数据角度)

  1. 或许有人会问:
    1. 为什么返回的时候不告诉用户失败了?
      1. A系统其实是成功的,并没有全部失败。
    2. 为什么不告诉用户A系统成功B系统失败了?
      1. 用户理解不了A或B系统的概念,他也不理解哪些数据会放在A系统哪些数据放在B系统。
    3. 为什么B系统失败后,A系统不回滚,然后告诉用户失败了?
      1. A系统回滚不了,因为调用B系统异常不会放在A系统的事务中。
    4. 那B系统的调用放在A系统本地事务中不就可以了?
      1. 跨系统调用耗时较长,放在本地事务中会严重拖慢本地事务,影响整个系统的效率,严重可能导致数据库链接池耗尽。
      2. 另外就算不考虑上述原因,考虑另外一个情况:如果A系统调用B系统后再进行数据变更,调用B系统成功,但是本地数据变更失败,也导致数据不一致,只是情况系统相反而已。
      3. 再考虑,如果不止AB系统,存在更多系统的之间数据分区的时候,在单分区事务内进行调用,都是没办法保证一致性的。
  2. 那如何解决一致性问题?首先这么考虑,这个世界并不是非黑即白的,存在灰色的解决方案,社会也是这样,当官的也不是要么领袖清风,要么巨贪,不扯远。首先要知道一个事实:网络不可靠的概率其实是极低的,可能是在5到6个9的可靠级别,但如果经常存在网络问题,就是公司的机房搭建或者选择云服务提供商存在问题了。
  3. 在绝大部分系统中,都会优先保证A而不是C。作为一个系统开发者,其实不需要花太多精力去考虑因为网络不稳定而带来的数据一致性问题,而应该花更多精力来保证避免因为系统分析不足、领域设计不合理,或纯粹因为代码BUG导致的数据不一致。也就是说,如果碰到跨分区调用(RPC)失败,做好如下节点:异常抛出->日志打印->报警配置->收到报警->查询日志->业务判断->数据订正,其实就可以了。也就是弱一致性
  4. 如果业务领域合适,且对技术有追求的话,可以考虑下本地事务表,事务消息等方式来保证最终一致性
    1. 本地事务表就是通过在业务库内创建一个任务表,将需要跨分区调用的所有信息,包括系统名、服务名、参数,都存储下来,这样,分布式事务就变为本地事务,只要保证任务表插入成功。然后通过调度任务系统执行。这种方式需要在各个系统内维护一张任务表并配置任务调度,且方案适用于各个系统间的调用无顺序要求(任务表可以设计层支持顺序的能力,但会增加表和调度的复杂度,不建议这么做)以及数据依赖。
    2. 通过支持事务的消息中间件,比如rocketmq、rabbitmq等,同上述任务表一样,将调用元信息封装成消息进行发送,支持事务的消息中间件可以保证本地数据和插入消息的之间的一致性。这种方式需要引入支持事务的消息中间件,增加了系统复杂度。同理,这种方式也只适用于各个系统间的调用无顺序要求(有的消息中间件也可以保证顺序,但性能不如无序消费,更建议无序)以及数据依赖。
  5. 但是对于涉及资金、账务、库存的系统,C的优先级则会高于A,但也不是相关系统中所有接口都是如此,一般也就是系统中几个增加、扣除的核心接口才需要强一致性保证。这时候需要用到分布式事务,分布式事务一般采用两阶段提交(2PC)的思想来实现:第一阶段事务协调者询问各个子系统是否可以发起事务,子系统进行资源预留并响应协调者,协调者如果根据响应结果发起第二阶段执行或回滚(全部响应OK则执行,有任何一个系统响应不OK(包括响应不OK和不响应)那么事务失败,并让已OK的系统回滚)。3PC则是在2PC的第一阶段之前上增加一个检查对方是否在线的阶段,降低2PC第一阶段因为不响应而导致事务失败的概率,为什么这么做呢?因为资源预留是需要成本的,比如10个系统,9个系统都预留资源并返回OK了第10个系统不响应导致全部回滚,这是非常划不来的,所以有了3PC的概念。
    1. 可能有人会问:网络传输的不可靠性对传输的双方都有响应啊,为什么我们在考虑网络不可靠的时候只考虑对方响应时不可靠,而不考虑我方发出请求时不可靠呢?也就是在2PC中协调者发起事务请求或者通知子系统确认或回滚请求的时候失败怎么办?以前我也一直搞不懂这个点,后来就理解了,因为如果我方发请求失败,我方是可以感知到的,那么就有很多方式应对,可以提示用户失败手动重试,或系统自动重试,也就是说不管怎么样我方发请求失败后是可控的,而对方响应失败,我方是不可控的,因为我方不知道对方到底有没有完成我们请求从而无法进行后续处理。打个比方,你向暗恋很久的小红表白,如果你的表白对方没收到,你自己肯定是知道的,所以可以选择再次表白或者放弃表白,但是如果你用钉钉表白后对方都已读了,并且几秒钟后你看屏幕上面也显示对方正在输入…,但这时候阿里的钉钉服务器挂掉了,那么你就不知道她的发的回复是啥,你们下次见面的时候,你是应该叫他女票呢,还是叫她小红同学呢?毕竟不管叫哪个叫错了对方都会生气。
    2. TCC是实现2PC的一个常用的方案,也就是try-confirm-cancel的缩写,但系统要应用TCC是需要进行业务改造或者在业务开发的时候就需要进行相应设计的,所以成本是很大。它需要相关为相关资源设计预留方案,比如资金、库存表需要有一个预留字段,比如当需要进行扣除100块钱的业务时:
      1. 初始状态:金额=1000,金额预留=0
      2. 发起事务也就是try阶段,需要将金额预留下来,也就是改为金额=900,金额预留=100
      3. 如果其他系统也都OK,那么confirm阶段,将预留金额扣除,金额=900,金额预留=0
      4. 如果其他系统不ok,那么cancel阶段,将预留金额恢复,金额=1000,金额预留=0。
      5. 也就是说支持TCC的分布式事务,需要三块工作内容
        • 表设计需要增加预留字段。
        • 开发协调者类。
        • 将扣钱接口改造为try-confirm-cancel三个接口。
  6. 综上,对数据一致性C的保障方案上,成本和效果是相对的,人工介入->本地事务表->事务消息->分布式事务,这些方式并无优劣之分,只是不同系统不同场景下的不同选择而且。