The cache of springboot is used in work, which is quite convenient to use. It directly introduces cache dependency packages such as redis or ehcache and the starter dependency packages of related caches, and then adds the @EnableCaching annotation to the startup class, and then you can use @Cacheable and @CacheEvict to use and delete the cache where needed. This is very simple to use. I believe that those who have used springboot cache will play, so I won’t say more here. The only drawback is that springboot uses plug-in integration. Although it is very convenient to use, when you integrate ehcache, you use ehcache, and when you integrate redis, you use redis. If you want to use both together, ehcache is used as the local level 1 cache and redis is used as an integrated level 2 cache. As far as I know, it is impossible to achieve the default method (if there is an expert who can implement it, please give me some advice). After all, many services require multi-point deployment. If you choose ehcache alone, you can realize local cache well. However, if you share cache between multiple machines, it will take time to make trouble. If you choose centralized redis cache, because you have to go to the network every time you get data, you will always feel that the performance will not be too good. This topic mainly discusses how to seamlessly integrate ehcache and redis as first- and second-level caches based on springboot, and realize cache synchronization.
In order not to invade the original cache method of springboot, I have defined two cache-related annotations here, as follows
@Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Cacheable { String value() default ""; String key() default ""; //Generic Class type Class<?> type() default Exception.class; } @Target({ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface CacheEvict { String value() default ""; String key() default ""; }As the above two annotations are basically the same as the cached annotations in spring, but some infrequently used attributes are removed. Speaking of this, I wonder if any friends have noticed that when you use redis cache alone in springboot, the value attributes annotated by Cacheable and CacheEvict actually become a zset value key in redis, and the zset is still empty, such as @Cacheable(value="cache1",key="key1"). Under normal circumstances, cache1 -> map(key1,value1) should appear in redis, where cache1 is used as the cache name, map as the cache value, and key as the key in map, which can effectively isolate caches under different cache names. But in fact, there are cache1 -> empty (zset) and key1 -> value1, two independent key-value pairs. The experiment found that the cache under different cache names is completely shared. If you are interested, you can try it. That is to say, this value attribute is actually a decoration, and the uniqueness of the key is only guaranteed by the key attribute. I can only think that this is a bug in the cache implementation of spring, or it was designed specifically (if you know the reason, please give me some advice).
Back to the topic, with annotation, there is also annotation processing class. Here I use the section of aop for interception, and the native implementation is actually similar. The section processing class is as follows:
import com.xuanwu.apaas.core.multicache.annotation.CacheEvict; import com.xuanwu.apaas.core.multicache.annotation.Cacheable; import com.xuanwu.apaas.core.utils.JsonUtil; import org.apache.commons.lang3.StringUtils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.reflect.MethodSignature; import org.json.JSONArray; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.LocalVariableTableParameterNameDiscoverer; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.stereotype.Component; import java.lang.reflect.Method; /** * Multilevel cache section* @author rongdi */ @Aspect @Component public class MultiCacheAspect { private static final Logger logger = LoggerFactory.getLogger(MultiCacheAspect.class); @Autowired private CacheFactory cacheFactory; // Here the listener is initialized through a container, and the cache switch is controlled according to the externally configured @EnableCaching annotation private boolean cacheEnable; @Pointcut("@annotation(com.xuanwu.apaas.core.multicache.annotation.Cacheable)") public void cacheAspect() { } @Pointcut("@annotation(com.xuanwu.apaas.core.multicache.annotation.CacheEvict)") public void cacheEvict() { } @Around("cacheableAspect()") public Object cache(ProceedingJoinPoint joinPoint) { //Get the parameter list of the method that is modified by the facet Object[] args = joinPoint.getArgs(); // result is the final return result of the method Object result = null; //If the cache is not enabled, directly call the processing method to return if(!cacheEnable){ try { result = joinPoint.proceed(args); } catch (Throwable e) { logger.error("",e); } return result; } // Get the return value type of the proxy method Class returnType = ((MethodSignature) joinPoint.getSignature()).getReturnType(); // Get the proxy method Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); // Get the comment on the proxy method Cacheable ca = method.getAnnotation(Cacheable.class); //Get the key value parsed by el String key = parseKey(ca.key(), method,args); Class<?> elementClass = ca.type(); //Get the cache name from the annotation String name = ca.value(); try { //First get data from ehcache String cacheValue = cacheFactory.ehGet(name,key); if(StringUtils.isEmpty(cacheValue)) { //If there is no data in ehcache, get data from redis cacheValue = cacheFactory.redisGet(name,key); if(StringUtils.isEmpty(cacheValue)) { //If there is no data in ehcache, get data from redis cacheValue = cacheFactory.redisGet(name,key); if(StringUtils.isEmpty(cacheValue)) { //If there is no data in redis //Call the business method to get the result result = joinPoint.proceed(args); //Serialize the result and put it in redis cacheFactory.redisPut(name,key,serialize(result)); } else { //If data can be obtained from redis //Deserialize the data obtained in the cache and return if(elementClass == Exception.class) { result = deserialize(cacheValue, returnType); } else { result = deserialize(cacheValue, returnType,elementClass); } } //Serialize the result and put it in ehcache cacheFactory.ehPut(name,key,serialize(result)); } else { //Deserialize the data obtained in the cache and return if(elementClass == Exception.class) { result = deserialize(cacheValue, returnType); } else { result = deserialize(cacheValue, returnType,elementClass); } } } catch (Throwable throwable) { logger.error("",throwable); } return result; } /** * Clear the cache before the method is called, and then call the business method* @param joinPoint * @return * @throws Throwable * */ @Around("cacheEvict()") public Object evictCache(ProceedingJoinPoint joinPoint) throws Throwable { // Get the proxy method Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); // Get the parameter list of the method modified by the facet Object[] args = joinPoint.getArgs(); // Get the annotation on the proxy method CacheEvict ce = method.getAnnotation(CacheEvict.class); //Get the key value parsed by el String key = parseKey(ce.key(), method,args); //Get the cache name from the annotation String name = ce.value(); //Clear the corresponding cache cacheFactory.cacheDel(name,key); return joinPoint.proceed(args); } /** * Get the cached key * key defined on the annotation, supporting SPEL expressions* @return */ private String parseKey(String key,Method method,Object [] args){ if(StringUtils.isEmpty(key)) return null; //Get the parameter name list of intercepted method (using Spring support class library) LocalVariableTableParameterNameDiscoverer u = new LocalVariableTableParameterNameDiscoverer(); String[] paraNameArr = u.getParameterNames(method); //Use SPEL for key parsing ExpressionParser parser = new SpelExpressionParser(); //SPEL context StandardEvaluationContext context = new StandardEvaluationContext(); //Put the method parameters into the SPEL context for(int i=0;i<paraNameArr.length;i++){ context.setVariable(paraNameArr[i], args[i]); } return parser.parseExpression(key).getValue(context,String.class); } //Serialize private String serialize(Object obj) { String result = null; try { result = JsonUtil.serialize(obj); } catch(Exception e) { result = obj.toString(); } return result; } //Deserialize private Object deserialize(String str,Class clazz) { Object result = null; try { if(clazz == JSONObject.class) { result = new JSONObject(str); } else if(clazz == JSONArray.class) { result = new JSONArray(str); } else { result = JsonUtil.deserialize(str,clazz); } } catch(Exception e) { } return result; } //Deserialization, support List<xxx> private Object deserialize(String str,Class clazz,Class elementClass) { Object result = null; try { if(clazz == JSONObject.class) { result = new JSONObject(str); } else if(clazz == JSONArray.class) { result = new JSONArray(str); } else { result = JsonUtil.deserialize(str,clazz,elementClass); } } catch(Exception e) { } return result; } public void setCacheEnable(boolean cacheEnable) { this.cacheEnable = cacheEnable; } }The above interface uses a cacheEnable variable to control whether to use cache. In order to achieve seamless access to springboot, it is necessary to be controlled by the native @EnableCaching annotation. Here I use a listener loaded by the spring container, and then find in the listener whether there is a class modified by the @EnableCaching annotation. If so, get the MultiCacheAspect object from the spring container, and then set cacheEnable to true. This will enable seamless access to springboot. I wonder if there is any more elegant way for friends? Welcome to communicate! The listener class is as follows
import com.xuanwu.apaas.core.multicache.CacheFactory; import com.xuanwu.apaas.core.multicache.MultiCacheAspect; import org.springframework.cache.annotation.EnableCaching; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.stereotype.Component; import java.util.Map; /** * Used to find whether there is annotation to enable cache in the project after spring loading is completed @EnableCaching * @author rongdi */ @Component public class ContextRefreshedListener implements ApplicationListener<ContextRefreshedEvent> { @Override public void onApplicationEvent(ContextRefreshedEvent event) { // Determine that the root container is a Spring container to prevent the occurrence of two calls (mvc loading will also trigger once) if(event.getApplicationContext().getParent()==null){ //Get all classes modified by @EnableCaching annotation Map<String,Object> beans = event.getApplicationContext().getBeansWithAnnotation(EnableCaching.class); if(beans != null && !beans.isEmpty()) { MultiCacheAspect multiCache = (MultiCacheAspect)event.getApplicationContext().getBean("multiCacheAspect"); multiCache.setCacheEnable(true); } } } } }To achieve seamless access, we also need to consider how multi-point ehcache is consistent with redis cache when deploying multi-point ehcache. In normal applications, redis is generally suitable for long-term centralized cache, and ehcache is suitable for short-term local cache. Assume that there are now A, B and C servers, A and B deploy business services, and C deploys redis services. When a request comes in, the front-end entry, whether it is load software such as LVS or nginx, will forward the request to a specific server. Assuming it is forwarded to server A and a certain content is modified, and this content is available in both redis and ehcache. At this time, the ehcache cache of server A and redis of server C are easier to control whether the cache is invalid or deleted. But how to control the ehcache of server B at this time? The commonly used method is to use the publish subscription mode. When you need to delete the cache, you publish a message on a fixed channel. Then each business server subscribes to this channel. After receiving the message, you delete or expire the local ehcache cache (it is best to use expired, but Redis currently only supports expired operations on the key. There is no way to operate the expiration of members in the map under the key. If you have to force the expiration, you can add a timestamp to implement it yourself. However, the chance of using delete to cause problems is very small. After all, those who add caches are applications with more reads and fewer writes. Here, for convenience, they will directly delete the cache). In summary, the process is to update a certain piece of data, first delete the corresponding cache in redis, and then publish a message with invalid cache in a certain channel of redis. The local business service subscribes to the message of this channel. When the business service receives this message, it deletes the local ehcache cache. The various configurations of redis are as follows.
import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import com.xuanwu.apaas.core.multicache.subscriber.MessageSubscriber; import org.springframework.cache.CacheManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.listener.PatternTopic; import org.springframework.data.redis.listener.RedisMessageListenerContainer; import org.springframework.data.redis.listener.adapter.MessageListenerAdapter; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; @Configuration public class RedisConfig { @Bean public CacheManager cacheManager(RedisTemplate redisTemplate) { RedisCacheManager rcm = new RedisCacheManager(redisTemplate); //Set cache expiration time (seconds) rcm.setDefaultExpiration(600); return rcm; } @Bean public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) { StringRedisTemplate template = new StringRedisTemplate(factory); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); template.setValueSerializer(jackson2JsonRedisSerializer); template.afterPropertiesSet(); return template; } /** * redis message listener container* You can add multiple redis listeners that listen to different topics. You only need to bind the message listener and the corresponding message subscription processor, and the message listener* Calling related methods of message subscription processor through reflection technology for some business processing * @param connectionFactory * @param listenerAdapter * @return */ @Bean public RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory, MessageListenerAdapter listenerAdapter) { RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(connectionFactory); //Subscribe to a channel container.addMessageListener(listenerAdapter, new PatternTopic("redis.uncache")); //This container can add multiple messageListener return container; } /** * Message listener adapter, binds the message processor, and uses reflection technology to call the message processor's business methods* @param receiver * @return */ @Bean MessageListenerAdapter listenerAdapter(MessageSubscriber receiver) { //This place is to pass a messageListenerAdapter to the messageListenerAdapter, and use the reflection method to call "handle" return new MessageListenerAdapter(receiver, "handle"); } }The message publishing class is as follows:
import com.xuanwu.apaas.core.multicache.CacheFactory; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class MessageSubscriber { private static final Logger logger = LoggerFactory.getLogger(MessageSubscriber.class); @Autowired private CacheFactory cacheFactory; /** * After receiving the message of redis subscription, the cache of ehcache is invalidated* @param message format is name_key */ public void handle(String message){ logger.debug("redis.ehcache:"+message); if(StringUtils.isEmpty(message)) { return; } String[] strs = message.split("#"); String name = strs[0]; String key = null; if(strs.length == 2) { key = strs[1]; } cacheFactory.ehDel(name,key); } }The specific operation cache classes are as follows:
import com.xuanwu.apaas.core.multicache.publisher.MessagePublisher; import net.sf.ehcache.Cache; import net.sf.ehcache.CacheManager; import net.sf.ehcache.Element; import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.RedisConnectionFailureException; import org.springframework.data.redis.core.HashOperations; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Component; import java.io.InputStream; /** * Multi-level cache section* @author rongdi */ @Component public class CacheFactory { private static final Logger logger = LoggerFactory.getLogger(CacheFactory.class); @Autowired private RedisTemplate redisTemplate; @Autowired private MessagePublisher messagePublisher; private CacheManager cacheManager; public CacheFactory() { InputStream is = this.getClass().getResourceAsStream("/ehcache.xml"); if(is != null) { cacheManager = CacheManager.create(is); } } public void cacheDel(String name,String key) { //Delete the cache corresponding to redis; //Delete the local ehcache cache, which is not needed, and the subscriber will delete // ehDel(name,key); if(cacheManager != null) { //Publish a message telling the subscribed service that the cache is invalid messagePublisher.publish(name, key); } } public String ehGet(String name,String key) { if(cacheManager == null) return null; Cache cache=cacheManager.getCache(name); if(cache == null) return null; cache.acquireReadLockOnKey(key); try { Element ele = cache.get(key); if(ele == null) return null; return (String)ele.getObjectValue(); } finally { cache.releaseReadLockOnKey(key); } } public String redisGet(String name,String key) { HashOperations<String,String,String> opera = redisTemplate.opsForHash(); try { return opera.get(name, key); } catch(RedisConnectionFailureException e) { //The connection fails, no error is thrown, and the logger.error("connect redis error ",e); return null; } } public void ehPut(String name,String key,String value) { if(cacheManager == null) return; if(!cacheManager.cacheExists(name)) { cacheManager.addCache(name); } Cache cache=cacheManager.getCache(name); //Get the write lock on the key, different keys do not affect each other, similar to synchronized(key.intern()){} cache.acquireWriteLockOnKey(key); try { cache.put(new Element(key, value)); } finally { //Release the write lock cache.releaseWriteLockOnKey(key); } } public void redisPut(String name,String key,String value) { HashOperations<String,String,String> opera = redisTemplate.opsForHash(); try { operator.put(name, key, value); } catch (RedisConnectionFailureException e) { //The connection failed, no error was thrown, and the logger.error("connect redis error ",e); } } public void ehDel(String name,String key) { if(cacheManager == null) return; if(cacheManager.cacheExists(name)) { //If the key is empty, delete directly according to the cache name if(StringUtils.isEmpty(key)) { cacheManager.removeCache(name); } else { Cache cache=cacheManager.getCache(name); cache.remove(key); } } } public void redisDel(String name,String key) { HashOperations<String,String,String> opera = redisTemplate.opsForHash(); try { //If the key is empty, delete if(StringUtils.isEmpty(key)) { redisTemplate.delete(name); } else { opera.delete(name,key); } } catch (RedisConnectionFailureException e) { //The connection failed, no error was thrown, and the logger.error("connect redis error",e); } } }The tool class is as follows
import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.JavaType; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.commons.lang3.StringUtils; import org.json.JSONArray; import org.json.JSONObject; import java.util.*; public class JsonUtil { private static ObjectMapper mapper; static { mapper = new ObjectMapper(); mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); } /** * Serialize the object into json * * @param obj Object to be serialized* @return * @throws Exception */ public static String serialize(Object obj) throws Exception { if (obj == null) { throw new IllegalArgumentException("obj should not be null"); } return mapper.writeValueAsString(obj); } /** Deserialization with generics, such as deserialization of a JSONArray into List<User> */ public static <T> T deserialize(String jsonStr, Class<?> collectionClass, Class<?>... elementClasses) throws Exception { JavaType javaType = mappper.getTypeFactory().constructParametriizedType( collectionClass, collectionClass, elementClasses); return mapper.readValue(jsonStr, javaType); } /** * Deserialize the json string into an object* @param src The json string to be deserialized* @param t The class type of the object deserialized into* @return * @throws Exception */ public static <T> T deserialize(String src, Class<T> t) throws Exception { if (src == null) { throw new IllegalArgumentException("src should not be null"); } if("{}".equals(src.trim())) { return null; } return mappper.readValue(src, t); } }To use cache specifically, just pay attention to @Cacheable and @CacheEvict annotations, and also support spring el expressions. Moreover, the cache name represented by the value attribute here does not have the problem mentioned above. Different caches can be isolated using value. Examples are as follows
@Cacheable(value = "bo",key="#session.productVersionCode+''+#session.tenantCode+''+#objectcode")@CacheEvict(value = "bo",key="#session.productVersionCode+''+#session.tenantCode+''+#objectcode")
Attached the main dependency package
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.