Spring Boot緩存實戰 Redis + Caffeine 實現多級緩存

在前文咱們介紹瞭如何使用Redis或者Caffeine來作緩存。java

問題描述:

經過使用redis和Caffeine來作緩存,咱們會發現一些問題。git

  • 若是隻使用redis來作緩存咱們會有大量的請求到redis,可是每次請求的數據都是同樣的,假如這一部分數據就放在應用服務器本地,那麼就省去了請求redis的網絡開銷,請求速度就會快不少。可是使用redis橫向擴展很方便。
  • 若是隻使用Caffeine來作本地緩存,咱們的應用服務器的內存是有限,而且單獨爲了緩存去擴展應用服務器是很是不划算。因此,只使用本地緩存也是有很大侷限性的。

至此咱們是否是有一個想法了,兩個一塊兒用。將熱點數據放本地緩存(一級緩存),將非熱點數據放redis緩存(二級緩存)。github

緩存的選擇

  • 一級緩存:Caffeine是一個一個高性能的 Java 緩存庫;使用 Window TinyLfu 回收策略,提供了一個近乎最佳的命中率。Caffeine 緩存詳解
  • 二級緩存:redis是一高性能、高可用的key-value數據庫,支持多種數據類型,支持集羣,和應用服務器分開部署易於橫向擴展。

解決思路

Spring 原本就提供了Cache的支持,最核心的就是實現Cache和CacheManager接口。redis

Cache接口

主要是實現對緩存的操做,如增刪查等。spring

public interface Cache {
	String getName();

	Object getNativeCache();

	<T> T get(Object key, Class<T> type);

	<T> T get(Object key, Callable<T> valueLoader);

	void put(Object key, Object value);

	ValueWrapper putIfAbsent(Object key, Object value);

	void evict(Object key);

	void clear();

	...
}

CacheManager接口

根據緩存名稱來管理Cache,核心方法就是經過緩存名稱獲取Cache。數據庫

public interface CacheManager {

	Cache getCache(String name);

	Collection<String> getCacheNames();

}

經過上面的兩個接口個人大體思路是,寫一個LayeringCache來實現Cache接口,LayeringCache類中集成對Caffeine和redis的操做。 寫一個LayeringCacheManager來管理LayeringCache就好了。c#

這裏的redis緩存使用的是我擴展後的RedisCache詳情請看:緩存

LayeringCache

LayeringCache類,由於須要集成對Caffeine和Redis的操做,因此至少須要有name(緩存名稱)、CaffeineCache和CustomizedRedisCache三個屬性,還增長了一個是否使用一級緩存的開關usedFirstCache。在LayeringCache類的方法裏面分別去調用操做一級緩存的和操做二級緩存的方法就能夠了。服務器

在這裏特別說明一下:網絡

  • 在查詢方法如get等,先去查詢一級緩存,若是沒查到再去查二級緩存。
  • put方法沒有順序要求,可是建議將一級緩存的操做放在前面。
  • 若是是刪除方法如evict和clear等,須要先刪掉二級緩存的數據,再去刪掉一級緩存的數據,不然有併發問題。
  • 刪除一級緩存須要用到redis的Pub/Sub(訂閱發佈)模式,不然集羣中其餘服服務器節點的一級緩存數據沒法刪除。
  • redis的Pub/Sub(訂閱發佈)模式發送消息是無狀態的,若是遇到網絡等緣由有可能致使一些應用服務器上的一級緩存沒辦法刪除,若是對L1和L2數據同步要求較高的話,這裏可使用MQ來作。

完整代碼:

/**
 * @author yuhao.wang
 */
public class LayeringCache extends AbstractValueAdaptingCache {
    Logger logger = LoggerFactory.getLogger(LayeringCache.class);

    /**
     * 緩存的名稱
     */
    private final String name;

    /**
     * 是否使用一級緩存
     */
    private boolean usedFirstCache = true;

    /**
     * redi緩存
     */
    private final CustomizedRedisCache redisCache;

    /**
     * Caffeine緩存
     */
    private final CaffeineCache caffeineCache;

    RedisOperations<? extends Object, ? extends Object> redisOperations;

    /**
     * @param name              緩存名稱
     * @param prefix            緩存前綴
     * @param redisOperations   操做Redis的RedisTemplate
     * @param expiration        redis緩存過時時間
     * @param preloadSecondTime redis緩存自動刷新時間
     * @param allowNullValues   是否容許存NULL,默認是false
     * @param usedFirstCache    是否使用一級緩存,默認是true
     * @param forceRefresh      是否強制刷新(走數據庫),默認是false
     * @param caffeineCache     Caffeine緩存
     */
    public LayeringCache(String name, byte[] prefix, RedisOperations<? extends Object, ? extends Object> redisOperations,
                         long expiration, long preloadSecondTime, boolean allowNullValues, boolean usedFirstCache,
                         boolean forceRefresh, com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeineCache) {

        super(allowNullValues);
        this.name = name;
        this.usedFirstCache = usedFirstCache;
        this.redisOperations = redisOperations;
        this.redisCache = new CustomizedRedisCache(name, prefix, redisOperations, expiration, preloadSecondTime, forceRefresh, allowNullValues);
        this.caffeineCache = new CaffeineCache(name, caffeineCache, allowNullValues);
    }

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

    @Override
    public Object getNativeCache() {
        return this;
    }

    public CustomizedRedisCache getSecondaryCache() {
        return this.redisCache;
    }

    public CaffeineCache getFirstCache() {
        return this.caffeineCache;
    }

    @Override
    public ValueWrapper get(Object key) {
        ValueWrapper wrapper = null;
        if (usedFirstCache) {
            // 查詢一級緩存
            wrapper = caffeineCache.get(key);
            logger.debug("查詢一級緩存 key:{},返回值是:{}", key, JSON.toJSONString(wrapper));
        }

        if (wrapper == null) {
            // 查詢二級緩存
            wrapper = redisCache.get(key);
            logger.debug("查詢二級緩存 key:{},返回值是:{}", key, JSON.toJSONString(wrapper));
        }
        return wrapper;
    }

    @Override
    public <T> T get(Object key, Class<T> type) {
        T value = null;
        if (usedFirstCache) {
            // 查詢一級緩存
            value = caffeineCache.get(key, type);
            logger.debug("查詢一級緩存 key:{},返回值是:{}", key, JSON.toJSONString(value));
        }

        if (value == null) {
            // 查詢二級緩存
            value = redisCache.get(key, type);
            caffeineCache.put(key, value);
            logger.debug("查詢二級緩存 key:{},返回值是:{}", key, JSON.toJSONString(value));
        }
        return value;
    }

    @Override
    public <T> T get(Object key, Callable<T> valueLoader) {
        T value = null;
        if (usedFirstCache) {
            // 查詢一級緩存,若是一級緩存沒有值則調用getForSecondaryCache(k, valueLoader)查詢二級緩存
            value = (T) caffeineCache.getNativeCache().get(key, k -> getForSecondaryCache(k, valueLoader));
        } else {
            // 直接查詢二級緩存
            value = (T) getForSecondaryCache(key, valueLoader);
        }

        if (value instanceof NullValue) {
            return null;
        }
        return value;
    }

    @Override
    public void put(Object key, Object value) {
        if (usedFirstCache) {
            caffeineCache.put(key, value);
        }
        redisCache.put(key, value);
    }

    @Override
    public ValueWrapper putIfAbsent(Object key, Object value) {
        if (usedFirstCache) {
            caffeineCache.putIfAbsent(key, value);
        }
        return redisCache.putIfAbsent(key, value);
    }

    @Override
    public void evict(Object key) {
        // 刪除的時候要先刪除二級緩存再刪除一級緩存,不然有併發問題
        redisCache.evict(key);
        if (usedFirstCache) {
            // 刪除一級緩存須要用到redis的訂閱/發佈模式,不然集羣中其餘服服務器節點的一級緩存數據沒法刪除
            Map<String, Object> message = new HashMap<>();
            message.put("cacheName", name);
            message.put("key", key);
            // 建立redis發佈者
            RedisPublisher redisPublisher = new RedisPublisher(redisOperations, ChannelTopicEnum.REDIS_CACHE_DELETE_TOPIC.getChannelTopic());
            // 發佈消息
            redisPublisher.publisher(message);
        }
    }

    @Override
    public void clear() {
        redisCache.clear();
        if (usedFirstCache) {
            // 清除一級緩存須要用到redis的訂閱/發佈模式,不然集羣中其餘服服務器節點的一級緩存數據沒法刪除
            Map<String, Object> message = new HashMap<>();
            message.put("cacheName", name);
            // 建立redis發佈者
            RedisPublisher redisPublisher = new RedisPublisher(redisOperations, ChannelTopicEnum.REDIS_CACHE_CLEAR_TOPIC.getChannelTopic());
            // 發佈消息
            redisPublisher.publisher(message);
        }
    }

    @Override
    protected Object lookup(Object key) {
        Object value = null;
        if (usedFirstCache) {
            value = caffeineCache.get(key);
            logger.debug("查詢一級緩存 key:{},返回值是:{}", key, JSON.toJSONString(value));
        }
        if (value == null) {
            value = redisCache.get(key);
            logger.debug("查詢二級緩存 key:{},返回值是:{}", key, JSON.toJSONString(value));
        }
        return value;
    }

    /**
     * 查詢二級緩存
     *
     * @param key
     * @param valueLoader
     * @return
     */
    private <T> Object getForSecondaryCache(Object key, Callable<T> valueLoader) {
        T value = redisCache.get(key, valueLoader);
        logger.debug("查詢二級緩存 key:{},返回值是:{}", key, JSON.toJSONString(value));
        return toStoreValue(value);
    }
}

LayeringCacheManager

由於咱們須要在CacheManager中來管理緩存,因此咱們須要在CacheManager定義一個容器來存儲緩存。在這裏咱們新建一個ConcurrentMap<String, Cache> cacheMap來存在緩存,CacheManager的兩個方法getCache和getCacheNames都經過操做這個cacheMap來實現。Map<String, FirstCacheSetting> firstCacheSettings和Map<String, SecondaryCacheSetting> secondaryCacheSettings屬性是針對每個緩存的特殊配置,如一級緩存的過時時間配置,二級緩存的過時時間和自動刷新時間配置。剩下的屬性就不一一介紹了,可直接看下面的源碼。

getCache 方法

這能夠說是CacheManager最核心的方法,全部CacheManager操做都圍繞這個方法進行。

@Override
public Cache getCache(String name) {
	Cache cache = this.cacheMap.get(name);
	if (cache == null && this.dynamic) {
		synchronized (this.cacheMap) {
			cache = this.cacheMap.get(name);
			if (cache == null) {
				cache = createCache(name);
				this.cacheMap.put(name, cache);
			}
		}
	}
	return cache;
}

從這段邏輯咱們能夠看出這個方法就是根據名稱獲取緩存,若是沒有找到而且動態建立緩存的開關dynamic爲true的話,就調用createCache方法動態的建立緩存。

createCache 方法

去建立一個LayeringCache

protected Cache createCache(String name) {

	return new LayeringCache(name, (usePrefix ? cachePrefix.prefix(name) : null), redisOperations,
			getSecondaryCacheExpirationSecondTime(name), getSecondaryCachePreloadSecondTime(name),
			isAllowNullValues(), getUsedFirstCache(name), getForceRefresh(name), createNativeCaffeineCache(name));
}

在建立緩存的時候咱們會調用getSecondaryCacheExpirationSecondTime、getSecondaryCachePreloadSecondTime和getForceRefresh等方法去獲取二級緩存的過時時間、自動刷新時間和是否強制刷新(走數據庫)等值,這些都在secondaryCacheSettings屬性中獲取;調用createNativeCaffeineCache方法去建立一個一級緩存Caffeine的實例。createNativeCaffeineCache在這個方法裏面會調用getCaffeine方法動態的去讀取一級緩存的配置,並根據配置建立一級緩存,若是沒有找到特殊配置,就使用默認配置,而這裏的特殊配置則在firstCacheSettings屬性中獲取。

getCaffeine

動態的獲取一級緩存配置,並建立對應Caffeine對象。

private Caffeine<Object, Object> getCaffeine(String name) {
	if (!CollectionUtils.isEmpty(firstCacheSettings)) {
		FirstCacheSetting firstCacheSetting = firstCacheSettings.get(name);
		if (firstCacheSetting != null && StringUtils.isNotBlank(firstCacheSetting.getCacheSpecification())) {
			// 根據緩存名稱獲取一級緩存配置
			return Caffeine.from(CaffeineSpec.parse(firstCacheSetting.getCacheSpecification()));
		}
	}

	return this.cacheBuilder;
}private Caffeine<Object, Object> getCaffeine(String name) {
	if (!CollectionUtils.isEmpty(firstCacheSettings)) {
		FirstCacheSetting firstCacheSetting = firstCacheSettings.get(name);
		if (firstCacheSetting != null && StringUtils.isNotBlank(firstCacheSetting.getCacheSpecification())) {
			// 根據緩存名稱獲取一級緩存配置
			return Caffeine.from(CaffeineSpec.parse(firstCacheSetting.getCacheSpecification()));
		}
	}

	return this.cacheBuilder;
}

setFirstCacheSettings和setSecondaryCacheSettings

咱們借用了RedisCacheManager的setExpires(Map<String, Long> expires)方法的思想。用setFirstCacheSettings和setSecondaryCacheSettings方法對一級緩存和二級緩存的特殊配置進行設值。

/**
 * 根據緩存名稱設置一級緩存的有效時間和刷新時間,單位秒
 *
 * @param firstCacheSettings
 */
public void setFirstCacheSettings(Map<String, FirstCacheSetting> firstCacheSettings) {
	this.firstCacheSettings = (!CollectionUtils.isEmpty(firstCacheSettings) ? new ConcurrentHashMap<>(firstCacheSettings) : null);
}

/**
 * 根據緩存名稱設置二級緩存的有效時間和刷新時間,單位秒
 *
 * @param secondaryCacheSettings
 */
public void setSecondaryCacheSettings(Map<String, SecondaryCacheSetting> secondaryCacheSettings) {
	this.secondaryCacheSettings = (!CollectionUtils.isEmpty(secondaryCacheSettings) ? new ConcurrentHashMap<>(secondaryCacheSettings) : null);
}

完整代碼:

/**
 * @author yuhao.wang
 */
@SuppressWarnings("rawtypes")
public class LayeringCacheManager implements CacheManager {
    // 常量
    static final int DEFAULT_EXPIRE_AFTER_WRITE = 60;
    static final int DEFAULT_INITIAL_CAPACITY = 5;
    static final int DEFAULT_MAXIMUM_SIZE = 1_000;

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


    /**
     * 一級緩存配置
     */
    private Map<String, FirstCacheSetting> firstCacheSettings = null;

    /**
     * 二級緩存配置
     */
    private Map<String, SecondaryCacheSetting> secondaryCacheSettings = null;

    /**
     * 是否容許動態建立緩存,默認是true
     */
    private boolean dynamic = true;

    /**
     * 緩存值是否容許爲NULL
     */
    private boolean allowNullValues = false;

    // Caffeine 屬性
    /**
     * expireAfterWrite:60
     * initialCapacity:5
     * maximumSize: 1_000
     */
    private Caffeine<Object, Object> cacheBuilder = Caffeine.newBuilder()
            .expireAfterWrite(DEFAULT_EXPIRE_AFTER_WRITE, TimeUnit.SECONDS)
            .initialCapacity(DEFAULT_INITIAL_CAPACITY)
            .maximumSize(DEFAULT_MAXIMUM_SIZE);

    // redis 屬性
    /**
     * 操做redis的RedisTemplate
     */
    private final RedisOperations redisOperations;

    /**
     * 二級緩存使用使用前綴,默認是false,建議設置成true
     */
    private boolean usePrefix = false;
    private RedisCachePrefix cachePrefix = new DefaultRedisCachePrefix();

    /**
     * redis緩存默認時間,默認是0 永不過時
     */
    private long defaultExpiration = 0;

    public LayeringCacheManager(RedisOperations redisOperations) {
        this(redisOperations, Collections.<String>emptyList());
    }

    public LayeringCacheManager(RedisOperations redisOperations, Collection<String> cacheNames) {
        this(redisOperations, cacheNames, false);
    }

    public LayeringCacheManager(RedisOperations redisOperations, Collection<String> cacheNames, boolean allowNullValues) {
        this.allowNullValues = allowNullValues;
        this.redisOperations = redisOperations;

        setCacheNames(cacheNames);
    }

    @Override
    public Cache getCache(String name) {
        Cache cache = this.cacheMap.get(name);
        if (cache == null && this.dynamic) {
            synchronized (this.cacheMap) {
                cache = this.cacheMap.get(name);
                if (cache == null) {
                    cache = createCache(name);
                    this.cacheMap.put(name, cache);
                }
            }
        }
        return cache;
    }

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

    @SuppressWarnings("unchecked")
    protected Cache createCache(String name) {
        return new LayeringCache(name, (usePrefix ? cachePrefix.prefix(name) : null), redisOperations,
                getSecondaryCacheExpirationSecondTime(name), getSecondaryCachePreloadSecondTime(name),
                isAllowNullValues(), getUsedFirstCache(name), getForceRefresh(name), createNativeCaffeineCache(name));
    }

    /**
     * Create a native Caffeine Cache instance for the specified cache name.
     *
     * @param name the name of the cache
     * @return the native Caffeine Cache instance
     */
    protected com.github.benmanes.caffeine.cache.Cache<Object, Object> createNativeCaffeineCache(String name) {
        return getCaffeine(name).build();
    }

    /**
     * 使用該CacheManager的當前狀態從新建立已知的緩存。
     */
    private void refreshKnownCaches() {
        for (Map.Entry<String, Cache> entry : this.cacheMap.entrySet()) {
            entry.setValue(createCache(entry.getKey()));
        }
    }

    /**
     * 在初始化CacheManager的時候初始化一組緩存。
     * 使用這個方法會在CacheManager初始化的時候就會將一組緩存初始化好,而且在運行時不會再去建立更多的緩存。
     * 使用空的Collection或者從新在配置裏面指定dynamic後,就可從新在運行時動態的來建立緩存。
     *
     * @param cacheNames
     */
    public void setCacheNames(Collection<String> cacheNames) {
        if (cacheNames != null) {
            for (String name : cacheNames) {
                this.cacheMap.put(name, createCache(name));
            }
            this.dynamic = cacheNames.isEmpty();
        }
    }

    /**
     * 設置是否容許Cache的值爲null
     *
     * @param allowNullValues
     */
    public void setAllowNullValues(boolean allowNullValues) {
        if (this.allowNullValues != allowNullValues) {
            this.allowNullValues = allowNullValues;
            refreshKnownCaches();
        }
    }

    /**
     * 獲取是否容許Cache的值爲null
     *
     * @return
     */
    public boolean isAllowNullValues() {
        return this.allowNullValues;
    }

    /**
     * 在生成key的時候是不是否使用緩存名稱來做爲緩存前綴。默認是false,可是建議設置成true。
     *
     * @param usePrefix
     */
    public void setUsePrefix(boolean usePrefix) {
        this.usePrefix = usePrefix;
    }

    protected boolean isUsePrefix() {
        return usePrefix;
    }

    /**
     * 設置redis默認的過時時間(單位:秒)
     *
     * @param defaultExpireTime
     */
    public void setSecondaryCacheDefaultExpiration(long defaultExpireTime) {
        this.defaultExpiration = defaultExpireTime;
    }


    /**
     * 根據緩存名稱設置一級緩存的有效時間和刷新時間,單位秒
     *
     * @param firstCacheSettings
     */
    public void setFirstCacheSettings(Map<String, FirstCacheSetting> firstCacheSettings) {
        this.firstCacheSettings = (!CollectionUtils.isEmpty(firstCacheSettings) ? new ConcurrentHashMap<>(firstCacheSettings) : null);
    }

    /**
     * 根據緩存名稱設置二級緩存的有效時間和刷新時間,單位秒
     *
     * @param secondaryCacheSettings
     */
    public void setSecondaryCacheSettings(Map<String, SecondaryCacheSetting> secondaryCacheSettings) {
        this.secondaryCacheSettings = (!CollectionUtils.isEmpty(secondaryCacheSettings) ? new ConcurrentHashMap<>(secondaryCacheSettings) : null);
    }


    /**
     * 獲取過時時間
     *
     * @return
     */
    public long getSecondaryCacheExpirationSecondTime(String name) {
        if (StringUtils.isEmpty(name)) {
            return 0;
        }

        SecondaryCacheSetting secondaryCacheSetting = null;
        if (!CollectionUtils.isEmpty(secondaryCacheSettings)) {
            secondaryCacheSetting = secondaryCacheSettings.get(name);
        }
        Long expiration = secondaryCacheSetting != null ? secondaryCacheSetting.getExpirationSecondTime() : defaultExpiration;
        return expiration < 0 ? 0 : expiration;
    }

    /**
     * 獲取自動刷新時間
     *
     * @return
     */
    private long getSecondaryCachePreloadSecondTime(String name) {
        // 自動刷新時間,默認是0
        SecondaryCacheSetting secondaryCacheSetting = null;
        if (!CollectionUtils.isEmpty(secondaryCacheSettings)) {
            secondaryCacheSetting = secondaryCacheSettings.get(name);
        }
        Long preloadSecondTime = secondaryCacheSetting != null ? secondaryCacheSetting.getPreloadSecondTime() : 0;
        return preloadSecondTime < 0 ? 0 : preloadSecondTime;
    }

    /**
     * 獲取是否使用二級緩存,默認是true
     */
    public boolean getUsedFirstCache(String name) {
        SecondaryCacheSetting secondaryCacheSetting = null;
        if (!CollectionUtils.isEmpty(secondaryCacheSettings)) {
            secondaryCacheSetting = secondaryCacheSettings.get(name);
        }

        return secondaryCacheSetting != null ? secondaryCacheSetting.getUsedFirstCache() : true;
    }

    /**
     * 獲取是否強制刷新(走數據庫),默認是false
     */
    public boolean getForceRefresh(String name) {
        SecondaryCacheSetting secondaryCacheSetting = null;
        if (!CollectionUtils.isEmpty(secondaryCacheSettings)) {
            secondaryCacheSetting = secondaryCacheSettings.get(name);
        }

        return secondaryCacheSetting != null ? secondaryCacheSetting.getForceRefresh() : false;
    }

    public void setCaffeineSpec(CaffeineSpec caffeineSpec) {
        Caffeine<Object, Object> cacheBuilder = Caffeine.from(caffeineSpec);
        if (!ObjectUtils.nullSafeEquals(this.cacheBuilder, cacheBuilder)) {
            this.cacheBuilder = cacheBuilder;
            refreshKnownCaches();
        }
    }

    private Caffeine<Object, Object> getCaffeine(String name) {
        if (!CollectionUtils.isEmpty(firstCacheSettings)) {
            FirstCacheSetting firstCacheSetting = firstCacheSettings.get(name);
            if (firstCacheSetting != null && StringUtils.isNotBlank(firstCacheSetting.getCacheSpecification())) {
                // 根據緩存名稱獲取一級緩存配置
                return Caffeine.from(CaffeineSpec.parse(firstCacheSetting.getCacheSpecification()));
            }
        }

        return this.cacheBuilder;
    }
}

FirstCacheSettings:

一級緩存配置類

public class FirstCacheSetting {

    /**
     * 一級緩存配置,配置項請點擊這裏 {@link CaffeineSpec#configure(String, String)}
     * @param cacheSpecification
     */
    public FirstCacheSetting(String cacheSpecification) {
        this.cacheSpecification = cacheSpecification;
    }

    private String cacheSpecification;

    public String getCacheSpecification() {
        return cacheSpecification;
    }
}

SecondaryCacheSetting:

二級緩存的特殊配置類

/**
 * @author yuhao.wang
 */
public class SecondaryCacheSetting {

    /**
     * @param expirationSecondTime 設置redis緩存的有效時間,單位秒
     * @param preloadSecondTime    設置redis緩存的自動刷新時間,單位秒
     */
    public SecondaryCacheSetting(long expirationSecondTime, long preloadSecondTime) {
        this.expirationSecondTime = expirationSecondTime;
        this.preloadSecondTime = preloadSecondTime;
    }

    /**
     * @param usedFirstCache       是否啓用一級緩存,默認true
     * @param expirationSecondTime 設置redis緩存的有效時間,單位秒
     * @param preloadSecondTime    設置redis緩存的自動刷新時間,單位秒
     */
    public SecondaryCacheSetting(boolean usedFirstCache, long expirationSecondTime, long preloadSecondTime) {
        this.expirationSecondTime = expirationSecondTime;
        this.preloadSecondTime = preloadSecondTime;
        this.usedFirstCache = usedFirstCache;
    }

    /**
     * @param expirationSecondTime 設置redis緩存的有效時間,單位秒
     * @param preloadSecondTime    設置redis緩存的自動刷新時間,單位秒
     * @param forceRefresh         是否使用強制刷新(走數據庫),默認false
     */
    public SecondaryCacheSetting(long expirationSecondTime, long preloadSecondTime, boolean forceRefresh) {
        this.expirationSecondTime = expirationSecondTime;
        this.preloadSecondTime = preloadSecondTime;
        this.forceRefresh = forceRefresh;
    }

    /**
     * @param expirationSecondTime 設置redis緩存的有效時間,單位秒
     * @param preloadSecondTime    設置redis緩存的自動刷新時間,單位秒
     * @param usedFirstCache       是否啓用一級緩存,默認true
     * @param forceRefresh         是否使用強制刷新(走數據庫),默認false
     */
    public SecondaryCacheSetting(long expirationSecondTime, long preloadSecondTime, boolean usedFirstCache, boolean forceRefresh) {
        this.expirationSecondTime = expirationSecondTime;
        this.preloadSecondTime = preloadSecondTime;
        this.usedFirstCache = usedFirstCache;
        this.forceRefresh = forceRefresh;
    }

    /**
     * 緩存有效時間
     */
    private long expirationSecondTime;

    /**
     * 緩存主動在失效前強制刷新緩存的時間
     * 單位:秒
     */
    private long preloadSecondTime = 0;

    /**
     * 是否使用二級緩存,默認是true
     */
    private boolean usedFirstCache = true;

    /**
     * 是否使用強刷新(走數據庫),默認是false
     */
    private boolean forceRefresh = false;

    public long getPreloadSecondTime() {
        return preloadSecondTime;
    }

    public long getExpirationSecondTime() {
        return expirationSecondTime;
    }

    public boolean getUsedFirstCache() {
        return usedFirstCache;
    }

    public boolean getForceRefresh() {
        return forceRefresh;
    }
}

使用方式

在上面咱們定義好了LayeringCacheManager和LayeringCache接下來就是使用了。

新建一個配置類CacheConfig,在這裏指定一個LayeringCacheManager的Bean。我那的緩存就生效了。完整代碼以下:

/**
 * @author yuhao.wang
 */
@Configuration
@EnableConfigurationProperties(CacheProperties.class)
public class CacheConfig {

    // redis緩存的有效時間單位是秒
    @Value("${redis.default.expiration:3600}")
    private long redisDefaultExpiration;

    // 查詢緩存有效時間
    @Value("${select.cache.timeout:1800}")
    private long selectCacheTimeout;
    // 查詢緩存自動刷新時間
    @Value("${select.cache.refresh:1790}")
    private long selectCacheRefresh;

    @Autowired
    private CacheProperties cacheProperties;

    @Bean
    @Primary
    public CacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) {
        LayeringCacheManager layeringCacheManager = new LayeringCacheManager(redisTemplate);
        // Caffeine緩存設置
        setFirstCacheConfig(layeringCacheManager);

        // redis緩存設置
        setSecondaryCacheConfig(layeringCacheManager);
        return layeringCacheManager;
    }

    private void setFirstCacheConfig(LayeringCacheManager layeringCacheManager) {
        // 設置默認的一級緩存配置
        String specification = this.cacheProperties.getCaffeine().getSpec();
        if (StringUtils.hasText(specification)) {
            layeringCacheManager.setCaffeineSpec(CaffeineSpec.parse(specification));
        }

        // 設置每一個一級緩存的過時時間和自動刷新時間
        Map<String, FirstCacheSetting> firstCacheSettings = new HashMap<>();
        firstCacheSettings.put("people", new FirstCacheSetting("initialCapacity=5,maximumSize=500,expireAfterWrite=10s"));
        firstCacheSettings.put("people1", new FirstCacheSetting("initialCapacity=5,maximumSize=50,expireAfterAccess=10s"));
        layeringCacheManager.setFirstCacheSettings(firstCacheSettings);
    }

    private void setSecondaryCacheConfig(LayeringCacheManager layeringCacheManager) {
        // 設置使用緩存名稱(value屬性)做爲redis緩存前綴
        layeringCacheManager.setUsePrefix(true);
        //這裏能夠設置一個默認的過時時間 單位是秒
        layeringCacheManager.setSecondaryCacheDefaultExpiration(redisDefaultExpiration);

        // 設置每一個二級緩存的過時時間和自動刷新時間
        Map<String, SecondaryCacheSetting> secondaryCacheSettings = new HashMap<>();
        secondaryCacheSettings.put("people", new SecondaryCacheSetting(selectCacheTimeout, selectCacheRefresh));
        secondaryCacheSettings.put("people1", new SecondaryCacheSetting(selectCacheTimeout, selectCacheRefresh, true));
        secondaryCacheSettings.put("people2", new SecondaryCacheSetting(false, selectCacheTimeout, selectCacheRefresh));
        secondaryCacheSettings.put("people3", new SecondaryCacheSetting(selectCacheTimeout, selectCacheRefresh, false, true));
        layeringCacheManager.setSecondaryCacheSettings(secondaryCacheSettings);
    }

    /**
     * 顯示聲明緩存key生成器
     *
     * @return
     */
    @Bean
    public KeyGenerator keyGenerator() {

        return new SimpleKeyGenerator();
    }

}

在cacheManager中指定Bean的時候,咱們經過調用LayeringCacheManager 的setFirstCacheSettings和setSecondaryCacheSettings方法爲緩存設置一級緩存和二級緩存的特殊配置。

剩下的就是在Service方法上加註解了,如:

@Override
@Cacheable(value = "people1", key = "#person.id", sync = true)//3
public Person findOne1(Person person, String a, String[] b, List<Long> c) {
	Person p = personRepository.findOne(person.getId());
	logger.info("爲id、key爲:" + p.getId() + "數據作了緩存");
	return p;
}

@Cacheable的sync屬性建議設置成true。

測試

最後經過jmeter測試,50個線程,使用多級緩存,比只使用redis級緩存性能提高2倍多,只是用redis吞吐量在1243左右,使用多級緩存後在2639左右。

源碼地址: https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases

spring-boot-student-cache-redis-caffeine 工程

爲監控而生的多級緩存框架 layering-cache這是我開源的一個多級緩存框架的實現,若是有興趣能夠看一下

相關文章
相關標籤/搜索