设计到的内容:

  • 本地缓存:caffeine 使用
  • redis 缓存:和本地缓存结合
  • double check
  • 自定义缓存注解
  • 表达式语言 Spring Expression Language
  • LocalVariableTableParameterNameDiscoverer 获取方法的参数名

1. 目录结构

image.png
单独设置一个分包,用来统一管理缓存,并且缓存类型(本地缓存、redis 缓存)放入 strategy 包下。

2. 本地缓存基础类

  1. import com.github.benmanes.caffeine.cache.*;
  2. import lombok.extern.slf4j.Slf4j;
  3. import java.util.List;
  4. import java.util.concurrent.TimeUnit;
  5. /**
  6. * @description: 本地缓存基础类
  7. */
  8. @Slf4j
  9. public abstract class BaseCache {
  10. /**
  11. * 缓存对象
  12. */
  13. private LoadingCache cache;
  14. /**
  15. * 缓存最大容量,默认为 10
  16. */
  17. protected Integer maximumSize = 1024;
  18. /**
  19. * 缓存失效时长
  20. */
  21. protected Long duration = 300L;
  22. /**
  23. * 缓存失效单位,默认为 5s
  24. */
  25. protected TimeUnit timeUnit = TimeUnit.SECONDS;
  26. /**
  27. * @description: 获取cache
  28. */
  29. private LoadingCache<String, Object> getCache() {
  30. if (cache == null) {
  31. synchronized (BaseCache.class) {
  32. if (cache == null) {
  33. LoadingCache<String, Object> tempCache = Caffeine.newBuilder()
  34. .maximumSize(maximumSize)
  35. .expireAfterWrite(duration, timeUnit)
  36. .removalListener((RemovalListener<String, Object>) (key, value, cause) -> {
  37. log.warn("剔除监听 > key:{}, value:{}, cause:{}", key, value, cause.toString());
  38. })
  39. .build(key -> getLoadData(key));
  40. cache = tempCache;
  41. }
  42. }
  43. }
  44. return cache;
  45. }
  46. /**
  47. * @description:返回加载到内存中的数据,一般从数据库中加载
  48. */
  49. public abstract Object getLoadData(String key);
  50. /**
  51. * @description:如果getLoadData返回值为null,返回的默认值
  52. */
  53. public abstract Object getLoadDataIfNull(String key);
  54. /**
  55. * @description:批量清除缓存
  56. */
  57. public void batchInvalidate(List<String> keys) {
  58. if (keys != null) {
  59. getCache().invalidateAll(keys);
  60. } else {
  61. getCache().invalidateAll();
  62. }
  63. }
  64. /**
  65. * @description:清除缓存
  66. */
  67. public void invalidateOne(String key) {
  68. getCache().invalidate(key);
  69. }
  70. /**
  71. * @description:加入缓存
  72. */
  73. public void set(String key, Object value) {
  74. getCache().put(key, value);
  75. }
  76. /**
  77. * @description:获取缓存
  78. */
  79. public Object get(String key) {
  80. return getCache().get(key);
  81. }
  82. public void del(String key, Long version, String eventType) {
  83. invalidateOne(key);
  84. }
  85. }

BaseCache 为缓存基础类,是一个抽象类。

3. 本地缓存类

  1. @Slf4j
  2. @Service
  3. public class CacheDefault extends BaseCache {
  4. @Autowired
  5. private RedisServiceImpl<String, Object> redisService;
  6. /**
  7. * @description:从redis中读取并加载到本地缓存
  8. */
  9. @Override
  10. public Object getLoadData(String key) {
  11. return redisService.get(key);
  12. }
  13. @Override
  14. public MokaPackageModel getLoadDataIfNull(String key) {
  15. return null;
  16. }
  17. /**
  18. * @description:设置操作
  19. */
  20. @Override
  21. public void set(String key, Object value) {
  22. super.set(key, value);
  23. redisService.set(key, value, duration, timeUnit);
  24. }
  25. /**
  26. * @description:获取操作
  27. */
  28. @Override
  29. public Object get(String key) {
  30. return super.get(key);
  31. }
  32. /**
  33. * @description:删除操作
  34. */
  35. @Override
  36. public void del(String key, Long version, String eventType) {
  37. try {
  38. // 先从redis中获取,如果为空,可能是被其他机器删除了,也有可能本来就没有。
  39. Object data = getLoadData(key);
  40. if (data == null) {
  41. // 如果redis中没有,再从本地缓存获取,如果都没有,则返回
  42. data = get(key);
  43. if (data == null) {
  44. return;
  45. }
  46. }
  47. JSONObject localCacheJson = (JSONObject)JSONObject.toJSON(data);
  48. String oldVersion = localCacheJson.getString("version");
  49. if (StringUtils.isBlank(oldVersion)) {
  50. return;
  51. }
  52. log.info("CacheDefault del. oldVersion={}, newVersion={}", version, oldVersion);
  53. if (BinlogEventTypeEnum.DELETE.getCode().equals(eventType)) {
  54. if (version >= Long.parseLong(oldVersion)) {
  55. super.del(key, version, eventType);
  56. redisService.delete(key);
  57. }
  58. }
  59. if (BinlogEventTypeEnum.UPDATE.getCode().equals(eventType)) {
  60. if (version > Long.parseLong(oldVersion)) {
  61. super.del(key, version, eventType);
  62. redisService.delete(key);
  63. }
  64. }
  65. } catch (Exception e) {
  66. log.error("CacheDefault.del error.", e);
  67. }
  68. }
  69. }

3. 自定义注解

  1. @Target({ElementType.TYPE, ElementType.METHOD})
  2. @Retention(RetentionPolicy.RUNTIME)
  3. public @interface MokaCache {
  4. /**
  5. * 缓存前缀
  6. */
  7. String prefix();
  8. /**
  9. * 分割符
  10. */
  11. String separator() default ":";
  12. /**
  13. * 缓存key定义
  14. */
  15. String key();
  16. /**
  17. * 是否开启缓存加载
  18. */
  19. boolean turn() default true;
  20. /**
  21. * 失效时间
  22. */
  23. long expire() default 1800L;
  24. /**
  25. * 失效时间是否随机
  26. */
  27. boolean random() default false;
  28. /**
  29. * 失效时间范围:最小值
  30. */
  31. long minExpire() default 1800L;
  32. /**
  33. * 失效时间范围:最大值
  34. */
  35. long maxExpire() default 3600L;
  36. /**
  37. * 失效时间单位
  38. */
  39. TimeUnit timeUnit() default TimeUnit.SECONDS;
  40. }

4. AOP 拦截器

  1. @Aspect
  2. @Component
  3. @Slf4j
  4. public class MokaCacheAop {
  5. LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
  6. @Autowired
  7. private KVConfig kvConfig;
  8. @Autowired
  9. private CacheDefault cacheDefault;
  10. @Around("@annotation(com.hwl.moka.cache.annotation.MokaCache)")
  11. public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable {
  12. MethodSignature signature = (MethodSignature) joinPoint.getSignature();
  13. Method method = signature.getMethod();
  14. // 获得参数值
  15. Object[] args = joinPoint.getArgs();
  16. MokaCache mokaCache = method.getAnnotation(MokaCache.class);
  17. // 总开关关闭或者当前开关关闭,直接返回
  18. if (!kvConfig.mokaCacheSwitch || !mokaCache.turn()) {
  19. return joinPoint.proceed(args);
  20. }
  21. // 获得参数名
  22. String[] paramNames = discoverer.getParameterNames(method);
  23. SpelParser<String> parser = new SpelParser<>();
  24. EvaluationContext context = parser.setAndGetContextValue(paramNames, args);
  25. String prefix = mokaCache.prefix();
  26. if (StringUtils.isBlank(prefix)) {
  27. log.error("注解MokaCache参数prefix为空. method={}", method);
  28. return joinPoint.proceed(args);
  29. }
  30. //解析SpEL表达式
  31. String cacheKey = mokaCache.key();
  32. if (StringUtils.isBlank(cacheKey)) {
  33. log.error("注解MokaCache参数key为空. method={}", method);
  34. return joinPoint.proceed(args);
  35. }
  36. List<String> cacheKeyList = splitSuper(cacheKey, "#");
  37. StringBuilder keyBuilder = new StringBuilder(prefix).append(mokaCache.separator());
  38. for (String key : cacheKeyList) {
  39. keyBuilder.append(String.valueOf(parser.parse(key, context))).append(mokaCache.separator());
  40. }
  41. String key = keyBuilder.substring(0, keyBuilder.length() - 1);
  42. // 从缓存中获取
  43. Object cache = cacheDefault.get(key);
  44. if (cache != null) {
  45. log.info("cache hit. prefix={}, key={}", prefix, key);
  46. return cache;
  47. }
  48. log.info("cache miss. prefix={}, key={}", prefix, key);
  49. // 执行业务方法
  50. Object proceed = joinPoint.proceed(args);
  51. // 将数据存入缓存中
  52. cacheDefault.set(key, proceed, mokaCache.expire(), mokaCache.timeUnit());
  53. return proceed;
  54. }
  55. public static List<String> splitSuper(String str, String split) {
  56. String[] splitArr = str.split(split);
  57. List<String> result = new ArrayList<>();
  58. for (String s : splitArr) {
  59. if (StringUtils.isBlank(s)) {
  60. continue;
  61. }
  62. result.add(split + s);
  63. }
  64. return result;
  65. }
  66. }

5. 解析器

  1. public class SpelParser<T> {
  2. /**
  3. * 表达式解析器
  4. */
  5. ExpressionParser parser = new SpelExpressionParser();
  6. /**
  7. * 解析SpEL表达式
  8. *
  9. * @param spel
  10. * @param context
  11. * @return T 解析出来的值
  12. */
  13. public T parse(String spel, EvaluationContext context) {
  14. return (T) parser.parseExpression(spel).getValue(context);
  15. }
  16. /**
  17. * 将参数名和参数值存储进EvaluationContext对象中
  18. *
  19. * @param object 参数值
  20. * @param params 参数名
  21. * @return EvaluationContext对象
  22. */
  23. public EvaluationContext setAndGetContextValue(String[] params, Object[] object) {
  24. EvaluationContext context = new StandardEvaluationContext();
  25. for (int i = 0; i < params.length; i++) {
  26. context.setVariable(params[i], object[i]);
  27. }
  28. return context;
  29. }
  30. }

6. 使用

  1. @MokaCache(prefix = "cache:test", key = "#id#name#age")
  2. public String getByParam(String id, String name, int age) {
  3. return id + name + age;
  4. }

7. 说明

  • LocalVariableTableParameterNameDiscoverer discoverer 是获取方法的参数名列表,用于之后根据 key = "#id#name#age" 解析出对应参数的值,来组装缓存的 key

  • AOP 类中还用到了 Spring Expression language(SpEL),主要用于根据注解中的 "#id#name#age" 来解析出对应的值。因为入参可能有多个,因此需要先分隔再依次解析出参数值,最后拼装成 key 去使用。

  • 最后如果没有查询到值的话,调用 Object proceed = joinPoint.proceed(args); 方法获取执行结果并进行缓存。