SpringBoot中通過自定義緩存注解(AOP切面攔截)實現數據庫數據緩存到Redis


場景

若依前后端分離版本地搭建開發環境並運行項目的教程:

https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/108465662

基於上面搭建起來前后端分離的Vue+SpringBoot的項目。

其中數據庫使用的是Mysql,緩存層用的是Redis。

數據庫中某個表存儲的信息,在業務代碼比如定時任務中,需要頻繁的查詢。

所以將表中的數據存儲到redis中一份。

其原理是,在調用查詢方法時,判斷redis中是否已經有,如果有則優先從redis中查詢。

如果沒有則在數據庫中查詢后並存入到Redis中一份,並給其設置過期時間。

這樣在過期時間之內,查詢數據會從redis中查詢,過期之后會重新從Mysql中查詢並存入到Redis一份。

並且還要實現,再對這個Mysql表進行新增、編輯、刪除的操作時,將redis中存儲的數據

進行刪除,這樣下次查詢就會查詢數據庫中最新的。

注:

博客:
https://blog.csdn.net/badao_liumang_qizhi
關注公眾號
霸道的程序猿
獲取編程相關電子書、教程推送與免費下載。

實現

首先在Mysql中新建一個表bus_student

 

 

然后基於此表使用代碼生成,前端Vue與后台各層代碼生成並添加菜單。

 

 

然后來到后台代碼中,在后台框架中已經添加了操作redis的相關依賴和工具類。

但是這里還需要添加aspect依賴

        <!-- https://mvnrepository.com/artifact/org.springframework/spring-aspects -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aspects</artifactId>
            <version>4.3.14.RELEASE</version>
        </dependency>

 

然后在存放配置類的地方新建新增redis緩存的注解

package com.ruoyi.system.redisAop;


import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/*
 * @Author
 * @Description 新增redis緩存
 **/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AopCacheEnable {
    //redis緩存key
    String[] key();
    //redis緩存存活時間默認值(可自定義)
    long expireTime() default 3600;

}

 

以及刪除redis緩存的注解

package com.ruoyi.system.redisAop;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/*
 * @Description 刪除redis緩存注解
 **/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AopCacheEvict {
    //redis中的key值
    String[] key();
}

 

然后再新建一個自定義緩存切面具體實現類CacheEnableAspect

存放位置

 

 

package com.ruoyi.system.redisAop;


import com.ruoyi.system.domain.BusStudent;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/*
 * @Description 自定義緩存切面具體實現類
 **/
@Aspect
@Component
public class CacheEnableAspect {

    @Autowired
    public RedisTemplate redisCache;

    /**
     * Mapper層切點 使用到了我們定義的 AopCacheEnable 作為切點表達式。
     */
    @Pointcut("@annotation(com.ruoyi.system.redisAop.AopCacheEnable)")
    public void queryCache() {
    }

    /**
     * Mapper層切點 使用到了我們定義的 AopCacheEvict 作為切點表達式。
     */
    @Pointcut("@annotation(com.ruoyi.system.redisAop.AopCacheEvict)")
    public void ClearCache() {
    }

    @Around("queryCache()")
    public Object Interceptor(ProceedingJoinPoint pjp) {
        Object result = null;
        //注解中是否有#標識
        boolean spelFlg = false;
        //判斷是否需要走數據庫查詢
        boolean selectDb = false;
        //redis中緩存的key
        String redisKey = "";
        //獲取當前被切注解的方法名
        Method method = getMethod(pjp);
        //獲取當前被切方法的注解
        AopCacheEnable aopCacheEnable = method.getAnnotation(AopCacheEnable.class);
        //獲取方法參數值
        Object[] arguments = pjp.getArgs();
        //從注解中獲取字符串
        String[] spels = aopCacheEnable.key();
        for (String spe1l : spels) {
            if (spe1l.contains("#")) {
                //注解中包含#標識,則需要拼接spel字符串,返回redis的存儲redisKey
                redisKey = spe1l.substring(1) + arguments[0].toString();
            } else {
                //沒有參數或者參數是List的方法,在緩存中的key
                redisKey = spe1l;
            }
            //取出緩存中的數據
            result = redisCache.opsForValue().get(redisKey);
            //緩存是空的,則需要重新查詢數據庫
            if (result == null || selectDb) {
                try {
                    result =  pjp.proceed();
                    //從數據庫查詢到的結果不是空的
                    if (result != null && result instanceof ArrayList) {
                        //將redis中緩存的結果轉換成對象list
                        List<BusStudent> students = (List<BusStudent>) result;
                        //判斷方法里面的參數是不是BusStudent
                        if (arguments[0] instanceof BusStudent) {
                            //將rediskey-students 存入到redis
                            redisCache.opsForValue().set(redisKey, students, aopCacheEnable.expireTime(), TimeUnit.SECONDS);
                        }
                    }
                } catch (Throwable e) {
                    e.printStackTrace();
                }
            }
        }
        return result;
    }

    /*** 定義清除緩存邏輯,先操作數據庫,后清除緩存*/
    @Around(value = "ClearCache()")
    public Object evict(ProceedingJoinPoint pjp) throws Throwable {
        //redis中緩存的key
        Method method = getMethod(pjp);
        // 獲取方法的注解
        AopCacheEvict cacheEvict = method.getAnnotation(AopCacheEvict.class);
        //先操作db
        Object result = pjp.proceed();
        // 獲取注解的key值
        String[] fieldKeys = cacheEvict.key();
        for (String spe1l : fieldKeys) {
            //根據key從緩存中刪除
            redisCache.delete(spe1l);
        }
        return result;
    }

    /**
     * 獲取被攔截方法對象
     */
    public Method getMethod(ProceedingJoinPoint pjp) {
        Signature signature = pjp.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method targetMethod = methodSignature.getMethod();
        return targetMethod;
    }
}

 

注意這里的queryCache和ClearCache,里面切點表達式

分別對應上面自定義的兩個AopCacheEnable和AopCacheEvict。

然后在環繞通知的queryCache方法執行前后時

獲取被切方法的參數,參數中的key,然后根據key去redis中去查詢,

如果查不到,就把方法的返回結果轉換成對象List,並存入到redis中,

如果能查到,則將結果返回。

然后找到這個表的查詢方法,mapper層,比如要將查詢的返回結果存儲進redis

    @AopCacheEnable(key = "BusStudent",expireTime = 40)
    public List<BusStudent> selectBusStudentList(BusStudent busStudent);

 

然后在這個表的新增、編輯、刪除的mapper方法上添加

    /**
     * 新增學生
     *
     * @param busStudent 學生
     * @return 結果
     */
    @AopCacheEvict(key = "BusStudent")
    public int insertBusStudent(BusStudent busStudent);

    /**
     * 修改學生
     *
     * @param busStudent 學生
     * @return 結果
     */
    @AopCacheEvict(key = "BusStudent")
    public int updateBusStudent(BusStudent busStudent);

    /**
     * 刪除學生
     *
     * @param id 學生ID
     * @return 結果
     */
    @AopCacheEvict(key = "BusStudent")
    public int deleteBusStudentById(Integer id);

 

注意這里的注解上的key要和上面的查詢的注解的key一致。

然后啟動項目,如果啟動時提示:

Consider marking one of the beans as @Primary, updating the consumer to acce

 

 

因為sringboot通過@Autowired注入接口的實現類時發現有多個,也就是有多個類繼承了這個接口,spring容器不知道使用哪一個。

找到redis的配置類,在RedisTemplate上添加@Primary注解

 

 

 

驗證注解的使用

debug啟動項目,在CacheEnableAspect中查詢注解中打斷點,然后調用查詢方法,

就可以看到能進斷點,然后就可以根據自己想要的邏輯和效果進行修改注解。

 

 

第一次查詢時redis中是沒有的,所以會走mysql查詢,在過期時間之內就不再查詢mysq,而是查詢redis了。

 

 

然后再調用新增、編輯、刪除接口時會將redis中緩存的數據刪掉。

但是使用若依這套框架,在新增、編輯、刪除操作后會調用查詢接口,所以會直接又存儲進來。

所以可以用postman等接口測試工具測試。

然后就是當操作完之后如果redis中的數據還沒過期,前端頁面查詢的仍然是redis中的數據,不是最新數據。

所以redis中過期的時間自己把握。

另外此種緩存機制,建議不要和前端請求的mapper進行混用。

建議自定義新的mapper只取用需要的數據,然后給其他比如高頻率的定時任務查詢用。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM