這里提到的緩存是通用的緩存鸳兽,如表格獲取時優(yōu)先查詢redis,如果有罕拂,則讀取 redis 中的數(shù)據(jù)揍异,如果沒有,讀取數(shù)據(jù)庫爆班,并將返回結(jié)果存入 redis 中衷掷,然后新增/修改/刪除等操作時,需要將對應(yīng)的緩存數(shù)據(jù)清空柿菩,以保證每次獲取都是最新的數(shù)據(jù)戚嗅。當(dāng)然,有其他需求的時候,也可以直接通過 redisTemplate來對 redis 進行增刪改查渡处,這個做法跟 MySQL 等關(guān)系型數(shù)據(jù)庫是差不多的镜悉,關(guān)于 redis 操作的部分因為不是這篇文章的重點,所以這里就不贅述了医瘫。
這里的實現(xiàn)思路有兩種侣肄,第一種是完全通過 spring AOP面向切面編程,給 select 做一層切面醇份,給 save/update/delete 做一層切面稼锅,來完成上面說的功能。第二種是Spring的 cache 庫(其實還是面向切面的技術(shù))僚纷,輔以 spring AOP矩距,可以大大簡化切面編程部分代碼。
首先是要導(dǎo)入相關(guān) jar 包怖竭,我現(xiàn)在項目都是 spring-boot 的锥债,所以jar 包帶有 spring-boot 前綴的,如果不是 spring-boot 的痊臭,直接找對應(yīng) spring 版本就可以了哮肚,都是比較通用的 jar 包,也不難找广匙。
<!-- redis —>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- cache 緩存 —>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
首先是cache 和 redis 的配置允趟,我這里將兩個配置寫在了一個類中
@Configuration
@EnableCaching
@PropertySource(value = "classpath:/application.properties")
public class CacheConfig extends CachingConfigurerSupport {
/**
*
* @return
*/
@Bean
public CacheManager cacheManager(RedisTemplate redisTemplate) {
RedisCacheManager manager = new RedisCacheManager(redisTemplate);
//設(shè)置統(tǒng)一的過期時間
manager.setDefaultExpiration(120l);
//可在 map 中給不同的 key 設(shè)置對應(yīng)的過期時間
// manager.setExpires(new HashMap<String, Long>());
return manager;
}
@Value("${spring.redis.host}")
String hostName;
@Value("${spring.redis.password}")
String password;
@Value("${spring.redis.port}")
Integer port;
@Value("${spring.redis.timeout}")
Integer timeout;
@Bean
JedisConnectionFactory jedisConnectionFactory() {
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
jedisConnectionFactory.setHostName(hostName);
jedisConnectionFactory.setPort(port);
jedisConnectionFactory.setPassword(password);
jedisConnectionFactory.setDatabase(0);
jedisConnectionFactory.setTimeout(timeout);
return jedisConnectionFactory;
}
}
這個類有幾個地方要注意的,首先是類的注解鸦致,@configuration 是 spring-boot 將類裝配成 bean 的注解潮剪,@EnableCaching 是聲明開啟緩存功能的注解,@PropertySource 是引用配置文件的注解
配置類里面有兩個 bean分唾,cacheManager是給 cache 庫配置緩存的實際位置抗碰,因為 spring-cache 可以搭配的緩存很多,也就有很多cacheManager 可以使用鳍寂,我們這里用的是 redis改含,所以用的是 RedisCacheManager。
這里可以給緩存上過期時間迄汛,但是這里要注意一下這個過期時間指的是通過 cache類庫添加進 Redis 緩存的過期時間捍壤,也就是@Cacheable 和@CachePut 這兩個注解。如果單獨使用 RedisTemplate 添加緩存鞍爱,是要另外設(shè)置過期時間的鹃觉。另外一個bean是 Redis 的配置方法。
然后簡單介紹一下 cache 庫睹逃,這個庫是 Spring的緩存庫盗扇,使用起來也是非常簡單祷肯,通過幾個使用在類或方法上面的注解即可達成目的
@Cacheable //通過方法的查找是優(yōu)先查找緩存,如果找到,方法不會執(zhí)行,當(dāng)找不到時會去找數(shù)據(jù)庫,并將返回結(jié)果放入緩存
@CachePut //通過方法的返回值會放入緩存中,無論緩存中是否有值,方法一定會被執(zhí)行
@CacheEvict //清除緩存中的一個或多個條目
@Cacheing //這是一個分組的注解,能夠同時應(yīng)用多個其他的緩存注解
這里只是簡單介紹一下這幾個注解的含義,這些內(nèi)容在其他文章中也都有比較詳實的說明,這里也不過多的介紹了疗隶。
著重要說的是@Cacheable 注解佑笋,這個注解是可以達到查找時優(yōu)先查找緩存,當(dāng)緩存中沒有才去訪問數(shù)據(jù)庫功能的斑鼻,這個功能實際是通過 AOP 來完成的蒋纬,所以我們也可以自己寫相關(guān)代碼,不過既然有現(xiàn)成的坚弱,那直接調(diào)用是極好的蜀备。
@Cacheable 注解有幾個屬性,對緩存有著較大的影響(這些屬性其實這幾個注解都有荒叶,但是上面幾個注解有缺陷碾阁,下面會講)。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {
String key() default "";
String keyGenerator() default "";
String condition() default "";
String unless() default "";
}
這里只截取了 Cacheable 注解中部分屬性些楣,其他屬性具體功效可以看源碼脂凶,介紹的也都比較詳實。因為 redis 是鍵值對數(shù)據(jù)庫愁茁,存儲模型都是 key-value 格式的艰猬,所以要存入緩存,得要有對應(yīng)的 key埋市。key 有兩種生成方式,第一種是直接指定命贴,這種方式一般用于某些定制化的緩存當(dāng)中道宅,通用返回接口的緩存得要用第二種方式,也就是通過鍵生成器來生成緩存的 key胸蛛。condition 和 unless 的區(qū)別是污茵,condition 是條件成立時,存入緩存葬项;unless 是排除值不成立的泞当,存入緩存。這兩者看起來是一個意思民珍,但是在邏輯上來說還是有細微差別的襟士,一個是阻止存入緩存(unless),但是還是會在緩存中查找嚷量;另外一個是先判斷 condition 的條件(condition)陋桂,如果不成立,則不會查找緩存蝶溶,也不會存入緩存嗜历。以上的 key,condition,unless 都是使用的 SpEL 表達式梨州,后面會在實例中有一些簡單的介紹痕囱,詳細的說明也可通過其他文章學(xué)習(xí)。
在這些屬性中暴匠,key 的生成格外重要鞍恢,要能夠通用,不必為每個需要用緩存的方法都手動生成 key有序,又要保證唯一性,因為不止是方法名會影響 key 值岛请,每次調(diào)用時參數(shù)的不一致 都應(yīng)該是不同的 key旭寿。這里keyGenerator很好的幫我們完成了這樣的工作。
可以使用Spring默認的SimpleKeyGenerator來生成 key崇败,但是這個 key 是不會將函數(shù)名組合在 key 中盅称,也是有缺陷,所以我們需要自定義一個 keyGenerator后室。
@Override
public Object generate(Object target, Method method, Object... params) {
Logger log = Logger.getLogger("Keygenerate");
StringBuilder key = new StringBuilder();
key.append(method.getDeclaringClass().getName()).append(".").append(method.getName()).append(":"); //先將類的全限定名和方法名拼裝在 key 中
if (params.length == 0) {
return key.append(NO_PARAM_KEY).toString();
}
for (Object param : params) { //通過遍歷參數(shù),將參數(shù)也拼裝在 key 中,保證每次獲取key 的唯一性
if (param == null) {
key.append(NULL_PARAM_KEY);
} else if (ClassUtils.isPrimitiveArray(param.getClass())) {
int length = Array.getLength(param);
for (int i = 0; i < length; i++) {
key.append(Array.get(param, i));
key.append(',');
}
} else if (ClassUtils.isPrimitiveOrWrapper(param.getClass()) || param instanceof String) {
key.append(param);
} else {
key.append(param.hashCode()); //如果是map 或 model 類型
}
key.append('-');
}
return key.toString();
}
我是將實現(xiàn)了 KeyGenerator 的類放在了 Spring 的上下文中缩膝,這樣子只要是使用了@Cacheable 注解的方法都可以根據(jù)規(guī)則來生成唯一的 key 了。
當(dāng)然實際項目中還是有可能是會需要針對特定的方法來生成 key 的岸霹,下面給一個實例來說明:
@Cacheable(unless = "#result == '{\"data\":[],\"total_count\":0}'",key = "'keyGenerator' + '.' + #methodName + ':' + #id")
這是一個帶有 unless 和 key 的注解疾层,實現(xiàn)的功能是查詢,當(dāng)沒有值時不存入緩存贡避。
到此我們已經(jīng)可以通過注解將獲取到的值存入緩存痛黎,并直接通過緩存將值獲取出來,不用額外的調(diào)用數(shù)據(jù)庫了刮吧,接下來的工作是當(dāng) save/update/delete 等操作時湖饱,將相關(guān)數(shù)據(jù)從緩存中清除,以保證獲取數(shù)據(jù)的準確性杀捻。
cache 庫的注解中是有可以實現(xiàn)這個功能的井厌,@CacheEvict。但是當(dāng)我們通過 KeyGenerator 來自定義生成 key 時致讥,@CacheEvict 便無法獲取對應(yīng)的 Key仅仆,則無法正確清除相關(guān)緩存。舉個栗子拄踪,有用戶表和訂單表蝇恶,用戶表有一次查詢,在緩存中生成這次查詢的緩存惶桐,訂單表也有一次查詢撮弧,在緩存中生成這次查詢的緩存潘懊。這時新增一條訂單,需要清空訂單相關(guān)的全部緩存贿衍,否則會出現(xiàn)獲取數(shù)據(jù)不一致的情況授舟。但是如果通過@CacheEvict 來做,是無法知道目前 Redis 中關(guān)于訂單緩存的 Key 的贸辈,當(dāng)然可以全部清空释树,這樣會把用戶表的緩存也清空掉,這必然是不好的擎淤。
那為了實現(xiàn)相關(guān)操作只清空相關(guān)的所有緩存奢啥,這里采取 AOP 的方式,對 save/update/delete 切面嘴拢,當(dāng)有這些操作的時候桩盲,就清空對應(yīng)的緩存。
這里有兩種做法席吴,第一種直接將切點設(shè)置成save/update/delete赌结,在每次進入方法前刪除緩存,但是這種做法一個是得嚴格限制方法名孝冒,另外一個是如果有一些其他方法想要刪除緩存柬姚,就得要增加切點。所以這里用第二種方法庄涡,自定義注解量承,然后通過檢查方法是否帶有該注解,來判斷是不是需要清空緩存穴店。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ClearRedis {
}
首先是自定義的緩存宴合,然后在需要用到刪除的方法上面使用該緩存即可
@ClearRedis
public String save(User user){...}
這樣準備工作就算完成了,接下來是AOP 相關(guān)的代碼
@Component
@Aspect
public class AspectJConfig {
@Pointcut("execution(* com.Demo.*.*(..))")
private void clearCache(){}
//save 方法前,清空相關(guān) key的緩存
@Before("clearCache()")
public void beforeSave(JoinPoint joinPoint) throws Throwable {
String targetName = joinPoint.getTarget().getClass().getName();
Class targetClass = Class.forName(targetName);
Method[] methods = targetClass.getMethods();
String methodName = joinPoint.getSignature().getName();
boolean isClearRedisPresent = false;
for (Method method : methods)
{
if (method.getName().equals(methodName))
{
if (method.isAnnotationPresent(ClearRedis.class))
{
isClearRedisPresent = true;
break;
}
}
}
if (isClearRedisPresent)
{
//獲取切點作用的方法
Signature signature = joinPoint.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
//獲取有作用方法前綴的 key,遍歷以刪除該切點作用方法關(guān)聯(lián)的緩存,因為 save 有刷新數(shù)據(jù)庫
Set<String> keys = stringRedisTemplate.keys(methodSignature.getDeclaringTypeName() + "*");
stringRedisTemplate.delete(keys);
keys = stringRedisTemplate.keys(methodSignature.getDeclaringTypeName() + "*");
}
}
}
由于是通過增加注解的方式迹鹅,所以切點的范圍可以定位到 service 層的所有方法上。邏輯上來說在save/delete/update 前贞言,后刪除緩存都是可以的斜棚,所以是用@Before 還是@After 或者@Around 都是可以的。
方法體內(nèi)分為兩個部分该窗,第一個部分是判斷該切點方法有沒有使用@ClearRedis 注解弟蚀,第二部分是當(dāng)使用了注解,則清空對應(yīng)的緩存酗失。這兩步都是利用了 JAVA 的反射機制义钉,通過 joinPoint 來找到對應(yīng)的調(diào)用類和調(diào)用方法以及調(diào)用方法的參數(shù),這樣我們就可以根據(jù)類+方法名+參數(shù)列表來組裝對應(yīng)的 key规肴,這個跟上面在 KeyGenerator 中生成 key 的邏輯是一一對應(yīng)的捶闸。最后是利用的 Redis 的模糊查詢的邏輯可刪除對應(yīng)前綴的 Key夜畴,也就是我們可以不管參數(shù)是多少,是用哪個方法調(diào)用的删壮,直接刪除指定類獲取到的所有緩存贪绘。
到此為止所有的部分就介紹完畢了,通過這樣的配置央碟,可以簡單的在獲取方法上增加@Cacheable 注解生成緩存税灌,同時在方法上增加@ClearRedis 來清空類對應(yīng)的緩存,保證了讀的效率亿虽,也同時保證的讀的準確性菱涤。
最后有兩點存疑
1.我之前試過用@CacheEvict,想來生成指定的 Key,并通過前綴刪除洛勉,但是好像做不到粘秆,因為前綴刪除這個功能是 Redis 的,不是 cache 的坯认。不過我覺得既然 cache 提供這樣一個注解翻擒,應(yīng)該不至于做不到這個需求,所以還得再繼續(xù)看一下牛哺。
2.第二個就是如果將切點設(shè)置成 service 層所有的方法陋气,會不會對效率有所影響,這個也要在實際的運行環(huán)境中檢測才行引润。