Appearance
Redis 缓存问题
本文介绍在使用Redis作为缓存的过程中,会出现的问题,包括缓存预热、缓存雪崩、缓存穿透和缓存击穿。
1. 缓存预热
缓存预热是指在系统上线或服务重启后,以及在某些特定场景(如秒杀活动前)下,提前将热点数据加载到缓存中的过程。这样可以避免用户在首次访问时因缓存中没有数据而直接访问数据库,从而导致响应时间过长或数据库压力过大。
实现方式如下:
手动导入:手动导入一批热点数据到Redis。
启动时加载: 在应用启动时,通过程序加载预设的热点数据到 Redis。
定时任务: 设置定时任务,周期性地将数据库中的热点数据同步到 Redis。
异步加载: 用户访问时,如果缓存中没有数据,除了从数据库加载并回填缓存外,还可以触发一个异步任务来预加载相关数据。
数据分析: 通过对历史访问日志进行分析,找出热点数据并进行预热。
2. 缓存雪崩
缓存雪崩是指在某个时间段内,缓存中大量的 Key 集中失效,导致这些请求全部打到数据库上,从而在短时间内对数据库造成巨大的压力,甚至可能使数据库崩溃。
发生原因:
- 大量 Key 同时过期: 缓存中大量数据的过期时间设置相同,导致在同一时间点集中失效。
- Redis 服务宕机: Redis 服务因故障宕机,所有缓存数据丢失,导致所有请求直接打到数据库。
解决方案:
- 过期时间加随机值: 在设置缓存 Key 的过期时间时,在基础过期时间上加上一个小的随机值,使得 Key 的过期时间错开,避免大量 Key 同时失效。
- 高可用架构: 搭建 Redis 集群(如主从、哨兵、集群模式),提高 Redis 服务的可用性,避免单点故障。
- 限流降级: 当数据库压力过大时,可以采取限流措施,限制对数据库的访问频率,或者进行服务降级,牺牲一部分功能以保证核心功能的可用性。
- 多级缓存: 引入多级缓存机制(如本地缓存 + Redis 缓存),当 Redis 失效时,还有本地缓存作为备用。
- 熔断: 当数据库压力达到一定阈值时,直接拒绝部分请求,保护数据库。
3. 缓存穿透
缓存穿透是指用户请求的数据在缓存中和数据库中都不存在,导致每次请求都会绕过缓存直接访问数据库。如果恶意攻击者不断请求大量不存在的数据,可能会造成数据库压力过大,甚至压垮数据库。
解决方案如下:
缓存空值:如果从数据库查询的结果为空,也将这个空结果缓存起来(设置一个较短的过期时间)。这样,下次请求相同的数据时,就会直接从缓存中获取空值,而不会再次访问数据库。
但是,缓存空值的方法仍有缺陷:如果恶意攻击者不断发送不同的请求,例如,每次请求的数据ID+1,这样仍然会直接查询数据库,而且会造成缓存中垃圾数据过多。
布隆过滤器:在数据写入缓存和数据库时,同时将该数据的 K-V 对的 Key 存入布隆过滤器。用户请求时,先通过布隆过滤器判断 Key 是否存在。如果布隆过滤器判断不存在,则直接返回空,避免访问数据库。布隆过滤器有误判率,可能会将不存在的 Key 判断为存在,但不会将存在的 Key 判断为不存在。
使用布隆过滤器来解决缓存穿透的关键在于,将数据放入缓存时,也要同步放入布隆过滤器。
但是,布隆过滤器不支持删除操作,因此,键过期、删除键等操作需要另行考虑。
也可以考虑使用Cuckoo Filter(杜鹃过滤器),该过滤器支持删除操作。
4. 缓存击穿
缓存击穿是指某个热点 Key(例如某个商品的详情页数据)在缓存中过期了,此时有大量的并发请求同时访问这个 Key。这些请求会同时发现缓存中没有数据,于是都去查询数据库,导致数据库在短时间内面临巨大的压力。
缓存击穿与缓存穿透的区别:
- 缓存穿透: 请求的数据一直都不存在缓存和数据库中。
- 缓存击穿: 请求的数据是热点数据,并且曾经存在于缓存中,只是现在过期了。
解决方案:
互斥锁 (Mutex Lock): 当第一个请求发现缓存中没有数据时,它会获取一个分布式锁。其他并发请求在获取锁失败后,会等待或者直接从缓存中获取(如果第一个请求已经将数据回填到缓存)。当第一个请求从数据库加载完数据并回填到缓存后,释放锁。
永不过期: 对于一些访问频率极高且数据变化不大的热点数据,可以考虑将其设置为永不过期。但是,这意味着数据更新时需要主动更新缓存。
多级缓存:对于某个热点Key,为了防止到期失效可以设置多级缓存,假设设置两级缓存,主缓存和副缓存。
主缓存 (Primary Cache):通常是 Redis 或其他内存数据库,用于存储最新的、实时性要求高的数据。过期时间可以设置为正常业务需求的时间。
副缓存 (Secondary Cache):可以也是 Redis,或者一个持久化存储(如硬盘上的文件、本地内存缓存等),用于存储主缓存失效后仍然可用的数据。它的过期时间通常比主缓存长很多,甚至可以设置为“永不过期”(但需要手动更新)。
当查询时,如果主缓存查询不到,则查询副缓存,并且,查询副缓存时异步查询数据库,更新缓存。
这种方案适用于非强一致性场景,如果需要强一致性,则需要复杂的同步机制。