分布式事务的两种实现:2PC和3PC

分布式事务是指跨多个独立的计算系统或数据库进行的事务。在分布式环境中,事务通常需要在不同的节点上执行操作,这些节点可能位于不同的物理位置。为了保证事务的原子性、一致性、隔离性和持久性(也称为ACID属性),需要特殊的协议来协调这些不同节点之间的操作。

  1. 分布式事务:当事务涉及多个分布式系统或组件时,就形成了分布式事务。比如,在一个电子商务应用中,一个订单可能需要同时更新库存系统和账户余额,这两个系统可能部署在不同的服务器上。

  2. 2PC(Two-Phase Commit,两阶段提交):2PC是一种确保分布式事务中所有参与者要么全部提交事务,要么全部回滚事务的协议。它分为两个阶段:

    • 准备阶段(投票阶段):协调者询问所有参与者是否准备好提交事务,并等待参与者响应。参与者执行事务操作,将数据写入到日志中,但不提交,然后反馈给协调者它们是否准备好。
    • 提交阶段:如果所有参与者都准备好提交,协调者发送一个全局提交的命令给所有参与者;如果任一参与者未准备好,协调者发送一个全局回滚的命令。
  3. 3PC(Three-Phase Commit,三阶段提交):3PC是对2PC的改进,增加了一个额外的阶段来减少阻塞和减少单点故障的可能性。它包括三个阶段:

    • 预提交阶段:协调者向所有参与者发送预提交请求,参与者准备事务并响应是否可以提交。
    • 准备阶段:协调者收到所有参与者的确认后,向它们发送准备提交的请求。参与者记录事务即将提交的日志,并告知协调者它们已经准备好。
    • 提交/中断阶段:根据前面阶段的结果,协调者决定是提交还是中断事务,并通知所有参与者相应的操作。

这两种协议各有优缺点,2PC更简单但存在阻塞问题,而3PC通过引入额外的阶段来降低阻塞和单点故障的风险,但实现更复杂。在实际应用中,选择哪种协议取决于系统的具体需求和设计。

两阶段提交原理

两阶段提交(2PC)是一种分布式事务协议,旨在确保事务在所有参与者中要么全部提交,要么全部回滚,从而保持数据的一致性。从源码角度解析2PC,我们通常会涉及事务协调者(Transaction Coordinator)和参与者(Participants)的交互。这里我将从概念上描述2PC的工作流程,因为具体实现可能因不同的库和语言而异。

两阶段提交的阶段

第一阶段:准备阶段

  1. 投票:事务协调者向所有参与者发送一个准备(PREPARE)命令。参与者执行事务操作,将变更写入日志,但不提交。这确保了即使系统崩溃,参与者也有足够的信息来完成事务。

  2. 响应:参与者回应协调者它们是否准备好提交事务。如果参与者准备好提交,它回应一个"同意"(YES)消息;如果无法提交,它发送一个"拒绝"(NO)消息。

第二阶段:提交/回滚阶段

  1. 所有同意:如果所有参与者都回应"同意",协调者发送一个提交(COMMIT)命令给所有参与者。参与者收到提交命令后,将事务提交到数据库,并向协调者发送一个完成消息。

  2. 任一拒绝:如果任一参与者发送了"拒绝"消息,或者协调者在指定时间内未收到回应,协调者将发送回滚(ROLLBACK)命令给所有参与者。参与者收到回滚命令后,撤销其事务操作,并向协调者发送完成消息。

从源码角度的解析

在源码层面,实现2PC通常涉及以下几个关键组件:

  1. 事务协调者:负责协调事务的整个生命周期,发送指令给所有参与者,并根据参与者的回应决定下一步行动。

  2. 参与者:执行事务操作,根据协调者的指令进行提交或回滚。

  3. 日志系统:在两阶段提交过程中,参与者和协调者都需要日志来记录事务的状态。这是确保事务持久性和恢复能力的关键。

以Java环境为例,如果我们使用JTA(Java Transaction API)来处理分布式事务,我们会有如下组件:

  • Transaction Manager:充当协调者,管理事务的边界,确保所有资源都在同一个事务中被正确处理。

  • Resource Manager:充当参与者,通常是数据库或消息服务,管理实际的资源。

  • XA Protocol:一种基于两阶段提交的协议,定义了资源管理器(RM)和事务管理器(TM)之间的交互。

具体的代码实现依赖于具体使用的框架和库。例如,在使用Spring框架和Atomikos事务管理器的情况下,开发者并不需要直接处理2PC的底层细节,而是通过声明式事务控制来管理分布式事务。

三阶段提交原理

三阶段提交(3PC)是两阶段提交(2PC)的一个改进版本,旨在降低系统在协调器故障时的阻塞和单点故障的可能性。3PC通过添加一个额外的阶段来增强系统的容错性,这个额外的阶段位于原有的两阶段之前,称为"预提交"阶段。下面详细解析3PC的原理和各个阶段的流程,以及从源码的角度探讨实现细节。

三阶段提交的阶段

第一阶段:预提交

  1. 协调器行动:事务协调器向所有参与者发送一个预提交请求。
  2. 参与者响应:参与者执行事务操作,记录必要的日志,但不实际提交事务。然后,它们向协调器发送确认消息,表明它们已准备好提交或无法提交(准备好或未准备好)。

第二阶段:准备阶段

  1. 协调器决定:如果所有参与者都准备好提交,协调器进入第二阶段,发送一个准备提交的请求;如果任何参与者未准备好,或协调器在超时后没有收到所有参与者的响应,它将中止事务。
  2. 参与者准备:参与者在接收到准备提交的请求后,将事务状态更改为可提交,并向协调器发送已准备好提交的响应。

第三阶段:提交或中断

  1. 提交:如果协调器从所有参与者接收到已准备好提交的响应,它将进入提交阶段,向所有参与者发送提交请求。参与者接收到提交请求后,完成事务的提交,并向协调器发送提交完成的消息。
  2. 中断:如果协调器决定中断事务(例如,因为某个参与者未准备好或通信失败),它将向所有参与者发送回滚请求。参与者收到回滚请求后,撤销事务,并向协调器发送回滚完成的消息。

从源码角度的解析

从源码角度实现3PC,关键是理解协调器和参与者之间的交互协议。这里不涉及特定语言的具体实现,而是提供一个概念上的框架,因为实际的实现细节会根据使用的编程语言和框架而有所不同。

  1. 协调器:需要实现一个协调器类,负责管理整个事务流程,包括发送预提交、准备提交和提交/回滚命令,以及接收参与者的响应。
pseudo 复制代码
class Coordinator {
    void sendPreCommit() {}
    void receivePreCommitResponses() {}
    void sendPrepare() {}
    void receivePrepareResponses() {}
    void commitOrAbort() {}
}
  1. 参与者:需要实现一个参与者类,能够响应协调器的各种请求,执行相应的事务操作,发送状态更新,并根据协调器的指示提交或回滚事务。
pseudo 复制代码
class Participant {
    void receivePreCommit() {}
    void sendPreCommitResponse() {}
    void receivePrepare() {}
    void prepareToCommit() {}
    void commit() {}
    void abort() {}
}
  1. 网络通信:在分布式系统中,协调器和参与者通常位于不同的机器上。因此,需要实现网络通信机制,允许协调器和参与者之间交换消息。这可能涉及到socket编程、REST API调用或使用某种消息队列系统。

  2. 持久持久化日志:在3PC中,协调器和参与者都需要维护事务日志。这些日志记录事务的状态和关键操作,以便在系统故障后能够恢复到正确的状态。以下是事务日志的关键操作:

pseudo 复制代码
class TransactionLog {
    void writePreCommit() {}
    void writePrepare() {}
    void writeCommit() {}
    void writeAbort() {}
}

当事务进入不同阶段时,参与者和协调器会在各自的日志中记录相应的状态。例如,当参与者接收到预提交请求时,它会在日志中记录此状态。如果系统在提交前崩溃,参与者可以检查其日志来确定应该继续提交还是回滚事务。

示例代码框架

以下是3PC过程中协调器和参与者交互的一个高级伪代码框架,展示了基本的逻辑流程:

pseudo 复制代码
// 协调器伪代码
class Coordinator {
    List<Participant> participants;

    void executeTransaction() {
        if (sendPreCommit() == SUCCESS && receivePreCommitResponses() == SUCCESS) {
            if (sendPrepare() == SUCCESS && receivePrepareResponses() == SUCCESS) {
                commitOrAbort(COMMIT);
            } else {
                commitOrAbort(ABORT);
            }
        } else {
            commitOrAbort(ABORT);
        }
    }

    // 发送预提交请求,接收响应,发送准备提交请求等方法在此实现
}

// 参与者伪代码
class Participant {
    State state;

    void receivePreCommit() {
        // 执行事务操作,更新状态,写入日志
        sendPreCommitResponse();
    }

    void receivePrepare() {
        // 准备提交,更新状态,写入日志
        sendPrepareResponse();
    }

    void commit() {
        // 提交事务,写入日志
    }

    void abort() {
        // 回滚事务,写入日志
    }

    // 接收预提交请求,发送响应,接收准备提交请求等方法在此实现
}

在实际实现中,需要处理网络通信、错误处理、超时处理等多种情况,确保协议的健壮性和容错性。3PC相比2PC增加了预提交阶段,提高了系统的可用性和容错性,但同时也增加了通信开销和复杂性。在实际系统设计中,需要根据具体需求权衡使用2PC还是3PC。此外,还有其他分布式事务协议,如Paxos和Raft,它们在特定场景下可能更加适用。

两阶段提交实例代码

在Spring Boot项目中实现2PC(两阶段提交)通常涉及多个数据库或服务的事务协调。由于2PC是一个复杂的过程,涉及到事务管理器、资源管理器等多个组件的协作,我们通常依赖于一些成熟的框架来简化实现。比如,我们可以使用Spring的JTA(Java Transaction API)支持,结合一个兼容JTA的事务管理器(如Atomikos)来实现。

这里是一个使用Spring Boot,JTA和Atomikos来实现2PC的简单示例。假设我们有两个数据库,我们需要在这两个数据库中执行事务性操作,并确保这些操作要么全部成功,要么全部失败。

  1. 添加依赖 :首先,在pom.xml中添加必要的依赖:
xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

这里我们以H2数据库为例,实际项目中可根据需要替换为其他数据库依赖。

  1. 配置数据源和JTA :在application.properties中配置两个数据源和JTA事务管理器:
properties 复制代码
# 第一个数据源
spring.jta.atomikos.datasource.primary.unique-resource-name=primary
spring.jta.atomikos.datasource.primary.xa-data-source-classname=org.h2.jdbcx.JdbcDataSource
spring.jta.atomikos.datasource.primary.xa-properties.url=jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE

# 第二个数据源
spring.jta.atomikos.datasource.secondary.unique-resource-name=secondary
spring.jta.atomikos.datasource.secondary.xa-data-source-classname=org.h2.jdbcx.JdbcDataSource
spring.jta.atomikos.datasource.secondary.xa-properties.url=jdbc:h2:mem:testdb2;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
  1. 创建实体和仓库:接下来,创建实体类和Spring Data JPA仓库接口,分别对应两个数据库。
java 复制代码
@Entity
public class EntityOne {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String name;
    // getters and setters
}

@Entity
public class EntityTwo {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String name;
    // getters and setters
}

public interface EntityOneRepository extends JpaRepository<EntityOne, Long> {}

public interface EntityTwoRepository extends JpaRepository<EntityTwo, Long> {}
  1. 编写服务层 :在服务层中,我们使用@Transactional注解来定义一个事务性方法,该方法会同时操作两个数据库。
java 复制代码
@Service
public class MyService {

    @Autowired
    private EntityOneRepository entityOneRepository;

    @Autowired
    private EntityTwoRepository entityTwoRepository;

    @Transactional
    public void createEntities() {
        EntityOne entityOne = new EntityOne();
        entityOne.setName("Test1");
        entityOneRepository.save(entityOne);

        EntityTwo entityTwo = new EntityTwo();
        entityTwo.setName("Test2");
        entityTwoRepository.save(entityTwo);

        // 模拟异常,观察事务回滚
        if (true) {
            throw new RuntimeException("模拟异常");
        }
    }
}

在上述代码中,createEntities方法中创建了两个实体,并保存到两个不同的数据库中。如果过程中出现异常,则两个操作都会被回滚,这就是2PC的体现。这个例子展示了如何在Spring Boot应用中通过JTA和Atomikos实现两阶段提交。在实际应用中,可能会更复杂,特别是当涉及到跨多个服务或应用时。在这些情况下,可能需要考虑使用分布式事务协调器如Seata等工具来管理分布式事务。

三阶段提交实例代码

在Spring Boot中实现3PC(三阶段提交)是相对复杂的,因为标准的Spring框架或常用的事务管理器如Atomikos并不直接支持3PC。三阶段提交协议是两阶段提交的改进版,增加了一个额外的准备阶段,以减少在出现故障时资源锁定的时间,但这也导致其实现变得更为复杂。

在实际应用中,3PC通常不如2PC流行,部分原因是由于其增加的复杂性和在某些场景下只提供了有限的改进。然而,如果你确实需要在Spring Boot项目中实现3PC,你可能需要依赖外部系统或服务,如使用支持3PC的分布式事务管理器。

一个实际的例子是使用TCC(Try-Confirm-Cancel)模式实现的3PC,这在一些分布式事务管理框架中被支持,如Seata。TCC是一种补偿事务模式,可以看作是3PC的一种实现方式,其中Try、Confirm、Cancel分别对应于3PC的三个阶段。

下面是一个使用Seata来实现类似3PC行为的示例:

  1. 添加依赖 :首先,在pom.xml中添加Seata和数据库相关的依赖。
xml 复制代码
<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>最新版本</version>
</dependency>
  1. 配置Seata和数据源 :在application.propertiesapplication.yml中配置Seata和数据源。
properties 复制代码
seata.enabled=true
seata.application-id=my-spring-boot-app
seata.tx-service-group=my_tx_group

同时,需要配置Seata服务端的相关信息,通常这需要一个独立的Seata服务器。

  1. 实现业务逻辑 :在服务层实现业务逻辑,使用@GlobalTransactional注解来声明全局事务。
java 复制代码
@Service
public class MyService {

    @Autowired
    private EntityOneRepository entityOneRepository;

    @Autowired
    private EntityTwoRepository entityTwoRepository;

    @GlobalTransactional
    public void createEntities() {
        // Try 阶段
        // 执行业务逻辑,例如插入记录到数据库等
        entityOneRepository.save(new EntityOne("Test1"));
        entityTwoRepository.save(new EntityTwo("Test2"));

        // 确认或取消阶段通常是由Seata管理,基于事务执行的结果自动进行
    }
}

在这个例子中,createEntities方法中的操作会在全局事务的上下文中执行,Seata管理着这个全局事务的各个分支事务,并确保它们要么全部成功提交,要么全部回滚。请注意,这个例子展示的是一个基于TCC模式的3PC近似实现。如果需要严格的3PC协议实现,需要查看更专业的分布式事务解决方案或自行实现相应的事务管理逻辑。由于3PC的复杂性和相对较少的使用场景,通常推荐先考虑是否有其他更简单的解决方案可以满足需求。

相关推荐
m0_748254884 分钟前
DataX3.0+DataX-Web部署分布式可视化ETL系统
前端·分布式·etl
程序猿进阶1 小时前
深入解析 Spring WebFlux:原理与应用
java·开发语言·后端·spring·面试·架构·springboot
字节程序员1 小时前
Jmeter分布式压力测试
分布式·jmeter·压力测试
ProtonBase2 小时前
如何从 0 到 1 ,打造全新一代分布式数据架构
java·网络·数据库·数据仓库·分布式·云原生·架构
时时刻刻看着自己的心2 小时前
clickhouse分布式表插入数据不用带ON CLUSTER
分布式·clickhouse
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭10 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
Data跳动10 小时前
Spark内存都消耗在哪里了?
大数据·分布式·spark
李小白6611 小时前
Spring MVC(上)
java·spring·mvc
Java程序之猿12 小时前
微服务分布式(一、项目初始化)
分布式·微服务·架构