ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

分布式开发核心考点全解析:事务、锁与缓存一致性

分布式开发核心考点全解析:事务、锁与缓存一致性 前几天整理移动硬盘里的旧资料翻到一份顺丰科技2019秋招分布式开发工程师的客观题合集顺手又做了一遍。说实话距离这份题集出现已经过去好几年但里面的知识点放在今天依然非常能打——分布式事务、分布式锁、分布式缓存、分布式存储、全局ID、任务调度几乎覆盖了一个分布式开发工程师日常会碰到的绝大多数核心问题。我当时刷这份题集时最大的感受是客观题看似只是选ABCD实际上每一道题背后都藏着一整套知识体系。只会背结论的话换个问法就懵要是把背后的原理吃透了客观题反而是最省力的提分项。这篇文章就顺着这份题集的知识结构展开结合我这些年做分布式系统开发的实际经验把高频考点逐个拆开讲清楚。无论你是准备秋招的应届生还是想系统梳理分布式知识体系的在职开发应该都能从中拿到一些可复用的东西。1. 客观题集背后的考点全景先看清这份题集在考什么1.1 从岗位职责反推知识边界刷题之前我习惯先看岗位JD。分布式开发工程师这个岗位名字里带分布式三个字实际工作内容往往横跨多个子系统要拆服务、要定接口、要处理数据一致性问题、要保证系统在高并发下的可用性还得跟缓存、消息队列、定时任务打交道。换句话说这个岗位的知识边界不是某个框架怎么用而是多个节点协同工作时怎么保证数据正确、系统可用、性能达标。顺丰这套客观题正是按照这个边界来设计的这也是为什么题目里既有CAP、BASE这类基础理论又有Redis分布式锁、Seata事务原理这类具体实践。有个很典型的细节这份题集里几乎每道题都不是孤立考一个点而是把多个知识点串在一起。比如考分布式锁的时候同时考察了Redis命令的原子性、过期时间设置、锁续期、以及主从切换下的安全性。这说明出题人想试探的不是记忆而是你在真实业务场景里的判断力。1.2 客观题的三类考法概念、原理、场景我把这类客观题归纳成三个层面分别对应不同的考察目标。第一类是概念辨析题比如CAP三者如何取舍、BASE理论的具体含义、幂等性的定义。这类题目考察的是基础术语的准确理解。很多人挂在概念题上不是因为不知道术语而是被看起来差不多的选项干扰。比如最终一致性和弱一致性经常被混在一起出选项考的就是你能不能分辨它们之间的细微差异。第二类是原理机制题比如2PC的准备阶段发生了什么、Seata AT模式的一阶段二阶段各做了什么、Redis分布式锁为什么不能用SETNX加EXPIRE两条命令。这类题目需要你真正理解机制的执行流程而不只是记住结论。第三类是场景应用题通常会给你一个业务场景比如订单服务和库存服务分属两个数据库如何保证下单扣库存不超卖然后让你选择最合适的方案。这类题目最接近真实工作也最考验知识迁移能力。顺丰的物流系统天然就是分布式场景订单、运单、路由、仓储、结算各自独立部署它们之间的数据交互和一致性保障正是客观题里场景题的原型。1.3 为什么客观题不客观既然叫客观题按理说应该有唯一正确答案。但实际刷下来你会发现很多题在真实工程里并没有绝对的对错只有权衡。出题人会用最合适最优以下哪种说法不正确这类措辞来限定范围目的就是看你能不能识别出特定上下文下的最佳实践。打个比方分布式事务的解决方案有十几种放在不同业务场景下各有优劣。出题人会故意把强一致高并发跨异构数据库这些条件塞进题干如果你不关注这些限定条件很容易选出一个看似合理但不符合语境的标准答案。所以刷这套题的正确姿势不是背题库而是把每道题的题干当成一个需求文档先弄清楚它限定了什么场景再判断哪个方案在这个场景下最合适。这个思维方式本身就是分布式开发最核心的能力之一。2. 分布式事务客观题里最常翻车的深水区2.1 单机事务的经验为什么会失效先问一个问题为什么分布式事务这么难答案写在ACID里。单机数据库里事务靠着本地锁、redo log、undo log可以严格保证原子性、一致性、隔离性、持久性。但一旦数据分散在多个数据库或微服务里就没有一个全局的锁管理器能够协调所有参与者了。你没法用一个数据库的undo log去回滚另一个数据库已经提交的数据也没法用一条数据库连接去管理跨库的锁。这就导致了一个很反直觉的现象在单机事务里commit就是commit一旦返回成功数据就一定落库了。但在分布式环境下提交成功这个结果本身变得不可靠——A服务提交成功了B服务可能在提交前宕机了你无法保证整个调用链路的原子性。理解了这一点你就能看懂为什么2PC、TCC、Saga这些方案会存在。它们本质上都是在不同约束条件下用不同方式去逼近单机事务的效果。2.2 主流方案的取舍2PC、TCC、Saga、最大努力通知客观题里最常出现的几类分布式事务方案我用一个对比表来梳理它们的核心差异方案一致性强度业务侵入性适用场景典型代价2PC/3PC强一致低数据库层面单库扩展、跨库同构同步阻塞、协调者单点、性能差TCC最终一致高需实现Try/Confirm/Cancel跨服务、高并发业务代码复杂需处理幂等Saga最终一致中编排或 choreography长事务、流程可拆无隔离性需人工补偿最大努力通知最终一致低跨系统、对实时性要求低不保证时序需对账2PC是最经典的方案也是很多客观题的考点。它的核心流程分两步第一阶段协调者问所有参与者能不能提交参与者各自执行事务但不提交把结果告诉协调者第二阶段协调者根据所有参与者的反馈统一发出commit或rollback指令。这里有个必须记住的细节2PC在第一阶段会锁定资源直到第二阶段结束才释放。如果协调者在第二阶段宕机了所有参与者都会一直持有锁整个系统就卡死了。这就是2PC最大的痛点也是为什么它很少被直接用在跨微服务的互联网业务里。TCC把事务的每个操作拆成三个动作Try阶段做资源预留Confirm阶段做真正的提交Cancel阶段做补偿。它的优势是业务控制力强但代价是每个业务方法都要写三套逻辑。举个例子扣库存的TCC就是Try阶段先冻结库存Confirm阶段扣减冻结库存Cancel阶段解冻。这个方案在订单、支付这类跨服务场景里非常常见。Saga则把长事务拆成一系列本地事务每个本地事务都有对应的补偿事务。如果中间的某个本地事务失败就逆序执行之前的补偿事务来回滚。它更贴近业务流程本身适合那种流程很长、中间环节可以拆分的场景。最大努力通知是这几类方案里最轻的。它不追求同步的强一致而是通过重试和定时任务把结果尽可能可靠地通知给对方实在通知不到就靠对账系统兜底。订单支付成功后异步通知商家系统就是典型的最大努力通知。2.3 Seata AT模式怎么做到对业务无侵入顺丰这份题集出现的时间节点正好是Seata开始普及的时候所以里面有一批围绕Seata的题目。Seata AT模式在客观题里的出现频率很高核心考点是它的两阶段机制。AT模式的一阶段业务SQL正常执行并提交关键是在提交前Seata会生成该SQL执行前后的数据快照存到undo_log表里。同时注册分支事务拿到全局锁。二阶段分两种情况如果全局事务顺利就异步删除各分支的undo_log如果某个分支失败协调者通知所有分支回滚各分支通过undo_log里的前镜像数据反向生成补偿SQL把数据恢复原样。AT模式最聪明的地方在于对业务代码几乎零侵入。你只需要在业务方法上加一个GlobalTransactional注解Seata框架就帮你把分支事务注册、全局锁、回滚补偿全做了。这种低侵入的特质让它比手工写TCC更受互联网公司青睐。不过AT模式有一个容易被忽略的代价全局锁的竞争。在高并发场景下如果多个全局事务操作同一行数据全局锁会成为瓶颈。所以客观题喜欢考AT模式在高并发下的性能瓶颈是什么这类问题答案就是全局锁竞争和undo_log的额外存储开销。2.4 订单与库存场景一道典型题的推演订单服务和库存服务数据分库如何保证下单不超卖是这类面试出镜率最高的场景题顺丰的题集里也有类似变体。我把这类题的思路推演一遍。第一步先判断一致性要求。下单场景对一致性要求很高不能出现用户下单成功但库存扣减失败的情况也不能出现超卖。此时不可能用单纯的异步消息因为异步消息无法保证强一致。第二步排除掉不合适的方案。2PC虽然能保证强一致但同步阻塞和协调者单点问题在电商高频场景下不可接受。最大努力通知也不行因为它无法保证下单和扣库存在同一时间窗内完成。第三步在TCC和Seata AT之间选择。对于跨服务、且要求较高并发吞吐的场景TCC的Try冻结库存设计可以精确控制资源占用是很多交易系统的首选。而如果团队不想在每个服务里维护三套接口Seata AT也能接受只是要做好全局锁竞争的心理准备。第四步别忘了兜底方案。无论选哪种事务方案最终都要配合消息队列和定时对账来保证最终一致。对账脚本逐笔比对订单和库存流水发现不一致就自动补偿。这个思路在面试里说出来会明显加分——因为说明你有线上运维和故障兜底的意识而不仅仅是停留在理论层面。3. 分布式锁与缓存一致性高频考点背后的坑3.1 Redis分布式锁的演进从SETNX到RedLock分布式锁是客观题里的固定嘉宾因为它是分布式架构里最基础也最容易踩坑的组件。面试官喜欢问Redis分布式锁核心原因是用Redis实现锁足够简单但简单里藏着无数细节。最早的写法是SETNX加EXPIRE两条命令。SETNX成功拿到锁然后用EXPIRE设置过期时间。这个方案有一个致命问题如果SETNX之后、EXPIRE之前客户端挂了锁就会永远不释放其他线程全部卡死。后来演进成一条命令搞定SET key value NX PX 30000。这条命令同时实现了不存在才设置和设置过期时间两个语义保证了原子性。客观题如果考Redis分布式锁的正确实现标准答案基本就是这条命令。但这条命令也不是银弹。锁的value必须是一个唯一标识比如UUID释放锁时要先比对value是自己的才能DEL。这里又有一个经典坑比对和删除必须是原子操作否则在比对通过但还没来得及DEL的时候锁过期了其他线程拿到锁你DEL掉的就是别人的锁。正确做法是用Lua脚本来完成判断删除。再往深一层就是RedLock。RedLock的思想是多实例加锁在N个独立的Redis节点上分别尝试加锁只要超过N/2个节点加锁成功就认为获得了锁。这个方案本身存在争议著名的分布式系统专家Martin Kleppmann和Redis作者antirez就红过一轮。争议的焦点在于Redis节点如果发生时钟跳跃或GC停顿锁的安全性还是会被破坏。不过客观题一般只考到RedLock的基本思路和适用场景知道它是为了降低单点故障风险就够了。3.2 缓存一致性先更新DB还是先删缓存缓存一致性是另一个高频考点而且这个问题在真实系统里也天天遇到。答案的标准范式叫Cache Aside读的时候先读缓存没命中就读数据库再回填缓存写的时候先更新数据库再删除缓存。为什么是删缓存而不是更新缓存因为更新缓存的前提是你能拿到完整的缓存数据而在很多场景下一次数据库更新只涉及一个字段更新缓存必须查一次全量数据再写进去白白浪费一次查询。删缓存则简单粗暴下次读的时候自然会重新加载。但先更新DB再删缓存也有问题。考虑一个并发场景线程A读缓存未命中去数据库读了旧值线程B更新了数据库线程B删除缓存线程A把旧值写回缓存。结果缓存里存的是旧数据DB里是新数据不一致了。要解决这个问题最常用的做法是设置缓存过期时间作为兜底让不一致状态最多维持到过期时间点。在实际工程里还有一个进阶方案消息队列加监听binlog。更新DB后发送一条消息到MQ消费端收到消息后删除对应的缓存。这比直接在业务代码里删缓存更可靠——即使业务代码里忘记删了binlog监听也会兜底删掉。这个方案通常用在并发量较高、缓存无法容忍长时间不一致的系统中。3.3 穿透、击穿、雪崩分布式缓存三大经典问题这三个问题几乎是必考内容而且经常被混在一起考每次都不缺人栽在这上面。缓存穿透指的是查询一个数据库里根本不存在的数据。因为数据不存在缓存里也没有请求每次都打到数据库量大的时候数据库直接被打崩。解决办法主要有三种对空结果也做缓存设置较短过期时间、用布隆过滤器拦截不存在的key、或者对查询参数做合法性校验。布隆过滤器是最优雅的方案用一个bit数组就能以极小的内存代价挡住大部分非法请求缺点是存在误判率且不支持删除元素除非用计数布隆过滤器。缓存击穿指的是某个热点key在过期的一瞬间大量并发请求同时打到数据库。这个问题的关键在热点key 过期瞬间两个条件同时成立。解决办法是热点数据不设置过期时间或者用互斥锁保证只有一个线程去数据库查询并回填。缓存雪崩指的是大量key在同一时间段集体过期导致请求全部落到数据库。最常见的诱因是缓存key设置了相同的过期时间比如晚上十二点整统一失效。解决办法包括过期时间加随机偏移量、多级缓存本地缓存Caffeine/Redis、缓存高可用方案Redis主从哨兵或者Cluster集群。顺丰这类物流系统的运单查询和轨迹查询就非常依赖缓存如果缓存雪崩用户端会立刻感受到查询超时所以他们在题集里考这个点非常合理。4. 分布式存储、全局ID与任务调度决定区分度的基础题4.1 分布式存储的分片与复制数据怎么放才安全分布式存储的考题一般集中在两个维度分片sharding和复制replication。分片解决的是单机容量和单机性能的上限问题复制解决的是数据安全和可用性的问题。先看分片。最常见的分片策略有两种范围分片和哈希分片。范围分片按某个字段的区间来分比如按用户ID的尾号分表优点是查询范围型数据方便缺点是有热点问题——尾号是1的表可能数据量特别大。哈希分片对key做哈希取模数据分布均匀但一旦扩容大量的key需要重新分布迁移成本非常高。这就引出了一致性哈希。一致性哈希把哈希值空间组织成一个圆环数据落到环上后顺时针找到第一个节点存放。这样增加或删除节点时只有环上部分数据需要迁移而不是全量迁移。一致性哈希的经典缺陷是数据倾斜解决办法是引入虚拟节点。客观题里只要考到为什么一致性哈希要引入虚拟节点答案就是让数据分布更均匀缓解节点负载不均。再看复制。主从复制是最基础的形态主库负责写从库负责读通过binlog同步到从库。但主从复制有延迟极端情况下会有数据丢失于是有了半同步复制和强同步复制。分布式存储中还有个关键概念叫Quorum法定数量在N个副本中写操作至少要成功W个副本读操作至少要读R个副本只要WR N就能保证读到的数据一定包含最新版本。这个WRN的公式是很多客观题的计算题考点。4.2 分布式ID的工程选型UUID为什么不够用很多人在前期准备时觉得分布式ID不重要结果一考就暴露。分布式ID要满足几个特性全局唯一、趋势递增、高性能高可用。客观题喜欢把不同方案摆在一起让你选。UUID是最容易想到的方案。它本地生成不需要网络交互性能极高全局唯一性也能保证。但它的致命问题是无序且太长作为数据库主键会导致B树频繁页分裂写入性能断崖式下跌。所以UUID几乎不会用在核心业务的数据库主键上但可以用在日志ID、消息ID这类不关心顺序的场景。数据库自增ID的变体是批量取号从数据库取一批ID比如每次取1000个缓存在本地内存里用完了再取。这个方案简单可靠但存在ID空洞和号段耗尽时的高峰期锁竞争问题。目前最主流的方案是雪花算法Snowflake。它生成的64位ID由四部分组成1位符号位 41位时间戳 10位机器ID 12位序列号单机每秒可以生成约409.6万个ID。雪花算法的考点聚焦在两个地方一是机器ID的分配方式二是时钟回拨问题。如果服务器时钟回拨就可能生成重复ID解决办法一般是记录上一次生成ID的时间戳发现回拨就等待时钟追上或者直接报错。美团开源的Leaf、百度开源的UidGenerator都是基于雪花算法或号段模式的具体实现面试时能说出这些开源方案的原理和适用场景会显得功力深厚。顺丰这股题集里还考过分布式ID方案如何做高可用这实际上是在问如果号段服务或雪花ID生成服务所在节点挂了你怎么保证ID还能继续生成答案主要是部署多个ID生成节点客户端做故障转移让ID生成链路变成多活模式。4.3 分布式任务调度从单机Quartz到分布式调度平台任务调度也是分布式开发的日常工作。最早大家用QuartzQuartz本身是单机调度框架虽然有集群模式但依赖数据库锁来控制多节点互斥执行。它的问题是调度逻辑和业务代码耦合在一块扩展性差而且当任务多了以后数据库锁竞争会成为瓶颈。针对这些问题行业里出现了两款代表性框架ElasticJob和XXL-Job。ElasticJob基于ZooKeeper做分布式协调任务可以被分片多个节点各自执行自己的分片适合数据量大的批处理任务。它引入了一个分片概念你可以把100万条数据分成10片10台机器各跑一片极大提升批处理效率。XXL-Job则是中心化调度架构调度中心统一管理任务执行器以jar包方式部署在业务系统里调度中心通过HTTP回调触发执行器。它的优点是部署运维简单可视化控制台做得完善支持动态修改任务配置非常适合中小团队。Spring Cloud Alibaba生态里也有对应的分布式任务调度解决方案比如基于Redis或ZooKeeper的分布式锁来保证任务单机执行或者是接入更完备的调度平台。客观题在这一块喜欢出定时任务如何避免重复执行的问题。答案不外乎几种分布式锁保证同一时刻只有一个节点执行通过任务幂等设计让重复执行不产生副作用或者采用分片方式让每个节点只处理自己的数据从源头避免冲突。4.4 Hadoop伪分布式为什么总被拿来出题很多人看到Hadoop伪分布式这个词会觉得偏大数据方向不太像分布式开发考题。但实际上伪分布式是理解分布式系统运行机制的最佳入门抓手。所谓伪分布式就是在一台机器上同时启动NameNode、DataNode、ResourceManager、NodeManager等所有进程模拟出一个完整的分布式运行环境。它用一套真实的HDFS和YARN框架把数据块复制心跳机制主备切换任务分配这些抽象的分布式概念全部实例化了。想理解分布式系统的心跳机制就可以在Hadoop里看到DataNode周期性地给NameNode发送心跳超过阈值没有收到心跳NameNode就把这个节点标记为dead并把数据块复制到其他节点。这个机制跟你在实际分布式架构中任一服务健康检查的实现原理一模一样。学习分布式强烈建议拿Hadoop伪分布式当第一个实验环境。不花钱买机器不搭机房一台普通电脑就能跑起来。你可以在里面做各种破坏性实验比如手动kill掉一台DataNode观察数据块怎么恢复。这些亲身体验比刷一百道客观题更能帮你建立分布式系统的直觉。5. 从刷题到面谈我的复盘与建议5.1 客观题的正确打开方式先说一个我见过太多人犯的错误把客观题当期末考试来背。拿着题库背选项背选B的原因是一二三结果一到面试环节面试官换个场景问如果Redis集群发生了主从切换刚才那把锁还能不能保证安全当场就卡住了。客观题正确的刷法是每做一道题就追问三个问题这道题在问什么场景下的什么决策这个决策背后依赖什么机制如果机制的一个前提条件不成立决策还会不会变比如这道题Redis分布式锁的过期时间应该怎么设置标准答案是设置一个合理的业务执行时间上限比如3秒。但你要继续追问如果业务执行超过3秒怎么办答案是需要锁续期看门狗机制。再追问主从切换会有什么问题答案是主节点复制延迟可能导致新主节点上没锁并发安全性下降。一层层追问下去一道题就能串起Redis主从复制、分布式锁缺陷、CAS乐观锁、ZooKeeper临时节点等多个知识点。5.2 一套可落地的学习路径如果你希望系统地准备分布式开发岗位我建议按下面这条路径走顺序很重要。第一步先把基础理论夯实。CAP理论、BASE理论、一致性模型强一致、弱一致、最终一致这些是分析一切分布式问题的底层语言。能用自己的话讲清楚CAP三者关系是最基本的要求。第二步动手做实验。搭建Hadoop伪分布式环境感受一下NameNode和DataNode的交互机制。搭一个Spring Cloud微服务项目把Eureka注册中心、OpenFeign远程调用、Spring Cloud Gateway网关都串起来。这个阶段的核心不是敲代码而是观察节点之间是怎么通信、怎么协调的。第三步深入一个核心组件。我建议先从Redis入手因为它既是缓存、又是锁、还能做消息队列麻雀虽小五脏俱全。把Redis的主从复制、哨兵模式、Cluster集群逐一实验一遍再配合Redisson看分布式锁的源码实现你会发现之前很多悬空的直觉一下就落地了。第四步理解分布式事务。从Seata官方文档入手分别把AT模式和TCC模式跑通再看源码里的全局事务管理器和分支事务注册流程。这一步最耗时但也是面试时最能拉开差距的部分。第五步回归项目实践。找一个足够复杂的业务场景比如秒杀系统来综合运用锁、缓存、事务、消息队列。真正去解决超卖、库存一致性、热点key击穿这些问题比什么都管用。这套路径走下来你再去回看顺丰那份客观题会发现大部分题都不再是题目了而是你曾经踩过的坑、排查过的线上问题。5.3 最后分享几个我踩过的坑第一个坑是纸上谈兵。早年我看了一堆分布式理论觉得自己什么都懂了结果第一次写分布式锁就出了事故忘记了锁的value必须是唯一标识结果一个线程把另一个线程的锁删了并发直接乱套。技术这东西尤其是分布式这种工程属性极强的领域光看不练等于没学。第二个坑是忽略监控。分布式系统比单机系统复杂得多一台机器上的问题很难排查比一个节点宕机了对整个链路的影响。我吃过一次大亏某次大促前一个定时任务因为数据库锁竞争超时大量业务被阻塞而监控系统没有针对任务执行时长的告警等到用户反馈才发现。从那以后我给所有关键分布式组件都加了监控大盘和告警规则。第三个坑是过度设计。分布式事务、分库分表不是银弹。小规模的业务用单库加事务就能解决没必要为了架构炫技强行引入Seata和消息队列。方案越复杂带来的运维成本越高出问题的概率也越大。务实的做法是从最简单的方案起步只有当业务量真正到了瓶颈再逐步演进架构。顺丰那份题集里有一道题大意是在分布式系统中可用性和一致性的矛盾如何取舍没有唯一答案但它在提醒每一个做分布式开发的人好的架构不是追求最完美的方案而是在约束条件下做最合适的选择这恰恰是这份题目真正想让你学会的东西。
返回列表