上一节还在为缓存头疼?来看看更优雅的缓存方案(一)中,搭建了一个简单的项目,介绍了SpringCache
几个常用的注解,回顾一下:
-
@Cacheable
缓存中没有则加入缓存,有则读取缓存 -
@CachePut
缓存中没有则加入,有则强制更新 -
@CacheEvit
从缓存中删除数据 -
@Caching
组合操作
其中@CacheEvit
的删除操作,在实际应用中,我们会碰到一个问题,就是有@CacheEvit
的函数,那么它是在函数执行之前还是之后去删除缓存呢?我们是否可以进行控制?来看一下源码
// IntelliJ API Decompiler stub source generated from a class file
// Implementation of methods is not available
package org.springframework.cache.annotation;
@java.lang.annotation.Target({java.lang.annotation.ElementType.TYPE, java.lang.annotation.ElementType.METHOD})
@java.lang.annotation.Retention(java.lang.annotation.RetentionPolicy.RUNTIME)
@java.lang.annotation.Inherited
@java.lang.annotation.Documented
public @interface CacheEvict {
@org.springframework.core.annotation.AliasFor("cacheNames")
java.lang.String[] value() default {};
@org.springframework.core.annotation.AliasFor("value")
java.lang.String[] cacheNames() default {};
java.lang.String key() default "";
java.lang.String keyGenerator() default "";
java.lang.String cacheManager() default "";
java.lang.String cacheResolver() default "";
java.lang.String condition() default "";
boolean allEntries() default false;
boolean beforeInvocation() default false;
}
可以看到,其中有一个beforeInvocation()
的布尔类型,默认为false
,意思就是说@CacheEvit 默认在函数执行后进行缓存的删除
,所以我们可以根据业务需要,选择在函数执行前/后删除缓存中的数据.
接着上节的内容来说,使用redis
经常碰到的缓存穿透
、缓存雪崩
、缓存击穿
究竟如何解决呢?先来看下他们的定义
-
缓存穿透
客户端大量/频繁查询一个/多个不存在的
key
,导致业务直接穿过缓存到达数据库
-
缓存雪崩
大量
key
同时过期,导致同一时间内请求都直接到数据库去取数据
-
缓存击穿
热点
key
过期,同一时间大量针对该key
的请求频繁到达数据库层
那么以上常见的三种问题解决方案分别是什么呢?
-
缓存穿透
对于空值进行缓存,即使从数据库查到的数据为空,那么也进行缓存,这样,即使大量/频繁的查询一个/多个
key
,也能直接命中缓存并且返回给前端
-
缓存雪崩
不同的
key
设置不同的或者随机的过期时间,避免key
在相同的时间大量过期.当然有时候可能弄巧成拙,比如采用随机的过期时间 + 最小过期时间 = 过期时间,那么由于业务发生的时间本身就是随机的,所以计算出来的过期时间就有可能为相同的值.所以,最终只要给key设置过期时间即可
-
缓存击穿
对于非强一致的数据,在单个微服务中加锁进行管控,在热点
key
过期的时候进行加锁,只允许第一个请求到达数据库层,查到结果放入缓存后,再允许其他的业务进入,有效避免大量请求同时到达数据库层.
对于强一致性数据,则需要增加分布式锁来进行管控,与单个服务加锁逻辑不同,所有微服务同一时间只允许一个业务进入.分布式锁可以考虑使用Redisson
进行加锁,后面也会给出实例.
SpringCache解决缓存穿透、缓存雪崩、缓存击穿
SpringCache
实际就已经考虑到了我们大多数的环境和问题,所以针对上面的问题,也有对应的解决方案,如何应用?来看下面的示例/配置
-
SpringCache解决缓存穿透
在还在为缓存头疼?来看看更优雅的缓存方案(一)中的配置文件中,有这样一段配置
spring: cache: redis: cache-null-values: true
从字面意思就可以看出,这里配置允许进行
null
缓存,当然也就能满足我们解决缓存穿透
的需求.
-
SpringCache解决缓存雪崩
上面其实已经说明了,解决缓存雪崩其实就是尽量为我们的缓存设置过期时间即可,配置文件中就默认了缓存的过期时间为
3600ms
,当然也可以在业>务层面进行自定义时间的设置spring: cache: redis: time-to-live: 3600000
-
SpringCache解决缓存击穿(使用绝大多数环境)
解决缓存击穿的思路就是在我们的服务中加锁,只需添加
sync=true
,在业务中就是@Cacheable(value = {"PMS_PRODUCT#120"},key = "'PRODUCT'",sync = true)
. 即可达到在函数执行前加锁,执行后解锁的功能. SpringCache虽然已经为我们实现了这个功能,但它并不是分布式锁,当我们部署了多个微服务的时候,只能保证单个微服务每次只通过一个业务请求,多个微服务还是存在多个请求同时到达数据库层的可能.不过,即使我们有几十几百个相同的微服务,同一时间也最多只会有几十上百个请求到达数据库层,对于数据库层的压力也并不大,所以说SpringCache在缓存击穿方面已经适用于绝大多数的环境了.
示例演示
对于缓存穿透、缓存雪崩仅需要配置文件即可,那么我们来演示一下SpringCache的防止缓存击穿效果
首先我们把CacheServiceImpl
中的getAllProducts
上的sync=true
去掉
@Cacheable(value = {"PMS_PRODUCT#120"},key = "'PRODUCT'")
@Override
public List<TestProduct> getAllProducts() {
System.out.println("查询缓存信息" + new Date().toString());
return getProductsFromDB(3);
}
来想一下,此时访问localhost:8000/test/getCache的时候,会直接到CacheServiceImpl
中的getAllProducts
的方法中,然后会从数据库中获取数据,耗时10s
,在10s
内,如果有新的请求到达服务器,那么由于没有添加任何锁,所以第二个请求也会去数据库中查询数据,耗时10s
,也就是我们所说的缓存穿透
首先访问localhost:8000/test/getCache,然后在10s
内再次访问localhost:8000/test/getCache
来看一下控制台的日志
在
10:52:01
客户端发起了一次请求,在10:52:02
又发起了一次新的请求,这两个请求查询相同的内容,但是都触发了查询数据库的逻辑,可见此处已经造成了缓存穿透.
这次,我们把CacheServiceImpl
中的getAllProducts
上的sync=true
加上
@Cacheable(value = {"PMS_PRODUCT#120"},key = "'PRODUCT'",sync=true)
@Override
public List<TestProduct> getAllProducts() {
System.out.println("查询缓存信息" + new Date().toString());
return getProductsFromDB(3);
}
再来访问localhost:8000/test/getCache,然后在10s
内再次访问localhost:8000/test/getCache
来看一下控制台的日志
我们仍然是在
10s
内发送了两个请求,但是查询数据库的逻辑仅仅调用了一次,可见,此处添加了sync=true
之后,SpringCache
已经为我们的函数进行了加锁,在单个服务中防止了缓存击穿
的发生.
当然,像前面所说,SpringCache
解决的是单机的缓存击穿
,面对多个相同的微服务,它仍然存在一些问题,我们创建一个一模一样的服务redisTest2
端口设置为8001
现在访问localhost:8000/test/getCache,然后在10s
内再次访问localhost:8001/test/getCache
redisTest1
控制台输出如下
redisTest2
控制台输出如下image.png
可见我们的两次请求,在多个微服务的情况下,仍然无法完全避免
缓存击穿
,无法满足我们对于数据强一致性的要求.要解决这个问题,我们就要考虑使用分布式锁了,下一节,我们会使用分布式锁解决这个问题.
以上内容转载请注明出处,同时也请大家不吝你的关注和下面的赞赏
↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
网友评论