作为一名程序员,你是不是经常在很多场景,例如看博客、聊天吹水等等时候听到这样一个词"系统数据一致性",是不是有时候感觉到了迷糊,不知道这个"系统数据一致性"到底是在说什么?其实,你可能只是不明白这个词,但是你肯定在实际工作中发现、解决过这样的问题。
单体架构下系统数据一致性问题
在传统的系统应用中,一般都是使用单体架构来构建系统的。即所有的功能模块都放在一起实现,打成一个WAR包部署在Tomcat中,数据一般存放在关系型数据库中,如MySQL数据库。
前面我说过即使这种单体架构的系统也是数据一致性的问题的,举一个电商下单的例子,用户提交完订单,系统,系统在订单表order表中写入订单金额、用户等相关数据,在订单明细order_item表中写入商品价格、购买的数量等数据,最后更新商品的库存sku信息。用户下单成功之后,系统操作了order、order_item、sku这三个数据表,对于这三个表的操作无论成功与失败,都应该是原子的,操作成功则都要成功,失败则都要一起失败。不然就会出现脏数据,数据一致性被破坏。
1、 如果操作order和order_item表成功,操作sku表失败,则会导致本应该扣减的库存没有扣减,则商品有可能出现超卖。
2、如果操作order和order_item表失败,操作sku表成功,则会导致本不应该扣减的库存扣减了,则商品有可能出现少卖。
3、如果操作order和sku表成功,order_item操作失败,则这个订单数据丢失,订单后续的操作肯定也是操作不了了。
上面只是简单的举了三种可能出现的情况,也可能会有其他的情况发生。那我们怎么避免这些情况的发生呢?其实这种问题稍微有的开发经验的同学都会想到解决方案,那就是使用数据库的事务,事务的原子性保证上述的步骤成功则一起成功,失败则一起失败。
BEGIN;
INSERT INTO order;
INSERT INTO order_item;
UPDATE sku;
COMMIT; # ROLLBACK
在单体架构的系统下解决内部模块的数据一致性的问题,用数据库的ACID特性就能保证。
单体架构的优点就是相对分布式来说开发简单,功能可以集中管理,模块之间通信没有损耗。但随着业务越来越复杂、需求越来越庞大,人们对系统响应时间、吞吐量和出现故障的时候的系统可用性的要求也越来越高!传统的单体架构系统在这种情况下暴露的缺点也越来越多,人们开始寻求转变。既然部署在一个服务器上的单体架构系统搞不定,那就多部署几台,即用多台单机节点组成集群,再用负载均衡向外提供服务。
但是这样做还是解决不了单体架构存在的一些问题:
- 只能使用同种语言开发,不能针对不同业务场景利用不同语言的优势开发对应的模块。
- 系统模块耦合性太强,系统中某一个模块出现问题,例如高并发、大数据场景或者出现bug,整个系统都会受到牵连。
- 某个模块发布,整个系统都要停机发布,系统所有模块都不能对外提供服务,这样无法快速响应市场需求。
- 集群负担大,如果想要集群,只能对整个系统进行集群,即使只有一个模块有压力。
集群(Cluster): 系统单机部署对外服务能力出现瓶颈,则将系统进行多机部署,这些系统对外提供相同的服务,每个单机系统我们称之为节点,多个节点统一起来则可以称之为集群。
分布式架构下系统数据一致性问题
天下大事分久必合、合久必分!既然单体架构解决不了问题,那我们就尝试拆分系统,让专业的人做专业的事,那如何进行拆分呢?拆分一般分为水平拆分和垂直拆分。这里说的拆分并不单指数据库拆分,而是所有模块都进行拆分,每个模块都有自己的缓存、数据库等等。
- 水平拆分指的是单一的节点无法满足性能的需求,需要进行数量上的扩展。每一个节点都具有相同的功能,每一个节点都负责一部分请求,节点们组成一个集群,对外进行提供服务。
- 垂直拆分指的是按照功能进行拆分,秉着"专业的人干专业的事",把复杂的系统拆分成各个模块。模块之间通过RPC进行通信,可以做到高内聚、低耦合,每个模块独立部署和维护,可以快速迭代响应市场需求。
因此,分布式架构在这种背景下应运而生。
分布式(Distributed)架构:分布式系统是由集中式系统逐渐演变而来。所谓的集中式系统,就是把系统中所用的功能都集中到一起,从而向外提供服务的单体应用。
软件行业是没有银弹的,每一个被发明出来的新技术,都是一把双刃剑,都是在特定的领域解决了某些老问题,但是同时也会带来新的问题。那么微服务这种分布式架构解决了什么老问题?同时它又带来了哪些新问题呢?
解决了老问题
微服务这种分布式架构主要解决了单体架构存在的一些问题。
- 各个服务可以使用不同的语言开发,可以利用不同语言的优势开发不同模块。
- 服务之间可以做到高内聚、低耦合。每个服务可以独立维护、部署,可以快速响应市场需求。
- 可以单独对某个有高并发、大流量的服务单独进行优化,不浪费资源。
带来了新问题
- 系统的监控难度加大。
- 数据的一致性成为问题。
- 系统的复杂度提高,系统的维护、设计成本增加,调试、纠错难度加大。
新问题中的 数据一致性问题 才是本文接下来的重点。
为啥会有这个数据一致性问题呢?
单体架构按照文中的说法,是一种不太时髦的架构方式,都能轻松解决数据一致性问题,新发明的分布式架构却又成了一个棘手的问题,这个到底是技术的进步还是技术在退步呢?哈哈(我的一点点吐槽)!!接下来我来解释一下为啥分布式系统会有这样的问题。 分布式系统每个功能大都部署在不同的服务器上,部署在不同国家和地区的服务器中,部署在不同的网络中,部署在不同国家和地区的网络中。这样一个需要大量的服务器共同协作,向外提供服务的系统,面临着诸多的挑战:
- 良莠不齐的服务器和系统能力
分布式系统中的服务器,可能配置不一样,其上部署的系统可能也是由不同的程序语言、架构实现,因此处理请求的能力也就不一样。
- 不可靠的网络
如上文所说,系统中各个服务可能部署在不同国家和地区,各个服务通过网络进行通信,但是网络是不可靠的。网络经常会出现抖动、延时、分割、丢包等问题。 网络通信中最让人头痛的是因为网络抖动、延时等问题导致系统之间的通信出现超时:A服务向B服务发出请求,A服务没有在约定的时间内接受到B服务的响应,你不能确定B服务到底有没有处理完A服务的请求,这样的不确定性就需要我们进行重试处理,那么B服务就要解决请求幂等性问题。
服务器的机房发生火灾、断电等事故。 支付宝出现过服务器的电缆被挖断的问题。
- 普遍存在的单点故障
分布式系统为了保证故障发生的时候,系统仍然保证可用,每个模块都采用集群部署。单个节点的故障概率较低,但是节点数量达到一定规模时,系统中的节点出现故障的概率可能就变高了。
分布式系统就是这样一些处在不同区域、有着不同能力和拥有单一功能的服务组成,他们通力合作才能向外提供服务,那如何保证他们的状态、信息一致并且协调有序就成了一个难题。
分布式系统就是要解决解决集中式的单体架构系统的各种缺陷,实现整个系统的 高性能 、高可用、可扩展,但是要实现这三个目标并不容易,将系统进行拆分的过程中会出现上文中说到的问题,为了解决这些问题,诞生了很多关于分布式的基本理论,比如CAP、BASE等等。
分布式架构有很多相关的理论和算法,这里我只说了CAP、BASE理论,其他诸如Paxos算法、Raft算法、ZAB协议等等,这些大家自己找资料看看吧!
我们先来说说CAP理论
这个CAP理论相信很多人都听说过,下面请允许我写下教科书般的理论内容:
CAP原则又称CAP定理,指的是在一个分布式系统中,Consistency(一致性)、 Availability(可用性)、Partition tolerance(分区容错性),三者不可得兼。
一致性(C):在分布式系统中的所有数据备份,在同一时刻是否同样的值,也就是等同于所有节点访问同一份最新的数据副本。 可用性(A):保证每个请求不管成功或者失败都有响应,即使数据不是最新的。 分区容忍性(P):系统中的某个节点或者网络分区出现了故障的时候,整个系统仍然能对外提供的服务。
什么情况?这个CAP理论上来就给出三个概念或者说是指标,还说分布式系统只能满足上面两个指标。大家是不是经常听到CAP理论,但是却又不是很理解为什么CAP三个指标只能满足其中的两个,那么接下来我给大家解释一下:
就如前面的“分布式架构图”展示的一样,系统一个对外的服务涉及到多个节点通讯和交互,节点所处的网络发生分区故障的问题又无法避免,所以分布式系统中分区容错性必须要考虑,那么系统自然也不可能同时满足上面说的三个指标。
分布式系统中CAP如何抉择?
在分布式系统内,各种因素导致分区是必然的会发生的,不考虑分区容忍性(P),一旦发生分区错误,整个分布式系统就完全无法使用了,这其实和最开始的单体应用一样有单点问题,这样的系统是和分布式架构理论是相违背的,同时也是不符合实际需要的。所以,对于分布式系统,我们只能能考虑当发生分区错误时,如何选择一致性(C)和可用性(A)。
根据一致性和可用性的选择不同,开源的分布式系统往往又被分为 CP 系统和 AP 系统。 当系统在发生分区故障后,客户端的任何请求都被阻塞或者超时,但是,系统的每个节点总是会返回一致的数据,则这样的系统就是 CP 系统,经典的比如 Zookeeper。 当系统发生分区故障后,客户端依然可以访问系统,但是获取的数据是不一致的,有的是新的数据,有的还是老数据,那么这样系统就是 AP 系统,经典的比如 Eureka。
前面说分布式系统不考虑分区容忍性(P)为啥分区错误发生,系统就不能用了,这里我再解释一下: 不考虑分区容忍性(P),那就是选择CA。假设发生了分区错误,系统由于可用性(A)的要求,即使系统发生分区故障也要提供服务,那系统就仍然向外提供服务,因为服务肯定的包含对数据的读取、写入、更新、删除,可是由于一致性(C)的要求,系统中所有的节点数据都要保持一致,因为分区错误发生,节点的数据同步肯定无法进行,数据副本的一致性就无法保证,那就不能像对外提供服务。那这样CA就相互矛盾,系统无法保证可用性(A)和一致性(C),系统自然是不能使用了,也就是说没有选择CA的分布式系统,而且这分区容忍性(P)必须要考虑!而且,不是一个系统选择了可用性(A)或者一致性(C),可以是其中的模块选择了可用性(A)和一致性(C)
Zookeeper常常有人用它作为dubbo的注册中心,Eureka作为Spring Cloud体系中的注册中心,其实对于注册中心角色来说,我觉得Eureka比Zookeeper更适合!
还有一点这里我说一下,其实大部分情况下分布式系统是没有问题的,C和A两个指标都是同时满足的,只是在分区问题发生的情况下,才需要我们考虑到底是选择C还是A。
前文说到解决单点故障的问题,我们引入了集群。在分布式系统中我们为了提高系统的可用性,也是不可避免的使用副本的机制,引入了副本则就需要同步数据到不同的副本,从而引发了副本一致性的问题。就如前面展示的“分布式架构图”中,会员、订单和产品服务都是独立部署且分别使用不同的数据库,每个服务内部又是使用数据库集群,数据在服务与服务之间、在某个服务的数据库集群中间等等的流转、同步,这些过程都是有网络、时间消耗的,一个数据从最开始的产生到它应该到的地方不会瞬时完成,而CAP理论是基于瞬时,在同一时刻任意节点都保持着最新的数据副本,它是忽略网络延迟、节点处理数据的速度的,这个在目前的技术下是不可能做到的,从这个角度来看,CAP理论实在是乐观主义了。
CAP理论的缺点是什么?
CAP理论其实是有缺点的,前文也提到一些,具体的缺点如下:
- 理论忽略网络延迟、节点处理数据的速度
CAP的理论的作者布鲁尔在定义一致性时,并没有将上述的问题考虑进去。即当事务提交之后,数据能够瞬间复制到所有节点。但实际情况下,数据从产生到复制到各个服务、各个节点,总是需要花费一定时间的。如果在相同机房可能是几毫秒,如果跨地域、跨机房,可能是几十毫秒甚至是一百多毫秒。这也就是说,CAP理论中的C在实践中是不可能完美实现的,在数据副本的同步的过程中,节点之间的数据在一个短时间内并不一致。
- 理论中的一致性是强一致性
CAP理论中的一致性的概念是,在分布式系统中的所有数据备份,在同一时刻是否同样的值,也就是等同于所有节点访问同一份最新的数据副本。在某些场景下这种强一致性要求并不是那么高。在一个日志搜集系统,在高并发、大数据的情况下,一条日志写入需要稍后一会才能在ELK中展示出来,这样是没有问题的。通过牺牲强一致性获得可用性,在一定时间之后最终数据达成一致性即可。
- 理论中的指标的选择和放弃并不是三选二的关系
CAP理论告诉我们三者只能取两个,需要放弃另外一个,这里的放弃是有一定误导作用的,因为“放弃”让很多人理解成什么也不做。实际上,CAP理论的“放弃”只是说在系统分区错误过程中,我们无法同时保证C和A,但并不意味着什么都不做。分区期间放弃C或者A,并不意味着永远放弃C和A,我们可以在分区期间进行一些操作,从而让分区故障解决后,系统能够重新达到CA的状态。最典型的就是主从数据库中主数据挂了,后面进行修复,使得重新达到CA状态。
CAP理论的改进版BASE理论
由于CAP理论在定义时过于的乐观,导致他有些缺陷,于是又有大神改进了CAP理论,从而引申出理论改进版本:BASE理论。eBay的架构师Dan Pritchett根据他自身在大规模分布式系统的实践经验,提出了BASE理论。BASE理论是对CAP理论的延伸和补充,它满足CAP理论,通过牺牲强一致性获得可用性,在一定的时间窗口内,达到数据的最终一致性。
BASE理论模型包含如下三个元素:
- BA:Basically Available,基本可用。
- S:Soft State,软状态,状态可以在一定时间内不同步。
- E:Eventually Consistent,最终一致性,在一定的时间窗口内,最终数据达成一致即可。
Basically Available 基本可用
BASE理论中的Basically Available 基本可用,就是系统在出现问题的时候,牺牲一部分的功能,来保障核心功能正常。这其实就是一种妥协,相当于壁虎断臂求生。 就像前几年的双十一淘宝,订单支付、退款直接崩掉了,后面就进行改进限流需要你多试几次才能付款、退款,再后来双十一那几天是不能申请退款的,直接就把你这个功能给关闭了,相当于服务熔断了。这就是牺牲非核心的功能,将所有的资源都用来保障核心的支付功能。
Soft State,软状态
允许系统在一定时间内的状态不同步,允许系统处于软状态,这个软状态其实就是中间状态。比如采用分布式架构的电商系统,用户下单完成并付款,是否支付成功,是支付系统完成的,订单系统不会等支付系统返回是否支付成功再把结果返回给客户的,而是先把订单状态设置为付款中,返回给客户,然后支付系统收到异步通知确定支付成功成功,再把状态设置为付款完成,再把付款完成信息推送给订单系统。这样,就可以提高系统的响应速度。即使这支付系统出现故障宕机了,系统重启之后可以通过定时任务补偿处理未完成的数据,然后根据数据所处的状态进行补偿处理,最终完成数据处理。付款中这个状态,就是软状态即中间状态。
Eventually Consistent,最终一致性
数据不会一直处在中间状态,就如上面的例子所说,处于中间状态的数据会有采用类似定时任务一样的补偿处理,将数据修复成正确的状态,最终数据达成一致。
重说“带来了新问题”
前文说到分布式架构解决了单体架构的一些问题,但是同时也带来了一些新的问题,这里我们着重说一下,本来不是大问题的“数据一致性”问题。前面举了一个电商系统中的经典案例:下订单与扣库存。单体架构的应用我们直接用数据库事务的ACID特性就可解决,但是采用分布式架构的系统就没有那么好解决了,我们先说一下在分布式架构下的系统是如何完成“下订单与扣库存”的,这里就不画图了直接用伪代码来展示:
public void buildOrder(OrderDto orderDto) {
// 1.保存订单
orderService.saveOrder(orderDto);
// 2.扣除产品库存
inventoryService.deductInventory(orderDto);
}
这步骤一保存订单是在订单系统中执行,步骤二扣除产品库存是在库存系统执行,这个下订单与扣库存两个步骤分别涉及到了两个系统,使用RPC的方式和两个系统进行交互。由于这两个步骤不是原子的,不能保持一致的话会导致很多的问题:
- 比如先保存订单成功,然后扣除产品库存失败,那订单就要回滚处理;
- 如果先扣除产品库存成功,然后保存订单失败,那库存就要回滚;
- 或者说先保存订单然后扣除产品库存时请求超时,其实库存已经扣除成功等等问题。
这些问题你不解决,就有可能导致产品多卖或者是产品出现少卖,不管出现哪个都会造成资损或者客诉,任何一种情况都不是我们想发生的。
由于库存系统和订单系统分别使用各自的数据库,那原先使用数据库事务的ACID特性保证数据的一致性就不能奏效了,分布式架构的系统就产生了数据一致性的问题,这种跨多个数据库的事务问题,其实就是分布式事务问题。要解决分布式架构的系统的数据一致性问题,其实就是解决分布式事务的问题。
目前业界也出现了很多分布式事务的解决方案,例如两阶段提交2PC、三阶段提交3PC、TCC还有基于可靠消息等方案,他们用不同的方案实现分布式事务,解决数据一致性的问题,这里就不再详述。
基于可靠消息解决分布式事务,解决数据一致性的问题,其中一种方案叫做本地消息表,这种方案名称大家可能不知道,但是你很有可能这样做过,这个后面的文章再细说。
作者:山姆刘
链接:https://juejin.cn/post/6929795131042168846
来源:掘金