当前位置: 首页 > 科技观察

彻底掌握分布式事务2PC、3PC模型

时间:2023-03-15 22:09:09 科技观察

彻底掌握分布式事务的2PC和3PC模型本文转载请联系源码兴趣圈公众号。本地事务在工作中用的最多,但是单个项目拆分成SOA和微服务后,就会涉及到分布式事务场景。文章以分布式事务为主线进行讲解,详细介绍了2PC和3PC算法的Explain,最后用一个Demo来加深对分布式事务的理解。文章目录结构如下:什么是交易?什么是分布式事务DTP模型和XA规范?什么是DTP模型?什么是XA规范?Phase2PC算法优缺点3PC一致性算法JDBC操作MySQLXA事务结语什么是事务事务是数据库操作的最小工作单元,是一组不可分割的操作集合,是作为一个逻辑工作单元执行的一系列操作。这些操作作为一个整体提交给系统,要么全部执行,要么都不执行。事务具有四个特性,即原子性、一致性、隔离性和持久性。空头交易的ACID特性如何保证交易的ACID特性?原子性(Atomicity):事务中的SQL要么同时成功,要么同时失败,基于撤销日志(undolog)实现一致性(Consistency):系统从一个正确的状态转移到另一个正确的状态,由应用通过AID,可以说是事务的核心特性隔离(Isolation):控制事务并发执行时数据的可见性,基于锁和多版本并发控制实现持久化(Durability)(MVCC)。):提交后,必须存储成功,不会丢失。本文基于重做日志(redolog)实现,主要介绍分布式事务2PC和3PC。redo、undolog、mvcc、lock的内容后面会详细介绍,有时候,我们的应用还是一个单一的项目,所以我们操作的是单一的数据库。在这种情况下,我们称之为本地事务。本地事务的ACID一般都是数据库级别支持的,比如我们工作中常用的MySQL数据库。通常我们在操作MySQL客户端时,MySQL会自动隐式提交事务,所以日常工作不会涉及到手动编写事务。创建、提交、回滚等。如果想测试锁、MVCC等特性,可以创建多个session,使用begin、commit、rollback等命令测试不同事务之间的数据,看执行结果是否和你想的一致。我们平时在开发项目代码的时候最主要的就是Spring封装的事务,所以不会手动写提交和回滚数据库事务等方法(某些情况除外)。这里我使用原生JDBC写了一个示例代码,帮助大家理解如何通过事务保证ACID的四大特性Connectionconn=...;//获取数据库连接conn.setAutoCommit(false);//启动事务try{//...执行增删改查sqlconn.commit();//提交事务}catch(Exceptione){conn.rollback();//事务回滚}finally{conn.close();//关闭链接}试想一下,每次对数据库操作写重复的创建事务、提交、回滚的方法是不是很痛苦,那么Spring是如何自动帮我们管理事务的呢?在Spring项目中,我们一般使用两种方式来管理事务,编程在事务式和声明式事务项目中使用Spring来管理事务,要么在接口方法上加注解@Transactional,要么使用AOP配置切面事务。其实这两种方法是类似的,只是@Transactional的粒度更细,实现原理依赖于AOP。比如@ServicepublicclassTransactionalService{@Transactionalpublicvoidsave(){//业务操作}}TransactionalService会被Spring创建为代理对象放入容器中,创建的代理对象相当于下面的类publicclassTransactionalServiceProxy{privateTransactionalServicetransactionalService;publicTransactionalServiceProxy(TransactionalServicetransactionalService){this.transactionalService=transactionalService;}publicvoidsave(){try{//开启事务服务transactionalService.save();}catch(Exceptione){//有异常则回滚}//提交事务}示例代码看起来简单明了,但是真正的代码生成代码对比要复杂的多。关于事务管理器,Spring提供了接口PlatformTransactionManager,它包含两个重要的实现类DataSourceTransactionManager:支持本地事务,内部通过java.sql.Connection来开启、提交和回滚事务JtaTransactionManager:用于支持分布式事务,其JTA规范是实现,XA协议用于两阶段提交。通过这两个实现类,我们知道我们平时使用的程序化事务和声明式事务都依赖于本地事务管理的实现。Spring也支持分布式事务。关于JTA分发关于正式事务的支持网上已经有不少资料了,这里就不赘述了。我们在分布式事务的日常业务代码中一直在使用本地事务,理解起来也不难。然而,随着面向服务(SOA)和微服务的流行,我们的单一业务系统通常被拆分成多个系统。为了迎合业务系统的变化,数据库也结合业务进行拆分。例如,以学校管理系统为例。注意,可能拆分成学生服务、课程服务、教师服务等,数据库也拆分成多个库。这样的话,当不同的服务部署到服务器上时,可能会面临如下服务调用ServiceA服务需要操作数据库进行本地事务,同时需要调用ServiceB和ServiceC服务发起事务调用,如何保证三个服务的事务要么一起成功,要么一起失败,如何保证用户发起的事务的ACID特性?毫无疑问,这是分布式事务场景,三个服务的单个本地事务并不能保证整个请求的事务。分布式事务场景有很多解决方案。从不同的分类来看,强一致性方案、最终一致性方案,细分的方案有2PC、3PC、TCC、可靠消息……业界使用较多的像阿里的RocketMQ事务消息、SeataXA模式、可靠消息等几种方案模型。但是无一例外,分布式事务都会直接或间接地操作多个数据库,分布式事务的使用也会带来新的挑战,那就是性能问题。如果为了保证强一致性分布式事务或补偿方案的最终一致性,导致性能下降,对于正常业务来说,无疑得不偿失。DTP模型和XA规范X/Open组织定义了分布式事务模型(DTP)和分布式事务协议(XA),DTP由以下模型元素组成AP(ApplicationApplicationProgram):用于定义事务边界(即,定义事务的开始和结束),并在事务边界内进行资源操作TM(TransactionManager):负责为事务分配唯一标识符,监控事务执行进度,并负责事务的提交、回滚等。RM(ResourceManagerResourceManager):如数据库、文件系统等,提供访问资源模式CRM(CommunicationResourceManager):控制TM域(TM域)内或跨TM域的分布式应用程序之间的通信CP(CommunicationProtocolcommunicationprotocol):提供CRM提供的分布式应用节点之间的底层通信服务IntDTP分布式事务模型,基本构成需要覆盖AP、TM、RMS(没有CRM和CP也是可以的),如下图。XA规范最重要的作用就是定义了RM(ResourceManager)和TM(TransactionManager)之间的交互接口。此外,XA规范除了定义了2PC之间的交互接口外,还对2PC进行了优化。DTP、XA、2PC之间的关系。DTP规定了分布式事务中的角色模型,规定了其中的全局事务。控制需要使用2PC协议来保证数据的一致性。2PC是Two-PhaseCommit的缩写,即两阶段提交。它属于计算机网络领域,尤其是数据库领域。为了保证分布式系统架构下的所有节点都能设计出保证原子性和一致性的算法。同时,2PC也被认为是一种一致性协议,用于保证分布式系统数据的一致性。XA规范是由X/Open组织提出的分布式事务处理规范。XA规范定义了2PC(两阶段提交协议)。需要用到的接口就是上图中RM和TM的交互。2PC和XA是最容易混淆的。可以理解为DTP模型定义了TM和RM之间通信的接口规范称为XA,而关系型数据库(如MySQL)是基于X/Open提出的XA规范(核心依赖于2PC算法)称为XA方案2PC共识算法。当应用程序(AP)发起一个需要跨越多个分布式节点的事务操作时,每个分布式节点(RM)都知道自己事务操作的结果是成功还是失败,但无法获取其他分布式节点的运行结果。为了保证事务处理的ACID特性,需要引入一个叫做“协调器”的组件(TM)来统一调度和分布式执行逻辑。协调器负责调度参与整个事务的分布式节点的行为,并最终确定这些分布。节点是否应提交或回滚事务。因此,基于这种思想,衍生出了两阶段提交和三阶段提交两种分布式共识算法协议。第二阶段是指准备阶段和提交阶段。我们先来看看准备阶段都做了些什么。接下来的事务提交步骤是事务查询:协调器向所有参与这个分布式事务的参与者发送事务内容,询问是否可以进行事务提交操作,然后开始等待每个参与者的响应来执行事务:参与者收到协调者的交易请求,执行相应的交易,并将内容写入Undo和Redo日志,返回响应:如果每个参与者都执行了交易,则反馈协调者响应Yes;如果每个参与者都未能成功执行事务,则向协调器返回无响应。如果第一阶段所有参与者都返回成功响应,则进入事务提交步骤,否则,本次分布式事务返回失败。以MySQL数据库为例,在第一阶段,事务管理器(TM)向所有涉及的数据库(RM)发送prepare(准备提交)请求,数据库收到请求后进行数据修改和日志记录处理.处理完成后将事务的状态更改为“可提交”,最后将结果返回给事务处理器2PC-提交阶段提交阶段分为两个过程,一个是每个参与者正常执行事务提交过程,并返回Yes响应,表示每个参与者一个是每个参与者没有返回No响应或者超时,会触发全局回滚,表示分布式事务执行失败执行事务提交中断事务。如果反馈是Yes响应,则执行事务提交操作。事务提交:协调者向所有参与节点发送提交请求。各参与者收到Commit请求后,提交本地事务,提交完成后释放。事务执行周期内占用的事务资源完成事务:每个参与者完成事务提交后,向协调器发送Ack响应,协调器收到响应后完成分布式事务中断事务。假设任意一个交易参与节点向协调器发送了Noresponse被反馈(注意这里的Noresponse指的是第一阶段),或者等待超时后协调器没有收到所有参与者的反馈,那么交易中断流程将被回滚:协调者发送所有参与者发送回滚请求。参与者收到回滚请求后,使用第一阶段写入的undolog进行事务回滚,并在完成回滚事务后释放占用的资源。参与者完成事务回滚后,向协调者发送Ack消息。协调器收到事务参与者的Ack消息后,事务中断。2PC优缺点2PCcommit将交易过程分为两个阶段:投票和执行。每个事务都以先试后提交的方式处理。2PC的优点很明显,就是原理简单,实现方便。简单也意味着很多地方无法完美。这里有三个核心缺陷:同步阻塞:无论是第一阶段还是第二阶段,所有的参与者资源和协调者资源都是被锁定的,只有当所有节点都准备好??了,事务协调者才会通知全局提交,参与者才会本地事务提交后释放资源。这样一个过程会比较长,对性能的影响也比较大。单点故障:如果协调器出现问题,整个两阶段提交过程将无法进行。另外,如果协调者在第二阶段失败,其他参与者将处于锁定事务资源的状态。数据不一致:当协调器在第二阶段向所有参与者发送Commit请求时,部分网络异常或者协调器本身在发送Commit请求之前崩溃,导致只有部分参与者收到了Commit请求,那么接收到的参与者将提交事务,从而形成数据不一致。由于2PC的简单方便,所以会出现上述的同步阻塞、单点故障、数据不一致等问题,所以在2PC的基础上进行了改进,提出了一个三阶段提交(3PC)已介绍。使用2PC有很多限制。首先是数据库需要支持XA规范,对性能和数据一致性数据不友好,所以Seata虽然支持XA模式,但主推的还是AT模式3PC共识算法三阶段提交(3PC)改进版的两阶段提交(2PC),引入了两个新特性的协调者和参与者都引入了超时机制,通过超时机制解决2PC的同步阻塞问题,避免了事务资源被永久锁定,并将第二阶段转化为三阶段,将第一阶段“两阶段提交协议”的“准备阶段”一分为二,形成新的三阶段事务处理协议:CanCommit、PreCommit、doCommit。3PC的详细提交过程这里不再赘述,与2PC相比,3PC最大的优势在于减少了参与者的Blocking范围,并且可以持续达到con协调器出现单点故障后的sensus。虽然通过超时机制解决了资源永久阻塞的问题,但是3PC还是存在数据不一致的问题。参与者收到PreCommit消息后,如果网络出现分区,协调者和参与者就无法正常通信。在这种情况下,参与者仍然会提交交易。了解了2PC和3PC后可以知道,两者都不能完全解决分布式JDBC操作下的数据一致性问题MySQLXA事务MySQL从5.0.3开始支持XA分布式事务,只有InnoDB存储引擎支持。MySQLConnector/J从5.0.0版本开始直接提供了对XA的支持。在DTP模型中,MySQL属于RM资源管理器,所以这里不再演示MySQL支持XA事务的说法,因为它只执行自己的单个事务Branch,我们使用JDBC来演示如何使用TM来控制多个RM完成2PC分布式事务。这里先说明一下需要引入Maven版本的GAV,因为8.x高版本去掉了对XA分布式事务的支持(可能也是我觉得没人会用)mysqlmysql-connector-java5.1.38/dependency>这里,公众号为了保证阅读的舒适,通过IDEA将多行代码合并为一行,如果小伙伴需要粘贴到IDEA中,格式化一下即可,因为基础XA协议的是2PC共识算法,所以看代码的时候可以对比上面文章中提到的DTP模型和2PC来理解和模拟错误和执行结果importcom.mysql.jdbc.jdbc2.optional.MysqlXAConnection;importcom.mysql.jdbc.jdbc2.optional.MysqlXid;importjavax.sql.XAConnection;importjavax.transaction.xa.XAException;importjavax.transaction.xa.XAResource;importjavax.transaction。xa.Xid;importjava.sql.*;publicclassMysqlXAConnectionTest{publicstaticvoidmain(String[]args)throwsSQLException{//true表示打印XA语句,用于调试booleanlogXaCommands=true;//获取资源管理器操作接口实例RM1Connectionconn1=DriverManager.getConnection("jdbc:mysql://localhost:3306/test","root","root");XAConnectionxaConn1=newMysqlXAConnection((com.mysql.jdbc.Connection)conn1,logXaCommands);XAResourcerm1=xaConn1.getXAResource();//获取资源管理器操作接口实例RM2Connectionconn2=DriverManager.getConnection("jdbc:mysql://localhost:3306/test","root","root");XAConnectionxaConn2=newMysqlXAConnection((com.mysql.jdbc.Connection)conn2,logXaCommands);XAResourcerm2=xaConn2.getXAResource();//AP(Application)请求TM(事务管理器)执行一个分布式事务,TM生成一个全局事务IDbyte[]gtrid="distributed_transaction_id_1".getBytes();intformatId=1;try{//==============分别在RM1和RM2上执行事务分支=====================//TM生成事务分支IDbyte[]bqual1="onRM1transaction_001".getBytes();Xidxid1=newMysqlXid(gtrid,bqual1,formatId);//执行RM1上的事务分支rm1.start(xid1,XAResource.TMNOFLAGS);PreparedStatementps1=conn1.prepareStatement("INSERTintouser(name)VALUES('jack')");ps1.execute();rm1.end(xid1,XAResource.TMSUCCESS);//TM生成事务分支IDbyte[]bqual2onRM2="transaction_002".getBytes();Xidxid2=newMysqlXid(gtrid,bqual2,formatId);//在RM2上执行事务分支rm2.start(xid2,XAResource.TMNOFLAGS);PreparedStatementps2=conn2.prepareStatement("INSERTintouser(name)VALUES('rose')");ps2.execute();rm2.end(xid2,XAResource.TMSUCCESS);//=====================二-phasecommit===================================//phase1:要求所有RM准备提交事务分支intrm1_prepare=rm1.prepare(xid1);intrm2_prepare=rm2.prepare(xid2);//phase2:提交所有事务分支if(rm1_prepare==XAResource.XA_OK&&rm2_prepare==XAResource.XA_OK){//所有事务分支准备成功,提交所有交易分支rm1。commit(xid1,false);rm2.commit(xid2,false);}else{//如果有事务分支不成功,回滚rm1.rollback(xid1);rm1.rollback(xid2);}}catch(XAExceptione){e.printStackTrace();}}}结语本文通过图文并茂的方式讲解了如何保证本地事务的四个特性,分布式事务的输出背景,以及为什么2PC和3PC无法解决分布式情况下的数据.一致性,最后通过JDBC演示2PC的执行流程相信看完之后你也会对分布式事务有深刻的印象,同时对DTP、XA、2PC这些比较容易混淆的概念也有了清晰的认识。这是《分布式事务》专栏第一章的开头。后面我们会通过消息中间件、可靠消息模型、SeataXA模型完成分布式事务的文章,总结不同实现方式的优缺点,选择适合的场景使用不同的分布式事务方案。笔者认为,最好的学习方式就是实战。如果你没有分布式事务的经验,你可以通过你正在编写的项目来模拟分布式事务的业务场景,在加深印象的同时更好地理解分布式事务。与业务解决方案相关的设计思路。