基於註解的方法緩存
阿新 • • 發佈:2019-03-16
esp count cat stp git out ota exc 查詢
基於註解的方法緩存
在大數據項目中, 查詢impala或hive非常耗時耗資源, 所以, 對於同一方法相同參數的請求, 應該保存到緩存中, 而不是每次都去查數據庫, 方法的返回值可以保存到Mongodb或redis中
具體實現:
@MethodCache註解
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface MethodCache { /** * 指定緩存的過期時間, 默認60秒 * * @return int */ int expireSeconds() default 60; /** * 緩存的key, 如果不指定, 默認按照方法的簽名作為key * * @return String */ String key() default ""; /** * 緩存防擊穿的標誌, 默認是開啟防擊穿功能 * * @return boolean */ boolean limitQuery() default true; /** * 防擊穿的時限 * * @return int */ int limitQuerySeconds() default 5; /** * 是否保存空的結果 * * @return boolean */ boolean saveEmptyResult() default true; }
MethodCacheAspect切面
該切面的核心思想就是用方法的簽名+實參對象的哈希值作為key, 先從緩存中取, 取不到再調用具體方法去查詢, 查詢結果後保存到緩存中, 其中可以設置一次只能有一個線程去查, 還可以設置重試次數, 還可以設置是否保存空結果
@Aspect @Order(value = 2) @Component public class MethodCacheAspect { @Resource private JedisPool jedisPool; @Resource private DisLockUtil disLockUtil; private static final Logger LOGGER = LoggerFactory.getLogger(MethodCacheAspect.class); private static final String EMPTY_RESULT = "NULL"; /** * 切面具體的操作 * * @param proceedingJoinPoint 切面 * @param methodCache 註解 * @return Object * @throws Throwable 拋出異常 */ @Around("@annotation(methodCache)") public Object execute(ProceedingJoinPoint proceedingJoinPoint, MethodCache methodCache) throws Throwable { MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature(); Method method = methodSignature.getMethod(); // 方法參數解析 int size = proceedingJoinPoint.getArgs().length; // 解析請求參數 List<Object> list = parseRequestParam(proceedingJoinPoint, size); // 根據方法獲取相應的key String key = methodCache.key(); if (StringUtils.isBlank(key)) { key = getSignature(method); } key += HashAlgorithms.mixHash(JSON.toJSONString(list)); Object deserialize = tryGetFromCache(key); if (deserialize != null) { // 防止緩存擊穿, 查詢不到數據時也會設置空結果的標記, 避免直接把壓力落到DB上 if (EMPTY_RESULT.equals(deserialize)) { return null; } return deserialize; } Object proceed; String mutexKey = "mutex_" + key; if (methodCache.limitQuery()) { boolean lock = disLockUtil.lock(mutexKey, methodCache.limitQuerySeconds()); // 如果第一次設置分布式鎖失敗, 最多允許重試三次 int count = 1; while (!lock && count < 3) { lock = disLockUtil.lock(mutexKey, methodCache.limitQuerySeconds()); count++; TimeUnit.SECONDS.sleep(1); } if (lock) { // 允許查詢 proceed = executeConcreteMethod(proceedingJoinPoint, mutexKey); Object cacheResult = proceed; // 緩存中不存在, 需要執行方法查詢 if (cacheResult == null) { cacheResult = EMPTY_RESULT; } try (Jedis jedis = jedisPool.getResource()) { jedis.setnx(key.getBytes(), KryoUtil.writeToByteArray(cacheResult)); jedis.expire(key, methodCache.expireSeconds()); } } else { LOGGER.warn("設置防擊穿鎖失敗, key為:{}", mutexKey); throw new CustomException(ErrorCodeEnum.DUPLICATE_REQUEST.getMessage()); } } else { // 允許查詢 proceed = executeConcreteMethod(proceedingJoinPoint, mutexKey); } return proceed; } /** * 執行具體的方法 * * @param proceedingJoinPoint 切面 * @return Object * @throws Throwable 異常 */ private Object executeConcreteMethod(ProceedingJoinPoint proceedingJoinPoint, String mutexKey) throws Throwable { Object proceed; try { proceed = proceedingJoinPoint.proceed(); } finally { disLockUtil.unlock(mutexKey, false); } return proceed; } /** * 嘗試從緩存中獲取 * * @param key key * @return Object */ private Object tryGetFromCache(String key) { byte[] resultBytes; try (Jedis jedis = jedisPool.getResource()) { if (!jedis.exists(key)) { return null; } resultBytes = jedis.get(key.getBytes()); } if (resultBytes != null) { LOGGER.info("key:{}獲取到緩存", key); return KryoUtil.readFromByteArray(resultBytes); } return null; } /** * 解析請求參數 * * @param proceedingJoinPoint 切面 * @param size 參數個數 * @return List<Object> */ private List<Object> parseRequestParam(ProceedingJoinPoint proceedingJoinPoint, int size) { Object[] args = proceedingJoinPoint.getArgs(); List<Object> argList = new ArrayList<>(); for (int i = 0; i < size; i++) { if (args[i] instanceof HttpServletRequest) { HttpServletRequest request = (HttpServletRequest) args[i]; argList.add(request.getParameterMap()); } else if (args[i] instanceof HttpServletResponse || args[i] instanceof HttpSession || args[i] instanceof HttpCookie) { continue; } else { argList.add(args[i]); } } return argList; } /** * 生成方法簽名 * * @param method 方法 * @return String */ private String getSignature(Method method) { StringBuilder sb = new StringBuilder(); String methodName = method.getName(); if (StringUtils.isNotBlank(methodName)) { sb.append(method).append("#"); } return sb.toString(); } }
項目已經上傳到gitee和github
gitee: https://gitee.com/ericwo/second-kill
github: https://github.com/wangjisong1993/second-kill
基於註解的方法緩存