Redis缓存
缓存问题 ⭐⭐⭐⭐⭐ 缓存三剑客,重点!!!
缓存穿透,原因及解决方案(布隆过滤器、空值缓存等)
缓存击穿,原因及解决方案(互斥锁、热点数据永不过期等)
缓存雪崩,原因及解决方案(过期时间随机化、集群部署、热点数据永不过期等)
存储数据的统一性问题(更新策略:先更新数据库再删除缓存,延迟双删等)
缓存的更新策略(LRU、LFU 等)及适用场景
缓存穿透、缓存击穿、缓存雪崩
缓存穿透
缓存穿透:指查询不存在的数据,导致请求直接穿透缓存访问数据库,压垮数据库。
解决方案
缓存空值:对不存在的 key 缓存空值(设置较短过期时间),避免重复穿透。
布隆过滤器:预先将存在的 key 存入布隆过滤器,请求时先校验,不存在则直接返回。
接口限流与校验:对恶意高频请求限流,校验请求参数合法性。
缓存击穿
缓存击穿:请求的 key 对应的是热点数据,该数据存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期)。大量的请求无法从缓存中读取,直接打到了数据库上,数据库被高并发的请求冲垮。
解决方案
互斥锁(分布式锁):缓存失效时,仅一个线程能获取分布式锁(如 Redisson 锁),该线程负责查询数据库并重建缓存,其他线程等待锁释放后直接查缓存,避免同时访问数据库。
逻辑过期:
(1)在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前key设置过期时间。
(2)当查询的时候,从redis取出数据后判断时间是否过期,若未过期,直接返回缓存数据。
(3)若已过期,异步启动线程重建缓存(重建时加分布式锁,避免重复重建),当前线程正常返回旧数据。
热点数据永不过期
(1)不给热点数据设置过期时间,由后台异步更新缓存。
(2)在热点数据快要过期之前,提前通知后台线程要更新缓存并重新设置过期时间。
缓存雪崩
缓存雪崩:指大量缓存key同时过期或Redis 集群故障,导致大量请求集中到达数据库。
解决方案:
过期时间随机化:为不同 key 设置随机过期时间,避免同时失效。
缓存集群高可用:部署 Redis 主从 + 哨兵或集群模式,避免单点故障。
预热缓存:提前加载热点数据到缓存,并设置合理的过期时间,避免冷启动时的缓存缺失。
Redis 集群:采用 Redis 集群,搭建 Redis 哨兵(Sentinel)或 集群(Cluster)模式,防止单点故障导致整个缓存服务不可用。Redis缓存的主节点故障宕机时,从节点可以切换成为主节点,继续提供缓存服务,也就是通过主从节点的方式构建了Redis缓存高可靠集群。
多级缓存:设置多级缓存,例如本地缓存+Redis 缓存的二级缓存组合,当 Redis 缓存出现问题时,还可以从本地缓存中获取到部分数据。
先修改数据库,再删除缓存
数据一致性:
先修改数据库,再删除缓存是为了确保即使缓存被删除之后,数据库中依然存有最新的数据。如果先删除缓存,再修改数据库,可能会造成短时间内的数据不一致:在删除缓存后,缓存中没有数据,数据库也没有最新的数据时,系统可能会出现查询失败或者使用到旧数据的情况。
避免缓存穿透:
如果在修改数据库之前删除了缓存,在高并发的情况下,用户可能会触发大量查询请求,因为缓存中没有数据。所有的请求都会直接访问数据库,从而可能会导致数据库压力激增。先修改数据库后删除缓存,确保数据库更新后,再去清理缓存,避免缓存穿透。
缓存击穿和不一致性:
如果修改数据库后删除缓存,下一次查询会从数据库中获取数据,并将新的数据重新加载到缓存中,这样能保证缓存和数据库中的数据一致。
常见流程:
修改数据库:先将数据更新到数据库中,确保数据存储是正确的。
删除缓存:数据修改完成后,删除缓存中的对应条目,防止下次请求仍然读取到过期数据。
重新加载缓存:可以选择在删除缓存之后立即重新将数据加载到缓存中,以提高后续请求的速度。
如果先删除缓存再修改数据库的风险:
可能导致缓存不一致:如果在删除缓存后,修改数据库失败或者延迟,可能导致缓存中没有数据而数据库也没有更新。
性能问题:如果缓存为空,下一次查询将直接访问数据库,可能会对数据库造成较大的压力,尤其是在高并发的情况下。
一般来说,先修改数据库,再删除缓存是一个更安全且高效的方案,有助于确保数据一致性,避免缓存穿透和不一致的情况。如果操作有复杂的业务逻辑,甚至可以使用分布式锁来保证操作的原子性,进一步避免并发问题。
延时双删
在 Redis 中,延时双删是一种用于解决缓存一致性问题的策略,通常与缓存过期的场景相关。这个策略的目的是为了确保缓存中的数据被正确删除,同时避免因缓存和数据库之间的不一致而导致的读取错误。
原理:
第一步:删除缓存当数据被修改或删除时,首先删除缓存中的数据。这样可以确保后续的请求不会从缓存中读取到过期或错误的数据。
第二步:等待一段时间删除缓存后,程序会等待一个短暂的时间,这个时间窗口可以让其他正在执行的请求正常通过,防止因为缓存的删除导致对数据库的重复操作。
第三步:再次删除缓存在等待一定时间后,程序再次检查缓存,如果缓存依然存在,则再执行一次删除操作。这一步是为了确保即使有其他请求未及时删除缓存,这个缓存也会在第二次删除时被清理。
为什么使用延时双删?
延时双删的主要目的是避免以下问题:
缓存穿透:用户请求的数据不在缓存中,导致直接访问数据库。
缓存雪崩:大量缓存失效,导致瞬间高并发请求数据库,造成数据库压力。
缓存击穿:缓存数据过期后,不同的请求同时访问数据库并更新缓存,可能会造成数据不一致。
通过延时双删机制,减少了在高并发情况下缓存未能及时删除的情况,从而提高了缓存一致性,避免了因缓存和数据库之间的不同步而产生的问题。
实际操作:
删除缓存:直接删除缓存中的数据。
延迟一段时间:假设 100 毫秒,这段时间内其他请求可能会重新加载缓存并修改数据。
再次删除缓存:在延迟后再次尝试删除缓存,确保缓存被清理。
延时双删的不足:
虽然延时双删可以解决大部分缓存一致性问题,但它并不是完美的。最主要的问题是它无法保证在缓存和数据库之间的一致性永远能够做到百分百的同步。比如在高并发的情况下,即使使用了延时双删,也有可能发生缓存没有及时删除或者删除时没有完全一致的情况。
总的来说,延时双删是一种在特定场景下能有效减少缓存问题的策略,尤其在分布式环境中很有用。
缓存淘汰策略
LRU:Least Recently Used
LRU:淘汰最近最少使用的数据。
如果一个数据最近被访问过,那么它未来短时间内再次被访问的概率也比较高。这就是时间局部性。
LRU 适合时间局部性明显的场景,实现常用 HashMap + 双向链表,优点是简单高效,缺点是对顺序扫描和稳定高频热点不够友好。
LFU:Least Frequently Used
LFU:淘汰访问频率最低的数据。
访问次数越多的数据,越可能是热点数据,应该保留。
LFU 适合热点数据相对稳定,某些数据长期高频访问。
Cache Aside
Cache Aside(旁路缓存):应用程序主动管理缓存的读写操作。
Cache Aside 的读流程:首先尝试从缓存中获取数据,如果命中缓存直接返回;如果未命中,则从数据库获取数据,并将结果回填到缓存中。
Cache Aside 的写流程:首先更新数据库,然后删除对应的缓存。
如何解决短暂不一致性和缓存击穿问题
缓存重试机制:
在删除缓存或更新缓存时,增加重试机制,确保缓存被正确更新。
异步更新:
使用队列或后台线程进行缓存更新,避免直接阻塞主进程,减少对性能的影响。
双删策略:
在更新数据库后,删除缓存两次,一次立即删除,第二次延迟删除,防止缓存数据被新请求重写。
缓存雪崩防护:
给缓存添加过期时间,并通过随机化过期时间来减少大量缓存同时过期的问题。
面试题
你在项目中怎么用的?
在项目中,通常采用的是Cache Aside +TTL+ 失效淘汰策略。
读请求先查 Redis,未命中就查数据库并回填缓存;写请求先更新数据库,再删除对应缓存。
对于缓存淘汰,底层一般依赖缓存系统提供的近似 LRU/LFU 机制;业务层还会配合 TTL,避免脏数据长期存在。
对热点 key,我们会做适当保护,比如加互斥锁、防止击穿;对大量 key 同时过期也会加随机过期时间,避免雪崩。
