彻底掌握分布式事务2PC、3PC模型(三)
3PC 一致性算法
三阶段提交(3PC)是二阶段提交(2PC)的一个改良版本,引入了两个新的特性
- 协调者和参与者均引入超时机制,通过超时机制来解决 2PC 的同步阻塞问题,避免事务资源被永久锁定
- 把二阶段演变为三阶段,二阶段提交协议中的第一阶段"准备阶段"一分为二,形成了新的 CanCommit、PreCommit、do Commit 三个阶段组成事务处理协议
这里将不再赘述 3PC 的详细提交过程,3PC 相比较于 2PC 最大的优点就是降低了参与者的阻塞范围,并且能够在协调者出现单点故障后继续达成一致
虽然通过超时机制解决了资源永久阻塞的问题,但是 3PC 依然存在数据不一致的问题。当参与者接收到 PreCommit 消息后,如果网络出现分区,此时协调者与参与者无法进行正常通信,这种情况下,参与者依然会进行事务的提交
通过了解 2PC 和 3PC 之后,我们可以知道这两者都无法彻底解决分布式下的数据一致性
JDBC 操作 MySQL XA 事务
MySQL 从 5.0.3 开始支持 XA 分布式事务,且只有 InnoDB 存储引擎支持。MySQL Connector/J 从5.0.0 版本之后开始直接提供对 XA 的支持
在 DTP 模型中,MySQL 属于 RM 资源管理器,所以这里就不再演示 MySQL 支持 XA 事务的语句,因为它执行的只是自己单一事务分支,我们通过 JDBC 来演示如何通过 TM 来控制多个 RM 完成 2PC 分布式事务
这里先来说明需要引入 GAV 的 Maven 版本,因为高版本 8.x 移除了对 XA 分布式事务的支持(可能也是觉得没人会用吧)
<dependencies>
<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.38</version>
</dependency>
</dependencies>
这里为了保证在公众号阅读的舒适性,通过 IDEA 将多行代码合并为一行了,如果小伙伴需要粘贴到 IDEA 中,格式化一下就好了
因为 XA 协议的基础是 2PC 一致性算法,所以小伙伴在看代码时可以对照上面文章讲的 DTP 模型和 2PC 来进行理解以及模拟错误和执行结果
import com.mysql.jdbc.jdbc2.optional.MysqlXAConnection;import com.mysql.jdbc.jdbc2.optional.MysqlXid;import javax.sql.XAConnection;import javax.transaction.xa.XAException;import javax.transaction.xa.XAResource;import javax.transaction.xa.Xid;import java.sql.*;
public class MysqlXAConnectionTest {
public static void main(String[] args) throws SQLException {
// true 表示打印 XA 语句, 用于调试
boolean logXaCommands = true;
// 获得资源管理器操作接口实例 RM1
Connection conn1 = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "root");XAConnection xaConn1 = new MysqlXAConnection((com.mysql.jdbc.Connection) conn1, logXaCommands);XAResource rm1 = xaConn1.getXAResource();
// 获得资源管理器操作接口实例 RM2
Connection conn2 = DriverManager.getConnection("jdbc:mysql://localhost:3306/test", "root", "root");XAConnection xaConn2 = new MysqlXAConnection((com.mysql.jdbc.Connection) conn2, logXaCommands);XAResource rm2 = xaConn2.getXAResource();
// AP(应用程序)请求 TM(事务管理器) 执行一个分布式事务, TM 生成全局事务 ID
byte[] gtrid = "distributed_transaction_id_1".getBytes();int formatId = 1;
try {
// ============== 分别执行 RM1 和 RM2 上的事务分支 ====================
// TM 生成 RM1 上的事务分支 ID
byte[] bqual1 = "transaction_001".getBytes();Xid xid1 = new MysqlXid(gtrid, bqual1, formatId);
// 执行 RM1 上的事务分支
rm1.start(xid1, XAResource.TMNOFLAGS);PreparedStatement ps1 = conn1.prepareStatement("INSERT into user(name) VALUES ('jack')");ps1.execute();rm1.end(xid1, XAResource.TMSUCCESS);
// TM 生成 RM2 上的事务分支 ID
byte[] bqual2 = "transaction_002".getBytes();Xid xid2 = new MysqlXid(gtrid, bqual2, formatId);
// 执行 RM2 上的事务分支
rm2.start(xid2, XAResource.TMNOFLAGS);PreparedStatement ps2 = conn2.prepareStatement("INSERT into user(name) VALUES ('rose')");ps2.execute();rm2.end(xid2, XAResource.TMSUCCESS);
// =================== 两阶段提交 ================================
// phase1: 询问所有的RM 准备提交事务分支
int rm1_prepare = rm1.prepare(xid1);int rm2_prepare = rm2.prepare(xid2);
// phase2: 提交所有事务分支
if (rm1_prepare == XAResource.XA_OK && rm2_prepare == XAResource.XA_OK) {
// 所有事务分支都 prepare 成功, 提交所有事务分支
rm1.commit(xid1, false);rm2.commit(xid2, false);
} else {
// 如果有事务分支没有成功, 则回滚
rm1.rollback(xid1);rm1.rollback(xid2);
}
} catch (XAException e) { e.printStackTrace(); } }}
结言
本文通过图文并茂的方式讲解了如何保证本地事务的四大特性,分布式事务的产出背景,以及 2PC、3PC 为何不能解决分布式情况下的数据一致性,最后通过 JDBC 演示了 2PC 的执行流程。相信大家看过后也对分布式事务有了较深的印象,同时对 DTP、XA、2PC 这几种比较容易混淆的概念有了清楚的认识
这是《分布式事务》专栏的第一章开篇,后面陆续完成通过消息中间件、可靠消息模型、Seata XA模型完成分布式事务的文章,并对不同的实现方式进行总结利弊,挑选出合适场景使用不同的分布式事务解决方案
作者认为最好的学习方式那就是实战,如果没有接触过分布式事务的小伙伴,可以通过自己正在写的项目,模拟出分布式事务的业务场景,加深印象的同时也能够更好理解分布式事务解决方案相关设计思路
创作不易,文章看到这里如果有所帮助,可以点个关注支持一下,祝好。我们下期见
站在巨人的肩膀
- 《从Paxos到Zookeeper分布式一致性原理与实践》
- 《田守枝Java技术博客》
文章转自公众号:龙台的技术笔记