3、缓存清除cacheEvict
3.1 基本原理
在实际应用中,缓存并非是一成不变的,我们写入缓存之后还需要更新缓存。这常常存在于一些更新操作和删除操作中。
例如我们查询了一个产品列表的第一页,之后缓存写入了这一页,在缓存中我们有这样一个映射:
key : DAWN-CACHE.cn.hengyumo.dawn.example.service.ProductService.searchProduct.size=5&page=0
value:
{
"code": 200,
"message": "请求成功",
"data": {
"total": 625000,
"list": [
{
"id": 1,
"name": "Aa1",
"createTime": "2021-03-02T01:01:01.357+08:00",
"price": 1
},
{
"id": 2,
"name": "Aa2",
"createTime": "2021-04-03T02:02:02.357+08:00",
"price": 2
},
{
"id": 3,
"name": "Aa3",
"createTime": "2021-05-04T03:03:03.357+08:00",
"price": 3
},
{
"id": 4,
"name": "Aa4",
"createTime": "2021-06-05T04:04:04.357+08:00",
"price": 4
},
{
"id": 5,
"name": "Aa5",
"createTime": "2021-07-06T05:05:05.357+08:00",
"price": 5
}
],
"pageNum": 0,
"pageSize": 5,
"size": 5,
"startRow": 1,
"endRow": 5,
"pages": 125000,
"prePage": 0,
"nextPage": 1,
"isFirstPage": false,
"isLastPage": false,
"hasPreviousPage": false,
"hasNextPage": true,
"navigatePages": 8,
"navigatepageNums": [
1,
2,
3,
4,
5,
6,
7,
8
],
"navigateFirstPage": 1,
"navigateLastPage": 8
}
}
之后我们更新了id为1的产品,将其名称修改为“PRODUCT-B”,那么我们需要保证再次查询第一页时,能正确的反映我们的修改操作。
这时有两种思路,一种是更新key : DAWN-CACHE.cn.hengyumo.dawn.example.service.ProductService.searchProduct.size=5&page=0
对应的缓存,将其id为1的产品的名称修改过来。
另一种就是直接清除该缓存,之后查询时发现没有缓存就会查询数据库,从而保证获得到的是最新的数据。
更新缓存明显逻辑更为复杂,而清除缓存的操作更为简单、不容易出错,因此我们大部分情况下选择清除缓存,而不是更新缓存。
类比上一节提到的@DawnCacheable注解,我们实现一个@DawnCacheEvict注解,用来实现清除缓存的功能,如下:
@DawnCacheEvict(name = "evictSearchProduct", method = "searchProduct", cacheType = DawnCacheType.REDIS)
public Product updateProduct(Product product) {
Product productFound = findProductById(product.getId());
try {
ClassUtil.coverNotNullProperties(productFound, product);
} catch (IllegalAccessException e) {
e.printStackTrace();
}
return productFound;
}
当这个方法被调用之后,在 @DawnCacheEvict 的注解声明作用下,它会自动的删除我们的searchProduct方法在Redis中产生的缓存。
从而使得下一次查询分页时从数据库查询,然后再写入新的缓存。
这个注解声明如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DawnCacheEvict {
@AliasFor("name")
String value() default "";
/**
* 命名
*/
@AliasFor("value")
String name() default "";
/**
* 写入缓存的方法
*
* @return 方法名
*/
String method() default "";
/**
* 缓存的key生成类
*
* 默认是{@link DawnDefaultCacheKeyCreator}
* @return DawnCacheKeyCreator
*/
Class<? extends DawnCacheKeyCreator> cacheKeyCreator() default DawnDefaultCacheKeyCreator.class;
/**
* 使用的缓存方式
*
* @return 默认是 DawnCacheType.DEFAULT
*/
DawnCacheType cacheType() default DawnCacheType.DEFAULT;
}
3.2 扩展cacheKeyCreator
在上节中,我们描述了cacheKeyCreator的设计:
cacheKeyCreator其设计的思路来自于mybatis的SqlProvider,这种通过实现一个class的方式来完成注解的某一块功能,具有灵活和易于阅读的优点
为了使其也支持生成要被清除的缓存前缀,我们给这个接口加上了一个方法:
/**
* 生成要清除的缓存的KEY前缀
*
* @param cacheMethodName 缓存方法名
* @param request 请求
* @param target 注解的方法的所在对象
* @param method 方法携带的参数Map<String,Object>
* @param params Key 字符串
* @return 要清除的缓存的KEY前缀
*/
String createCacheEvictKey(String cacheMethodName, HttpServletRequest request, Object target, Method method, Map<String, Object> params);
其目的是在@DawnCacheEvict这个注解中通过cacheKeyCreator生成要清除的缓存的KEY前缀,之后就可以针对性的清除这些缓存,相比createCacheKey方法,多了的一个参数是清除的目标缓存方法的方法名。
完整的cacheKeyCreator接口要求如下:
public interface DawnCacheKeyCreator {
/**
* 生成KEY
*
* @param request 请求
* @param target 注解的方法的所在对象
* @param method 注解的方法
* @param params 方法携带的参数Map<String,Object>
* @return Key 字符串
*/
String createCacheKey(HttpServletRequest request, Object target, Method method, Map<String, Object> params);
/**
* 生成要清除的缓存的KEY前缀
*
* @param cacheMethodName 缓存方法名
* @param request 请求
* @param target 注解的方法的所在对象
* @param method 方法携带的参数Map<String,Object>
* @param params Key 字符串
* @return 要清除的缓存的KEY前缀
*/
String createCacheEvictKey(String cacheMethodName, HttpServletRequest request, Object target, Method method, Map<String, Object> params);
}
同样的扩展了DawnDefaultCacheKeyCreator这个默认的Key生成类,以支持生成清除的Key前缀:
public class DawnDefaultCacheKeyCreator implements DawnCacheKeyCreator {
public final static String CACHE_PER = "DAWN-CACHE";
@Override
public String createCacheKey(HttpServletRequest request, Object target, Method method, Map<String, Object> params) {
String queryString = request.getQueryString();
String targetClassName = target.getClass().getName();
String methodName = method.getName();
if (StringUtil.isEmpty(queryString) && params != null && params.size() > 0) {
queryString = JSON.toJSONString(params);
}
return String.format("%s.%s.%s.%s", CACHE_PER, targetClassName, methodName , queryString);
}
@Override
public String createCacheEvictKey(String cacheMethodName, HttpServletRequest request, Object target, Method method, Map<String, Object> params) {
String targetClassName = target.getClass().getName();
return String.format("%s.%s.%s", CACHE_PER, targetClassName, cacheMethodName);
}
}
createCacheKey它的key生成规则是,取 缓存前缀 DAWN-CACHE
加上 目标对象的类名
加上 目标方法的方法名
加上url后边的请求参数或方法参数
来生成的。
因此用createCacheKey 生成的key拥有一个固定的前缀,那就是 缓存前缀 DAWN-CACHE
加上 目标对象的类名
加上 目标方法的方法名
,我们的createCacheEvictKey就只需要生成这样一个字符串,这便是我们需要清除的那些缓存的前缀。
例如:DAWN-CACHE.cn.hengyumo.dawn.example.service.ProductService.searchProduct.size=5&page=0
、DAWN-CACHE.cn.hengyumo.dawn.example.service.ProductService.searchProduct.size=5&page=1
,这两个Key分别代表的是分页查询第一页、第二页的缓存Key,那么它们都拥有DAWN-CACHE.cn.hengyumo.dawn.example.service.ProductService.searchProduct
这样一个前缀。
在@DawnCacheEvict注解中我们配置:
@DawnCacheEvict(name = "evictSearchProduct", method = "searchProduct", cacheType = DawnCacheType.REDIS)
那么它就会清除所有searchProduct这个方法产生的缓存,达到我们需要的效果。
3.3 清除多个不同方法产生的缓存
在实际使用中,对于同一份数据我们可能有多个缓存:
例如:
// 根据ID查询产品
@DawnCacheable(sync = false, cacheKeyCreator = ProductIdCacheKeyCreator.class)
public Product getProductById(Long id)
// 获取产品的最高价格
@DawnCacheable(cacheType = DawnCacheType.SIMPLE, expire = 30)
public BigDecimal getProductTopPrice()
// 分页查询产品
@DawnCacheable(cacheType = DawnCacheType.REDIS, expire = 1, cacheTimeUnit = TimeUnit.MINUTES)
public PageInfo<Product> searchProduct(ProductSearchDto productSearchDto, Pageable pageable)
如果我们更新某个产品之后,需要清除缓存则需要同时清除这三个方法产生的缓存。
所以增加了一个注解,用来实现这个功能:
@DawnCacheEvicts(
name = "evictProduct",
dawnCacheEvicts = {
@DawnCacheEvict(name = "evictSearchProduct", method = "searchProduct", cacheType = DawnCacheType.REDIS),
@DawnCacheEvict(name = "evictGetProductById", cacheKeyCreator = ProductIdCacheKeyCreator.class),
@DawnCacheEvict(name = "evictGetProductTopPrice", method = "getProductTopPrice", cacheType = DawnCacheType.SIMPLE)
})
public Product updateProduct(Product product)
这个注解中包含了多个 @DawnCacheEvict 子注解,定义如下:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DawnCacheEvicts {
@AliasFor("name")
String value() default "";
/**
* 命名
*/
@AliasFor("value")
String name() default "";
/**
* 清除缓存的规则列表
*
* @return DawnCacheEvict[]
*/
DawnCacheEvict[] dawnCacheEvicts() default {};
}
这样我们在更新产品之后,就会清除掉上文那三个方法产生的缓存。
3.4 别名机制
在日常的使用中,我们可能会出现多个更新或者删除的方法都需要清除同一个缓存,例如:
// 更新产品
@DawnCacheEvicts(
name = "evictProduct",
dawnCacheEvicts = {
@DawnCacheEvict(name = "evictSearchProduct", method = "searchProduct", cacheType = DawnCacheType.REDIS),
@DawnCacheEvict(name = "evictGetProductById", cacheKeyCreator = ProductIdCacheKeyCreator.class),
@DawnCacheEvict(name = "evictGetProductTopPrice", method = "getProductTopPrice", cacheType = DawnCacheType.SIMPLE)
})
public Product updateProduct(Product product)
// 删除产品
@DawnCacheEvicts("evictProduct")
public void deleteProduct(Long id)
// 添加产品
@DawnCacheEvicts(dawnCacheEvicts = {
@DawnCacheEvict("evictSearchProduct"),
@DawnCacheEvict("evictGetProductTopPrice")
})
public Product addProduct(Product product)
其中,更新产品和删除产品,我们需要清除三个缓存,(1)分页查询的缓存,(2)按Id查询的缓存,(3)获取产品最高价格的缓存
添加产品时,我们需要清除两个缓存,(1)分页查询的缓存,(2)获取产品最高价格的缓存
这时候别名就起作用了,它可以让我们少写代码,并且修改规则时只需要改一个地方。
别名的实现原理主要是以下这个方法:
private final static Map<String, Map<String, DawnCacheEvict[]>> EVICTS_CACHE
= new ConcurrentHashMap<>();
/**
* 生成目标对象的缓存清除规则的命名缓存
*
* @param target 目标对象的类型
*/
private void cacheDawnCacheEvictsForTarget(Class<?> target) {
Method[] methods = target.getMethods();
String targetClassName = target.getName();
Map<String, DawnCacheEvict[]> map = new HashMap<>();
// 反射遍历这个对象的所有方法,然后遍历方法上的
for (Method method : methods) {
DawnCacheEvicts dawnCacheEvicts = AnnotationUtils.getAnnotation(method, DawnCacheEvicts.class);
DawnCacheEvict dawnCacheEvict = AnnotationUtils.getAnnotation(method, DawnCacheEvict.class);
if (dawnCacheEvicts != null) {
String dawnCacheEvictsName = dawnCacheEvicts.name();
DawnCacheEvict[] children = dawnCacheEvicts.dawnCacheEvicts();
if (children.length > 0) {
for (DawnCacheEvict child : children) {
cacheDawnCacheEvict(map, child, targetClassName);
}
if (StringUtil.isNotEmpty(dawnCacheEvictsName)) {
map.put(String.format("%s.%s",
targetClassName, dawnCacheEvictsName), children);
}
}
}
if (dawnCacheEvict != null) {
cacheDawnCacheEvict(map, dawnCacheEvict, targetClassName);
}
}
EVICTS_CACHE.put(targetClassName, map);
}
/**
* 如果该缓存清除规则名字不为空,并且不是只配置了名字,则将其放入缓存Map中
*
* @param map 保存这个对象的缓存清除规则
* @param dawnCacheEvict 缓存清除规则注解
* @param targetClassName 目标对象的类名
*/
private void cacheDawnCacheEvict(Map<String, DawnCacheEvict[]> map,
DawnCacheEvict dawnCacheEvict, String targetClassName) {
String name = dawnCacheEvict.name();
boolean isNotConfig = dawnCacheEvictIsNotConfig(dawnCacheEvict);
if (StringUtil.isNotEmpty(name) && ! isNotConfig) {
map.put(String.format("%s.%s", targetClassName, name),
new DawnCacheEvict[]{dawnCacheEvict});
}
}
/**
* 判断该缓存清除规则是否是未配置
*
* @param dawnCacheEvict 缓存清除规则注解
* @return true是未配置
*/
private boolean dawnCacheEvictIsNotConfig(DawnCacheEvict dawnCacheEvict) {
return dawnCacheEvict.cacheKeyCreator().equals(DawnDefaultCacheKeyCreator.class)
&& StringUtil.isEmpty(dawnCacheEvict.method())
&& dawnCacheEvict.cacheType().equals(DawnCacheType.DEFAULT);
}
之后我们就可以通过缓存清除规则的名称尝试去Map里查找对应的缓存清除规则:
/**
* 获取对于名称的缓存清除规则,如果没有初始化该类的缓存清除规则,那么会先进行初始化
*
* @param target 目标类
* @param evictName 缓存清除规则
* @return 缓存清除规则列表DawnCacheEvict[]
*/
private DawnCacheEvict[] getDawnCacheEvictsForTarget(Class<?> target, String evictName) {
String targetClassName = target.getName();
if (!EVICTS_CACHE.containsKey(targetClassName)) {
synchronized (DawnCacheAspect.class) {
if (!EVICTS_CACHE.containsKey(targetClassName)) {
cacheDawnCacheEvictsForTarget(target);
}
}
}
DawnCacheEvict[] dawnCacheEvicts = EVICTS_CACHE.get(targetClassName)
.get(String.format("%s.%s", targetClassName, evictName));
if (dawnCacheEvicts == null || dawnCacheEvicts.length == 0) {
log.warn("{} 无法找到对应名称的缓存清除注解!", evictName);
}
return dawnCacheEvicts;
}
END(2)