今天啊,本片博客我们一起来学习一下微服务中的一个重点和难点知识:分布式事务。
我们会基于Seata 这个框架来学习。
1、分布式事务问题
事务,我们应该比较了解,我们知道所有的事务,都必须要满足ACID的原则。也就是
在我们以前所学习的单体架构当中的这个服务直接访问一个数据库,业务比较简单。基于数据库本身的特性,就已经能够实现ACID了。
但是,我们现在要研究的是微服务。微服务的业务往往比较复杂,可能一个业务啊,就会跨越多个服务,而每个服务又会有自己的数据库。
这个时候你再靠数据库本身的特性,还能保证整个业务的ACID吗?这可就不一定了。
我们来看一个例子啊。
比方说我这里有一个微服务,它里边啊包含三个服务啊,有订单有账户和库存。
现在,我们有一个用户下单的业务,用户下单时,我希望订单服务去创建订单,并且写入数据库。
然后,他再去调用账户服务和库存服务,账户服务呢,去扣减用户的余额,而库存服务呢,则去扣减商品库存。
那你可以看到这个业务里边就包含了三个不同的微服务的调用,而每个微服务都有自己独立的数据库,也就是独立的事务。
那我们最终希望的肯定是我这个下单业务一旦执行,每一个是不是都要成功?当然,如果你失败,是不是大家都失败?
1.1 演示分布式事务问题
结果是这样一个的结果,但是能不能达到这样一个效果?我们来验证一下。
微服务结构如下:
其中:
seata-demo:父工程,负责管理项目依赖
- account-service:账户服务,负责管理用户的资金账户。提供扣减余额的接口
- storage-service:库存服务,负责管理商品库存。提供扣减库存的接口
- order-service:订单服务,负责管理订单。创建订单时,需要调用account-service和storage-service
我们先来看一下这个order(订单服务)。看下它里边的业务逻辑啊,在controller里边啊我们可以看到一个创建订单的接口。
在controller当中呢,它就调了service,所以我们进去入service方法。
在这个service方法当中 ,我们可以看到它先去创建订单,把这个订单数据直接写到数据库当中。
那创建完订单了就去调用accountClient (扣用户余额)和storageClient(扣库存)完成扣余额和扣库存。
accountClient 和 storageClient 是通过 Feign调用的。
这是这三张表的数据。
业务逻辑就是这样的了,我们接下来使用Postman去做一下测试。
我们发送请求。
返回了500,我们看一下数据库。
发现用户余额少了200。
订单并没有创建
库存也没有减少。
这个时候事务的状态是一致的吗?不是吧?
那为什么会这样子呢?
那在刚才的业务当中,我们的订单服务去创建了订单。
然后去调用账务服务和库存服务,完成余额扣减和库存扣减。
其中,订单和账务服务啊,都执行成功了。
而库存服务在执行的时候却因为库存不足而报错了。
那按照理论上讲,这里一报错,前边是不是都应该跟着回滚啊?但是我们所看到的结果是库存服务失败了,账户的余额该扣还是扣了?为什么呢?
第一,我们的每一个服务都是独立的。
那现在库存服务你抛了异常。
账户服务它知不知道?它是不知道的呀!
那我都不知道你抛异常了,我去回滚什么呢?
第二,每一个服务是独立的,所以他们的事务呢,也是独立的。
那现在我订单服务和账务服务,我执行完业务以后,我事务结束了,我是不是直接就提交了呀?
现在你让我回滚,我怎么回滚?我事都办完了,提交了,就撤销不了了。
所以最终就没有达成事务状态的一致,那么这个时候啊,就出现了分布式事务的问题了!
1.2 什么是分布式事务
那我们总结一下什么是分布式事务。
在分布式系统下一个业务,它跨越了多个服务和数据源。每一个服务都可以认为是一个分支事务,而我们要保证的是所有分支事务最终状态一致。
要么大家都成功,要么大家都失败。那么这样的一种事务就是分布式事务了。
那为什么分布式事务出现了问题?
就是因为各个服务之间,或者说各个分支事务之间互相是感知不到的,大家各提交各的,那将来呀,就无法去回滚,就导致状态无法一致。
2、理论基础
接下来我们就进入分布式事务理论基础的学习。
解决分布式事务问题,需要一些分布式系统的基础知识作为理论指导。
2.1 CAP定理
CAP定理是1998年啊,加州大学的计算机科学家Eric Brewer 提出的,那它说分布式系统里边往往有三个指标。
分别是:
- Consistency(一致性)
- Availability(可用性)
- Partition tolerance (分区容错性)
那艾瑞克说呢,这三个指标啊,分布式系统它不可能同时满足。
你像这三个圆,不就是分别三个特性吗?
但是你看这三个圆不会出现三个同时重叠啊,最多就是两两重叠。
那么这个结论就叫CAP定理了。那为什么会出现这样一个情况呢?
我们必须得先弄清楚啊,一致性,可用性和分区容错性代表的含义才能理解这个其中含义。
2.1.1.一致性
那我们先来看一下其中的第一个,一致性。
一致性是说用户在访问分布式系统中的任意节点时得到的数据必须是一致的。比方说我现在有两个结点。
那第一个结点上面有一个数据叫data值是v0,第二个结点上也是如此。那它们其实就形成了一个什么呢?主从。
现在用户去访问这两个节点啊,不管访问的是谁,拿到结果是不是都一样的?
但是呢,现在如果我对node 1节点的数据发生了修改。
这个时候两个节点数据是不是就不一样了?
那为了满足一致性,你要做什么?
你是不是一定要把node 01的数据同步给node 02啊?
一旦两者的数据同步完成,数据是不是再次一致了?所以呢,作为一个分布式系统。
在做数据备份的时候,你一定要及时的去完成数据同步,这样才能满足一致性。
2.1.2.可用性
第二个概念可用性。
它说用户在访问集群中的任意健康节点的时候啊,必须得到响应,而不是超时或者拒绝。
比方说我现在这个集群里就有三个节点啊。
正常情况下用户访问任何一个是不是都没问题。
现在啊,这三个节点没有出现宕机的情况。但是不知道什么原因啊,比如说这个node3,它的请求就会被阻塞或者拒绝了。
那所有请求进来根本就无法访问了。这个时候node3不就不可用了。所以可用性是指这个节点能不能被正常的访问。
2.1.3 分区容错
第三个概念 :分区容错。
分区是指因为网络的故障或者其他原因导致分布式系统中的部分节点与其他节点失去了连接,形成独立分区。
比方说还是这三个节点啊node 123。
然后用户呢,可以访问其中任意一个。
但是因为网络出现了故障,机器没有挂,然后node 3与node 1和node 2之间断开了连接。
node 1,node 2,正常访问啊,它们之间是能够感知到对方的,但node 3感知不到了。所以呢,此时整个集群就会被划分成两个区了。
那node1和node2它们俩是一个区,node3自己是一个分区。
这个时候如果有用户向node 02写入了一个新的数据。
那node 02是可以把数据同步给node 01的。
但是那么node 3上面有没有同步?
显然没有,因为他们感知不到了,怎么去做同步?
那这两个分区数据就不一致了。
那分区容错是什么意思呢?
容错就是不管集群有没有出现分区啊,整个系统也要持续对外提供服务。
2.1.4.矛盾
那也就是说,尽管你这儿分区了,那用户该访问是不是还要访问呢?
但是如果我现在去访问node1,我拿到的结果和访问node3拿到结果一样吗?不一样。
所以出现了数据不一致的情况,没有满足一致性。那如果我一定要满足一致性。
我应该怎么办?那我是不是可以这么做?
我让node 3它等待node 2这个网络的恢复和数据的同步。
在恢复之前。所有来访问我的请求,我都阻塞在这里,说你们等等我这数据还没好。
可不可以?
那如果这么做,我是不是就能够满足数据的一致性了?但是你的node3明明是一个健康的节点,结果进来的请求你都卡在这里,不让人家访问了。
那node3不就变成不可用了吗?所以它就不满足可用性了。
所以你现在就会发现当网络出现分区时,可用性和一致性是不是没有办法同时满足?但是呢,这个分区它又是不可避免的。
为什么这么说呢?
只要你是一个分布式系统,你的节点之间是不是一定是通过网络连接的?而你只要是通过网络连接的,你有没有办法保证网络100%永远是健康的?
这不可能吧?
所以我们可以认为凡是分布式系统分区一定会出现,既然分区一定会出现。
而你整个集群,又必须要对外提供服务,那也就认为.
Partition tolerance (分区容错性)一 定要实现,那么这个时候Consistency(一致性)和 Availability(可用性)之间,你是不是要做出抉择了?你要么Consistency,要么Availability。没有办法同时满足啊,
这也就是CAP 定理的一个原因了。
2.2 BASE理论
我们已经学习了CAP 的定理,我们知道在分布式系统下,因为分区不可避免,所以你不得不在一致性和可用性之间做出一个选择,但是这两个特性啊,其实都非常的重要,我一个都不想放弃,那我该怎么办呢?
好那么 BASE 理论正好可以解决这个问题.
BASE 理论它是对CAP 的一种解决的思路。其实呢,主要包含了三个思想:
- Basically Available (基本可用):分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
- **Soft State(软状态):**在一定时间内,允许出现中间状态,比如临时的不一致状态。
- Eventually Consistent(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致。
其实BASE理论正是对CAP里边,这种Consistency(一致性)和 Availability(可用性)的矛盾去做一种调和和选择。
那在CAP里边你要达到了一致性,你就要牺牲可用性,但是在BASE 里我们如果达到了强的一致性,你是要牺牲可用性,但不是不可用,而是部分可用性的一个牺牲或者临时的不可用。
2.3.解决分布式事务的思路
说了这么多的思想,它能不能去解决我们分布式事务的问题呢?
其实是可以的。那我们分布式事务里边出现了什么问题呢?
分布式事务当中往往包含n个子事务,每个事务各自执行和提交。结果呢,有些成功,有些失败了,这个时候大家的状态不一致。
而我们希望的就是这个分布式事务里边的每个子事务,大家最终状态一定要一致,要么都成功,要么都失败。
那我们基于base理论怎么样去解决这分布式事务呢?
第一种解决方案其实就是基于AP的模式。
AP模式:各子事务分别执行和提交,允许出现结果不一致,然后采用弥补措施恢复数据即可,实现最终一致。
也就是满足可用性,牺牲一定的一致性。比如说我们各个子事务,将来我们执行的时候分别去执行和提交,那有些成功有些失败了,那这叫什么?
这叫状态不一致。
也就是说你处于一个什么?
软状态了。
临时的不一致状态,没关系,为什么呢?执行完了以后我们各个子事务可以通个气。
互相看一看哎,你成功了吗?哦,我成功了呃,你成功了吗?哎,这么一对比发现有人失败了怎么办?
这个时候别着急啊,我们采取一个弥补的措施去恢复数据啊,那有的说已经提交了,没法恢复了呀,那我们可以做反向的操作呀,比如说你之前新增了一个对不对?
那我接下来我把它删了不就完了吗?
这样不就把数据又恢复到原来的状态了,那这样不就实现了最终一致了吗?
所以这种模式啊,其实就是一种AP的一种思想了,
那反过来呢,我可不可以达成强一致呢?哎,也是可以的。
CP模式:各个子事务执行后互相等待,同时提交,同时回滚,达成强一致。但事务等待过程中,处于弱可用状态。
之前是各个子事务啊,是分别执行和提交,你一上来全部执行完了,那么没法回滚,对不对?
但现在呢,我各个子事务执行完,别提交,互相等待,大家彼此看一看啊诶,我这执行完了,你执行完了没有?这样直到什么我们全部都执行完都没问题。
那我们同时提交或者中间有人失败了,那我们同时回滚,那么这样是不是就能达成强一致了?没有中间状态对不对?
只不过在这个过程中啊,你的各个子事务是不是要互相等待啊?等待彼此的执行嘛,所以在这个过程当中,你的服务其实是处于一个弱可用状态。
因为你会锁定资源啊,导致无法访问嘛。
你看我们是不是就基于base的这种理论来实现了分布式事务的一种解决的一个思想了?
后面我们解决分布式事务都会基于这样一个思想去做,但是无论你是CP还是AP,这里边有一个共同点那就是各个子事务将来要做一个互相的通信,去辨别对方的执行状态。
那么各个子事务之间怎么去通信呢?
所以呢,它就需要有一个协调者来帮助分布式事务中的各个子事务进行一个通信啊感知。对方的或者彼此的状态,那我们就举个例子啊,就以我们之前的那个下单为例。
用户下单调用订单服务,然后去调用账务服务和库存服务。那这个地方呢?我们就需要有一个事务的协调者了,然后每一个微服务都跟事务的协调者啊,保持一个联系。
业务来了以后啊,大家各自执行。如果你现在要做强一致,那好第三服务执行的时候不要提交啊。执行下单扣款服务,执行扣款库存服务,执行库存。
但是执行完了以后,结果发现库存失败了。
怎么知道的?他们要把自己的执行结果是不是告知这个协调者?然后这协调者一看有人失败了再通知他们将来去做这个回滚。
那这样大家是不是就能保持一致了?所以呢,这个事务的协调者啊,就起到了一个非常关键的作用了,那么在整个过程当中啊,我们参与分布式事务当中的每一个子系统的事务,我们称之为叫分支事务。
而整个分支事务称之为叫全局事务,所以事务协调者其实就是来去协调各个分支事务的状态的,让他们达成一致。