分布式技术原理与设计
03 面试官如何考察与 CAP 有关的分布式理论?
在互联网技术面试中,考察分布式技术已经是面试的标配了。打开拉勾招聘,你能发现,一线互联网公司在对候选人的要求中都有“分布式系统设计”这一关键词。无论你是程序员,还是架构师,都要掌握分布式系统设计。那么从今天起,我用 4 讲的时间带你打卡分布式技术的面试内容。今天这一讲,我们就先来看一看怎么回答分布式的基础理论,才能抓住面试官的心。
案例背景
CAP 理论是分布式系统中最核心的基础理论,虽然在面试中,面试官不会直白地问你 CAP 理论的原理,但是在面试中遇到的分布式系统设计问题,都绕不开你对 CAP 的理解和思考。
而且在面试中,针对面试不同岗位的候选者,面试官的要求也会不一样,要求你回答的深度也不一样。所以在这一讲中,我会针对初中级研发工程师和高级研发工程师两个不同的角度,分析面试思路。
案例分析
相信只要学习过分布式技术的相关知识,基本上都知道 CAP 理论指的是什么:C(Consistency)是数据一致性、A(Availability)是服务可用性、P(Partition tolerance)是分区容错性。C、A、P 只能同时满足两个目标,而由于在分布式系统中,P 是必须要保留的,所以要在 C 和 A 间进行取舍。假如要保证服务的可用性,就选择 AP 模型,而要保证一致性的话,就选择 CP 模型。
很多候选者如果发现面试题(比如“为了数据容灾,我们会做数据的主从备份,那么主从节点的数据一致性对调用端有什么影响呢?”)涉及了对“CAP 的理解和思考”,会下意识地做出类似的答案:“ CAP 理论描述了在出现网络分区的情况下,要在 C 和 A 之间做取舍,所以会影响站在调用端的视角看系统是不可用的”。如果是我的话,大概会给个及格分,并认为这样的回答,只能证明你有准备,不能证明你有能力。
因为在面试中遇到理论问题时,单纯做浮于表面的概念性阐述,很难向面试官证明你的技术能力。 面试官会觉得你是一个刚接触分布式系统,或者对分布式系统理解不够深入的研发,如果这恰好是你第一个面试题,会直接影响面试官对你的第一印象,甚至影响你的定级。
从我的经验出发,如果你想答得更好,你需要先掌握 CAP 的原理、实践经验、技术认知,然后再结合具体的面试题具体分析。
问题解答
理解原理
现在有一个分布式系统 A,它有一个副本 A1,在正常情况下,客户端 Client 写数据到系统 A,然后数据从 A 节点同步到 A1 节点,再返回给 Client 成功状态。这时,客户端 Client 从任何节点 A 或 A1 读取数据,都能读取到最新写入的数据,说明 A 和 A1 的数据是一致的,并且 A 和 A1 也都是可用的。
但由于网络是不可靠的,节点 A 和 A1 的网络随时会因为中断而出现分区。所谓网络分区就是由于网络不通导致节点 A 和 A1 被隔离在不同的网络子集中,此时节点 A 的数据就不能及时同步到节点 A1 中了。
在分布式系统中,由于网络问题导致的网络分区是常态。也就是说出现网络分区时,根据 CAP 理论,需要在 A 和 C 中进行取舍,即要么保证系统的可用性,要么保证数据一致性。
这里你要注意了,上面的例子有个大前提,就是系统出现了网络分区,但实际情况是,在绝大多数时间里并不存在网络分区(网络不会经常出现问题)。那么还要进行三选二吗(CP 或者 AP)?
其实,不同的分布式系统要根据业务场景和业务需求在 CAP 三者中进行权衡。CAP 理论用于指导在系统设计时需要衡量的因素,而非进行绝对地选择。
当网络没有出现分区时,CAP 理论并没有给出衡量 A 和 C 的因素,但如果你做过实际的分布式系统设计,一定会发现系统数据同步的时延(Latency),即例子中节点 A 同步数据到节点 A1 的时间才是衡量 A 和 C 最重要的因素,此时就不会有绝对的 AP 模型还是 CP 模型了,而是源于对实际业务场景的综合考量。
因此,才会有如 PACELC 这样的新模型优化原有的 CAP 理论,理论指导实践,实践优化理论。根据 PACELC 模型的定义,如果有网络分区产生,系统就必须在 A 和 C 之间取得平衡,否则(Else,即 PACELC 中的 E)当系统运行在无网络分区情况下,系统需要在 L(延迟)和 C 之间取得平衡。
PACELC
但理解到这个程度还不够,你还需要结合落地经验进行证明。
实践经验
你要意识到,互联网分布式的设计方案是数据一致性和系统可用性的权衡,并不是非此即彼,这一点尤为重要。所以即使无法做到强一致性(简单来讲强一致性就是在任何时刻所有的用户查询到的数据都是最新的),也可以根据自身的业务特点,采用适当的方式来使系统达到最终一致性。
这时就要引出 BASE 理论,它是 CAP 理论的延伸。BASE 是 Basically Available(基本可用)、Soft State(软状态)和 Eventually Consistent(最终一致性)三个单词的简写,作用是保证系统的可用性,然后通过最终一致性来代替强一致性,它是目前分布式系统设计中最具指导意义的经验总结。那么在实际项目中,你如何通过 BASE 理论来指导设计实践呢?
BASE 中的基本可用指的是保障核心功能的基本可用,其实是做了“可用性”方面的妥协,比如:
- 电商网站在双十一大促等访问压力较大的时候,关闭商品排行榜等次要功能的展示,从而保证商品交易主流程的可用性,这也是我们常说的服务降级;
- 为了错开双十一高峰期,电商网站会将预售商品的支付时间延后十到二十分钟,这就是流量削峰;
- 在你抢购商品的时候,往往会在队列中等待处理,这也是常用的延迟队列。
软状态和最终一致性指的是允许系统中的数据存在中间状态,这同样是为了系统可用性而牺牲一段时间窗内的数据一致性,从而保证最终的数据一致性的做法。
目前这种处理数据的方式几乎成了互联网的标配设计模式,最经典的例子是在用户下单的时候不需要真正地扣减库存,而是仅在前台计个数,然后通过异步任务在后台批量处理。
如果你想应聘的是初中级研发工程师,那么结合上述思路,从理论理解到落地实践,你已经可以把 CAP 理论答得较为清楚了。回答问题的逻辑可以参考我给出的建议:
- 先充分理解理论原理,不能仅浮在概念上(这一点需要你课下下功夫);
- 其次需要有自己的思考,表现出你思考能力的不同;
- 然后将理论结合于实践,讨论实际中处理问题时的思考逻辑。
技术认知
如果你应聘的是高级研发工程师或架构师,在回答时,还要尽可能地展示知识体系和技术判断力,这是这两个岗位的基本素质。 因为分布式技术错综复杂,各种技术又相互耦合,在面试中,如果你能通过一个 CAP 理论的知识点,扩展出一个脉络清晰的分布式核心技术知识体系,就会与其他人拉开差距。
分布式系统看起来就像一个计算机。计算机包括五大体系结构(即冯诺依曼结构),它有五大部件:分别是控制器、运算器、存储器、输入及输出。你可以这么理解:一个分布式系统也包含这五大部件,其中最重要的是计算与存储。计算与存储由一系列网络节点组成,每个节点之间的通信就是输入与输出,各节点之间的调度管理就是控制器。
分布式架构技术组成
这么看来,分布式系统就像一个网络计算机,它的知识体系包括四个角度:
- 存储器,即分布式存储系统,如 NoSQL 数据库存储;
- 运算器,即分布式计算,如分布式并行计算;
- 输入输出,即分布式系统通信,如同步 RPC 调用和异步消息队列;
- 控制器,即调度管理,如流量调度、任务调度与资源调度。
你可以从这四个角度来概括分布式系统的知识体系(每个分支的具体子知识体系和知识点,我会在后面的课程中一一为你讲解)。
那么具体的解题思路是什么呢? 还是以“Redis 是否可以作为分布式锁”为例,咱们一起来分析一下问题背后隐藏的分布式理论知识,以及作为高级研发工程师的解题思路。
解题思路
- 说明现实存在的问题
一般使用 setnx 方法,通过 Redis 实现锁和超时时间来控制锁的失效时间。但是在极端的情况下,当 Reids 主节点挂掉,但锁还没有同步到从节点时,根据哨兵机制,从就变成了主,继续提供服务。这时,另外的线程可以再来请求锁,此时就会出现两个线程拿到了锁的情况。
- 回归理论的指导
根据对 CAP 理论的理解,Redis 的设计模型是 AP 模型,而分布式锁是一个 CP 场景,那么很明显,将 Redis 这种 AP 模型的架构应用于 CP 的场景,在底层的技术选型上就是错误的。
- 扩展到知识体系
Redis 属于分布式存储系统,你的头脑里就要有对分布式存储系统领域的知识体系。思考它的数据存储、数据分布、数据复制,以及数据一致性都是怎么做的,用了哪些技术来实现,为什么要做这样的技术或算法选型。你要学会从多维度、多角度去对比、分析同一分布式问题的不同方法,然后综合权衡各种方法的优缺点,最终形成自己的技术认知和技术判断力。
- 有技术的判断力
比如通过 Redis,你能想到目前分布式缓存系统的发展现状以及技术实现,如果让你造一个“Redis”出来,你会考虑哪些问题等。虽然在实际工作中不推荐重复“造轮子”,但在面试中要表现出自己具备“造轮子”的能力。
总结
CAP 理论看似简单,但在面试中,对它的理解深度可以从侧面反映出你对分布式系统的整体理解能力和驾驭能力。
所以你不但要掌握如何在面试中回答案例中 CAP 原理的问题,而且还要掌握回答问题的思路,以后遇到类似的理论性知识的考察,都可以从三个层面回答。
- 展示理论深度。你可以从一个熟知的知识点出发,深入浅出地回答,比如它的工作原理、优劣势、适用场景等。
- 结合落地经验。你不能仅停留在理论理解,还要结合落地方案的技术实现,这样才能体现你的技术闭环思维。
- 展示知识体系,这是任何一个程序员向上发展的基础能力。理论深度和落地经验体现了作为程序员的基本素质,而知识体系和技术判断力则体现了你是否达到架构师的能力边界。
04 亿级商品存储下,如何深度回答分布式系统的原理性问题?
上一讲,我结合 CAP 理论为你讲解了分布式系统的原理性问题,并通过理论引出了在做分布式系统设计时需要考虑和解决的复杂点,然后为你梳理了在分布式系统中,你需要掌握的技术知识体系,以及该具备的架构师思维。
这一讲,我将以“亿级商品存储设计”为背景,深度考察你对分布式原理的掌握程度,其中会涉及海量数据的存储、分片、复制,以及一致性共识算法的答题思路。
案例背景
先来回顾一下,你在面试时,有没有被问到以下几个问题:
- 如何设计一个支持海量商品存储的高扩展性架构?
- 在做分库分表时,基于 Hash 取模和一致性 Hash 的数据分片是如何实现的?
- 在电商大促时期,如何对热点商品数据做存储策略 ?
- 强一致性和最终一致性的数据共识算法是如何实现的 ?
在分布式系统中,核心的考察点包括了分布式系统中数据的存储、分布、复制,以及相关协议和算法,上述问题都与此相关。而在实际面试中,面试官通常会提出一个业务场景,如“如何设计海量商品数据的存储?”然后在候选者回答问题的过程中,通过一环扣一环的提问,把各考察点串联在一起。
案例分析
在互联网业务场景下,为了解决单台存储设备的局限性,会把数据分布到多台存储节点上,以此实现数据的水平扩展。既然要把数据分布到多个节点,就会存在数据分片的问题。数据分片即按照一定的规则将数据路由到相应的存储节点中,从而降低单存储节点带来的读写压力。常见的实现方案有 Hash(哈希分片)与 Range(范围分片)。
明确了如何分片后,就需要对数据进行复制,数据复制会产生副本,而副本是分布式存储系统解决高可用的唯一手段,这也是我们熟知的主从模式,又叫 master-slave。在分布式存储系统中,通常会设置数据副本的主从节点,当主节点出现故障时,从节点可以替代主节点提供服务,从而保证业务正常运行。
那么如何让从节点替代主节点呢?这就涉及数据一致性的问题了(只有在主从节点数据一致的情况下,才能进行主从替换)。
关于数据一致性,通常要考虑一致性强弱(即强一致性和最终一致性的问题)。而要解决一致性的问题,则要进行一系列的一致性协议:如两阶段提交协议(Two-Phrase Commit,2PC)、Paxos 协议选举、Raft 协议、Gossip 协议。
所以分布式数据存储的问题可以分成:数据分片、数据复制,以及数据一致性带来的相关问题。接下来,我会针对这些问题,提炼出面试中最为核心和高频的考察点。
案例解答
就如我开篇提到的,面试官往往会把“案例背景中”的四个问题串联到具体的场景中,以具体的场景设问,比如“假设你是一家电商网站的架构师,现在要将原有单点上百 G 的商品做数据重构,存储到多个节点上,你会如何设计存储策略 ?”
因为是商品存储扩容的设计问题,很容易想到做数据的分库分表,也就是重新设计数据的分片规则,常用的分片策略有两种,即 Hash(哈希)分片和 Range(范围)分片。从这一点出发会考察你Hash(哈希)分片的具体实现原理。
商品表包括主键、商品 ID、商品名称、所属品类和上架时间等字段。如果以商品 ID 作为关键字进行分片,系统会通过一个 Hash 函数计算商品 ID 的 Hash 值,然后取模,就能得到对应的分片。模为 4 就表示系统一共有四个节点,每个节点作为一个分片。
假设Hash 函数为 “商品 ID % 节点个数 4”,通过计算可以得到每个数据应该存入的节点:计算结果为 0 的数据存入节点 A;结果为 1 的数据存入节点 B;结果为 2 的数据存入节点 C;计算为 3 的数据存储节点 D。
商品数据 Hash 存储
可以看出,Hash 分片的优点在于可以保证数据非常均匀地分布到多个分片上,并且实现起来简单,但扩展性很差,因为分片的计算方式就是直接用节点取模,节点数量变动,就需要重新计算 Hash,就会导致大规模数据迁移的工作。
这时,就会延伸出第二个问题,如何解决 Hash 分片的缺点,既保证数据均匀分布,又保证扩展性?
答案就是一致性 Hash :它是指将存储节点和数据都映射到一个首尾相连的哈希环上。存储节点一般可以根据 IP 地址进行 Hash 计算,数据的存储位置是从数据映射在环上的位置开始,依照顺时针方向所找到的第一个存储节点。
在具体操作过程中,通常会选择带有虚拟节点的一致性 Hash。假设在这个案例中将虚拟节点的数量设定为 10 个,就形成 10 个分片,而这 10 个分片构成了整个 Hash 空间。现在让 A 节点对应虚拟节点 0 ~ 3,B 节点对应虚拟节点 4 ~ 6,C 节点对应虚拟节点 7 ~ 8,D 节点对应虚拟节点 9。
同样根据哈希函数为 “商品 ID % 节点个数 10”得到每一个商品在 Hash 环上的位置,然后根据顺时针查找最近的存储节点,即数据实际映射的位置。计算结果为:0 ~ 3 的数据存入节点 A;结果为 4 ~ 6 的数据存入节点 B;结果为 7 ~ 8 的数据存入节点 C;计算为 9 的数据存储节点 D。
商品一致性Hash存储
当我们新增一台服务器,即节点 E 时,受影响的数据仅仅是新服务器到所处环空间中前一台服务器(即沿着逆时针方向的第一台服务器)之间的数据。结合我们的示例,只有商品 100 和商品 101 从节点 A 被移动到节点 E,其他节点的数据保持不变。此后,节点 A 只存储 Hash 值为 2 和 3 的商品,节点 E 存储 Hash 值为 0 和 1 的商品。
商品数据迁移
一致性 Hash 分片的优点是数据可以较为均匀地分配到各节点,其并发写入性能表现也不错。如果你应聘的是初级研发工程师,面试官通常不会追问下去,但是应聘中高级别研发的话,这样的回答还不够,你还要进一步阐述对分布式数据存储的理解。
要知道,虽然一致性 Hash 提升了稳定性,使节点的加入和退出不会造成大规模的数据迁移,但本质上 Hash 分片是一种静态的分片方式,必须要提前设定分片的最大规模,而且无法避免单一热点问题, 某一数据被海量并发请求后,不论如何进行 Hash,数据也只能存在一个节点上,这势必会带来热点请求问题。比如案例中的电商商品,如果某些商品卖得非常火爆,通过 Hash 分片的方式很难针对热点商品做单独的架构设计。
所以,如果面试官想深入考核你对分布式数据存储的架构设计,一般会追问你:如何解决单一热点问题?
答案是做 Range(范围)分片。 与 Hash 分片不同的是,Range 分片能结合业务逻辑规则,例如,我们用 “Category(商品类目)” 作为关键字进行分片时,不是以统一的商品一级类目为标准,而是可以按照一、二、三级类目进行灵活分片。例如,对于京东强势的 3C 品类,可以按照 3C 的三级品类设置分片;对于弱势品类,可以先按照一级品类进行分片,这样会让分片间的数据更加平衡。
按业务品类分片
要达到这种灵活性,前提是要有能力控制数据流向哪个分区,一个简单的实现方式是:预先设定主键的生成规则,根据规则进行数据的分片路由,但这种方式会侵入商品各条线主数据的业务规则,更好的方式是基于分片元数据(不过架构设计没有好坏,只有适合与否,所以在面试场景中,我建议你用擅长的解决方案来回答问题)。
基于分片元数据的方式,就是调用端在操作数据的时候,先问一下分片元数据系统数据在哪,然后在根据得到的地址操作数据。元数据中存储的是数据分片信息,分片信息就是数据分布情况。在一个分布式存储系统中,承担数据调度功能的节点是分片元数据,当客户端收到请求后,会请求分片元数据服务,获取分片对应的实际节点地址,才能访问真正的数据。而请求分片元数据获取的信息也不仅仅只有数据分片信息,还包括数据量、读写 QPS 和分片副本的健康状态等。
这种方式的灵活性在于分片规则不固定,易扩展,但是高灵活性就会带来高复杂性,从存储的角度看,元数据也是数据,特殊之处在于它类似一个路由表,每一次请求都要访问它,所以分片元数据本身就要做到高可用。如果系统支持动态分片,那么分片信息的变更数据还要在节点之间进行同步,这又带来多副本之间的一致性问题,以此延伸出如何保证分片元数据服务的可用性和数据一致性?
最直接的方式是专门给元数据做一个服务集群,并通过一致性算法复制数据。在实现方式上,就是将元数据服务的高可用和数据一致性问题转嫁给外围协调组件,如 ETCD 集群,这样既保证了系统的可靠,数据同步的成本又比较低。知道了设计思路,那具体的架构实现上怎么做 ?
- 给分片元数据做集群服务,并通过 ETCD 存储数据分片信息。
- 每个数据存储实例节点定时向元数据服务集群同步心跳和分片信息。
- 当调用端的请求过来时,元数据服务节点只需要做好高可用和缓存即可。
元数据分片
掌握了这些知识后,你基本可以应对大多数公司对于研发工程师在数据架构设计上考点了,但如果面试官想挖掘你的能力,还会深入聊到共识算法,在一致性共识算法和最终一致性共识算法方面提出类似的问题,比如, ETCD 是如何解决数据共识问题的?为什么要选择这种数据复制方式呢?
对于这类问题,你要从一致性算法原理层面解答,思路是:清楚 ETCD 的共识算法是什么,还有哪些常用的共识算法,以及为什么 ETCD 会做这样的选型。
ETCD 的共识算法是基于 Raft 协议实现的强一致性算法,同类的强一致性算法还有 Paxos,在面试过程中,面试官很可能让你从自己的角度理解一下这两个算法,当然也会直接问:为什么没有选择 Paxos 而选择了 Raft ?这个问题对应聘高级研发的同学来讲很常见,主要考核你对以下内容的理解:
- Paxos 算法解决了什么问题?
- Basic Paxos 算法的工作流程是什么?
- Paxos 算法和 Raft 算法的区别又是什么?
在分布式系统中,造成系统不可用的场景很多,比如服务器硬件损坏、网络数据丢包等问题,解决这些问题的根本思路是多副本,副本是分布式系统解决高可用的唯一手段,也就是主从模式,那么如何在保证一致性的前提下,提高系统的可用性,Paxos 就被用来解决这样的问题,而 Paxos 又分为 Basic Paxos 和 Multi Paxos,然而因为它们的实现复杂,工业界很少直接采用 Paxos 算法,所以 ETCD 选择了 Raft 算法 (在面试过程中,面试官容易在这里设置障碍,来对候选者做技术分层)。
Raft 是 Multi Paxos 的一种实现,是通过一切以领导者为准的方式,实现一系列值的共识,然而不是所有节点都能当选 Leader 领导者,Raft 算法对于 Leader 领导者的选举是有限制的,只有最全的日志节点才可以当选。正因为 ETCD 选择了 Raft,为工业界提供了可靠的工程参考,就有更多的工程实现选择基于 Raft,如 TiDB 就是基于 Raft 算法的优化。
如果你应聘的部门非基础架构部,那么对于中高级别研发工程师来说,掌握以上问题的主线知识基本可以应对面试了(我没有过多涉及算法细节,因为每一个算法都可以单独花一讲,而我侧重讲解分析问题,答题的思维,你可以在课下夯实算法基础,并在留言区与我互动)。
如果把问题设计的极端一些,考察你对最终一致性算法的掌握,还可以有一种思路:分片元数据服务毕竟是一个中心化的设计思路,而且基于强一致性的共识机制还是可能存在性能的问题,有没有更好的架构思路呢?
既然要解决可用性的问题,根据 Base 理论,需要实现最终一致性,那么 Raft 算法就不适用了,因为 Raft 需要保证大多数节点正常运行后才能运行。这个时候,可以选择基于 Gossip 协议的实现方式。
Gossip 的协议原理有一种传播机制叫谣言传播,指的是当一个节点有了新数据后,这个节点就变成了活跃状态,并周期性地向其他节点发送新数据,直到所有的节点都存储了该条数据。这种方式达成的数据一致性是 “最终一致性”,即执行数据更新操作后,经过一定的时间,集群内各个节点所存储的数据最终会达成一致,很适合动态变化的分布式系统。
从图中你可以看到,节点 A 向节点 B、C 发送新数据,节点 B 收到新数据后,变成了活跃节点,然后节点 B 向节点 C、D 发送新数据。
到此,我们对一致性共识算法做个总结,共识算法的选择和数据副本数量的多少息息相关,如果副本少、参与共识的节点少,推荐采用广播方式,如 Paxos、Raft 等协议。如果副本多、参与共识的节点多,那就更适合采用 Gossip 这种最终一致性协议。
总结
总的来说,今天我通过电商场景下商品的存储设计,一步步延伸出了分布式系统的数据存储、分片,与数据一致性等分布式问题,它们包含了分布式系统知识体系中最基础的理论,也是最复杂的问题。今天这一讲,我强调这样几点:
- 面试官往往会通过“海量数据的存储设计”问题考察候选人对分布式系统技术的掌握情况,而回答好基于 Hash 取模、一致性 Hash 实现分库分表的解决方案,是你能否通过这第一关的关键。
- 当你掌握了常规的 Hash 取模分片方式后,面试官会引入一个场景问题(如大促热点问题)来考察你解决架构设计问题的思路。因为分布式系统架构设计离不开系统可用性与一致性之间的权衡,所以你的解题思路要站在这两个技术点之上。
- 如果面试官满意你的表现,会进一步考察你算法原理,所以对于分布式系统中的一致性共识算法,如 Basic Paxos、Multi Paxos、Raft、Zab、Gossip 也是你要提前掌握的。
05 海量并发场景下,如何回答分布式事务一致性问题?
上一讲我通过亿级商品存储,带你了解了分布式技术下的数据分片、存储、复制与一致性的原理性问题,这一讲我将继续带你了解一致性的另一个话题:事务一致性。
案例背景
在互联网分布式场景中,原本一个系统被拆分成多个子系统,要想完成一次写入操作,你需要同时协调多个系统,这就带来了分布式事务的问题(分布式事务是指:一次大的操作由多个小操作组成,这些小的操作分布在不同的服务器上,分布式事务需要保证这些小操作要么全部成功,要么全部失败)。那怎么设计才能实现系统之间的事务一致性呢? 这就是咱们今天要讨论的问题,也是面试的高频问题。
这一讲,我先从“解决分布式事务”这个问题本身出发,讲解答题思路和你要掌握的知识点。然后再结合“高并发”场景,看在该场景下如何保证分布式系统事务一致性?希望通过这种方式,让你彻底掌握分布式系统事务一致性的解题思路和技术认知。
以京东旅行系统为例,早期的交易系统是通过 .NET 实现的,所有的交易下单逻辑都写在一个独立的系统中。随着技术改造,我们用 Java 重写了核心系统,原本的系统也被拆分成多个子系统,如商品系统、促销系统、订单系统(为了方便理解,我只拿这三个系统举例)。当用户下单时,订单系统生成订单,商品系统扣减库存,促销系统扣减优惠券,只有当三个系统的事务都提交之后,才认为此次下单成功,否则失败。
案例分析
这是一个很典型的分布式事务问题,解决方案也很多,有两阶段提交协议(Two-Phase Commit,2PC)、3PC 、TCC 和基于消息队列的实现方式。
所以当很多候选者听到“怎么实现系统之间的分布式一致性?”的问题之后,会信心满满地选择一个方案,回答说:方案很多,可以选择 2PC ,2PC 实现的流程是……
这种答题思路犯了一个很明显的错误,因为在实际工作中,很少采用前几种方案,基本都是基于 MQ 的可靠消息投递的方式来实现。所以一上来就说 2PC、3PC 或者 TCC 会让我觉得你并没有实际做过。那答题的套路是什么呢?
我建议你先介绍目前主流实现分布式系统事务一致性的方案(也就是基于 MQ 的可靠消息投递的机制)然后回答出可实现方案和关键知识点。另外,为了和面试官进一步交流,你可以提出 2PC 或 TCC (这是一种交流方案)。因为 2PC 或 TCC 在工业界落地代价很大,不适合互联网场景,所以只有少部分的强一致性业务场景(如金融支付领域)会基于这两种方案实现。而你可以围绕它们的解决思路和方案弊端与面试官讨论,这会让你和面试官由不平等的“面试与被面试”变成平等且友好的“双方沟通”,是一种面试套路。
但要做到这几点,需要建立在你深入掌握分布式事务一致性问题的基础之上,所以接下来,我们就解析一下面试中最为常见的两种实现方案。
案例解答
基于两阶段提交的解决方案
2PC 是分布式事务教父级协议,它是数据库领域解决分布式事务最典型的协议。它的处理过程分为准备和提交两个阶段,每个阶段都由协调者(Coordinator)和参与者(Participant)共同完成:
- 协调者就是事务管理器;
- 参与者就是具体操作执行的资源管理器。
Java 程序员都知道,XA 是由 X/Open 组织提出的分布式事务的规范,规范主要定义了事务管理器(Transaction Manager)和资源管理器(Resource Manager)之间的接口,事务管理器负责全局事务的协调者,资源管理器负责管理实际资源(如 MySQL、Oracle 等数据库)。而Java 平台上事务规范 JTA(Java Transaction API)就是对 XA 分布式事务规范标准的实现。例如在 Spring 中就通过 JtaTransactionManager 来配置分布式事务,然后通过管理多个 ResourceManager 来管理多个数据源,进而操作多个数据库之间的事务。
那么 2PC 具体是如何运行的呢? 以课程开头的系统为例,订单数据、商品数据,以及促销数据被分别存储在多个数据库实例中,用户在执行下单的时候,交易主流程的业务逻辑则集中部署在一个应用服务器集群上,然后通过 Spring 容器访问底层的数据库实例,而容器中的 JTA 事务管理器在这里就作为事务管理器,Resource 资源管理器就作为底层的数据库实例的资源管理器。
Spring事务管理
我们假设订单数据,商品数据和促销数据分别保存在数据库 D1,数据库 D2 和数据库 D3 上。
- 准备阶段,事务管理器首先通知所有资源管理器开启事务,询问是否做好提交事务的准备。如资源管理器此时会将 undo 日志和 redo 日志计入事务日志中,并做出应答,当协调者接收到反馈 Yes 后,则准备阶段结束。
2PC 准备阶段
- 提交阶段,当收到所有数据库实例的 Yes 后,事务管理器会发出提交指令。每个数据库接受指令进行本地操作,正式提交更新数据,然后向协调者返回 Ack 消息,事务结束。
2PC 提交阶段
- 中断阶段,如果任何一个参与者向协调者反馈了 No 响应,例如用户 B 在数据库 D3 上面的余额在执行其他扣款操作,导致数据库 D3 的数据无法锁定,则只能向事务管理器返回失败。此时,协调者向所有参与者发出 Rollback 请求,参与者接收 Rollback 请求后,会利用其在准备阶段中记录的 undo 日志来进行回滚操作,并且在完成事务回滚之后向协调者发送 Ack 消息,完成事务回滚操作。
2PC 中断阶段
以上就是基于 2PC 实现分布式事务的原理。
当你和面试官交流 2PC 的原理时,往往不止于此,就像我们开篇提到的,我们并不会基于 2PC 来实现分布式事务一致性,虽然 2PC 可以借助数据库的本地事务操作,实现起来较为简单,不用侵入业务逻辑,但是它也存在着很多问题。
2PC 在准备阶段会要求每个资源管理器进行资源锁定,如 MySQL 的行锁。否则如果在提交阶段提交之前数据发生改变,就会出现数据不一致的情况。
还是上面的例子,如果商品库存数据为 1,也就是数据库 D1 为 1,在准备阶段询问是否可以扣减库存,商品数据返回可以,此时如果不锁定数据,在提交阶段之前另外一个请求去扣减了数据库 D1 的数据,这时候,在提交阶段再去扣减库存时,数据库 D1 的数据就会超售变成了负 1。
但正因为要加锁,会导致两阶段提交存在一系列问题,最严重的就是死锁问题,一旦发生故障,数据库就会阻塞,尤其在提交阶段,如果发生故障,数据都还处于资源锁定状态,将无法完成后续的事务提交操作。
其次是性能问题,数据库(如 MySQL )在执行过程中会对操作的数据行执行数据行锁,如果此时其他的事务刚好也要操作被锁定的数据行,那它们就只能阻塞等待,使分布式事务出现高延迟和性能低下。
再有就是数据不一致性,在提交阶段,当事务管理器向参与者发送提交事务请求之后,如果此时出现了网络异常,只有部分数据库接收到请求,那么会导致未接收到请求的数据库无法提交事务,整个系统出现数据不一致性。
至此,我们就了解了基于 2PC 实现的分布式事务一致性的解决方案,你可以从这几点出发,与面试官进行友好的交流。
基于 MQ 的可靠消息投递方案
基于 MQ 的可靠消息队列投递方案是目前互联网最为常用的方式,在应对高并发场景下的分布式事务问题时,种方案通过放弃强一致性,而选择最终一致性,来提高系统的可用性。
还是拿下单场景举例,当订单系统调用优惠券系统时,将扣减优惠券的事件放入消息队列中,最终给优惠券系统来执行,然后只要保证事件消息能够在优惠券系统内被执行就可以了,因为消息已经持久化在消息中间件中,即使消息中间件发生了宕机,我们将它重启后也不会出现消息丢失的问题。
基于 MQ 的消息投递
基于 MQ 的可靠消息投递的方案不仅可以解决由于业务流程的同步执行而造成的阻塞问题,还可以实现业务解耦合流量削峰。这种方案中的可选型的 MQ 也比较多,比如基于 RabbitMQ 或者 RocketMQ,但并不是引入了消息队列中间件就万事大吉了,通常情况下,面试官会着重通过以下两个知识点来考察你对这种方案的掌握程度。
- MQ 自动应答机制导致的消息丢失
订阅消息事件的优惠券服务在接收订单服务投递的消息后,消息中间件(如 RabbitMQ)默认是开启消息自动应答机制,当优惠券系统消费了消息,消息中间件就会删除这个持久化的消息。
但在优惠券系统执行的过程中,很可能因为执行异常导致流程中断,那这时候消息中间件中就没有这个数据了,进而会导致消息丢失。因此你要采取编程的方式手动发送应答,也就是当优惠券系统执行业务成功之后,消息中间件才能删除这条持久化消息。
这个知识点很容易被忽略掉,但却很重要,会让面试官认为你切切实实的做过,另外还有一个高频的问题,就是在大促的时候,瞬时流量剧增,很多没能及时消费的消息积压在 MQ 队列中,这个问题如何解决呢?
- 高并发场景下的消息积压导致消息丢失
分布式部署环境基于网络进行通信,而在网络通信的过程中,上下游可能因为各种原因而导致消息丢失。比如优惠券系统由于流量过大而触发限流,不能保证事件消息能够被及时地消费,这个消息就会被消息队列不断地重试,最后可能由于超过了最大重试次数而被丢弃到死信队列中。
但实际上,你需要人工干预处理移入死信队列的消息,于是在这种场景下,事件消息大概率会被丢弃。而这个问题源于订单系统作为事件的生产者进行消息投递后,无法感知它下游(即优惠券系统)的所有操作,那么优惠券系统作为事件的消费者,是消费成功还是消费失败,订单系统并不知道。
顺着这个思路,如果让订单知道消费执行结果的响应,即使出现了消息丢失的情况,订单系统也还是可以通过定时任务扫描的方式,将未完成的消息重新投递来进行消息补偿。这是基于消息队列实现分布式事务的关键,是一种双向消息确认的机制。
那么如何落地实现呢?你可以先让订单系统把要发送的消息持久化到本地数据库里,然后将这条消息记录的状态设置为代发送,紧接着订单系统再投递消息到消息队列,优惠券系统消费成功后,也会向消息队列发送一个通知消息。当订单系统接收到这条通知消息后,再把本地持久化的这条消息的状态设置为完成。
队列双向确认
这样做后,即使最终 MQ 出现了消息丢失,也可以通过定时任务从订单系统的本地数据库中扫描出一段时间内未完成的消息,进行重新投递,最终保证订单系统和优惠券系统的最终事务一致性。
总结
无论是初中级还是高级工程师,都需要掌握“分布式事务”,对初中级研发工程师来说,它是你的加分项,对于高级研发工程师来说,它是你的必备能力。所以这一讲中我并没有针对不同的人群给予不同的解答思路,主要想强调这样几个重点:
- 基于 MQ 的可靠消息投递的考核点是可落地性,所以你在回答时要抓住“双向确认”的核心原则,只要能实现生产端和消费端的双向确认,这个方案就是可落地了,又因为基于 MQ 来实现,所以天生具有业务解耦合流量削峰的优势。
- 基于 2PC 的实现方案很少有实际的场景,但你还是要掌握它的实现原理和存在的问题,因为面试不同于实际工作,有些问题的回答是为了告诉面试官:我有这个能力。尽管它在实际工作中并不适用。
最后,有一点需要你注意,在实际工作中,并不是所有的业务对事务一致性的要求都那么高。因为更高的要求意味着更多的成本,这也是很多架构复杂度来源之一,所以你要尽可能地站在业务实际场景的立足点来回答分布式事务问题。
06 分布式系统中,如何回答锁的实现原理?
上一讲,我讲了分布系统的事务一致性,今天这一讲,我想带你了解分布式系统中与锁有关的面试问题。
案例背景
分布式锁是解决协调分布式系统之间,同步访问共享资源的一种方式。详细来讲:在分布式环境下,多个系统在同时操作共享资源(如写数据)时,发起操作的系统通常会通过一种方式去协调其他系统,然后获取访问权限,得到访问权限后才可以写入数据,其他系统必须等待权限释放。
分布式锁
我和其他的面试官交流后发现,很多面试官都会问候选人与分布式锁相关的问题,在一些细节上挖得还比较细。比如在分布式系统中涉及共享资源的访问,一些面试官会深挖如何控制并发访问共享资源;如何解决资源争抢等技术细节,这些问题在下单场景、优惠券场景都会被考察到,足以证明“分布式锁”考点的重要性。
那么假设你正在面试,面试官模拟了系统秒杀的场景:为了防止商品库存超售,在并发场景下用到了分布式锁的机制,做商品扣减库存的串行化操作。然后问你:“你如何实现分布式锁?”你该怎么回答呢?
案例分析
当你听到这个问题后,心里会不会窃喜?觉得这是一道送分题,因为可选方案有很多,比如:
- 基于关系型数据库 MySQL 实现分布式锁;
- 基于分布式缓存 Redis 实现分布式锁;
你从中选择一个熟悉的实现方式,然后和面试官展开拉锯式的问答环节。
你:“可以基于 Redis 的 setnx 命令来实现分布式锁。” 面试官:“当拿到锁的服务挂掉,如何防止死锁?” 你:“可以为锁设置一个过期时间。” 面试官:“那如何保证加锁和设置过期时间是原子操作?” ……
如果面试官觉得你回答问题的思路清晰有条理,给出的实现方案也可以落地,并且满足你的业务场景,那么他会认可你具备初中级研发工程师该具备的设计能力,但不要高兴得太早。
因为有些面试官会继续追问:“分布式锁用 Zookeeper 实现行不行?”,“分布式锁用 etcd 实现行不行?” 借机考察你对分布式协调组件的掌握。你可能会觉得开源组件那么多,自己不可能每一个都用过,答不出来也无妨。但面试官提问的重点不是停留在组件的使用上,而是你对分布式锁的原理问题的掌握程度。
换句话说,“如果让借助第三方组件,你怎么设计分布式锁?” 这背后涉及了分布式锁的底层设计逻辑,是你需要掌握的。
02 讲我提到,在给出方案之前,你要明确待解决的问题点是什么。虽然你可以借助数据库 DB、Redis 和 ZooKeeper 等方式实现分布式锁,但要设计一个分布式锁,就需要明确分布式锁经常出现哪些问题,以及如何解决。
- 可用问题:无论何时都要保证锁服务的可用性(这是系统正常执行锁操作的基础)。
- 死锁问题:客户端一定可以获得锁,即使锁住某个资源的客户端在释放锁之前崩溃或者网络不可达(这是避免死锁的设计原则)。
- 脑裂问题:集群同步时产生的数据不一致,导致新的进程有可能拿到锁,但之前的进程以为自己还有锁,那么就出现两个进程拿到了同一个锁的问题。
总的来说,设计分布式锁服务,至少要解决上面最核心的几个问题,才能评估锁的优劣,从问题本质来回答面试中的提问,以不变应万变。接下来,我就以开篇的 “库存扣减” 为例,带你了解分布式锁的常见实现方式、优缺点,以及方案背后的原理。
案例解答
基于关系型数据库实现分布式锁
基于关系型数据库(如 MySQL) 来实现分布式锁是任何阶段的研发同学都需要掌握的,做法如下:先查询数据库是否存在记录,为了防止幻读取(幻读取:事务 A 按照一定条件进行数据读取,这期间事务 B 插入了相同搜索条件的新数据,事务 A 再次按照原先条件进行读取时,发现了事务 B 新插入的数据 )通过数据库行锁 select for update 锁住这行数据,然后将查询和插入的 SQL 在同一个事务中提交。
以订单表为例:
select id from order where order_id = xxx for update
基于关系型数据库实现分布式锁比较简单,不过你要注意,基于 MySQL 行锁的方式会出现交叉死锁,比如事务 1 和事务 2 分别取得了记录 1 和记录 2 的排它锁,然后事务 1 又要取得记录 2 的排它锁,事务 2 也要获取记录 1 的排它锁,那这两个事务就会因为相互锁等待,产生死锁。
数据库交叉死锁
当然,你可以通过“超时控制”解决交叉死锁的问题,但在高并发情况下,出现的大部分请求都会排队等待,所以“基于关系型数据库实现分布式锁”的方式在性能上存在缺陷,所以如果你回答“基于关系型数据库 MySQL 实现分布式锁”,通常会延伸出下面两个问题。
- 数据库的事务隔离级别
如果你想让系统支持海量并发,那数据库的并发处理能力就尤为重要,而影响数据库并发能力最重要的因素是数据库的事务隔离机制。
数据库的四种隔离级别从低到高分别是:
- 读未提交(READ UNCOMMITTED);
- 读已提交(READ COMMITTED);
- 可重复读(REPEATABLE READ);
- 可串行化(SERIALIZABLE)。
其中,可串行化操作就是按照事务的先后顺序,排队执行,然而一个事务操作可能要执行很久才能完成,这就没有并发效率可言了,所以数据库隔离级别越高,系统的并发性能就越差。
- 基于乐观锁的方式实现分布式锁
在数据库层面,select for update 是悲观锁,会一直阻塞直到事务提交,所以为了不产生锁等待而消耗资源,你可以基于乐观锁的方式来实现分布式锁,比如基于版本号的方式,首先在数据库增加一个 int 型字段 ver,然后在 SELECT 同时获取 ver 值,最后在 UPDATE 的时候检查 ver 值是否为与第 2 步或得到的版本值相同。
## SELECT 同时获取 ver 值
select amount, old_ver from order where order_id = xxx## UPDATE 的时候检查 ver 值是否与第 2 步获取到的值相同
update order set ver = old_ver + 1, amount = yyy where order_id = xxx and ver = old_ver
此时,如果更新结果的记录数为1,就表示成功,如果更新结果的记录数为 0,就表示已经被其他应用更新过了,需要做异常处理。
你可以看到,初中级的研发工程师除了要掌握如何基于关系型数据库实现分布式锁,还要提前掌握数据库隔离级别的相关知识点,以及锁机制,它们是 MySQL 知识体系的一环,我会在第 10 讲中帮你梳理“如何回答 MySQL 的事务隔离级别和锁的机制?”相关问题。
基于分布式缓存实现分布式锁
我在开篇提到,因为数据库的性能限制了业务的并发量,所以针对“ 618 和双 11 大促”等请求量剧增的场景,你要引入基于缓存的分布式锁,这个方案可以避免大量请求直接访问数据库,提高系统的响应能力。
基于缓存实现的分布式锁,就是将数据仅存放在系统的内存中,不写入磁盘,从而减少 I/O 读写。接下来,我以 Redis 为例讲解如何实现分布式锁。
在加锁的过程中,实际上就是在给 Key 键设置一个值,为避免死锁,还要给 Key 键设置一个过期时间。
SET lock_key unique_value NX PX 10000
- lock_key 就是 key 键;
- unique_value 是客户端生成的唯一的标识;
- NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作;
- PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁。
而解锁的过程就是将 lock_key 键删除,但不能乱删,要保证执行操作的客户端就是加锁的客户端。而这个时候, unique_value 的作用就体现出来,实现方式可以通过 lua 脚本判断 unique_value 是否为加锁客户端。
选用 Lua 脚本是为了保证解锁操作的原子性。因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。
// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] thenreturn redis.call("del",KEYS[1])
elsereturn 0
end
以上,就是基于 Redis 的 SET 命令和 Lua 脚本在 Redis 单节点上完成了分布式锁的加锁、解锁,不过在实际面试中,你不能仅停留在操作上,因为这并不能满足应对面试需要掌握的知识深度, 所以你还要清楚基于 Redis 实现分布式锁的优缺点;Redis 的超时时间设置问题;站在架构设计层面上 Redis 怎么解决集群情况下分布式锁的可靠性问题。
需要注意的是,你不用一股脑全部将其说出来,而是要做好准备,以便跟上面试官的思路,同频沟通。
- 基于 Redis 实现分布式锁的优缺点
基于数据库实现分布式锁的方案来说,基于缓存实现的分布式锁主要的优点主要有三点。
- 性能高效(这是选择缓存实现分布式锁最核心的出发点)。
- 实现方便。很多研发工程师选择使用 Redis 来实现分布式锁,很大成分上是因为 Redis 提供了 setnx 方法,实现分布式锁很方便。但是需要注意的是,在 Redis2.6.12 的之前的版本中,由于加锁命令和设置锁过期时间命令是两个操作(不是原子性的),当出现某个线程操作完成 setnx 之后,还没有来得及设置过期时间,线程就挂掉了,就会导致当前线程设置 key 一直存在,后续的线程无法获取锁,最终造成死锁的问题,所以要选型 Redis 2.6.12 后的版本或通过 Lua 脚本执行加锁和设置超时时间(Redis 允许将 Lua 脚本传到 Redis 服务器中执行, 脚本中可以调用多条 Redis 命令,并且 Redis 保证脚本的原子性)。
- 避免单点故障(因为 Redis 是跨集群部署的,自然就避免了单点故障)。
当然,基于 Redis 实现分布式锁也存在缺点,主要是不合理设置超时时间,以及 Redis 集群的数据同步机制,都会导致分布式锁的不可靠性。
- 如何合理设置超时时间
通过超时时间来控制锁的失效时间,不太靠谱,比如在有些场景中,一个线程 A 获取到了锁之后,由于业务代码执行时间可能比较长,导致超过了锁的超时时间,自动失效,后续线程 B 又意外的持有了锁,当线程 A 再次恢复后,通过 del 命令释放锁,就错误的将线程 B 中同样 key 的锁误删除了。
锁超时导致的误操作
所以,如果锁的超时时间设置过长,会影响性能,如果设置的超时时间过短,有可能业务阻塞没有处理完成,能否合理设置超时时间,是基于缓存实现分布式锁很难解决的一个问题。
那么如何合理设置超时时间呢? 你可以基于续约的方式设置超时时间:先给锁设置一个超时时间,然后启动一个守护线程,让守护线程在一段时间后,重新设置这个锁的超时时间。实现方式就是:写一个守护线程,然后去判断锁的情况,当锁快失效的时候,再次进行续约加锁,当主线程执行完成后,销毁续约锁即可。
不过这种方式实现起来相对复杂,我建议你结合业务场景进行回答,所以针对超时时间的设置,要站在实际的业务场景中进行衡量。
- Redis 如何解决集群情况下分布式锁的可靠性?
我在 03 讲中提到了,在回答基于 Redis 实现分布式锁时候,你需要具备的答题思路和扩展点。其中也提到了基于 Redis 集群节点实现分布式锁会存在高可用的问题。
由于 Redis 集群数据同步到各个节点时是异步的,如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。
但 03 讲我没有说怎么解决这个问题,其实 Redis 官方已经设计了一个分布式锁算法 Redlock 解决了这个问题。
而如果你能基于 Redlock 原理回答出怎么解决 Redis 集群节点实现分布式锁的问题,会成为面试的加分项。那官方是怎么解决的呢?
为了避免 Redis 实例故障导致锁无法工作的问题,Redis 的开发者 Antirez 设计了分布式锁算法 Redlock。Redlock 算法的基本思路,是让客户端和多个独立的 Redis 实例依次请求申请加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。
这样一来,即使有某个 Redis 实例发生故障,因为锁的数据在其他实例上也有保存,所以客户端仍然可以正常地进行锁操作,锁的数据也不会丢失。那 Redlock 算法是如何做到的呢?
我们假设目前有 N 个独立的 Redis 实例, 客户端先按顺序依次向 N 个 Redis 实例执行加锁操作。这里的加锁操作和在单实例上执行的加锁操作一样,但是需要注意的是,Redlock 算法设置了加锁的超时时间,为了避免因为某个 Redis 实例发生故障而一直等待的情况。
当客户端完成了和所有 Redis 实例的加锁操作之后,如果有超过半数的 Redis 实例成功的获取到了锁,并且总耗时没有超过锁的有效时间,那么就是加锁成功。
总结
在课程的最后,我们总结一下,分布式锁是解决多个进程同时访问临界资源的常用方法,在分布式系统中非常普遍,常见的实现方式是基于数据库,基于 Redis。在同等服务器配置下,Redis 的性能是最好的,数据库最差。
但是在面试时,你要分清楚面试官的考查点,并结合工作中的业务场景给出答案,面试官不侧重你是否能很快地给出结果,而是你思考的过程。
对于分布式锁,你要从“解决可用性、死锁、脑裂”等问题为出发点来展开回答各分布式锁的实现方案的优缺点和适用场景。 另外,在设计分布式锁的时候,为了解决可用性、死锁、脑裂等问题,一般还会再考虑一下锁的四种设计原则。
- 互斥性:即在分布式系统环境下,对于某一共享资源,需要保证在同一时间只能一个线程或进程对该资源进行操作。
- 高可用:也就是可靠性,锁服务不能有单点风险,要保证分布式锁系统是集群的,并且某一台机器锁不能提供服务了,其他机器仍然可以提供锁服务。
- 锁释放:具备锁失效机制,防止死锁。即使出现进程在持有锁的期间崩溃或者解锁失败的情况,也能被动解锁,保证后续其他进程可以获得锁。
- 可重入:一个节点获取了锁之后,还可以再次获取整个锁资源。