Spring boot integrates spring cache and has multiple cache implementations, such as Redis, Caffeine, JCache, EhCache, etc. But if you only use one cache, either it will have a large network consumption (such as Redis), or it will have too much memory usage (such as Caffeine's application memory cache). In many scenarios, the first and second level caches can be combined to achieve large-scale improvement in the processing efficiency of the application.
Content description:
To a simple understanding, cache is to read data from slower reading media and put it on medium with faster reading, such as disk-->memory. Usually we store data on disk, such as: database. If you read it from the database every time, the disk itself will affect the reading speed, so there will be a memory cache like redis. You can read the data out and put it in memory, so that when you need to obtain the data, you can directly get the data from the memory and return it, which can greatly improve the speed. However, generally redis is deployed separately into a cluster, so there will be consumption on network IO. Although there are connection pooling tools for linking to the redis cluster, there will still be some consumption in data transmission. So there is an in-app cache, such as: caffeine. When there is data that meets the criteria in the application cache, it can be used directly without having to obtain it through the network to redis, thus forming a two-level cache. In-app cache is called first-level cache, and remote cache (such as redis) is called second-level cache
spring cache
When using cache, the following process is generally the following:
It can be seen from the flow chart that in order to use cache, a lot of cache operations have been added based on the original business processing. If these are coupled to the business code, there will be a lot of repetitive work when developing, and it is not conducive to understanding the business based on the code.
Spring cache is a cache component provided in the spring-context package based on annotation. It defines some standard interfaces. By implementing these interfaces, cache can be achieved by adding annotations to the method. This will avoid the problem of the cache code being coupled with business processing. The implementation of spring cache is an extension of method interface (MethodInterceptor) encapsulation in spring aop. Of course, spring aop is also implemented based on Aspect.
There are two core interfaces of spring cache: Cache and CacheManager
Cache interface
Provide specific cache operations, such as putting in, reading, and cleaning caches. The implementations provided by the spring framework are:
Except for RedisCache, which is in the spring-data-redis package, the others are basically in the spring-context-support package.
#Cache.javapackage org.springframework.cache;import java.util.concurrent.Callable;public interface Cache { // cacheName, the name of the cache. In the default implementation, CacheManager passes cacheName when creating the Cache bean. String getName(); // Get the actual cache, such as: RedisTemplate, com.github.benmanes.caffeine.cache.Cache<Object, Object>. I haven't found the actual use yet. I may just provide beans that get native cache so that some cache operations or statistics need to be extended. Object getNativeCache(); // Get the cache value through the key, note that the value returned is ValueWrapper. In order to be compatible with null values, the return value is wrapped in a layer, and the actual value is obtained through the get method ValueWrapper get(Object key); // Get the cache value through the key, which returns the actual value, that is, the return value type of the method <T> T get(Object key, Class<T> type); // Get the cache value through the key, you can use valueLoader.call() to call the method using @Cacheable annotation. Use this method when the sync attribute of the @Cacheable annotation is configured to true. Therefore, the synchronization of the return source to the database needs to be ensured within the method. Avoid large amount of requests to return to the source to the database when the cache fails. <T> T get(Object key, Callable<T> valueLoader); // Put the data returned by the @Cacheable annotation method into the cache void put(Object key, Object value); // Put the cache only when there is no key in the cache. The return value is the original data when the key exists ValueWrapper putIfAbsent(Object key, Object value); // Delete cache void evict(Object key); // Delete all data in the cache. It should be noted that in the specific implementation, only all data cached using @Cacheable annotation is deleted, and do not affect other caches in the application void clear(); // wrappers the cache return value interface ValueWrapper { // Return the actual cached object Object get(); } // When an exception is thrown by {@link #get(Object, Callable)}, it will be wrapped as this exception thrown by @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'", key, loader), ex); this.key = key; } public Object getKey() { return this.key; } }}CacheManager interface
It mainly provides the creation of Cache implementation beans. Each application can isolate the Cache through cacheName, and each cacheName corresponds to a Cache implementation. The implementation provided by the spring framework and the implementation of Cache appear in pairs, and the package structure is also in the figure above.
#CacheManager.javapackage org.springframework.cache;import java.util.Collection;public interface CacheManager { // Create the cache implementation bean through cacheName. The specific implementation needs to store the created Cache implementation beans to avoid repeated creation, and also avoid the situation where the original cache content is lost after the memory cache object (such as Caffeine) is recreated Cache getCache(String name); // Return all cacheName Collection<String> getCacheNames();}Common annotations
@Cacheable: mainly applied to the method of querying data
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@Documentedpublic @interface Cacheable { // cacheNames, CacheManager creates the corresponding Cache implementation bean through this name @AliasFor("cacheNames") String[] value() default {}; @AliasFor("value") String[] cacheNames() default {}; // Cache key, supports SpEL expressions. The default is an object wrapped by all parameters and their hashCode (SimpleKey) String key() default ""; // Cache key generator, the default implementation is SimpleKeyGenerator String keyGenerator() default ""; // Specify which CacheManager to use String cacheManager() default ""; // Cache parser String cacheResolver() default ""; // Cache condition, supports SpEL expression, and cache data only when the satisfactory conditions are met. String condition() default "" will be judged before and after calling the method; // The cache is not updated when the condition is met, SpEL expression is supported, and String unless() default "" is judged only after calling the method; // When returning to the source to the actual method to obtain data, whether to keep synchronized? If false, the Cache.get(key) method is called; if true, the Cache.get(key, Callable) method is called boolean sync() default false;} @CacheEvict: Clear cache, mainly applied to the method of deleting data. There are two more properties than 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@Documentedpublic @interface CacheEvict { // ...For the same attribute description, please refer to the instructions in @Cacheable // Whether to clear all cached data, the Cache.evict(key) method is called when false; when true, the Cache.clear() method is called boolean allEntries() default false; // Clear cache before or after calling the method boolean beforeInvocation() default false;}Spring cache has been integrated in spring boot and provides a variety of cache configurations. When using it, you only need to configure which cache (enum CacheType) to use.
There is an additional extension thing added to spring boot, which is the CacheManagerCustomizer interface. You can customize this interface and then make some settings for CacheManager, such as:
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<RedisCacheManager> { @Override public void customize(RedisCacheManager cacheManager) { // Default expiration time, unit seconds cacheManager.setDefaultExpiration(1000); cacheManager.setUsePrefix(false); Map<String, Long> expires = new ConcurrentHashMap<String, Long>(); expires.put("userIdCache", 2000L); cacheManager.setExpires(expires); }}Load this bean:
package com.itopener.demo.cache.redis.config;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;/** * @author fuwei.deng * @date December 22, 2017 at 10:24:54 am * @version 1.0.0 */@Configurationpublic class CacheRedisConfiguration { @Bean public RedisCacheManagerCustomizer redisCacheManagerCustomizer() { return new RedisCacheManagerCustomizer(); }}The commonly used cache is Redis. Redis implements the spring cache interface in the spring-data-redis package.
Here are some of the shortcomings in the RedisCache implementation I think:
1. At the moment when the cache fails, if a thread obtains the cache data, it may return null. The reason is that the following steps are in the implementation of RedisCache:
Therefore, when the cache fails after judging that the key exists, and then obtaining the cache has no data, it returns null.
2. The attribute (cacheNullValues) that is allowed to store null values in RedisCacheManager is false by default, that is, it is not allowed to store null values, which will risk cache penetration. The defect is that this property is of final type, and the object can only be created when the object is created through the constructor method. Therefore, to avoid cache penetration, you can only declare the RedisCacheManager bean in the application.
3. The properties in RedisCacheManager cannot be directly configured through configuration files. They can only be set in the CacheManagerCustomizer interface. I personally think it is not convenient.
Caffeine is a high-performance memory cache based on Google's open source Guava design concept. It is developed using Java 8. After spring boot introduced Caffeine, Guava integration has been gradually abandoned. Caffeine source code and introduction address: caffeine
caffeine provides a variety of cache filling strategies and value recycling strategies, and also includes statistics such as cache hits, which can provide great help in cache optimization.
For the introduction of caffeine, please refer to: http://www.VeVB.COM/article/134242.htm
Here we briefly talk about the following types of caffeine time-based recycling strategies:
I mentioned at the beginning that even if redis cache is used, there will be a certain degree of consumption on network transmission. In actual applications, there will be some data with very low changes, which can be cached directly within the application. For some data with less real-time requirements, it can also be cached within the application for a certain period of time to reduce access to redis and improve response speed
Since redis has some shortcomings in the implementation of spring cache in the spring-data-redis framework, some problems may arise when using it, so we will not extend it based on the original implementation. We will directly refer to the implementation method to implement the Cache and CacheManager interfaces
It should also be noted that generally applications deploy multiple nodes, and the first-level cache is a cache within the application, so when data is updated and cleared, all nodes need to be notified to clean up the cache. There are many ways to achieve this effect, such as: zookeeper, MQ, etc., but since redis cache is used, redis itself supports subscription/publishing functions, so it does not rely on other components. It directly uses redis channel to notify other nodes to clean up cache operations.
The following are the starter encapsulation steps and source code for spring boot + spring cache to implement two-level cache (redis + caffeine)
Define properties configuration property class
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 January 29, 2018 at 11:32:15 am * @version 1.0.0 */@ConfigurationProperties(prefix = "spring.cache.multi")public class CacheRedisCaffeineProperties { private Set<String> cacheNames = new HashSet<>(); /** Whether to store null values, default true, prevent cache penetration*/ private boolean cacheNullValues = true; /** Whether to create cache implementation dynamically based on cacheName, default true*/ private boolean dynamic = true; /** Prefix of cache key*/ private String cachePrefix; private Redis redis = new Redis(); private Caffeine caffeine = new Caffeine(); public class Redis { /** Global expiration time, unit milliseconds, default expiration*/ private long defaultExpiration = 0; /** Expiration time, unit milliseconds, priority is higher than defaultExpiration*/ private Map<String, Long> expires = new HashMap<>(); /** Notify other nodes of topic names when cache updates*/ private String topic = "cache:redis:caffeine:topic"; public long getDefaultExpiration() { return defaultExpiration; } public void setDefaultExpiration(long defaultExpiration) { this.defaultExpiration = defaultExpiration; } public Map<String, Long> getExpires() { return expires; } public void setExpires(Map<String, Long> expires) { this.expires = expires; } public String getTopic() { return topic; } public void setTopic(String topic) { this.topic = topic; } } public class Caffeine { /** Expiration time after access, in milliseconds*/ private long expireAfterAccess; /** Expiration time after writing, unit milliseconds*/ private long expireAfterWrite; /** Refresh time after writing, unit milliseconds*/ private long refreshAfterWrite; /** Initialization size*/ private int initialCapacity; /** Maximum number of cache objects, the cache placed before exceeds this number will be invalid */ private long maximumSize; /** Since the weight needs to be provided by cache objects, it is not very suitable for scenarios like using spring cache, so configuration is not supported yet*/// 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<String> getCacheNames() { return cacheNames; } public void setCacheNames(Set<String> 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; }} There is an abstract class AbstractValueAdaptingCache that implements the Cache interface in spring cache, which contains the packaging of empty values and the packaging of cache values, so there is no need to implement the Cache interface, and directly implement the AbstractValueAdaptingCache abstract class.
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 January 26, 2018 at 5:24:11 pm * @version 1.0.0 */public class RedisCaffeineCache extends AbstractValueAdaptingCache { private final Logger logger = LoggerFactory.getLogger(RedisCaffeineCache.class); private String name; private RedisTemplate<Object, Object> redisTemplate; private Cache<Object, Object> caffeineCache; private String cachePrefix; private long defaultExpiration = 0; private Map<String, Long> expires; private String topic = "cache:redis:caffeine:topic"; protected RedisCaffeineCache(boolean allowNullValues) { super(allowNullValues); } public RedisCaffeineCache(String name, RedisTemplate<Object, Object> redisTemplate, Cache<Object, Object> 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> 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(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; // Consider using distributed locks, or change redis' setIfAbsent to atomic operation 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) { // First clear the cached data in redis, then clear the cache in caffeine, to avoid the cache in a short period of time if the cache cache is cleared first, and other requests will be loaded from redis to caffeine redisTemplate.delete(getKey(key)); push(new CacheMessage(this.name, key)); caffeineCache.invalidate(key); } @Override public void clear() { // Clear the cached data in redis, then clear the cache in caffeine, to avoid the cache in a short period of time if the cache cache is cleared first, then other requests will be loaded from redis to caffeine from redis Set<Object> 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 : {}", cacheKey); return value; } value = redisTemplate.opsForValue().get(cacheKey); if(value != null) { logger.debug("get cache from redis and put in caffeine, the key is : {}", cacheKey); cacheineCache.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 Notify other nodes to clean up the local cache when cache changes* @author fuwei.deng * @date January 31, 2018 at 3:20:28 pm * @version 1.0.0 * @param message */ private void push(CacheMessage message) { redisTemplate.convertAndSend(topic, message); } /** * @description Clean the local cache* @author fuwei.deng * @date January 31, 2018 at 3:15:39 pm * @version 1.0.0 * @param key */ public void clearLocal(Object key) { logger.debug("clear local cache, the key is : {}", key); if(key == null) { caffeineCache.invalidateAll(); } else { caffeineCache.invalidate(key); } }} Implement the CacheManager interface
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 January 26, 2018 at 5:24:52 pm * @version 1.0.0 */public class RedisCaffeineCacheManager implements CacheManager { private final Logger logger = LoggerFactory.getLogger(RedisCaffeineCacheManager.class); private ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<String, Cache>(); private CacheRedisCaffeineProperties cacheRedisCaffeineProperties; private RedisTemplate<Object, Object> redisTemplate; private boolean dynamic = true; private Set<String> cacheNames; public RedisCaffeineCaffeineManager(CacheRedisCaffeineProperties cacheRedisCaffeineProperties, RedisTemplate<Object, Object> 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, cache(), cacheRedisCaffeineProperties); Cache oldCache = cacheMap.putIfAbsent(name, cache); logger.debug("create cache instance, the cache name is : {}", name); return oldCache == null ? cache : oldCache; } public com.github.benmanes.caffeine.cache.Cache<Object, Object> caffeineCache(){ Caffeine<Object, Object> 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<String> 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 message publish/subscribe, message class for transmission
package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.support;import java.io.Serializable;/** * @author fuwei.deng * @date January 29, 2018 1:31:17 pm * @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; }} Listening to redis messages requires the MessageListener interface
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 January 30, 2018 at 5:22:33 pm * @version 1.0.0 */public class CacheMessageListener implements MessageListener { private final Logger logger = LoggerFactory.getLogger(CacheMessageListener.class); private RedisTemplate<Object, Object> redisTemplate; private RedisCaffeineCacheManager redisCaffeineCacheManager; public CacheMessageListener(RedisTemplate<Object, Object> 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 {}", cacheMessage.getCacheName(), cacheMessage.getKey()); redisCaffeineCacheManager.clearLocal(cacheMessage.getCacheName(), cacheMessage.getKey()); }} Add spring boot configuration class
package com.itopener.cache.redis.caffeine.spring.boot.autoconfigure;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.boot.autoconfigure.AutoConfiguration;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 January 26, 2018 at 5:23:03 pm * @version 1.0.0 */@Configuration@AutoConfigureAfter(RedisAutoConfiguration.class)@EnableConfigurationProperties(CacheRedisCaffeineProperties.class)public class CacheRedisCaffeineAutoConfiguration { @Autowired private CacheRedisCaffeineProperties cacheRedisCaffeineProperties; @Bean @ConditionalOnBean(RedisTemplate.class) public RedisCaffeineCaffeine cacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) { return new RedisCaffeineCacheManager(cacheRedisCaffeineProperties, redisTemplate); } @Bean public RedisMessageListenerContainer redisMessageListenerContainer(RedisTemplate<Object, Object> redisTemplate, RedisCaffeineCacheManager redisCaffeineCacheManager) { RedisMessageListenerContainer redisMessageListenerContainer = new RedisMessageListenerContainer(); redisMessageListenerContainer.setConnectionFactory(redisTemplate.getConnectionFactory()); CacheMessageListener cacheMessageListener = new CacheMessageListener(redisTemplate, redisCaffeineCaffeineCacheManager); redisMessageListenerContainer.addMessageListener(cacheMessageListener, new ChannelTopic(cacheRedisCaffeineProperties.getRedis().getTopic())); return redisMessageListenerContainer; }} Add spring boot configuration scan to resources/META-INF/spring.factories file
# Auto Configureorg.springframework.boot.autoconfigure.EnableAutoConfiguration=/com.itopener.cache.redis.caffeine.spring.boot.autoconfigure.CacheRedisCaffeineAutoConfiguration
Next, you can use maven to introduce it
<dependency> <groupId>com.itopener</groupId> <artifactId>cache-redis-caffeine-spring-boot-starter</artifactId> <version>1.0.0-SNAPSHOT</version> <type>pom</type></dependency>
在启动类上增加@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;@Servicepublic class CacheRedisCaffeineService { private final Logger logger = LoggerFactory.getLogger(CacheRedisCaffeineService.class); @Cacheable(key = "'cache_user_id_' + #id", value = "userIdCache", cacheManager = "cacheManager") 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", value = "userNameCache", cacheManager = "cacheManager") 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", value = "userIdCache", cacheManager = "cacheManager") public UserVO update(UserVO userVO) { logger.info("update to db"); userVO.setCreateTime(TimestampUtil.current()); return userVO; } @CacheEvict(key = "'cache_user_id_' + #id", value = "userIdCache", cacheManager = "cacheManager") 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 clusterspring.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
Extended
个人认为redisson的封装更方便一些
后续可以增加对于缓存命中率的统计endpoint,这样就可以更好的监控各个缓存的命中情况,以便对缓存配置进行优化
Source code download
starter目录:springboot / itopener-parent / spring-boot-starters-parent / cache-redis-caffeine-spring-boot-starter-parent
示例代码目录: springboot / itopener-parent / demo-parent / demo-cache-redis-caffeine
The above is all the content of this article. I hope it will be helpful to everyone's learning and I hope everyone will support Wulin.com more.