redis缓存_Guava、Redis实现二级缓存

本文介绍了如何利用Redis和Guava构建二级缓存系统,探讨了基本设计、配置方法、Guava内存占用及性能压测,并提出了改进空间。通过二级缓存可以提升接口性能,减少数据库压力,实现本地缓存与分布式缓存的优势互补。
摘要由CSDN通过智能技术生成

点击上方蓝色字体,选择“标星公众号”

优质文章,第一时间送达

最近在重构老服务,在公共配置服务当中,博主自己设计了一个二级缓存的简易框架,支持一级缓存及二级缓存的技术替换,例如将Guava换成Caffeine,或者将Redis换成Memcached。

缓存是服务端提高接口访问速度和减轻数据库压力的利器,对于热点数据的缓存是很有必要的。

缓存也有分类:

  1. 本地缓存:Guava,Caffeine,Ehcache或者自己简单的使用HashMap实现;

  2. 分布式缓存:Redis,Memcached等。

虽然分布式缓存(redis等)单节点QPS已经到达10W级别,但是相对于本地缓存,还是多了一次网络请求。所以如果对性能提出更高的要求,就需要将本地缓存和分布式缓存各取所长,组合成为二级缓存。

同时,为了防止一级缓存和二级缓存同时失效,二级缓存的时间应该比一级缓存稍长,这样一级缓存失效的时候,请求会直接从二级缓存中获取数据,能够大量的避免缓存穿透。

一级缓存失效的时候,触发回调方法,进行对应key的缓存同步。

1、基本设计

下面是二级缓存的基本架构图:

dfeb894809a2736980f6f7c95366589a.png

主要组件有:

  1. 缓存管理器:管理缓存,其中会组合一级缓存操作者和二级缓存操作者

  2. value缓存操作者:针对简单数据类型进行缓存操作

  3. hash缓存操作者:针对hash数据类型进行缓存操作

缓存管理器分别有默认的value和hash实现,值得注意的是,在获取多个参数的时候,为了方便客户端调用,会将List转换为Map,其中key为cacheKey(由对应的dto或者vo生成),value为对应的vo对象。

当前默认使用guava实现一级缓存(本地缓存),使用redis实现分布式缓存。如果需要自己扩展,例如使用caffeine替换guava,或者使用memcached替换redis,只需要实现对应的接口即可,不需要在缓存管理器中做修改,遵循开闭原则。

a5db481f483df9d9e07bfff8cdfb4c1e.png

/**
 * @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、内存大小占用

26d67bde6560ca0dd6ac0b715abe003e.png

看看Cache当中存储1W对象消耗的内存大小,4.1M。

3.2、性能压测对比

不使用二级缓存,直接查库:

9ab26b8e58e1a32bd145e2447986655c.png

使用二级缓存:

2ae63b381779f717edb65280cd4ba2d3.png

使用同样的线程组,没有使用缓存前,tps为55.1,使用缓存后tps为1283.2,性能提升为之前的23.33倍。

结论:仅仅使用4.1M的内存,提升23倍的性能。

4、改进空间

  1. 需要后台配合,修改对应表数据的时候,需要通知公共配置服务,更新缓存值;

  2. 当前使用单例 + 工厂模式实现CacheManager的创建,如果觉得繁琐,后面直接交由Spring管理。

  3. 增加自定义注解,使用环绕通知实现缓存的获取,缓存的同步设置,简化业务代码,只需要在对应的方法上配置上相应的注解即可实现二级缓存。

相关资料:

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

c626f3fb26ea868af2a85e15dc52b730.gif

80ad47bc82f88d6e403ded20004f96da.gif

  • 新款SpringBoot在线教育平台开源了

  • 精品帖子大汇总

  • 一把“乐观锁”轻松搞定高并发下的幂等性问题(附视频教程)

  • 一文搞懂Java8 Lambda表达式(附视频教程)

感谢点赞支持下哈 78dc7fa79193549ca3e88de9f8a00036.gif

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值