设计分布式系统的幂等性是确保在面对重复请求或操作时系统能够产生相同结果的重要方面。以下是一些设计方法,并结合一个简单的例子说明:
假设有一个订单支付的场景,设计一个分布式系统来保证支付请求的幂等性:
一个完整的 HTTP 请求通常经历以下步骤:
上述过程描述了一个简单的HTTP请求的生命周期。需要注意的是,实际的请求可能会涉及到缓存、重定向、Cookie处理等额外的步骤,具体的步骤也可能因请求的性质(如GET、POST)而有所不同。在复杂的应用场景中,可能还涉及到HTTPS加密、负载均衡、反向代理等技术。
分布式事务是企业集成中的一个技术难点,也是每一个分布式系统架构中都会涉及到的一个东西,
特别是在微服务架构中,几乎可以说是无法避免。
首先要搞清楚:ACID、CAP、BASE理论。
1. 原子性(Atomicity):
一个事务是一个不可分割的工作单位,事务中的操作要么全部执行成功,要么全部失败回滚。
2.一致性(Consistency):
事务开始前和结束后,数据库的完整性约束没有被破坏,事务执行过程中数据库从一种一致性状态变换到另一种一致性状态。
3. 隔离性(Isolation):
多个事务并发执行时,每个事务的执行都不受其他事务的影响,每个事务感觉就像在系统中是唯一执行的。
4. 持久性(Durability):
一旦事务提交,其结果应该永久保存,即使系统发生故障,数据也能够恢复。
1. 一致性(Consistency):
所有节点在同一时间具有相同的数据视图。
2. 可用性(Availability):
在有限时间内,每个请求都能收到一个非空的响应,不保证响应的数据是否为最新。
3. 分区容忍性(Partition Tolerance):
系统在遇到网络分区的情况下仍然能够继续运行。
CAP理论表明在分布式系统中,不可能同时满足一致性、可用性和分区容忍性这三个条件,最多只能同时满足其中两个。
假设有一个分布式系统,其中包含两个数据中心,分别位于美国和欧洲,用于存储用户数据。现在考虑以下三个方面:
一致性(Consistency):
如果强调一致性,那么在一个数据中心写入的数据应该立即在另一个数据中心可见。这意味着当用户在美国的数据中心写入数据后,欧洲的数据中心必须立即感知到这个变化。
可用性(Availability):
如果强调可用性,系统应该保持对用户请求的高响应性。即使由于网络分区导致美国和欧洲数据中心之间通信失败,用户仍然可以在自己所在的数据中心执行读取和写入操作。
分区容忍性(Partition Tolerance):
分区容忍性要求系统能够在面对网络分区时继续运行。即使美国和欧洲之间的网络通信断开,两个数据中心仍然可以独立运作。
在这个场景中,如果强调一致性,就可能牺牲可用性,因为要求立即在两个数据中心同步数据;如果强调可用性,就可能牺牲一致性,因为在网络分区时,用户可能会看到不同的数据。这就是 CAP 理论的核心观点,即在分布式系统中无法同时满足强一致性、高可用性和分区容忍性。
实际系统的设计通常会根据业务需求和系统要求做出权衡,选择适合的一致性模型。例如,NoSQL数据库中的一些系统可能更注重可用性和分区容忍性,而传统的关系型数据库则可能更注重一致性。
1. 基本可用(Basic Availability):
系统保证基本的可用性,即在有限时间内返回非错误的响应。
2. 软状态(Soft State):
允许系统在不同节点的数据副本之间存在延时。
3. 最终一致性(Eventually Consistent):
系统保证在一定时间内,所有节点的数据最终达到一致状态。
BASE理论是对CAP理论的一种放宽,认为在分布式系统中,不一定要求强一致性,而是追求最终一致性。在系统出现故障或网络分区时,可以容忍一定的时间窗口内的数据不一致,但最终会达到一致状态。
二阶段提交(Two-Phase Commit,2PC)是一种分布式事务协议,用于确保分布式系统中多个参与者(节点)对事务的提交或回滚具有一致性。
该协议分为两个阶段:
1. 准备阶段(Phase 1 - Prepare):
事务协调者(Coordinator)向所有参与者发送准备请求,询问它们是否可以提交事务。每个参与者在接收到准备请求后,会执行事务的所有操作,并将事务的执行结果记录到本地,但不提交。
2. 提交阶段(Phase 2 - Commit):
如果所有参与者都成功地完成了事务的操作,协调者会向所有参与者发送提交请求,要求它们提交事务。如果有任何一个参与者在准备阶段失败或者拒绝提交,那么协调者会向所有参与者发送回滚请求,要求它们回滚事务。
二阶段提交的主要优点是在整个协议过程中,保证了事务的一致性。如果任何一个参与者不能提交,整个事务都会回滚,保持了分布式系统的一致性。
然而,二阶段提交也有一些缺点,主要包括:
1. 阻塞问题(Blocking Problem):
在准备阶段,如果有一个参与者无法响应,那么整个系统可能会处于阻塞状态,等待超时或者其他机制来解决问题。
2.单点故障(Single Point of Failure):
协调者是一个单点,如果协调者发生故障,整个事务过程可能无法继续。
3. 性能开销(Performance Overhead):
二阶段提交的过程涉及多次网络通信,可能引入较大的性能开销。
由于这些缺点,一些分布式系统在实际应用中选择使用其他协议或者技术来处理分布式事务,如补偿性事务、Saga模式等。
举例:一个简单的分布式系统,涉及两个服务(Service A和Service B)和一个事务协调者(Coordinator)。这两个服务分别存储用户的余额信息和交易记录。
以下是二阶段提交的执行过程:
准备阶段(Prepare Phase):
协调者向Service A和Service B发送准备请求,询问它们是否可以提交事务。
Service A和Service B接收到准备请求后,执行相应的操作,比如扣减用户余额、记录交易信息,并将执行结果记录到本地,但暂时不提交。
如果两个服务都顺利完成操作,它们向协调者发送准备就绪的消息。
提交阶段(Commit Phase):
协调者在收到两个服务的准备就绪消息后,向它们发送提交请求。
Service A和Service B接收到提交请求后,正式提交之前记录的操作,将事务的结果永久保存。
如果所有参与者都成功提交,协调者向所有参与者发送全局提交的消息,宣告整个事务提交完成。
如果在准备阶段有任何一个服务出现问题,比如执行操作失败,那么它们会向协调者发送回滚的消息。协调者在收到回滚消息后,向所有参与者发送全局回滚的消息,要求它们回滚之前的操作,确保整个事务的一致性。
这个例子说明了二阶段提交如何通过协调者和参与者的交互来确保分布式系统中的事务一致性。然而,二阶段提交的缺点也显而易见,例如在网络分区情况下可能导致阻塞、单点故障等问题。因此,在实际应用中,一些系统可能会选择其他分布式事务处理机制。
三阶段提交(Three-Phase Commit,3PC)是二阶段提交的改进版本,旨在解决二阶段提交的一些问题,尤其是在存在网络分区的情况下可能导致的阻塞问题。
三阶段提交引入了“超时”机制,将分布式事务的过程划分为三个阶段:
1. CanCommit 阶段(准备阶段,Pre-Commit Phase):
协调者向所有参与者发送 CanCommit 请求,询问它们是否可以提交事务。
参与者接收到 CanCommit 请求后,执行相应的操作,比如扣减余额、记录交易信息,并将执行结果记录到本地,但不提交。如果参与者可以正常执行操作,就向协调者发送 Yes 响应,否则发送 No 响应。
2. PreCommit 阶段:
协调者在收到所有参与者的 Yes 响应后,向所有参与者发送 PreCommit 请求,通知它们准备提交事务。
参与者在接收到 PreCommit 请求后,正式提交之前记录的操作,并向协调者发送 Acknowledge 响应,表示已准备好提交。
3. DoCommit 阶段(Commit 阶段):
协调者在收到所有参与者的 Acknowledge 响应后,向所有参与者发送 DoCommit 请求,要求它们提交事务。
参与者接收到 DoCommit 请求后,正式提交之前记录的操作,并向协调者发送 Acknowledge 响应,表示事务已经提交。
与二阶段提交相比,三阶段提交引入了 PreCommit 阶段,这个阶段允许参与者在执行完操作后,在实际提交之前,先通知协调者准备好了可以提交。这就使得在出现网络分区或者部分参与者故障的情况下,可以更及时地发现问题,并避免了二阶段提交可能导致的阻塞问题。
尽管三阶段提交解决了一些问题,但它仍然不是完美的,可能存在一些极端情况下的缺陷。因此,在实际应用中,一些系统可能会考虑使用其他分布式事务处理机制,如补偿性事务、Saga模式等。
一个简单的分布式系统,涉及两个服务(Service A 和 Service B)和一个事务协调者(Coordinator),用于处理跨服务的转账操作。
以下是三阶段提交的执行过程:CanCommit 阶段:
协调者向 Service A 和 Service B 发送 CanCommit 请求,询问它们是否可以提交事务。
Service A 和 Service B 接收到 CanCommit 请求后,执行转账操作,将操作结果记录到本地,但不提交。如果两个服务都可以正常执行操作,它们向协调者发送 Yes 响应,否则发送 No 响应。
PreCommit 阶段:
协调者在收到两个服务的 Yes 响应后,向它们发送 PreCommit 请求,通知它们准备提交事务。
Service A 和 Service B 接收到 PreCommit 请求后,正式提交之前记录的操作,并向协调者发送 Acknowledge 响应,表示已准备好提交。
DoCommit 阶段(Commit 阶段):
协调者在收到两个服务的 Acknowledge 响应后,向它们发送 DoCommit 请求,要求它们提交事务。
Service A 和 Service B 接收到 DoCommit 请求后,正式提交之前记录的操作,并向协调者发送 Acknowledge 响应,表示事务已经提交。
如果在 CanCommit 阶段有任何一个服务返回 No 响应,或者在 PreCommit 阶段发生故障导致 Acknowledge 未能成功发送,协调者可以根据情况选择中止事务(Abort)或者重新尝试。
补偿事务是一种处理分布式系统中失败或错误事务的方法。在分布式环境中,由于网络问题、系统故障等原因,事务的某些步骤可能无法正常执行或者执行失败。补偿事务的目标是在发生故障时,通过执行逆向的操作或者补偿操作,将系统恢复到一致的状态。
以下是补偿事务的一般步骤:
1. 正向操作(Forward Operation):
执行分布式事务的正向操作,包括跨多个服务的一系列步骤。
2. 检查点(Checkpoint):
在正向操作的途中,将事务的执行进度记录为检查点。检查点记录了事务已经执行到哪一步。
3. 发生故障:
当在正向操作的过程中发生故障,例如某个服务不可用、网络问题等,导致事务无法继续执行。
4. 补偿操作(Compensation Operation):
根据检查点的信息,执行逆向的操作或者补偿操作,将系统恢复到故障发生前的一致状态。补偿操作是正向操作的逆向操作,用于消除正向操作可能引入的变化。
5. 完成事务:
一旦补偿操作成功执行,事务就被认为是完成的。系统达到了一致状态,尽管正向操作中的某些步骤失败了。
补偿事务的优点在于它能够处理分布式环境中的故障,保障系统最终的一致性。然而,补偿事务也存在一些挑战,例如补偿操作的设计需要考虑到正向操作可能带来的状态变化,以及在执行补偿操作时可能发生的故障。
例子:一个在线购物系统,其中涉及两个服务:订单服务和支付服务。当用户下单时,需要在订单服务中创建订单记录,并在支付服务中执行支付操作。这两个步骤构成一个分布式事务。
正向操作(Forward Operation):
用户在前端下单,前端服务调用订单服务的接口创建订单记录,然后调用支付服务的接口执行支付操作。
检查点(Checkpoint):
在创建订单记录和执行支付操作的过程中,可以设置检查点,记录订单服务和支付服务执行到的步骤。
发生故障:
在支付操作执行成功后,网络故障或者支付服务不可用的情况下,导致订单服务无法继续执行。
补偿操作(Compensation Operation):
订单服务根据检查点的信息,执行逆向的操作,即取消订单。这可能涉及将订单记录标记为取消状态、释放库存等。
完成事务:
一旦订单服务执行了取消订单的补偿操作,系统就恢复到了故障发生前的状态。虽然支付操作没有成功,但用户的订单也没有被创建,系统最终达到了一致状态。
这个例子说明了补偿事务在处理分布式系统中可能发生的故障时的应用。在这种情况下,通过补偿操作,系统能够在出现问题时回滚到一致的状态,确保用户不会受到不正确的影响。
Sagas 是一种分布式事务模型,用于处理跨多个服务的事务操作。相比传统的二阶段提交(2PC)等协议,Sagas 模型更灵活,允许在分布式系统中执行一系列相互关联的操作,并且具有更好的可维护性和可扩展性。
Sagas 模型的核心思想是将一个大型的、原子性的事务分解成一系列小的、局部的、可补偿的事务步骤。每个事务步骤都对应系统中的一个服务,并且具有以下特点:
正向和逆向操作: 每个事务步骤都有一个正向操作用于执行实际的业务逻辑,同时有一个逆向操作(补偿操作)用于撤销或者回滚正向操作。
局部性和可补偿性: 每个事务步骤都是局部的,只处理一个服务的事务。而且,如果后续步骤发生故障或者某一步骤需要回滚,Sagas 可以执行相应的逆向操作来保证整个事务的一致性。
最终一致性: Sagas 模型追求最终一致性,即在一系列的步骤执行完成后,系统最终达到一致的状态,即使中间有部分步骤失败。
以下是一个简单的 Sagas 模型的例子,以订单支付为场景:
这个 Saga
包含了多个步骤,每个步骤都有正向和逆向操作。如果某个步骤失败,Sagas 模型可以通过执行逆向操作来回滚或者补偿,保证系统最终的一致性。
相比传统的分布式事务协议,Sagas 模型更容忍故障,更适用于大规模、高度分布式的系统,因为它将事务拆分成小的、独立的步骤,每个步骤都有自己的逻辑和可补偿性。
在分布式系统中,生成唯一的分布式 ID 是一个常见的需求,以避免冲突和确保全局唯一性。以下是几种常见的分布式 ID 生成方案:
1. UUID(Universally Unique Identifier):
UUID 是一种标准的 128 位长度的全局唯一标识符。它可以在不同的机器上生成,通过不同的算法确保唯一性,例如基于时间的 UUID(Version 1)、基于名称和随机数的 UUID(Version 4)等。UUID 的缺点是相对较长,且不易于按时间顺序排序。
2. Snowflake 算法:
Snowflake 是一种分布式 ID 生成算法,由 Twitter 开发。它使用 64 位的整数,其中包含了时间戳、机器 ID 和序列号。通过组合这三个部分,Snowflake 生成的 ID 在同一时刻、同一机器上具有唯一性。缺点是对机器时钟同步要求较高。
3. 自增主键:
在分布式系统中,可以使用数据库的自增主键来生成唯一的 ID。每个分布式节点可以拥有自己的数据库,通过数据库的自增主键生成唯一 ID。缺点是可能存在数据库的单点故障,并且可能影响性能。
4. 雪花算法(Snowflake的变体):
针对 Snowflake 算法的一些变体,根据具体需求和系统特点进行调整。例如,可以修改位数分配、增加数据中心标识等。
5. Zookeeper 等分布式协调服务:
利用分布式协调服务(如 Zookeeper)来生成全局唯一 ID。通过在 Zookeeper 上创建全局唯一的节点或者利用序列化节点的特性,每个节点可以通过读取 Zookeeper 上的节点信息生成唯一 ID。
6.基于数据库的全局唯一序列(Global Unique Sequence):
通过在数据库中维护一个全局唯一的序列,每个分布式节点可以通过向数据库请求获取唯一的序列值作为 ID。这需要保证数据库的高可用性。
幂等性是指对于同一个操作的多次执行,产生的效果是一致的。在分布式系统中,由于网络问题、系统故障等原因,同一个请求可能会被重复执行,因此确保操作的幂等性变得非常重要。以下是一些常见的幂等性解决方法:
为每个请求生成唯一标识,并在系统中维护一个去重表。在处理请求前,先检查去重表,如果已经存在相同的唯一标识,则认为是重复请求,直接忽略。这种方式适用于维护一段时间内的请求唯一性。
在设计接口时考虑幂等性,使得同一个请求的多次执行不会产生不一致的结果。例如,设计幂等的接口应该能够在多次执行时返回相同的结果,并且对于重复请求,只执行一次实际操作。
通过在请求中包含版本号或时间戳等信息,服务端可以判断是否为重复请求。如果请求中的版本号小于或等于已处理的请求版本号,则认为是重复请求。
在每个请求中包含一个令牌(Token),服务端在处理请求时检查令牌的有效性。如果令牌已经被使用过,认为是重复请求。这种方式需要客户端生成和维护令牌。
在数据库层面使用乐观锁,通过版本号或时间戳等方式来判断数据是否已经被修改。如果在更新数据时发现版本号不匹配,则认为是重复请求。
在请求中包含一个幂等性校验码,服务端根据校验码判断请求是否重复。校验码可以是请求参数的哈希值或者其他形式的校验码。
使用分布式锁来确保某个操作在同一时刻只能被执行一次。分布式锁的实现可以基于分布式协调服务(如 Zookeeper、etcd)或者其他方式。
负载均衡算法用于分配请求到多个服务器,以确保各服务器的负载相对均衡,提高系统的性能、稳定性和可扩展性。以下是一些常见的负载均衡算法:
轮询算法(Round Robin):
请求按照顺序轮流分配到每个服务器,平均分配负载。适用于每个服务器性能相近的情况。
加权轮询算法(Weighted Round Robin):
在轮询算法的基础上,通过为每个服务器分配不同的权重,使得性能较高的服务器能够处理更多的请求。
随机算法(Random):
将请求随机分配给服务器。适用于每个服务器性能相近的情况,且不需要考虑权重分配。
加权随机算法(Weighted Random):
在随机算法的基础上,为每个服务器分配不同的权重,以影响请求分配的概率。
最小连接数算法(Least Connections):
将请求分配给当前连接数最少的服务器。适用于服务器处理连接时间较短的场景。
加权最小连接数算法(Weighted Least Connections):
在最小连接数算法的基础上,通过为每个服务器分配不同的权重,影响连接数的计算。
响应时间加权算法(Least Response Time):
将请求分配给响应时间最短的服务器。适用于服务器响应时间相差较大的情况。
哈希算法(Hashing):
根据请求的特征(如 IP 地址、URL 等)计算哈希值,并将请求分配给对应哈希槽的服务器。相同的请求始终被分配到同一台服务器,适用于需要保持会话一致性的场景。
一致性哈希算法(Consistent Hashing):
在哈希算法的基础上,通过引入虚拟节点等机制,解决增减节点时的数据迁移问题。
IP 地址散列算法(IP Hash):
根据请求的源 IP 地址计算哈希值,将请求分配给对应的服务器。
限流算法是一种用于控制访问流量的手段,防止系统被过多请求压垮。以下是一些常见的限流算法及其实现方式:
1. 令牌桶算法(Token Bucket):
令牌桶算法通过维护一个令牌桶,以固定的速率往桶中放入令牌,每个请求需要获取一个令牌才能被处理。如果桶中没有足够的令牌,则请求被拒绝。
实现方式:使用定时任务或者线程池,以固定速率向令牌桶中添加令牌。每个请求需要获取令牌,如果桶中有足够的令牌,则处理请求,否则进行限流。
2. 漏桶算法(Leaky Bucket):
漏桶算法将请求按照固定速率处理,未被处理的请求被放入一个漏桶中。如果漏桶满了,超出容量的请求会被拒绝。
实现方式:使用定时任务或者线程池,以固定速率处理漏桶中的请求。每个请求被放入漏桶,如果漏桶未满,则请求被处理,否则进行限流。
3. 计数器算法:
简单的计数器算法统计一定时间窗口内的请求数量,当请求数超过阈值时进行限流。
实现方式:维护一个计数器,每个请求到来时进行累加。定时清零或者使用滑动窗口统计请求数。
4. 滑动窗口算法:
滑动窗口算法是计数器算法的一种改进,通过维护一个固定大小的时间窗口,统计窗口内的请求数量,当请求数超过阈值时进行限流。
实现方式:使用一个固定大小的队列或数组,记录每个时间窗口内的请求数。每个请求到来时,将当前时间戳加入队列,并移除过期的时间戳,统计队列长度。
5. 漏斗算法(Funnel):
漏斗算法模拟一个漏斗,允许瞬时突发的请求,但限制了长时间内的平均请求速率。
实现方式:维护一个漏斗,根据漏斗的容量和漏嘴大小来控制请求的处理速率。
6. 令牌桶 + 漏桶混合使用:
将令牌桶和漏桶算法结合使用,既控制了瞬时突发流量,又限制了长时间内的平均请求速率。
关注公众号“洪都新府笑颜社”,发送 “面试题”即可免费领取一份超全的面试题PDF文件!!!!