分布式事务入门
今天和大佬们讨论框架和技术时提到了事务,然后自己一直都是做的单体应用的事务,比如使用Spring Boot中的@Transactional注解做事务处理,对于分布式事务完全没有了解过。又一次发现自己菜得很,所以赶紧学习一下,尽量缩小一下与大佬们的差距。
事务(Transaction)
事务提供一种机制,将一个活动涉及的所有操作纳入到一个不可分割的执行单元,只有在活动内的所有操作均能正常执行的情况下,才算完成了活动,否则只要有一个操作执行失败,活动就算失败。这里的活动就是事务,只要事务中有任一操作失败,就会导致整个事务的回滚。简单理解,就是一种"要么什么都不做,要么做全套(All or Nothing)"的机制。
举个例子,我微信给静静转账,这时就有两个操作,操作一是我的微信支出200,操作二是静静的微信收入200,这两个操作就组成了一个事务:要么都执行成功,要么都执行失败。好好想想,要是操作一成功了,操作二失败了,我岂不是要白白损失了200?事务就是为了避免我损失这200,而诞生的解决方案(手动滑稽)。
数据库本地事务(DataBase Local Transaction)
数据库本地事务通常理解也就是我前面提到的单体应用的事务。
数据库事务有四个特性(ACID),即原子性、一致性、隔离性和持久性。
原子性(Atomicity):一个事务中的所有操作,要么全部完成,要么全部不完成。如果事务在执行的过程中发生错误,就会回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。例子就是前面微信给静静转账的例子。
一致性(Consistency):一个事务执行之前和执行之后,数据都必须处在一致性状态。即事务在完成的时候,所有的数据都要发生改变,以保证数据的完整性。我的微信上减少了200,静静的微信也要相应增加200。
隔离性(Isolation):在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是在另一个事务修改它之前的状态,要么是另一个事务修改它之后的状态,事务不会查看到中间状态的数据。还是微信给静静转账的例子,我分两次给静静转200,一次转100,我转账是一个事务,静静查看是一个事务,静静查看微信查看到的是最终的200,而不会看到中间的100。
持久性(Durability):事务只要成功结束(提交),数据库的变化就永久保存下来。我给静静转账200这件事情永久保存在微信的记录上了。
数据库本地事务通常是使用数据库的日志和锁来实现的,而事务的核心其实就是为了处理异常情况,比如数据库在提交事务的时候突然断电。这里就使用SQL Server来举例,讲讲数据库在突然断电的情况下是怎么保证数据的一致性的。我们知道SQL Server数据库是由两个文件组成的,一个数据库文件和一个日志文件,在通常情况下,日志文件都要比数据库文件大很多。数据库进行任何写入操作的时候都是要先写日志的,同样的道理,我们在执行事务的时候,数据库会先首先记录下这个事务的redo操作日志,然后才开始真正操作数据库,在操作之前首先会把日志文件写入磁盘,那么当突然断电的时候,即使操作没有完成,在重新启动数据库的时候,数据库会根据当前数据的情况进行undo回滚或redo前滚,这样就保证了数据的强一致性。
分布式理论
分布式系统的核心同样是处理各种异常情况,这也是分布式系统复杂的地方,因为分布式的网络环境很复杂,类似"断电"的故障会比单机多很多,所以我们在做分布式系统的时候,最先考虑的就是这种情况。这些异常可能由机器宕机、网络异常、消息丢失、消息乱序、数据错误、不可靠的TCP、存储数据丢失、其他异常等等。
当我们的单个数据库的性能产生瓶颈的时候,我们可能会对数据库进行分区,这里所说的分区指的是物理分区,分区之后可能不同的库就处于不同的服务器上了,这个时候单个数据库库的ACID已经不能适应这种情况了,而在这种集群情况下,再想保证集群的ACID几乎很难达到,或者即使能达到效率和性能也会大幅下降,使得我们的系统变得很差。这时我们就需要引入一个新的理论来适应这种集群的情况,就是CAP定理(或者叫CAP原则)。
CAP定理
CAP定理又被叫做布鲁尔定理,对于设计分布式系统(不仅仅是分布式事务)的架构师来说,CAP就是一个入门的基础理论。这个入门的基础理论是由加州大学伯克利分校的Eric Brewer教授提出来的,他提出WEB服务无法同时满足以下3个属性:
一致性(Consistency):客户端知道一系列的操作都会同时发生(生效)。对于某个指定的客户端来说,读操作能返回最新的写操作的数据。也就是说,对于数据分布在不同节点上的数据来说,如果在某个节点更新(写)了数据,那么在其他节点如果都能读取到这个最新的数据,那么就称为强一致,如果在某个节点没有读取到,那么就是分布不一致。
可用性(Availability):每个操作都必须以预期的响应结束。非故障的节点必须在合理的时间内返回合理的响应(不是错误和超时的响应)。两个关键点,一个是合理的时间,一个是合理的响应。合理的时间指的是请求不能无限被阻塞,应该在合理的时间给出返回。合理的响应指的是系统应该明确返回结果并且结果是正确的。这里的正确指的是比如应该返回200,而不是返回20。
分区容忍/容错性(Partition Tolerance):即使出现单个组件无法可用,操作仍然可以完成。当出现网络分区后,系统能够继续工作。比如集群中由多台机器,其中某台机器网络出现了问题,但是这个集群仍然可以正常工作。
在分布式系统中,在任何数据库设计中,一个WEB应用最多只能同时支持上面的两个属性。但是因为网络无法100%可靠,且任何横向扩展策略都要依赖于数据分区,实际的分布式场景中,分区一定是要存在的,即必须要有分区容忍性。因此,CAP理论说是三选二,实际上就是二选一,设计人员必须在一致性和可用性之间做出选择。
CAP理论原本应该是产生三种组合:CA,CP和AP。如果我们选择了CA而放弃了P分区容错性,那么当发生分区现象的时候,为了保证C一致性,就必须拒绝请求,但是这样又不符合A可用性,所以分布式系统理论上不可能选择CA,除非应用在一个用不会通信故障的网络中(理想)。这样,就只有两种组合,一种是CP,一种是AP。
对于CP来说,放弃了A可用性,当节点间不可通信时,进行阻塞,直到通信恢复,期间无法再对外提供服务,用户体验不好。还是我微信给静静转账的例子,只有当我微信扣款成功并且静静微信收款成功,整个事务才算完成,显然耗费资源。
对于AP来说,放弃了C强一致性,具体实现是一致性的延迟。给用户一个可以忍受的时间段,在这个时间内达到数据的最终一致性,就像我在微信给静静转账一样,可以不是马上到账,可能是2小时内到账,可能是明天到账,也可能是明年到账(手动滑稽)。
关于CAP理论的论证是另外的知识点,这里不做论证,只有CAP理论是正确的结论。
BASE理论
在分布式系统中,我们往往追求的是可用性,它的重要程度比一致性要高,那么如何实现高可用性呢?前人已经给我们提出来了另外一个理论,就是BASE理论,它是对CAP定理的进一步扩展,是对CAP中的一致性和可用性进行一个权衡的结果。BASE理论指的是:
基本可用(Basically Available):分布式系统在出现故障时要保证核心功能可用,允许损失部分可用功能。
软状态(Soft state):允许系统中存在中间状态,这个状态不影响系统的可用性。
最终一致性(Eventually Consistent):经过一段时间后,所有节点的数据都达到一致,即延迟达到CAP中的C一致性。
BASE理论的核心思想就是:我们无法做到强一致,但每个应用都可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。BASE理论和数据库本地事务的ACID特性是相反的,它完全不同于ACID的强一致性模型,而是通过牺牲强一致性来获得可用性,并允许数据在一段时间内不一致,即使最终是会达到一致的状态(延迟达到一致)。
分布式事务的几种解决方案
在说方案之前,一定要明确是否真的需要分布式事务。出现分布式事务的一个原因是因为微服务太多,这样可能会导致一个团队中一个人维护几个微服务,太多团队设计,搞得所有人疲惫不堪。这种时候,如果可能的话,还是建议把需要事务的微服务聚合成一个单机服务,使用数据库本地事务。因为任何一种分布式事务的方案都会增加系统的复杂度,导致开发或维护的成本过高。所以千万不要因为追求某些设计,而引入不必要的成本和复杂度。
而在分布式系统中,要实现分布式事务,无外乎就是这几种解决方案:两阶段提交、补偿事务、本地消息表、MQ事务消息、Sagas事务模型等。
两阶段提交(2PC,2-Participant-Coordinator)
要说两阶段提交,一定要先讲数据库的两阶段提交。数据库支持的两阶段提交,叫做XA Transactions,MySQL从5.5版本开始支持,SQL Server从2005开始支持,Oracle从7开始支持。其中的XA是一个两阶段提交协议,这个协议分为两个阶段:
第一阶段:事务协调器要求每个涉及到事务的数据库预提交(Precommit)此操作,并反映是否可以提交。
第二阶段:事务协调器要求每个数据库提交数据。
其中,如果有任何一个数据库否认此提交,那么所有数据库都会被要求回滚它们在此事务中的那部分信息。
所有两阶段提交的实现,实际上都是用的XA协议的原理,通过流程图可以看出中间的一些比如Commit和Abort的细节:
两阶段提交这种解决方案属于牺牲了一部分可用性来换取的一致性,尽量保证了数据的强一致,实现成本也较低,各大主流数据库都有自己的实现。同时缺点也很明显:
单点问题:事务管理器在整个流程中扮演的角色很关键,如果其宕机,比如在第一阶段已经完成,在第二阶段正准备提交的时候事务管理器宕机,资源管理器就会一直阻塞,导致数据库无法使用。
同步阻塞:在准备就绪之后,资源管理器中的资源一直处与阻塞,知道提交完成,释放资源。
数据不一致:两阶段提交协议虽然是为了分布式数据的强一致性设计,但是仍然存在数据不一致的可能。比如在第二阶段中,假设协调者发出了事务Commit的通知,但是因为网络问题,该通知仅被一部分参与者收到并执行了Commit操作,其余参与者因为没有收到通知而一直处于阻塞状态,这时候就产生了数据的不一致性。
总的来说,XA协议虽然较为简单,成本较低,但是有单点问题,且不能支持高并发(因为基于同步阻塞,牺牲了可用性,对性能影响较大)。
补偿事务(TCC,Try-Confirm-Cancel)
补偿事务也就是用的补偿机制,其核心思想是:针对每个操作,都要注册一个与其对应的确认和补偿(撤销)操作,它分为三个阶段:
Try阶段:尝试执行,完成所有业务检查(一致性),预留必须的业务资源(准隔离性)。即对业务系统做检测和资源预留。
Confrim阶段:确认真正执行业务,不做任何业务检查,只使用Try阶段预留的业务资源,Confrim操作满足幂等性。要求具备幂等设计。即对业务系统做提交确认,Try阶段执行成功并开始执行Confirm阶段时,默认Confrim阶段是不会出错的。只要Try成功,Confirm一定成功。Confirm失败后会进行重试。
Cancel阶段:在业务执行出错需要回滚的状态下,执行取消业务、释放预留资源的操作。
还是我微信给静静转账的例子:
1.首先在Try阶段,要先调用远程接口,把我微信的钱和静静微信的钱冻结起来。
2.在Confirm阶段,执行远程调用的转账的操作,转账成功进行解冻。
3.如果第2步执行成功,那么转账成功;如果第2步执行失败,则调用远程冻结接口对应的解冻方法(Cancel)。
TCC事务机制相比于XA,解决了几个缺点:
1.解决了协调者单点:由主业务方发起并完成这个业务活动。业务活动管理器也变成多点,引入集群。
2.解决了同步阻塞:引入超时机制,超时后进行补偿,并且不会锁定整个资源,将资源转换为业务逻辑形式,粒度变小。
3.解决了数据一致性:有了补偿机制后,由业务活动管理器控制一致性。
总的来说,跟2PC比起来,实现以及流程相对简单了一些。但缺点也是比较明显的,在2、3步中都有可能失败。TCC是属于应用层的一种补偿方式,需要程序员在实现的时候多写很多补偿的代码。在一些场景中,一些业务流程可能用TCC不太好定义及处理。因此TCC适合一些强隔离性的、严格一致性要求的、执行时间较短的活动业务。
本地消息表(异步确认)
本地消息表这种实现方式应该是业界使用最多的,其核心思想是将分布式事务拆分成本地事务进行处理,这种思路来源于ebay。这个方案的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。人工重试更多的是应用于支付场景,通过对账系统对事后问题的处理。
基本思路就是:
消息生产方,需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说它们要在一个数据库里面。然后消息会经过消息队列(MQ)发送到消息的消费方。如果消息发送失败,会进行重试发送。
消息消费方,需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以个生产方发送一个业务补偿消息,通知生产方进行回滚等操作。
生产方和消费方定时扫描本地消息表,把还没处理完成的消息或失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。
这种方案遵循BASE理论,采用的是最终一致性,是几种方案里面比较适合实际业务场景的,既不会出现像2PC那样复杂的实现(当调用链很长的时候,2PC的可用性是非常低的),也不会像TCC那样可能出现确认或回滚不了的情况。
总的来说,本地消息表是一种非常经典的实现,避免了分布式事务,实现了最终一致性。但是消息表会耦合到业务系统中,如果没有封装好的解决方案,会有很多杂活需要处理。
MQ事务消息
有一些第三方的MQ是支持事务消息的,比如RocketMQ,它们支持事务消息的方式也是类似于采用的二阶段提交,但是市面上一些主流的MQ都是不支持事务消息的,比如RabbitMQ和Kafka都不支持。
以阿里的RocketMQ中间件为例,其思路大致是:
第一阶段Prepared消息,会拿到消息的地址。
第二阶段执行本地事务。
第三阶段通过第一阶段拿到的地址去访问消息,并修改状态。消息接收者就能使用这个消息。
如果确认消息失败,在RocketMQ Broker中提供了定时扫描没有更新状态的消息。如果有消息没有得到确认,会向消息发送者发送消息,来判断是否提交,在RocketMQ中是以Listener的形式给发送者,用来处理消息。如果消费超时,则需要一直重试,消息接收端需要保证幂等。如果消息消费失败,这时就需要人工进行处理。因为这个概率较低,如果为了这种小概率事件而设计这个复杂的流程反而得不偿失。
也就是说,在业务方法内要向消息队列提出两次请求,一次发送消息和一次确认消息。如果确认消息发送失败了,RocketMQ会定期扫描消息集群中的事务消息,这时候发现了Prepared消息,它会向消息发送者确认,所以生产方需要实现一个check接口,RocketMQ会根据发送端设置的策略来决定是回滚还是继续发送确认消息。这样就保证了消息发送与本地事务同时成功或同时失败。
MQ事务消息实现了最终一致性,且不需要依赖本地数据库事务。
Sagas事务模型
Sagas事务模型又叫做长时间运行的事务(Long-running-transaction),其核心思想是将长事务才分为多个本地短事务,由Saga事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作,重新进行业务回滚。它描述的是另外一种在没有两阶段提交的情况下解决分布式系统中复杂的业务事务问题。
这个理论较新,又难,这里只是入门,因此只要知道有这么个解决方案就好了(啊,好难啊)。
虚心地请教了大佬后了解到,tx-lcn是一个目前比较流行的解决方案,部门研发的新框架也打算用这个开源的分布式事务框架。在了解完分布式事务的概念与几种解决方案之后,接下来就要好好地学习具体的实现与应用(tx-lcn)了。