深入理解 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缓存高可靠集群

浙公网安备 33010602011771号