分布式存储面试
分布式存储面试
缓存
扩展
【简单】什么是缓存?为什么需要缓存?
缓存就是数据交换的缓冲区,用于将频繁访问的数据暂存在访问速度快的存储介质。
缓存的本质是一种利用空间换时间的设计:牺牲一定的数据实时性,使得访问更快、更近:
- 将数据存储到读取速度更快的存储(设备);
- 将数据存储到离应用最近的位置;
- 将数据存储到离用户最近的位置。
缓存是用于存储数据的硬件或软件的组成部分,以使得后续更快访问相应的数据。缓存中的数据可能是提前计算好的结果、数据的副本等。典型的应用场景:有 cpu cache, 磁盘 cache 等。本文中提及到缓存主要是指互联网应用中所使用的缓存组件。
缓存命中率是缓存的重要度量指标,命中率越高越好。
缓存命中率 = 从缓存中读取次数 / 总读取次数【简单】何时需要缓存?
引入缓存,会增加系统的复杂度,并牺牲一定的数据实时性。所以,引入缓存前,需要先权衡是否值得,考量点如下:
- CPU 开销 - 如果应用某个计算需要消耗大量 CPU,可以考虑缓存其计算结果。典型场景:复杂的、频繁调用的正则计算;分布式计算中间状态等。
- IO 开销 - 如果数据库连接池比较繁忙,可以考虑缓存其查询结果。
在数据层引入缓存,有以下几个好处:
- 提升数据读取速度。
- 提升系统扩展能力,通过扩展缓存,提升系统承载能力。
- 降低存储成本,Cache+DB 的方式可以承担原有需要多台 DB 才能承担的请求量,节省机器成本。

【中等】缓存有哪些分类?
缓存从部署角度,可以分为客户端缓存和服务端缓存。
客户端缓存
- Http 缓存:HTTP/1.1 中的
Cache-Control、HTTP/1 中的Expires - 浏览器缓存:HTML5 提供的 SessionStorage 和 LocalStorage、Cookie
- APP 缓存:Android、IOS
服务端缓存
- CDN 缓存 - CDN 将数据缓存到离用户物理距离最近的服务器,使得用户可以就近获取请求内容。CDN 一般缓存静态资源文件(页面,脚本,图片,视频,文件等)。
- 反向代理缓存 - 反向代理(Reverse Proxy)方式是指以代理服务器来接受网络连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给客户端,此时代理服务器对外就表现为一个反向代理服务器。反向代理缓存一般针对的是静态资源,而将动态资源请求转发到应用服务器处理。
- 数据库缓存 - 数据库(如 Mysql)自身一般也有缓存,但因为命中率和更新频率问题,不推荐使用。
- 进程内缓存 - 缓存应用字典等常用数据。
- 分布式缓存 - 缓存数据库中的热点数据。
其中,CDN 缓存、反向代理缓存、数据库缓存一般由专职人员维护(运维、DBA)。
后端开发一般聚焦于进程内缓存、分布式缓存。
【中等】什么是 CDN?CDN 的工作原理是什么?
CDN 是一种将内容缓存到离用户更近的节点的分布式网络系统。CDN 一般缓存静态资源文件(页面,脚本,图片,视频,文件等)。
国内网络异常复杂,跨运营商的网络访问会很慢。为了解决跨运营商或各地用户访问问题,可以在重要的城市,部署 CDN 应用。使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。

CDN 原理
- 就近调度:用户被导向最近的 CDN 节点
- 缓存+回源:节点有内容直接返回(缓存命中);节点无内容则向源站请求并缓存(缓存未命中)
CDN 特点
优点:
- 缓存加速:提升访问速度,尤其是含有大量图片和静态页面站点
- 带宽优化:自动生成服务器的远程 Mirror(镜像)cache 服务器,远程用户访问时从 cache 服务器上读取数据,减少远程访问的带宽、分担网络流量、减轻原站点 WEB 服务器负载等功能。
- 集群抗攻击 - 广泛分布的 CDN 节点加上节点之间的智能冗余机制,可以有效地预防黑客入侵以及降低各种 D.D.o.S 攻击对网站的影响,同时保证较好的服务质量。
缺点:
- 不适宜缓存动态资源
- 解决方案:主要缓存静态资源,动态资源建立多级缓存或准实时同步;
- 存在数据的一致性问题
- 解决方案(主要是在性能和数据一致性二者间寻找一个平衡)
- 设置缓存失效时间(1 个小时,过期后同步数据)。
- 针对资源设置版本号。
【中等】反向代理缓存的工作原理是什么?
反向代理服务器部署在应用服务器前端,作为流量入口。既是反向代理(转发请求),也是缓存服务器(缓存响应)。
反向代理(Reverse Proxy)方式是指以代理服务器来接受网络连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给客户端,此时代理服务器对外就表现为一个反向代理服务器。

反向代理位于应用服务器同一网络,处理所有对 WEB 服务器的请求。
反向代理缓存的原理:
- 如果用户请求的页面在代理服务器上有缓存的话,代理服务器直接将缓存内容发送给用户。
- 如果没有缓存则先向 WEB 服务器发出请求,取回数据,本地缓存后再发送给用户。
这种方式通过降低向 WEB 服务器的请求数,从而降低了 WEB 服务器的负载。
反向代理缓存一般针对的是静态资源,而将动态资源请求转发到应用服务器处理。常用的缓存应用服务器有 Varnish,Ngnix,Squid。
【中等】缓存有哪些淘汰算法?⭐⭐⭐
扩展
缓存算法设计思路
缓存一般存于访问速度较快的存储介质,快也就意味着资源昂贵并且有限。正所谓,好钢要用在刀刃上。因此,缓存要合理利用,需要设定一些机制,将一些访问频率偏低或过期的数据淘汰。
淘汰缓存首先要做的是,确定什么时候触发淘汰缓存,一般有以下几个思路:
- 基于空间 - 设置缓存空间大小。
- 基于容量 - 设置缓存存储记录数。
- 基于时间
- TTL(Time To Live,即存活期) - 缓存数据从创建到过期的时间。
- TTI(Time To Idle,即空闲期) - 缓存数据多久没被访问的时间。
主流缓存算法对比
接下来,就要确定如何淘汰缓存,常见的缓存淘汰算法有以下几个:
| 算法 | 淘汰策略 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| FIFO(先进先出) | 淘汰最先进入的数据(队列结构) | 实现简单 | 缓存命中率低(无视访问频率,可能淘汰热点数据) | 数据访问无规律,或实现简单的缓存系统 |
| LIFO(后进先出) | 淘汰最后进入的数据(栈结构) | 实现简单 | 缓存命中率低(无视访问频率,可能淘汰热点数据) | 特殊场景(如回退操作,新数据价值低) |
| MRU(最近最多使用) | 淘汰最近最多使用的数据 | 适合访问局部性强的场景(如用户浏览信息流,看过内容不再看) | 可能频繁淘汰缓存,降低命中率 | 数据访问模式具有“看后即弃”特性(如信息流推荐) |
| LRU(最近最少使用) | 淘汰最近最少被使用的数据(基于访问时间排序) | 避免 FIFO 问题,对热点数据友好,综合性能好 | 临界区问题(热点数据在统计窗口末期无访问会被误淘汰) | 通用场景(Web 缓存、数据库缓存等) |
| LFU(最近最少频率使用) | 淘汰使用频率最低的数据(额外记录访问频率) | 解决 LRU 临界区问题,对长期热点数据保护更好 | 空间开销大(需记录频率); 频率衰减问题(旧热点难以淘汰) | 热点数据稳定,访问模式变化慢(如视频热门榜) |
【困难】缓存更新有哪些策略?⭐⭐⭐

一般来说,系统如果不是严格要求缓存和数据库保持一致性的话,尽量不要将读请求和写请求串行化。串行化可以保证一定不会出现数据不一致的情况,但是它会导致系统的吞吐量大幅度下降。缓存更新的常见策略有以下几种:
- Cache Aside
- Wirte Through
- Read Though
- Wirte Behind
需要注意的是:以上几种缓存更新策略,都无法保证数据强一致。如果一定要保证强一致性,可以通过两阶段提交(2PC)或 Paxos 协议来实现。但是 2PC 太慢,而 Paxos 太复杂,所以如果不是非常重要的数据,不建议使用强一致性方案。
Cache Aside
Cache Aside 的思路是:先更新数据库,再删除缓存。具体来说:
失效:尝试读缓存,如果不命中,则读数据库,然后更新缓存。
命中:尝试读缓存,命中则直接返回数据。
更新:先更新数据库,再删除缓存。

为什么不能先更新数据库,再更新缓存?
多个并发的写操作可能导致脏数据:当有多个并发的写请求时,无法保证更新数据库的顺序和更新缓存的顺序一致,从而导致数据库和缓存数据不一致的问题。

说明:如上图的场景中,两个写线程由于执行顺序,导致数据库中 val = 2,而缓存中 val = 1,数据不一致。
为什么不能先删缓存,再更新数据库?
存在并发读请求和写请求时,可能导致脏数据。

说明:如上图的场景中,读线程和写线程并行执行,导致数据库中 val = 2,而缓存中 val = 1,数据不一致。
先更新数据库,再删除缓存就没问题了吗?
存在并发读请求和写请求时,可能导致脏数据。

上图中问题发生的概率非常低:因为通常数据库更新操作比内存操作耗时多出几个数量级,最后一步回写缓存速度非常快,通常会在更新数据库之前完成。所以 Cache Aside 模式选择先更新数据库,再删除缓存,而不是先删缓存,再更新数据库。
不过,如果真的出现了这种场景,为了避免缓存中一直保留着脏数据,可以为缓存设置过期时间,过期后缓存自动失效。通常,业务系统中允许少量数据短时间出现不一致的情况。
Read/Write Through

Read Through 的思路是:查询时更新缓存。当缓存失效时,缓存服务自己进行加载。
Write Through 的思路是:当数据更新时,缓存服务负责更新缓存。
Through vs. Cache Aside
Read Through vs. Cache Aside
- Cache Aside 模式中,应用需要维护两个数据源头:一个是缓存,一个是数据库。
- Read-Through 模式中,应用无需管理缓存和数据库,只需要将数据库的同步委托给缓存服务即可。
Write behind
Write Behind 又叫 Write Back。Write Behind 的思路是:应用更新数据时,只更新缓存, 缓存服务每隔一段时间将缓存数据批量更新到数据库中,即延迟写入。这个设计的好处就是让提高 I/O 效率,因为异步,Write Behind 还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的。
【困难】多级缓存架构如何设计?⭐
一般来说,多级缓存架构使用二级缓存已可以满足大部分业务需求,过多的分级会增加系统的复杂度以及维护的成本。因此,多级缓存不是分级越多越好,需要根据实际情况进行权衡。
一个典型的二级缓存架构,可以使用进程内缓存(如: Caffeine/Google Guava/Ehcache/HashMap)作为一级缓存;使用分布式缓存(如:Redis/Memcached)作为二级缓存。
多级缓存查询

多级缓存查询流程如下:
- 首先,查询 L1 缓存,如果缓存命中,直接返回结果;如果没有命中,执行下一步。
- 接下来,查询 L2 缓存,如果缓存命中,直接返回结果并回填 L1 缓存;如果没有命中,执行下一步。
- 最后,查询数据库,返回结果并依次回填 L2 缓存、L1 缓存。
多级缓存更新
对于 L1 缓存,如果有数据更新,只能删除并更新所在机器上的缓存,其他机器只能通过超时机制来刷新缓存。超时设定可以有两种策略:
- 设置成写入后多少时间后过期
- 设置成写入后多少时间刷新
对于 L2 缓存,如果有数据更新,其他机器立马可见。但是,也必须要设置超时时间,其时间应该比 L1 缓存的有效时间长。
为了解决进程内缓存不一致的问题,设计可以进一步优化:

通过消息队列的发布、订阅机制,可以通知其他应用节点对进程内缓存进行更新。使用这种方案,即使消息队列服务挂了或不可靠,由于先执行了数据库更新,但进程内缓存过期,刷新缓存时,也能保证数据的最终一致性。
【中等】什么是缓存雪崩?如何应对?⭐⭐⭐
“缓存雪崩”是指,缓存不可用或者大量缓存由于超时时间相同在同一时间段失效,大量请求直接访问数据库,数据库压力过大导致系统雪崩。
举例来说,对于系统 A,假设每天高峰期每秒 5000 个请求,本来缓存在高峰期可以扛住每秒 4000 个请求,但是缓存机器意外发生了全盘宕机。缓存挂了,此时 1 秒 5000 个请求全部落数据库,数据库必然扛不住,它会报一下警,然后就挂了。此时,如果没有采用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,但是数据库立马又被新的流量给打死了。
解决缓存雪崩的主要手段如下:
- 增加缓存系统可用性(事前)。例如:部署 Redis Cluster(主从+哨兵),以实现 Redis 的高可用,避免全盘崩溃。
- 采用多级缓存方案(事中)。例如:本地缓存(Ehcache/Caffine/Guava Cache) + 分布式缓存(Redis/ Memcached)。
- 限流、降级、熔断方案(事中),避免被流量打死。如:使用 Hystrix 进行熔断、降级。
- 缓存如果支持持久化,可以在恢复工作后恢复数据(事后)。如:Redis 支持持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
上面的解决方案简单来说,就是多级缓存方案。系统收到一个查询请求,先查本地缓存,再查分布式缓存,最后查数据库,只要命中,立即返回。
解决缓存雪崩的辅助手段如下:
- 监控缓存,弹性扩容。
- 缓存的过期时间可以取个随机值。这么做是为避免缓存同时失效,使得数据库 IO 骤升。比如:以前是设置 10 分钟的超时时间,那每个 Key 都可以随机 8-13 分钟过期,尽量让不同 Key 的过期时间不同。
【中等】什么是缓存穿透?如何应对?⭐⭐⭐
“缓存穿透”是指,查询的数据在数据库中不存在,那么缓存中自然也不存在。所以,应用在缓存中查不到,则会去查询数据库,当这样的请求多了后,数据库的压力就会增大。
解决缓存穿透,一般有两种方法:
(一)缓存空值
对于返回为 NULL 的依然缓存,对于抛出异常的返回不进行缓存。

采用这种手段的会增加我们缓存的维护成本,需要在插入缓存的时候删除这个空缓存,当然我们可以通过设置较短的超时时间来解决这个问题。
(二)过滤不可能存在的数据

制定一些规则过滤一些不可能存在的数据。可以使用布隆过滤器(针对二进制操作的数据结构,所以性能高),比如你的订单 ID 明显是在一个范围 1-1000,如果不是 1-1000 之内的数据那其实可以直接给过滤掉。
针对于一些恶意攻击,攻击带过来的大量 key 是不存在的,那么我们采用第一种方案就会缓存大量不存在 key 的数据。
此时我们采用第一种方案就不合适了,我们完全可以先对使用第二种方案进行过滤掉这些 key。
针对这种 key 异常多、请求重复率比较低的数据,我们就没有必要进行缓存,使用第二种方案直接过滤掉。
而对于空数据的 key 有限的,重复率比较高的,我们则可以采用第一种方式进行缓存。
【中等】什么是缓存击穿?如何应对?⭐⭐⭐
“缓存击穿”是指,热点缓存数据失效瞬间,大量请求直接访问数据库。例如,某些 key 是热点数据,访问非常频繁。如果某个 key 失效的瞬间,大量的请求过来,缓存未命中,然后去数据库访问,此时数据库访问量会急剧增加。
为了避免这个问题,我们可以采取下面的两个手段:
- 分布式锁 - 锁住热点数据的 key,避免大量线程同时访问同一个 key。
- 定时异步刷新 - 可以对部分数据采取失效前自动刷新的策略,而不是到期自动淘汰。淘汰其实也是为了数据的时效性,所以采用自动刷新也可以。
读写分离
【简单】什么是读写分离?为什么需要读写分离?
读写分离将数据库的读操作和写操作分离到不同的数据库实例。
读写分离作用
- 有效减少锁竞争 - 主服务器只负责写,从服务器只负责读,能够有效的避免由数据更新导致的行锁竞争,使得整个系统的查询性能得到极大的改善。
- 提高查询吞吐量 - 通过一主多从的配置方式,可以将查询请求均匀的分散到多个数据副本,能够进一步的提升系统的处理能力。
- 提升数据库可用性 - 使用多主多从的方式,不但能够提升系统的吞吐量,还能够提升数据库的可用性,可以达到在任何一个数据库宕机,甚至磁盘物理损坏的情况下仍然不影响系统的正常运行。
读写分离工作原理
- 写操作:所有写请求(增删改)只发送到主库
- 数据同步:主库通过复制机制将数据变更同步到从库
- 读操作:读请求分发到多个从库(负载均衡)
- 延迟处理:需处理主从同步延迟带来的数据不一致问题
【中等】如何实现读写分离?
| 组件 | 作用 | 记忆口诀 |
|---|---|---|
| SQL 解析器 | 识别 SELECT(读)和 INSERT/UPDATE(写) | 辨读写 |
| 路由引擎 | 写走主,读走从(轮询/随机) | 分主从 |
| 复制机制 | 主库变更自动同步到从库 | 同步数据 |
| 负载均衡 | 多个从库分摊读压力 | 读分摊 |
分库分表
【简单】什么是分库分表?为什么需要分库分表?
分库分表 = 将大数据库拆分成多个小库小表,分为:
- 分库:数据按规则分布到不同数据库实例
- 分表:数据按规则分布到同一个库的不同表
何时需要分库分表:
- 数据量:单表超过 500 万-1000 万 行
- 数据大小:单表超过 10GB
- 访问性能:查询响应明显变慢(>1 秒)
- 连接数:数据库连接经常打满
【困难】如何实现分库分表?⭐⭐
分库分表 = 选好拆分键 + 用对路由算法 + 平滑数据迁移 + 改造查询语句,核心是让数据均匀分布且查询能精准定位到分片。
数值范围路由
数值范围路由,就是根据 ID、时间范围 这类具有排序性的字段来进行划分。例如:用户 Id 为 1-9999 的记录分到第一个库,10000-20000 的分到第二个库,以此类推。
按这种策略划分出来的数据,具有数据连续性。
优点:数据迁移很简单。
缺点:容易产生热点问题,大量的流量都打在最新的数据上了。
Hash 路由
典型的 Hash 路由,如根据数值取模,当需要扩容时,一般以 2 的幂次方进行扩容(这样,扩容时迁移的数据量会小一些)。例如:用户 Id mod n,余数为 0 的记录放到第一个库,余数为 1 的放到第二个库,以此类推。
一般采用 预分区 的方式,提前根据 数据量 规划好 分区数,比如划分为 512 或 1024 张表,保证可支撑未来一段时间的 数据容量,再根据 负载情况 将 表 迁移到其他 数据库 中。扩容时通常采用 翻倍扩容,避免 数据映射 全部被 打乱,导致 全量迁移 的情况。
优点:数据离散分布,不存在热点问题。
缺点:数据迁移、扩容麻烦(之前的数据需要重新计算 hash 值重新分配到不同的库或表)。当 节点数量 变化时,如 扩容 或 收缩 节点,数据节点 映射关系 需要重新计算,会导致数据的 重新迁移。
路由表
这种策略,就是用一张独立的表记录路由信息。
优点:简单、灵活,尤其是在扩容、迁移时,只需要迁移指定的数据,然后修改路由表即可。
缺点:每次查询,必须先查路由表,增加了 IO 开销。并且,如果路由表本身太大,也会面临性能瓶颈,如果想对路由表再做分库分表,将出现死循环式的路由算法选择问题。
【困难】分库分表后,如何应对扩容和迁移?⭐⭐
停机迁移/扩容(不推荐)
停机迁移/扩容是最暴力、最简单的迁移、扩容方案。

停机迁移/扩容流程
- 预估停服时间,发布停服公告;停服,不允许数据访问。
- 编写临时的数据导入程序,从老数据库中读取数据。
- 将数据写入中间件。
- 中间件根据分片规则,将数据分发到分库(分表)中。
- 应用程序修改配置,重启。
停机迁移/扩容方案分析
- 优点:简单、无数据一致性问题。
- 缺点:
- 停服时间长(数据量大时可能需数小时)。
- 风险高,失败后难以回滚。
结论:代价过高,不推荐使用。
双写迁移
双写迁移方案核心思想:
- 新旧库同时写入,通过开关控制读写状态(只写旧库、只写新库、双写)。
- 逐步切换读请求到新库,确保数据一致性。
双写迁移方案关键步骤:
- 双写阶段:先写旧库,再写新库,以旧库结果为准。记录旧库成功但新库失败的日志,用于补偿。
- 数据校验:运行对比程序,检查新旧库数据差异并修复。
- 灰度切换读请求:逐步将读流量切至新库,观察稳定性。
- 最终切换:读写全部切至新库,清理旧库冗余数据。
双写迁移方案流程

- 修改应用程序配置,将数据同时写入老数据库和中间件。这就是所谓的双写,同时写俩库,老库和新库。
- 编写临时程序,读取老数据库。
- 将数据写入中间件。如果数据不存在,直接写入;如果数据存在,比较时间戳,只允许新数据覆盖老数据。
- 导入数据后,有可能数据还是存在不一致,那么就对数据进行校验,比对新老库的每条数据。如果存在差异,针对差异数据,执行(3)。循环(3)、(4)步骤,直至数据完全一致。
- 修改应用程序配置,将数据只写入中间件。
- 中间件根据分片规则,将数据分发到分库(分表)中。
双写迁移方案分析
优点:
- 无需停服,业务影响小。
- 可灰度验证,风险可控。
缺点:
- 实现复杂,需处理双写一致性和补偿逻辑。
主从替换
生产环境的数据库,为了保证高可用,一般会采用主从架构。主库支持读写操作,从库支持读操作。

由于主从节点数据一致,所以将从库升级为主节点,并修改分片配置,将从节点作为分库之一,就实现了扩容。

升级从库的流程
- 解除主从关系,从库升级为主库。
- 应用程序,修改配置,读写通过中间件。
- 分库分表中间,修改分片配置。将数据按照新的规则分发。
- 编写临时程序,清理冗余数据。比如:原来是一个单库,数据量为 400 万。从节点升级为分库之一后,每个分库都有 400 万数据,其中 200 万是冗余数据。清理完后,进行数据校验。
- 为每个分库添加新的从库,保证高可用。
升级从库方案分析
优点:不需要停机,无需数据迁移。