場景
若依前后端分離版本地搭建開發環境並運行項目的教程:
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只取用需要的數據,然后給其他比如高頻率的定時任務查詢用。