likes
comments
collection
share

查漏补缺第三期(分布式事务相关)

作者站长头像
站长
· 阅读数 26

前言

目前正在出一个查漏补缺专题系列教程, 篇幅会较多, 喜欢的话,给个关注❤️ ~

本专题主要以Java语言为主, 好了, 废话不多说直接开整吧~

Q1 & 分布式事务有了解过吗?你是如何解决的呢?

如果简历中出现过分布式相关项目经验,提到过分布式事务的字眼,并且对方要求面试者有相关开发经验,基本上这个问题是必问的。

什么是分布式事务

分布式事务是指在分布式系统中执行的涉及多个独立事务资源的事务操作。在分布式系统中,不同的服务或组件可能分布在不同的计算机节点上,而这些节点可能属于不同的管理域、运行在不同的操作系统上,甚至位于不同的物理位置。由于分布式系统的特性,需要在跨越多个节点的操作中保证数据的一致性可靠性,这就引入了分布式事务的概念。

传统的单机事务通常使用ACID(原子性、一致性、隔离性和持久性)属性来确保事务的正确执行。然而,在分布式环境中,由于存在网络延迟节点故障等因素,要实现ACID属性变得更加困难。因此,分布式事务提供了一种机制来确保在分布式环境下事务的正确性。

分布式事务的关键问题是如何协调多个节点之间的事务操作,以确保事务的一致性。常见的分布式事务模型包括两阶段提交(Two-Phase Commit,2PC)、三阶段提交(Three-Phase Commit,3PC)、补偿事务(Compensating Transaction)等。

在两阶段提交中,事务的协调者负责向参与者发送准备请求,并等待参与者的响应。第一阶段是准备阶段,协调者向所有参与者发送准备请求,并等待它们的响应。如果所有参与者都准备就绪,那么协调者进入第二阶段,即提交阶段,向所有参与者发送提交请求。参与者接收到提交请求后,执行事务,并向协调者发送响应。如果所有参与者都成功执行事务,那么协调者提交事务,否则协调者发送回滚请求,要求参与者进行回滚操作。

三阶段提交是对两阶段提交的改进,引入了超时机制预提交阶段。在预提交阶段,协调者向所有参与者发送预提交请求,并等待它们的响应。如果所有参与者都预提交成功,那么协调者进入第二阶段,即正式提交阶段,向所有参与者发送正式提交请求。参与者接收到正式提交请求后,执行事务,并向协调者发送响应。在第三阶段,协调者向所有参与者发送最终提交请求,参与者确认事务的最终提交。

补偿事务是一种基于补偿机制的分布式事务处理方法。在补偿事务中,每个参与者在执行事务之前,会记录执行事务所需的所有操作。如果在事务执行过程中发生错误,参与者可以使用记录的操作信息进行回滚操作,从而恢复到事务开始之前的状态。

分布式事务的设计与实现需要考虑到系统的可用性、性能、数据一致性等方面的权衡。合理选择分布式事务模型,并在系统设计中考虑故障恢复机制和容错机制,可以有效地处理分布式系统中的事务操作。

如何解决分布式事务问题,有哪些方案

在解决分布式事务问题时,有几种常见的方案

  • 两阶段提交(Two-Phase Commit,2PC):这是最常见的分布式事务协调机制之一。在2PC中,事务协调者(通常是一个中心节点)负责协调参与者节点的操作。该过程分为准备阶段和提交阶段。在准备阶段,协调者向所有参与者节点发送准备请求,并等待它们的响应。如果所有参与者都准备就绪,那么协调者进入提交阶段,向所有参与者发送提交请求。参与者接收到提交请求后,执行事务,并向协调者发送响应。如果所有参与者都成功执行事务,那么协调者提交事务,否则协调者发送回滚请求,要求参与者进行回滚操作。

  • 三阶段提交(Three-Phase Commit,3PC):3PC是对2PC的改进,旨在解决2PC中的一些问题,例如阻塞、单点故障等。与2PC不同,3PC引入了预提交阶段,其中包括CanCommitPreCommit两个子阶段。在CanCommit阶段,协调者向参与者发送准备请求,参与者根据自身状态判断是否可以参与事务。如果所有参与者都同意参与,进入PreCommit阶段,协调者向参与者发送PreCommit请求。参与者在接收到PreCommit请求后,会将事务记录到日志中,但不执行事务操作。最后,在Commit阶段,协调者向参与者发送Commit请求,参与者根据日志中的记录执行或回滚事务。

  • 补偿事务(Compensating Transaction):补偿事务是一种基于补偿机制的分布式事务处理方法。在补偿事务中,每个参与者在执行事务之前,会记录执行事务所需的所有操作。如果在事务执行过程中发生错误,参与者可以使用记录的操作信息进行回滚操作,从而恢复到事务开始之前的状态。补偿事务通过执行逆向操作来恢复系统的一致性,但要求开发人员设计和实现补偿逻辑。

  • 强一致性: 要么全部成功,要么全部失败,全局事务协调者需要知道每个事务参与者的执行状态,再根据状态来决定数据的提交或者回滚, 缺乏

  • 最终一致性(Eventual Consistency):最终一致性是一种放宽了事务一致性要求的策略。在最终一致性模型中,系统允许一段时间内的数据不一致,但最终会达到一致的状态。这种方式常用于分布式系统中的大规模数据处理和分布式计算等场景。最终一致性通常通过异步操作、消息队列、事件驱动等方式实现。

分布式事务框架 & Seta

Seta基本概念

Seata是一个开源的分布式事务解决方案,为分布式系统提供了高效、可靠的事务支持。Seata可以与Spring Cloud、Dubbo等常见的分布式框架集成,并提供了对一阶段、两阶段和补偿事务的支持。

Seata的实现原理主要包括三个核心组件:事务协调器(Transaction Coordinator,TC)、事务管理器(Transaction Manager,TM)和资源管理器(Resource Manager,RM)

  • 事务协调器(Transaction Coordinator,TC)

    • TC是Seata的核心组件之一,负责事务的全局协调和控制。它接收事务参与者的注册和注销请求,并协调各个参与者的事务状态。TC采用两阶段提交(2PC)协议来实现分布式事务的一致性。
    • 在分布式事务开始时,TC生成全局事务ID,并将其分发给各个参与者。在事务提交阶段,TC向所有参与者发送准备提交请求,等待参与者的响应。如果所有参与者都准备就绪,TC向参与者发送提交请求,完成事务提交。如果任何一个参与者出现异常或者拒绝提交,TC会发送回滚请求,要求参与者进行回滚操作。
  • 事务管理器(Transaction Manager,TM)

    • TM负责事务的启动、提交和回滚。在分布式事务开始时,TM创建并管理全局事务上下文,并协调各个参与者的事务操作。
    • TM与TC进行通信,协调分布式事务的进行。它根据TC的指令,通知参与者进行事务的准备、提交或回滚操作。TM还负责事务的超时处理和事务上下文的传递。
  • 资源管理器(Resource Manager,RM)

    • RM是分布式事务中的参与者,管理分布式事务中涉及的资源(如数据库、消息队列等)。
    • RM与TM进行交互,接收来自TM的指令,并执行相应的事务操作。在准备阶段,RM将事务的操作记录到本地日志中,但不进行实际提交。在提交阶段,RM根据TM的指令执行事务的提交或回滚操作。

Seata的实现原理基于以上组件的协作和通信。通过TC、TMRM之间的消息交互和状态协调,Seata能够实现分布式事务的一致性和可靠性。

在使用Seata时,需要配置和启动TC、TM和RM,并将Seata的客户端集成到应用程序中。应用程序中的事务操作将通过客户端与TMRM进行通信,由TC进行协调和控制。Seata还提供了全局锁、分布式ID生成等功能,以支持分布式事务的并发控制和唯一标识生成。

Seta中的事务模式

Seata的事务模式包括 AT(Automatic Transaction)模式TCC(Try-Confirm-Cancel)模式Saga模式XA模式

  • AT(Automatic Transaction)模式

    • AT模式是Seata默认的事务模式,也是最常用的模式。在AT模式中,应用程序开发人员无需显式编写事务处理逻辑,而是通过使用Seata提供的注解或者AOP切面来标记需要参与分布式事务的方法。
    • AT模式基于Seata的二阶段提交协议(2PC)实现。当事务开始时,Seata会自动在全局事务上下文中注册并标记参与该事务的各个分支(即参与者)。在事务提交阶段,Seata会协调并执行全局事务的提交或回滚操作,以确保所有参与者的事务状态保持一致。
  • TCC(Try-Confirm-Cancel)模式

    • TCC模式适用于需要在分布式事务中自定义补偿逻辑的场景。在TCC模式中,开发人员需要手动编写分布式事务的三个阶段逻辑:Try阶段、Confirm阶段和Cancel阶段。
    • 在Try阶段,参与者尝试执行事务操作,并将相关操作记录到本地日志中,但不进行实际提交。在Confirm阶段,如果所有参与者的Try操作都成功,则执行实际提交。在Cancel阶段,如果任何一个参与者的Try操作失败,则执行回滚操作。
    • TCC模式通过补偿操作来保证分布式事务的一致性,即使某个参与者的操作失败,也可以通过Cancel阶段来进行回滚操作,恢复到事务开始之前的状态。
  • Saga模式

    • Saga模式是一种长事务模式,适用于需要在多个步骤中执行一系列操作的场景。在Saga模式中,每个步骤都是一个原子的事务操作,如果某个步骤失败,可以回滚该步骤之前的操作。
    • Saga模式通过一系列的局部事务来实现全局事务的一致性。在每个步骤中,参与者执行局部事务并记录操作日志。如果当前步骤失败,Seata会根据事务日志执行回滚操作,恢复到上一个成功的步骤。如果当前步骤成功,Seata会记录当前步骤的状态,并继续执行下一个步骤。
    • Saga模式中的每个步骤通常是异步执行的,可以通过消息队列或事件驱动的方式实现。这种异步执行的特性使得Saga模式适用于长时间运行的事务和复杂的业务流程。
  • XA模式中,Seata作为全局事务的协调者,与每个参与者(也称为资源管理器,RM)之间通过XA协议进行通信和协调。XA协议是由X/Open组织定义的一种标准,用于实现分布式事务的强一致性。

    • 准备(Prepare)阶段:

    • 当全局事务开始时,Seata会向所有参与者发送准备(Prepare)请求。

    • 参与者执行本地事务,并将事务的执行结果和状态记录到本地日志中。

    • 参与者将准备的结果(Prepare Result)返回给Seata。

    • 提交(Commit)阶段:

    • 如果所有参与者的准备结果都成功(返回Prepare成功),Seata将向所有参与者发送提交(Commit)请求。

    • 参与者根据Seata的提交请求,执行实际的提交操作,并将提交结果返回给Seata。

    • 回滚(Rollback)阶段:

    • 如果任何一个参与者的准备结果失败(返回Prepare失败),Seata将向所有参与者发送回滚(Rollback)请求。

    • 参与者根据Seata的回滚请求,执行回滚操作,并将回滚结果返回给Seata。 -XA模式通过在每个参与者上实现XA协议的两阶段提交(2PC)来确保分布式事务的一致性。Seata作为全局事务的协调者,在准备阶段和提交/回滚阶段与各个参与者进行通信和协调。

    • XA模式通过XA协议实现分布式事务的协调和控制,保证了全局事务的一致性。在使用XA模式时,开发人员需要确保各个参与者(RM)正确实现了XA协议,并与Seata进行正确的通信和交互。

为什么使用TCC

  • 自定义补偿逻辑:TCC模式允许开发人员在分布式事务中定义自定义的补偿逻辑。这对于一些需要特定业务处理和恢复机制的场景非常有用。通过TCC模式,开发人员可以在事务的不同阶段编写自己的逻辑,包括尝试执行、确认提交和取消回滚。这使得可以灵活地根据业务需求定义和处理分布式事务的补偿行为。

  • 高可靠性:TCC模式通过补偿操作来保证分布式事务的一致性。即使在事务过程中出现了故障、错误或异常情况,TCC模式能够通过执行适当的补偿操作来还原事务状态或回滚已执行的操作。这种补偿机制能够提供更高的可靠性,保证系统的数据完整性和一致性。

  • 高并发性TCC模式在分布式事务的不同阶段可以进行并发执行,每个阶段之间没有强依赖关系。这使得TCC模式具有更好的可扩展性和并发性能,特别适用于高并发的业务场景。

  • 异步处理TCC模式支持在事务的不同阶段执行异步操作。例如,可以在Try阶段提交消息到消息队列,然后在Confirm阶段处理消息,最后在Cancel阶段撤销消息。这种异步处理的特性使得TCC模式适用于长时间运行的事务和复杂的业务流程。

需要注意的是,TCC模式相对于其他事务模式(如AT模式和XA模式)来说,对开发人员要求较高,需要手动编写和管理各个阶段的逻辑。此外,由于TCC模式需要执行补偿操作,因此对业务逻辑的幂等性可恢复性有一定的要求。

总而言之,选择使用TCC模式主要基于业务需求和特定的场景。当需要自定义补偿逻辑、提供高可靠性、高并发性和异步处理时,TCC模式可以是一个有价值的选择。

Q2 & 分布式事务经典理论有了解过吗?

  • ACID(原子性、一致性、隔离性和持久性)

    • ACID是传统数据库事务的核心特性,也适用于分布式事务。原子性确保事务中的所有操作要么全部执行成功,要么全部回滚。一致性确保事务将数据库从一个一致状态转移到另一个一致状态。隔离性确保并发事务之间相互隔离,以避免数据冲突。持久性确保事务提交后,对数据库的修改是永久的。
    • ACID属性确保了分布式事务的一致性,但在分布式环境中实现ACID属性可能会面临一些挑战。
  • 2PC(Two-Phase Commit)

    • 2PC是一种基于协调者和参与者的协议,用于在分布式环境中实现事务的一致性。它包括两个阶段:准备阶段和提交阶段。
    • 在准备阶段,协调者向所有参与者发送准备请求,并等待它们的准备响应。如果所有参与者都准备就绪,协调者发送提交请求,否则发送回滚请求。
    • 在提交阶段,参与者根据协调者的请求执行相应的操作,并发送确认消息给协调者。协调者根据收到的确认消息决定最终提交或回滚事务。
  • 3PC(Three-Phase Commit)

    • 3PC是对2PC的改进,旨在解决2PC中的一些问题,如阻塞和单点故障。
    • 3PC引入了准备提交阶段,它在准备阶段之后,提交阶段之前。在准备提交阶段,协调者要求参与者对准备操作再次确认。这样可以避免参与者在提交阶段中由于网络问题无法接收到协调者的提交请求。
  • BASE(Basically Available, Soft State, Eventually Consistent)

    • BASE是一种对于分布式系统一致性的松弛要求,与ACID的强一致性要求相对。它是对CAP理论(一致性、可用性和分区容错性)的一种折中。
    • BASE的基本思想是在分布式系统中追求可用性和分区容错性,而放宽对强一致性的要求。通过使用一致性级别较低的机制(如最终一致性),可以提高系统的可用性和性能。

Q3 & 分布式事务中幂等性问题

在分布式事务中,幂等性是指对于同一操作的多次执行,结果应该与一次执行的结果相同。换句话说,无论执行多少次,系统的状态和数据都应该保持一致。

幂等性问题在分布式环境中尤为重要,因为网络延迟、重试、消息重复等因素可能导致同一操作被执行多次。如果操作不具备幂等性,重复执行可能会导致意外的副作用和数据不一致。

以下是处理分布式事务中幂等性问题的几种常见方法:

  • 唯一标识符(Unique Identifier)

    • 在每个操作中引入唯一标识符,可以通过检查该标识符来判断是否已经执行过相同的操作。如果标识符已存在,即可判定为重复操作并忽略。
    • 唯一标识符可以是全局唯一的ID、请求ID、事务ID等,可以通过生成算法、数据库约束、分布式锁等方式保证其唯一性。
  • 幂等性校验

    • 在每个操作执行前,先检查操作的执行状态或结果,判断是否已经成功执行过。如果已经成功执行,可以避免重复执行该操作。
    • 幂等性校验可以基于操作的唯一标识符、操作日志、状态字段等。
  • 幂等性设计

    • 在系统设计阶段考虑幂等性,使得操作具备幂等性特性。例如,设计操作为幂等的API接口,使得重复调用对系统状态没有影响。
    • 幂等性设计可以通过幂等性算法、数据版本控制、事务状态机等方式实现。
  • 重试机制

    • 如果在执行操作时发生错误或失败,可以使用重试机制来重新执行操作。重试机制需要保证在重试时不会引入额外的副作用,即重复执行操作应该具备幂等性。

处理幂等性问题的方法应根据具体的业务需求和系统设计进行选择和实现。在分布式系统中,确保操作的幂等性是保证数据一致性和系统稳定性的重要措施之一。

借助Redis解决接口幂等问题

Redis是一种高性能的内存数据库,它可以用于解决分布式系统中的幂等性问题。

  • 分布式锁

    • 使用Redis的分布式锁可以确保在同一时间只有一个线程或进程能够执行特定的操作,从而避免重复执行。可以使用Redis的SETNX命令来实现简单的分布式锁,或者使用RedLock等分布式锁算法来实现更复杂的分布式锁。
    • 在处理幂等性操作之前,获取分布式锁,执行操作,然后释放锁。这样即使有多个并发请求同时到达,只有一个请求能够获取锁并执行操作,其他请求将被阻塞或放弃执行。
  • 重复请求判断

    • 使用Redis的数据结构,如Set或Sorted Set,记录已处理的请求的唯一标识符。在处理请求之前,先查询该数据结构判断请求是否已经处理过。
    • 可以使用Redis的SADD命令将请求的唯一标识符添加到Set中,并设置过期时间来自动清理已处理请求的记录。当新的请求到达时,可以使用Redis的SISMEMBER命令来检查请求是否已经存在于Set中。
  • 乐观锁和版本号控制

    • 在每个请求中使用版本号来标识操作的状态。每次请求到达时,先查询Redis中存储的当前版本号,并与请求中的版本号进行比较。
    • 如果请求中的版本号与Redis中的版本号相同,表示操作未重复执行,可以继续处理。处理完成后,更新Redis中的版本号为新的值,以便下一个请求进行比较。
  • 事务和原子操作

    • Redis支持事务和原子操作,可以将多个操作打包成一个原子性的操作序列。通过将幂等性操作和状态更新操作组合在一个Redis事务中,可以确保操作的原子性和一致性。
    • 在处理幂等性操作时,将操作和状态更新放在同一个Redis事务中,以保证操作的幂等性和一致性。如果操作已经执行过,事务将被回滚,不会对状态产生影响。

需要注意在使用Redis解决幂等性问题时,要确保Redis的可用性、一致性和持久性,以避免单点故障和数据丢失的风险。

接下来通过分布式锁的例子来看下如何解决幂等问题:

Spring Boot项目中使用Redis分布式锁来解决接口幂等性问题,可以借助RedisSETNX命令和过期时间来实现。

接下来,创建一个幂等性工具类(IdempotenceUtils)来处理幂等性操作:

@Component
public class IdempotenceUtils {
    private static final String LOCK_PREFIX = "idempotence:";
    private static final long LOCK_EXPIRE_TIME = 10; // 锁的过期时间,单位:秒

    @Autowired
    private StringRedisTemplate redisTemplate;

    public boolean acquireLock(String lockKey) {
        String redisKey = LOCK_PREFIX + lockKey;
        Boolean acquired = redisTemplate.opsForValue().setIfAbsent(redisKey, "locked", LOCK_EXPIRE_TIME, TimeUnit.SECONDS);
        return acquired != null && acquired;
    }

    public void releaseLock(String lockKey) {
        String redisKey = LOCK_PREFIX + lockKey;
        redisTemplate.delete(redisKey);
    }
}

acquireLock方法尝试获取分布式锁,它使用RedissetIfAbsent命令来设置锁的键值对。如果锁成功设置并且设置了过期时间,则认为获取锁成功,返回true;否则,返回false

releaseLock方法用于释放分布式锁,通过删除锁的键来释放锁资源。

@RestController
public class MyController {
    private static final int MAX_RETRY_ATTEMPTS = 3;
    private static final long RETRY_DELAY_MS = 1000;
    
    @Autowired
    private IdempotenceUtils idempotenceUtils;

    @PostMapping("/process")
    public ResponseEntity<String> processRequest(@RequestBody RequestData requestData) {
        String lockKey = "process:" + requestData.getId();

        int retryAttempts = 0;
        boolean acquired = false;

        while (!acquired && retryAttempts < MAX_RETRY_ATTEMPTS) {
            acquired = idempotenceUtils.acquireLock(lockKey);
            if (!acquired) {
                // 等待一段时间后进行重试
                try {
                    Thread.sleep(RETRY_DELAY_MS);
                } catch (InterruptedException e) {
                    // 处理中断异常
                    Thread.currentThread().interrupt();
                }
                retryAttempts++;
            }
        }

        if (!acquired) {
            return ResponseEntity.status(HttpStatus.CONFLICT).body("Request already in progress");
        }

        try {
            // 执行实际的业务逻辑
            processBusinessLogic(requestData);
            return ResponseEntity.ok("Request processed successfully");
        } catch (Exception e) {
            // 处理业务逻辑异常
            // ...
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error occurred during request processing");
        } finally {
            releaseLockSafely(lockKey);
        }
    }

    private void processBusinessLogic(RequestData requestData) {
        // 实际的业务逻辑处理
        // ...
    }

    // 安全释放锁
    private void releaseLockSafely(String lockKey) {
        try {
            idempotenceUtils.releaseLock(lockKey);
        } catch (Exception e) {
            // 处理释放锁时的异常,避免死锁发生
            // ...
        }
    }
}

首先,构造一个唯一的锁键(lockKey),可以根据具体的业务需求构造唯一标识。

  • 引入了最大重试次数和重试延迟时间的常量,用于控制重试策略。
  • 使用retryAttempts变量来记录已尝试的次数,acquired变量表示是否成功获取到锁。
  • 在while循环中,如果未能成功获取锁且未达到最大重试次数,等待一段时间后进行重试。通过Thread.sleep来实现等待的功能,并在等待过程中处理中断异常。
  • 如果达到最大重试次数仍未获取到锁,则返回冲突状态的响应。

然后,通过idempotenceUtils.acquireLock方法尝试获取锁,如果获取失败,则表示该请求已经在处理中,返回冲突状态的响应。 如果成功获取锁,则在try-finally代码块中执行实际的业务逻辑处理,并在最后使用idempotenceUtils.releaseLock释放锁资源,确保无论业务逻辑成功还是失败,都能正常释放锁。 这样,通过使用Redis分布式锁,我们可以保证同一请求在同一时间只能被一个线程或进程处理,实现接口的幂等性。

死锁的解决是一个相对复杂的问题,具体的解决方案需要根据实际情况进行综合考虑和设计。以上示例提供了一种基本的思路,但在实际应用中仍需根据具体需求进行调整和优化。

需要注意的是,以上示例仅为一种基本的实现方式,具体的超时时间、重试策略和异常处理方式需要根据实际情况进行调整和优化。另外,在实际应用中还应该考虑分布式锁的续期机制、处理等方面的问题,以确保分布式环境下的幂等性保证和系统的可靠性。

结束语

大家可以针对自己薄弱的地方进行复习, 然后多总结,形成自己的理解,不要去背~

本着把自己知道的都告诉大家,如果本文对您有所帮助,点赞+关注鼓励一下呗~

相关文章

项目源码(源码已更新 欢迎star⭐️)

往期设计模式相关文章

设计模式项目源码(源码已更新 欢迎star⭐️)

Kafka 专题学习

项目源码(源码已更新 欢迎star⭐️)

ElasticSearch 专题学习

项目源码(源码已更新 欢迎star⭐️)

往期并发编程内容推荐

推荐 SpringBoot & SpringCloud (源码已更新 欢迎star⭐️)

博客(阅读体验较佳)

转载自:https://juejin.cn/post/7234703748403609660
评论
请登录