導航:
通過註解實現接口自動緩存Redis和數據過期功能
一. 概述
1.1 Redis是什麼?
- 在java中,常見的數據持久化有幾種類型。
- 關係型數據庫:MySQL 、Orcale 、 SQL Server等。
- 非關係型數據庫: MongDB、Redis、Solr、ElasticSearch等。
- 總結:Redis是一個非關係型數據庫。它的業務執行是在內存中進行的,因此性能高,同時它是單線程的,一定程度上是線程安全的。如果接口查詢速度很慢,我們可以提前存入Redis,當接口接收到同樣的請求參數時,即可從Redis中直接拿取數據,不必再進行邏輯處理。如果數據要求及時性,我們可以通過設置Redis的過期時間來保證數據的及時更新。
1.1 爲什麼要通過註解實現接口自動緩存Redis和數據過期功能?
- 部分接口查詢速度慢,且通過優化也無法有效提升查詢速度時,需要通過查詢Redis進行提速;
- 使用註解,操作簡單方便、代碼侵入小、工作量低、易於維護、設計優雅。
使用Redis做緩存就是,在首次查詢時,將查詢結果存入了Redis數據庫中,後續的查詢均可以直接從Redis取值,而不必做邏輯處理,這樣做能很快的返回數據,提高用戶體驗。
1.2 使用緩存功能的業務展示:
- 使用註解:
- 調用接口:
- 我們調用接口後,檢查Redis裏面是否存在這個接口所緩存的數據:
說明我們調用此接口緩存成功了。調用這個接口的時候,會自動獲取key和參數,並做緩存。下次調這個接口的時候,如果遇到相同的參數則直接返回結果。
- 再使用其他參數測試是否能分別緩存,發現能分別存入。功能實現成功!
- 輸入其他參數調用接口:
- 發現能動態存入Redis,如圖:
注意,在圖中,我們設置的這個接口的expire過期時間爲1分鐘,所以過期時間很快,需要注意。在調用接口的過程中,發現接口返回的速度明顯提高,說明已先走Redis進行查詢了。也可以用Thred.sleep(3000L) 線程睡眠來模擬查詢慢的接口。
- 輸入其他參數調用接口:
二. 手把手實戰實現Redis註解緩存
2.1 實現步驟
- 博主使用環境:java8 + SpringBoot+SpringCloud+SpringDataRedis
- 使用目錄如圖:
- 包結構:
RedisUtils這個只是作爲存入Redis的工具。在看懂源碼的基礎上,小夥伴們可以將RedisUtils替換爲自己公司或者自己喜歡的Redis存入方式,也可以直接使用。
- 包結構:
- 如果你在你的工程中已經配置好了Redis的配置時,即可直接使用。使用方式就是將這四個類直接拷貝到你所在的項目中,然後在你需要緩存的地方加上@RedisCache註解即可。(註解內需要配置Key和field,expire看需要,如果不配置則會自動使用註解內的默認exipre的值)
@Pointcut("@annotation(com.xxx.common.redis.annotation.RedisCache)") 這裏的RedisCache指向的路徑是RedisCache的路徑。你需要進行修改,修改爲自己工程中RedisCache所在的位置路徑。其他同理。
2.2 註解類 1:
package com.cdmtc.annotation.redis;
import java.lang.annotation.*;
/**
* @program: cdmtc.commond.platform
* @description: redis緩存對象
* @author: 暗餘
* @create: 2019-12-05
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RedisCache {
/**
* 鍵名
*
* @return
*/
String key() default "";
/**
* 主鍵
*
* @return
* @author zmr
*/
String fieldKey();
/**
* 過期時間
*
* @return
*/
long expired() default 0; // 默認一天
/**
* 是否爲查詢操作
* 如果爲寫入數據庫的操作,該值需置爲 false
*
* @return
*/
boolean read() default true;
}
2.3 註解類2:
package com.cdmtc.annotation.redis;
import java.lang.annotation.*;
/**
* @program: cdmtc.commond.platform
* @description: redis緩存對象
* @author: 暗餘
* @create: 2019-12-05
*/
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedisEvict {
String key();
String fieldKey();
}
2.4 RedisUtils類
package com.cdmtc.annotation.redis;
import com.alibaba.fastjson.JSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Redis工具類
*/
@Component
public class RedisUtils {
@Autowired
private RedisTemplate redisTemplate;
@Resource(name = "stringRedisTemplate")
private ValueOperations<String, String> valueOperations;
/**
* 默認過期時長,單位:秒
*/
public final static long DEFAULT_EXPIRE = 60 * 60 * 24;
/**
* 不設置過期時長
*/
public final static long NOT_EXPIRE = -1;
/**
* 插入緩存默認時間
*
* @param key 鍵
* @param value 值
* @author zmr
*/
public void set(String key, Object value) {
set(key, value, DEFAULT_EXPIRE);
}
/**
* 插入緩存
*
* @param key 鍵
* @param value 值
* @param expire 過期時間(s)
* @author zmr
*/
public void set(String key, Object value, long expire) {
valueOperations.set(key, toJson(value));
redisTemplate.expire(key, expire, TimeUnit.SECONDS);
}
/**
* 返回字符串結果
*
* @param key 鍵
* @return
* @author zmr
*/
public String get(String key) {
return valueOperations.get(key);
}
/**
* 返回指定類型結果
*
* @param key 鍵
* @param clazz 類型class
* @return
* @author zmr
*/
public <T> T get(String key, Class<T> clazz) {
String value = valueOperations.get(key);
return value == null ? null : fromJson(value, clazz);
}
/**
* 刪除緩存
*
* @param key 鍵
* @author zmr
*/
public void delete(String key) {
redisTemplate.delete(key);
}
public void delectUserInfo(String key, String token) {
BoundHashOperations boundHashOperations = redisTemplate.boundHashOps(key);
boundHashOperations.delete(token);
}
;
/**
* Object轉成JSON數據
*/
private String toJson(Object object) {
if (object instanceof Integer || object instanceof Long || object instanceof Float || object instanceof Double
|| object instanceof Boolean || object instanceof String) {
return String.valueOf(object);
}
return JSON.toJSONString(object);
}
/**
* JSON數據,轉成Object
*/
private <T> T fromJson(String json, Class<T> clazz) {
return JSON.parseObject(json, clazz);
}
/**
* 獲取過期時間
*/
public Long getTime(String key) {
return redisTemplate.getExpire(key);
}
/**
* hash入對象
*/
public void setForObject(String key, Map<String, Object> map, Long time) {
BoundHashOperations boundHashOperations = redisTemplate.boundHashOps(key);
Set<String> keySet = map.keySet();
for (String s : keySet) {
boundHashOperations.put(s, map.get(s));
boundHashOperations.expire(time, TimeUnit.MICROSECONDS);
}
}
public Object getObject(String Bigkey, String SmallKey) {
BoundHashOperations boundHashOperations = redisTemplate.boundHashOps(Bigkey);
return boundHashOperations.get(SmallKey);
}
/**
* 插入Set
*/
public void setForSet(String key, String value, Long time) {
BoundSetOperations<String, Object> boundSetOperations = redisTemplate.boundSetOps(key);
boundSetOperations.add(value);
boundSetOperations.expire(time, TimeUnit.SECONDS);
}
/**
* 是否存在於SET
*
* @param key
*/
public Boolean isInSet(String key, String value) {
BoundSetOperations<String, Object> boundSetOperations = redisTemplate.boundSetOps(key);
return boundSetOperations.isMember(value);
}
/**
* 加鎖
*
* @param key 商品id
* @param value 當前時間+超時時間
* @return
*/
public boolean lock(String key, String value) {
if (redisTemplate.opsForValue().setIfAbsent(key, value)) {
redisTemplate.expire(key, 1000, TimeUnit.SECONDS);
return true;
}
//避免死鎖,且只讓一個線程拿到鎖
String currentValue = redisTemplate.opsForValue().get(key).toString();
//如果鎖過期了
if (!StringUtils.isEmpty(currentValue) && Long.parseLong(currentValue) < System.currentTimeMillis()) {
//獲取上一個鎖的時間
String oldValues = redisTemplate.opsForValue().getAndSet(key, value).toString();
/*
只會讓一個線程拿到鎖
如果舊的value和currentValue相等,只會有一個線程達成條件,因爲第二個線程拿到的oldValue已經和currentValue不一樣了
*/
if (!StringUtils.isEmpty(oldValues) && oldValues.equals(currentValue)) {
return true;
}
}
return false;
}
/**
* 解鎖
*
* @param key
* @param value
*/
public void unlock(String key, String value) {
try {
String currentValue = redisTemplate.opsForValue().get(key).toString();
if (!StringUtils.isEmpty(currentValue) && currentValue.equals(value)) {
redisTemplate.opsForValue().getOperations().delete(key);
System.out.println("釋放鎖");
}
} catch (Exception e) {
System.out.println("解鎖異常");
}
}
}
2.5 切面類
package com.ruoyi.common.redis.aspect;
import java.lang.reflect.Method;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
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.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.LocalVariableTableParameterNameDiscoverer;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.stereotype.Component;
import com.ruoyi.common.redis.annotation.RedisCache;
import com.ruoyi.common.redis.annotation.RedisEvict;
import com.ruoyi.common.redis.util.RedisUtils;
@Component
@Aspect
public class RedisAspect {
private final static Logger logger = LoggerFactory.getLogger(RedisAspect.class);
@Autowired
private RedisUtils redis;
/**
* 定義切入點,使用了 @RedisCache 的方法
*/
@Pointcut("@annotation(com.xxx.common.redis.annotation.RedisCache)")
public void redisCachePoint() {
}
@Pointcut("@annotation(com.xxx.common.redis.annotation.RedisEvict)")
public void redisEvictPoint() {
}
@After("redisEvictPoint()")
public void evict(JoinPoint point) {
Method method = ((MethodSignature) point.getSignature()).getMethod();
RedisEvict redisEvict = method.getAnnotation(RedisEvict.class);
// 獲取RedisCache註解
String fieldKey = parseKey(redisEvict.fieldKey(), method, point.getArgs());
String rk = redisEvict.key() + ":" + fieldKey;
logger.debug("<======切面清除rediskey:{} ======>" + rk);
redis.delete(rk);
}
/**
* 環繞通知,方法攔截器
*/
@Around("redisCachePoint()")
public Object WriteReadFromRedis(ProceedingJoinPoint point) {
try {
Method method = ((MethodSignature) point.getSignature()).getMethod();
// 獲取RedisCache註解
RedisCache redisCache = method.getAnnotation(RedisCache.class);
Class<?> returnType = ((MethodSignature) point.getSignature()).getReturnType();
if (redisCache != null && redisCache.read()) {
// 查詢操作
logger.debug("<======method:{} 進入 redisCache 切面 ======>", method.getName());
String fieldKey = parseKey(redisCache.fieldKey(), method, point.getArgs());
String rk = redisCache.key() + ":" + fieldKey;
Object obj = redis.get(rk, returnType);
if (obj == null) {
// Redis 中不存在,則從數據庫中查找,並保存到 Redis
logger.debug("<====== Redis 中不存在該記錄,從數據庫查找 ======>");
obj = point.proceed();
if (obj != null) {
if (redisCache.expired() > 0) {
redis.set(rk, obj, redisCache.expired());
} else {
redis.set(rk, obj);
}
}
}
return obj;
}
} catch (Throwable ex) {
logger.error("<====== RedisCache 執行異常: {} ======>", ex);
}
return null;
}
/**
* 獲取緩存的key
* key 定義在註解上,支持SPEL表達式
*
* @param pjp
* @return
*/
private String parseKey(String key, Method method, Object[] args) {
// 獲取被攔截方法參數名列表(使用Spring支持類庫)
LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer();
String[] paraNameArr = u.getParameterNames(method);
// 使用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]);
}
return parser.parseExpression(key).getValue(context, String.class);
}
}
三. 分析源碼
3.1 概述:
- 通過源碼可以看到,註解緩存的實現邏輯很清晰;就是通過
① 註解獲取類名,參數名,方法名;
② 切面RedisAspect類中的環繞通知方法WriteReadFromRedis進行處理;
③ 通過反射獲取到註解截獲的參數,然後從Redis查詢出對應的value值。
④ 先判斷查詢出來的值是否爲空;
⑤ 如果爲空則通過類名和方法名以及參數進行反射調用原方法拿到最新的數據,然後存入Redis,再返回給前端。
⑥ 如果不爲空,則直接返回前端;實際就是一個攔截器。始終是沒有進入Controller內部代碼的。通過切面直接進行處理了。
3.2 註解類:
- 概述:第一個參數就是 redis存入的key 第二個就是參數名(與key拼接組成新key) expired設置過期時間
3.3 切面類分析:
代碼實現邏輯如上。在能理解代碼邏輯的基礎上,是可以繼續加功能或者繼續完善的。