分布式系统的一致性协议之 一致性方案MS、MM、2PC 和 3PC

本文深入探讨了分布式系统中的一致性协议,包括冷备、Master/Slave、Master/Master、两阶段提交(2PC)、三阶段提交(3PC)等方案。分析了各种方案的原理、优缺点及在特定场景下的数据一致性问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

根据上一篇文章描述《分布式系统的一致性协议之 CAP理论原理》可知,目前分布式系统中通用的一致性方案,主要包括:冷备、Master/Slave、Master/Master、两阶段提交以及基于 Paxos 算法的各种实现。

冷备


所谓冷备,也就是做定时全量、增量的备份,在系统一致性、可用性出现问题时,能够通过备份数据还原系统到达某一点。这种方案操作难度小,但存在即时性低、丢失率高的问题。

Master/Slave


  1. 读写请求由Master负责
  2. 写请求写到Master后,由Master同步到Slave上。

同步方式有Master push or Slave pull两种,通常是由Slave 周期性来pull,所以是最终一致性。

问题: 若在 pull 周期内(不是期间?),master挂掉,那么会导致这个时间片内的数据丢失。

  • 若不想让数据丢掉,Slave 只能成为 ReadOnly方式等Master恢复
  • 若容忍数据丢失,可以让 Slave代替Master工作

问题:如何保证强一致性?

Master 写操作,写完成功后,再写 Slave,两者成功后返回成功。若 Slave失败,两种方法:

  1. 标记 Slave 不可用报错,并继续服务(等恢复后,再同步Master的数据,多个Slave少了一个而已)
  2. 回滚自己并返回失败

Master/Master


数据同步一般是通过 Master 间的异步完成,所以是最终一致。

好处: 一台Master挂掉,另外一台照样可以提供读写服务。当数据没有被赋值到别的Master上时,数据会丢失。

对同一数据的处理问题:

Dynamo的Vector Clock的设计(记录数据的版本号和修改者),当数据发生冲突时,要开发者自己来处理。

两阶段提交协议(2PC)


二阶段提交协议(Two-phase Commit,即2PC)是常用的分布式事务解决方案,它可以保证在分布式事务中,要么所有参与进程都提交事务,要么都取消事务,即实现 ACID 的原子性(A)。在数据一致性中,它的含义是:要么所有副本(备份数据)同时修改某个数值,要么都不更改,以此来保证数据的强一致性。

2PC 要解决的问题可以简单总结为:在分布式系统中,每个节点虽然可以知道自己的操作是成功还是失败,却是无法知道其他节点的操作状态。当一个事务需要跨越多个节点时,为了保持事务的 ACID 特性,需要引入一个作为协调者的组件来统一掌控所有节点(参与者)的操作结果并最终指示这些节点是否要把操作结果进行真正的提交(比如将更新后的数据写入磁盘等等)。因此,二阶段提交的算法思路可以概括为: 参与者将操作结果通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。

2PC 过程

关于两阶段提交的过程如下图所示:

两阶段提交过程

 

顾名思义,2PC 分为两个过程

  1. 表决阶段:此时 Coordinator (协调者)向所有的参与者发送一个 vote request,参与者在收到这请求后,如果准备好了就会向 Coordinator 发送一个 VOTE_COMMIT 消息作为回应,告知 Coordinator 自己已经做好了准备,否则会返回一个 VOTE_ABORT 消息;
  2. 提交阶段:Coordinator 收到所有参与者的表决信息,如果所有参与者一致认为可以提交事务,那么 Coordinator 就会发送 GLOBAL_COMMIT 消息,否则发送 GLOBAL_ABORT 消息;对于参与者而言,如果收到 GLOBAL_COMMIT 消息,就会提交本地事务,否则就会取消本地事务。

2PC 一致性问题

这里先讨论一下,2PC 是否可以在任何情况下都可以解决一致性问题,在实际的网络生产中,各种情况都有可能发生,这里,我们先从理论上分析各种意外情况。

2PC 在执行过程中可能发生 Coordinator 或者参与者突然宕机的情况,在不同时期宕机可能有不同的现象。

情况分析及解决方案
Coordinator 挂了,参与者没挂这种情况其实比较好解决,只要找一个 Coordinator 的替代者。当他成为新的 Coordinator 的时候,询问所有参与者的最后那条事务的执行情况,他就可以知道是应该做什么样的操作了。所以,这种情况不会导致数据不一致。
参与者挂了(无法恢复),Coordinator 没挂如果挂了之后没有恢复,那么是不会导致数据一致性问题。
参与者挂了(后来恢复),Coordinator 没挂恢复后参与者如果发现有未执行完的事务操作,直接取消,然后再询问 Coordinator 目前我应该怎么做,协调者就会比对自己的事务执行记录和该参与者的事务执行记录,告诉他应该怎么做来保持数据的一致性。

 还有一种情况是:参与者挂了,Coordinator 也挂了,需要再细分为几种类型来讨论:

情况分析及解决方案
Coordinator 和参与者在第一阶段挂了由于这时还没有执行 commit 操作,新选出来的 Coordinator 可以询问各个参与者的情况,再决定是进行 commit 还是 roolback。因为还没有 commit,所以不会导致数据一致性问题。
Coordinator 和参与者在第二阶段挂了,但是挂的这个参与者在挂之前还没有做相关操作这种情况下,当新的 Coordinator 被选出来之后,他同样是询问所有参与者的情况。只要有机器执行了 abort(roolback)操作或者第一阶段返回的信息是 No 的话,那就直接执行 roolback 操作。如果没有人执行 abort 操作,但是有机器执行了 commit 操作,那么就直接执行 commit 操作。这样,当挂掉的参与者恢复之后,只要按照 Coordinator 的指示进行事务的 commit 还是 roolback 操作就可以了。因为挂掉的机器并没有做 commit 或者 roolback 操作,而没有挂掉的机器们和新的 Coordinator 又执行了同样的操作,那么这种情况不会导致数据不一致现象。
Coordinator 和参与者在第二阶段挂了,挂的这个参与者在挂之前已经执行了操作。但是由于他挂了,没有人知道他执行了什么操作。这种情况下,新的 Coordinator 被选出来之后,如果他想负起 Coordinator 的责任的话他就只能按照之前那种情况来执行 commit 或者 roolback 操作。这样新的 Coordinator 和所有没挂掉的参与者就保持了数据的一致性,我们假定他们执行了 commit。但是,这个时候,那个挂掉的参与者恢复了怎么办,因为他已经执行完了之前的事务,如果他执行的是 commit 那还好,和其他的机器保持一致了,万一他执行的是 roolback 操作呢?这不就导致数据的不一致性了么?虽然这个时候可以再通过手段让他和 Coordinator 通信,再想办法把数据搞成一致的,但是,这段时间内他的数据状态已经是不一致的了!

 所以,2PC协议中,如果出现协调者和参与者都挂了的情况,有可能导致数据不一致。为了解决这个问题,衍生除了3PC。

2PC 优缺点

简单总结一下 2PC 的优缺点:

  • 优点:原理简洁清晰、实现方便;
  • 缺点:同步阻塞、单点问题、某些情况可能导致数据不一致。

关于这几个缺点,在实际应用中,都是对2PC 做了相应的改造:

  1. 同步阻塞:2PC 有几个过程(比如 Coordinator 等待所有参与者表决的过程中)都是同步阻塞的,在实际的应用中,这可能会导致长阻塞问题,这个问题是通过超时判断机制来解决的,但并不能完全解决同步阻塞问题;
  2. Coordinator 单点问题:实际生产应用中,Coordinator 都会有相应的备选节点;
  3. 数据不一致:这个在前面已经讲述过了,如果在第二阶段,Coordinator 和参与者都出现挂掉的情况下,是有可能导致数据不一致的。

三阶段提交协议(3PC)


三阶段提交协议(Three-Phase Commit, 3PC)最关键要解决的就是 Coordinator 和参与者同时挂掉导致数据不一致的问题,所以 3PC 把在 2PC 中又添加一个阶段,这样三阶段提交就有:CanCommit、PreCommit 和 DoCommit 三个阶段。

3PC 过程

三阶段提交协议的过程如下图(图来自 维基百科:三阶段提交)所示:

三节点提交过程
三节点提交过程

3PC 的详细过程如下(这个过程步骤内容来自 2PC到3PC到Paxos到Raft到ISR):

阶段一 CanCommit

  1. 事务询问:Coordinator 向各参与者发送 CanCommit 的请求,询问是否可以执行事务提交操作,并开始等待各参与者的响应;
  2. 参与者向 Coordinator 反馈询问的响应:参与者收到 CanCommit 请求后,正常情况下,如果自身认为可以顺利执行事务,那么会反馈 Yes 响应,并进入预备状态,否则反馈 No。

阶段二 PreCommit

执行事务预提交:如果 Coordinator 接收到各参与者反馈都是Yes,那么执行事务预提交:

  1. 发送预提交请求:Coordinator 向各参与者发送 preCommit 请求,并进入 prepared 阶段;
  2. 事务预提交:参与者接收到 preCommit 请求后,会执行事务操作,并将 Undo 和 Redo 信息记录到事务日记中;
  3. 各参与者向 Coordinator 反馈事务执行的响应:如果各参与者都成功执行了事务操作,那么反馈给协调者 ACK 响应,同时等待最终指令,提交 commit 或者终止 abort,结束流程;

中断事务:如果任何一个参与者向 Coordinator 反馈了 No 响应,或者在等待超时后,Coordinator 无法接收到所有参与者的反馈,那么就会中断事务。

  1. 发送中断请求:Coordinator 向所有参与者发送 abort 请求;
  2. 中断事务:无论是收到来自 Coordinator 的 abort 请求,还是等待超时,参与者都中断事务。

阶段三 doCommit

执行提交

  1. 发送提交请求:假设 Coordinator 正常工作,接收到了所有参与者的 ack 响应,那么它将从预提交阶段进入提交状态,并向所有参与者发送 doCommit 请求;
  2. 事务提交:参与者收到 doCommit 请求后,正式提交事务,并在完成事务提交后释放占用的资源;
  3. 反馈事务提交结果:参与者完成事务提交后,向 Coordinator 发送 ACK 信息;
  4. 完成事务:Coordinator 接收到所有参与者 ack 信息,完成事务。

中断事务:假设 Coordinator 正常工作,并且有任一参与者反馈 No,或者在等待超时后无法接收所有参与者的反馈,都会中断事务

  1. 发送中断请求:Coordinator 向所有参与者节点发送 abort 请求;
  2. 事务回滚:参与者接收到 abort 请求后,利用 undo 日志执行事务回滚,并在完成事务回滚后释放占用的资源;
  3. 反馈事务回滚结果:参与者在完成事务回滚之后,向 Coordinator 发送 ack 信息;
  4. 中断事务:Coordinator 接收到所有参与者反馈的 ack 信息后,中断事务。

3PC 分析

3PC 虽然解决了 Coordinator 与参与者都异常情况下导致数据不一致的问题,3PC 依然带来其他问题:比如,网络分区问题,在 preCommit 消息发送后突然两个机房断开,这时候 Coordinator 所在机房会 abort, 另外剩余参与者的机房则会 commit(网络中断导致未收到消息的节点会执行commit)。

而且由于3PC 的设计过于复杂,在解决2PC 问题的同时也引入了新的问题,所以在实际上应用不是很广泛


参考:

分布式事务与一致性算法Paxos & raft & zab

分布式系统的一致性协议之 2PC 和 3PC

跟我学分布式事务之2PC和3PC

一致性及解决一致性的两种方式:2PC和3PC

<think>首先,用户的问题是:“如何在分布式系统或并发环境中保证缓存一致性?”这是一个关于分布式系统中缓存与数据库数据一致性的问题。我需要参考用户提供的引用:-引用[1]:讨论了电商分布式场景中数据库与缓存的一致性,提到了缓存穿透、雪崩、脏数据等问题,以及解决方案。-引用[2]:讨论了高并发分布式事务下的事务一致性,提到了CAP定理(一致性、可用性、分区容错性)。-引用[3]:详细描述了缓存一致性问题的本质,如缓存过期、穿透、击穿、雪崩,以及常见策略。-引用[4]:提供了缓存更新的Lua脚本示例,以及一致性保障的黄金法则,如没有银弹原则、分层设计原则、成本优先原则。用户指令要求:-系统级指令:所有行内数学表达式用$...$,独立公式用$$...$$,使用中文回答,生成相关问题,回答中引用的段落末尾自然地添加引用标识。-用户可见层:回答结构清晰,帮助用户逐步解决问题,保证真实可靠。所以,我的回答应该:1.结构清晰:逐步解释如何保证缓存一致性2.使用中文。3.引用时在末尾添加引用标识。4.生成相关问题。关键内容覆盖:-问题本质:缓存一致性问题源于数据库缓存的更新不同步。-常见策略:如缓存更新策略(Cache-Aside,Read-Through,Write-Through,Write-Back),版本控制,事务等。-解决方案:基于引用,包括版本号更新、最终一致性、监控等。结构化回答:1.**介绍问题**:解释缓存一致性问题在分布式系统中的重要性。2.**分析原因**:简要描述导致不一致的原因,如并发更新、网络延迟。3.**解决方案**:详细讨论各种策略。-缓存更新模式。-使用版本号或时间戳。-事务性保障。-最终一致性。4.**实践建议**:结合引用,提供实际例子或代码片段。5.**总结**:强调关键原则。6.**生成相关问题**:在结尾添加相关问题。引用标识:当引用具体内容时,在段落末尾添加[^1],[^2]等。例如,引用[^3]提到缓存过期、穿透等问题。数学表达式:如果有数学内容,用$...$或$$...$$,但在这个主题中可能较少,如果有概率或公式,才使用。例如,在描述一致性模型时,可能提到概率或公式。生成相关问题:像示例中那样,添加“§§相关问题§§”部分。最终回答大纲:1.引言:概述缓存一致性问题。2.原因分析:为什么在分布式系统中难以保证一致性3.解决方案策略:-缓存更新策略(Cache-Aside等)。-版本控制。-事务锁。-最终一致性。4.代码示例:基于引用[4],提供Lua脚本示例。5.最佳实践:结合引用,讨论原则如分层设计、成本优先。6.总结。7.相关问题。</think>### 如何保证分布式系统或并发环境中的缓存一致性? 在分布式系统或并发环境中,缓存一致性是一个核心挑战。缓存(如 Redis 或 Memcached)用于提升系统性能,但数据库与缓存之间的数据更新不同步会导致问题,如缓存过期(旧数据)、缓存穿透(查询不存在的数据)、缓存击穿(热点数据失效瞬间高并发)或缓存雪崩(大量缓存同时失效)。这些问题在高并发场景下可能引发数据库过载服务不可用。保证一致性的关键在于设计合适的策略,平衡性能与数据正确性。下面我将逐步解释解决方案,结合理论实践,确保回答真实可靠。 #### 1. **缓存一致性问题的本质** 在分布式系统中,缓存与数据库的更新操作可能在不同节点或时间点发生,导致数据不一致。原因包括: - **并发冲突**:多个线程或服务同时读写数据,引发竞态条件。 - **网络延迟**:分布式节点间通信延迟,导致更新顺序错乱。 - **故障恢复**:节点故障后恢复时,缓存与数据库状态不一致。 根据 CAP 定理(Consistency, Availability, Partition Tolerance),分布式系统无法同时满足强一致性、高可用性分区容错性。因此,大多数场景采用最终一致性(Eventual Consistency),即数据最终会一致,但允许短暂不一致[^2][^3]。 #### 2. **常见缓存一致性策略** 解决缓存一致性问题的策略可分为几类,以下结合引用内容详细说明。核心是减少脏数据风险,并通过原子操作保障更新顺序。 - **缓存更新模式**:选择合适的缓存交互模式是基础。 - **Cache-Aside(旁路缓存)**:应用层直接管理缓存。读取时,先查缓存,若未命中则从数据库加载并写入缓存;写入时,先更新数据库,再使缓存失效(如删除缓存)。这减少了缓存脏数据,但并发下可能因失效延迟导致不一致。优化方法:使用双删(更新数据库后延迟删除缓存两次)或版本号控制[^1][^3]。 - **Write-Through(直写)**:写入时同时更新数据库缓存,确保强一致性,但性能较低,适用于写少读多场景。 - **Write-Back(回写)**:写入时只更新缓存,异步批量写回数据库,性能高但数据丢失风险大,需配合 WAL(Write-Ahead Log)日志。 - **Read-Through**:缓存层自动加载数据库数据,简化应用逻辑,但需缓存支持。 - **版本控制与原子操作**:引入版本号(如时间戳或序列号)来保证更新顺序。例如,在缓存中存储数据值版本号,更新时比较版本,避免旧数据覆盖。引用[^4]提供了一个基于 Redis Lua 脚本的原子更新示例,确保版本检查与更新在单操作中完成: ```lua -- KEYS[1]=数据Key, KEYS[2]=版本Key, ARGV[1]=新值, ARGV[2]=新版本号 local currentVersion = tonumber(redis.call(&#39;GET&#39;, KEYS[2])) if currentVersion >= tonumber(ARGV[2]) then return 0 -- 版本过低,更新失败 end redis.call(&#39;SET&#39;, KEYS[1], ARGV[1]) redis.call(&#39;SET&#39;, KEYS[2], ARGV[2]) return 1 -- 更新成功 ``` 此脚本在 Java 中可通过 Jedis 或 Lettuce 调用,实现原子性,避免并发冲突[^4]。 - **分布式事务与锁**:在强一致性要求高的场景,使用分布式事务(如二阶段提交 2PC)或锁机制。 - **乐观锁**:通过版本号实现,如 CAS(Compare-and-Swap),适用于低冲突场景。 - **悲观锁**:如 Redis 分布式锁(Redlock 算法),锁定期间阻止其他操作,但可能降低可用性。公式示例:锁租期 $T$ 需满足 $T > \text{网络延迟} + \text{处理时间}$,以避免死锁[^2][^4]。 - **Saga 模式**:将长事务拆分为多个本地事务,通过补偿操作回滚,适用于微服务架构。 - **最终一致性保障**:90% 场景适用最终一致性,通过消息队列(如 Kafka 或 RabbitMQ)异步同步数据。 - **流程**:更新数据库后,发送事件到消息队列,消费者异步更新缓存。这隔离了数据库压力,但需处理消息丢失(重试机制)顺序问题(分区键保证顺序)。 - **监控与兜底**:添加 TTL(Time-To-Live)自动失效缓存,并监控缓存命中率。如引用[^4]建议,通过压测确定 TTL 锁参数,平衡一致性与性能。 - **多级缓存与架构优化**:分层设计降低风险。 - **多级缓存**:本地缓存(如 Caffeine)+ 分布式缓存,减少穿透风险。 - **一致性哈希**:分布式缓存节点分布均匀,降低热点问题概率,公式示例:节点映射 $h(key) \mod N$ 可优化为环状结构[^4]。 - **熔断与降级**:当缓存故障时,熔断机制直接访问数据库,防止雪崩。 #### 3. **实践建议与黄金法则** 基于引用内容,保证缓存一致性的最佳实践包括: - **没有银弹原则**:无通用方案,需根据业务容忍度选择组合策略。例如,电商订单支付需强一致性(事务锁),商品列表可容忍最终一致性(消息队列)[^1][^4]。 - **成本优先原则**:优先最终一致性,强一致性仅用于关键业务。通过压测优化参数,如缓存 TTL 设为数据库更新频率的 2 倍。 - **工程层保障**:完善重试、监控(如 Prometheus)告警,确保故障时快速恢复。引用[^4]强调分层设计:架构层(缓存模式)、算法层(版本号)、工程层(兜底机制)。 - **Java 代码示例**:在 Spring Boot 中,结合 Redis 实现 Cache-Aside 模式: ```java @Service public class CacheService { @Autowired private RedisTemplate<String, Object> redisTemplate; public Object getData(String key) { // 先查缓存 Object value = redisTemplate.opsForValue().get(key); if (value == null) { // 缓存未命中,查数据库 value = databaseService.fetchFromDB(key); // 写入缓存,设置TTL redisTemplate.opsForValue().set(key, value, 30, TimeUnit.SECONDS); } return value; } @Transactional public void updateData(String key, Object newValue) { // 先更新数据库 databaseService.updateDB(key, newValue); // 使缓存失效 redisTemplate.delete(key); // 可选:延迟双删,防止并发旧数据 new Thread(() -> { try { Thread.sleep(100); // 延迟100ms redisTemplate.delete(key); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }).start(); } } ``` 此代码展示了基本模式,实际中可加入版本号(如存储为 JSON 包含版本字段)[^1][^3]。 #### 4. **总结** 保证分布式缓存一致性的核心在于权衡:在高并发下,优先最终一致性性能,通过版本控制、原子操作异步机制减少风险。强一致性方案(如分布式事务)应谨慎使用,避免系统瓶颈。实践中,监控测试是关键,确保策略适应业务需求。记住,一致性不是绝对,而是根据场景优化[^3][^4]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值