点击上方蓝色字体,选择“标星公众号”
优质文章,第一时间送达
最近在重构老服务,在公共配置服务当中,博主自己设计了一个二级缓存的简易框架,支持一级缓存及二级缓存的技术替换,例如将Guava换成Caffeine,或者将Redis换成Memcached。
缓存是服务端提高接口访问速度和减轻数据库压力的利器,对于热点数据的缓存是很有必要的。
缓存也有分类:
本地缓存:Guava,Caffeine,Ehcache或者自己简单的使用HashMap实现;
分布式缓存:Redis,Memcached等。
虽然分布式缓存(redis等)单节点QPS已经到达10W级别,但是相对于本地缓存,还是多了一次网络请求。所以如果对性能提出更高的要求,就需要将本地缓存和分布式缓存各取所长,组合成为二级缓存。
同时,为了防止一级缓存和二级缓存同时失效,二级缓存的时间应该比一级缓存稍长,这样一级缓存失效的时候,请求会直接从二级缓存中获取数据,能够大量的避免缓存穿透。
一级缓存失效的时候,触发回调方法,进行对应key的缓存同步。
1、基本设计
下面是二级缓存的基本架构图:
主要组件有:
缓存管理器:管理缓存,其中会组合一级缓存操作者和二级缓存操作者
value缓存操作者:针对简单数据类型进行缓存操作
hash缓存操作者:针对hash数据类型进行缓存操作
缓存管理器分别有默认的value和hash实现,值得注意的是,在获取多个参数的时候,为了方便客户端调用,会将List转换为Map,其中key为cacheKey(由对应的dto或者vo生成),value为对应的vo对象。
当前默认使用guava实现一级缓存(本地缓存),使用redis实现分布式缓存。如果需要自己扩展,例如使用caffeine替换guava,或者使用memcached替换redis,只需要实现对应的接口即可,不需要在缓存管理器中做修改,遵循开闭原则。
/**
* @Desription: 缓存管理接口
* @Author: yangchenhui
* @Date: 2020/9/18 13:09
*/
public interface CacheManager {
/**
* 是否使用缓存
*
* @return
*/
Boolean useCache();
/**
* 缓存管理者名称
*
* @return
*/
String cacheManagerName();
/**
* 缓存失效
*
* @param key
*/
Boolean invalidateAll(H key);
}
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @Desription:
* @Author: yangchenhui
* @Date: 2020/9/22 13:26
*/
public interface HashCacheManager extends CacheManager {
/**
* 获取缓存值
*
* @param key
* @param hashKey
* @return
*/
HV getHashCacheValue(H key, HK hashKey);
/**
* 同步缓存,针对一二级缓存做同步,将二级缓存中的值设置到一级缓存中
*
* @param key
* @param hashKey
* @param value
* @return
*/
Boolean syncHashCache(H key, HK hashKey, HV value, long timeout, TimeUnit unit);
/**
* 缓存失效
*
* @param key
* @param hashKey
* @return
*/
Boolean invalidateKey(H key , HK hashKey);
/**
* 当前key的缓存值map
* @param key
* @return
*/
Map showAsMap(H key);
}
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @Desription:
* @Author: yangchenhui
* @Date: 2020/9/22 14:08
*/
public interface ValueCacheManager extends CacheManager {
/**
* 获取缓存值
*
* @param key
* @return
*/
HV getCacheValue(H key);
Map multiGet(Collection keys);
/**
* 同步缓存
*
* @param key
* @return
*/
Boolean syncCache(H key, HV value, long timeout, TimeUnit unit);
Boolean multiSyncCache(Map kvMap, long timeout, TimeUnit unit);
}
import org.springframework.lang.Nullable;
/**
* @Desription: hash缓存操作
* @Author: yangchenhui
* @Date: 2020/9/21 19:28
*/
public interface HashCacheOperations {
/**
* 获取hash对应的缓存值
*
* @param key
* @param hashKey
* @return
*/
@Nullable
HV get(H key, HK hashKey);
/**
* 存储hash缓存值
*
* @param key
* @param hashKey
* @param value
*/
void put(H key, HK hashKey, HV value);
/**
* 删除hashKey缓存值
*
* @param key
* @param hashKeys
* @return
*/
Long delete(H key, HK... hashKeys);
/**
* 删除key缓存值
*
* @param key
* @return
*/
Boolean delete(H key);
}
import org.springframework.lang.Nullable;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* @Desription: 操作简单key --> value
* @Author: yangchenhui
* @Date: 2020/9/21 18:53
*/
public interface ValueCacheOperations {
void set(K key, V value);
void set(K key, V value, long timeout, TimeUnit unit);
@Nullable
V get(K key);
@Nullable
V getAndSet(K key, V value);
@Nullable
Map multiGet(Collection keys);
void delete(K key);
}
2、使用配置
2.1、redis配置
同理,如果使用其他的二级缓存实现,进行相关配置即可。
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @Desription:
* @Author: yangchenhui
* @Date: 2020/9/22 11:05
*/
@Component("jedisPoolProperties")
@ConfigurationProperties(prefix = "cache.redis.pool")
public class JedisPoolProperties {
private int maxTotal;
private int maxIdle;
private int minIdle;
private boolean testOnBorrow;
private long maxWaitMillis;
public int getMaxTotal() {
return maxTotal;
}
public void setMaxTotal(int maxTotal) {
this.maxTotal = maxTotal;
}
public int getMaxIdle() {
return maxIdle;
}
public void setMaxIdle(int maxIdle) {
this.maxIdle = maxIdle;
}
public int getMinIdle() {
return minIdle;
}
public void setMinIdle(int minIdle) {
this.minIdle = minIdle;
}
public boolean isTestOnBorrow() {
return testOnBorrow;
}
public void setTestOnBorrow(boolean testOnBorrow) {
this.testOnBorrow = testOnBorrow;
}
public long getMaxWaitMillis() {
return maxWaitMillis;
}
public void setMaxWaitMillis(long maxWaitMillis) {
this.maxWaitMillis = maxWaitMillis;
}
}
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* @Desription:
* @Author: yangchenhui
* @Date: 2020/9/22 11:15
*/
@Component("redisServerProperties")
@ConfigurationProperties(prefix = "cache.redis.server")
public class RedisServerProperties {
private String host;
private int port;
private String password;
private int database;
private int timeout;
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public int getDatabase() {
return database;
}
public void setDatabase(int database) {
this.database = database;
}
public int getTimeout() {
return timeout;
}
public void setTimeout(int timeout) {
this.timeout = timeout;
}
}
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisPassword;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import redis.clients.jedis.JedisPoolConfig;
/**
* @Desription: redis配置
* @Author: yangchenhui
* @Date: 2020/9/22 10:44
*/
@Configuration
public class RedisCacheConfig extends CachingConfigurerSupport {
@Resource
JedisPoolProperties jedisPoolProperties;
@Resource
RedisServerProperties redisServerProperties;
@Bean
public RedisConnectionFactory redisConnectionFactory(JedisPoolConfig jedisPool, RedisStandaloneConfiguration jedisConfig) {
JedisConnectionFactory connectionFactory = new JedisConnectionFactory(jedisConfig);
connectionFactory.setPoolConfig(jedisPool);
return connectionFactory;
}
@Configuration
public class JedisConf {
@Bean
public JedisPoolConfig jedisPool() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxIdle(jedisPoolProperties.getMaxIdle());
jedisPoolConfig.setMaxWaitMillis(jedisPoolProperties.getMaxWaitMillis());
jedisPoolConfig.setMaxTotal(jedisPoolProperties.getMaxTotal());
jedisPoolConfig.setMinIdle(jedisPoolProperties.getMinIdle());
jedisPoolConfig.setTestOnBorrow(jedisPoolProperties.isTestOnBorrow());
return jedisPoolConfig;
}
@Bean
public RedisStandaloneConfiguration jedisConfig() {
RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
config.setHostName(redisServerProperties.getHost());
config.setPort(redisServerProperties.getPort());
config.setDatabase(redisServerProperties.getDatabase());
config.setPassword(RedisPassword.of(redisServerProperties.getPassword()));
return config;
}
}
/**
* 设置 redisTemplate 的序列化设置
* @param redisConnectionFactory
* @return
*/
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 1.创建 redisTemplate 模版
RedisTemplate template = new RedisTemplate<>();
// 2.关联 redisConnectionFactory
template.setConnectionFactory(redisConnectionFactory);
// 3.创建 序列化类
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
// 4.设置可见度
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 5.启动默认的类型
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
// 6.序列化类,对象映射设置
jackson2JsonRedisSerializer.setObjectMapper(om);
// 7.设置redis序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setKeySerializer(jackson2JsonRedisSerializer);
template.setHashKeySerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();return template;
}
}
2.2、guava配置
guava不支持动态的对单个的key设置不同的过期时间,过期时间是在Cache构建的时候就已经确定的。
需要注意的一点是,在配置redis的二级缓存的时候,由于我们需要将List转换为Map,所以需要实现抽象类中的方法multiGet()。
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.CollectionUtils;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* @Desription: 缓存管理工厂
* @Author: yangchenhui
* @Date: 2020/9/22 14:59
*/
public class CacheManagerFactory {
private static Logger logger = LogManager.getLogger(CacheManagerFactory.class);
public static Map cacheManagerMap = new ConcurrentHashMap<>(10);
public static RedisTemplate redisTemplate;
public static CacheManager obtainCacheManager(String cacheManagerName) {
CacheManager cacheManager = cacheManagerMap.get(cacheManagerName);if (cacheManager != null) {return cacheManager;
}return createCacheManager(cacheManagerName);
}
private static synchronized CacheManager createCacheManager(String cacheManagerName) {
// 双重锁检查,防止对象重复创建
CacheManager cacheManager = cacheManagerMap.get(cacheManagerName);if (cacheManager != null) {return cacheManager;
}if (CacheManagerEnum.COMMON_PARAM_CACHE_MANAGER.getCacheName().equals(cacheManagerName)) {return buildCommonParamCacheManager();
} else if (CacheManagerEnum.PRD_PARAM_CACHE_MANAGER.getCacheName().equals(cacheManagerName)) {return buildPrdParamCacheManager();
} else if (CacheManagerEnum.ENUM_DIST_CACHE_MANAGER.getCacheName().equals(cacheManagerName)) {return buildEnumDistCacheManager();
} else if (CacheManagerEnum.ACCESS_RULE_CACHE_MANAGER.getCacheName().equals(cacheManagerName)) {return buildAccessRuleCacheManager();
} else {
throw new CacheException("-1", "暂无该缓存管理者");
}
}
private static DefaultValueCacheManager buildEnumDistCacheManager() {
logger.info("======> start build enumDistCacheManager");
Cache cache = CacheBuilder.newBuilder()
.concurrencyLevel(8)
.expireAfterWrite(6, TimeUnit.HOURS)
.initialCapacity(80)
.maximumSize(800)
.recordStats()
.removalListener(notification -> logger.info("enumDistPrimaryCache remove cache,key={},cause={}", notification.getKey(), notification.getCause()))
.build();
ValueGuavaPrimaryCacheOperations enumDistPrimaryCache = new ValueGuavaPrimaryCacheOperations<>(cache);
ValueRedisSecondaryCacheOperations enumDistSecondaryCache = new ValueRedisSecondaryCacheOperations(redisTemplate) {
@Override
public Map multiGet(Collection keys) {
List enumDistVoList = redisTemplate.opsForValue().multiGet(keys);if (CollectionUtils.isEmpty(enumDistVoList)) {return null;
}
Map enumDistMap = enumDistVoList.stream().filter(e -> e != null).
collect(Collectors.toMap(e -> e.buildCacheKey(), e -> e, (k1, k2) -> k1));return enumDistMap;
}
};return new DefaultValueCacheManager<>(CacheManagerEnum.ENUM_DIST_CACHE_MANAGER.getCacheName(),
enumDistPrimaryCache, enumDistSecondaryCache);
}
private static DefaultHashCacheManager buildCommonParamCacheManager() {
logger.info("======> start build commonParamCacheManager");
Cache> cache = CacheBuilder.newBuilder()
//设置并发级别为8,并发级别是指可以同时写缓存的线程数
.concurrencyLevel(8)
//设置写缓存后6小时过期
.expireAfterWrite(6, TimeUnit.HOURS)
//设置缓存容器的初始容量为100
.initialCapacity(100)
//设置缓存最大容量为1000,超过1000之后就会按照LRU最近虽少使用算法来移除缓存项
.maximumSize(1000)
//设置要统计缓存的命中率
.recordStats()
//设置缓存的移除通知
.removalListener(notification -> logger.info("commonParamPrimaryCache remove cache,key={},cause={}", notification.getKey(), notification.getCause()))
.build();
HashGuavaPrimaryCacheOperations commonParamPrimaryCache = new HashGuavaPrimaryCacheOperations<>(cache);
HashRedisSecondaryCacheOperations commonParamSecondaryCache =
new HashRedisSecondaryCacheOperations<>(redisTemplate);
DefaultHashCacheManager cacheManager =
new DefaultHashCacheManager<>(CacheManagerEnum.COMMON_PARAM_CACHE_MANAGER.getCacheName(),
commonParamPrimaryCache, commonParamSecondaryCache);
cacheManagerMap.put(CacheManagerEnum.COMMON_PARAM_CACHE_MANAGER.getCacheName(), cacheManager);return cacheManager;
}
public static void setRedisTemplate(RedisTemplate redisTemplate) {
CacheManagerFactory.redisTemplate = redisTemplate;
}
}
3、guava内存占用情况及性能压测
3.1、内存大小占用
看看Cache当中存储1W对象消耗的内存大小,4.1M。
3.2、性能压测对比
不使用二级缓存,直接查库:
使用二级缓存:
使用同样的线程组,没有使用缓存前,tps为55.1,使用缓存后tps为1283.2,性能提升为之前的23.33倍。
结论:仅仅使用4.1M的内存,提升23倍的性能。
4、改进空间
需要后台配合,修改对应表数据的时候,需要通知公共配置服务,更新缓存值;
当前使用单例 + 工厂模式实现CacheManager的创建,如果觉得繁琐,后面直接交由Spring管理。
增加自定义注解,使用环绕通知实现缓存的获取,缓存的同步设置,简化业务代码,只需要在对应的方法上配置上相应的注解即可实现二级缓存。
相关资料:
guava中文文档地址:
https://wizardforcel.gitbooks.io/guava-tutorial/content/1.html
redis内存使用情况:
https://blog.csdn.net/yangchenhui666/article/details/10878619
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:
https://blog.csdn.net/yangchenhui666/article/details/108970061
新款SpringBoot在线教育平台开源了
精品帖子大汇总
一把“乐观锁”轻松搞定高并发下的幂等性问题(附视频教程)
一文搞懂Java8 Lambda表达式(附视频教程)
感谢点赞支持下哈