设计到的内容:
- 本地缓存: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: 本地缓存基础类*/@Slf4jpublic 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@Servicepublic class CacheDefault extends BaseCache {@Autowiredprivate RedisServiceImpl<String, Object> redisService;/*** @description:从redis中读取并加载到本地缓存*/@Overridepublic Object getLoadData(String key) {return redisService.get(key);}@Overridepublic MokaPackageModel getLoadDataIfNull(String key) {return null;}/*** @description:设置操作*/@Overridepublic void set(String key, Object value) {super.set(key, value);redisService.set(key, value, duration, timeUnit);}/*** @description:获取操作*/@Overridepublic Object get(String key) {return super.get(key);}/*** @description:删除操作*/@Overridepublic 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@Slf4jpublic class MokaCacheAop {LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();@Autowiredprivate KVConfig kvConfig;@Autowiredprivate 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);方法获取执行结果并进行缓存。
�
�
