Springboot 關於緩存的一些思路 關於Mysql Redis

前言

關於 Springboot AOP 集成控制 Redis實現緩存
網上有很多相關的例子,我也稍微瞭解了一二。
但我習慣還是自己折騰一遍,便於理解整個過程。
所以本文的方法和網上的略有不同,也可能不是最優解,只是記錄自己折騰的過程。

方法1 AOP + Redis

本方法適合一些訪問頻率較高,響應時間較長的Controller,具體就是查SQL拼JSON的過程會比較慢的Controller,對數據實時程度要求也不那麼高的話,第一次跑完把結果存redis,之後一段時間內直接讀redis來返回結果即可。

具體流程如下

  • 通過AOP 非侵入性實現,不破壞原來的Controller
  • 用方法名+參數JSON取MD5
  • MD5作爲key,返回值JsonString作爲value存redis
  • 執行前查詢redis存在緩存則直接返回緩存數據
  • 不存在緩存正常執行方法,執行完成後保存緩存數據

親測速度可以從2000ms 提升到 20ms

主要代碼如下

/**
 * AOP 切面 用於緩存數據
 */
@Aspect
@Component
public class ApiControllerCacheAspect {
    private final static Logger log = LoggerFactory.getLogger(ApiLogAspect.class);
    /**
     * 默認過期時長 3小時
     */
    public final static long DEFAULT_EXPIRE = 60 * 60 * 3;

    @Autowired
    private RedisUtils redisUtils;

    // 切面的對象,這裏指定了Controller來防止不需要緩存的也被緩存
    @Pointcut("execution(* com.zzzmh.api.controller.ApiController.*(..))")
    public void loginPointCut() {

    }

    @Around("loginPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        R result = R.error();
        try {
            // 從切面中獲取方法名 + 參數名
            String methodName = ((MethodSignature) point.getSignature()).getMethod().getName();
            String params = JSON.toJSONString(point.getArgs());
            // 轉換成md5
            String md5 = DigestUtils.md5Hex(methodName + params);
            // 從redis獲取緩存
            String cache = redisUtils.get(md5);
            if (StringUtils.isBlank(cache)) {
                // 讀不到緩存正常執行方法
                result = (R) point.proceed();
                // 執行完畢後結果寫入Redis緩存
                redisUtils.set(md5, result.get("result"), DEFAULT_EXPIRE);
            } else {
                // 讀取到緩存直接返回,不執行方法
                result = R.ok(JSON.parseArray(cache));
            }
        } catch (RRException e) {
            result.put("code", e.getCode());
            result.put("msg", e.getMsg());
        } catch (Exception e) {
            log.error("AOP 執行中異常 :" + e.toString());
            e.printStackTrace();
        }
        return result;
    }
}

方法1.5 AOP + Redis 加強進階版

這幾天在折騰過程中發現,按照Controller來緩存,顆粒太粗,
一些Controller 或 Controller裏的一些方法不需要緩存。
另外返回碼正確的才需要緩存,返回錯誤不應執行緩存。
於事想到用自定義註解搭配AOP來實現精細化的緩存
由於涉及的代碼的地方較多,就選最主要的貼出來講了

具體流程如下

  • 先實現一個自定義註解 Cache.java 參數time 默認0
  • 在需要緩存的method上加註解@Cache
  • 若參數time = 0 說明沒有設置緩存時間,根據統一配置時間緩存
  • 若參數time != 0 說明設置過緩存時間,按照設置的時間緩存
  • 如果沒有緩存正常執行方法,結束執行後先驗證狀態碼,正確的才緩存本次數據。

註解類 /annotation/Cache.java

/**
 * 緩存控制
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Cache {
    /**
     * 緩存時間
     * 有該註解的全部緩存
     * time默認0 爲根據數據庫sys_config配置時間爲準
     * time非0 根據註解時間緩存
     * */
    long time() default 0;
}

需要緩存的Controller

    // 指定緩存時間
    @Cache(time = 3600L)
    @PostMapping("getDataList")
    public R getDataList() {
        return R.ok();
    }
    // 不指定緩存時間 通過統一配置的時間緩存
    @Cache
    @PostMapping("getDataList")
    public R getDataList() {
        return R.ok();
    }

AOP切面的核心代碼

/**
 * AOP 切面 用於緩存數據
 */
@Aspect
@Component
public class ApiControllerCacheAspect {
    private final static Logger log = LoggerFactory.getLogger(ApiLogAspect.class);
    /**
     * 默認過期時長 3小時
     */
    public final static long DEFAULT_EXPIRE = 60 * 60 * 3;

    @Autowired
    private RedisUtils redisUtils;

    // 切面的對象,這裏指定了Controller來防止不需要緩存的也被緩存
    @Pointcut("execution(* com.zzzmh.api.controller.ApiController.*(..))")
    public void loginPointCut() {

    }

    @Around("loginPointCut()")
    public Object around(ProceedingJoinPoint point) throws Throwable {
        R result = R.error();
        try {
            // 從切面中獲取方法名 + 參數名
            Method method = ((MethodSignature) point.getSignature()).getMethod();
            String methodName = method.getName();
            // 不支持參數包含HttpServletRequest等 如需要建議用@Autowired注入
            String params = point.getArgs() == null ? "" : JSON.toJSONString(point.getArgs());
            Cache cache = method.getAnnotation(Cache.class);
            // 不爲空說明該方法有此註解
            if (cache != null) {
                // 從redis獲取緩存
                String jsonString = redisUtils.get(md5);
                if (StringUtils.isBlank(jsonString)) {
                    // 讀不到緩存正常執行方法
                    result = (R) point.proceed();
                    // 執行完畢後結果寫入Redis緩存 只緩存正確數據
                    if((int) result.get("code") == 0){
                        // Cache.time() 默認值是0 如等於0 使用統一緩存時間 如不等於0 說明需要自定義,用Cache.time()
                        long time = cache.time() != 0 ? cache.time() : DEFAULT_EXPIRE;
                        redisUtils.set(md5, result.get("result"), time);
                    }
                } else {
                    // 讀取到緩存直接返回,不執行方法
                    result = R.ok(JSON.parseArray(jsonString));
                }
            }else{
                result = (R) point.proceed();
            }
        } catch (RRException e) {
            result.put("code", e.getCode());
            result.put("msg", e.getMsg());
        } catch (Exception e) {
            log.error("AOP 執行中異常 :" + e.toString());
            e.printStackTrace();
        }
        return result;
    }
}

再補充一種需求
如果特殊情況下前端不希望某次請求讀取到緩存,在 request -> header 中加入 no-cache 來阻止緩存。

// 在切面中加入獲取 request
HttpServletRequest request = (HttpServletRequest) RequestContextHolder.getRequestAttributes().resolveReference(RequestAttributes.REFERENCE_REQUEST);
// 獲取 header
String NoCache = request.getHeader("no-cache");
// 判斷是否緩存中加入NoCache字段判斷
/* 例如:
 * if("true".equalsIgnoreCase(NoCache))
 * 不走緩存 直接正常查詢SQL返回
 *
 * 除此之外如果有需要嚴格禁止緩存的話?
 * Mysql的查詢語句也可以加上 SQL_NO_CACHE 來防止Mysql緩存
 */

方法2 Redis + Mysql

核心思路
拋棄Mysql,以Redis數據爲主讀寫,Mysql作爲備份方案
直接在Redis進行數據讀寫,
Mysql開一張表也是Key Value記錄數據,
最大長度支持到varchar(20000)
每次Redis寫數據完成後,都再異步處理整個Value存一次Mysql。
每次Redis讀取數據都判斷一下是否讀到,
讀不到的時候再去Mysql讀,
Mysql讀到就存redis並返回。
好處就是最大化讀寫速度,
缺點是最大長度不能超過2萬、
特殊情況下也會造成數據丟失等。
只能說是一定程度下減少Redis數據丟失風險,
只需要備份Mysql即可。

未完待續

END

我相信網上的方法比這個好的還有很多。但很多東西還是要自己去試着做一遍才瞭解其流程、規律。

本文同時也會發布在我的個人博客
https://zzzmh.cn/single?id=68

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章