一,分布式事务
在分布式系统中,为了保证数据的高可用,通常,我们会将数据保留多个副本,这些副本会放置在不同的物理的机器上。为了对用户提供正确的 CRUD 等语义,我们需要保证这些放置在不同物理机器上的副本是一致的。分布式事务在现在遍地都是分布式部署的系统中几乎是必要的。
1, 分布式事务简介
事务是访问并可能更新数据库中各种数据项的一个程序执行单元。在关系数据库中,一个事务由一组SQL语句组成。事务应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。
原子性:事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。
一致性:事务必须是使数据库从一个一致性状态变到另一个一致性状态,事务的中间状态不能被观察到的。
隔离性:一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
隔离性又分为四个级别:读未提交、读已提交、可重复读、串行化。
持久性:持久性也称永久性,指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。任何事务机制在实现时,都应该考虑事务的ACID特性,包括:本地事务、分布式事务,及时不能都很好的满足,也要考虑支持到什么程度。
2, 本地事务
大多数场景下,我们的应用都只需要操作单一的数据库,这种情况下的事务称之为本地事务。本地事务的ACID特性是数据库直接提供支持。本地事务应用架构如下所示:
本地事务.png
很多java应用都整合了spring,并使用其声明式事务管理功能来完成事务功能。一般使用的步骤如下: 1、配置事务管理器。spring提供了一个 PlatformTransactionManager 接口,其有2个重要的实现类: DataSourceTransactionManager :用于支持本地事务,事实上,其内部也是通过操作java.sql.Connection 来开启、提交和回滚事务。 JtaTransactionManager :用于支持分布式事务,其实现了JTA规范,使用XA协议进行两阶段提交。需要注意的是,这只是一个代理,我们需要为其提供一个JTA provider,一般是Java EE容器提供的事务协调器,也可以不依赖容器,配置一个本地的JTA provider。
2、 在需要开启的事务的bean的方法上添加 @Transitional 注解
可以看到,spring除了支持本地事务,也支持分布式事务,下面我们先对分布式事务的典型应用场景进行介绍。
3, 不同场景下的分布式事务
当下互联网发展如火如荼,绝大部分公司都进行了数据库拆分和服务化。在这种情况下,完成某一个业务功能可能需要横跨多个服务,操作多个数据库。这就涉及到到了分布式事务,用需要操作的资源位于多个资源服务器上,而应用需要保证对于多个资源服务器的数据的操作,要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同资源服务器的数据一致性。
跨库事务
跨库事务指的是,一个应用某个功能需要操作多个库,不同的库中存储不同的业务数据。下图演示了一个服务同时操作2个库的情况:
垮库事务.png
分库分表事务
通常一个库数据量比较大或者预期未来的数据量比较大,都会进行水平拆分,也就是分库分表。如下图,将数据库B拆分成了2个库:
分库分表事务.png
对于分库分表的情况,一般开发人员都会使用一些数据库中间件来降低sql操作的复杂性。如,对于 sql: insert into user values , 。这条sql是操作单库的语法,单库情况下,可以保证事务的一致性。 但是由于现在进行了分库分表,开发人员希望将1号记录插入分库1,2号记录插入分库2。所以数据库中间件要将其改写为2条sql,分别插入两个不同的分库,此时要保证两个库要不都成功,要不都失败,因此基本上所有的数据库中间件都面临着分布式事务的问题。
跨应用事务
微服务架构是目前一个比较火的概念。例如上面提到的一个案例,某个应用同时操作了9个库,这样的应用业务逻辑必然非常复杂,对于开发人员是极大的挑战,应该拆分成不同的独立服务,以简化业务逻辑。拆分后,独立服务之间通过RPC框架来进行远程调用,实现彼此的通信。下图演示了一个3个服务之间彼此调用的架构:
垮应用事务.png
Service A完成某个功能需要直接操作数据库,同时需要调用Service B和Service C,而Service B又同时操作了2个数据库,Service C也操作了一个库。需要保证这些跨服务的对多个数据库的操作要不都成功,要不都失败,实际上这可能是最典型的分布式事务场景。 上述讨论的分布式事务场景中,无一例外的都直接或者间接的操作了多个数据库。如何保证事务的ACID特性,对于分布式事务实现方案而言,是非常大的挑战。同时,分布式事务实现方案还必须要考虑性能的问题,如果为了严格保证ACID特性,导致性能严重下降,那么对于一些要求快速响应的业务,是无法接受的。
二,分布式事务理论
分布式事务可以有多种分类,比如柔性事务和强一致性事务,这些事务操作会遵循一定的定理,比如CAP原理、base理论。
1, CAP原理
CAP理论在互联网界有着广泛的知名度,知识稍微宽泛一点的工程师都会把其作为衡量系统设计的准则。大家都非常清楚地理解了CAP:任何分布式系统在可用性、一致性、分区容错性方面,不能兼得,最多只能得其二,因此,任何分布式系统的设计只是在三者中的不同取舍而已。
由于对系统或者数据进行了拆分,我们的系统不再是单机系统,而是分布式系统,针对分布式系统的CAP原理包含如下三个元素:
C:Consistency,一致性。在分布式系统中的所有数据备份,在同一时刻具有同样的值,所有节点在同一时刻读取的数据都是最新的数据副本。
A:Availability,可用性,好的响应性能。完全的可用性指的是在任何故障模型下,服务都会在有限的时间内处理完成并进行响应。
P: Partition tolerance,分区容忍性。尽管网络上有部分消息丢失,但系统仍然可继续工作。
CAP原理指的是,这三个要素最多只能同时实现两点,不可能三者兼顾。因此在进行分布式架构设计时,必须做出取舍。而对于分布式数据系统,分区容忍性是基本要求,否则就失去了价值。因此设计分布式数据系统,就是在一致性和可用性之间取一个平衡。对于大多数web应用,其实并不需要强一致性,因此牺牲一致性而换取高可用性,是目前多数分布式数据库产品的方向。 当然,牺牲一致性,并不是完全不管数据的一致性,否则数据是混乱的,那么系统可用性再高分布式再好也没有了价值。牺牲一致性,只是不再要求关系型数据库中的强一致性,而是只要系统能达到最终一致性即可,考虑到客户体验,这个最终一致的时间窗口,要尽可能的对用户透明,也就是需要保障“用户感知到的一致性”。通常是通过数据的多份异步复制来实现系统的高可用和数据的最终一致性的,“用户感知到的一致性”的时间窗口则取决于数据复制到一致状态的时间。
2, base理论
base理论是指,Basically Available、Soft-state、Eventual Consistency。是基于CAP定理演化而来,是对CAP中一致性和可用性权衡的结果。核心思想:即使无法做到强一致性,但每个业务根据自身的特点,采用适当的方式来使系统达到最终一致性。
1、基本可用 BA:: 指分布式系统在出现故障的时候,允许损失部分可用性,保证核心可用。但不等价于不可用。比如:搜索引擎0.5秒返回查询结果,但由于故障,2秒响应查询结果;网页访问过大时,部分用户提供降级服务等。简单来说就是基本可用。
2、软状态 S:: 软状态是指允许系统存在中间状态,并且该中间状态不会影响系统整体可用性。即允许系统在不同节点间副本同步的时候存在延时。简单来说就是状态可以在一段时间内不同步。
3、最终一致性 E:: 系统中的所有数据副本经过一定时间后,最终能够达到一致的状态,不需要实时保证系统数据的强一致性。最终一致性是弱一致性的一种特殊情况。base理论面向的是大型高可用可扩展的分布式系统,通过牺牲强一致性来获得可用性。ACID是传统数据库常用的概念设计,追求强一致性模型。简单来说就是在一定的时间窗口内, 最终数据达成一致即可。
3, 刚柔事务
何谓刚柔事务?刚性事务它的事务是原子的,要么都成功要么都失败,也就是需要保障ACID理论,而柔性事务只需要保障数据最终一致即可,需要遵循base理论。
刚性事务满足ACID理论
柔性事务满足base理论
柔性事务分为:
两阶段型
补偿型
异步确保型
最大努力通知型。
金融项目对柔性事务用的比较多,例如支付宝、微信支付、银联支付几乎所有架构是SOA架构,因此传统单机环境下数据库的ACID事务满足了分布式环境下的业务需要,以上几种事务类似就是针对分布式环境下业务需要设定的。
三, 常用事务解决方案模型
分布式事务解决方案几乎都是柔性事务,常见的有2PC/3PC、TCC、MQ最终一致性解决方案,至于工作中用哪种方案,需要根据业务场景选取, 2PC/3PC、TCC 数据强一致性高,而MQ是最终数据一致。
1,TDP模型
TDP模型中有5个基本元素:
应用程序:用于定义事务边界,并且在事务边界内对资源进行操作。
资源管理器:如数据库、文件系统等,并提供访问资源的方式。
事务管理器:负责分配事务唯一标识,监控事务的执行进度,并负责事务的提交、回滚等。
通信资源管理器:控制一个TM域内或者跨TM域的分布式应用之间的通信。
通信协议:提供CRM提供的分布式应用节点之间的底层通信服务。
2,2PC/3PC
两阶段提交又称2PC,2PC是一个非常经典的强一致、中心化的原子提交协议 。 这里所说的中心化是指协议中有两类节点:一个是中心化协调者节点 和 N个参与者节点 。 两个阶段 :第一阶段:投票阶段和第二阶段:提交/执行阶段。 举例订单服务A,需要调用 支付服务B 去支付,支付成功则处理购物订单为待发货状态,否则就需要将购物订单处理为失败状态。 那么看2PC阶段是如何处理的。
1、事务询问 协调者向所有的参与者发送事务预处理请求,称之为Prepare,并开始等待各 参与者 的响应。 2、 执行本地事务 各个 参与者 节点执行本地事务操作,但在执行完成后并不会真正提交数据库本地事务,而是先向 协调者报告说:“我这边可以处理了/我这边不能处理”。 3、各参与者向协调者反馈事务询问的响应 如果 参与者 成功执行了事务操作,那么就反馈给协调者 Yes 响应,表示事务可以执行,如果没有 参与者 成功执行事务,那么就反馈给协调者 No 响应,表示事务不可以执行。
1、 所有的参与者反馈给协调者的信息都是Yes,那么就会执行事务提交协调者 向 所有参与者 节点发出Commit请求. 2、事务提交 参与者 收到Commit请求之后,就会正式执行本地事务Commit操作,并在完成提交之后释放整个事务执行期间占用的事务资源。
3PC
三阶段提交又称3PC,其在两阶段提交的基础上增加了CanCommit阶段,并引入了超时机制。一旦事务参与者迟迟没有收到协调者的Commit请求,就会自动进行本地commit,这样相对有效地解决了协调者单点故障的问题。
这个阶段类似于2PC中的第二个阶段中的Ready阶段,是一种事务询问操作,事务的协调者向所有参与者询问“你们是否可以完成本次事务?”,如果参与者节点认为自身可以完成事务就返回“YES”,否则“NO”。而在实际的场景中参与者节点会对自身逻辑进行事务尝试,简单来说就是检查下自身状态的健康性,看有没有能力进行事务操作。
在阶段一中,如果所有的参与者都返回Yes的话,那么就会进入PreCommit阶段进行事务预提交。此时分布式事务协调者会向所有的参与者节点发送PreCommit请求,参与者收到后开始执行事务操作,并将Undo和Redo信息记录到事务日志中。参与者执行完事务操作后,就会向协调者反馈“Ack”表示我已经准备好提交了,并等待协调者的下一步指令。 否则,如果阶段一中有任何一个参与者节点返回的结果是No响应,或者协调者在等待参与者节点反馈的过程中超时。整个分布式事务就会中断,协调者就会向所有的参与者发送“abort”请求。
在阶段二中如果所有的参与者节点都可以进行PreCommit提交,那么协调者就会从“预提交状态”-》“提交状态”。然后向所有的参与者节点发送"doCommit"请求,参与者节点在收到提交请求后就会各自执行事务提交操作,并向协调者节点反馈“Ack”消息,协调者收到所有参与者的Ack消息后完成事务。
相反,如果有一个参与者节点未完成PreCommit的反馈或者反馈超时,那么协调者都会向所有的参与者节点发送abort请求,从而中断事务。
2PC VS 3PC
相比较2PC而言,3PC对于协调者和参与者都设置了超时时间,而2PC只有协调者才拥有超时机制。这解决了一个什么问题呢?这个优化点,主要是避免了参与者在长时间无法与协调者节点通讯的情况下,无法释放资源的问题,因为参与者自身拥有超时机制会在超时后,自动进行本地commit从而进行释放资源。而这种机制也侧面降低了整个事务的阻塞时间和范围。
另外,通过
CanCommit、PreCommit、DoCommit三个阶段的设计,相较于2PC而言,多设置了一个缓冲阶段保证了在最后提交阶段之前各参与节点的状态是一致的。
以上就是3PC相对于2PC的一个提高,但是3PC依然没有完全解决数据不一致的问题。
TCC
TCC与2PC、3PC一样,也是分布式事务的一种实现方案。TCC又称补偿事务。其核心思想是:"针对每个操作都要注册一个与其对应的确认和补偿"。它分为三个操作:
Try阶段:主要是对业务系统做检测及资源预留。
/confirm/i阶段:确认执行业务操作。
Cancel阶段:取消执行业务操作。
TCC事务的处理流程与2PC两阶段提交类似,不过2PC通常都是在跨库的DB层面,而TCC本质上就是一个应用层面的2PC,需要通过业务逻辑来实现。这种分布式事务的实现方式的优势在于,可以让应用自己定义数据库操作的粒度,使得降低锁冲突、提高吞吐量成为可能。 不足之处则在于对应用的侵入性非常强,业务逻辑的每个分支都需要实现try、/confirm/i、cancel三个操作。此外,其实现难度也比较大,需要按照网络状态、系统故障等不同的失败原因实现不同的回滚策略。为了满足一致性的要求,/confirm/i和cancel接口还必须实现幂等。
MQ分布式事务
在OLTP系统领域,我们在很多业务场景下都会面临事务一致性方面的需求,而大型互联网平台往往是由一系列分布式系统构成的,开发语言平台和技术栈也相对比较杂,尤其是在SOA和微服务架构盛行的今天,一个看起来简单的功能,内部可能需要调用多个“服务”并操作多个数据库或分片来实现,情况往往会复杂很多,如果对数据一致性要求很高,可以采用上面的2PC或者3PC以及TCC方案,如果数据强一致性要求没那么高,可以采用消息中间件实现事务最终一致。 在支付系统中,常常使用的分布式事务解决方案就是基于MQ实现的,它对数据强一致性要求没那么高,但要求数据最终一致即可。例如:向借呗申请借钱,借呗审核通过后支付宝的余额才会增加,但借呗和支付宝有可能不是同一个系统,这时候如何实现事务呢?实现方案如下图:
MQ事务实现方案.png
上图执行流程:找花呗借钱 2:花呗借钱审核通过,同步生成借款单 3:借款单生成后,向MQ发送消息,通知支付宝转账 4:支付宝读取MQ消息,并增加账户余额
上图最复杂的其实是如何保障2、3在同一个事务中执行,借款结束后,借呗数据处理就完成了,接下来支付宝才能读到消息,然后执行余额增加,这才完成整个操作。如果中途操作发生异常,例如支付宝余额增加发生问题怎么办?此时需要人工解决,没有特别好的办法,但这种事故概率极低。
四, Seata分布式事务
我们目前已经实现了商城项目中部分核心功能了,整个项目用的是微服务架构,如果此时服务间调用出现异常是会出现事务问题,而且是跨服务间的事务问题,上面虽然讲解了事务以及分布式事务相关知识点,但分布式事务究竟该如何解决呢?
一, 订单问题分析
上面是我们之前写的下单代码,整个看起来没有任何问题,但其实存在事务问题,如果上述操作①和②都操作成功了,但是③操作发生异常,②是可根据异常实现本地事务回滚,但①处是没法实现本地事务回滚的,因为它跨应用了,操作流程已经结束。这个问题就是我们说的分布式事务问题。可以采用当前主流分布式事务解决方案Seata来解决。
二, Seata介绍
Seata 是一款开源的分布式事务解决方案,致力于在微服务架构下提供高性能和简单易用的分布式事务服务。在 Seata 开源之前,Seata 对应的内部版本在阿里经济体内部一直扮演着分布式一致性中间件的角色,帮助经济体平稳的度过历年的双11,对各BU业务进行了有力的支撑。经过多年沉淀与积累,商业化产品先后在阿里云、金融云进行售卖。2019.1 为了打造更加完善的技术生态和普惠技术成果,Seata 正式宣布对外开源,开放以来,广受欢迎,不到一年已经成为最受欢迎的分布式事务解决方案。
三,Seata AT模式
Seata 将为用户提供了 AT、TCC、SAGA 和 XA 事务模式,为用户打造一站式的分布式解决方案。其中AT模式最受欢迎,使用也非常简单,但它内在的原理不简单。
AT模式前提:
基于支持本地 ACID 事务的关系型数据库。 Java 应用,通过 JDBC 访问数据库。
整体机制:2PC协议的演变
1PC:业务数据和回滚日志记录在同一个本地事务中提交,释放本地锁和连接资源。 2PC:
提交异步化,非常快速地完成。
回滚通过一阶段的回滚日志进行反向补偿。
AT模式工作机制
以一个示例来说明整个 AT 分支的工作过程。 业务表: product
执行修改操作:。
update product set name = 'GTS' where name = 'TXC';
1PC:
a.解析 SQL 语句,得到类型为 UPDATE ,表为 product ,条件 where name = 'TXC' 。
b.根据解析的 SQL 语句进行要操作的结果查询:
select * product where name ='TXC';
得到的修改前数据结果如下:
c.执行业务 SQL 查询修改后的结果:
d.镜像备份 将修改前的结果和修改后的结果添加到数据库表 undo_log 中。
e.提交前,向TC注册分支申请 product 表中id=1的数据的全局锁。
f.本地事务提交。
8.本地事务提交结果上报给TC。
2PC-提交:
a.收到 TC 的分支提交请求,把请求放入一个异步任务的队列中,马上返回提交成功的结果给 TC。
b.异步任务阶段的分支提交请求将异步和批量地删除相应 UNDO LOG 记录。
2PC-回滚:
a.收到 TC 的分支回滚请求,开启一个本地事务,执行如下操作。
b.通过 XID 和 Branch ID 查找到相应的 UNDO LOG 记录。
c.数据校验:拿 UNDO LOG 中的后镜与当前数据进行比较,如果有不同,说明数据被当前全局事务之外 的动作做了修改。这种情况,需要根据配置策略来做处理,详细的说明在另外的文档中介绍。
上面说到的借呗借钱数据最终一致性分布式事务解决方案可以采用RocketMQ,其实除了RocketMQ之外,其他MQ也可以实现分布式事务,但实现原理不一样,其他MQ采用的几乎都是ACK机制,只有RocketMQ支持事务消息,我们来讲述一下RocketMQ的事务消息在实战案例中该如何使用。
一,RocketMQ架构
RocketMQ的部署架构:
1,启动Namesrv,Namesrv起来后监听端口,等待Broker、Produer、Consumer连上来,相当于一个路由控制中心。2,Broker启动,跟所有的Namesrv保持长连接,定时发送心跳包。心跳包中包含当前Broker信息以及存储所有topic信息。注册成功后,namesrv集群中就有Topic跟Broker的映射关系。3,收发消息前,先创建topic,创建topic时需要指定该topic要存储在哪些Broker上。也可以在发送消息时自动创建Topic。4,Producer发送消息,启动时先跟Namesrv集群中的其中一台建立长连接,并从Namesrv中获取当前发送的Topic存在哪些Broker上,然后跟对应的Broker建立长连接,直接向Broker发消息。5,Consumer跟Producer类似。跟其中一台Namesrv建立长连接,获取当前订阅Topic存在哪些Broker上,然后直接跟Broker建立连接通道,开始消费消息。
二,Docker安装RocketMQ
三,RocketMQ事务消息
在RocketMQ中生产者有三种角色 NormalProducer 、 OrderProducer 、 TransactionProducer ,根据名字大概可以看出各个代表着什么作用,我们这里用 TransactionProducer 来解决问题。
RocketMQ事务消息.png
四,最终一致案例分析
我们上面已经完成了下订单操作,但如果用户完成了支付,此时需要通知当前系统处理订单状态,并增加用户金币,不过大家可以发现个问题,其实用户支付完成后,订单状态并非立即发生改变,很多时候都是过了一会儿订单状态才发生改变、奖励的金币才到账,这是因为支付系统和订单系统是独立系统,最主要的是支付业务和订单业务并不需要数据强一致性,最终一致即可。 我们举个例子,比如用户支付成功了,但赠送金币失败了,这个时候就需要给用户退款吗?很明显是不需要的,只需要处理下赠送金币的流程即可。
支付流程设计.png
上图是支付流程设计:
1:用户下单成功后,会进入支付2:支付需要从我们支付系统中获取支付二维码3:用户支付完成后,微信服务器会将支付结果返回给我们的支付系统4:支付系统会在本地数据库中做一个支付结果保存,同时通过RocketMQ将支付结果告知订单系统5:订单系统读取RocketMQ支付结果,并发起订单状态变更以及赠送积分操作
上述流程中4-5两个流程可以采用RocketMQ事务消息实现事务最终一致操作。