这篇文章解释了LIXA和XTA如何实现多语言分布式事务系统的开发,希望对正在学习中的你有用!
两阶段提交协议是在大型机(如大型机和UNIX服务器)时代设计的。 XA规范是在1991年定义的,当时典型的部署模型是将所有软件安装在单个服务器中。 令人惊讶的是,可以重复使用规范的一致部分以支持基于微服务的体系结构内的分布式事务。 这篇文章解释了LIXA和XTA如何实现多语言分布式事务系统的开发。
介绍
简而言之,两阶段提交协议[1]是需要两个阶段的专门共识协议。 在第一阶段,即投票阶段,所有相关资源都需要“准备”:积极的回答意味着已经达到“持久”状态。 第二阶段,即提交阶段,确认所有相关资源的新状态。 发生错误时,协议回滚并且所有资源都达到先前的状态。
XA规范
XA规范[2]是两阶段提交协议的最常见实现之一:它在九十年代开发的许多中间件中得到了支持,并且在JTA规范[3]中得到了利用。
历史验收
自从两阶段提交协议开始以来,就一直在争论中。一方面,发烧友试图在任何情况下都使用它。另一方面,批评者在所有情况下都避免了这种情况。
必须报告的第一个注意事项与性能有关:对于每个共识协议,两阶段提交都会增加事务花费的时间。这种副作用是无法避免的,必须在设计时加以考虑。
甚至众所周知,某些资源管理器在管理XA事务时会受到可伸缩性限制的影响:这种行为更多地取决于实现的质量,而不是两阶段提交协议本身。
滥用两阶段提交会严重损害分布式系统的性能,但是在显而易见的解决方案中尝试避免使用它会导致巴洛克式和过度设计的系统难以维护。更具体地说,当必须确保事务行为并且不使用诸如两阶段提交之类的共识协议时,对现有服务的集成需要进行认真的重新设计。
两阶段提交协议和微服务
许多作者不鼓励在微服务领域同时使用XA标准和两阶段提交协议。 读者可以在参考部分[4]中找到一些帖子。
无论如何,这篇文章提供了一些支持分布式事务的开发工具。 读者可以在几分钟内尝试使用它们,并评估重用它们的机会。
体系结构
LIXA是一个事务管理器,它实现两阶段提交并支持XA规范。 它是根据GNU公共许可证和次级GNU公共许可证的条款许可的免费开源软件; 可以从GitHub和SourceForge [5]免费下载。
XTA代表XA Transaction API,它是一个旨在满足当代编程需求的接口:它重用了LIXA项目开发的核心组件,并引入了新的编程模型(请参见下文)。 XTA当前可用于C,C ++,Java和Python。 它可以从源代码编译并安装在Linux系统中,也可以作为Docker容器执行。
上图显示了有关XTA体系结构的主要情况:与编程语言无关,“应用程序”直接与XTA交互以管理事务,并与一个或多个“资源管理器”交互以管理持久数据。 典型的资源管理器是MySQL / MariaDB和PostgreSQL。
上图显示了本文中解释的示例实现的高级体系结构:
- “ rest-client”是使用Python 3开发的客户端程序; 它将数据持久保存在MySQL数据库中。
- “ rest-server”是用Java实现的服务,通过REST API进行调用; 它将数据持久存储在PostgreSQL数据库中。
··“ lixad”是LIXA状态服务器,它代表XTA进程保留事务状态。
为了简便起见,所有组件都作为Docker容器执行[6],但是也可以完成传统安装。
执行示例
在开始之前,你需要一个具有两个基本工具的操作系统:git和Docker。 两者都是免费提供的,并且易于在Linux,MacOS和Windows中进行设置。
设定
首先,克隆包含所需内容的git存储库:
1 git clone https://github.com/tiian/lixa-docker.git
然后转到示例目录:
1 cd lixa-docker/examples/PythonJavaREST
为“ rest-client”和“ rest-server”构建Docker镜像:
1 docker build -f Dockerfile-client -t rest-client . 2 3 docker build -f Dockerfile-server -t rest-server .
检查生成的图像,你应该看到类似以下内容的图像:
1 docker images | grep rest 2 3 rest-server latest 81eda2af0fd4 25 hours ago 731MB 4 5 rest-client latest 322a3a26e040 25 hours ago 390MB
启动MySQL,PostgreSQL和LIXA状态服务器(lixad):
1 docker run --rm -e MYSQL_ROOT_PASSWORD=mysecretpw -p 3306:3306 -d lixa/mysql 2 3 docker run --rm -e POSTGRES_PASSWORD=lixa -p 5432:5432 -d lixa/postgres -c 'max_prepared_transactions=10' 4 5 docker run --rm -p 2345:2345 -d lixa/lixad
检查启动的容器,你应该看到类似以下内容的内容:
1 docker ps | grep lixa 2 3 16099992bd82 lixa/lixad "/home/lixa/lixad-en…" 6 seconds ago Up 3 seconds 0.0.0.0:2345->2345/tcp sharp_yalow 4 5 15297ed6ebb1 lixa/postgres "docker-entrypoint.s…" 13 seconds ago Up 9 seconds 0.0.0.0:5432->5432/tcp unruffled_brahmagupta 6 7 3275a2738237 lixa/mysql "docker-entrypoint.s…" 21 seconds ago Up 18 seconds 0.0.0.0:3306->3306/tcp, 33060/tcp sharp_wilson
启动程序
激活Java服务(用Docker主机的IP地址替换IP地址“ 192.168.123.35”):
1 docker run -ti --rm -e MAVEN_OPTS="-Djava.library.path=/opt/lixa/lib" -e LIXA_STATE_SERVERS="tcp://192.168.123.35:2345/default" -e PQSERVER="192.168.123.35" -p 18080:8080 rest-server
等待服务就绪,然后在控制台中检查以下消息:
1 Mar 24, 2019 9:21:45 PM org.glassfish.grizzly.http.server.NetworkListener start 2 3 INFO: Started listener bound to [0.0.0.0:8080] 4 5 Mar 24, 2019 9:21:45 PM org.glassfish.grizzly.http.server.HttpServer start 6 7 INFO: [HttpServer] Started. 8 9 Jersey app started with WADL available at http://0.0.0.0:8080/xta/application.wadl 10 11 Hit enter to stop it...
从另一个终端启动Python客户端(用Docker主机的IP地址替换IP地址“ 192.168.123.35”):
1 docker run -ti --rm -e SERVER="192.168.123.35" -e LIXA_STATE_SERVERS="tcp://192.168.123.35:2345/default" rest-client
此时,你应该在Java服务的控制台中看到一些消息:
1 ***** REST service called: xid='1279875137.c5b6d8bf7d584065b4ee29d62fe35ab5.ac3d62eb862b49fe6a50bfee46d142b9', oper='delete' ***** 2 3 2019-03-24 21:23:04.047857 [1/139693866854144] INFO: LXC000I this process is starting a new LIXA transaction manager (lixa package version is 1.7.6) 4 5 Created a subordinate branch with XID '1279875137.c5b6d8bf7d584065b4ee29d62fe35ab5.ac3d62eb862b49fe9de52bd41657494a' 6 7 PostgreSQL: executing SQL statement >DELETE FROM authors WHERE id=1804< 8 9 Executing first phase of commit (prepare) 10 11 Returning 'PREPARED' to the client 12 13 Executing second phase of commit 14 15 ***** REST service called: xid='1279875137.91f1712af1164dd38dcc0b14d58819d2.ac3d62eb862b49fe6a50bfee46d142b9', oper='insert' ***** 16 17 Created a subordinate branch with XID '1279875137.91f1712af1164dd38dcc0b14d58819d2.ac3d62eb862b49fe20cca6a7fc7c4802' 18 19 PostgreSQL: executing SQL statement >INSERT INTO authors VALUES(1804, 'Hawthorne', 'Nathaniel')< 20 21 Executing first phase of commit (prepare) 22 23 Returning 'PREPARED' to the client 24 25 Executing second phase of commit
还有Python客户端控制台中的其他消息:
1 2019-03-24 21:23:03.909808 [1/140397122036736] INFO: LXC000I this process is starting a new LIXA transaction manager (lixa package version is 1.7.6) 2 3 ***** REST client ***** 4 5 MySQL: executing SQL statement >DELETE FROM authors WHERE id=1840< 6 7 Calling REST service passing: xid='1279875137.c5b6d8bf7d584065b4ee29d62fe35ab5.ac3d62eb862b49fe6a50bfee46d142b9', oper='delete' 8 9 Server replied >PREPARED< 10 11 Executing transaction commit 12 13 ***** REST client ***** 14 15 MySQL: executing SQL statement >INSERT INTO authors VALUES(1840, 'Zola', 'Emile')< 16 17 Calling REST service passing: xid='1279875137.91f1712af1164dd38dcc0b14d58819d2.ac3d62eb862b49fe6a50bfee46d142b9', oper='insert' 18 19 Server replied >PREPARED< 20 21 Executing transaction commit
执行说明
Python客户端:
- 第1行:客户启动其事务管理器。·
- 第3行:客户端在MySQL中执行SQL语句(“ DELETE”)。
- 第4行:客户端调用Java服务,并传递事务标识符(xid)和所需的操作(“删除”)。
Java服务
- 第1行:服务被调用,它接收事务标识符(xid)和所需的操作(“删除”)。
- 第2行:服务启动其事务管理器。
- 第3行:服务分支了全局事务,并创建了一个新的事务标识符。
- 第4行:服务在PostgreSQL中执行SQL语句(“ DELETE”)。·
- 第5行:服务执行提交协议的第一阶段,“准备” PostgreSQL。
- 第6行:服务将结果“ PREPARED”返回给客户端。·
- 第7行:服务执行提交协议的第二阶段。·
- Python客户端:
- 第5行:客户从服务中收到结果“ PREPARED”。·
- 第6行:客户端执行提交协议。
其余步骤对第二个SQL语句(“ INSERT”)重复相同的操作。
XTA编程模型
上面描述的客户端/服务器示例实现了XTA支持的模式之一:“多个应用程序,并发分支/伪同步”。
上面的图描述了客户端和服务器之间的交互。
序列图的事务部分由红色虚线框界定。
洋红色的虚线框包含REST调用和提交协议的第一阶段:tx.commit()被称为传递“ true”作为“非阻塞”参数的值。
蓝色虚线框包含提交协议的第二阶段。
该图未显示“ rest-client”,“ rest-server”和LIXA状态服务器之间的交互:
- 当Java服务器的后台线程调用tx.commit(false)时,魔术就会发生。
- LIXA状态服务器识别多个分支事务,并阻塞“ rest-server”,直到“ rest-client”完成提交的第一阶段。
- 届时,以图中的虚线绿色标记,已达成全球共识。
最后,两个参与者都可以继续执行提交协议的第二阶段。
如果在客户端或服务器端发生崩溃,则当事方会回滚或自动恢复第二次:有关这些情况的说明留待以后发表。
提供以下代码,以显示必须如何使用XTA对象和方法。
Python客户端代码
忽略样板和脚手架,这是Python客户端源代码中有趣的部分:
1 # initialize XTA environment 2 3 Xta_init() 4 5 # create a new MySQL connection 6 7 # Note: using MySQLdb functions 8 9 rm = MySQLdb.connect(host=hostname, user="lixa", password="passw0rd", db="lixa") 10 11 # create a new XTA Transaction Manager object 12 13 tm = TransactionManager() 14 15 # create an XA resource for MySQL 16 17 # second parameter "MySQL" is descriptive 18 19 # third parameter "localhost,0,lixa,,lixa" identifies the specific database 20 21 xar = MysqlXaResource(rm._get_native_connection(), "MySQL", 22 23 hostname + "/lixa") 24 25 # Create a new XA global transaction and retrieve a reference from 26 27 # the TransactionManager object 28 29 tx = tm.createTransaction() 30 31 # Enlist MySQL resource to transaction 32 33 tx.enlistResource(xar) 34 35 sys.stdout.write("***** REST client *****\n") 36 37 # Start a new XA global transaction with multiple branches 38 39 tx.start(True) 40 41 # Execute DELETE statement 42 43 sys.stdout.write("MySQL: executing SQL statement >" + delete_stmt + "<\n") 44 45 cur = rm.cursor() 46 47 cur.execute(delete_stmt) 48 49 # Retrieving xid 50 51 xid = tx.getXid().toString() 52 53 # Calling server passing xid 54 55 sys.stdout.write("Calling REST service passing: xid='" + xid + "', oper='delete'\n") 56 57 r = requests.post("http://" + hostname + ":18080/xta/myresource", 58 59 data={'xid':xid, 'oper':'delete'}) 60 61 sys.stdout.write("Server replied >" + r.text + "<\n") 62 63 # Commit the transaction 64 65 sys.stdout.write("Executing transaction commit\n") 66 67 tx.commit()
- 第14行:XA资源(xar)链接到MySQL连接(rm)。
- 第22行:XA资源已加入事务对象。
- 第26行:全局事务已启动。
- 第38行:客户端调用REST服务。
- 第44行:交易已提交。
Java服务器代码
忽略样板和脚手架,该示例使用Jersey框架,这是Java服务器源代码中有趣的部分:
1 // 1. create an XA Data Source 2 3 xads = new PGXADataSource(); 4 5 // 2. set connection parameters (one property at a time) 6 7 xads.setServerName(System.getenv("PQSERVER")); 8 9 xads.setDatabaseName("lixa"); 10 11 xads.setUser("lixa"); 12 13 xads.setPassword("passw0rd"); 14 15 // 3. get an XA Connection from the XA Data Source 16 17 xac = xads.getXAConnection(); 18 19 // 4. get an XA Resource from the XA Connection 20 21 xar = xac.getXAResource(); 22 23 // 5. get an SQL Connection from the XA Connection 24 25 conn = xac.getConnection(); 26 27 // 28 29 // XTA code 30 31 // 32 33 // Create a mew XTA Transaction Manager 34 35 tm = new TransactionManager(); 36 37 // Create a new XA global transaction using the Transaction 38 39 // Manager as a factory 40 41 tx = tm.createTransaction(); 42 43 // Enlist PostgreSQL resource to transaction 44 45 tx.enlistResource(xar, "PostgreSQL", 46 47 System.getenv("PQSERVER") + ";u=lixa;db=lixa"); 48 49 // create a new branch in the same global transaction 50 51 tx.branch(xid); 52 53 System.out.println("Created a subordinate branch " + 54 55 "with XID '" + tx.getXid().toString() + "'"); 56 57 // 58 59 // Create and Execute a JDBC statement for PostgreSQL 60 61 // 62 63 System.out.println("PostgreSQL: executing SQL statement >" + 64 65 sqlStatement + "<"); 66 67 // create a Statement object 68 69 stmt = conn.createStatement(); 70 71 // Execute the statement 72 73 stmt.executeUpdate(sqlStatement); 74 75 // close the statement 76 77 stmt.close(); 78 79 // perform first phase of commit (PREPARE ONLY) 80 81 System.out.println("Executing first phase of commit (prepare)"); 82 83 tx.commit(true); 84 85 // start a backgroud thread: control must be returned to the client 86 87 // but finalization must go on in parallel 88 89 new Thread(new Runnable() { 90 91 @Override 92 93 public void run() { 94 95 try { 96 97 // perform second phase of commit 98 99 System.out.println("Executing second phase of commit"); 100 101 tx.commit(false); 102 103 // Close Statement, SQL Connection and XA 104 105 // Connection for PostgreSQL 106 107 stmt.close(); 108 109 conn.close(); 110 111 xac.close(); 112 113 } catch (XtaException e) { 114 115 System.err.println("XtaException: LIXA ReturnCode=" + 116 117 e.getReturnCode() + " ('" + 118 119 e.getMessage() + "')"); 120 121 e.printStackTrace(); 122 123 } catch (Exception e) { 124 125 e.printStackTrace(); 126 127 } 128 129 } 130 131 }).start(); 132 133 System.out.println("Returning 'PREPARED' to the client"); 134 135 return "PREPARED";
- 第11行:PostgreSQL连接(xac)检索XA资源(xar)。
- 第23行:XA资源已加入事务对象。
- 第26行:创建了全球交易的新分支。
- 第42行:落实协议的第一阶段(“准备”)。
- 第51行:提交协议的第二阶段由后台线程执行。
- 第69行:服务将结果返回给调用方。
LIXA的设计和开发具有清晰明确的体系结构:所有事务信息都由状态服务器(lixad)保留,大多数事务逻辑由客户端库(lixac及其派生类)管理。 “逻辑”与“状态”之间的强大去耦关系使事务管理功能可以嵌入“应用程序”中,而无需框架或应用服务器。
XTA进一步推动了“分布式事务”的概念:客户端和服务器不必由任何类型的“主管中间件”执行,因为它们可以自动协调自己与LIXA状态服务器的交互。客户端必须传递给服务器的唯一信息是XID的ASCII字符串表示形式。过去实施的其他方法需要在应用服务器之间进行配置和协调;大多数时候,甚至通信协议也必须了解事务性方面。
此外,XTA在同一个全局事务中支持多个客户端/服务器:同一个XID可以由许多被称为服务的分支,甚至是分层的。
XTA支持同步协议(如本示例中所示的RESTful),以及通过不同模式(“多个应用程序,并发分支/伪异步”)的异步协议。
结论
XTA API与LIXA状态服务器结合使用,可以开发实现2个或更多应用程序(服务)之间的ACID [7]分布式事务的系统。 XTA支持开发使用多种资源管理器和任何通信协议的多语言交易系统,而无需某种应用程序管理器。
抽丝剥茧,细说架构那些事 通过 优锐课的微服务体系分享,自己又掌握了新技能~
如有不足之处,欢迎朋友们评论补充!