前言
緩存是web項目不可或缺的一部分,通過緩存能夠降低服務器數(shù)據(jù)庫壓力绪钥,提高服務器的穩(wěn)定性及響應速度哩掺。
spring cache
spring cache是spring框架自帶的一套緩存框架,其具有多種實現(xiàn)厘擂,比較常用的是基于Redis的實現(xiàn)昆淡,其核心注解有 @CacheConfig
,@Cacheable
刽严,@CachePut
昂灵,@CacheEvict
,不熟悉用法的可以參考官方文檔舞萄,有很詳細的說明眨补,https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#spring-integration 。建議大家有時間還是多看看spring官方文檔倒脓,比從網上找文章看高效多了撑螺。
這里主要介紹一下@CacheConfig
這個注解,此注解有四個屬性崎弃,cacheNames
用于指定緩存名字膳叨,可以按照在緩存中按模塊保存襟诸,keyGenerator
緩存鍵生成器潘懊,如果指定了緩存鍵則忽略狼电,cacheManager
由spring管理的緩存管理器的名字,如果沒有指定則采用默認的緩存管理器盆均,cacheResolver
塞弊。
spring cache具有極高的易用性,在保存緩存時能夠根據(jù)Spring EL表達式自由定制緩存鍵泪姨,但是spring cache在使用過程中有兩點缺陷:
- 在使用
@CacheEvict
時游沿,如果指定了allEntries=true
,在從Redis中刪除緩存時使用的是 keys指令驴娃,keys指令時間復雜度是O(N)奏候,如果緩存數(shù)量較大會產生明顯的阻,因此在生產環(huán)境中Redis會禁用這個指令唇敞,導致報錯蔗草。
看下DefaultRedisCacheWriter
的clean方法:
@Override
public void clean(String name, byte[] pattern) {
Assert.notNull(name, "Name must not be null!");
Assert.notNull(pattern, "Pattern must not be null!");
execute(name, connection -> {
boolean wasLocked = false;
try {
if (isLockingCacheWriter()) {
doLock(name, connection);
wasLocked = true;
}
//keys 指令
byte[][] keys = Optional.ofNullable(connection.keys(pattern)).orElse(Collections.emptySet())
.toArray(new byte[0][]);
if (keys.length > 0) {
statistics.incDeletesBy(name, keys.length);
connection.del(keys);
}
} finally {
if (wasLocked && isLockingCacheWriter()) {
doUnlock(name, connection);
}
}
return "OK";
});
}
- 在通過
cacheManager
屬性指定緩存管理器時咒彤,如果不指定則采用全局的聲明的緩存管理器,無法調整緩存的過期時間咒精,而如果指定了緩存管理器則必須要手動創(chuàng)建一個緩存管理器且需要交給spring托管镶柱,無法動態(tài)指定緩存管理器。
這里對上述兩個缺陷進行了修改模叙,一是通過scan指令替代keys指令歇拆,雖然scan指令的時間復雜度也是O(N),但是其通過指定游標和count能夠分批執(zhí)行范咨,不會導致長時間的阻塞故觅;二是在項目啟動后,通過掃描注解動態(tài)生成cacheManager渠啊,能夠滿足不同緩存模塊指定不同的緩存時間的需求输吏,且無需手動創(chuàng)建RedisCacheManager。
重寫DefaultRedisCacheWriter
DefaultRedisCacheWriter
是spring cache提供的默認的Redis緩存寫出器替蛉,其內部封裝了緩存增刪改查等邏輯贯溅,但是由于其不是public修飾的,因此重寫了一個Redis緩存寫出器躲查,大部分代碼均與DefaultRedisCacheWriter
相同它浅,只有clean方法做了修改。
package com.cube.share.cache.writer;
import org.springframework.dao.PessimisticLockingFailureException;
import org.springframework.data.redis.cache.CacheStatistics;
import org.springframework.data.redis.cache.CacheStatisticsCollector;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStringCommands.SetOption;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.core.types.Expiration;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.function.Function;
/**
* @author poker.li
* @date 2021/7/17 13:20
* <p>
* 自定義的RedisCacheWriter的實現(xiàn),重寫DefaultRedisCacheWriter的clear()方法,使用scan指令替換keys指令
* <p>
*/
@SuppressWarnings({"WeakerAccess", "unused"})
public class IRedisCacheWriter implements RedisCacheWriter {
private final RedisConnectionFactory connectionFactory;
private final Duration sleepTime;
private final CacheStatisticsCollector statistics;
/**
* @param connectionFactory must not be {@literal null}.
*/
public IRedisCacheWriter(RedisConnectionFactory connectionFactory) {
this(connectionFactory, Duration.ZERO);
}
/**
* @param connectionFactory must not be {@literal null}.
* @param sleepTime sleep time between lock request attempts. Must not be {@literal null}. Use {@link Duration#ZERO}
* to disable locking.
*/
public IRedisCacheWriter(RedisConnectionFactory connectionFactory, Duration sleepTime) {
this(connectionFactory, sleepTime, CacheStatisticsCollector.none());
}
/**
* @param connectionFactory must not be {@literal null}.
* @param sleepTime sleep time between lock request attempts. Must not be {@literal null}. Use {@link Duration#ZERO}
* to disable locking.
* @param cacheStatisticsCollector must not be {@literal null}.
*/
public IRedisCacheWriter(RedisConnectionFactory connectionFactory, Duration sleepTime,
CacheStatisticsCollector cacheStatisticsCollector) {
Assert.notNull(connectionFactory, "ConnectionFactory must not be null!");
Assert.notNull(sleepTime, "SleepTime must not be null!");
Assert.notNull(cacheStatisticsCollector, "CacheStatisticsCollector must not be null!");
this.connectionFactory = connectionFactory;
this.sleepTime = sleepTime;
this.statistics = cacheStatisticsCollector;
}
@Override
public CacheStatistics getCacheStatistics(String cacheName) {
return statistics.getCacheStatistics(cacheName);
}
@Override
public void clearStatistics(String name) {
statistics.reset(name);
}
@Override
public RedisCacheWriter withStatisticsCollector(CacheStatisticsCollector cacheStatisticsCollector) {
return new IRedisCacheWriter(connectionFactory, sleepTime, cacheStatisticsCollector);
}
@Override
public void put(String name, byte[] key, byte[] value, @Nullable Duration ttl) {
Assert.notNull(name, "Name must not be null!");
Assert.notNull(key, "Key must not be null!");
Assert.notNull(value, "Value must not be null!");
execute(name, connection -> {
if (shouldExpireWithin(ttl)) {
connection.set(key, value, Expiration.from(ttl.toMillis(), TimeUnit.MILLISECONDS), SetOption.upsert());
} else {
connection.set(key, value);
}
return "OK";
});
}
@Override
public byte[] get(String name, byte[] key) {
Assert.notNull(name, "Name must not be null!");
Assert.notNull(key, "Key must not be null!");
return execute(name, connection -> connection.get(key));
}
@Override
public byte[] putIfAbsent(String name, byte[] key, byte[] value, @Nullable Duration ttl) {
Assert.notNull(name, "Name must not be null!");
Assert.notNull(key, "Key must not be null!");
Assert.notNull(value, "Value must not be null!");
return execute(name, connection -> {
if (isLockingCacheWriter()) {
doLock(name, connection);
}
try {
//noinspection ConstantConditions
if (connection.setNX(key, value)) {
if (shouldExpireWithin(ttl)) {
connection.pExpire(key, ttl.toMillis());
}
return null;
}
return connection.get(key);
} finally {
if (isLockingCacheWriter()) {
doUnlock(name, connection);
}
}
});
}
@Override
public void remove(String name, byte[] key) {
Assert.notNull(name, "Name must not be null!");
Assert.notNull(key, "Key must not be null!");
execute(name, connection -> connection.del(key));
}
@Override
public void clean(String name, byte[] pattern) {
Assert.notNull(name, "Name must not be null!");
Assert.notNull(pattern, "Pattern must not be null!");
execute(name, connection -> {
boolean wasLocked = false;
try {
if (isLockingCacheWriter()) {
doLock(name, connection);
wasLocked = true;
}
//使用scan命令代替keys命令
Cursor<byte[]> cursor = connection.scan(new ScanOptions.ScanOptionsBuilder().match(new String(pattern)).count(1000).build());
Set<byte[]> byteSet = new HashSet<>();
while (cursor.hasNext()) {
byteSet.add(cursor.next());
}
byte[][] keys = byteSet.toArray(new byte[0][]);
if (keys.length > 0) {
connection.del(keys);
}
} finally {
if (wasLocked && isLockingCacheWriter()) {
doUnlock(name, connection);
}
}
return "OK";
});
}
/**
* Explicitly set a write lock on a cache.
*
* @param name the name of the cache to lock.
*/
void lock(String name) {
execute(name, connection -> doLock(name, connection));
}
/**
* Explicitly remove a write lock from a cache.
*
* @param name the name of the cache to unlock.
*/
void unlock(String name) {
executeLockFree(connection -> doUnlock(name, connection));
}
private Boolean doLock(String name, RedisConnection connection) {
return connection.setNX(createCacheLockKey(name), new byte[0]);
}
@SuppressWarnings("UnusedReturnValue")
private Long doUnlock(String name, RedisConnection connection) {
return connection.del(createCacheLockKey(name));
}
private boolean doCheckLock(String name, RedisConnection connection) {
//noinspection ConstantConditions
return connection.exists(createCacheLockKey(name));
}
/**
* @return {@literal true} if {@link RedisCacheWriter} uses locks.
*/
private boolean isLockingCacheWriter() {
return !sleepTime.isZero() && !sleepTime.isNegative();
}
private <T> T execute(String name, Function<RedisConnection, T> callback) {
try (RedisConnection connection = connectionFactory.getConnection()) {
checkAndPotentiallyWaitUntilUnlocked(name, connection);
return callback.apply(connection);
}
}
private void executeLockFree(Consumer<RedisConnection> callback) {
try (RedisConnection connection = connectionFactory.getConnection()) {
callback.accept(connection);
}
}
private void checkAndPotentiallyWaitUntilUnlocked(String name, RedisConnection connection) {
if (!isLockingCacheWriter()) {
return;
}
try {
while (doCheckLock(name, connection)) {
Thread.sleep(sleepTime.toMillis());
}
} catch (InterruptedException ex) {
// Re-interrupt current thread, to allow other participants to react.
Thread.currentThread().interrupt();
throw new PessimisticLockingFailureException(String.format("Interrupted while waiting to unlock cache %s", name),
ex);
}
}
private static boolean shouldExpireWithin(@Nullable Duration ttl) {
return ttl != null && !ttl.isZero() && !ttl.isNegative();
}
private static byte[] createCacheLockKey(String name) {
return (name + "~lock").getBytes(StandardCharsets.UTF_8);
}
}
自定義緩存注解替代spring cache的注解
- @ICacheConfig
package com.cube.share.cache.anonotation;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* @author poker.li
* @date 2021/7/17 16:08
* <p>
* 基于{@link org.springframework.cache.annotation.CacheConfig}提供的緩存配置注解
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@CacheConfig
@Inherited
public @interface ICacheConfig {
/**
* 緩存前綴名,通過該屬性指定不同模塊緩存的存放位置,
* 在Redis中分塊展示,對于指定的緩存鍵key="11235813",存在Redis的實際鍵為 "sysUser::11235813"
*/
@AliasFor(annotation = CacheConfig.class, attribute = "cacheNames")
String[] cacheNames() default {};
/**
* 緩存鍵生成器
*/
@AliasFor(annotation = CacheConfig.class, attribute = "keyGenerator")
String keyGenerator() default "";
/**
* 緩存管理器,如果沒有指定則采用默認的緩存管理器,如果需要自定義緩存的過期時間镣煮。
* 則必須指定該屬性,并且要使該屬性唯一,這樣能創(chuàng)建一個新的RedisCacheManager(bean的名字就是cacheManager)
*/
@AliasFor(annotation = CacheConfig.class, attribute = "cacheManager")
String cacheManager() default "";
@AliasFor(annotation = CacheConfig.class, attribute = "cacheResolver")
String cacheResolver() default "";
/**
* 是否允許緩存存入null
*/
boolean allowCachingNullValues() default false;
/**
* 緩存的有效期限,如果值小于等于0則表示永久保存
*/
int expire() default 8;
/**
* 緩存過期的時間單位
*/
TimeUnit timeUnit() default TimeUnit.HOURS;
/**
* 設置是否兼容事務,
* 默認是true,只在事務成功提交后才會進行緩存的put/evict操作
*/
boolean transactionAware() default true;
}
- @ICache
package com.cube.share.cache.anonotation;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
/**
* @author poker.li
* @date 2021/7/17 17:08
* <p>
* 基于{@link Cacheable}實現(xiàn)的緩存存放注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@SuppressWarnings("SpringCacheNamesInspection")
@Cacheable
public @interface ICache {
/**
* 緩存的名字(緩存鍵的前綴),例如,指定為"sysUser",
* 對于指定的緩存鍵key="11235813",存在Redis的實際鍵為 "sysUser::11235813"
*/
@AliasFor(annotation = Cacheable.class, attribute = "value")
String[] value() default {};
/**
* 緩存的名字(緩存鍵的前綴)
*/
@AliasFor(annotation = Cacheable.class, attribute = "cacheNames")
String[] cacheNames() default {};
/**
* 緩存鍵
*/
@AliasFor(annotation = Cacheable.class, attribute = "key")
String key() default "";
/**
* 緩存鍵生成器
*/
@AliasFor(annotation = Cacheable.class, attribute = "keyGenerator")
String keyGenerator() default "";
/**
* 緩存管理器,如果沒有指定則采用默認的緩存管理器
*/
@AliasFor(annotation = Cacheable.class, attribute = "cacheManager")
String cacheManager() default "";
@AliasFor(annotation = Cacheable.class, attribute = "cacheResolver")
String cacheResolver() default "";
/**
* 判斷是否放入緩存的條件,使用Spring EL表達式
*/
@AliasFor(annotation = Cacheable.class, attribute = "condition")
String condition() default "";
/**
* 在方法執(zhí)行結束后,根據(jù)方法的執(zhí)行結果執(zhí)行是否需要放入緩存,例如
* unless = "#result != null",表示僅當方法執(zhí)行結果不為null時才放入緩存
*/
@AliasFor(annotation = Cacheable.class, attribute = "unless")
String unless() default "";
/**
* 是否需要同步調用,如果設置為true,具有相同key的多次調用串行執(zhí)行
*/
@AliasFor(annotation = Cacheable.class, attribute = "sync")
boolean sync() default false;
}
- @ICachePut
package com.cube.share.cache.anonotation;
import org.springframework.cache.annotation.CachePut;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
/**
* @author poker.li
* @date 2021/7/17 17:33
* <p>
* 基于{@link CachePut}提供的緩存更新注解
*/
@SuppressWarnings("SpringCacheNamesInspection")
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@CachePut
public @interface ICachePut {
/**
* 緩存的名字(緩存鍵的前綴),例如,指定為"sysUser",
* 對于指定的緩存鍵key="11235813",存在Redis的實際鍵為 "sysUser::11235813"
*/
@AliasFor(annotation = CachePut.class, attribute = "value")
String[] value() default {};
/**
* 緩存的名字(緩存鍵的前綴)
*/
@AliasFor(annotation = CachePut.class, attribute = "cacheNames")
String[] cacheNames() default {};
/**
* 緩存鍵
*/
@AliasFor(annotation = CachePut.class, attribute = "key")
String key() default "";
/**
* 緩存管理器
*/
@AliasFor(annotation = CachePut.class, attribute = "cacheManager")
String cacheManager() default "";
@AliasFor(annotation = CachePut.class, attribute = "cacheResolver")
String cacheResolver() default "";
/**
* 判斷是否放入緩存的條件,使用Spring EL表達式
*/
@AliasFor(annotation = CachePut.class, attribute = "condition")
String condition() default "";
/**
* 在方法執(zhí)行結束后,根據(jù)方法的執(zhí)行結果執(zhí)行是否需要放入緩存,例如
* unless = "#result != null",表示僅當方法執(zhí)行結果不為null時才放入緩存
*/
@AliasFor(annotation = CachePut.class, attribute = "unless")
String unless() default "";
}
- @ICacheEvict
package com.cube.share.cache.anonotation;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;
/**
* @author cube.li
* @date 2021/7/17 21:23
* @description {@link org.springframework.cache.annotation.CacheEvict}提供的緩存清除注解
*/
@SuppressWarnings({"SingleElementAnnotation", "SpringCacheNamesInspection"})
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@CacheEvict
public @interface ICacheEvict {
/**
* 緩存的名字(緩存鍵的前綴),例如,指定為"sysUser",
* 對于指定的緩存鍵key="11235813",存在Redis的實際鍵為 "sysUser::11235813"
*/
@AliasFor(annotation = CacheEvict.class, attribute = "value")
String[] value() default {};
/**
* 緩存的名字(緩存鍵的前綴)
*/
@AliasFor(annotation = CacheEvict.class, attribute = "cacheNames")
String[] cacheNames() default {};
/**
* 緩存鍵
*/
@AliasFor(annotation = CacheEvict.class, attribute = "key")
String key() default "";
/**
* 緩存管理器
*/
@AliasFor(annotation = CacheEvict.class, attribute = "cacheManager")
String cacheManager() default "";
@AliasFor(annotation = CacheEvict.class, attribute = "cacheResolver")
String cacheResolver() default "";
/**
* 判斷是否放入緩存的條件,使用Spring EL表達式
*/
@AliasFor(annotation = CacheEvict.class, attribute = "condition")
String condition() default "";
/**
* 是否刪除緩存中所有的記錄(當前指定的cacheNames下),
* 如果設置為false,僅刪除設定的key
*/
@AliasFor(annotation = CacheEvict.class, attribute = "allEntries")
boolean allEntries() default false;
/**
* 是否在方法調用前刪除緩存,默認是false,僅當方法成功執(zhí)行后才刪除緩存,
* 如果設定為true,則在調用前即刪除緩存,無論方法最終是否調用成功
*/
@AliasFor(annotation = CacheEvict.class, attribute = "beforeInvocation")
boolean beforeInvocation() default false;
}
上面的四個注解實際上只有@ICacheConfig對原生注解@CacheConfig做了再封裝姐霍,增加了三個屬性,另外三個注解只是對spring cache對應的原生注解起了個別名怎静,以后可能會有拓展的需要邮弹。
指定默認的RedisCacheManager配置
package com.cube.share.cache.config;
import com.cube.share.cache.writer.IRedisCacheWriter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
/**
* @author poker.li
* @date 2021/7/17 14:07
* <p>
* Redis配置
*/
@Configuration
@ConditionalOnProperty(prefix = "ICache", name = "enabled", havingValue = "true")
@EnableCaching
public class RedisCacheConfig {
@Bean
@Primary
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration cacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(8))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager.RedisCacheManagerBuilder
.fromConnectionFactory(redisConnectionFactory)
.cacheWriter(redisCacheWriter(redisConnectionFactory));
return builder.transactionAware()
.cacheDefaults(cacheConfiguration).build();
}
@Bean
public RedisCacheWriter redisCacheWriter(RedisConnectionFactory redisConnectionFactory) {
return new IRedisCacheWriter(redisConnectionFactory);
}
}
如果沒有指定RedisCacheManager黔衡,則采用上述配置的RedisCacheManger作為默認的緩存管理器蚓聘,其指定了緩存的過期時間是8個小時。
動態(tài)生成RedisCacheManager并交給Spring托管
package com.cube.share.cache.processor;
import com.cube.share.cache.anonotation.ICacheConfig;
import com.cube.share.cache.constant.RedisCacheConstant;
import com.cube.share.cache.writer.IRedisCacheWriter;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.config.ConstructorArgumentValues;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.lang.NonNull;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.time.Duration;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @author poker.li
* @date 2021/7/20 11:50
*/
@Component
@SuppressWarnings("unused")
@ConditionalOnProperty(prefix = "ICache", name = "enabled", havingValue = "true")
public class CacheManagerProcessor implements BeanFactoryAware, ApplicationContextAware {
private DefaultListableBeanFactory beanFactory;
private ApplicationContext applicationContext;
@Resource(type = IRedisCacheWriter.class)
private IRedisCacheWriter redisCacheWriter;
private Set<String> cacheManagerNameSet = new HashSet<>();
@PostConstruct
public void registerCacheManager() {
cacheManagerNameSet.add(RedisCacheConstant.DEFAULT_CACHE_MANAGER_BEAN_NAME);
//獲取所有使用ICacheConfig注解的Bean
Map<String, Object> annotatedBeanMap = this.applicationContext.getBeansWithAnnotation(ICacheConfig.class);
//獲取所有Bean上的ICacheConfig注解
Set<Map.Entry<String, Object>> entrySet = annotatedBeanMap.entrySet();
for (Map.Entry<String, Object> entry : entrySet) {
Object instance = entry.getValue();
ICacheConfig iCacheConfig = instance.getClass().getAnnotation(ICacheConfig.class);
registerRedisCacheManagerBean(iCacheConfig);
}
}
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = (DefaultListableBeanFactory) beanFactory;
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
private void registerRedisCacheManagerBean(ICacheConfig annotation) {
final String cacheManagerName = annotation.cacheManager();
if (StringUtils.isBlank(cacheManagerName)) {
return;
}
if (!cacheManagerNameSet.contains(cacheManagerName)) {
RootBeanDefinition definition = new RootBeanDefinition(RedisCacheManager.class);
ConstructorArgumentValues argumentValues = new ConstructorArgumentValues();
argumentValues.addIndexedArgumentValue(0, redisCacheWriter);
argumentValues.addIndexedArgumentValue(1, getRedisCacheConfiguration(annotation));
definition.setConstructorArgumentValues(argumentValues);
beanFactory.registerBeanDefinition(cacheManagerName, definition);
if (annotation.transactionAware()) {
//事務
RedisCacheManager currentManager = applicationContext.getBean(cacheManagerName, RedisCacheManager.class);
currentManager.setTransactionAware(true);
}
}
}
@NonNull
private RedisCacheConfiguration getRedisCacheConfiguration(ICacheConfig annotation) {
final boolean allowCachingNullValues = annotation.allowCachingNullValues();
final int expire = annotation.expire();
final TimeUnit timeUnit = annotation.timeUnit();
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
if (!allowCachingNullValues) {
config = config.disableCachingNullValues();
}
if (expire > 0) {
Duration duration = getDuration(expire, timeUnit);
config = config.entryTtl(duration);
}
return config;
}
@NonNull
private RedisCacheManager getRedisCacheManager(ICacheConfig annotation) {
return RedisCacheManager.RedisCacheManagerBuilder
.fromCacheWriter(redisCacheWriter)
.transactionAware()
.cacheDefaults(getRedisCacheConfiguration(annotation))
.build();
}
@NonNull
private Duration getDuration(int expire, TimeUnit timeUnit) {
switch (timeUnit) {
case DAYS:
return Duration.ofDays(expire);
case HOURS:
return Duration.ofHours(expire);
case MINUTES:
return Duration.ofMinutes(expire);
case SECONDS:
return Duration.ofSeconds(expire);
case MILLISECONDS:
return Duration.ofMillis(expire);
case NANOSECONDS:
return Duration.ofNanos(expire);
default:
throw new IllegalArgumentException("Illegal Redis Cache Expire TimeUnit!");
}
}
}
這里在容器啟動后掃描@ICacheConfig注解修飾的Bean盟劫,并根據(jù)指定的cacheManager屬性生成對應的RedisCacheManager管理器夜牡。
測試
package com.cube.share.cache.service;
import com.cube.share.cache.anonotation.ICache;
import com.cube.share.cache.anonotation.ICacheConfig;
import com.cube.share.cache.anonotation.ICacheEvict;
import com.cube.share.cache.anonotation.ICachePut;
import com.cube.share.cache.model.SysDepartment;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* @author poker.li
* @date 2021/7/20 14:35
*/
@Service
@Slf4j
@ICacheConfig(cacheNames = "sysDepartment", cacheManager = "sysDepartmentCacheManager", expire = -1)
public class SysDepartmentService {
@ICache(key = "#a0")
public SysDepartment getById(Integer id) {
return new SysDepartment(id, "部門名字" + id, "部門別名" + id);
}
@ICachePut(key = "#p0?.id", condition = "#p0 != null")
public SysDepartment update(SysDepartment sysDepartment) {
return sysDepartment;
}
@ICacheEvict(key = "#p0")
public void deleteById(Integer id) {
log.debug("刪除: {}", id);
}
}
package com.cube.share.cache.service;
import com.cube.share.cache.anonotation.ICache;
import com.cube.share.cache.anonotation.ICacheConfig;
import com.cube.share.cache.anonotation.ICachePut;
import com.cube.share.cache.model.SysLog;
import org.springframework.stereotype.Service;
/**
* @author cube.li
* @date 2021/7/20 23:27
* @description
*/
@Service
@ICacheConfig(cacheNames = "sysLog", cacheManager = "sysLogCacheManager", expire = 1)
public class SysLogServiceImpl implements SysLogService {
@Override
@ICache(key = "#id")
public SysLog getById(Integer id) {
return new SysLog(id, "操作" + id);
}
@Override
@ICachePut(key = "#p0.id", condition = "#p0?.id != null")
public SysLog update(SysLog sysLog) {
return sysLog;
}
}
package com.cube.share.cache.service;
import com.cube.share.cache.anonotation.ICache;
import com.cube.share.cache.anonotation.ICacheConfig;
import com.cube.share.cache.anonotation.ICacheEvict;
import com.cube.share.cache.anonotation.ICachePut;
import com.cube.share.cache.model.SysUser;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* @author poker.li
* @date 2021/7/17 15:28
*/
@Service
@ICacheConfig(cacheNames = "sysUser")
@Slf4j
public class SysUserService {
@ICache(key = "#p0")
public SysUser getById(Integer id) {
return new SysUser(id, id + "姓名", id + "地址");
}
@ICachePut(key = "#sysUser.id")
public SysUser update(SysUser sysUser) {
return sysUser;
}
@ICacheEvict(allEntries = true)
public void deleteById(Integer id) {
log.debug("刪除 {}", id);
}
}
在配置文件中開啟緩存
spring:
redis:
host: 127.0.0.1
ssl: false
port: 6379
database: 1
connect-timeout: 1000
lettuce:
pool:
max-active: 10
max-wait: -1
min-idle: 0
max-idle: 20
server:
port: 8899
ICache:
enabled: true
寫幾個單元測試,看一下Redis里的數(shù)據(jù):
從測試結果來看侣签,如果指定了cacheManager塘装,則動態(tài)生成對應的RedisCacheManager,如果沒有指定影所,則采用默認的緩存管理器蹦肴。
示例代碼: https://gitee.com/li-cube/share/tree/master/cache/src