1、缓存概念简述
缓存是应用程序中常用的一种优化技术,可以显著提高应用程序的性能。为了避免频繁的读写数据库操作,应用程序将数据存储在缓存中,当下次需要相同的数据时,直接从缓存中读取,而不是从数据库中读取。
2、缓存雪崩
缓存雪崩是指缓存中大量的数据同时失效或者缓存服务不可用,导致所有的请求都要直接请求数据库,数据库瞬间变得非常繁忙,甚至宕机。这种情况会引起整个系统的崩溃。
2.1、缓存失效
当缓存中的数据过期或者被清空时,如果没有及时的刷新缓存,那么下一次请求将会去请求数据库,由于此时请求非常之多,会导致数据库瞬间被打满,甚至直接宕机。接下来我们看一段代码来模拟缓存失效的情况:
public Object getData(String key) {
Object value = redis.get(key);
if (value == null) {
value = queryDataFromDB(key);
if (value != null) {
redis.setex(key, 60 * 5, value);
}
}
return value;
}
上面的代码中,如果缓存中没有数据,则需要去数据库中查询,然后将查询到的数据放入缓存中,缓存时间为5分钟。但是如果在这5分钟内,缓存中的数据被清空或者过期了,那么下一次请求又会去请求数据库,这时就会引起缓存雪崩。
2.2、缓存服务不可用
如果缓存服务不可用,请求会直接访问数据库,数据库的请求量瞬间就会变得非常巨大,导致数据库性能下降甚至宕机。
3、缓存击穿
缓存击穿是指一个不存在于缓存中的key,被并发请求时,这些请求会直接访问数据库,导致数据库瞬间变得非常繁忙,甚至宕机。
举个例子,假如我们要查询id为1的用户数据:
public User getUserById(Integer id) {
User user = redis.get("user:" + id);
if (user == null) {
user = queryUserFromDB(id);
if (user != null) {
redis.setex("user:" + id, 60 * 5, user);
}
}
return user;
}
当并发请求如果同时请求id为1的用户数据,那么由于缓存中没有这个key,每个请求都会去请求数据库,导致数据库压力剧增。
4、缓存穿透
缓存穿透是指一个不存在于缓存和数据库中的key,被并发请求时,每个请求都会去请求数据库,导致数据库压力剧增。
举个例子,当我们查询id为-1的用户数据时,由于缓存中和数据库中都没有这个key,因此每个请求都会去访问数据库:
public User getUserById(Integer id) {
User user = redis.get("user:" + id);
if (user == null) {
user = queryUserFromDB(id);
if (user != null) {
redis.setex("user:" + id, 60 * 5, user);
} else {
redis.setex("user:" + id, 60 * 1, null); // 防止缓存穿透,缓存一个空对象
}
}
return user;
}
为了避免缓存穿透,我们可以在缓存中缓存一个空对象,并设置一个较短的过期时间。这样就可以保护数据库不会遭受攻击,并且响应速度也会更快。
5、解决方案
为了避免缓存雪崩、缓存击穿和缓存穿透,我们可以采用以下几种方法:
5.1、缓存时间随机
为了避免缓存同时失效,可以让缓存时间随机。每个缓存对象的失效时间都在一个范围内随机,这样就可以避免缓存同时失效,从而降低数据库压力。
5.2、热点数据预热
热点数据是指访问量非常高的数据,如果这些数据没有被缓存,那么每次请求都需要访问数据库。因此,我们可以在应用程序启动时,预先将热点数据加载到缓存中,从而避免缓存失效带来的问题。
5.3、分布式锁
分布式锁可以避免缓存击穿的问题。当多个请求同时请求同一个不存在于缓存中的key时,可以使用分布式锁。只有一个请求能够获得锁,其他的请求会等待一段时间,直到获得锁为止。
5.4、布隆过滤器
布隆过滤器可以避免缓存穿透的问题。当一个key查找时,先使用布隆过滤器判断这个key是否存在,如果不存在,直接返回空,否则再去访问缓存或数据库。
6、总结
缓存是一种很好的优化技术,但是如果缓存失效或者缓存服务不可用,会引起缓存雪崩。如果缓存中没有数据,会引起缓存击穿。如果请求过来的key在缓存和数据库中都不存在,那么由于每个请求都要去访问数据库,会引起缓存穿透。为了避免这些问题的发生,我们可以采用一些方法来保护数据库和缓存系统,如缓存时间随机、热点数据预热、分布式锁、布隆过滤器等。