MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

微服务架构的分布式事务处理

2023-05-183.9k 阅读

微服务架构下分布式事务的挑战

在传统的单体架构中,事务处理相对简单,因为所有的业务逻辑和数据都在一个应用程序中。数据库本身提供的事务机制,如ACID(原子性、一致性、隔离性、持久性)特性,可以很好地保证数据的一致性。然而,在微服务架构中,一个业务流程通常会涉及多个微服务,每个微服务可能有自己独立的数据库,这就带来了分布式事务处理的挑战。

例如,在一个电商系统中,下订单的业务流程可能涉及订单微服务、库存微服务和支付微服务。当用户下单时,订单微服务需要创建订单记录,库存微服务需要扣减库存,支付微服务需要处理支付操作。这一系列操作需要作为一个整体的事务来处理,要么全部成功,要么全部失败,以保证数据的一致性。但由于这些操作分布在不同的微服务和数据库中,传统的数据库事务机制无法直接应用。

分布式事务面临的问题主要包括以下几个方面:

  1. 网络问题:微服务之间通过网络进行通信,网络的不可靠性可能导致消息丢失、延迟或乱序。例如,在订单创建成功后,库存扣减的消息可能因为网络故障而未被库存微服务接收,从而导致数据不一致。
  2. 节点故障:某个微服务节点可能因为硬件故障、软件错误等原因而崩溃。如果在事务处理过程中,某个微服务节点发生故障,可能会导致事务无法正常完成。
  3. CAP 定理限制:在分布式系统中,Consistency(一致性)、Availability(可用性)和 Partition tolerance(分区容错性)三者只能同时满足其中两个。在微服务架构中,通常优先保证可用性和分区容错性,这就对一致性带来了挑战。

分布式事务处理模式

为了应对微服务架构下分布式事务的挑战,出现了多种分布式事务处理模式,下面详细介绍几种常见的模式。

2PC(两阶段提交)

2PC 是一种经典的分布式事务处理协议,它将事务的提交过程分为两个阶段:准备阶段(Prepare)和提交阶段(Commit)。

准备阶段:协调者向所有参与者发送 Prepare 消息,询问是否可以提交事务。参与者接收到 Prepare 消息后,执行事务操作,但不提交事务,然后向协调者回复 Yes 或 No。如果所有参与者都回复 Yes,说明所有参与者都准备好提交事务;如果有任何一个参与者回复 No,说明有参与者无法提交事务。

提交阶段:如果在准备阶段所有参与者都回复 Yes,协调者向所有参与者发送 Commit 消息,参与者接收到 Commit 消息后,正式提交事务;如果在准备阶段有参与者回复 No,协调者向所有参与者发送 Rollback 消息,参与者接收到 Rollback 消息后,回滚事务。

下面是一个简单的 2PC 代码示例(以 Java 为例,使用 JTA - Java Transaction API):

import javax.transaction.*;
import javax.transaction.xa.*;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;

public class TwoPhaseCommitExample {
    public static void main(String[] args) {
        try {
            // 获取事务管理器
            UserTransaction userTransaction = (UserTransaction) new InitialContext().lookup("java:comp/UserTransaction");
            // 获取第一个数据库连接
            XADataSource xaDataSource1 = new OracleXADataSource();
            xaDataSource1.setURL("jdbc:oracle:thin:@localhost:1521:xe");
            xaDataSource1.setUser("user1");
            xaDataSource1.setPassword("password1");
            XAConnection xaConnection1 = xaDataSource1.getXAConnection();
            Connection connection1 = xaConnection1.getConnection();
            XAResource xaResource1 = xaConnection1.getXAResource();
            // 获取第二个数据库连接
            XADataSource xaDataSource2 = new OracleXADataSource();
            xaDataSource2.setURL("jdbc:oracle:thin:@localhost:1521:xe");
            xaDataSource2.setUser("user2");
            xaDataSource2.setPassword("password2");
            XAConnection xaConnection2 = xaDataSource2.getXAConnection();
            Connection connection2 = xaConnection2.getConnection();
            XAResource xaResource2 = xaConnection2.getXAResource();

            userTransaction.begin();
            // 在第一个数据库执行操作
            PreparedStatement statement1 = connection1.prepareStatement("INSERT INTO TABLE1 (COLUMN1) VALUES ('VALUE1')");
            statement1.executeUpdate();
            // 在第二个数据库执行操作
            PreparedStatement statement2 = connection2.prepareStatement("INSERT INTO TABLE2 (COLUMN2) VALUES ('VALUE2')");
            statement2.executeUpdate();

            // 注册 XA 资源
            userTransaction.enlistResource(xaResource1);
            userTransaction.enlistResource(xaResource2);

            userTransaction.commit();

            connection1.close();
            connection2.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

2PC 的优点是实现相对简单,能够保证强一致性。但它也有明显的缺点:

  1. 性能问题:2PC 过程中需要协调者与参与者之间多次通信,性能开销较大。特别是在涉及大量微服务和数据库的场景下,性能问题会更加突出。
  2. 单点故障:协调者是整个 2PC 过程的核心,如果协调者发生故障,整个事务将无法继续进行。虽然可以通过选举等机制来解决协调者的单点故障问题,但这会增加系统的复杂性。
  3. 同步阻塞:在准备阶段和提交阶段,参与者需要等待协调者的指令,期间资源处于锁定状态,这可能会导致其他事务长时间等待,降低系统的并发性能。

3PC(三阶段提交)

3PC 是在 2PC 的基础上进行改进的分布式事务处理协议,它将事务的提交过程分为三个阶段:询问阶段(CanCommit)、预提交阶段(PreCommit)和提交阶段(DoCommit)。

询问阶段:协调者向所有参与者发送 CanCommit 消息,询问是否可以提交事务。参与者接收到 CanCommit 消息后,检查自身是否可以提交事务,然后向协调者回复 Yes 或 No。

预提交阶段:如果在询问阶段所有参与者都回复 Yes,协调者向所有参与者发送 PreCommit 消息,参与者接收到 PreCommit 消息后,执行事务操作,但不提交事务,然后向协调者回复 ACK。如果有任何一个参与者在询问阶段回复 No,协调者向所有参与者发送 Abort 消息,参与者接收到 Abort 消息后,回滚事务。

提交阶段:如果在预提交阶段所有参与者都回复 ACK,协调者向所有参与者发送 DoCommit 消息,参与者接收到 DoCommit 消息后,正式提交事务;如果在预提交阶段有参与者没有回复 ACK,协调者向所有参与者发送 Abort 消息,参与者接收到 Abort 消息后,回滚事务。

3PC 相比 2PC 的优点在于:

  1. 减少单点故障影响:3PC 引入了询问阶段,使得协调者在发送 PreCommit 消息之前,能够了解参与者的状态。如果协调者在发送 PreCommit 消息之前发生故障,新的协调者可以根据参与者在询问阶段的回复情况,决定是否继续提交事务,而不需要像 2PC 那样等待协调者恢复。
  2. 降低同步阻塞时间:在预提交阶段,参与者在回复 ACK 后,并不需要一直等待协调者的 DoCommit 消息,可以进行其他操作,直到接收到 DoCommit 或 Abort 消息。这样可以减少资源锁定的时间,提高系统的并发性能。

然而,3PC 也存在一些缺点:

  1. 复杂性增加:相比 2PC,3PC 增加了一个询问阶段,通信流程更加复杂,实现难度也相应增加。
  2. 一致性问题:虽然 3PC 在一定程度上降低了同步阻塞时间,但在网络分区等异常情况下,仍然可能出现数据不一致的问题。

TCC(Try - Confirm - Cancel)

TCC 是一种基于补偿机制的分布式事务处理模式,它将事务处理过程分为三个阶段:Try 阶段、Confirm 阶段和 Cancel 阶段。

Try 阶段:尝试执行业务操作,完成所有业务检查(一致性),预留必须的业务资源。例如,在电商系统的下单流程中,订单微服务在 Try 阶段创建订单记录,库存微服务在 Try 阶段检查库存是否足够,并锁定相应的库存。

Confirm 阶段:确认执行业务操作,真正提交事务。如果 Try 阶段所有操作都成功,在 Confirm 阶段,订单微服务正式提交订单,库存微服务扣减库存。

Cancel 阶段:取消执行业务操作,回滚 Try 阶段预留的业务资源。如果 Try 阶段有任何操作失败,在 Cancel 阶段,订单微服务删除订单记录,库存微服务释放锁定的库存。

下面是一个简单的 TCC 代码示例(以 Java 为例,使用 Spring Cloud Alibaba Seata 框架):

首先,定义 Try 方法:

@LocalTCC
public interface OrderTCCService {
    @TwoPhaseBusinessAction(name = "orderTCCAction", commitMethod = "commit", rollbackMethod = "rollback")
    boolean tryOrder(BusinessActionContext context);
}

@Service
public class OrderTCCServiceImpl implements OrderTCCService {
    @Override
    public boolean tryOrder(BusinessActionContext context) {
        // 创建订单记录
        System.out.println("尝试创建订单");
        return true;
    }
}

然后,定义 Confirm 方法:

@Service
public class OrderTCCServiceImpl implements OrderTCCService {
    @Override
    public boolean commit(BusinessActionContext context) {
        // 正式提交订单
        System.out.println("确认提交订单");
        return true;
    }
}

最后,定义 Cancel 方法:

@Service
public class OrderTCCServiceImpl implements OrderTCCService {
    @Override
    public boolean rollback(BusinessActionContext context) {
        // 删除订单记录
        System.out.println("取消订单");
        return true;
    }
}

TCC 的优点是:

  1. 性能较好:TCC 模式不需要像 2PC 和 3PC 那样长时间锁定资源,在 Try 阶段完成业务检查和资源预留后,就可以释放部分资源,提高了系统的并发性能。
  2. 灵活性高:TCC 模式的业务逻辑由开发者自己实现,能够更好地适应不同的业务场景。

但 TCC 也有一些缺点:

  1. 开发成本高:需要开发者自己实现 Try、Confirm 和 Cancel 三个阶段的业务逻辑,开发工作量较大。
  2. 一致性问题:如果在 Confirm 或 Cancel 阶段发生网络故障等异常情况,可能会导致数据不一致。需要通过重试机制等手段来保证最终一致性。

本地消息表

本地消息表是一种基于可靠消息最终一致性的分布式事务处理模式。它的核心思想是将分布式事务拆分为多个本地事务,通过消息队列来异步协调各个本地事务的执行。

具体实现过程如下:

  1. 业务系统:在业务系统的数据库中创建一个消息表,当业务操作发生时,首先将消息插入到消息表中,同时将业务操作作为一个本地事务提交。例如,在电商系统的下单流程中,订单微服务在创建订单记录的同时,将库存扣减的消息插入到消息表中。
  2. 消息发送服务:有一个专门的消息发送服务,定时从消息表中读取未发送的消息,并将其发送到消息队列中。消息发送成功后,更新消息表中消息的状态为已发送。
  3. 消息消费服务:库存微服务监听消息队列,当接收到库存扣减的消息后,执行库存扣减操作,并将操作结果反馈给消息队列。如果库存扣减操作失败,消息消费服务可以根据情况进行重试。

下面是一个简单的本地消息表代码示例(以 Java 为例,使用 Spring Boot 和 RabbitMQ):

首先,创建消息表:

CREATE TABLE message (
    id INT AUTO_INCREMENT PRIMARY KEY,
    content VARCHAR(255),
    status INT,
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

然后,定义消息发送服务:

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import javax.persistence.Query;
import java.util.List;

@Service
public class MessageSenderService {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @PersistenceContext
    private EntityManager entityManager;

    @Scheduled(fixedRate = 5000)
    public void sendMessages() {
        String jpql = "SELECT m FROM Message m WHERE m.status = 0";
        Query query = entityManager.createQuery(jpql);
        List<Message> messages = query.getResultList();
        for (Message message : messages) {
            rabbitTemplate.convertAndSend("exchange", "routingKey", message.getContent());
            message.setStatus(1);
            entityManager.merge(message);
        }
    }
}

最后,定义消息消费服务:

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class MessageConsumerService {
    @RabbitListener(queues = "queue")
    public void receiveMessage(String message) {
        // 执行库存扣减操作
        System.out.println("接收到消息:" + message);
    }
}

本地消息表的优点是:

  1. 实现简单:基于现有的数据库和消息队列技术,实现相对容易,对业务系统的侵入性较小。
  2. 可靠性高:通过消息队列的持久化和重试机制,能够保证消息的可靠传递和处理。

缺点是:

  1. 一致性问题:由于是异步处理,可能会存在一定时间的数据不一致。例如,在消息发送成功但库存微服务还未消费消息时,订单已创建但库存未扣减。
  2. 性能问题:消息发送服务定时读取消息表可能会对数据库造成一定的压力,特别是在消息量较大的情况下。

可靠消息最终一致性

可靠消息最终一致性与本地消息表类似,也是基于消息队列来实现分布式事务的最终一致性。不同之处在于,可靠消息最终一致性模式通常使用专门的消息中间件来保证消息的可靠传递,而不是依赖本地消息表。

在这种模式下,业务系统在执行本地事务时,同时向消息中间件发送消息。消息中间件保证消息的可靠存储和投递,如果消息投递失败,会进行重试。接收消息的微服务在接收到消息后,执行相应的业务操作,并反馈处理结果。

例如,在电商系统中,订单微服务在创建订单后,向消息中间件发送库存扣减消息。库存微服务监听消息中间件,接收到消息后扣减库存。如果库存扣减失败,库存微服务可以通知消息中间件进行重试。

可靠消息最终一致性的优点是:

  1. 专业的消息处理:利用专业的消息中间件,能够提供更可靠的消息传递和处理机制,减少开发工作量。
  2. 高可用性:消息中间件通常具有高可用性和扩展性,能够适应大规模的分布式系统。

缺点是:

  1. 一致性问题:同样存在最终一致性的问题,在消息处理过程中可能会出现数据不一致的短暂时间窗口。
  2. 依赖外部组件:依赖消息中间件的稳定性和性能,如果消息中间件出现故障,可能会影响整个分布式事务的处理。

选择合适的分布式事务处理模式

在实际应用中,选择合适的分布式事务处理模式非常重要,需要综合考虑多个因素:

  1. 业务场景:不同的业务场景对事务的一致性和性能要求不同。例如,对于金融交易等对数据一致性要求极高的业务场景,可能需要选择 2PC 或 TCC 等能够保证强一致性的模式;而对于一些对一致性要求相对较低,但对性能要求较高的业务场景,如电商的下单流程,可以选择本地消息表或可靠消息最终一致性等模式。
  2. 系统规模:系统规模越大,涉及的微服务和数据库越多,分布式事务处理的复杂性就越高。对于大规模系统,需要选择性能较好、可扩展性强的模式,如 TCC 或可靠消息最终一致性。
  3. 开发成本:不同的分布式事务处理模式开发成本不同。2PC 和 3PC 实现相对简单,但性能和可用性方面存在一些问题;TCC 需要开发者自己实现较多的业务逻辑,开发成本较高;本地消息表和可靠消息最终一致性基于现有的技术,开发成本相对较低。
  4. 维护成本:除了开发成本,还需要考虑维护成本。例如,2PC 和 3PC 中协调者的单点故障问题需要额外的机制来解决,增加了维护的复杂性;TCC 模式中 Confirm 和 Cancel 阶段的重试机制也需要进行合理的设计和维护。

在实际项目中,可能需要根据具体情况进行权衡和选择,甚至可以结合多种分布式事务处理模式来满足不同业务场景的需求。例如,对于核心业务流程,可以使用 TCC 模式保证强一致性;对于一些非核心业务流程,可以使用本地消息表或可靠消息最终一致性模式提高性能和可用性。

分布式事务处理的最佳实践

  1. 幂等性设计:在分布式事务处理中,由于网络问题等原因,可能会导致消息重复接收或操作重试。因此,微服务的接口应该设计为幂等性的,即多次执行相同的操作,结果应该是一致的。例如,在库存扣减接口中,可以通过记录操作日志或使用唯一标识来保证幂等性。如果接收到重复的库存扣减消息,首先检查是否已经执行过该操作,如果已经执行过,则直接返回成功。
  2. 重试机制:为了保证分布式事务的最终一致性,重试机制是必不可少的。在消息发送失败、操作执行失败等情况下,需要进行适当的重试。重试次数和重试间隔需要根据具体业务场景进行合理设置。例如,对于一些对实时性要求较高的业务,可以适当增加重试次数和缩短重试间隔;对于一些对实时性要求较低的业务,可以减少重试次数和延长重试间隔,以避免对系统造成过大的压力。
  3. 监控与报警:分布式事务处理涉及多个微服务和复杂的网络通信,容易出现各种问题。因此,需要建立完善的监控与报警机制,实时监控分布式事务的执行情况。例如,监控消息的发送和接收情况、事务的提交和回滚情况等。一旦发现异常情况,及时报警通知相关人员进行处理,以减少数据不一致的风险。
  4. 数据补偿:即使采用了各种措施,在一些极端情况下,仍然可能出现数据不一致的情况。因此,需要设计数据补偿机制,定期检查数据的一致性,并对不一致的数据进行修复。例如,可以通过定期对账等方式,发现库存数量与订单数量不一致的情况,并进行相应的调整。

总结

微服务架构下的分布式事务处理是一个复杂而关键的问题,直接影响到系统的数据一致性和业务的正常运行。不同的分布式事务处理模式各有优缺点,在实际应用中需要根据业务场景、系统规模、开发成本和维护成本等因素进行综合考虑和选择。同时,通过幂等性设计、重试机制、监控与报警以及数据补偿等最佳实践,可以进一步提高分布式事务处理的可靠性和稳定性,确保微服务架构下系统的高效运行。在未来,随着分布式技术的不断发展,相信会有更先进、更高效的分布式事务处理方案出现,为微服务架构的广泛应用提供更坚实的支持。