您的当前位置:首页正文

springboot+springcache实现两级缓存(redis+caffeine)

2023-08-30 来源:步旅网
springboot+springcache实现两级缓存(redis+caffeine)

spring boot中集成了spring cache,并有多种缓存⽅式的实现,如:Redis、Caffeine、JCache、EhCache等等。但如果只⽤⼀种缓存,要么会有较⼤的⽹络消耗(如Redis),要么就是内存占⽤太⼤(如Caffeine这种应⽤内存缓存)。在很多场景下,可以结合起来实现⼀、⼆级缓存的⽅式,能够很⼤程度提⾼应⽤的处理效率。内容说明:

1. 缓存、两级缓存

2. spring cache:主要包含spring cache定义的接⼝⽅法说明和注解中的属性说明3. spring boot + spring cache:RedisCache实现中的缺陷4. caffeine简介

5. spring boot + spring cache 实现两级缓存(redis + caffeine)

缓存、两级缓存

简单的理解,缓存就是将数据从读取较慢的介质上读取出来放到读取较快的介质上,如磁盘-->内存。平时我们会将数据存储到磁盘上,如:数据库。如果每次都从数据库⾥去读取,会因为磁盘本⾝的IO影响读取速度,所以就有了像redis这种的内存缓存。可以将数据读取出来放到内存⾥,这样当需要获取数据时,就能够直接从内存中拿到数据返回,能够很⼤程度的提⾼速度。但是⼀般redis是单独部署成集群,所以会有⽹络IO上的消耗,虽然与redis集群的链接已经有连接池这种⼯具,但是数据传输上也还是会有⼀定消耗。所以就有了应⽤内缓存,如:caffeine。当应⽤内缓存有符合条件的数据时,就可以直接使⽤,⽽不⽤通过⽹络到redis中去获取,这样就形成了两级缓存。应⽤内缓存叫做⼀级缓存,远程缓存(如redis)叫做⼆级缓存spring cache

当使⽤缓存的时候,⼀般是如下的流程:

从流程图中可以看出,为了使⽤缓存,在原有业务处理的基础上,增加了很多对于缓存的操作,如果将这些耦合到业务代码当中,开发起来就有很多重复性的⼯作,并且不太利于根据代码去理解业务。

spring cache是spring-context包中提供的基于注解⽅式使⽤的缓存组件,定义了⼀些标准接⼝,通过实现这些接⼝,就可以通过在⽅法上增加注解来实现缓存。这样就能够避免缓存代码与业务处理耦合在⼀起的问题。spring cache的实现是使⽤spring aop中对⽅法切⾯(MethodInterceptor)封装的扩展,当然spring aop也是基于Aspect来实现的。spring cache核⼼的接⼝就两个:Cache和CacheManagerCache接⼝

提供缓存的具体操作,⽐如缓存的放⼊、读取、清理,spring框架中默认提供的实现有:

除了RedisCache是在spring-data-redis包中,其他的基本都是在spring-context-support包中

#Cache.java

package org.springframework.cache;import java.util.concurrent.Callable;public interface Cache {

// cacheName,缓存的名字,默认实现中⼀般是CacheManager创建Cache的bean时传⼊cacheName String getName();

// 获取实际使⽤的缓存,如:RedisTemplate、com.github.benmanes.caffeine.cache.Cache。暂时没发现实际⽤处,可能只是提供获取原⽣缓存的bean,以便需要扩展⼀些缓存操作或统计之类的东西 Object getNativeCache();

// 通过key获取缓存值,注意返回的是ValueWrapper,为了兼容存储空值的情况,将返回值包装了⼀层,通过get⽅法获取实际值 ValueWrapper get(Object key);

// 通过key获取缓存值,返回的是实际值,即⽅法的返回值类型 T get(Object key, Class type);

// 通过key获取缓存值,可以使⽤valueLoader.call()来调使⽤@Cacheable注解的⽅法。当@Cacheable注解的sync属性配置为true时使⽤此⽅法。因此⽅法内需要保证回源到数据库的同步性。避免在缓存失效时⼤量请求回源到数据库

T get(Object key, Callable valueLoader); // 将@Cacheable注解⽅法返回的数据放⼊缓存中 void put(Object key, Object value);

// 当缓存中不存在key时才放⼊缓存。返回值是当key存在时原有的数据 ValueWrapper putIfAbsent(Object key, Object value); // 删除缓存

void evict(Object key);

// 删除缓存中的所有数据。需要注意的是,具体实现中只删除使⽤@Cacheable注解缓存的所有数据,不要影响应⽤内的其他缓存 void clear();

// 缓存返回值的包装

interface ValueWrapper { // 返回实际缓存的对象 Object get(); }

// 当{@link #get(Object, Callable)}抛出异常时,会包装成此异常抛出 @SuppressWarnings(\"serial\")

class ValueRetrievalException extends RuntimeException { private final Object key;

public ValueRetrievalException(Object key, Callable loader, Throwable ex) {

super(String.format(\"Value for key '%s' could not be loaded using '%s'\ this.key = key; }

public Object getKey() { return this.key; } }}

CacheManager接⼝

主要提供Cache实现bean的创建,每个应⽤⾥可以通过cacheName来对Cache进⾏隔离,每个cacheName对应⼀个Cache实现。spring框架中默认提供的实现与Cache的实现都是成对出现,包结构也在上图中

#CacheManager.java

package org.springframework.cache;import java.util.Collection;public interface CacheManager {

// 通过cacheName创建Cache的实现bean,具体实现中需要存储已创建的Cache实现bean,避免重复创建,也避免内存缓存对象(如Caffeine)重新创建后原来缓存内容丢失的情况 Cache getCache(String name); // 返回所有的cacheName

Collection getCacheNames();}

常⽤注解说明

@Cacheable:主要应⽤到查询数据的⽅法上

package org.springframework.cache.annotation;import java.lang.annotation.Documented;import java.lang.annotation.ElementType;import java.lang.annotation.Inherited;import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;import java.util.concurrent.Callable;

import org.springframework.core.annotation.AliasFor;@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Inherited@Documented

public @interface Cacheable {

// cacheNames,CacheManager就是通过这个名称创建对应的Cache实现bean @AliasFor(\"cacheNames\") String[] value() default {}; @AliasFor(\"value\")

String[] cacheNames() default {};

// 缓存的key,⽀持SpEL表达式。默认是使⽤所有参数及其计算的hashCode包装后的对象(SimpleKey) String key() default \"\";

// 缓存key⽣成器,默认实现是SimpleKeyGenerator String keyGenerator() default \"\"; // 指定使⽤哪个CacheManager String cacheManager() default \"\"; // 缓存解析器

String cacheResolver() default \"\";

// 缓存的条件,⽀持SpEL表达式,当达到满⾜的条件时才缓存数据。在调⽤⽅法前后都会判断 String condition() default \"\";

// 满⾜条件时不更新缓存,⽀持SpEL表达式,只在调⽤⽅法后判断 String unless() default \"\";

// 回源到实际⽅法获取数据时,是否要保持同步,如果为false,调⽤的是Cache.get(key)⽅法;如果为true,调⽤的是Cache.get(key, Callable)⽅法 boolean sync() default false;}

@CacheEvict:清除缓存,主要应⽤到删除数据的⽅法上。相⽐Cacheable多了两个属性

package org.springframework.cache.annotation;import java.lang.annotation.Documented;import java.lang.annotation.ElementType;import java.lang.annotation.Inherited;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;import java.lang.annotation.Target;

import org.springframework.core.annotation.AliasFor;@Target({ElementType.METHOD, ElementType.TYPE})@Retention(RetentionPolicy.RUNTIME)@Inherited@Documented

public @interface CacheEvict {

// ...相同属性说明请参考@Cacheable中的说明

// 是否要清除所有缓存的数据,为false时调⽤的是Cache.evict(key)⽅法;为true时调⽤的是Cache.clear()⽅法 boolean allEntries() default false; // 调⽤⽅法之前或之后清除缓存

boolean beforeInvocation() default false;}

1. @CachePut:放⼊缓存,主要⽤到对数据有更新的⽅法上。属性说明参考@Cacheable2. @Caching:⽤于在⼀个⽅法上配置多种注解

3. @EnableCaching:启⽤spring cache缓存,作为总的开关,在spring boot的启动类或配置类上需要加上此注解才会⽣效

spring boot + spring cache

spring boot中已经整合了spring cache,并且提供了多种缓存的配置,在使⽤时只需要配置使⽤哪个缓存(enum CacheType)即可。

spring boot中多增加了⼀个可以扩展的东西,就是CacheManagerCustomizer接⼝,可以⾃定义实现这个接⼝,然后对CacheManager做⼀些设置,⽐如:

package com.itopener.demo.cache.redis.config;import java.util.Map;

import java.util.concurrent.ConcurrentHashMap;

import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer;import org.springframework.data.redis.cache.RedisCacheManager;

public class RedisCacheManagerCustomizer implements CacheManagerCustomizer { @Override

public void customize(RedisCacheManager cacheManager) { // 默认过期时间,单位秒

cacheManager.setDefaultExpiration(1000); cacheManager.setUsePrefix(false);

Map expires = new ConcurrentHashMap(); expires.put(\"userIdCache\ cacheManager.setExpires(expires); }}

加载这个bean:

package com.itopener.demo.cache.redis.config;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

/**

* @author fuwei.deng

* @date 2017年12⽉22⽇ 上午10:24:54 * @version 1.0.0 */

@Configuration

public class CacheRedisConfiguration {

@Bean

public RedisCacheManagerCustomizer redisCacheManagerCustomizer() { return new RedisCacheManagerCustomizer(); }}

常⽤的缓存就是Redis了,Redis对于spring cache接⼝的实现是在spring-data-redis包中

这⾥提下我认为的RedisCache实现中的缺陷:

1.在缓存失效的瞬间,如果有线程获取缓存数据,可能出现返回null的情况,原因是RedisCache实现中是如下步骤:1. 判断缓存key是否存在

2. 如果key存在,再获取缓存数据,并返回

因此当判断key存在后缓存失效了,再去获取缓存是没有数据的,就返回null了。

2.RedisCacheManager中是否允许存储空值的属性(cacheNullValues)默认为false,即不允许存储空值,这样会存在缓存穿透的风险。缺陷是这个属性是final类型的,只能在创建对象是通过构造⽅法传⼊,所以要避免缓存穿透就只能⾃⼰在应⽤内声明RedisCacheManager这个bean了

3.RedisCacheManager中的属性⽆法通过配置⽂件直接配置,只能在应⽤内实现CacheManagerCustomizer接⼝来进⾏设置,个⼈认为不太⽅便

Caffeine

Caffeine是⼀个基于Google开源的Guava设计理念的⼀个⾼性能内存缓存,使⽤java8开发,spring boot引⼊Caffeine后已经逐步废弃Guava的整合了。Caffeine源码及介绍地址:caffeinecaffeine提供了多种缓存填充策略、值回收策略,同时也包含了缓存命中次数等统计数据,对缓存的优化能够提供很⼤帮助这⾥简单说下caffeine基于时间的回收策略有以下⼏种:

1. expireAfterAccess:访问后到期,从上次读或写发⽣后的过期时间2. expireAfterWrite:写⼊后到期,从上次写⼊发⽣之后的过期时间3. ⾃定义策略:到期时间由实现Expiry接⼝后单独计算

spring boot + spring cache 实现两级缓存(redis + caffeine)

本⼈开头提到了,就算是使⽤了redis缓存,也会存在⼀定程度的⽹络传输上的消耗,在实际应⽤当中,会存在⼀些变更频率⾮常低的数据,就可以直接缓存在应⽤内部,对于⼀些实时性要求不太⾼的数据,也可以在应⽤内部缓存⼀定时间,减少对redis的访问,提⾼响应速度

由于spring-data-redis框架中redis对spring cache的实现有⼀些不⾜,在使⽤起来可能会出现⼀些问题,所以就不基于原来的实现去扩展了,直接参考实现⽅式,去实现Cache和CacheManager接⼝

还需要注意⼀点,⼀般应⽤都部署了多个节点,⼀级缓存是在应⽤内的缓存,所以当对数据更新和清除时,需要通知所有节点进⾏清理缓存的操作。可以有多种⽅式来实现这种效果,⽐如:zookeeper、MQ等,但是既然⽤了redis缓存,redis本⾝是有⽀持订阅/发布功能的,所以就不依赖其他组件了,直接使⽤redis的通道来通知其他节点进⾏清理缓存的操作以下就是对spring boot + spring cache实现两级缓存(redis + caffeine)的starter封装步骤和源码定义properties配置属性类

package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure;import java.util.HashMap;import java.util.HashSet;import java.util.Map;import java.util.Set;

import org.springframework.boot.context.properties.ConfigurationProperties;/**

* @author fuwei.deng

* @date 2018年1⽉29⽇ 上午11:32:15 * @version 1.0.0 */

@ConfigurationProperties(prefix = \"spring.cache.multi\")public class CacheRedisCaffeineProperties {

private Set cacheNames = new HashSet<>(); /** 是否存储空值,默认true,防⽌缓存穿透*/ private boolean cacheNullValues = true;

/** 是否动态根据cacheName创建Cache的实现,默认true*/ private boolean dynamic = true;

/** 缓存key的前缀*/

private String cachePrefix;

private Redis redis = new Redis();

private Caffeine caffeine = new Caffeine(); public class Redis {

/** 全局过期时间,单位毫秒,默认不过期*/ private long defaultExpiration = 0;

/** 每个cacheName的过期时间,单位毫秒,优先级⽐defaultExpiration⾼*/ private Map expires = new HashMap<>();

/** 缓存更新时通知其他节点的topic名称*/

private String topic = \"cache:redis:caffeine:topic\"; public long getDefaultExpiration() { return defaultExpiration; }

public void setDefaultExpiration(long defaultExpiration) { this.defaultExpiration = defaultExpiration; }

public Map getExpires() { return expires; }

public void setExpires(Map expires) { this.expires = expires; }

public String getTopic() { return topic; }

public void setTopic(String topic) { this.topic = topic; } }

public class Caffeine {

/** 访问后过期时间,单位毫秒*/ private long expireAfterAccess;

/** 写⼊后过期时间,单位毫秒*/ private long expireAfterWrite;

/** 写⼊后刷新时间,单位毫秒*/ private long refreshAfterWrite;

/** 初始化⼤⼩*/

private int initialCapacity;

/** 最⼤缓存对象个数,超过此数量时之前放⼊的缓存将失效*/ private long maximumSize;

/** 由于权重需要缓存对象来提供,对于使⽤spring cache这种场景不是很适合,所以暂不⽀持配置*/// private long maximumWeight;

public long getExpireAfterAccess() { return expireAfterAccess; }

public void setExpireAfterAccess(long expireAfterAccess) { this.expireAfterAccess = expireAfterAccess; }

public long getExpireAfterWrite() { return expireAfterWrite; }

public void setExpireAfterWrite(long expireAfterWrite) { this.expireAfterWrite = expireAfterWrite; }

public long getRefreshAfterWrite() { return refreshAfterWrite; }

public void setRefreshAfterWrite(long refreshAfterWrite) {

this.refreshAfterWrite = refreshAfterWrite; }

public int getInitialCapacity() { return initialCapacity; }

public void setInitialCapacity(int initialCapacity) { this.initialCapacity = initialCapacity; }

public long getMaximumSize() { return maximumSize; }

public void setMaximumSize(long maximumSize) { this.maximumSize = maximumSize; } }

public Set getCacheNames() { return cacheNames; }

public void setCacheNames(Set cacheNames) { this.cacheNames = cacheNames; }

public boolean isCacheNullValues() { return cacheNullValues; }

public void setCacheNullValues(boolean cacheNullValues) { this.cacheNullValues = cacheNullValues; }

public boolean isDynamic() { return dynamic; }

public void setDynamic(boolean dynamic) { this.dynamic = dynamic; }

public String getCachePrefix() { return cachePrefix; }

public void setCachePrefix(String cachePrefix) { this.cachePrefix = cachePrefix; }

public Redis getRedis() { return redis; }

public void setRedis(Redis redis) { this.redis = redis; }

public Caffeine getCaffeine() { return caffeine; }

public void setCaffeine(Caffeine caffeine) { this.caffeine = caffeine; }}

spring cache中有实现Cache接⼝的⼀个抽象类AbstractValueAdaptingCache,包含了空值的包装和缓存值的包装,所以就不⽤实现Cache接⼝了,直接实现AbstractValueAdaptingCache抽象类

package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support;import java.lang.reflect.Constructor;import java.util.Map;import java.util.Set;

import java.util.concurrent.Callable;import java.util.concurrent.TimeUnit;

import java.util.concurrent.locks.ReentrantLock;import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.cache.support.AbstractValueAdaptingCache;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.util.StringUtils;

import com.github.benmanes.caffeine.cache.Cache;

import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.CacheRedisCaffeineProperties;

/**

* @author fuwei.deng

* @date 2018年1⽉26⽇ 下午5:24:11 * @version 1.0.0 */

public class RedisCaffeineCache extends AbstractValueAdaptingCache {

private final Logger logger = LoggerFactory.getLogger(RedisCaffeineCache.class); private String name;

private RedisTemplate redisTemplate; private Cache caffeineCache; private String cachePrefix;

private long defaultExpiration = 0; private Map expires;

private String topic = \"cache:redis:caffeine:topic\";

protected RedisCaffeineCache(boolean allowNullValues) { super(allowNullValues); }

public RedisCaffeineCache(String name, RedisTemplate redisTemplate, Cache caffeineCache, CacheRedisCaffeineProperties cacheRedisCaffeineProperties) { super(cacheRedisCaffeineProperties.isCacheNullValues()); this.name = name;

this.redisTemplate = redisTemplate; this.caffeineCache = caffeineCache;

this.cachePrefix = cacheRedisCaffeineProperties.getCachePrefix();

this.defaultExpiration = cacheRedisCaffeineProperties.getRedis().getDefaultExpiration(); this.expires = cacheRedisCaffeineProperties.getRedis().getExpires(); this.topic = cacheRedisCaffeineProperties.getRedis().getTopic();

}

@Override

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

@Override

public Object getNativeCache() { return this; }

@SuppressWarnings(\"unchecked\") @Override

public T get(Object key, Callable 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(valueLoader.call()); 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) {

if (!super.isAllowNullValues() && value == null) { this.evict(key); return; }

long expire = getExpire(); if(expire > 0) {

redisTemplate.opsForValue().set(getKey(key), toStoreValue(value), expire, TimeUnit.MILLISECONDS); } else {

redisTemplate.opsForValue().set(getKey(key), toStoreValue(value)); }

push(new CacheMessage(this.name, key));

caffeineCache.put(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); if(prevValue == null) { long expire = getExpire(); if(expire > 0) {

redisTemplate.opsForValue().set(getKey(key), toStoreValue(value), expire, TimeUnit.MILLISECONDS); } else {

redisTemplate.opsForValue().set(getKey(key), toStoreValue(value)); }

push(new CacheMessage(this.name, key));

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

return toValueWrapper(prevValue); }

@Override

public void evict(Object key) {

// 先清除redis中缓存数据,然后清除caffeine中的缓存,避免短时间内如果先清除caffeine缓存后其他请求会再从redis⾥加载到caffeine中 redisTemplate.delete(getKey(key));

push(new CacheMessage(this.name, key));

caffeineCache.invalidate(key); }

@Override

public void clear() {

// 先清除redis中缓存数据,然后清除caffeine中的缓存,避免短时间内如果先清除caffeine缓存后其他请求会再从redis⾥加载到caffeine中 Set keys = redisTemplate.keys(this.name.concat(\":\")); for(Object key : keys) { redisTemplate.delete(key); }

push(new CacheMessage(this.name, null));

caffeineCache.invalidateAll(); }

@Override

protected Object lookup(Object key) { Object cacheKey = getKey(key);

Object value = caffeineCache.getIfPresent(key); if(value != null) {

logger.debug(\"get cache from caffeine, the key is : {}\ return value; }

value = redisTemplate.opsForValue().get(cacheKey);

if(value != null) {

logger.debug(\"get cache from redis and put in caffeine, the key is : {}\ caffeineCache.put(key, value); }

return value; }

private Object getKey(Object key) {

return this.name.concat(\":\").concat(StringUtils.isEmpty(cachePrefix) ? key.toString() : cachePrefix.concat(\":\").concat(key.toString())); }

private long getExpire() {

long expire = defaultExpiration;

Long cacheNameExpire = expires.get(this.name);

return cacheNameExpire == null ? expire : cacheNameExpire.longValue(); } /**

* @description 缓存变更时通知其他节点清理本地缓存 * @author fuwei.deng

* @date 2018年1⽉31⽇ 下午3:20:28 * @version 1.0.0 * @param message */

private void push(CacheMessage message) { redisTemplate.convertAndSend(topic, message); } /**

* @description 清理本地缓存 * @author fuwei.deng

* @date 2018年1⽉31⽇ 下午3:15:39 * @version 1.0.0 * @param key */

public void clearLocal(Object key) {

logger.debug(\"clear local cache, the key is : {}\ if(key == null) {

caffeineCache.invalidateAll(); } else {

caffeineCache.invalidate(key); } }}

实现CacheManager接⼝

package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support;import java.util.Collection;import java.util.Set;

import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.ConcurrentMap;import java.util.concurrent.TimeUnit;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.cache.Cache;

import org.springframework.cache.CacheManager;

import org.springframework.data.redis.core.RedisTemplate;

import com.github.benmanes.caffeine.cache.Caffeine;

import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.CacheRedisCaffeineProperties;/**

* @author fuwei.deng

* @date 2018年1⽉26⽇ 下午5:24:52 * @version 1.0.0 */

public class RedisCaffeineCacheManager implements CacheManager {

private final Logger logger = LoggerFactory.getLogger(RedisCaffeineCacheManager.class);

private ConcurrentMap cacheMap = new ConcurrentHashMap();

private CacheRedisCaffeineProperties cacheRedisCaffeineProperties;

private RedisTemplate redisTemplate; private boolean dynamic = true; private Set cacheNames;

public RedisCaffeineCacheManager(CacheRedisCaffeineProperties cacheRedisCaffeineProperties, RedisTemplate redisTemplate) { super();

this.cacheRedisCaffeineProperties = cacheRedisCaffeineProperties; this.redisTemplate = redisTemplate;

this.dynamic = cacheRedisCaffeineProperties.isDynamic();

this.cacheNames = cacheRedisCaffeineProperties.getCacheNames(); }

@Override

public Cache getCache(String name) { Cache cache = cacheMap.get(name); if(cache != null) { return cache; }

if(!dynamic && !cacheNames.contains(name)) { return cache; }

cache = new RedisCaffeineCache(name, redisTemplate, caffeineCache(), cacheRedisCaffeineProperties); Cache oldCache = cacheMap.putIfAbsent(name, cache);

logger.debug(\"create cache instance, the cache name is : {}\ return oldCache == null ? cache : oldCache; }

public com.github.benmanes.caffeine.cache.Cache caffeineCache(){ Caffeine cacheBuilder = Caffeine.newBuilder();

if(cacheRedisCaffeineProperties.getCaffeine().getExpireAfterAccess() > 0) {

cacheBuilder.expireAfterAccess(cacheRedisCaffeineProperties.getCaffeine().getExpireAfterAccess(), TimeUnit.MILLISECONDS); }

if(cacheRedisCaffeineProperties.getCaffeine().getExpireAfterWrite() > 0) {

cacheBuilder.expireAfterWrite(cacheRedisCaffeineProperties.getCaffeine().getExpireAfterWrite(), TimeUnit.MILLISECONDS); }

if(cacheRedisCaffeineProperties.getCaffeine().getInitialCapacity() > 0) {

cacheBuilder.initialCapacity(cacheRedisCaffeineProperties.getCaffeine().getInitialCapacity()); }

if(cacheRedisCaffeineProperties.getCaffeine().getMaximumSize() > 0) {

cacheBuilder.maximumSize(cacheRedisCaffeineProperties.getCaffeine().getMaximumSize()); }

if(cacheRedisCaffeineProperties.getCaffeine().getRefreshAfterWrite() > 0) {

cacheBuilder.refreshAfterWrite(cacheRedisCaffeineProperties.getCaffeine().getRefreshAfterWrite(), TimeUnit.MILLISECONDS); }

return cacheBuilder.build(); }

@Override

public Collection getCacheNames() { return this.cacheNames; }

public void clearLocal(String cacheName, Object key) { Cache cache = cacheMap.get(cacheName); if(cache == null) { return ; }

RedisCaffeineCache redisCaffeineCache = (RedisCaffeineCache) cache; redisCaffeineCache.clearLocal(key); }}

redis消息发布/订阅,传输的消息类

package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support;import java.io.Serializable;

/**

* @author fuwei.deng

* @date 2018年1⽉29⽇ 下午1:31:17 * @version 1.0.0 */

public class CacheMessage implements Serializable {

/** */

private static final long serialVersionUID = 5987219310442078193L; private String cacheName; private Object key;

public CacheMessage(String cacheName, Object key) { super();

this.cacheName = cacheName; this.key = key; }

public String getCacheName() { return cacheName; }

public void setCacheName(String cacheName) { this.cacheName = cacheName; }

public Object getKey() { return key; }

public void setKey(Object key) { this.key = key; }}

监听redis消息需要实现MessageListener接⼝

package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support;import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.data.redis.connection.Message;

import org.springframework.data.redis.connection.MessageListener;import org.springframework.data.redis.core.RedisTemplate;/**

* @author fuwei.deng

* @date 2018年1⽉30⽇ 下午5:22:33 * @version 1.0.0 */

public class CacheMessageListener implements MessageListener {

private final Logger logger = LoggerFactory.getLogger(CacheMessageListener.class); private RedisTemplate redisTemplate;

private RedisCaffeineCacheManager redisCaffeineCacheManager;

public CacheMessageListener(RedisTemplate redisTemplate, RedisCaffeineCacheManager redisCaffeineCacheManager) { super();

this.redisTemplate = redisTemplate;

this.redisCaffeineCacheManager = redisCaffeineCacheManager; }

@Override

public void onMessage(Message message, byte[] pattern) {

CacheMessage cacheMessage = (CacheMessage) redisTemplate.getValueSerializer().deserialize(message.getBody());

logger.debug(\"recevice a redis topic message, clear local cache, the cacheName is {}, the key is {}\ redisCaffeineCacheManager.clearLocal(cacheMessage.getCacheName(), cacheMessage.getKey()); }}

增加spring boot配置类

package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.autoconfigure.AutoConfigureAfter;

import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;

import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;import org.springframework.boot.context.properties.EnableConfigurationProperties;import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.listener.ChannelTopic;

import org.springframework.data.redis.listener.RedisMessageListenerContainer;

import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support.CacheMessageListener;

import com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support.RedisCaffeineCacheManager;/**

* @author fuwei.deng

* @date 2018年1⽉26⽇ 下午5:23:03 * @version 1.0.0 */

@Configuration

@AutoConfigureAfter(RedisAutoConfiguration.class)

@EnableConfigurationProperties(CacheRedisCaffeineProperties.class)public class CacheRedisCaffeineAutoConfiguration {

@Autowired

private CacheRedisCaffeineProperties cacheRedisCaffeineProperties;

@Bean

@ConditionalOnBean(RedisTemplate.class)

public RedisCaffeineCacheManager cacheManager(RedisTemplate redisTemplate) { return new RedisCaffeineCacheManager(cacheRedisCaffeineProperties, redisTemplate); }

@Bean

public RedisMessageListenerContainer redisMessageListenerContainer(RedisTemplate redisTemplate, RedisCaffeineCacheManager redisCaffeineCacheManager) {

RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer(); redisMessageListenerContainer.setConnectionFactory(redisTemplate.getConnectionFactory());

CacheMessageListener cacheMessageListener = new CacheMessageListener(redisTemplate, redisCaffeineCacheManager);

redisMessageListenerContainer.addMessageListener(cacheMessageListener, new ChannelTopic(cacheRedisCaffeineProperties.getRedis().getTopic())); return redisMessageListenerContainer; }}

在resources/META-INF/spring.factories⽂件中增加spring boot配置扫描

# Auto Configure

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\\

com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.CacheRedisCaffeineAutoConfiguration

接下来就可以使⽤maven引⼊使⽤了

com.itopener

cache-redis-caffeine-spring-boot-starter 1.0.0-SNAPSHOT pom

在启动类上增加@EnableCaching注解,在需要缓存的⽅法上增加@Cacheable注解

package com.itopener.demo.cache.redis.caffeine.service;import java.util.Random;import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.cache.annotation.CacheEvict;import org.springframework.cache.annotation.CachePut;import org.springframework.cache.annotation.Cacheable;import org.springframework.stereotype.Service;

import com.itopener.demo.cache.redis.caffeine.vo.UserVO;import com.itopener.utils.TimestampUtil;

@Service

public class CacheRedisCaffeineService {

private final Logger logger = LoggerFactory.getLogger(CacheRedisCaffeineService.class);

@Cacheable(key = \"'cache_user_id_' + #id\ public UserVO get(long id) { logger.info(\"get by id from db\"); UserVO user = new UserVO(); user.setId(id);

user.setName(\"name\" + id);

user.setCreateTime(TimestampUtil.current()); return user; }

@Cacheable(key = \"'cache_user_name_' + #name\ public UserVO get(String name) { logger.info(\"get by name from db\"); UserVO user = new UserVO();

user.setId(new Random().nextLong()); user.setName(name);

user.setCreateTime(TimestampUtil.current()); return user; }

@CachePut(key = \"'cache_user_id_' + #userVO.id\ public UserVO update(UserVO userVO) { logger.info(\"update to db\");

userVO.setCreateTime(TimestampUtil.current()); return userVO; }

@CacheEvict(key = \"'cache_user_id_' + #id\ public void delete(long id) { logger.info(\"delete from db\"); }}

properties⽂件中redis的配置跟使⽤redis是⼀样的,可以增加两级缓存的配置

#两级缓存的配置

spring.cache.multi.caffeine.expireAfterAccess=5000spring.cache.multi.redis.defaultExpiration=60000#spring cache配置

spring.cache.cache-names=userIdCache,userNameCache

#redis配置

#spring.redis.timeout=10000

#spring.redis.password=redispwd#redis pool

#spring.redis.pool.maxIdle=10#spring.redis.pool.minIdle=2

#spring.redis.pool.maxActive=10#spring.redis.pool.maxWait=3000#redis cluster

spring.redis.cluster.nodes=127.0.0.1:7001,127.0.0.1:7002,127.0.0.1:7003,127.0.0.1:7004,127.0.0.1:7005,127.0.0.1:7006spring.redis.cluster.maxRedirects=3

扩展

个⼈认为redisson的封装更⽅便⼀些

1. 对于spring cache缓存的实现没有那么多的缺陷

2. 使⽤redis的HASH结构,可以针对不同的hashKey设置过期时间,清理的时候会更⽅便

3. 如果基于redisson来实现多级缓存,可以继承RedissonCache,在对应⽅法增加⼀级缓存的操作即可4. 如果有使⽤分布式锁的情况就更⽅便了,可以直接使⽤Redisson中封装的分布式锁5. redisson中的发布订阅封装得更好⽤

后续可以增加对于缓存命中率的统计endpoint,这样就可以更好的监控各个缓存的命中情况,以便对缓存配置进⾏优化starter⽬录:springboot / itopener-parent / spring-boot-starters-parent / cache-redis-caffeine-spring-boot-starter-parent⽰例代码⽬录: springboot / itopener-parent / demo-parent / demo-cache-redis-caffeine以上就是本⽂的全部内容,希望对⼤家的学习有所帮助,也希望⼤家多多⽀持。

因篇幅问题不能全部显示,请点此查看更多更全内容

步旅网还为您提供以下相关内容希望对您有帮助:

Redis+Caffeine两级缓存,让访问速度更加流畅

在代码层面,实现两级缓存管理主要涉及以下步骤:使用如 Caffeine 的本地缓存和 Redis 作为远程缓存,整合后通过 Spring Boot 项目搭建环境,并配置 Redis 连接信息。接下来,通过 V1.0 版本实现手动操作缓存逻辑,包括在业务代码中注入和使用缓存,实现数据的读取和存储。V1.0 版本示例中,首先创建 Cac...

Spring Boot多级缓存实现方案

首先,设计一个多级缓存配置类,用于动态配置缓存属性,实现可插拔功能。在自动配置类中启用该配置类,注入相关属性。随后,重写Spring的Cache接口,集成Caffeine本地缓存与Redis分布式缓存,构建多级缓存机制。为监控缓存变动,实现Caffeine键值移除监听与Redis消息监听,确保数据一致性。最后,自动配置类注入相关be...

【Redis】SpringBoot中的cache注解、缓存的两种使用方式

在SpringBoot中,缓存的实现通常通过使用像Redis这样的中间件来提升程序性能。为了利用缓存,可以在分层架构中添加一个专门的缓存层,使得控制器调用时,数据首先在缓存中查找,若有则直接返回,否则调用服务层处理,处理结果再存入缓存,这有助于提高程序的可用性,但需要手动管理key和数据关联,影响维护性。

SpringBoot的Cacheable缓存注解

在此 RedisCacheManager 中,我们使用默认的 RedisCacheConfiguration 进行了一些配置,例如设置缓存的过期时间,同时指定了 Redis 的连接信息。本文介绍了如何在 Spring Boot 应用程序中使用 Redis 进行缓存,包括配置 RedisTemplate、使用 Cacheable 注解、设置缓存有效期、使用 @CacheEvict 清除缓存数据以及使...

SpringBoot整合redis并使用Spring Cache缓存注解

在Spring Boot 2.7中,为了利用Redis作为缓存存储,首先需要在配置文件中启用缓存功能并指定Redis配置。通过在application.properties或.yml中添加相关配置,Spring Boot会自动识别并使用Redis。确保在项目依赖中包含spring-boot-starter-data-redis以支持Redis操作。启用缓存通过@EnableCaching注解,同时配合@Cache...

spring-boot Cache redis 类型转换错误

我们把上面出错的地方单独拿出来 对于这个问题,网上也有人遇到过,但是都说什么是spring-boot devtools工具的问题,感觉挺扯的,我没用这个devtools工具,但是看网上分析的大致可以得出以下结论: 当cache将数据放入缓存时,使用了默认的序列化方式(JdkSerializationRedisSerializer),该序列化使用了二进制...

Java 缓存中间件

Spring Boot Cache 是针对 Spring Cache 的封装,简化了配置步骤,使得缓存集成更加便捷。只需引入依赖、配置缓存类型(如 Redis)并启用缓存功能即可。JetCache 是一个基于 Java 的缓存系统,提供统一 API 和注解,简化缓存使用。JetCache 支持 TTL、两级缓存、分布式自动刷新等功能,适用于复杂缓存场景。有...

SpringBoot整合SpringSeesion实现Redis缓存

首先我们创建一个Spring Boot 2.x的项目,在application.properties配置文件中添加Redis的配置,Spring和Redis的整合可以参考我其他的文章,此处不再详解。我们设置服务端口server.port为8080端口用于启动第一个服务。接下来我们需要在pom文件中添加spring-boot-starter-data-redis和spring-session-data-redis这两...

本地缓存解决方案-Caffeine Cache

Caffeine Cache基于LRU算法实现,支持多种缓存过期策略。在Spring Boot 2.0中,Caffeine Cache将取代Guava成为默认缓存方案。使用Caffeine Cache在Spring Boot 2.0项目中集成Caffeine Cache,可以使用以下步骤:准备工作搭建Spring Boot 2.x项目,集成MyBatis与MySQL数据库。java工程添加依赖在项目的pom.xml文件...

SpringBoot整合Redis做缓存-自定义缓存序列化方式,防止缓存数据乱码问 ...

SpringBoot整合Redis做缓存-自定义缓存序列化方式,防止缓存数据乱码问题首先是添加依赖:&lt;dependency&gt;&lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;&lt;artifactId&gt;spring-boot-starter-d

Copyright © 2019- 版权所有