【原】Spring AOP實現對Redis的緩存同步

    前言:剛開始採用spring cache做爲緩存數據,到後面發現擴展性不靈活,因而基於sprig cache原理自定義一套規則用於緩存數據。


 

請求過程:

  1. 根據請求參數生成Key,後面咱們會對生成Key的規則,進一步說明;
  2. 根據Key去緩存服務器中取數據,若是取到數據,則返回數據,若是沒有取到數據,則執行service中的方法調用dao從DB中獲取數據,同時成功後將數據放到緩存中。
  3. 刪除、新增、修改會觸發更新緩存的攔截類對緩存服務器進行更新。

 

    1.首先貼上核心註解類redis

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
public @interface RedisLogService {

   
    enum CACHE_OPERATION {
        FIND, // 查詢緩存操做
        UPDATE, // 須要執行修改緩存的操做
        INSERT; // 須要執行新增緩存的操做
    }

    /** 存儲的分組 */
    String[] group();

    /** 當前緩存操做類型 */
    CACHE_OPERATION cacheOperation() default CACHE_OPERATION.FIND;

    /** 存儲的Key 默認加入類名跟方法名 */
    String key() default "";

    /** 是否使用緩存 */
    boolean use() default true;

    /** 超時時間 */
    int expire() default 0;

    enum LOG_OPERATION {
        ON, // 開啓日誌記錄
        OFF, // 關閉日誌記錄
    }

    /** 當前緩存操做類型 */
    LOG_OPERATION logOperation() default LOG_OPERATION.ON;

    /** 操做名稱 */
    String name() default "";

    /** 操做參數 */
    String param() default "";

    /** 日誌參數 操做人操做IP,操做IP歸屬地 */
    String logParam() default "";

 

 2.使用註解案例。spring

@RedisLogService(group = {
            "group.news" }, key = "#record", name = "網站維護-公司新聞管理-分頁查詢公司新聞", param = "#record", logParam = "#map")

    解釋下上面註解:根據業務的須要,將緩存key進行分組,第一個group參數便是分組,用來標識某個模塊,例如新聞模塊統一是group.news;第二個key是根據參數拼接成的key,第三個name只是一個名稱而已,沒什麼太大的做用,主要是用於給其它開發人員理解, 第四個param則是操做參數,這個很重要,到時候會用它來拼接key,第五個logParam是日誌。數據庫

 

3.貼上具體攔截類緩存

@Aspect
@Order(value = 1)
@Component("redisLogServiceInterceptor")
public class RedisLogServiceInterceptor {

    private static final Logger LOGGER = LoggerFactory.getLogger(RedisLogServiceInterceptor.class);

    @Autowired
    private UserLogRecordService userLogRecordService;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 
     * 
     * @Title: execute
     * @Description: 切入點業務邏輯
     * @param proceedingJoinPoint
     * @return
     */
    @Around("@annotation(RedisLogService)")
    public Object execute(ProceedingJoinPoint proceedingJoinPoint) throws ServiceException {
        Object result = null;

        try {
            Method method = getMethod(proceedingJoinPoint);

            // 獲取註解對象
            RedisLogService redisLogService = method.getAnnotation(RedisLogService.class);

            // 判斷是否使用緩存
            boolean useRedis = redisLogService.use();

            if (useRedis) {

                // 使用redis
                ValueOperations<String, Object> operations = redisTemplate.opsForValue();

                // 判斷當前操做
                switch (redisLogService.cacheOperation()) {

                case FIND:

                    result = executeDefault(redisLogService, operations, proceedingJoinPoint, method);

                    break;
                case UPDATE:

                    result = executeUpdate(redisLogService, operations, proceedingJoinPoint);

                    break;
                case INSERT:

                    result = executeInsert(redisLogService, operations, proceedingJoinPoint);

                    break;
                default:

                    result = proceedingJoinPoint.proceed();

                    break;
                }
            } else {

                result = proceedingJoinPoint.proceed();
            }

        } catch (ServiceException e) {
            throw e;
        } catch (Throwable e) {
            throw new ServiceException(new Result<Object>("500", e.getMessage()), e);
        }
        return result;
    }

  /**
     *
     * @Title: getMethod
     * @Description: 獲取被攔截方法對象
     * @param joinPoint
     * @return
     */
    protected Method getMethod(JoinPoint joinPoint) throws Exception {

        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();

        Method method = methodSignature.getMethod();

        return method;
    }

    上面的代碼使用了@Around環繞切面這個註解,爲何不用@Befor或者@After呢?服務器

      因爲@Befor是在方法執行開始前才進行切面,而@After是方法結束後進行切面。 根據業務場景的須要,@Around 能夠在所攔截方法的先後執行一段邏輯,例如在查詢前先去Redis查數據,發現沒有數據再回到service層去執行查db,查完了以後須要把數據從新放到Redis,此時其餘線程的請求就能夠直接從Redis得到數據,減小頻繁對數據庫的操做。app

 4.下面貼上查詢的具體實現方法ide

/**
     * 
     * @Title: executeDefault
     * @Description: 默認操做的執行
     * @param redisLogService
     * @param result
     * @param operations
     * @param proceedingJoinPoint
     * @param method
     * @throws Throwable
     */
    @SuppressWarnings("unchecked")
    private Object executeDefault(RedisLogService redisLogService, ValueOperations<String, Object> operations,
            ProceedingJoinPoint proceedingJoinPoint, Method method) throws Throwable {

        Object result = null;

        Object[] args = proceedingJoinPoint.getArgs();

        // 獲取被攔截方法參數名列表(使用Spring支持類庫)
        LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();

        String[] paraNameArr = u.getParameterNames(method);

        // 獲取key的後綴的參數名
        String key = redisLogService.key();

        if (StringUtils.isNotBlank(key)) {
            // 使用SPEL進行key的解析
            ExpressionParser parser = new SpelExpressionParser();

            // SPEL上下文
            StandardEvaluationContext context = new StandardEvaluationContext();

            // 把方法參數放入SPEL上下文中
            for (int i = 0; i < paraNameArr.length; i++) {

                context.setVariable(paraNameArr[i], args[i]);
            }

            Object object = parser.parseExpression(key).getValue(context);

            if (null != object) {

                if (object instanceof Map<?, ?>) {

                    key = GzdtlStringUtil.transMapToString((Map<String, Object>) object);

                } else if (object instanceof Collection<?>) {

                    Collection<Object> collection = (Collection<Object>) object;

                    StringBuffer stringBuffer = new StringBuffer();

                    for (Object o : collection) {

                        stringBuffer.append(o.toString());
                    }

                    key = stringBuffer.toString();
                } else {

                    key = object.toString();
                }
            }
        }

        String className = proceedingJoinPoint.getTarget().getClass().getName();

        if (className.indexOf(".") >= 0) {

            className = className.substring(className.lastIndexOf(".") + 1, className.length());
        }

        String methodName = method.getName();

        String[] group = redisLogService.group();

        if (null != group && group.length > 0) {

            if (StringUtils.isNotBlank(key)) {

                key = group[0] + ":" + className + ":" + methodName + ":" + key;
            } else {

                key = group[0] + ":" + className + ":" + methodName;
            }
        } else {

            if (StringUtils.isNotBlank(key)) {

                key = "group" + ":" + className + ":" + methodName + ":" + key;
            } else {

                key = "group" + ":" + className + ":" + methodName;
            }
        }

        result = operations.get(key);

        // 若是緩存沒有數據則更新緩存
        if (result == null) {

            result = proceedingJoinPoint.proceed();

            int expire = redisLogService.expire();

            // 更新緩存
            if (expire > 0) {

                operations.set(key, result, expire, TimeUnit.SECONDS);
            } else {

                operations.set(key, result);
            }
        }

        return result;
    }
View Code

    proceedingJoinPoint.getArgs() 做用,瞭解過aop 以及反射相關的都知道這是從方法內取出傳入參數,例如傳入的是 (String user,String age), 經過這個方法能夠分別獲得user和age的值。網站

   例如以下方法:lua

public Result<PageInfo<WebInfoBase>> findPageByParam(WebInfoFindParam record, Map<String, String> map)spa

  1. 上面這個方法我是傳入的第一個參數是實體bean(傳入的查詢參數別名必定要寫record,由於在aop緩存攔截代碼裏已經顯示規定了)

String[] paraNameArr = u.getParameterNames(method);

   從paraNameArr獲取參數的別名分別是recordmap, 注意在上面的代碼裏裏我用到了自定義redis註解: key = "#record" , 接着使用SPEL表達式進行解析取出record的值,爲何要取出record的值是由於須要拼接key存放到redis,因此record做爲key拼接的條件之一是必須的。

   分析完畢後舉個請求的例子

                  假設用戶id = 1,分頁查詢了訂單信息,這時候 record 參數爲:pageSize:10,pageNum:2,id:1。key的最終格式 : grop+namespace+record(這樣基本是惟一不會重複)。

相關文章
相關標籤/搜索