15分钟速通Redis是什么?怎么实现高可用?集群模式是怎么样的?_哔哩哔哩_bilibili
加入你维护了一个商品服务,背后直连商品服务,商品服务需要对外提供10k/s查询,但mysql只能提供5k/s查询,比如秒杀,双十一,所以要加一个中间件
有了本地缓存的加持,真正打到mysql的查询量就很少
远程缓存:
为了达到系统高可用,商品服务不止一个缓存实例,如果每个实例都重复缓存一份本地内存,就浪费内存条
将这部分字典内存抽出来,单独做成一个服务,就是所谓的远程缓存服务。多个商品服务通过网络去读写同一份远程缓存,会存在并发问题。所以对外不管又多少个网络连接,收到读写命令后,都统一塞到一个线程上,在一个线程上对字典进行读写,所以并发问题,线程切换开销都解决了
支持多种数据类型
除了字典类型(key和value都是字符串),于是对字段的value进行扩展,除了字符串还支持先进先出的队列list和用于去重的set类型,再加入
内存过期策略
调用方判断并调用内存过期时间,让用户通过expire命令指定哪些数据多久过期
缓存淘汰
再内存接近上限的时候,根据一些策略删除掉一些内存,比如Least Recently Used
持久化
RDB
一旦缓存重启,内存全部丢掉,这时流量会全都打在Mysql上,所以需要给内存加入最大程度的持久化保证。于是可以在缓存服务里加个异步子进程,定期将全量内存数据持久化到磁盘文件里,而这种将内存文件生成快照保存到文件的方式就是RDB,可以每隔几分钟记录下缓存服务的全量数据,类似于游戏的存档,这样就算给进程挂了,重启的时候通过加载快照文件,就能复原大部分数据
AOF
全量写入数据耗时,化整为零,在每次写入数据时,顺手将数据记录到文件缓存中,并每秒将文件缓存刷入磁盘
AOF文件会很大,可以定期重写压缩
缓存击穿?缓存雪崩?
些问题确实是Redis和应用系统设计中的核心高频面试题,它们关系到系统的稳定性、可用性和性能。我们来非常详细地、通俗易懂地拆解一下。
核心概念:为什么需要缓存?
首先,要理解这两个问题,必须明白我们为什么用Redis做缓存。
在现代应用架构(如“客户端 - Redis - 数据库”)中,Redis作为缓存的核心目的是:保护底层数据库(如MySQL),并提升读写速度。
速度快:Redis基于内存,读写速度极快(微秒级)。
减轻数据库压力:绝大多数请求直接由Redis处理,只有Redis没有的数据才会去查询数据库,并将结果回写到Redis。数据库只需要处理少量请求,从而能支撑更高的并发。
正常流程:一个请求来了,业务代码会先尝试从Redis中获取数据。如果取到(缓存命中),直接返回。如果没取到(缓存未命中),就去数据库查询,然后将查询结果写入Redis(并设置过期时间),再返回数据。
缓存击穿和缓存雪崩就是发生在“缓存未命中”时,大量请求直接涌向数据库,导致数据库压力激增甚至崩溃的两种异常场景。
一、缓存击穿 (Cache Breakdown)
1. 是什么?(现象)
一个非常热点的Key(比如某爆款商品的详情数据),在它失效(过期)的瞬间,海量的并发请求同时发现缓存失效,这些请求全部直接打到数据库上,就像一颗子弹击穿了缓存这层防护,直接命中了数据库。
这个Key就像是防洪堤坝上的一个“关键点”,这个点一垮,所有洪水瞬间从这个点冲垮后面的数据库。
2. 会造成什么后果?
数据库瞬间承受巨大的、重复的查询压力,可能导致数据库响应变慢甚至宕机。
3. 如何解决?
解决思路的核心是:避免大量请求同时去数据库查询同一个Key。
永不过期 (设置合理的长期过期时间)
物理不过期:不设置过期时间(
expire
)。这意味着数据永远不会因时间而失效。逻辑过期:在Value中存储一个过期时间戳(例如
{value: obj, expireTime: 1740988800}
)。业务代码查询到数据后,判断是否已过逻辑过期时间。如果已过期,则发起一个异步任务去更新缓存,而当前请求仍然返回旧数据。优点:实现简单,彻底避免击穿。
缺点:需要自己维护数据的更新逻辑(如用定时任务或在写入DB时更新缓存),数据一致性需要开发者自己保证。
互斥锁 (Mutex Lock) - 最常用
思路:只让一个请求去数据库查询和重建缓存,其他请求等待,缓存重建成功后直接使用新数据。
实现:
请求A发现缓存失效。
请求A尝试使用Redis的
SETNX key value
(或SET key value NX PX 3000
)命令设置一个锁Key(例如lock:product_123
)。这个操作是原子的,只有一个请求能设置成功。设置成功的请求A,去数据库查询数据,回写到Redis,然后删除这个锁Key。
其他请求在执行
SETNX
时会失败,于是它们等待一小段时间(例如100ms)后,重新尝试从缓存获取数据。由于请求A已经重建了缓存,后续请求就能直接命中了。
优点:能很好地保护数据库,逻辑清晰。
缺点:如果获取锁的请求重建缓存失败,可能导致死锁(所以锁一定要设置超时时间)。等待可能会增加请求的响应时间。
二、缓存雪崩 (Cache Avalanche)
1. 是什么?(现象)
大量的缓存Key在同一时间段失效,或者Redis服务本身直接宕机了,导致所有请求都无法命中缓存,全部请求都直达数据库,数据库压力骤增,甚至瞬间被压垮。
这就像是整个防洪堤坝(大量Key)在同一时间大面积垮塌,引发的“雪崩”瞬间淹没了后端的数据库。
2. 会造成什么后果?
比缓存击穿更严重,数据库可能面临的是所有业务线的全部请求,极易导致数据库连锁崩溃,整个系统瘫痪。
3. 如何解决?
解决思路的核心是:让Key的失效时间变得均匀 和 保证Redis服务的高可用。
差异化过期时间
这是解决“大量Key同时过期”最有效、最简单的方案。
在设置Key的过期时间时,增加一个随机值。
例如:原本统一设置1小时过期,可以改为
基础时间(1小时) + 随机时间(0~300秒)
。这样就能保证Key是均匀地失效,而不是同时失效,将压力平摊到不同的时间点上。
构建高可用的Redis集群
这是解决“Redis服务宕机”的根本方案。
采用 主从复制(Replication) + 哨兵模式(Sentinel) 或者 Redis集群(Cluster)。
这样即使一台Redis服务器宕机,哨兵机制会自动进行故障转移,将请求切换到从节点上,保证服务整体可用,缓存层不会完全崩溃。
服务降级和熔断
在应用系统中引入熔断器(如Hystrix, Sentinel)。
当检测到数据库压力过大,响应缓慢或错误率升高时,系统会自动熔断,直接返回一个预设的默认值(如“系统繁忙,请稍后再试”)、空值或兜底数据,而不是让所有请求都去冲击已经濒临崩溃的数据库。这是一种“舍卒保帅”的策略。
提前演练
对于重要的系统,可以提前模拟缓存宕机的场景,进行压力测试,看看系统在没有缓存的情况下能支撑多久,做到心中有数。
三、面试常考的“邻居”:缓存穿透 (Cache Penetration)
面试中经常会把这三个放在一起考,所以我们一并讲了。
1. 是什么?(现象)
用户请求查询一个数据库中也根本不存在的数据。比如请求一个不存在的商品ID -1
或一个非常大的ID。
由于缓存中不会有这个数据(每次查询都未命中),数据库中也查不到(不会回写到缓存),导致这个请求每次都会穿透缓存去查询数据库。如果有人恶意构造大量这样的请求进行攻击,数据库压力会非常大。
2. 如何解决?
接口层增加校验
对请求参数进行基础校验,如ID<=0的直接拦截返回,防止无效请求落到数据库。
缓存空对象 (Cache Null Object)
即使从数据库没查到,也向Redis写入一个空值(如
SET keynull 60
)并设置一个较短的过期时间(如60秒)。这样后续相同的无效请求在短时间内就能在缓存层面被拦截住。
缺点:可能会在缓存中存储大量无意义的空Key,消耗内存。
使用布隆过滤器 (Bloom Filter) - 最优解
布隆过滤器是一个占用空间极小、效率很高的数据结构,它可以准确地判断一个元素“一定不存在” 或 “可能存在”于某个集合中。
流程:
系统启动时,将所有可能存在的查询Key(如所有有效的商品ID)预先加载到布隆过滤器中。
请求来时,先让请求参数经过布隆过滤器。
如果过滤器说“这个Key一定不存在”,则直接返回空,根本不会去查缓存和数据库。
如果过滤器说“这个Key可能存在”,则继续后面的正常缓存查询流程。
优点:空间效率和查询时间都远超其他算法,非常适合这种场景。
缺点:有轻微的误判率(“可能存在”意味着它可能会误判一个不存在的Key为存在,但不会漏掉存在的Key),且数据更新时需要同步更新布隆过滤器。
总结与对比
问题 | 缓存穿透 | 缓存击穿 | 缓存雪崩 |
---|---|---|---|
问题Key | 不存在的数据(数据库和缓存都没有) | 热点Key突然失效 | 大量Key同时失效 或 Redis服务宕机 |
危害层面 | 数据库压力 | 数据库压力 | 系统整体瘫痪风险 |
解决方案 | 1. 参数校验 2. 缓存空值 3. 布隆过滤器 |
1. 互斥锁 2. 逻辑永不过期 |
1. 差异化过期时间 2. Redis高可用集群 3. 服务降级熔断 |
简化网络协议
远程缓存能力对外提供读写能力是对外提供的HTTP接口吗?显然不是。HTTP是基于TCP做的通信,实现了很多笨重的特性。所以直接抛弃这些笨重的特性,直接抛弃HTTP,直接基于TCP做传输,传输协议也设置地简单点,比如只需要TCP传入 set key value就能完成写入,传入get key就能获得对应地value
Redis是什么?
高性能,支持多种数据类型地和各种缓存淘汰策略并提供一定持久化能力地的超强缓存服务
REDIS
说白了就是个改进的远程字典服务
通过官方提供的命令行工具redis-cli,可以输入一些命令,读写Redis服务器里的各种内存数据
Redis作为架构中最常见的提速神奇,是万金油一般的存在,将它放在mysql前面挡挡只是最基础的用法
高阶的:比如redis json 支持复杂的JSON查询和更行(内存版本的MongoDB)
ReisSearch支持全文搜索(内存丐版的ES)
ReisGraph支持图数据库功能,类似Neoco4j
Redis是个单机服务,虽然redis性能很高,但读写性能受限于单个服务器的cpu和内存上限,并且一旦挂了,外部就没法用了
高可用
主从模式
最简单的方式就是加副本,将它们分为主从关系,主节点对外提供读写操作,并将写入的数据同步给从节点,从节点也不能闲着,对外提供读操作,加的读节点越多支持的读请求量就越高,支持的读请求量就越高。将主从Redis节点部署到不同的服务器上,这样既提升了Redis的可用性,也提升了Redis读性能
主节点可以将RDB文件传给从文件完成大部分数据同步,那如果同步过程中还有一些写操作,可以将他记录到主节点中
同时如果同步过程中主节点正在写,可以将这个记录到主节点里一个叫复制积压缓冲区的地方,等RDB同步结束后,再将里面的内容同步给从节点
主从切换问题
经典主从模式,在主节点挂了之后,是需要手动切换到主从节点的,需要时间成本、
所以一般会通过一些自动化脚本工具去进行自动切换,比如
这些外部工具只能粗糙地检查reids是否存活,比如Redis进程时候存在等,但是并不能感知Redis内部的复制便宜量和数据一致性等等。因此在多从节点的场景下,被选为新主的从节点并不一定是最合适的那个,就需要引入哨兵
哨兵
我们可以在原来主从模式的基础上加入一个监控redis内部状态的进程。监控进程先跟主节点获取当前又哪些从节点,再每秒ping一下从节点,就选择一个健康的从节点顶上,会优先选择配置里填的优先级最高的从节点,比如服务器内存越大,配的优先级越高,其次会选择复制偏移量最大的从节点(数据最接近原主节点),这个监控进程就是所谓的哨兵Sentiel。功能不多,而且还需要跟底层Redis通信,也就是要复用Redis底层的通信协议能力,所以直接放到redis里
哨兵集群,哨兵间互相通信,只有大多数哨兵确认这个redis下线了才会认为真的下线了,也就是客观下线
Redis集群模式
单机服务器内存总有瓶颈
数据切分
单机内存有限而是数据无限
如果像cache那样取余来确定数据放在哪块redis,但是如果扩展redis节点,那么所有数据都要进行迁移,显然不行
为了确定公式,在数据的key值和redis节点之间加一层长度固定的数组层
这样可以减少要迁移的数据,