在我们的项目中多少都会使用缓存,因为有些数据我们没有必要每次都去查询数据库,特别是高QPS的系统,每次都去查询数据库会影响数据库性能。
业务系统一般调用流程:
![](https://i-blog.csdnimg.cn/blog_migrate/7b6dda20537bb5acd52780c66cc446e6.png)
我们一般的做法是先从缓存中查询,如果缓存中查询到了则直接返回,如果缓存中没有,则查询数据库,如果从数据库中查询到了,则先将数据写入缓存,然后返回数据给调用方。
public Result<Post> queryPostFromCache(Long id) {
// POST:id
final String key = Constant.Cache.POST + Constant.COLON + id;
// 从缓存中查询
Object post = cacheService.get(key);
if(post != null) {
return Result.success((Post) post);
}
// 从数据库中查询
post = postMapper.queryPostById(id);
if(post != null) {
cacheService.set(key, post, Duration.ofHours(1));
}
return Result.success((Post) post);
}
正常情况下,我们查询的数据都是存在的,比如从文章列表页面跳转到文章详情页查询文章详细信息,此时根据文章ID查询,文章的ID是真实存在的。不正常的情况是如果请求查询的文章ID是一条根本不存在的数据,也就是说缓存和数据库中都不会有值,这会导致请求每次都会到数据库中查询。这种查询不存在数据的现象我们称之为缓存穿透
。
缓存穿透
是指用户查询的数据在数据库中没有,那么在缓存中也不会有,也就是说查询的是一条根本不存在的记录
。这样就会导致用户首先在缓存中找不到 ,则每次都要去数据库再查询一遍,然后返回空。这样就相当于进行了两次无用的查询。要是有人利用这种不存在的key频繁攻击我们的系统,很可能导致数据库压力增大,甚至导致数据库挂掉。
解决方案
1.将未在数据库中查询到值的key也写入缓存,缓存的值为空对象,同时设置一个较短的过期时间,以防止后面真的有数据。
2.采用布隆过滤器(Bloom Filter),使用一个足够大的bitmap,用于存储数据库中可能访问的key。布隆过滤器可以理解为一个不太精确的set集合,当你使用它的contains方法判断某个对象是否存在时,它可能会误判。它的特点是当布隆过滤器判断某个值存在时,这个值可能不存在(误判),当它判断某个值不存在时,那就肯定不存在。这个特点可以用在判断要查询的key是否存在,如果判断不存在时,那就肯定不存在,这个时候就不用再查询数据库了,直接返回给调用方值不存在。
使用空对象解决缓存穿透问题的示例代码
public Result<Post> queryPostFromCache(Long id) {
// POST:id
final String key = Constant.Cache.POST + Constant.COLON + id;
// 从缓存中查询
Object post = cacheService.get(key);
if(post != null) {
// 判断缓存中的值是否为默认对象
if(post instanceof NullObjectValue) {
return Result.fail(ResultCode.RESOURCES_NOT_FOUND);
}
return Result.success((Post) post);
}
// 从数据库中查询
post = postMapper.queryPostById(id);
if(post != null) {
cacheService.set(key, post, Duration.ofHours(1));
}else {
// 数据库中没有对应记录,给这个key缓存设置一个默认值
cacheService.set(key, new NullObjectValue(), Duration.ofHours(1));
}
return Result.success((Post) post);
}
有关使用布隆过滤器来解决缓存穿透问题的具体实现,我将会在下篇文章中介绍。