SpringBoot 整合 caffeine+Redis 实现二级缓存

caffeine 本地缓存

Caffeine是基于JAVA 1.8 Version的高性能缓存库。Caffeine提供的内存缓存使用参考Google guava的API。Caffeine是基于Google Guava Cache设计经验上改进的成果。

<dependency>
    <groupId>com.github.ben-manes.caffeinegroupId>
    <artifactId>caffeineartifactId>
    <version>2.8.2version>
dependency>

常用API

caffeine 有4中初始化方法:

Cache 手动加载

Cache<Key, Object> cache = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .maximumSize(10_000)
    .build();

// 查找一个缓存元素, 没有查找到的时候返回null
Object graph = cache.getIfPresent(key);
// 查找缓存,如果缓存不存在则生成缓存元素,  如果无法生成则返回null
graph = cache.get(key, k -> createObject(key));
// 添加或者更新一个缓存元素
cache.put(key, graph);
// 移除一个缓存元素
cache.invalidate(key);

LoadingCache 自动加载

LoadingCache<Key, Graph> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(key -> createExpensiveGraph(key)); //传入根据key获取数据的方法

// 查找缓存,如果缓存不存在则生成缓存元素,  如果无法生成则返回null
Graph graph = cache.get(key);
// 批量查找缓存,如果缓存不存在则生成缓存元素
Map<Key, Graph> graphs = cache.getAll(keys);

AsyncCache 手动异步加载

AsyncCache<Key, Graph> cache = Caffeine.newBuilder()
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .maximumSize(10_000)
    .buildAsync();

// 查找缓存元素,如果不存在,则异步生成
CompletableFuture<Graph> graph = cache.get(key, k -> createExpensiveGraph(key));

AsyncLoadingCache 自动异步加载

AsyncLoadingCache<Key, Graph> cache = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    // 你可以选择: 去异步的封装一段同步操作来生成缓存元素
    .buildAsync(key -> createExpensiveGraph(key));
    // 你也可以选择: 构建一个异步缓存元素操作并返回一个future
    .buildAsync((key, executor) -> createExpensiveGraphAsync(key, executor));

// 查找缓存元素,如果其不存在,将会异步进行生成
CompletableFuture<Graph> graph = cache.get(key);
// 批量查找缓存元素,如果其不存在,将会异步进行生成
CompletableFuture<Map<Key, Graph>> graphs = cache.getAll(keys);

可选参数

//设置缓存大小
cache..maximumSize(10_000); 

//在最后一次写入缓存后开始计时,在指定的时间后过期。(优先级高)
cache.expireAfterWrite(long, TimeUnit) 

//在最后一次读或写后开始计时,在指定的时间后过期。假如一直有请求访问该key,那么这个缓存将一直不会过期。
cache.expireAfterAccess(long, TimeUnit) 

//传入自定义的过期处理类
cache.expire();    

//这个参数是 LoadingCache 和 AsyncLoadingCache 的才会有的。在刷新的时候如果查询缓存元素,其旧值将仍被返回,直到该元素的刷新完毕后结束后才会返回刷新后的新值。
cache.refreshAfterWrite(long, TimeUnit);

//设置缓存被删除时的监听器
cache.removalListener();

//开启监控功能,使用 cache.status()会返回 CacheStats 对象: 
//CacheStats: hitRate()查询缓存的命中率,evictionCount(): 被驱逐的缓存数量,averageLoadPenalty(): 新值被载入的平均耗时
cache.recordStats()

更多使用方法参考: https://zhuanlan.zhihu.com/p/329684099

SpringBoot 整合 Caffeine + Redis 实现 2级缓存

一级缓存: caffeine, 二级缓存: redis。使用caffeine一级缓存能够减去 查询redis时网络传输耗时。

实现方案: 自定义缓存管理器整合redis和caffeine,查询时从redis内的缓存放入caffeine,修改或删除缓存时通过redis发布订阅功能所有应用删除caffeine的缓存。

mvn 依赖:


<dependency>
    <groupId>com.github.ben-manes.caffeinegroupId>
    <artifactId>caffeineartifactId>
dependency>

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
dependency>

1 自定义缓存类

继承 AbstractValueAdaptingCache 实现自定义的缓存类

@Slf4j
public class RedisCaffeineCache extends AbstractValueAdaptingCache {

    //cacheName
    private final String name;

    private final RedisTemplate redisTemplate;

    private final Cache<Object, Object> caffeineCache;

    private final CacheConfigProperties properties;


    public RedisCaffeineCache(String name, RedisTemplate redisTemplate, Cache caffeineCache, CacheConfigProperties properties) {
        //是否缓存null,可防止缓 存穿透
        super( properties.getAllowNull() );

        this.name = name;

        this.redisTemplate = redisTemplate;
        this.caffeineCache = caffeineCache;
        this.properties = BeanUtil.copyProperties(properties,CacheConfigProperties.class);
    }

    @Override
    public String getName() {
        return this.name;
    }

    @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 = 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) {
            try {
                Class<?> c = Class.forName("org.springframework.cache.Cache$ValueRetrievalException");
                Constructor<?> constructor = c.getConstructor(Object.class, Callable.class, Throwable.class);
                RuntimeException exception = (RuntimeException) constructor.newInstance(key, valueLoader, e.getCause());
                throw exception;
            } catch (Exception e1) {
                throw new IllegalStateException(e1);
            }
        } finally {
            lock.unlock();
        }
    }

    @Override
    public void put(Object key, Object value) {
        //如果key是null 并且不支持null 直接删除
        if ( value == null && !super.isAllowNullValues() ) {
            this.evict(key);
            return;
        }

        if( properties.getExpireMillis() > 0) {
            redisTemplate.opsForValue().set(getKey(key), toStoreValue(value), properties.getExpireMillis(), TimeUnit.MILLISECONDS);
        } else {
            redisTemplate.opsForValue().set(getKey(key), toStoreValue(value));
        }

        caffeineCache.put(getKey(key), value);
    }

    @Override
    public ValueWrapper putIfAbsent(Object key, Object value) {
        Object cacheKey = getKey(key);
        Object prevValue = null;
        // 考虑使用分布式锁,或者将redis的setIfAbsent改为原子性操作
        synchronized (key) {
            prevValue = redisTemplate.opsForValue().get(cacheKey);

            boolean putSuccess = false;

            if( properties.getExpireMillis() > 0 ) {
                putSuccess = redisTemplate.opsForValue().setIfAbsent(getKey(key), toStoreValue(value), properties.getExpireMillis(), TimeUnit.MILLISECONDS);
            } else {
                putSuccess = redisTemplate.opsForValue().setIfAbsent(getKey(key), toStoreValue(value));
            }

            //如果设置成功
            if( putSuccess ) {
                publish( new DelCacheMessage( this.name,key ) );
                caffeineCache.put(key, toStoreValue(value));
            }
        }
        return toValueWrapper(prevValue);
    }

    /**
     * 剔除某个缓存
     */
    @Override
    public void evict(Object key) {

        final Object cacheKey = getKey(key);

        // 先清除redis中缓存数据,然后清除caffeine中的缓存,避免短时间内如果先清除caffeine缓存后其他请求会再从redis里加载到caffeine中
        redisTemplate.delete( cacheKey );
        //发布缓存 让所有caffeine清除缓存
        publish( new DelCacheMessage( name,cacheKey ) );
        //清除本地缓存
        caffeineCache.invalidate(cacheKey);
    }

    /**
     * 清除所有缓存
     */
    @Override
    public void clear() {
        // 先清除redis中缓存数据,然后清除caffeine中的缓存,避免短时间内如果先清除caffeine缓存后其他请求会再从redis里加载到caffeine中
        Set<Object> keys = redisTemplate.keys(this.name.concat(":"));
        for(Object key : keys)  redisTemplate.delete(key);

        //发布消息让所有caffeine清除缓存
        publish( new DelCacheMessage(name,"ALL") );

        //清除本地缓存
        caffeineCache.invalidateAll();
    }

    /**
     * 查询缓存
     */
    @Override
    protected Object lookup (Object key ) {
        //获取缓存key
        Object cacheKey = getKey(key);

        //查询一级缓存(caffeine)
        Object value = caffeineCache.getIfPresent(cacheKey);

        if(value != null) {
            log.info("从caffeine内获取缓存,key: {},value: {}", cacheKey,value);
            return value;
        }

        //查询二级缓存
        value = redisTemplate.opsForValue().get(cacheKey);

        if(value != null) {
            log.info("从redis获取缓存并放入caffeine,key: {} ,value: {}", cacheKey,value);
            caffeineCache.put(cacheKey, value);
        }
        return value;
    }

    /**
     * 获取缓存key
     */
    private String getKey(Object key) {
        String cacheKey = key.toString();
        return this.name.concat(":").concat( cacheKey );
    }

    /**
     * @description 缓存变更时通知其他节点清理本地缓存
     * @param message 要删除的缓存
     */
    private void publish( DelCacheMessage message) {
        redisTemplate.convertAndSend(properties.getChannelTopic(), message);
    }

    /**
     * @description 清理本地缓存
     */
    public void clearLocal(Object key) {
        log.info("清除本地caffeine缓存, key : {}", key);
        if(key == null) {
            caffeineCache.invalidateAll();
        } else {
            caffeineCache.invalidate(key);
        }
    }
}

消息包装类

@Data
@NoArgsConstructor
@AllArgsConstructor
public class DelCacheMessage {

    private String cacheName;

    private Object cacheKey;
}

发布订阅消费者

@Slf4j
public class CacheDelListener implements MessageListener {


    private RedisTemplate redisTemplate;

    private Cache caffeineCache;

    public CacheDelListener(RedisTemplate  redisTemplate, Cache caffeineCache){
        this.redisTemplate = redisTemplate;
        this.caffeineCache = caffeineCache;
    }

    @Override
    public void onMessage(Message message, byte[] pattern) {
        //接收到消息说明要删除本地的缓存
        DelCacheMessage delMessage = (DelCacheMessage)redisTemplate.getValueSerializer().deserialize(message.getBody());

        Object channel = redisTemplate.getStringSerializer().deserialize(message.getChannel());

        log.info("删除本地缓存,channel: {{}}, cacheKey:{{}}",channel,delMessage.getCacheKey());
        //删除caffeine的缓存
        caffeineCache.invalidate( delMessage.getCacheKey() );
    }
}

CacheConfigProperties

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CacheConfigProperties {

    /**
     * 是否允许控制,主要用于缓存穿透
     */
    private Boolean allowNull = true;

    /**
     * 缓存过期时间
     */
    private Long expireMillis = 60 * 1000L;

    /**
     * redis发布订阅的主题,用于删除缓存
     */
    private String channelTopic = "cache:redis:caffeine:topic";

}

2 自定义缓存管理器

@Slf4j
public class RedisCaffeineCacheManager implements CacheManager {

    private ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<String, Cache>();

    private CacheConfigProperties properties;

    private RedisTemplate redisTemplate;

    private com.github.benmanes.caffeine.cache.Cache caffeineCache;

    public RedisCaffeineCacheManager(RedisTemplate redisTemplate,
                                     com.github.benmanes.caffeine.cache.Cache caffeineCache,
                                     CacheConfigProperties cacheRedisCaffeineProperties){
        this.properties = cacheRedisCaffeineProperties;
        this.redisTemplate = redisTemplate;
        this.caffeineCache = caffeineCache;
    }

    @Override
    public Cache getCache(String name) {
        Cache cache = cacheMap.get(name);

        if( cache != null ) return cache;

        cache = new RedisCaffeineCache(name,redisTemplate,this.caffeineCache,this.properties);


        //放入map
        cacheMap.put(name,cache);
        return cache;
    }

    @Override
    public Collection<String> getCacheNames() {
        return this.cacheMap.keySet();
    }

    public com.github.benmanes.caffeine.cache.Cache getCaffeineCache() {
        return caffeineCache;
    }

    public CacheConfigProperties getProperties() {
        return properties;
    }
}

3 使用缓存管理器

public class RedisConfig {

    private static final StringRedisSerializer STRING_SERIALIZER = new StringRedisSerializer();
    private static final GenericJackson2JsonRedisSerializer JACKSON_SERIALIZER = new GenericJackson2JsonRedisSerializer();


    //redis的缓存管理器
    @Primary
    @Bean("redisCacheManager")
    public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
        //设置缓存过期时间
        RedisCacheConfiguration redisCacheCfg = RedisCacheConfiguration.defaultCacheConfig()
                .entryTtl(Duration.ofHours(1))
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(STRING_SERIALIZER))
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(JACKSON_SERIALIZER));
        return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
                .cacheDefaults(redisCacheCfg)
                .build();
    }


    //配置redis缓存
    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        // 配置redisTemplate
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate();
        redisTemplate.setConnectionFactory(factory);
        // key序列化
        redisTemplate.setKeySerializer(STRING_SERIALIZER);
        // value序列化
        redisTemplate.setValueSerializer(JACKSON_SERIALIZER);
        // Hash key序列化
        redisTemplate.setHashKeySerializer(STRING_SERIALIZER);
        // Hash value序列化
        redisTemplate.setHashValueSerializer(JACKSON_SERIALIZER);
        redisTemplate.afterPropertiesSet();

        return redisTemplate;
    }

    @Bean
    //配置发布订阅 消费者
    public RedisMessageListenerContainer cacheDelListenerContainer(RedisConnectionFactory redisConnectionFactory,MessageListenerAdapter cacheDelListenerAdapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer(  );

        container.setConnectionFactory(redisConnectionFactory);

        container.addMessageListener(cacheDelListenerAdapter,new ChannelTopic("cache:redis:caffeine:topic")  );
        return container;
    }

    @Bean
    public MessageListenerAdapter cacheDelListenerAdapter( @Autowired RedisTemplate redisTemplate,@Autowired RedisCaffeineCacheManager cacheManager ){

        Cache caffeineCache = cacheManager.getCaffeineCache();
        CacheDelListener cacheDelListener = new CacheDelListener(redisTemplate, caffeineCache);

        MessageListenerAdapter messageListenerAdapter = new MessageListenerAdapter();
        messageListenerAdapter.setDelegate( cacheDelListener );

        return messageListenerAdapter;
    }



    //自定义的缓存管理器
    @Bean("level2CacheManager")
    public RedisCaffeineCacheManager redisCaffeineCacheManager( @Autowired RedisTemplate redisTemplate){

        CacheConfigProperties properties = new CacheConfigProperties(true,60000L,"cache:redis:caffeine:topic");

        return new RedisCaffeineCacheManager(redisTemplate,caffeineCache(),properties);
    }


    //配置 caffeine缓存
    public com.github.benmanes.caffeine.cache.Cache<String, Object> caffeineCache(){
        Caffeine<Object, Object> cacheBuilder = Caffeine.newBuilder();

        // 访问后过期时间,单位毫秒
        cacheBuilder.expireAfterAccess(7000, TimeUnit.MILLISECONDS);

        // 写入后过期时间,单位毫秒
        cacheBuilder.expireAfterWrite(6000, TimeUnit.MILLISECONDS);

        //初始化大小
        cacheBuilder.initialCapacity(10);

        //最大缓存对象个数,超过此数量时之前放入的缓存将失效
        cacheBuilder.maximumSize( 1000 );

        // 写入后刷新时间,单位毫秒
        cacheBuilder.refreshAfterWrite(8000, TimeUnit.MILLISECONDS);

        cacheBuilder.removalListener((RemovalListener<String, Object>) (key, value, cause) -> {
            log.info("caffeine缓存过期,key:{}",key);
        });

        return cacheBuilder.softValues().build( cacheLoader() );
    }

    //caffeine用到的类
    public CacheLoader<String, Object> cacheLoader() {
        CacheLoader<String, Object> cacheLoader = new CacheLoader<String, Object>() {
            @Override
            public Object load(String key) throws Exception {
                return null;
            }
            // 重写这个方法将oldValue值返回回去,进而刷新缓存
            @Override
            public Object reload(String key, Object oldValue) throws Exception {
                return oldValue;
            }
        };
        return cacheLoader;
    }


}

你可能感兴趣的:(springBoot,redis,缓存,spring,boot)