天天看点

谈谈缓存穿透、缓存击穿、缓存雪崩及数据库与缓存的一致性

作者:IvanLan
在软件开发中,缓存是一种常见的技术手段,用于加速数据的读取和提高系统性能。但在使用缓存的时候,有些问题我们不得不考虑,其中典型的问题就是:缓存穿透、缓存雪崩、缓存击穿和与关系型数据库的一致性。它们可能导致缓存失效或缓存访问异常,从而影响系统的性能和稳定性。下面对这几种问题进行详细的介绍。

一、缓存穿透

  1. 概念

缓存穿透是指大量请求查询不存在的数据,导致缓存无法命中,从而导致请求直接访问后端数据库或其他数据存储系统,对系统造成过度压力。攻击者可以利用缓存穿透漏洞进行恶意攻击。

谈谈缓存穿透、缓存击穿、缓存雪崩及数据库与缓存的一致性

如上图所示,一般的缓存流程大致是先查缓存,如果key不存在或者key已经过期,再从数据库查询,如果查询结果不为空,则把结果写进缓存然后返回;如果数据库查询结果为空,则不放进缓存。

在一些特定场景,例如秒杀活动,同一时刻会有大量的请求,都在秒杀同一件产品,这些请求同时去查缓存中没有数据,然后直接访问数据库。数据库就有可能扛不住压力,直接挂掉。

也可能有人进行恶意攻击,拿一堆不存在产品ID去发起请求。这样就会每次都无法命中缓存而都去直接查询数据库,而每次查询都是空,都不会进行缓存。这样就会对数据库造成巨大的压力,直至压死数据库。

  1. 解决方案

(1)缓存空对象

如果查询结果为空也缓存起来,同时设置一个过期时间,这样就可以减小数据库的压力。不过这种方法有可能导致缓存了大量的空对象,如果空对象多会需要大量的内存。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;

@Service
public class MyService {
    
    @Autowired
    private CacheManager cacheManager;
    
    public Object getData(String key) {
        Cache cache = cacheManager.getCache("myCache");
        Object result = cache.get(key, Object.class);
        if (result == null) {
            // 数据不存在,进行查询
            result = myDatabase.getData(key);
            if (result != null) {
                // 将数据存入缓存,设置过期时间
                cache.put(key, result);
                cacheManager.getCache("myCache").expire(key, 60 * 5); // 5 分钟过期
            } else {
                // 数据不存在,将一个空对象存入缓存,设置较短的过期时间
                cache.put(key, new Object());
                cacheManager.getCache("myCache").expire(key, 60); // 1 分钟过期
            }
        }
        return result;
    }
}           

(2)布隆过滤器

布隆过滤器(Bloom Filter)是一种数据结构,用于判断一个元素是否在集合中存在。它最大的优点就是性能高,空间占用率极小。它的基本原理是,使用多个哈希函数对每个元素进行哈希计算,并将结果存储在一个位数组中。当需要判断一个元素是否在集合中存在时,将该元素进行相同的哈希计算,如果所有的位都为1,则认为该元素可能在集合中存在,否则可以确定该元素不存在于集合中。

我们可以提前将真实正确的产品ID,都添加到布隆过滤器中,当有查询请求过来时,先使用布隆过滤器判断请求中的key是否存在于布隆过滤器中,如果不存在,则可以直接返回,避免了对数据库的查询操作。如果存在于布隆过滤器中,则再从缓存中查询数据,如果缓存中不存在,则查询数据库,并将查询结果放入缓存中。

这样就可以避免因为大量不存在的key请求而导致缓存穿透的问题。但是需要注意的是,布隆过滤器也有一定的误判率,也就是说,存在一定概率将不存在的元素误判为存在,因此在使用布隆过滤器时需要权衡误判率和内存占用等因素。

(3)接口层校验

可以接口层进行校验,对产品ID之类规则做基础的校验,判断出非法的产品ID请求。

二、缓存击穿

  1. 概念

缓存击穿是指大量请求查询同一个热点数据,但这时缓存中没有这个数据而数据库中有这个数据,大并发就穿破缓存,直接请求数据库,对后端系统造成过度压力,从而影响系统性能。

  1. 解决方案

(1)使用分布式锁或使用互斥锁机制,避免多个线程同时访问数据库缓存数据。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;

@Service
public class MyService {
    
    @Autowired
    private CacheManager cacheManager;
    
    public Object getData(String key) {
        Cache cache = cacheManager.getCache("myCache");
        Object result = cache.get(key, Object.class);
        if (result == null) {
            // 数据不存在,加锁查询数据库
            synchronized (this) {
                result = cache.get(key, Object.class);
                if (result == null) {
                    // 查询数据库
                    result = myDatabase.getData(key);
                    if (result != null) {
                        // 将数据存入缓存,设置过期时间
                        cache.put(key, result);
                        cacheManager.getCache("myCache").expire(key, 60 * 5); // 5 分钟过期
                    } else {
                        // 数据不存在,将一个空对象存入缓存,设置较短的过期时间
                        cache.put(key, new Object());
                        cacheManager.getCache("myCache").expire(key, 60); // 1 分钟过期
                    }
                }
            }
        }
        return result;
    }
}           

(2)设置热点数据永不过期

这个方法就比较粗暴,如果热点数据要求实时性比较低,可以通过定时任务按一定频率将数据库数据刷新到缓存。

三、缓存雪崩

  1. 概念

缓存雪崩是指缓存中的大量数据在同一时间段内过期失效,导致请求无法命中缓存,全部走数据库,从而对后端数据存储系统造成过度压力,甚至导致系统瘫痪。和缓存击穿不同的是,缓存雪崩是不同的数据都过期了,缓存击穿指同一条数据。

  1. 解决方案

(1)合理设置缓存的过期时间,防止同一时间大量缓存数据同时失效。

  • 可以加上一个随机因子,尽可能分散缓存过期时间。
  • 设置热点数据永不过期。

(2)使用多级缓存或冷热数据分离

如热门产品缓存时间长一些,冷门产品缓存时间短一些,也能节省缓存服务的资源。

(3)使用分布式缓存

如Redis,可将热点数据均匀分布在不同的缓存节点中。

(4)熔断、降级、限流

当访问次数急剧增加导致服务出现问题时,通过熔断、降级、限流三个手段来降低雪崩发生后的损失。只要确保数据库不死,系统总可以响应请求。

四、数据库与缓存一致性

在使用缓存时,保持数据库和缓存之间的一致性是非常重要的。否则,如果缓存中的数据与数据库中的数据不一致,就会导致数据的不准确。

所以我们在更新数据库中的数据时,同时更新缓存中的数据。这样,下一次查询缓存时,就会从缓存中获取最新的数据。那么更新缓存我们要注意什么呢?这里有两个问题!

  1. 更新缓存时我们应该选择删除缓存呢,还是更新缓存?

先看个例子:

谈谈缓存穿透、缓存击穿、缓存雪崩及数据库与缓存的一致性

如图所示:

(1)线程A先发起更新数据库。

(2)线程B再发起一个更新数据库。

(3)线程B先更新了缓存。

(4)线程A更新缓存。

这时候,缓存保存的是A的数据(老数据),数据库保存的是B的数据(新数据),数据就不一致,出现了脏数据。如果是删除缓存取代更新缓存则不会出现这个脏数据问题。

  1. 更新数据时,先操作数据库还是先操作缓存?

再看一下例子:

谈谈缓存穿透、缓存击穿、缓存雪崩及数据库与缓存的一致性

(1)线程A发起更新数据库,先delete cache。

(2)线程B发起一个读操作,miss cache。

(3)线程B从DB读取,读出来一个老数据。

(4)然后线程B把老数据set入cache。

(5)线程A写入DB最新的数据。

这时候,缓存保存的是老数据,数据库保存的是A的数据(新数据),数据就不一致,出现了脏数据。所以要选择先操作数据库而不是先操作缓存。

继续阅读