分布式原理
CAP 理论是分布式系统中的一个基本理论,它由计算机科学家 Eric Brewer 在 2000 年提出。CAP 代表一致性(Consistency)、可用性(Availability)、分区容错性(Partition Tolerance)三个概念的首字母缩写。
一致性(Consistency): 所有节点看到的数据是一致的。在任何给定时间,所有节点的数据都是相同的,不会出现数据冲突或不一致的情况。在强一致性系统中,当一个写操作完成后,所有节点都能立即读取到最新的数据。
可用性(Availability): 系统保证在有限时间内对请求做出响应,并返回有效的结果。即使系统中的部分节点发生故障,仍能保证可用的服务。
分区容错性(Partition Tolerance): 系统能够容忍网络中的某些节点或通信链路的故障,即便发生网络分区(部分节点之间无法通信)的情况,系统仍能够继续工作。
CAP 理论指出,在分布式系统设计中,无法同时满足这三个特性,只能在一致性、可用性和分区容错性之间进行权衡。在网络分区(网络故障)的情况下,必须选择放弃一致性或者放弃可用性之一,以确保系统的分区容错性。这就意味着在分布式系统中,必须根据实际需求和场景来权衡选择。
**CP 架构:**对于 CP 来说,放弃可用性,追求一致性和分区容错性。
我们熟悉的 ZooKeeper,就是采用了 CP 一致性,ZooKeeper 是一个分布式的服务框架,主要用来解决分布式集群中应用系统的协调和一致性问题。其核心算法是 Zab,所有设计都是为了一致性。在 CAP 模型中,ZooKeeper 是 CP,这意味着面对网络分区时,为了保持一致性,它是不可用的。
AP 架构:对于 AP 来说,放弃强一致性,追求分区容错性和可用性,这是很多分布式系统设计时的选择,后面的 Base 也是根据 AP 来扩展的。
和 ZooKeeper 相对的是 Eureka,Eureka 是 Spring Cloud 微服务技术栈中的服务发现组件,Eureka 的各个节点都是平等的,几个节点挂掉不影响正常节点的工作,剩余的节点依然可以提供注册和查询服务,只要有一台 Eureka 还在,就能保证注册服务可用,只不过查到的信息可能不是最新的版本,不保证一致性。
BASE 理论是对分布式系统中数据一致性与可用性之间的折中原则的描述。它是对 ACID 特性相对的一种理念,主要用于描述分布式系统在面对网络分区和并发访问时的行为。
BASE 包含以下三个核心概念:
Basically Available(基本可用): 分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。
Soft state(软状态): ACID中的原子性可以理解为一种"硬状态",软状态则是在一定时间内,允许出现中间状态,比如临时的不一致状态。
**Eventually consistent(最终一致性):**虽然无法保证强一致性,但是在软状态结束以后,最终达到数据一致。
与 ACID 理论相对应,BASE
理论更侧重于分布式系统的可用性,允许系统在一定程度上放宽对一致性的要求。在大规模的分布式系统中,通过牺牲强一致性来实现可用性和分区容忍性,BASE
理论提供了一种更加灵活的设计思路。
Paxos 算法是分布式系统领域中用于解决一致性问题的一种算法,由 Leslie Lamport 在 1989 年提出。Paxos 算法解决了分布式系统中的一致性问题,特别是在存在故障和网络延迟的情况下,如何保证多个节点之间就某个值达成一致。
Paxos 算法的核心目标是在存在网络分区或节点故障的情况下,确保节点之间就某个值(提议)达成一致的算法。它主要包含三个阶段:
提议阶段(Prepare Phase): 提议者(Proposer)向接收者(Acceptor)发送准备请求(prepare request),请求接收者承诺不再接受编号小于当前提案编号的提案。
承诺阶段(Promise Phase): 接收者在接收到准备请求后,如果接收者还没有接受过编号大于当前提案编号的提案,就会向提议者发送承诺(promise),表示可以接受提案,并保证不再接受编号小于当前提案编号的提案。
接受阶段(Accept Phase): 如果提议者收到了大多数接收者的承诺,那么提议者就可以向接收者发送接受请求(accept request),并要求接收者接受该提案。
Paxos
算法通过这三个阶段的协调和通信,最终实现了多个节点对某个值达成一致。它能够处理网络分区、消息丢失、延迟等情况下的一致性问题,但是由于算法本身较为复杂,实现和理解上有一定的困难。然而,Paxos
算法为分布式系统中一致性问题提供了一种理论基础和解决思路。
Raft 是一种分布式一致性算法,旨在解决分布式系统中的一致性问题。它被设计为易于理解和实现,是一种领导者选举算法,适用于构建分布式一致性的系统,比如分布式数据库或分布式存储系统。
核心概念:
Raft 将节点分为三种角色:领导者(Leader)、跟随者(Follower)和候选人(Candidate)。领导者负责处理客户端的请求,并且在没有故障的情况下,每个任期只有一个领导者。
当领导者宕机或失去连接时,跟随者可以发起新一轮的领导者选举。选举的过程包括投票、候选人的提议等。
Raft 使用日志来记录系统状态的变化。领导者负责接收客户端的写请求,并将这些请求追加到其日志中。然后领导者会通知跟随者复制这些日志条目。
当大多数节点(多数派)确认已经复制了这些日志条目后,领导者可以提交这些日志条目,并应用到状态机中,确保所有节点状态的一致性。
Raft
算法致力于解决分布式一致性问题,通过领导者选举和日志复制等机制保证了分布式系统的一致性、可用性和容错性。其相对简单且清晰的设计使其成为分布式系统领域中的重要算法之一。
Quorum 选举机制是一种在分布式系统中用于达成共识或选举领导者的方法。它通常基于投票机制,确保在多个节点之间达成多数派(quorum)的共识,以决定某个操作的执行或者领导者的选举。
在 Quorum 选举机制中,多数派的概念非常重要。多数派表示系统中节点数量的一半加一(n/2 + 1),它是一种决策的阈值,确保了共识的达成。在一个典型的使用场景中,如果节点数为奇数,多数派的节点数将大于一半,确保了在节点间的投票中有足够的节点支持某项操作或者选举结果。
例如,对于一个由 5 个节点组成的系统,需要至少3个节点(即5的一半再加1)达成一致才能进行操作或选举领导者。如果只有两个节点同意,则不足以形成多数派,操作或选举将无法进行。
Quorum 选举机制在分布式系统中具有重要意义,因为它确保了系统在进行重要决策时能够达成一致,同时对抗了少数节点可能带来的错误或故障。这种机制被广泛应用于分布式数据库、分布式一致性算法(比如 Paxos 或 Raft)以及分布式存储系统等场景中。
在分布式场景中,ZooKeeper的应用十分广泛,比如数据发布和订阅、命名服务、配置中心、注册中心、分布式锁等。
ZooKeeper提供了一个类似于 Linux 文件系统的数据类型,和基于 Watcher 机制的分布式事件通知,这些特性都依赖于 ZooKeeper 的高容错数据一致性协议。
那么,在分布式场景下,ZooKeeper 是如何实现数据一致性的呢?
ZooKeeper 是通过 Zab 协议来保证分布式事务的最终一致性。Zab(ZooKeeper Atomic Broadcast,ZooKeeper 原子广播协议)支持崩溃恢复,基于该协议,ZooKeeper 实现了一种主备模式的系统架构来保持集群中各个副本之间数据一致性。
系统架构图如下:
在 ZooKeeper 集群中,所有客户端的请求都是写入到 Leader 进程中的,然后,由 Leader 同步到其他节点,称为 Follower。在集群数据同步的过程中,如果出现 Follower 节点崩溃或者 Leader 进程崩溃时,都会通过 Zab 协议来保证数据一致性。
Zab 协议的具体实现可以分为以下两部分:
消息广播阶段
Leader 节点接受事务提交,并且将新的 Proposal 请求广播给 Follower 节点,收集各个节点的反馈,决定是否进行 Commit,在这个过程中,也会使用到 Quorum 选举机制。
崩溃恢复阶段
如果在同步过程中出现 Leader 节点宕机,会进入崩溃恢复阶段,重新进行 Leader 选举,崩溃恢复阶段还包含数据同步操作,同步集群中最新的数据,保持集群的数据一致性。
整个 ZooKeeper 集群的一致性保证就是在上面两个状态之前切换,当 Leader 服务正常时,就是正常的消息广播模式;当 Leader 不可用时,则进入崩溃恢复模式,崩溃恢复阶段会进行数据同步,完成以后,重新进入消息广播阶段。
Zxid 是 Zab 协议的一个事务编号,Zxid 是一个 64 位的数字,其中低 32 位是一个简单的单调递增计数器,针对客户端每一个事务请求,计数器加 1;而高 32 位则代表 Leader 周期年代的编号。
这里 Leader 周期的英文是 epoch,可以理解为当前集群所处的年代或者周期,对比另外一个一致性算法 Raft 中的 Term 概念。在 Raft 中,每一个任期的开始都是一次选举,Raft 算法保证在给定的一个任期最多只有一个领导人。
Zab 协议的实现也类似,每当有一个新的 Leader 选举出现时,就会从这个 Leader 服务器上取出其本地日志中最大事务的 Zxid,并从中读取 epoch 值,然后加 1,以此作为新的周期 ID。总结一下,高 32 位代表了每代 Leader 的唯一性,低 32 位则代表了每代 Leader 中事务的唯一性。
Zab 的具体流程可以拆分为消息广播、崩溃恢复和数据同步三个过程
客户端的写请求进来之后,Leader 会将写请求包装成 Proposal 事务,并添加一个递增事务 ID,也就是 Zxid,Zxid 是单调递增的,以保证每个消息的先后顺序
广播这个 Proposal 事务,Leader 节点和 Follower 节点是解耦的,通信都会经过一个先进先出的消息队列,Leader 会为每一个 Follower 服务器分配一个单独的 FIFO 队列,然后把 Proposal 放到队列中
Follower 节点收到对应的 Proposal 之后会把它持久到磁盘上,当完全写入之后,发一个 ACK 给 Leader
当 Leader 收到超过半数 Follower 机器的 ack 之后,会提交本地机器上的事务,同时开始广播 commit, Follower 收到 commit 之后,完成各自的事务提交
消息广播通过 Quorum 机制,解决了 Follower 节点宕机的情况,但是如果在广播过程中 Leader 节点崩溃呢?
这就需要 Zab 协议支持的崩溃恢复,崩溃恢复可以保证在 Leader 进程崩溃的时候可以重新选出 Leader,并且保证数据的完整性。
崩溃恢复和集群启动时的选举过程是一致的,也就是说,下面的几种情况都会进入崩溃恢复阶段:
Zab 中的节点有三种状态,伴随着的 Zab 不同阶段的转换,节点状态也在变化:
状态 | 说明 |
---|---|
following | 当前节点是跟随者,服从Leader节点的命令 |
leading | 当前节点是Leader,负责协调事务 |
election/looking | 节点处于选举状态 |
崩溃恢复完成选举以后,接下来的工作就是数据同步,在选举过程中,通过投票已经确认 Leader 服务器是最大Zxid 的节点,同步阶段就是利用 Leader 前一阶段获得的最新Proposal历史,同步集群中所有的副本。
Paxos 的思想在很多分布式组件中都可以看到,Zab 协议可以认为是基于 Paxos 算法实现的,先来看下两者之间的联系:
都存在一个 Leader 进程的角色,负责协调多个 Follower 进程的运行
都应用 Quorum 机制,Leader 进程都会等待超过半数的 Follower 做出正确的反馈后,才会将一个提案进行提交
在 Zab 协议中,Zxid 中通过 epoch 来代表当前 Leader 周期,在 Paxos 算法中,同样存在这样一个标识,叫做 Ballot Number
两者之间的区别是,Paxos 是理论,Zab 是实践,Paxos 是论文性质的,目的是设计一种通用的分布式一致性算法,而 Zab 协议应用在 ZooKeeper 中,是一个特别设计的崩溃可恢复的原子消息广播算法。
Zab 协议增加了崩溃恢复的功能,当 Leader 服务器不可用,或者已经半数以上节点失去联系时,ZooKeeper 会进入恢复模式选举新的 Leader 服务器,使集群达到一个一致的状态。
区块链是一种分布式数据库技术,以块(block)的形式按照时间顺序链接起来,形成一个不断增长的、不可篡改的记录链(chain)。每个区块包含了一批交易数据以及与前一个区块相关联的加密学证明,确保数据的安全性和完整性。
关键特征包括:
分布式性:区块链数据被存储在许多不同的计算机节点上,而不是集中存储在单个地点。
不可篡改性:区块链的设计使得一旦数据被记录在链上,就很难更改。因为每个区块都包含了前一个区块的哈希值,改变一个区块会导致链中后续所有区块的哈希值变化,从而很容易被检测到。
去中心化:区块链通常不依赖于中央权威机构,而是由网络中的参与者共同管理和验证交易。
加密安全:使用密码学技术确保数据的安全性,例如公钥加密和哈希函数等。
区块链最著名的应用之一是加密货币,比特币就是第一个成功应用区块链技术的加密货币。除了加密货币,区块链还在金融、供应链管理、投票系统、数字身份验证等领域得到广泛应用,因为它的特性能够提供可靠的数据存储、去中心化的信任机制和安全性。
当谈论区块链和共识机制时,常见的几种方法包括:
Proof of Work (PoW):
Proof of Stake (PoS):
Delegated Proof of Stake (DPoS):
拜占庭将军问题是计算机科学中的一个经典问题,它涉及分布式系统中的可靠性和容错性。这个问题描述了这样一种情况:一群拜占庭将军(或节点)围攻一座城市,他们需要达成共识来决定是否发起进攻或者撤退,但其中一些将军可能是叛徒,并试图误导其他将军做出错误的决定。
在这个问题中,将军们需要达成一致的决策,但叛徒的存在使得这个共识过程变得困难。拜占庭将军问题的目标是找到一种算法或机制,使得即使在存在叛徒的情况下,系统中的忠诚节点仍能就某个提议达成一致的决策。
解决这个问题需要满足一些条件:
这个问题对于分布式系统和区块链等领域具有重要意义,因为在这些系统中,节点可能会受到故障或者攻击,而拜占庭将军问题提出了如何在这种环境下达成共识的挑战。解决这个问题的一些算法被用于设计拜占庭容错的分布式共识算法,确保在存在恶意节点的情况下依然能够保持系统的正确性。
当谈论区块链和共识机制时,常见的几种方法包括:
Proof of Work (PoW):
原理:PoW是比特币和一些其他加密货币所采用的共识机制。参与者(也称为矿工)通过解决数学难题来验证交易并创建新的区块。解决这些难题需要大量计算能力,因此矿工需要进行大量的计算来获得机会创建区块并获得奖励。
优点:安全性高,防止双重支付和篡改数据。 缺点:需要大量的计算能力和能源消耗高。
Proof of Stake (PoS):
Delegated Proof of Stake (DPoS):
采用 DPOS(Delegated Proof of Stake,委托权益证明)机制的典型代表是 EOS,如果说 POS 类似股东大会,比较的是谁持有的股份多,那么 DPOS 类似于公司董事会制度,在 DPOS 共识制度下,会选出一定数量的代表,来负责生产区块。
在分布式系统下,一个业务跨越多个服务或数据源,每个服务都是一个分支事务,要保证所有分支事务最终状态一致,这样的事务就是分布式事务。
分布式事务的解决方案,典型的有两阶段和三阶段提交协议、 TCC 分段提交,和基于消息队列的最终一致性设计。
两阶段提交(2PC,Two-phase Commit Protocol)是非常经典的强一致性、中心化的原子提交协议,在各种事务和一致性解决方案中,都能看到两阶段提交的应用。
三阶段提交:是在两阶段提交之上扩展的提交协议,主要是为了解决两阶段提交协议的阻塞问题,从原来的两个阶段扩展为三个阶段,增加了超时机制。
TCC分段提交:TCC是一个分布式事务的处理模型,将事务过程拆分为Try、Confirm、Cancel 三个步骤,在保证强一致性的同时,最大限度提高系统的可伸缩性和可用性。
基于消息队列的最终一致性:
异步化在分布式系统设计中随处可见,基于消息队列的最终一致性就是一种异步事务机制,在业务中广泛应用。
在具体实现上,主要有本地消息表和第三方可靠消息队列等。
下面介绍一下本地消息表:本地消息表的方案最初是由 ebay 的工程师提出,核心思想是将分布式事务拆分成本地事务进行处理,通过消息日志的方式来异步执行。
本地消息表是一种业务耦合的设计,消息生产方需要额外建一个事务消息表,并记录消息发送状态,消息消费方需要处理这个消息,并完成自己的业务逻辑,另外会有一个异步机制来定期扫描未完成的消息,确保最终一致性。
分布式事务开源组件应用比较广泛的是蚂蚁金服开源的 Seata,也就是 Fescar,前身是阿里中间件团队发布的 TXC(Taobao Transaction Constructor)和升级后的 GTS(Global Transaction Service)。
Seata 的设计思想是把一个分布式事务拆分成一个包含了若干分支事务(Branch Transaction)的全局事务(Global Transaction)。分支事务本身就是一个满足 ACID 的 本地事务,全局事务的职责是协调其下管辖的分支事务达成一致,要么一起成功提交,要么一起失败回滚。
在 Seata 中,全局事务对分支事务的协调基于两阶段提交协议,类似数据库中的 XA 规范,XA 规范定义了三个组件来协调分布式事务,分别是 AP 应用程序、TM 事务管理器、RM 资源管理器、CRM 通信资源管理器。
两阶段提交(2PC)和三阶段提交(3PC)都是用于在分布式系统中实现事务一致性的协议。它们的目标都是确保跨多个节点或服务的事务操作能够要么全部提交(达成一致),要么全部回滚,以维护数据的一致性。
阶段1 - 准备阶段(Prepare Phase):
协调者(Coordinator)向所有参与者(Participants)发送准备请求,并等待它们的响应。
参与者收到请求后,执行事务操作,并将准备就绪的消息或“同意”响应返回给协调者。
阶段2 - 提交或回滚阶段(Commit or Rollback Phase):
如果所有参与者都准备就绪,协调者向所有参与者发送提交请求,并等待它们的响应。
参与者收到提交请求后,如果事务执行成功,则提交事务并发送“提交完成”响应;如果执行失败,则回滚事务并发送“回滚完成”响应。
特点:2PC存在单点故障(协调者故障可能导致阻塞)、同步阻塞(在阶段2需要等待所有参与者的响应)、存在潜在的两阶段提交问题(即可能出现参与者在已经同意事务后出现问题无法提交的情况)。
三阶段提交是对两阶段提交的改进,旨在减少一些2PC的缺点。
协调者向参与者发送 CanCommit 请求。
参与者执行事务操作,如果准备就绪,则发送“Yes”响应,否则发送“No”响应。
协调者收到所有“Yes”响应后,向所有参与者发送 PreCommit 请求。
参与者收到请求后,事务进入预提交状态,但不立即执行提交。
协调者收到所有 PreCommit 响应后,向所有参与者发送 DoCommit 请求。
参与者收到请求后,执行最终提交并向协调者发送“已提交”响应。
区别和联系:
XA(eXtended Architecture)是用于实现分布式事务的标准之一,它定义了事务管理器(Transaction Manager)和资源管理器(Resource Manager)之间的接口和协议。MySQL 实现了 XA 接口以支持分布式事务。
MySQL InnoDB 引擎中和一致性相关的有重做日志(redo log)、回滚日志(undo log)和二进制日志(binlog)。
XA 是由 X/Open 组织提出的分布式事务规范,XA 规范主要定义了事务协调者(Transaction Manager)和资源管理器(Resource Manager)之间的接口。
事务协调者(Transaction Manager),因为 XA 事务是基于两阶段提交协议的,所以需要有一个协调者,来保证所有的事务参与者都完成了准备工作,也就是 2PC 的第一阶段。如果事务协调者收到所有参与者都准备好的消息,就会通知所有的事务都可以提交,也就是 2PC 的第二阶段。
在前面的内容中我们提到过,之所以需要引入事务协调者,是因为在分布式系统中,两台机器理论上无法达到一致的状态,需要引入一个单点进行协调。协调者,也就是事务管理器控制着全局事务,管理事务生命周期,并协调资源。
资源管理器(Resource Manager),负责控制和管理实际资源,比如数据库或 JMS 队列。
目前,主流数据库都提供了对 XA 的支持,在 JMS 规范中,即 Java 消息服务(Java Message Service)中,也基于 XA 定义了对事务的支持。
XA 事务是两阶段提交的一种实现方式,根据 2PC 的规范,XA 将一次事务分割成了两个阶段,即 Prepare 和 Commit 阶段。
MySQL支持XA事务规范,这包括内部XA和外部XA两种情况。
内部 XA:
外部 XA:
TCC(Try-Confirm-Cancel)的概念来源于 Pat Helland 发表的一篇名为“Life beyond Distributed Transactions:an Apostate’s Opinion”的论文。
TCC 提出了一种新的事务模型,基于业务层面的事务定义,锁粒度完全由业务自己控制,目的是解决复杂业务中,跨表跨库等大颗粒度资源锁定的问题。TCC 把事务运行过程分成 Try、Confirm / Cancel 两个阶段,每个阶段的逻辑由业务代码控制,避免了长事务,可以获取更高的性能。
Try 阶段失败可以 Cancel,如果 Confirm 和 Cancel 阶段失败了怎么办? TCC 中会添加事务日志,如果
Confirm 或者 Cancel 阶段出错,则会进行重试,所以这两个阶段需要支持幂等;如果重试失败,则需要人工介入进行恢复和处理等。
实际开发中,TCC 的本质是把数据库的二阶段提交上升到微服务来实现,从而避免数据库二阶段中长事务引起的低性能风险。
所以说,TCC 解决了跨服务的业务操作原子性问题,比如下订单减库存,多渠道组合支付等场景,通过 TCC 对业务进行拆解,可以让应用自己定义数据库操作的粒度,可以降低锁冲突,提高系统的业务吞吐量。
TCC 的不足主要体现在对微服务的侵入性强,TCC 需要对业务系统进行改造,业务逻辑的每个分支都需要实现 try、Confirm、Cancel 三个操作,并且 Confirm、Cancel 必须保证幂等。
另外 TCC 的事务管理器要记录事务日志,也会损耗一定的性能。
2PC/XA 是数据库或者存储资源层面的事务,实现的是强一致性,在两阶段提交的整个过程中,一直会持有数据库的锁。
TCC 关注业务层的正确提交和回滚,在 Try 阶段不涉及加锁,是业务层的分布式事务,关注最终一致性,不会一直持有各个业务资源的锁。
TCC 的核心思想是针对每个业务操作,都要添加一个与其对应的确认和补偿操作,同时把相关的处理,从数据库转移到业务中,以此实现跨数据库的事务。
雪花算法的时钟回拨:
因为服务器的本地时钟并不是绝对准确的,在一些业务场景中,比如在电商的整点抢购中,为了防止不同用户访问的服务器时间不同,则需要保持服务器时间的同步。为了确保时间准确,会通过
NTP 的机制来进行校对,NTP(Network Time Protocol)指的是网络时间协议,用来同步网络中各个计算机的时间。
如果服务器在同步 NTP 时出现不一致,出现时钟回拨,那么 SnowFlake 在计算中可能出现重复 ID。除了 NTP 同步,闰秒也会导致服务器出现时钟回拨,不过时钟回拨是小概率事件,在并发比较低的情况下一般可以忽略。关于如何解决时钟回拨问题,可以进行延迟等待,直到服务器时间追上来为止。
我们都知道,在业务开发中,为了保证在多线程下处理共享数据的安全性,需要保证同一时刻只有一个线程能处理共享数据。
Java 语言给我们提供了线程锁,开放了处理锁机制的 API,比如 Synchronized、Lock 等。当一个锁被某个线程持有的时候,另一个线程尝试去获取这个锁会失败或者阻塞,直到持有锁的线程释放了该锁。
分布式场景下解决并发问题,需要应用分布式锁技术。分布式锁的目的是保证在分布式部署的应用集群中,多个服务在请求同一个方法或者同一个业务操作的情况下,对应业务逻辑只能被一台机器上的一个线程执行,避免出现并发问题。
基于关系型数据库
基于关系型数据库实现分布式锁,是依赖数据库的唯一性来实现资源锁定,比如主键和唯一索引等。
以唯一索引为例,创建一张锁表,定义方法或者资源名、失效时间等字段,同时针对加锁的信息添加唯一索引,比如方法名,当要锁住某个方法或资源时,就在该表中插入对应方法的一条记录,插入成功表示获取了锁,想要释放锁的时候就删除这条记录。
相比基于数据库实现分布式锁,缓存的性能更好,并且各种缓存组件也提供了多种集群方案,可以解决单点问题。
常见的开源缓存组件都支持分布式锁,包括 Redis、Memcached 及 Tair。以常见的 Redis 为例,应用 Redis 实现分布式锁,最直接的想法是利用 setnx 和 expire 命令实现加锁。
在 Redis 中,setnx 是「set if not exists」如果不存在,则 SET 的意思,当一个线程执行 setnx 返回 1,说明 key 不存在,该线程获得锁;当一个线程执行 setnx 返回 0,说明 key 已经存在,那么获取锁失败,expire 就是给锁加一个过期时间。
ZooKeeper 有四种节点类型,包括持久节点、持久顺序节点、临时节点和临时顺序节点,利用 ZooKeeper 支持临时顺序节点的特性,可以实现分布式锁。
当客户端对某个方法加锁时,在 ZooKeeper 中该方法对应的指定节点目录下,生成一个唯一的临时有序节点。
判断是否获取锁,只需要判断持有的节点是否是有序节点中序号最小的一个,当释放锁的时候,将这个临时节点删除即可,这种方式可以避免服务宕机导致的锁无法释放而产生的死锁问题。
下面描述使用 ZooKeeper 实现分布式锁的算法流程,根节点为 /lock:
客户端连接 ZooKeeper,并在 /lock 下创建临时有序子节点,第一个客户端对应的子节点为 /lock/lock01/00000001,第二个为 /lock/lock01/00000002;
其他客户端获取 /lock01 下的子节点列表,判断自己创建的子节点是否为当前列表中序号最小的子节点;
如果是则认为获得锁,执行业务代码,否则通过 watch 事件监听 /lock01 的子节点变更消息,获得变更通知后重复此步骤直至获得锁;
完成业务流程后,删除对应的子节点,释放分布式锁。
一般来说,生产环境可用的分布式锁需要满足以下几点:
Redis 支持 setnx 指令,只在 key 不存在的情况下,将 key 的值设置为 value,若 key 已经存在,则 setnx 命令不做任何动作。使用 setnx 实现分布式锁的方案,获取锁的方法很简单,只要以该锁为 key,设置一个随机的值即可。如果 setnx 返回 1,则说明该进程获得锁;如果 setnx 返回 0,则说明其他进程已经获得了锁,进程不能进入临界区;如果需要阻塞当前进程,可以在一个循环中不断尝试 setnx 操作。
上面分布式锁的实现方案中,都是针对单节点 Redis 而言的,在生产环境中,为了保证高可用,避免单点故障,通常会使用 Redis 集群。
集群环境下,Redis 通过主从复制来实现数据同步,Redis 的主从复制(Replication)是异步的,所以单节点下可用的方案在集群的环境中可能会出现问题,在故障转移(Failover) 过程中丧失锁的安全性。
由于 Redis 集群数据同步是异步的,假设 Master 节点获取到锁后在未完成数据同步的情况下,发生节点崩溃,此时在其他节点依然可以获取到锁,出现多个客户端同时获取到锁的情况。
Redlock 算法是在单 Redis 节点基础上引入的高可用模式,Redlock 基于 N 个完全独立的 Redis 节点,一般是大于 3 的奇数个(通常情况下 N 可以设置为 5),可以基本保证集群内各个节点不会同时宕机。
假设当前集群有 5 个节点,运行 Redlock 算法的客户端依次执行下面各个步骤,来完成获取锁的操作:
客户端记录当前系统时间,以毫秒为单位;
依次尝试从 5 个 Redis 实例中,使用相同的 key 获取锁,当向 Redis 请求获取锁时,客户端应该设置一个网络连接和响应超时时间,超时时间应该小于锁的失效时间,避免因为网络故障出现的问题;
客户端使用当前时间减去开始获取锁时间就得到了获取锁使用的时间,当且仅当从半数以上的 Redis 节点获取到锁,并且当使用的时间小于锁失效时间时,锁才算获取成功;
如果获取到了锁,key 的真正有效时间等于有效时间减去获取锁所使用的时间,减少超时的几率;
如果获取锁失败,客户端应该在所有的 Redis 实例上进行解锁,即使是上一步操作请求失败的节点,防止因为服务端响应消息丢失,但是实际数据添加成功导致的不一致。
在 Redis 官方推荐的 Java 客户端 Redisson 中,内置了对 RedLock 的实现。
RPC(Remote Procedure Call,远程过程调用),指的是一种计算机通信协议,允许程序调用另一个地址空间(通常是另一台机器上)的程序或函数,就像调用本地函数一样,而无需程序员显式编写远程调用的细节。RPC使得分布式系统中的不同部分能够像调用本地函数一样进行通信和交互。
一些与Java相关的RPC框架包括:
优点:
缺点:
API网关在微服务架构中扮演着重要的角色,以下是它的几个关键作用:
网关的缺点:
在微服务领域,有许多开源网关实现,应用比较多的是 Spring Cloud Zuul 和 Spring Cloud Gateway。
Spring Cloud Zuul 是 Spring Cloud Netflix 项目的核心组件之一,是 Netflix 开发的一款提供动态路由、监控、弹性、安全的网关服务。
Zuul 分为 1.x 和 2.x 两个大版本,1.x 版本是基于 Servlet 构建的,采用的是阻塞和多线程方式。1.x 版本在 Spring Cloud 中做了比较好的集成,但是性能不是很理想。后来 Netflix 宣布开发 2.x 版本,目前已经更新到了 2.x 版本,不过 Spring Cloud 官方并没有集成,而是开发了自己的 Spring Cloud Gateway。
Spring Cloud Gateway 是 Spring Cloud 体系的第二代网关组件,基于 Spring 5.0 的新特性 WebFlux 进行开发,底层网络通信框架使用的是 Netty。
Spring Cloud Gateway 可以替代第一代的网关组件 Zuul。Spring Cloud Gateway 可以通过服务发现组件自动转发请求,集成了 Ribbon 做负载均衡,支持使用 Hystrix 对网关进行保护,当然也可以选择其他的容错组件,比如集成阿里巴巴开源的 Sentinel,实现更好的限流降级等功能。
分布式系统下微服务架构的一个重要特性就是可以快速上线或下线,从而可以让服务进行水平扩展,以保证服务的可用性。
假设有一个电商会员服务,随着业务发展,服务器负载越来越高,需要新增服务器。如果没有服务注册与发现,就要把新的服务器地址配置到所有依赖会员模块的服务,并相继重启它们,这显然是不合理的。
服务注册与发现就是保证当服务上下线发生变更时,服务消费者和服务提供者能够保持正常通信。
有了服务注册和发现机制,消费者不需要知道具体服务提供者的真实物理地址就可以进行调用,也无须知道具体有多少个服务者可用;而服务提供者只需要注册到注册中心,就可以对外提供服务,在对外服务时不需要知道具体是哪些服务调用了自己。
首先,在服务启动时,服务提供者会向注册中心注册服务,暴露自己的地址和端口等,注册中心会更新服务列表。服务消费者启动时会向注册中心请求可用的服务地址,并且在本地缓存一份提供者列表,这样在注册中心宕机时仍然可以正常调用服务。
如果提供者集群发生变更,注册中心会将变更推送给服务消费者,更新可用的服务地址列表。
在讨论分布式系统时,一致性是一个绕不开的话题,在服务发现中也是一样。CP 模型优先保证一致性,可能导致注册中心可用性降低,AP 模型优先保证可用性,可能出现服务错误。
为了保证微服务的高可用,避免单点故障,注册中心一般是通过集群的方式来对外服务,比如 ZooKeeper 集群。
ZooKeeper 核心算法是 Zab,实现的是 CP 一致性,所以 ZooKeeper 作为服务发现解决方案,在使用 ZooKeeper 获取服务列表时,如果 ZooKeeper 正在选主,或者 ZooKeeper 集群中半数以上机器不可用时,那么将无法获得数据。
在 Spring Cloud Eureka 中,各个节点都是平等的,几个节点挂掉不影响正常节点的工作,剩余的节点依然可以提供注册和查询服务。只要有一台 Eureka 还在,就能保证注册服务可用,只不过查到的信息可能不是最新的版本,不保证一致性。
Spring Cloud Nacos 在 1.0.0 版本正式支持 AP 和 CP 两种一致性协议,可以动态切换。
对于服务注册和发现场景来说,一般认为,可用性比数据一致性更加重要。针对同一个服务,即使注册中心的不同节点保存的服务提供者信息不相同,会出现部分提供者地址不存在等,不会导致严重的服务不可用。对于服务消费者来说,能消费才是最重要的,拿到可能不正确的服务实例信息后尝试消费,也要比因为无法获取实例信息而拒绝服务好。
在复杂的分布式系统中,一个请求可能会经过多个不同的服务和组件,分布式调用跟踪技术可以帮助追踪请求在系统中的传播路径,并且记录各个组件的响应时间、耗时、错误信息等重要数据,以便于排查问题、性能优化和分析系统的行为。
Zipkin是一个开源的分布式跟踪系统,用于收集、存储和可视化分布式系统中的调用链路和跟踪数据。它可以帮助开发人员追踪请求的路径,定位系统中的性能问题和瓶颈。
单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。
数据库的拆分(Sharding)是指根据不同的策略将一个大型数据库拆分成多个较小的数据库单元,以提高性能、可扩展性和并发处理能力。主要有水平拆分(Horizontal Sharding)和垂直拆分(Vertical Sharding)两种方式。
水平拆分(Horizontal Sharding):水平拆分是指按照某个规则将数据行拆分到多个数据库中,通常是按照某个列或者规则进行划分。例如,按照用户ID、时间范围、地理位置等将数据行分布到不同的数据库中。
垂直拆分(Vertical Sharding):垂直拆分是指将数据库中的表按照某种关系或者业务逻辑拆分成不同的表或者数据库,通常是按照表中的列进行拆分。例如,将一个大表按照列的关联性拆分成多个小表。
分布式事务问题:对业务进行分库之后,同一个操作会分散到多个数据库中,涉及跨库执行 SQL 语句,也就出现了分布式事务问题。
比如数据库拆分后,订单和库存在两个库中,一个下单减库存的操作,就涉及跨库事务。关于分布式事务的处理,可以使用分布式事务中间件,实现 TCC 等事务模型;也可以使用基于本地消息表的分布式事务实现。
跨库关联查询问题:分库分表后,跨库和跨表的查询操作实现起来会比较复杂,性能也无法保证。在实际开发中,针对这种需要跨库访问的业务场景,一般会使用额外的存储,比如维护一份文件索引。另一个方案是通过合理的数据库字段冗余,避免出现跨库查询。
限流算法是在分布式系统中用来控制流量并保护系统免受过载的影响的重要工具。以下是一些常见的限流算法:
负载均衡算法用于在多个服务器或节点间分发工作负载,以确保各个节点能够有效地处理请求,提高系统的性能和可靠性。以下是一些常见的负载均衡算法:
在分布式场景下,除了不方便查看集群日志以外,传统日志收集都存在哪些问题?
无法实现日志的快速搜索:传统的查找是基于文件的,搜索效率比较低,并且很难对日志文件之间的关系进行聚合,无法从全局上梳理日志,也很难进行调用链路的分析。
日志的集中收集和存储困难:当有上千个节点的时候,日志是分布在许多机器上的,如果要获取这些日志的文件,不可能一台一台地去查看,所以这就是很明显的一个问题。
日志分析聚合及可视化:由于日志文件分布在不同的服务器上,因此进行相关的分析和聚合非常困难,也不利于展开整体的数据分析工作。
除了这些,还有日志安全问题,以电商场景为例,很多业务日志都是敏感数据,比如用户下单数据、账户信息等,如何管理这些数据的安全性,也是一个很关键的问题。
ELK 日志系统
ELK 是一个流行的日志管理解决方案,它由三个开源工具组成:Elasticsearch、Logstash 和 Kibana。
Elasticsearch:
Elasticsearch 是一个分布式搜索和分析引擎,专门用于存储和检索大量数据。它以实时、高可用和可扩展的特性而闻名,能够快速地索引、搜索和分析海量数据。
Logstash:
Logstash 是一个用于日志收集、处理和转发的工具。它能够从各种来源收集数据,包括日志文件、消息队列、数据库等,然后进行清洗、结构化和转换,最终将数据发送到Elasticsearch等存储和分析平台。
Kibana:
Kibana 是一个用于数据可视化和分析的工具。它提供了一个直观的界面,可以通过图表、图形和仪表板等方式对Elasticsearch中的数据进行查询、分析和展示。用户可以通过Kibana创建定制化的可视化报表和仪表板,实时监控系统的状态和趋势。
ELK 日志系统通常用于以下方面: