本地事务
JDBC 事务的一个缺点是事务的范围局限于一个数据库连接。一个JDBC事务不能跨越多个数据库。也无法在通过RPC的方式调用中保证事务。
事务具有4个特性:原子性、一致性、隔离性、持久性。这四个属性通常称为 ACID 特性。
- 原子性(atomicity):一个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。
- 一致性(consistency):事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。
- 隔离性(isolation):一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
- 持久性(durability):持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
分布式事务的产生
众所周知,数据库能实现本地事务,也就是在同一个数据库中,你可以允许一组操作要么全都正确执行,要么全都不执行。这里特别强调了本地事务,也就是目前的数据库只能支持同一个数据库中的事务。一个业务要跨多个数据库,但是这些操作又需要在一个事务中完成,这种事务即为“分布式事务”
- 当我们的单个数据库的性能产生瓶颈的时候,我们可能会对数据库进行分区,分区之后可能不同的库就处于不同的服务器上了。
- 大型互联网项目往往是由一系列分布式系统构成的,开发语言平台和技术栈也相对比较杂,尤其是在微服务架构盛行的今天,一个看起来简单的功能,内部可能需要调用多个“服务”并操作多个数据库或分片来实现,情况往往会复杂很多。
分布式理论
当出现一个事务要操作多数据库的时候(分布式事务),单个数据库的ACID已经不能适应这种情况了,而在这种ACID的集群环境下,再想保证集群的ACID几乎是很难达到,或者即使能达到那么效率和性能会大幅下降,最为关键的是再很难扩展新的分区了,这个时候如果再追求集群的ACID会导致我们的系统变得很差,这时我们就需要引入一个新的理论原则来适应这种集群的情况,就是 CAP理论。
CAP定理
• C 一致性(Consistency) : 客户端知道一系列的操作都会同时发生(生效)
• A 可用性(Availability) : 每个操作都必须以可预期的响应结束
• P 分区容错性(Partition tolerance 必须满足) : 即使出现单个组件无法可用,操作依然可以完成
具体地讲在分布式系统中,在任何数据库设计中,一个Web应用至多只能同时支持上面的两个属性。显然,任何横向扩展策略都要依赖于数据分区。因此,设计人员必须在一致性与可用性之间做出选择。
在分布式系统中,同时满足“CAP定律”中的“一致性”、“可用性”和“分区容错性”三者是不可能的,这比现实中找对象需同时满足“高、富、帅”或“白、富、美”更加困难。在互联网领域的绝大多数的场景,都需要牺牲强一致性来换取系统的高可用性,系统往往只需要保证“最终一致性”,只要这个最终时间是在用户可以接受的范围内即可。
例如Eureka和Nacos只能满足可用性A和分区容错性P,而无法满足一致性C;Consul则只能满足一致性和分区容错性,而无法完全满足可用性。
在注册中心的场景中,一致性一般要求并不高,只要能达到最终一致性即可。毕竟在微服务架构中,涉及节点的注册和反注册,注册中心和客户端之间的通信需要一定时间,一致性本身也很难达到。所以在注册中心的选型中,一般会优先选择AP的系统,这也是目前还在以Spring Cloud构建微服务的实践中,除了自研外,开源技术中会优先选择Nacos作为服务注册中心的原因。
在分布式系统中,各个节点之间在物理上相互独立,通过网络进行沟通和协调。由于存在事务机制,可以保证每个独立节点上的数据操作可以满足ACID。但是,相互独立的节点之间无法准确的知道其他节点中的事务执行情况。所以从理论上讲,两台机器理论上无法达到一致的状态。如果想让分布式部署的多台机器中的数据保持一致性,那么就要保证在所有节点的数据写操作,要不全部都执行,要么全部的都不执行。但是,一台机器在执行本地事务的时候无法知道其他机器中的本地事务的执行结果。所以他也就不知道本次事务到底应该commit还是 roolback。所以,常规的解决办法就是引入一个“协调者”的组件来统一调度所有分布式节点的执行。
二阶段提交协议 2PC
在分布式系统中,每个节点虽然可以知晓自己的操作时成功或者失败,却无法知道其他节点的操作的成功或失败。当一个事务跨越多个节点时,为了保持事务的ACID特性,需要引入一个作为协调者的组件来统一掌控所有节点(称作参与者)的操作结果,并最终指示这些节点是否要把操作结果进行真正的提交。因此,二阶段提交的算法思路可以概括为:参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。
所谓的两个阶段是指:第一阶段:准备阶段(投票阶段)和第二阶段:提交阶段(执行阶段)。
准备阶段
事务协调者(事务管理器)给每个参与者(资源管理器)发送Prepare消息,每个参与者要么直接返回失败(如权限验证失败),要么在本地执行事务,写本地的redo和undo日志,但不提交,到达一种“万事俱备,只欠东风”的状态。
可以将准备阶段分为以下三个步骤:
- 协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等待各参与者节点的响应。
参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志。(注意:若成功这
里其实每个参与者已经执行了事务操作) - 各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个”
同意”消息;如果参与者节点的事务操作实际执行失败,则它返回一个”中止”消息。
提交阶段 - 如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(Rollback)消息;否则,发送提交(Commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)。
2PC缺陷
二阶段提交看起来确实能够提供原子性的操作,但是不幸的事,二阶段提交还是有几个缺点的:- 同步阻塞问题。执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
- 单点故障。由于协调者的重要性,一旦协调者发生故障。参与者会一直阻塞下去。尤其在第二阶段,协调者发生故障,那么所有的参与者还都处于锁定事务资源的状态中,而无法继续完成事务操作。(可以重新选举一个协调者,但是无法解决因为协调者宕机导致的参与者处于阻塞状态的问题)
- 数据不一致。在二阶段提交的阶段二中,当协调者向参与者发送commit请求之后,发生了局部网络异常或者在发送commit请求过程中协调者发生了故障,这会导致只有一部分参与者接受到了commit请求。而在这部分参与者接到commit请求之后就会执行commit操作。但是其他部分未接到commit请求的机器则无法执行事务提交。导致整个分布式系统出现了数据不一致性的现象。
- 二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
三阶段提交协议 3PC
由于二阶段提交存在着诸如同步阻塞、单点问题、脑裂等缺陷,所以,研究者们在二阶段提交的基础上做了改进,提出了三阶段提交。三阶段提交(Three-phase commit),是二阶段提交(2PC)的改进版本。阶段提交有两个改动点。
- 引入超时机制。同时在协调者和参与者中都引入超时机制。
- 在第一阶段和第二阶段中插入一个准备阶段。保证了在最后提交阶段之前各参与节点的状态是一致的。也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。
分布式事务协议
XA协议
XA协议最早的分布式事务模型是由 X/Open 国际联盟提出的 X/Open Distributed Transaction Processing(DTP) 模型,简称XA协议。它里面用到的也是二阶段三阶段提交的方式。 基于XA协议实现的分布式事务对业务侵入很小。 它最大的优势就是对使用方透明,用户可以像 使用本地事务一样使用基于XA协议的分布式事务。 XA协议能够严格保障事务 ACID 特性。 严格保障事务 ACID 特性是一把双刃剑。 事务执行在过程中需要将所需资源全部锁定,它更加 适用于执行时间确定的短事务。 对于长事务来说,整个事务进行期间对数据的独占,将导致对 热点数据依赖的业务系统并发性能衰退明显。 因此,在高并发的性能至上场景中,基于XA协议的分布式事务并不是最佳选择。
像我们的sharding-jdbc数据库中间件,分布式事务解决方案中就有xa事务
柔性事务
如果将实现了 ACID 的事务要素的事务称为刚性事务的话,那么基于 BASE 事务要素的事务则称为柔性事务。 BASE 是基本可用、柔性状态和最终一致性这三个要素的缩写。
- 基本可用(Basically Available)保证分布式事务参与方不一定同时在线。
- 柔性状态(Soft state)则允许系统状态更新有一定的延时,这个延时对客户来说不一定能够察觉。
- 最终一致性(Eventually consistent)通常是通过消息传递的方式保证系统的最终一致性。
在 ACID 事务中对隔离性的要求很高,在事务执行过程中,必须将所有的资源锁定。 柔性事务的理念则是通过业务逻辑将互斥锁操作从资源层面上移至业务层面。通过放宽对强一致性要求,来换取系统吞吐量的提升。 基于 ACID 的强一致性事务和基于 BASE 的最终一致性事务都不是银弹,只有在最适合的场景中 才能发挥它们的最大长处。 可通过下表详细对比它们之间的区别,以帮助开发者进行技术选
型。
Mq分布式解决方案
假设有这么一个下单的场景,你本地下单后要调用扣减库存和其它较强关联的业务如扣钱。在你调用saveOrder(entity)成功后,你先通过mq去下发扣减库存的消息,rocketmq可以通过SendResult来判断发送结果,rabbitmq则需要调用回调来拿到消息发送的结果,来确定ack为true则表明消息发送成功。
- 我们这里以rocketmq举例,发送成功的话则记录一条记录到本地消息record表,里面的字段可以记录为msg_id,msg_content,msg_status,create_time,update_time,msg_id可以是我们的业务唯一key比如订单号,msg_content可以保存为我们的消息,用来为后面的异常做处理。状态我们记录为0初始化,发送成功则更新为1,失败则更新为2,消费成功3,消费失败4
- 如果消息发送失败,更新我们的记录表状态为发送失败。然后通过定时任务去扫描我们的记录表,发现发送失败的则去做一个补发机制,再次向mq发送对应的消息,此时可能会造成重复生产的问题,需要对应的库存服务做幂等处理。库存服务则订阅我们的topic,进行消费。发送成功则去更新我们的记录表为发送成功。
- 库存服务消费完成后,成功则去调用订单服务更新记录表为3,失败则更新为4。如果成功,如果失败看自身能否解决,能解决就使用重试机制。需要注意一点,假设消费耗时很长,没返回ack,超过rocketmq的默认时间的话,会触发重发机制。
seata
阿里出品的开源的分布式事务解决方案,能够在微服务架构下提供高性能、简单易于使用的分布式事务 服务。默认使用的是AT模式。
Seata这个框架会自动记录你的事务A执行的SQL语句的逆向补偿SQL。什么意思呢?假设你事务A执行的是insert,那么Seata就知道补偿的时候可以delete删除,假设你执行的是update,那么Seata就可以记录你update之前的老数据,补偿的时候可以把数据重新update回老版本数据,而且这个逆向补偿日志也是记录在数据库里的,接着Seata还会提供一个Seata server来监控你的各个系统的事务执行情况,系统A的事务A执行成功了得告诉Seata server,系统B的事务B执行失败了也得告诉Seata server。当Seata server知道你的系统B的事务B执行失败了,他会告诉系统A里的Seata框架,小兄弟,人家系统B都失败了,你赶紧的吧,别墨迹,把你之前记录的事务A逆向补偿日志拿出来,把你之前提交的事务恢复到提交前的数据状态,搞一个逆向回滚。
- TC (Transaction Coordinator) 事务协调者 :维护全局和分支事务的状态,驱动全局事务提交或回滚。
- TM (Transaction Manager) 事务管理器 :定义全局事务的范围:开始全局事务、提交或回滚全局事务。
- RM (Resource Manager) 资源管理器 :管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。
文档地址
待更新