自定义key
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;
import java.lang.reflect.Array;
import java.lang.reflect.Method;
@Component //标记为组件
public class CacheKeyGenerator implements KeyGenerator {
public static final int NO_PARAM_KEY = 0;
public static final int NULL_PARAM_KEY = 53;
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuilder key = new StringBuilder();
key.append(target.getClass().getSimpleName()).append(".").append(method.getName()).append(":");
if (params.length == 0) {
return key.append(NO_PARAM_KEY).toString();
}
for (Object param : params) {
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());
}
key.append('-');
}
return key.toString();
}
}
自定义CacheManager
import com.github.benmanes.caffeine.cache.Caffeine;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;
@Component
@Slf4j
public class RedisCaffeineCacheManager implements CacheManager {
@Autowired
private RedisTemplate<String, Object> template;
@Value("${redisCaffeine.redisCacheExpire:86400}")
private Long redisCacheExpire;
@Value("${redisCaffeine.cachePrefix:gatewayCache}")
private String cachePrefix;
private ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<String, Cache>();
private Set<String> cacheNames=new HashSet<String>();
@Override
public Cache getCache(String name) {
Cache cache = cacheMap.get(name);
if (cache != null) {
return cache;
}
cache=new RedisCaffeineCache(true,getCaffeineCache(),name,template,cachePrefix,redisCacheExpire);
Cache oldCache = cacheMap.putIfAbsent(name, cache);
log.debug("create cache instance, the cache name is : {}", name);
cacheNames.add(name);
return oldCache == null ? cache : oldCache;
}
@Override
public Collection<String> getCacheNames() {
return this.cacheNames;
}
public com.github.benmanes.caffeine.cache.Cache<Object, Object> getCaffeineCache() {
return Caffeine.newBuilder()
// 设置最后一次写入或访问后经过固定时间过期
.expireAfterWrite(60, TimeUnit.SECONDS)
// 初始的缓存空间大小
.initialCapacity(100)
// 缓存的最大条数
.maximumSize(10000)
.build();
}
}
继承AbstractValueAdaptingCache 接口
import com.github.benmanes.caffeine.cache.Cache;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.support.AbstractValueAdaptingCache;
import org.springframework.data.redis.core.ConvertingCursor;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
@Component
@Slf4j
public class RedisCaffeineCache extends AbstractValueAdaptingCache {
@Getter
private Cache<Object, Object> caffeineCache;
@Getter
private String name;
@Getter
private String cachePrefix;
private RedisTemplate<String,Object> redisTemplate;
private Map<String, ReentrantLock> keyLockMap = new ConcurrentHashMap<>();
private Long redisCacheExpire;
RedisCaffeineCache(){
super(true);
}
public RedisCaffeineCache(boolean allowNullValues, Cache<Object, Object> caffeineCache,String name,RedisTemplate<String,Object> redisTemplate,String cachePrefix,Long redisCacheExpire) {
super(allowNullValues);
this.caffeineCache=caffeineCache;
this.name=name;
this.redisTemplate=redisTemplate;
this.cachePrefix=cachePrefix;
this.redisCacheExpire=redisCacheExpire;
}
@Override
protected Object lookup(Object key) {
Object cacheKey = getKey(key);
Object value = caffeineCache.getIfPresent(key);
if (value != null) {
log.debug("get cache from caffeine, the key is : {}", cacheKey);
return value;
}
// 避免自动一个 RedisTemplate 覆盖失效
redisTemplate.setKeySerializer(new StringRedisSerializer());
value = redisTemplate.opsForValue().get(cacheKey);
if (value != null) {
log.debug("get cache from redis and put in caffeine, the key is : {}", cacheKey);
caffeineCache.put(key, value);
}
return value;
}
@Override
public Object getNativeCache() {
return this;
}
@Override
public <T> T get(Object key, Callable<T> valueLoader) {
Object value = lookup(key);
if (value != null) {
return (T) value;
}
ReentrantLock lock = keyLockMap.computeIfAbsent(key.toString(), s -> {
log.trace("create lock for key : {}", s);
return new ReentrantLock();
});
try {
lock.lock();
value = lookup(key);
if (value != null) {
return (T) value;
}
value = valueLoader.call();
Object storeValue = toStoreValue(value);
put(key, storeValue);
return (T) value;
}
catch (Exception e) {
throw new ValueRetrievalException(key, valueLoader, e.getCause());
}
finally {
lock.unlock();
}
}
@Override
public void put(Object key, Object value) {
if (!super.isAllowNullValues() && value == null) {
this.evict(key);
return;
}
if (redisCacheExpire > 0) {
redisTemplate.opsForValue().set(String.valueOf(getKey(key)), toStoreValue(value), redisCacheExpire, TimeUnit.SECONDS);
}
caffeineCache.put(key, value);
}
@Override
public void evict(Object key) {
// 先清除redis中缓存数据,然后清除caffeine中的缓存,避免短时间内如果先清除caffeine缓存后其他请求会再从redis里加载到caffeine中
redisTemplate.delete(String.valueOf(getKey(key)));
caffeineCache.invalidate(key);
}
@Override
public void clear() {
// 先清除redis中缓存数据,然后清除caffeine中的缓存,避免短时间内如果先清除caffeine缓存后其他请求会再从redis里加载到caffeine中
// Set<String> keys = redisTemplate.keys(this.name.concat(":*"));
List<String> keys=getKeys();
if (!CollectionUtils.isEmpty(keys)){
redisTemplate.delete(keys);
}
caffeineCache.invalidateAll();
}
private Object getKey(Object key) {
return this.name.concat(":").concat(
StringUtils.isEmpty(cachePrefix) ? key.toString() : cachePrefix.concat(":").concat(key.toString()));
}
public List<String> getKeys() {
long start = System.currentTimeMillis();
//需要匹配的key
String patternKey = "pay:*";
ScanOptions options = ScanOptions.scanOptions()
//这里指定每次扫描key的数量
.count(10000)
.match(patternKey).build();
RedisSerializer<String> redisSerializer = (RedisSerializer<String>) redisTemplate.getKeySerializer();
Cursor cursor = (Cursor) redisTemplate.executeWithStickyConnection(redisConnection -> new ConvertingCursor<>(redisConnection.scan(options), redisSerializer::deserialize));
List<String> result = new ArrayList<>();
while(cursor.hasNext()){
result.add(cursor.next().toString());
}
//切记这里一定要关闭,否则会耗尽连接数。
try {
cursor.close();
} catch (IOException e) {
log.error("关闭cursor异常");
}
log.info("scan扫描共耗时:{} ms key数量:{}",System.currentTimeMillis()-start,result.size());
return result;
}
}
当实现了spring cache后,如果引用了spring-boot-starter-actuator,需要实现下面的两个类
import io.micrometer.core.instrument.Tag;
import io.micrometer.core.instrument.binder.MeterBinder;
import io.micrometer.core.instrument.binder.cache.CaffeineCacheMetrics;
import lombok.NoArgsConstructor;
import org.springframework.boot.actuate.metrics.cache.CacheMeterBinderProvider;
@NoArgsConstructor
public class RedisCaffeineCacheMeterBinderProvider implements CacheMeterBinderProvider<RedisCaffeineCache> {
@Override
public MeterBinder getMeterBinder(RedisCaffeineCache cache, Iterable<Tag> tags) {
return new CaffeineCacheMetrics(cache.getCaffeineCache(), cache.getName(), tags);
}
}
import io.micrometer.core.instrument.binder.MeterBinder;
import org.springframework.boot.actuate.metrics.cache.CacheMeterBinderProvider;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({MeterBinder.class, CacheMeterBinderProvider.class})
public class RedisCaffeineCacheMeterConfiguration {
@Bean
public RedisCaffeineCacheMeterBinderProvider redisCaffeineCacheMeterBinderProvider() {
return new RedisCaffeineCacheMeterBinderProvider();
}
}
下面是pom.xml ,springboot :2.2.6.RELEASE,spring-cloud-dependencies :Hoxton.SR9
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<exclusions>
<exclusion>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</exclusion>
<exclusion>
<artifactId>log4j-to-slf4j</artifactId>
<groupId>org.apache.logging.log4j</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.14</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.60</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.9</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>com.ctrip.framework.apollo</groupId>
<artifactId>apollo-client</artifactId>
<version>1.8.0</version>
</dependency>
<!-- mp -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.48</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.21</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.8.1</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
参考代码:https://github.com/pig-mesh/multilevel-cache-spring-boot-starter