Redis 典型应用:缓存机制与常见问题
Redis 最常见、也最有代表性的应用场景之一就是做缓存。缓存并不是 Redis 独有的概念,而是计算机系统中非常经典的一种性能优化手段。它的核心思想很简单:把经常访问的数据放到离使用者更近、访问速度更快的位置,从而减少访问慢速存储或远程服务的次数。
9.1 什么是缓存
缓存的本质,是用更快的存储空间保存一部分高频访问的数据。
可以把缓存理解成"随手可取"的位置。比如坐高铁时,身份证原本可以放在行李箱里,但进站、检票、乘车、出站都可能要反复刷身份证。如果每次都打开行李箱去找,就很麻烦。更好的做法是把身份证临时放进口袋里,需要时直接拿出来。这里的"口袋"就是"行李箱"的缓存。
在计算机系统中也类似。不同硬件或访问路径的速度通常可以粗略理解为:
text
CPU 寄存器 > 内存 > 硬盘 > 网络
因此,内存可以作为硬盘的缓存,硬盘也可以作为网络数据的缓存。访问越快的位置,通常成本越高、容量越小,所以缓存不可能保存所有数据。实际系统中,缓存通常只保存热点数据,也就是那些访问频率较高的数据。
这也对应了常说的"二八定律":少量热点数据往往能覆盖大部分访问请求。虽然具体比例不一定总是 20% 和 80%,但思想是一样的:只要把最常访问的一部分数据缓存起来,就能显著提升系统整体性能。
9.2 使用 Redis 作为缓存
在 Web 项目中,核心数据通常会存储在 MySQL 等关系型数据库中。关系型数据库功能强大,支持复杂查询、事务、约束和索引,但它也有明显的性能成本。
关系型数据库性能压力较大的原因主要包括:
- 数据主要存储在硬盘上,硬盘 IO 速度比内存慢,随机 IO 尤其明显。
- 如果查询没有命中索引,就可能发生全表扫描,进一步增加 IO 成本。
- SQL 执行前需要经过解析、校验、优化等步骤。
- 复杂查询,例如多表联合查询,会带来更高的计算和执行成本。
当访问并发量升高时,每个请求都会消耗 CPU、内存、磁盘 IO、网络带宽等资源。如果大量请求直接打到数据库,数据库很容易成为系统瓶颈,严重时甚至可能宕机。
提升数据库承载能力通常有两个方向:
- 开源:增加机器,部署更多数据库实例,例如主从复制、分库分表等。
- 节流:引入缓存,让热点数据优先从缓存读取,减少直接访问数据库的请求数量。
Redis 适合做缓存,主要是因为它的数据存储在内存中,访问速度远高于磁盘数据库;同时 Redis 采用 key-value 形式组织数据,操作模型相对简单,处理同样一次查询请求时通常消耗更少资源,能够支撑更高并发。
使用 Redis 作为缓存时,典型查询流程如下:
- 客户端向业务服务器发起查询请求。
- 业务服务器先查询 Redis。
- 如果 Redis 中存在目标数据,直接返回结果。
- 如果 Redis 中不存在目标数据,再查询 MySQL。
- 查询 MySQL 后,可以把结果写入 Redis,方便后续请求命中缓存。
这样一来,Redis 就像挡在 MySQL 前面的一层保护。大量热点请求会被 Redis 承接,只有缓存未命中的请求才会继续访问数据库。
需要注意的是,缓存主要优化的是读操作。对于写操作,最终仍然要以数据库为准,不能简单地认为加了缓存就能提升所有场景下的性能。
9.3 缓存的更新策略
既然缓存空间有限,就必须考虑一个问题:哪些数据应该进入缓存?
课件中介绍了两类常见思路:定期生成和实时生成。
1. 定期生成热点数据
定期生成的做法,是每隔一段时间统计访问频次,然后挑选访问量最高的一批数据放入缓存。
例如搜索引擎可以记录用户搜索的关键词,再通过日志统计出一段时间内的高频搜索词。对于这些高频词,系统可以提前把对应结果放入缓存。这样用户再次搜索时,就可以更快返回结果。
这种方案的优点是思路清晰,适合有明显访问统计周期的业务。缺点是实时性较弱,对突发热点不够敏感。比如某个节日、新闻或活动突然出现时,短时间内会产生新的热点,而定期统计可能无法立刻反映这种变化。
2. 实时生成热点数据
实时生成的做法更加动态。系统可以先给 Redis 设置容量上限,例如通过 maxmemory 控制最大内存。当用户查询数据时:
- 如果 Redis 命中,直接返回。
- 如果 Redis 未命中,就查询数据库。
- 查询数据库后,把结果写入 Redis。
- 如果 Redis 空间已满,就根据淘汰策略删除一部分旧数据。
持续运行一段时间后,Redis 中自然会留下更常被访问的数据。也就是说,热点数据不是提前算好的,而是在访问过程中逐渐沉淀出来的。
3. 常见缓存淘汰策略
当缓存空间不足时,就需要决定淘汰哪些数据。常见策略包括:
| 策略 | 含义 | 淘汰对象 |
|---|---|---|
| FIFO | First In First Out,先进先出 | 最早进入缓存的数据 |
| LRU | Least Recently Used,最近最少使用 | 最久没有被访问的数据 |
| LFU | Least Frequently Used,最不经常使用 | 一段时间内访问次数最少的数据 |
| Random | 随机淘汰 | 随机选择某些数据 |
Redis 本身也提供了多种内置淘汰策略,主要可以分为"只从设置了过期时间的 key 中淘汰"和"从所有 key 中淘汰"两类:
| Redis 策略 | 说明 |
|---|---|
volatile-lru |
从设置了过期时间的 key 中,使用 LRU 淘汰 |
allkeys-lru |
从所有 key 中,使用 LRU 淘汰 |
volatile-lfu |
从设置了过期时间的 key 中,使用 LFU 淘汰 |
allkeys-lfu |
从所有 key 中,使用 LFU 淘汰 |
volatile-random |
从设置了过期时间的 key 中随机淘汰 |
allkeys-random |
从所有 key 中随机淘汰 |
volatile-ttl |
从设置了过期时间的 key 中,优先淘汰更早过期的 key |
noeviction |
默认策略,内存不足时新写入直接报错 |
这些策略没有绝对好坏,关键要结合业务特点选择。如果缓存中所有数据都可以被替换,allkeys-lru 或 allkeys-lfu 往往更符合缓存场景;如果只有部分 key 适合被淘汰,则可以选择 volatile-* 系列策略。
9.4 缓存预热、缓存穿透、缓存雪崩和缓存击穿
引入缓存之后,系统读性能通常会明显提升,但缓存层也会带来一些新的问题。课件中重点介绍了缓存预热、缓存穿透、缓存雪崩和缓存击穿。
1. 缓存预热
缓存预热指的是在系统刚启动、Redis 刚恢复,或者大量缓存失效之后,提前把热点数据加载到 Redis 中。
如果 Redis 刚启动时里面没有任何缓存数据,那么原本应该由 Redis 承接的请求会直接访问 MySQL。对于高并发系统来说,这可能会让数据库突然承受很大压力。
缓存预热的做法就是提前准备一批热点数据,并主动写入 Redis。热点数据可以来自历史访问统计,不一定要求完全准确,只要能挡住大部分高频请求,就能让系统平稳度过启动阶段。随着系统持续运行,缓存内容也会逐渐根据真实访问情况自动调整。
2. 缓存穿透
缓存穿透指的是:请求查询的 key 在 Redis 中不存在,在数据库中也不存在。
由于数据库也查不到结果,这类 key 往往不会被写入缓存。如果后续继续频繁访问同样的非法 key,每次都会绕过 Redis 访问数据库,导致数据库压力增大。
缓存穿透常见原因包括:
- 业务设计不合理,缺少必要的参数校验。
- 开发或运维误操作,导致数据库中的部分数据被误删。
- 恶意攻击者不断请求不存在的数据。
常见解决方案有三种:
- 对查询参数做严格校验,例如手机号、用户 ID、商品 ID 等必须符合合法格式。
- 对数据库中也不存在的 key,在 Redis 中缓存一个空值,避免同一个 key 反复打到数据库。
- 使用布隆过滤器,在访问 Redis 和数据库之前,先判断 key 是否可能存在。
布隆过滤器可以用较小空间判断一个元素是否可能存在。它不能百分百确认"存在",但可以非常高效地判断"肯定不存在",因此适合用来拦截大量非法 key。
3. 缓存雪崩
缓存雪崩指的是:短时间内大量缓存 key 同时失效,导致大量请求突然打到数据库,造成数据库压力骤增,甚至宕机。
Redis 原本承担了保护数据库的作用。如果这层保护突然大面积失效,MySQL 就会从"只处理少量未命中请求"变成"直接承接大量请求",风险非常高。
缓存雪崩常见原因包括:
- Redis 实例故障。
- 大量 key 设置了相同或接近的过期时间,导致同一时间集中失效。
常见解决方案包括:
- 部署高可用 Redis 架构,例如主从、哨兵或集群,并完善监控报警。
- 不给部分核心热点 key 设置过期时间,或者由后台任务主动更新。
- 给 key 设置过期时间时加入随机因子,避免大量 key 在同一时刻过期。
4. 缓存击穿
缓存击穿可以看作缓存雪崩的一种特殊情况。它不是大量 key 同时失效,而是某一个热点 key 突然失效。由于这个 key 的访问量非常高,一旦它从缓存中消失,大量请求会同时访问数据库,也可能把数据库打垮。
例如某个热门商品、热门文章、秒杀活动信息,都可能成为热点 key。如果这个 key 过期的一瞬间有大量并发请求进来,就会形成缓存击穿。
常见解决方案包括:
- 通过统计发现热点 key,并设置为不过期,或者采用后台异步刷新机制。
- 使用互斥锁或分布式锁,限制同一时间只有一个请求去数据库加载数据,其余请求等待或返回降级结果。
- 对热点接口做服务降级,例如返回旧数据、默认数据或提示稍后再试。
总结
Redis 作为缓存,本质上是用内存中的热点数据减少对数据库的直接访问,从而提升读性能、降低数据库压力。使用 Redis 缓存时,需要重点理解以下内容:
- 缓存适合保存热点数据,不适合无差别保存所有数据。
- Redis 做缓存时,一般先查缓存,未命中再查数据库。
- 缓存更新可以通过定期生成或实时生成完成。
- 缓存空间不足时,需要结合业务选择合适的淘汰策略。
- 缓存预热、穿透、雪崩和击穿都是实际开发中必须考虑的问题。
缓存的价值不仅在于"快",更在于它能保护后端数据库,让系统在高并发场景下更加稳定。真正用好 Redis 缓存,不只是会写 get 和 set,还要理解数据如何进入缓存、如何被淘汰,以及缓存异常时系统应该如何兜底。