Java中使用Redis实现注解缓存
Java中使用Redis实现注解缓存

Java中使用Redis实现注解缓存

发布时间
Apr 22, 2020
标签
java
redis
介绍:通过方法的参数来缓存这个方法的返回内容,主要使用到redis的String和Set

首先,先定义一个注解,该注解的作用就是表示这个方法有缓存。该注解有两个属性,一个是该缓存key的前缀,可以使用入参中的变量;还有一个是缓存的过期时间。
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface Cache { // redisKey的一个因子,格式例子:t1_{userId}:t2 String keyPrefix() default ""; // -1-永不过期,单位为毫秒 long timeout() default 10000; }
 
写个切面类,在方法执行的时候使用到缓存
@Slf4j @Aspect @Component public class CacheAspect { public static final String NULL_STR = "null"; /** * 缓存key前缀 */ public final static String METHOD_KEYS = "METHOD_KEYS"; /** * 用于存放缓存key的一个集合 */ public final static String METHOD_KEYS_SET = "METHOD_KEYS_SET"; /** * 分布式锁key前缀 */ public final static String METHOD_KEY_LOCK_IDS = "METHOD_KEY_LOCK_IDS"; public final static String NEED_TO_DELETE_KEYS_SET = "NEED_TO_DELETE_KEYS_SET"; /** * 生成MD5用到 */ public final static char[] HEX_DIGITS = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; private final StringRedisTemplate stringRedisTemplate; private final SetOperations<String, String> setOperations; private final ValueOperations<String, String> valueOperations; public CacheAspect(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; this.setOperations = stringRedisTemplate.opsForSet(); this.valueOperations = stringRedisTemplate.opsForValue(); } /** * 自定义切点位置,匹配当前执行方法持有@Cache注解的方法 */ @Pointcut("@annotation(com.chu7.common.cache.annotation.Cache)") public void annotationPointCut() { } /** * 设置切点,匹配成功后的执行 * * @param joinPoint 连接点 */ @Around("annotationPointCut()") public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable { // 获取类名 String className = joinPoint.getTarget().getClass().getName(); // 获取其方法签名 MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature(); Method method = methodSignature.getMethod(); String methodName = method.getName(); Class<?> returnType = method.getReturnType(); // 获取注解内容 Cache cache = method.getAnnotation(Cache.class); String keyPrefix = cache.keyPrefix(); String[] keyPrefixArray = null; long timeout = cache.timeout(); // 获取方法参数值数组 StringBuilder argsString = new StringBuilder(); Object[] args = joinPoint.getArgs(); if (args != null && args.length > 0) { for (Object arg : args) { if (arg != null) { String t; // 可以序列化的对象就转成json,否则就用hashCode if (arg instanceof Serializable) { t = JsonUtils.objectToJson(arg); } else { t = String.valueOf(arg.hashCode()); } argsString.append(t).append(":"); } } if (!"".equals(keyPrefix) && keyPrefix.contains("}")) { String[] customerParam = keyPrefix.split("}"); for (int i = 0; i < customerParam.length; i++) { int j = customerParam[i].indexOf("{"); if (j != -1) { customerParam[i] = customerParam[i].substring(j + 1); } else { customerParam[i] = null; } } // 获取方法参数名称数组 String[] parameterNames = methodSignature.getParameterNames(); /* @Cache("student:info_{studentId}") String get(Long studentId); 例如以上方法传的studentId是5084,那么执行完以下代码生成的keyPrefix为student:info_5084 */ for (String s : customerParam) { if (s != null) { int keyIdIndex = ArrayUtils.indexOf(parameterNames, s); if (keyIdIndex != -1) { if (args[keyIdIndex] != null) { keyPrefix = keyPrefix.replace("{" + s + "}", String.valueOf(args[keyIdIndex])); } } } } } } // 生成redis key String redisKey = this.getRedisKey(className, methodName, argsString.toString(), keyPrefix); Object result; // 判断这个redisKey是否需要被删除 boolean invalidKey = setOperations.isMember(NEED_TO_DELETE_KEYS_SET, redisKey); // 这个redisKey不需要缓存 if (invalidKey) { // 不需要缓存直接走 result = joinPoint.proceed(args); } else { try { if (!"".equals(keyPrefix)) { keyPrefixArray = keyPrefix.split(":"); } result = this.getRedisData(joinPoint, args, redisKey, timeout, returnType, keyPrefixArray); } catch (TipException e) { throw e; } catch (CacheException cacheException) { // 如果是该方法里的异常,直接返回 throw cacheException.getThrowable(); } catch (Exception e) { // 如果报错了就直接走该方法 result = joinPoint.proceed(args); log.error("error", e); } } return result; } /** * 从缓存中获取数据 * * @param joinPoint 连接点 * @param args 参数 * @param redisKey 缓存key * @param timeout 过期时间 * @param returnType 方法返回类型 * @param keyPrefixArray key数组 * @return java.lang.Object * @author vital */ private Object getRedisData(ProceedingJoinPoint joinPoint, Object[] args, String redisKey, long timeout, Class<?> returnType, String[] keyPrefixArray) throws Throwable { Object result; String json = valueOperations.get(redisKey); // redis没有缓存 if (json == null || "".equals(json)) { String lockKey = METHOD_KEY_LOCK_IDS + ":" + redisKey; // 获取锁,防止重复执行 if (RedissonLockUtils.tryLock(lockKey, TimeUnit.SECONDS, 3, 3)) { try { // 执行该方法返回的结果 try { result = joinPoint.proceed(args); } catch (TipException e) { throw e; } catch (Throwable e) { throw new CacheException("方法内部异常", e); } if (result != null) { String strJson; if (!(result instanceof String)) { strJson = JsonUtils.objectToJson(result); } else { strJson = (String) result; if ("".equals(strJson)) { strJson = NULL_STR; } } if (timeout == -1L) { valueOperations.set(redisKey, strJson); } else { valueOperations.set(redisKey, strJson); stringRedisTemplate.expire(redisKey, timeout, TimeUnit.MILLISECONDS); } } else { if (timeout == -1L) { valueOperations.set(redisKey, NULL_STR); } else { valueOperations.set(redisKey, NULL_STR); stringRedisTemplate.expire(redisKey, timeout, TimeUnit.MILLISECONDS); } } if (keyPrefixArray != null) { // 将prefix中每个前缀都保存到集合中,这样删除缓存的时候可以通过最前面的前缀来删除这个前缀下的所有缓存 // 例如有两个keyPrefix,student:info_5084,student:list_1234 // 那么删除的时候可以通过 ”student“这个前缀来删除上面两个缓存 for (String keyPrefix : keyPrefixArray) { boolean member = setOperations.isMember(METHOD_KEYS_SET + ":" + keyPrefix, redisKey); if (!member) { setOperations.add(METHOD_KEYS_SET + ":" + keyPrefix, redisKey); stringRedisTemplate.expire(METHOD_KEYS_SET + ":" + keyPrefix, 1, TimeUnit.DAYS); } } } } catch (TipException | CacheException e) { throw e; } catch (Exception e) { log.error("读取缓存失败 redisKey:{}", redisKey, e); throw e; } finally { RedissonLockUtils.unlock(lockKey); } } else { // 如果获取锁失败,那么尝试直接从缓存中获取数据 int n = 0; while (true) { Thread.sleep(100L); json = valueOperations.get(redisKey); // 等待5秒超时 if (n > 50) { throw new TimeoutException("获取数据超时!"); } if (json != null && !"".equals(json)) { result = JsonUtils.jsonToBean(json, returnType); break; } n++; } } } else { // redis有缓存 if (NULL_STR.equals(json)) { result = null; } else { if (String.class.equals(returnType)) { result = json; } else { result = JsonUtils.jsonToBean(json, returnType); } } } return result; } /** * 生成RedisKey * * @param className 类名 * @param methodName 方法名 * @param args 方法参数值 * @param keyPrefix key前缀 * @return java.lang.String * @author vital */ private String getRedisKey(String className, String methodName, String args, String keyPrefix) { String redisKey = METHOD_KEYS + ":" + className + ":" + methodName; if (!"".equals(keyPrefix)) { redisKey = redisKey + ":" + keyPrefix; } if (!"".equals(args)) { redisKey = redisKey + ":" + MD5(args); } return redisKey; } /** * 将字符串转为MD5值 * * @param key 字符串 * @return java.lang.String * @author vital */ public static String MD5(String key) { try { byte[] btInput = key.getBytes(); // 获得MD5摘要算法的 MessageDigest 对象 MessageDigest mdInst = MessageDigest.getInstance("MD5"); // 使用指定的字节更新摘要 mdInst.update(btInput); // 获得密文 byte[] md = mdInst.digest(); // 把密文转换成十六进制的字符串形式 int j = md.length; char[] str = new char[j * 2]; int k = 0; for (byte byte0 : md) { str[k++] = HEX_DIGITS[byte0 >>> 4 & 0xf]; str[k++] = HEX_DIGITS[byte0 & 0xf]; } return new String(str); } catch (Exception e) { log.error("MD5 error", e); return null; } } }
 
如果数据更新了,那么要将缓存删除,此处我用一个专门的类来处理,如果是批量删除大的key,可以放到定时任务中去删,如果是小的key,就即刻删除
@Service public class CacheService { @Resource private StringRedisTemplate stringRedisTemplate; /** * 删除缓存 * * @param keys 缓存key * @author vital */ public void deleteKeys(String... keys) { for (String key : keys) { String[] tableArray; if (key.contains(":")) { tableArray = key.split(":"); } else { tableArray = new String[1]; tableArray[0] = key; } for (String table : tableArray) { String tableKey = CacheAspect.METHOD_KEYS_SET + ":" + table; Set<String> members = stringRedisTemplate.opsForSet().members(tableKey); if (CollectionUtils.isNotEmpty(members)) { for (String m : members) { stringRedisTemplate.delete(m); } stringRedisTemplate.delete(tableKey); } } } } }
 
使用方式
@Service public class StudentService { @Resource private StudentMapper studentMapper; @Resource private CacheService cacheService; /** * 获取学员信息,缓存1小时 * * @param studentId 学员id * @return net.vitalfans.cache.mapper.Student * @author vital */ @Cache(keyPrefix = "student:info_{studentId}", timeout = 1000 * 60 * 60) public Student getStudent(Long studentId) { return studentMapper.selectById(studentId); } /** * 更新学员信息并删除缓存 * * @param studentId 学员id * @param studentName 学员名称 * @author vital */ public void updateStudent(Long studentId, String studentName) { Student student = new Student(); student.setStudentId(studentId); student.setStudentName(studentName); studentMapper.updateById(student); // 删除当前学员的缓存,也可以使用 cacheService.deleteKeys("student"); 删除所有学员的缓存 cacheService.deleteKeys("student:info_" + studentId); } }