深入理解 Redis 缓存:原理、应用与挑战

深入理解 Redis 缓存:原理、应用与挑战

一、什么是缓存

1.1 缓存的定义和原理

缓存(Cache)是一种存储介质,用于临时存储经常被访问的数据,以加快数据读取速度、减少对后端服务(如数据库)的访问压力。它通常基于内存实现,具备高性能、高并发的特点。

在后端系统中,缓存位于客户端和数据库之间,起到中间加速层的作用。当用户请求某个数据时,系统优先从缓存中读取,若命中缓存,则快速返回结果;若未命中,则访问数据库并将结果写入缓存,以备下次使用。

1.2 缓存的作用

提高响应速度:内存访问速度远高于磁盘,显著降低用户请求的响应时间。

减轻数据库压力:缓存热点数据可以有效减少数据库访问次数,提升整体系统吞吐量。

增强系统稳定性:在高并发场景下,通过缓存限流减压,避免数据库过载。

1.3 缓存的成本与挑战

内存资源消耗大:缓存通常存储在内存中,成本较高。

一致性维护困难:缓存中的数据可能与数据库不一致。

复杂度提升:需要考虑缓存更新策略、失效策略、缓存预热等问题。

二、为什么选 Redis 作为缓存

2.1 Redis 简介

Redis 是一个开源的高性能键值对存储系统,数据完全存储在内存中,并定期持久化到磁盘,支持丰富的数据结构,如字符串、哈希、列表、集合、有序集合等。

2.2 Redis 的缓存优势

高性能:内存读写速度极快,可轻松支撑百万级 QPS。

丰富的数据结构:支持灵活的数据组织形式,便于实现各种缓存需求。

单线程模型+高并发处理:保证了操作的原子性,避免锁竞争。

支持持久化:通过 RDB 和 AOF 实现数据恢复能力。

成熟的生态系统:支持分布式部署(Redis Cluster)、高可用方案(Sentinel)、丰富的客户端工具和监控体系。

三、常见的缓存模型

在实际开发中,缓存的设计不仅要考虑“存什么”和“存多久”,更需要选择合适的缓存更新模型。不同的缓存模型在数据一致性、系统复杂度、性能表现等方面各有优劣。

3.1 常见缓存更新策略对比

策略

描述

一致性

维护成本

内存淘汰

不主动清除缓存,依赖 Redis 的内存淘汰机制,在内存不足时自动剔除旧数据,下次查询时回源数据库并回填缓存。

超时剔除

为缓存数据设置 TTL,到期自动失效,访问时发现不存在再从数据库加载并回填缓存。

一般

主动更新

在业务代码中主动更新缓存,如在数据库变更时同步刷新或删除相关缓存数据。

选择建议:

低一致性要求:如商品分类、店铺类型列表等冷变数据,可使用内存淘汰机制。

高一致性要求:如商品详情、订单信息等频繁变化的数据,建议使用主动更新 + TTL 兜底。

3.2 常见缓存模型

Cache Aside(旁路缓存)

最常用的模型,缓存由应用层手动维护。

读操作:

先查缓存 → 缓存未命中 → 查询数据库 → 回填缓存;

写操作:

更新数据库 → 删除缓存(或更新缓存);

优点:可控性高;

缺点:需要开发者显式维护缓存逻辑;

适用场景:读多写少、可接受短暂不一致的业务场景。

Read/Write Through(读/写穿透)

由缓存系统代理所有读写请求。

读操作:应用请求先到缓存,由缓存系统自动判断是否命中;

写操作:写入操作由缓存系统同步写入缓存与数据库;

优点:对调用者简单;缓存更新与数据库同步,保证较高一致性;

缺点:依赖中间缓存框架实现,侵入性高;

适用场景:中大型系统、对一致性要求高、适合使用成熟缓存代理中间件的场景。

Write Behind(异步写回)

写操作先写缓存,再异步写入数据库(延迟双写)。

优势:

对调用者简单

写操作性能高(多次缓存操作后,一次性写入数据库);

劣势:

维护复杂,需要实时监控缓存的变化

一致性难以保证

对缓存操作期间,尚未写入数据库,如果此时有其他线程访问了数据库,就会有数据不一致的问题出现

可靠性难以保证

缓存基于内存,未持久化时一旦宕机会导致数据丢失

适用场景:日志收集、计数器、埋点数据等对一致性要求较低的高频写业务。

TTL 缓存模型(基于过期时间)

通过为缓存设置过期时间自动清除失效数据。

常与旁路缓存结合,用于短期热点数据或容错兜底;

TTL 设计需结合业务特性,如短期活动数据、实时新闻、接口幂等性结果等。

四、缓存的生命周期管理

4.1 缓存失效策略

定期过期(TTL):为每个键设置过期时间,到期自动清除。

逻辑过期:缓存中存储数据+过期时间,由应用判断是否过期。

被动失效:访问数据时发现已过期再清除。

4.2 Redis 淘汰策略

当 Redis 达到最大内存限制时,自动触发淘汰策略,清理部分数据:

noeviction:不淘汰,返回错误(默认)

volatile-lru:淘汰设置了过期时间的键中最近最少使用的

allkeys-lru:淘汰所有键中最近最少使用的

volatile-random / allkeys-random:随机淘汰

volatile-ttl:优先淘汰最近要过期的数据

4.3 缓存预热和延迟加载

缓存预热:系统启动时提前将热点数据加载到缓存中,避免冷启动带来的性能问题。

延迟加载:缓存未命中时再访问数据库并写入缓存,常与旁路缓存结合使用。

五、缓存一致性问题

5.1 缓存一致性问题来源

在使用缓存的系统中,缓存与数据库的数据存在“非强一致性” 问题,即在某些时间点,缓存中的数据可能与数据库中的实际数据不一致,主要源自以下几个方面:

1. 非原子性更新导致数据不一致

缓存和数据库的更新是两个独立的操作,通常无法保证“原子性”,即无法在一个事务中同时完成。因此,在高并发或异常中断场景下,可能发生如下问题:

数据库已更新,但缓存仍为旧值;

缓存已更新,但数据库更新失败;

缓存和数据库分别被不同线程并发修改,导致更新顺序错乱。

2. 并发访问导致缓存脏读

在“缓存删除 + 数据库更新”模式下,若有并发请求在缓存被删除后、数据库尚未更新完成前,读取数据库旧值并重新写入缓存,可能会将旧数据错误写回缓存。

3. 缓存延迟更新引发短暂不一致

某些系统为了降低延迟,采用异步刷新缓存或延迟双删策略。虽然可以减少实时更新的性能开销,但在缓存与数据库之间仍会存在延迟窗口期,导致数据短时间不一致。

4. 异常或网络故障导致缓存更新失败

在实际部署中,网络异常、服务宕机、GC 卡顿等问题可能使缓存更新操作失败,尤其是在使用异步消息队列或延迟删除等策略时,可能导致缓存“漏删”或“漏更”。

5. 多级缓存或本地缓存未同步

在某些复杂架构中,系统会使用多级缓存(如本地缓存 + Redis 分布式缓存)。若这些缓存之间没有统一的失效机制或同步机制,就可能出现缓存层之间的不一致问题。

小结

缓存一致性问题的核心原因是缓存与数据库无法原子操作 + 缓存更新延迟或失败。虽然完全强一致性很难实现,但可以通过合理的策略(延迟双删、分布式锁、TTL、MQ 等)在性能与一致性之间取得工程上的平衡。

5.2 常见解决方案

缓存与数据库之间存在数据一致性问题的根源在于更新的非原子性,即更新过程中缓存和数据库无法同时成功或失败。以下是常见的应对策略:

方案一:延迟双删策略(Delayed Double Deletion)

更新流程:

删除缓存

更新数据库

延迟一定时间,再次删除缓存`

优点:

减少并发场景下缓存脏读的可能性;

弥补数据库更新慢导致的并发查询误缓存的问题。

缺点:

延迟时间难以把握,太短无效,太长影响性能;

依赖定时器或异步任务机制实现。

方案二:消息队列异步同步(Async Sync via MQ)

在数据库更新成功后,向消息队列(如 Kafka、RabbitMQ)发送通知,由消费者异步更新或清除缓存。

优点:

解耦服务,支持分布式系统;

可用于批量处理缓存更新,提高性能。

缺点:

实现复杂度高;

存在消息丢失、重复消费等一致性挑战。

适用场景:强一致性要求高的系统,如金融交易、库存系统等。

方案三:设置合理的过期时间(TTL 容错策略)

为缓存设置过期时间(如几分钟),即使出现短暂不一致,后续请求会自动回源数据库并回填缓存。

优点:

实现简单,开发成本低;

避免缓存长期脏读。

缺点:

无法立即反映数据库更新;

对数据敏感业务不适用。

适用场景:对一致性要求适中,如商品列表、资讯流等场景。

方案四:使用分布式锁或版本号控制并发写入

在更新缓存前,使用 Redis 分布式锁控制写操作;

或采用版本号机制,确保写入顺序一致性。

优点:

减少并发写冲突,保证数据顺序性;

适合热点数据并发更新场景。

缺点:

增加系统复杂度;

锁失效或竞争严重时可能影响性能。

适用场景:高并发场景下的用户信息、商品详情等核心数据。

5.3 缓存与数据库操作顺序分析

问题一:更新缓存还是删除缓存?

更新缓存(✗):

每次数据库变更都同步更新缓存;

如果没有读请求,则浪费性能,带来大量无效写操作。

删除缓存(✓):

更新数据库后直接删除缓存;

下次读请求时再加载最新数据到缓存中。

结论:推荐使用“删除缓存”方式更新缓存数据。

问题二:如何保证缓存和数据库更新操作的一致性?

单体系统:

可将缓存和数据库操作封装在一个事务中(如 Spring 的事务传播机制)。

分布式系统:

可采用 TCC、SAGA 等分布式事务方案;

或通过消息队列等方式实现最终一致性。

问题三:先操作缓存还是先操作数据库?

方法一:先删除缓存 → 再更新数据库

正常流程示意:

初始状态: 缓存 = 10,数据库 = 10

更新操作: 删除缓存 → 更新数据库为 20

最终状态: 缓存空,数据库 = 20,下次读取回填新值

异常情况:

线程 A:删除缓存

线程 B:查询缓存为空 → 读数据库(此时数据库尚未更新)→ 缓存回填旧值10

线程 A:更新数据库为 20

结果:缓存 = 10,数据库 = 20,数据不一致!

方法二:先更新数据库 → 再删除缓存

正常流程示意:

初始状态: 缓存 = 10,数据库 = 10

更新操作: 更新数据库为 20 → 删除缓存

最终状态: 缓存空,数据库 = 20,下次读取回填新值

异常情况:

线程 A:读取缓存为空,查询到数据库为10

线程 B:更新数据库为20 → 删除缓存

线程 A:将10写入缓存

结果:缓存 = 10,数据库 = 20

由于缓存操作速度快,一般情况下,A线程缓存写入操作不会过长导致中间还能塞下B的数据库操作+缓存操作,所以这种情况发生很少

最佳实践结论:

两种顺序都存在线程安全问题;

“先更新数据库,再删除缓存”方案更安全,因为缓存操作速度更快,先删缓存更容易造成并发读写错误;

建议结合 TTL 机制作为兜底,避免缓存长期不一致。

六、缓存穿透、击穿、雪崩

6.1 缓存穿透

概念

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

风险:由于数据不存在,情况下无法通过数据库里的数据给缓存添加值,所以每次请求都会打到数据库,当有大量此类请求时,数据库可能会瘫痪

产生原因

缓存穿透发生的常见原因有二:

误删:数据误删导致缓存和数据库都没有数据

恶意攻击:故意大量访问不存在的数据

常见解决方案

限制非法请求

在API入口对请求参数进行合理性判断:是否含非法值,字段是否存在

如果判断出是恶意请求就直接返回错误,避免进一步访问到缓存和数据库

缓存空对象或默认值

描述:

缓存空对象:在缓存中缓存null或默认值,避免请求打到数据库

优缺点:

优点:实现简单,维护方便

缺点:

额外的内存消耗(存储一堆乱七八糟的东西)

可能造成短期的不一致

通过设置TTL缓解

布隆过滤

描述:

使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在

具体实现流程:

布隆过滤器部署在​​应用层​​,在访问缓存层(Redis)之前进行拦截:

客户端请求 → 布隆过滤器 → 缓存层(Redis) → 数据库

​​请求到达应用层​​:当有查询请求到达时,首先检查布隆过滤器

​​布隆过滤器判断​​:

如果布隆过滤器说"不存在" → 直接返回空结果(拦截)

如果布隆过滤器说"可能存在" → 继续查询缓存

​​缓存查询​​:

缓存命中 → 返回结果

缓存未命中 → 查询数据库

​​数据库查询​​:

数据库有数据 → 回填缓存并返回

数据库无数据 → 可选择将空值也写入缓存(设置较短过期时间)

优缺点:

优点:内存占用少

缺点:

实现复杂

存在误判可能,但不会漏判(说存在不一定存在,说不存在一定不存在

其他方案

增强id的复杂度,避免被猜测id规律

做好数据的基础格式校验

加强用户权限校验

做好热点参数的限流

实践

上图展示了一个查询商铺功能解决缓存穿透的业务调整

核心思路:当被查询的数据不存在时,由直接返回异常变为了在缓存中写入空/默认对象

6.2 缓存击穿

概念

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击

上图展示的是由于缓存重建过程较长,期间大量请求到来时导致的数据库压力增加

常见解决方案

互斥锁

互斥锁方案:保证同一时间只有一个业务更新缓存。

未能获取互斥锁的请求。要么等待所释放后重新读取缓存,要么就返回空值或默认值

最好设置超时时间,避免获得锁的请求因意外一直阻塞而不能释放锁导致系统无响应

逻辑过期

即不给热点数据设置过期时间(TTL),将只是将过期时间存到VALUE里

一般用于热点key,例如活动期间加入该key,活动结束后再移除

KEY

VALUE

vks:game:1

解决方案对比

解决方案

优点

缺点

互斥锁

- 没有额外的内存消耗- 保持一致性- 实现简单

- 线程需要等待,性能受影响- 可能有死锁问题

逻辑过期

- 线程无需等待,性能较好

- 不保证一致性- 有额外内存消耗- 实现复杂

互斥锁:一致性

逻辑过期:可用性

实践

基于互斥锁

基于互斥锁方式解决缓存击穿问题

锁的设计

利用string的setnx

127.0.0.1:6379> help setnx

SETNX key value

summary: Set the value of a key, only if the key does not exist

since: 1.0.0

group: string

获取锁:setnx lock 1

set时需要设置有效期,避免没有正常执行释放锁的操作导致的问题

删除锁:del lock

基于逻辑过期

基于逻辑过期方式解决缓存击穿问题

6.3 缓存雪崩

概念

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力

解决方案

从概念上可以看出,缓存雪崩主要有两个原因:

大量数据同时过期

Redis故障宕机

大量数据同时过期

均匀设置过期时间

缓存预热时,会批量导入数据,这时可能大量数据有效期一致。

我们可以在设置过期时间时,给不同的Key的TTL添加随机值,从而减少同一时间有大量数据过期发生的可能性

互斥锁

类似上文解决缓存击穿的解决思路,当发现数据不在Redis中,就加个互斥锁,获得锁的线程构建缓存

后台更新缓存

不给缓存设置有效期,让缓存“永久有效”,将更新缓存的工作交由后台线程定时更新。

如何解决内存紧张时,缓存数据被淘汰,业务线程读取缓存失败的问题

方法一:后台线程除了更新缓存外,还要负责频繁地检测缓存是否有效,缓存失效时马上重建缓存。

从失效到被发现进而重构会有时间间隔,用户体验不好

方法二:发现缓存失效后通过消息队列发送一条消息通知后台线程更新缓存,后台线程收到消息后,判断缓存是否存在(期间可能有其他线程完成了更新),如不存在则重构缓存

实时性更高,用户体验相对较好

缓存预热:业务上线前提前把数据缓存起来,减少缓存失效的可能,不要等着用户访问才触发重构

Redis故障宕机

服务熔断或请求限流机制:

启用服务熔断机制,暂停业务应用对缓存服务的访问,直接返回错误。

为了减少对业务的影响,可以启用请求限流机制,只将少部分请求发送到数据库进行处理,再多的请求就在入口直接拒绝服务。

构建Redis缓存高可靠集群

相关推荐

儿童电动牙刷怎么选?一文揭秘五大好物优劣
365bet提款到账时间

儿童电动牙刷怎么选?一文揭秘五大好物优劣

📅 08-01 👁️ 3032
支付宝周周乐在哪里看
365bet提款到账时间

支付宝周周乐在哪里看

📅 07-08 👁️ 6989
如何进行反激活操作?
365提款会被冻结卡吗

如何进行反激活操作?

📅 11-02 👁️ 4276