Redis可以作为缓存增加访问速度,这篇文章是简单说一说Redis作为缓存的一些问题
缓存更新策略
定期生成
服务器会把一段时间内用户访问的条目存放在磁盘文件中,然后一个程序会定期(一天、一个月等,这取决于业务)统计文件中的所有条目分别出现的个数并进行排序,最终挑出出现次数最多的前几个条目更新到缓存中。
实时生成
不同于定期生成更新策略会一下子把热点数据保存下来而剔除冷门数据。实时生成更新需要在在一段时间后才能让缓存中的条目几乎都是热点,从而达到”动态平衡“
如果缓存命中就直接从缓存中取数据;如果缓存没命中,就将对应数据加载到缓存中,直到缓存被用尽。当缓存用尽的时候就是用淘汰策略淘汰掉一些缓存条目并插入新的缓存条目。下面是一些淘汰策略:
- 先进先出(FIFO,First In First Out)
这是最直观的策略,思想类似于排队。它会记录每个键值对进入缓存的时间戳,淘汰时优先移除在缓存中存活时间最久的数据,也就是“先来后走”。但这种策略不考虑数据的实际热度,即使一个老数据被频繁访问,只要它来得早,依然可能被优先淘汰。 - 最近最少使用(LRU,Least Recently Used)
相较于 FIFO,LRU 更关注数据的“时效性”。它会为每个键维护一个最近访问时间戳,淘汰时选择最长时间未被访问的键。如果一个键很久没被读过,系统就认为它在短期内大概率也不会被访问,从而将其移除。 - 最不经常使用(LFU,Least Frequently Used)
LFU 侧重于统计数据的“访问频率”。它会记录每个键在最近一段时间内被访问的总次数,淘汰时优先移除访问次数最少的键。这种策略能很好地抵御“突发冷数据”的冲击,确保那些长期被高频访问的热点数据得以保留(因为即使一段时间一直访问某个关键词,之前的热点数据也不会因为一段时间不被访问而淘汰)。 - 随机淘汰(Random)
这是最简单粗暴的策略。Redis 会从当前所有键中随机抽取一个“幸运儿”进行淘汰。虽然毫无“智慧”可言,但它对 CPU 的消耗极小,在某些访问模式极其均匀的场景下,效率反而非常高。
redis内部也对标这些淘汰策略提供了相应策略:
- 基于 LRU(最近最少使用)算法
volatile-lru:仅在设置了过期时间(expire)的键中,使用 LRU 算法进行淘汰。如果过期键集合为空,则退化为noeviction行为。allkeys-lru:在所有键(无论是否设置过期时间)中,使用 LRU 算法进行淘汰。这是最常用的通用策略之一,适合大多数“冷热数据分明”的业务场景。
- 基于 LFU(最不经常使用)算法
volatile-lfu:仅在设置了过期时间的键中,根据最近一段时间的访问频次进行淘汰。allkeys-lfu:在所有键中,根据最近一段时间的访问频次进行淘汰。适合需要长期统计热点、且不希望突发流量(如秒杀)冲走历史高频数据的场景。
- 基于 Random(随机)算法
volatile-random:仅在设置了过期时间的键中,随机挑选一个进行淘汰。allkeys-random:在所有键中,随机挑选一个进行淘汰。如果数据访问分布非常均匀,无显著热点,此策略效率极高。
- 基于 TTL(生存时间)的近似 FIFO
volatile-ttl:仅在设置了过期时间的键中,挑选剩余存活时间(TTL)最短(即越早过期)的键优先淘汰。本质上可视为一种限定在过期键范围内的先进先出(FIFO)变种——因为它遵循“谁先到期,谁先被清理”的逻辑。
- 默认策略:禁止淘汰
noeviction:这是 Redis 的默认策略。当内存不足时,新写入操作(如 SET、LPUSH 等)会直接返回错误,但读操作(GET)和删除操作依然可以正常进行。该策略保证了数据不会被自动删除,但会中断写入服务,适合需要严格保证数据不丢失的核心业务,但需提前规划好内存扩容。
缓存预热和缓存雪崩
在使用 Redis 作为 MySQL 前置缓存的典型架构中,缓存层的“冷启动”或“集中失效”往往是系统压力的高危时刻。当 Redis 刚刚启动(缓存层为空)、或是redis突然崩掉了没有及时重启、或是大量 Key 在同一时间段集中过期时,Redis 内部相当于一片“真空”地带,无法拦截任何读请求。此时,原本应由缓存承担的流量会瞬间全部穿透到 MySQL,形成缓存雪崩的隐患,对数据库造成巨大的瞬时压力,甚至直接宕机。
为了解决这一问题,我们必须在系统面临高流量之前,主动将热点数据提前加载进 Redis。这个准备动作被称为缓存预热,并提高redis的可用性。
预热的核心在于“热点数据”的选择。这部分数据不必追求绝对精准,也无需涵盖全量冷数据。我们完全可以依据业务的历史访问日志、实时计数器或者离线统计任务,挖掘出近期访问频率最高的 Top N 数据。即使预热的数据集与当前实时流量存在少量偏差,只要它能拦截住绝大部分(例如 80% 以上)的读请求,就已经能为 MySQL 撑起有效的保护伞,防止数据库因突发流量而宕机
缓存穿透
在缓存架构中,除了缓存雪崩,还有一种更为棘手的情况——缓存穿透。
缓存穿透指的是:客户端请求的 key 在 Redis 缓存中不存在,同时在数据库(如 MySQL)中也不存在。由于缓存层无法命中,每次请求都会直接穿透缓存,打到数据库上。更严重的是,这样的 key 不会被写入缓存,因此后续任何对该 key 的重复请求,依然会重复绕过缓存,持续访问数据库。这会导致数据库瞬间承受大量无效查询,压力陡增,甚至可能因过载而宕机。
产生这种情况的原因通常有三类:
业务设计不合理:系统缺乏必要的参数校验环节。例如,本该是合法手机号的查询,却传入了乱码或空值,导致非法 key 被放行并查询数据库。
开发/运维误操作:在维护过程中,可能不小心物理删除了数据库中的某些数据。
恶意攻击或爬虫:攻击者利用系统漏洞,故意构造大量数据库中根本不存在的随机 key 发起请求,意图耗尽服务器资源。
针对上述问题,业界通常采用以下三种主流方案进行防御:
严格的合法性校验(前置拦截)
在业务入口处,对查询参数的格式、类型、长度进行强校验。例如,查询用户信息时,必须校验 ID 是否符合规范(如正整数或 UUID 格式);查询手机号时,必须符合正则表达式规则。将绝大多数非法请求在业务层直接拒绝,避免它们触及数据库。缓存空对象(兜底占位)
当发现某个 key 在数据库中不存在时,仍然将其存入 Redis,但将对应的 value 设置为一个空值(如空字符串""或序列化的空对象),并设置一个较短的过期时间(例如 5 分钟)。这样,后续对该 key 的重复访问会直接从缓存中获取空值,从而有效保护数据库。这种方法实现简单,但需注意:若短时间内大量随机 key 进来,会在缓存中堆积大量无用空键,建议配合过期时间使用。布隆过滤器(超前拦截)
在查询缓存之前,引入一个布隆过滤器(Bloom Filter)。该过滤器结合了哈希算法与位图(Bitmap)思想,能够使用极小的内存空间,以极高的效率判断一个 key “绝对不存在”或“可能存在”。我们可以在系统启动时,先将数据库中所有存在的 key 同步到布隆过滤器中。当请求到来时,先经由布隆过滤器判断:若过滤器判定该 key 不存在,则直接返回,连缓存层都不必查询;若判定“可能存在”,才放行进入后续的缓存-数据库查询流程。
策略选择建议:
参数校验是基础防线,几乎零成本,必须执行;
缓存空对象适用于 key 数量有限、重复访问较高的场景,简单有效;
布隆过滤器则适合应对海量随机 key 的恶意攻击,虽然实现稍复杂,但它能将绝大多数无效请求扼杀在“门外”,防护能力最强。
在实际工程中,通常会根据业务敏感度,组合使用上述两种甚至三种方案,以构建纵深防御体系。
缓存击穿
是缓存雪崩的特殊情况,指的是大批量热点数据过期导致一时间数据库因承担大量请求宕机。解决方法:
- 基于统计发现热点 Key,并设置逻辑永不过期
- 进行必要的服务降级(分布式锁限流)