设计到的内容:
- 本地缓存:caffeine 使用
- redis 缓存:和本地缓存结合
- double check
- 自定义缓存注解
- 表达式语言 Spring Expression Language
- LocalVariableTableParameterNameDiscoverer 获取方法的参数名
1. 目录结构
单独设置一个分包,用来统一管理缓存,并且缓存类型(本地缓存、redis 缓存)放入 strategy
包下。
2. 本地缓存基础类
import com.github.benmanes.caffeine.cache.*;
import lombok.extern.slf4j.Slf4j;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* @description: 本地缓存基础类
*/
@Slf4j
public abstract class BaseCache {
/**
* 缓存对象
*/
private LoadingCache cache;
/**
* 缓存最大容量,默认为 10
*/
protected Integer maximumSize = 1024;
/**
* 缓存失效时长
*/
protected Long duration = 300L;
/**
* 缓存失效单位,默认为 5s
*/
protected TimeUnit timeUnit = TimeUnit.SECONDS;
/**
* @description: 获取cache
*/
private LoadingCache<String, Object> getCache() {
if (cache == null) {
synchronized (BaseCache.class) {
if (cache == null) {
LoadingCache<String, Object> tempCache = Caffeine.newBuilder()
.maximumSize(maximumSize)
.expireAfterWrite(duration, timeUnit)
.removalListener((RemovalListener<String, Object>) (key, value, cause) -> {
log.warn("剔除监听 > key:{}, value:{}, cause:{}", key, value, cause.toString());
})
.build(key -> getLoadData(key));
cache = tempCache;
}
}
}
return cache;
}
/**
* @description:返回加载到内存中的数据,一般从数据库中加载
*/
public abstract Object getLoadData(String key);
/**
* @description:如果getLoadData返回值为null,返回的默认值
*/
public abstract Object getLoadDataIfNull(String key);
/**
* @description:批量清除缓存
*/
public void batchInvalidate(List<String> keys) {
if (keys != null) {
getCache().invalidateAll(keys);
} else {
getCache().invalidateAll();
}
}
/**
* @description:清除缓存
*/
public void invalidateOne(String key) {
getCache().invalidate(key);
}
/**
* @description:加入缓存
*/
public void set(String key, Object value) {
getCache().put(key, value);
}
/**
* @description:获取缓存
*/
public Object get(String key) {
return getCache().get(key);
}
public void del(String key, Long version, String eventType) {
invalidateOne(key);
}
}
3. 本地缓存类
@Slf4j
@Service
public class CacheDefault extends BaseCache {
@Autowired
private RedisServiceImpl<String, Object> redisService;
/**
* @description:从redis中读取并加载到本地缓存
*/
@Override
public Object getLoadData(String key) {
return redisService.get(key);
}
@Override
public MokaPackageModel getLoadDataIfNull(String key) {
return null;
}
/**
* @description:设置操作
*/
@Override
public void set(String key, Object value) {
super.set(key, value);
redisService.set(key, value, duration, timeUnit);
}
/**
* @description:获取操作
*/
@Override
public Object get(String key) {
return super.get(key);
}
/**
* @description:删除操作
*/
@Override
public void del(String key, Long version, String eventType) {
try {
// 先从redis中获取,如果为空,可能是被其他机器删除了,也有可能本来就没有。
Object data = getLoadData(key);
if (data == null) {
// 如果redis中没有,再从本地缓存获取,如果都没有,则返回
data = get(key);
if (data == null) {
return;
}
}
JSONObject localCacheJson = (JSONObject)JSONObject.toJSON(data);
String oldVersion = localCacheJson.getString("version");
if (StringUtils.isBlank(oldVersion)) {
return;
}
log.info("CacheDefault del. oldVersion={}, newVersion={}", version, oldVersion);
if (BinlogEventTypeEnum.DELETE.getCode().equals(eventType)) {
if (version >= Long.parseLong(oldVersion)) {
super.del(key, version, eventType);
redisService.delete(key);
}
}
if (BinlogEventTypeEnum.UPDATE.getCode().equals(eventType)) {
if (version > Long.parseLong(oldVersion)) {
super.del(key, version, eventType);
redisService.delete(key);
}
}
} catch (Exception e) {
log.error("CacheDefault.del error.", e);
}
}
}
3. 自定义注解
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MokaCache {
/**
* 缓存前缀
*/
String prefix();
/**
* 分割符
*/
String separator() default ":";
/**
* 缓存key定义
*/
String key();
/**
* 是否开启缓存加载
*/
boolean turn() default true;
/**
* 失效时间
*/
long expire() default 1800L;
/**
* 失效时间是否随机
*/
boolean random() default false;
/**
* 失效时间范围:最小值
*/
long minExpire() default 1800L;
/**
* 失效时间范围:最大值
*/
long maxExpire() default 3600L;
/**
* 失效时间单位
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
}
4. AOP 拦截器
@Aspect
@Component
@Slf4j
public class MokaCacheAop {
LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
@Autowired
private KVConfig kvConfig;
@Autowired
private CacheDefault cacheDefault;
@Around("@annotation(com.hwl.moka.cache.annotation.MokaCache)")
public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 获得参数值
Object[] args = joinPoint.getArgs();
MokaCache mokaCache = method.getAnnotation(MokaCache.class);
// 总开关关闭或者当前开关关闭,直接返回
if (!kvConfig.mokaCacheSwitch || !mokaCache.turn()) {
return joinPoint.proceed(args);
}
// 获得参数名
String[] paramNames = discoverer.getParameterNames(method);
SpelParser<String> parser = new SpelParser<>();
EvaluationContext context = parser.setAndGetContextValue(paramNames, args);
String prefix = mokaCache.prefix();
if (StringUtils.isBlank(prefix)) {
log.error("注解MokaCache参数prefix为空. method={}", method);
return joinPoint.proceed(args);
}
//解析SpEL表达式
String cacheKey = mokaCache.key();
if (StringUtils.isBlank(cacheKey)) {
log.error("注解MokaCache参数key为空. method={}", method);
return joinPoint.proceed(args);
}
List<String> cacheKeyList = splitSuper(cacheKey, "#");
StringBuilder keyBuilder = new StringBuilder(prefix).append(mokaCache.separator());
for (String key : cacheKeyList) {
keyBuilder.append(String.valueOf(parser.parse(key, context))).append(mokaCache.separator());
}
String key = keyBuilder.substring(0, keyBuilder.length() - 1);
// 从缓存中获取
Object cache = cacheDefault.get(key);
if (cache != null) {
log.info("cache hit. prefix={}, key={}", prefix, key);
return cache;
}
log.info("cache miss. prefix={}, key={}", prefix, key);
// 执行业务方法
Object proceed = joinPoint.proceed(args);
// 将数据存入缓存中
cacheDefault.set(key, proceed, mokaCache.expire(), mokaCache.timeUnit());
return proceed;
}
public static List<String> splitSuper(String str, String split) {
String[] splitArr = str.split(split);
List<String> result = new ArrayList<>();
for (String s : splitArr) {
if (StringUtils.isBlank(s)) {
continue;
}
result.add(split + s);
}
return result;
}
}
5. 解析器
public class SpelParser<T> {
/**
* 表达式解析器
*/
ExpressionParser parser = new SpelExpressionParser();
/**
* 解析SpEL表达式
*
* @param spel
* @param context
* @return T 解析出来的值
*/
public T parse(String spel, EvaluationContext context) {
return (T) parser.parseExpression(spel).getValue(context);
}
/**
* 将参数名和参数值存储进EvaluationContext对象中
*
* @param object 参数值
* @param params 参数名
* @return EvaluationContext对象
*/
public EvaluationContext setAndGetContextValue(String[] params, Object[] object) {
EvaluationContext context = new StandardEvaluationContext();
for (int i = 0; i < params.length; i++) {
context.setVariable(params[i], object[i]);
}
return context;
}
}
6. 使用
@MokaCache(prefix = "cache:test", key = "#id#name#age")
public String getByParam(String id, String name, int age) {
return id + name + age;
}
7. 说明
LocalVariableTableParameterNameDiscoverer discoverer
是获取方法的参数名列表,用于之后根据key = "#id#name#age"
解析出对应参数的值,来组装缓存的 keyAOP 类中还用到了 Spring Expression language(SpEL),主要用于根据注解中的
"#id#name#age"
来解析出对应的值。因为入参可能有多个,因此需要先分隔再依次解析出参数值,最后拼装成 key 去使用。最后如果没有查询到值的话,调用
Object proceed = joinPoint.proceed(args);
方法获取执行结果并进行缓存。
�
�