Guava、Redis实现二级缓存

    科技2026-04-25  17

    最近在重构老服务,在公共配置服务当中,博主自己设计了一个二级缓存的简易框架,支持一级缓存及二级缓存的技术替换,例如将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<H, HV> { /** * 是否使用缓存 * * @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<H, HK, HV> extends CacheManager<H, HV> { /** * 获取缓存值 * * @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<HK, HV> 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<H, HV> extends CacheManager<H, HV> { /** * 获取缓存值 * * @param key * @return */ HV getCacheValue(H key); Map<H, HV> multiGet(Collection<H> keys); /** * 同步缓存 * * @param key * @return */ Boolean syncCache(H key, HV value, long timeout, TimeUnit unit); Boolean multiSyncCache(Map<H, HV> kvMap, long timeout, TimeUnit unit); } import org.springframework.lang.Nullable; /** * @Desription: hash缓存操作 * @Author: yangchenhui * @Date: 2020/9/21 19:28 */ public interface HashCacheOperations<H, HK, HV> { /** * 获取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<K, V> { 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<K, V> multiGet(Collection<K> 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<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { // 1.创建 redisTemplate 模版 RedisTemplate<Object, Object> 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<String, CacheManager> 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<String, EnumDistVo> buildEnumDistCacheManager() { logger.info("======> start build enumDistCacheManager"); Cache<String, EnumDistVo> 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<String, EnumDistVo> enumDistPrimaryCache = new ValueGuavaPrimaryCacheOperations<>(cache); ValueRedisSecondaryCacheOperations<String, EnumDistVo> enumDistSecondaryCache = new ValueRedisSecondaryCacheOperations<String, EnumDistVo>(redisTemplate) { @Override public Map<String, EnumDistVo> multiGet(Collection<String> keys) { List<EnumDistVo> enumDistVoList = redisTemplate.opsForValue().multiGet(keys); if (CollectionUtils.isEmpty(enumDistVoList)) { return null; } Map<String, EnumDistVo> 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<String, String, CommonParamVo> buildCommonParamCacheManager() { logger.info("======> start build commonParamCacheManager"); Cache<String, Map<String, CommonParamVo>> 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<String, String, CommonParamVo> commonParamPrimaryCache = new HashGuavaPrimaryCacheOperations<>(cache); HashRedisSecondaryCacheOperations<String, String, CommonParamVo> commonParamSecondaryCache = new HashRedisSecondaryCacheOperations<>(redisTemplate); DefaultHashCacheManager<String, String, CommonParamVo> 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

     

     

    Processed: 0.012, SQL: 9