In addition to Weibo, there is also WeChat
Please pay attention
WeChat public account
Shulou
2025-03-01 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >
Share
Shulou(Shulou.com)06/02 Report--
This article mainly introduces "how to achieve the SpringBoot cache system". In the daily operation, I believe that many people have doubts about how to achieve the SpringBoot cache system. The editor consulted all kinds of materials and sorted out simple and easy-to-use operation methods. I hope it will be helpful to answer the doubts about "how to achieve the SpringBoot cache system". Next, please follow the editor to study!
1. General cache interface
1. Cache basic algorithm
FIFO (First In First Out), first-in-first-out, is the same as FIFO in OS. If a data enters the cache first, when the cache is full, the data that first enters the cache should be removed.
LFU (Least Frequently Used), which is used least frequently, is less likely to be used in the future if a data is rarely used in the most recent period of time.
LRU (Least Recently Used) is the least used recently, and if a data has not been accessed recently, it is less likely to be accessed in the future. That is, when the limited space is full of data, the data that has not been accessed for the longest time should be removed.
2. Interface definition
Simply define the cache interface, which can be abstracted as follows:
Package com.power.demo.cache.contract; import java.util.function.Function; / * cache provider interface * * / public interface CacheProviderService {/ * query cache * * @ param key cache key cannot be empty * * / T get (String key) / * * query cache * * @ param key cache key cannot be empty * @ param function if there is no cache, the object returned by calling the callable function can be empty * * / T get (String key, Function function) / * * query cache * * @ param key cache key cannot be empty * @ param function if there is no cache, calling the callable function returns the call parameter * * / T get (String key, Function function, M funcParm) of the empty * @ param funcParm function function. / * * query cache * * @ param key cache key cannot be empty * @ param function if there is no cache, call the callable function to return an object that can be empty * @ param expireTime expiration time (in milliseconds) can be empty * / T get (String key, Function function, Long expireTime) / * * query cache * * @ param key cache key cannot be empty * @ param function if there is no cache Call the callable function to return an empty * @ param funcParm function function call parameter * @ param expireTime expiration time (in milliseconds) can be empty * * / T get (String key, Function function, M funcParm, Long expireTime) / * set cache key value * * @ param key cache key cannot be null * @ param obj cache value cannot be empty * * / void set (String key, T obj) / * set cache key value * * @ param key cache key cannot be null * @ param obj cache value cannot be empty * @ param expireTime expiration time (in milliseconds) can be empty * * / void set (String key, T obj, Long expireTime) / * * remove cache * * @ param key cache key cannot be empty * * / void remove (String key); / * * whether there is cache * * @ param key cache key cannot be empty * / boolean contains (String key);}
Note that only common caching function interfaces are listed here, and some statistical class interfaces, distributed locks, self-increment (subtraction) and other functions used in special scenarios are not discussed.
Get-related methods, pay attention to multiple parameters. Cache the Function passed from the API, which is a functional interface provided by Java8. Although the number of input parameters supported is limited (here you will miss the Func delegate under .NET very much), this is really a significant progress for the language Java.
Now that the interface is defined, it's time to implement the cache provider. According to the type of storage, this paper simply implements the two most commonly used cache providers: local cache and distributed cache.
Second, local cache
Local cache, that is, JVM-level cache (local cache can be thought of as a direct intra-process communication call, while distributed cache requires cross-process communication calls through the network), there are generally many ways to implement, such as directly using naturally thread-safe sets such as Hashtable and ConcurrentHashMap as cache containers, or using some mature open source components, such as EhCache, Guava Cache, etc. This article chooses a simple Guava cache to get started.
1. What is Guava
Guava, to put it simply, is a development class library and a very rich and powerful development kit that claims to make using the Java language more enjoyable, including basic tool class libraries and interfaces, caching, publish and subscribe style event buses, and so on. In actual development, I use collections, caches, and commonly used type helper classes most often, and many people praise this class library.
2. Add dependencies
Com.google.guava guava / dependency >
3. Implement the interface
/ * * Local Cache provider Service (Guava Cache) * * / @ Configuration @ ComponentScan (basePackages = AppConst.BASE_PACKAGE_NAME) @ Qualifier ("localCacheService") public class LocalCacheProviderImpl implements CacheProviderService {private static Map _ cacheMap = Maps.newConcurrentMap () Static {Cache cacheContainer = CacheBuilder.newBuilder () .maximumSize (AppConst.CACHE_MAXIMUM_SIZE) .roomreAfterWrite (AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS) / / the period of time after the last write is removed / / .roomreAfterAccess (AppConst.CACHE_MINUTE) TimeUnit.MILLISECONDS) / / remove .recordStats () / / enable statistics after the last visit. Build () _ cacheMap.put (String.valueOf (AppConst.CACHE_MINUTE), cacheContainer);} / * * query cache * * @ param key cache key cannot be empty * * / public T get (String key) {T obj = get (key, null, null, AppConst.CACHE_MINUTE); return obj } / * * query cache * * @ param key cache key cannot be empty * @ param function if there is no cache, calling the callable function to return the object can be empty * * / public T get (String key, Function function) {T obj = get (key, function, key, AppConst.CACHE_MINUTE); return obj } / * * query cache * * @ param key cache key cannot be empty * @ param function if there is no cache Calling this callable function returns an empty call parameter of * @ param funcParm function function * * / public T get (String key, Function function, M funcParm) {T obj = get (key, function, funcParm, AppConst.CACHE_MINUTE) Return obj } / * * query cache * * @ param key cache key cannot be empty * @ param function if there is no cache Call the callable function to return an object that can be empty * @ param expireTime expiration time (in milliseconds) can be empty * / public T get (String key, Function function, Long expireTime) {T obj = get (key, function, key, expireTime) Return obj } / * * query cache * * @ param key cache key cannot be empty * @ param function if there is no cache Calling the callable function returns an object that can be empty * @ param funcParm function function call parameter * @ param expireTime expiration time (in milliseconds) can be empty * * / public T get (String key, Function function, M funcParm, Long expireTime) {T obj = null If (StringUtils.isEmpty (key) = = true) {return obj;} expireTime = getExpireTime (expireTime); Cache cacheContainer = getCacheContainer (expireTime); try {if (function = = null) {obj = (T) cacheContainer.getIfPresent (key);} else {final Long cachedTime = expireTime Obj = (T) cacheContainer.get (key, ()-> {T retObj = function.apply (funcParm); return retObj;});}} catch (Exception e) {e.printStackTrace ();} return obj } / * set the cache key value to insert the value directly into the cache, which will directly overwrite the value mapped before the given key * * @ param key cache key cannot be null * @ param obj cache value cannot be null * * / public void set (String key, T obj) {set (key, obj, AppConst.CACHE_MINUTE) } / * set the cache key value to insert the value directly into the cache This directly overwrites the value mapped before the given key * * @ param key cache key cannot be null * @ param obj cache value cannot be null * @ param expireTime expiration time (in milliseconds) can be empty * / public void set (String key, T obj) Long expireTime) {if (StringUtils.isEmpty (key) = = true) {return } if (obj = = null) {return;} expireTime = getExpireTime (expireTime); Cache cacheContainer = getCacheContainer (expireTime); cacheContainer.put (key, obj) } / * remove cache * * @ param key cache key cannot be empty * * / public void remove (String key) {if (StringUtils.isEmpty (key) = = true) {return;} long expireTime = getExpireTime (AppConst.CACHE_MINUTE); Cache cacheContainer = getCacheContainer (expireTime) CacheContainer.invalidate (key);} / * * whether there is a cache * * @ param key cache key cannot be empty * * / public boolean contains (String key) {boolean exists = false; if (StringUtils.isEmpty (key) = = true) {return exists;} Object obj = get (key) If (obj! = null) {exists = true;} return exists;} private static Lock lock = new ReentrantLock (); private Cache getCacheContainer (Long expireTime) {Cache cacheContainer = null; if (expireTime = = null) {return cacheContainer;} String mapKey = String.valueOf (expireTime) If (_ cacheMap.containsKey (mapKey) = = true) {cacheContainer = _ cacheMap.get (mapKey); return cacheContainer;} try {lock.lock () CacheContainer = CacheBuilder.newBuilder () .maximumSize (AppConst.CACHE_MAXIMUM_SIZE) .roomreAfterWrite (expireTime, TimeUnit.MILLISECONDS) / / the period of time after the last write is removed / / .uploreAfterAccess (AppConst.CACHE_MINUTE) TimeUnit.MILLISECONDS) / / remove .recordStats () / / enable statistics after the last visit. Build () _ cacheMap.put (mapKey, cacheContainer);} finally {lock.unlock ();} return cacheContainer } / * get the expiration time in milliseconds * * @ param expireTime. If the expiration time in milliseconds is less than 1 minute, the default is 10 minutes * * / private Long getExpireTime (Long expireTime) {Long result = expireTime; if (expireTime = = null | | expireTime
< AppConst.CACHE_MINUTE / 10) { result = AppConst.CACHE_MINUTE; } return result; } } 4、注意事项 Guava Cache初始化容器时,支持缓存过期策略,类似FIFO、LRU和LFU等算法。 expireAfterWrite:最后一次写入后的一段时间移出。 expireAfterAccess:最后一次访问后的一段时间移出。 Guava Cache对缓存过期时间的设置实在不够友好。常见的应用场景,比如,有些几乎不变的基础数据缓存1天,有些热点数据缓存2小时,有些会话数据缓存5分钟等等。 通常我们认为设置缓存的时候带上缓存的过期时间是非常容易的,而且只要一个缓存容器实例即可,比如.NET下的ObjectCache、System.Runtime.Cache等等。 但是Guava Cache不是这个实现思路,如果缓存的过期时间不同,Guava的CacheBuilder要初始化多份Cache实例。 好在我在实现的时候注意到了这个问题,并且提供了解决方案,可以看到getCacheContainer这个函数,根据过期时长做缓存实例判断,就算不同过期时间的多实例缓存也是完全没有问题的。 三、分布式缓存 分布式缓存产品非常多,本文使用应用普遍的Redis,在Spring Boot应用中使用Redis非常简单。 1、什么是Redis Redis是一款开源(BSD许可)的、用C语言写成的高性能的键-值存储(key-value store)。它常被称作是一款数据结构服务器(data structure server)。它可以被用作缓存、消息中间件和数据库,在很多应用中,经常看到有人选择使用Redis做缓存,实现分布式锁和分布式Session等。作为缓存系统时,和经典的KV结构的Memcached非常相似,但又有很多不同。 Redis支持丰富的数据类型。Redis的键值可以包括字符串(strings)类型,同时它还包括哈希(hashes)、列表(lists)、集合(sets)和有序集合(sorted sets)等数据类型。对于这些数据类型,你可以执行原子操作。例如:对字符串进行附加操作(append);递增哈希中的值;向列表中增加元素;计算集合的交集、并集与差集等。 Redis的数据类型: Keys:非二进制安全的字符类型( not binary-safe strings ),由于key不是binary safe的字符串,所以像"my key"和"mykey\n"这样包含空格和换行的key是不允许的。 Values:Strings、Hash、Lists、 Sets、 Sorted sets。考虑到Redis单线程操作模式,Value的粒度不应该过大,缓存的值越大,越容易造成阻塞和排队。 为了获得优异的性能,Redis采用了内存中(in-memory)数据集(dataset)的方式。同时,Redis支持数据的持久化,你可以每隔一段时间将数据集转存到磁盘上(snapshot),或者在日志尾部追加每一条操作命令(append only file,aof)。 Redis同样支持主从复制(master-slave replication),并且具有非常快速的非阻塞首次同步( non-blocking first synchronization)、网络断开自动重连等功能。 同时Redis还具有其它一些特性,其中包括简单的事物支持、发布订阅 ( pub/sub)、管道(pipeline)和虚拟内存(vm)等 。 2、添加依赖 org.springframework.boot spring-boot-starter-data-redis 3、配置Redis 在application.properties配置文件中,配置Redis常用参数: ## Redis缓存相关配置 #Redis数据库索引(默认为0) spring.redis.database=0 #Redis服务器地址 spring.redis.host=127.0.0.1 #Redis服务器端口 spring.redis.port=6379 #Redis服务器密码(默认为空) spring.redis.password=123321 #Redis连接超时时间 默认:5分钟(单位:毫秒) spring.redis.timeout=300000ms #Redis连接池最大连接数(使用负值表示没有限制) spring.redis.jedis.pool.max-active=512 #Redis连接池中的最小空闲连接 spring.redis.jedis.pool.min-idle=0 #Redis连接池中的最大空闲连接 spring.redis.jedis.pool.max-idle=8 #Redis连接池最大阻塞等待时间(使用负值表示没有限制) spring.redis.jedis.pool.max-wait=-1ms 常见的需要注意的是最大连接数(spring.redis.jedis.pool.max-active )和超时时间(spring.redis.jedis.pool.max-wait)。Redis在生产环境中出现故障的频率经常和这两个参数息息相关。 接着定义一个继承自CachingConfigurerSupport(请注意cacheManager和keyGenerator这两个方法在子类的实现)的RedisConfig类: package com.power.demo.cache.config; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.EnableCaching; 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.serializer.StringRedisSerializer; /** * Redis缓存配置类 */ @Configuration @EnableCaching public class RedisConfig extends CachingConfigurerSupport { @Bean public CacheManager cacheManager(RedisConnectionFactory connectionFactory) { return RedisCacheManager.create(connectionFactory); } @Bean public RedisTemplate redisTemplate(RedisConnectionFactory factory) { RedisTemplate template = new RedisTemplate(); //Jedis的Key和Value的序列化器默认值是JdkSerializationRedisSerializer //经实验,JdkSerializationRedisSerializer通过RedisDesktopManager看到的键值对不能正常解析 //设置key的序列化器 template.setKeySerializer(new StringRedisSerializer()); ////设置value的序列化器 默认值是JdkSerializationRedisSerializer //使用Jackson序列化器的问题是,复杂对象可能序列化失败,比如JodaTime的DateTime类型 // //使用Jackson2,将对象序列化为JSON // Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); // //json转对象类,不设置默认的会将json转成hashmap // ObjectMapper om = new ObjectMapper(); // om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); // jackson2JsonRedisSerializer.setObjectMapper(om); // template.setValueSerializer(jackson2JsonRedisSerializer); //将redis连接工厂设置到模板类中 template.setConnectionFactory(factory); return template; } // //自定义缓存key生成策略 // @Bean // public KeyGenerator keyGenerator() { // return new KeyGenerator() { // @Override // public Object generate(Object target, java.lang.reflect.Method method, Object... params) { // StringBuffer sb = new StringBuffer(); // sb.append(target.getClass().getName()); // sb.append(method.getName()); // for (Object obj : params) { // if (obj == null) { // continue; // } // sb.append(obj.toString()); // } // return sb.toString(); // } // }; // } } 在RedisConfig这个类上加上@EnableCaching这个注解,这个注解会被Spring发现,并且会创建一个切面(aspect) 并触发Spring缓存注解的切点(pointcut)。据所使用的注解以及缓存的状态,这个切面会从缓存中获取数据,将数据添加到缓存之中或者从缓存中移除某个值。 cacheManager方法,申明一个缓存管理器(CacheManager)的bean,作用就是@EnableCaching这个切面在新增缓存或者删除缓存的时候会调用这个缓存管理器的方法。keyGenerator方法,可以根据需求自定义缓存key生成策略。 而redisTemplate方法,则主要是设置Redis模板类,比如键和值的序列化器(从这里可以看出,Redis的键值对必须可序列化)、redis连接工厂等。 RedisTemplate支持的序列化器主要有如下几种: JdkSerializationRedisSerializer:使用Java序列化; StringRedisSerializer:序列化String类型的key和value; GenericToStringSerializer:使用Spring转换服务进行序列化; JacksonJsonRedisSerializer:使用Jackson 1,将对象序列化为JSON; Jackson2JsonRedisSerializer:使用Jackson 2,将对象序列化为JSON; OxmSerializer:使用Spring O/X映射的编排器和解排器(marshaler和unmarshaler)实现序列化,用于XML序列化; 注意:RedisTemplate的键和值序列化器,默认情况下都是JdkSerializationRedisSerializer,它们都可以自定义设置序列化器。 推荐将字符串键使用StringRedisSerializer序列化器,因为运维的时候好排查问题,JDK序列化器的也能识别,但是可读性稍差(是因为缓存服务器没有JRE吗?),见如下效果: 而值序列化器则要复杂的多,很多人推荐使用Jackson2JsonRedisSerializer序列化器,但是实际开发过程中,经常有人碰到反序列化错误,经过排查多数都和Jackson2JsonRedisSerializer这个序列化器有关。 4、实现接口 使用RedisTemplate,在Spring Boot中调用Redis接口比直接调用Jedis简单多了。 package com.power.demo.cache.impl; import com.power.demo.cache.contract.CacheProviderService; import com.power.demo.common.AppConst; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; import org.springframework.util.StringUtils; import javax.annotation.Resource; import java.io.Serializable; import java.util.concurrent.TimeUnit; import java.util.function.Function; @Configuration @ComponentScan(basePackages = AppConst.BASE_PACKAGE_NAME) @Qualifier("redisCacheService") public class RedisCacheProviderImpl implements CacheProviderService { @Resource private RedisTemplate redisTemplate; /** * 查询缓存 * * @param key 缓存键 不可为空 **/ public T get(String key) { T obj = get(key, null, null, AppConst.CACHE_MINUTE); return obj; } /** * 查询缓存 * * @param key 缓存键 不可为空 * @param function 如没有缓存,调用该callable函数返回对象 可为空 **/ public T get(String key, Function function) { T obj = get(key, function, key, AppConst.CACHE_MINUTE); return obj; } /** * 查询缓存 * * @param key 缓存键 不可为空 * @param function 如没有缓存,调用该callable函数返回对象 可为空 * @param funcParm function函数的调用参数 **/ public T get(String key, Function function, M funcParm) { T obj = get(key, function, funcParm, AppConst.CACHE_MINUTE); return obj; } /** * 查询缓存 * * @param key 缓存键 不可为空 * @param function 如没有缓存,调用该callable函数返回对象 可为空 * @param expireTime 过期时间(单位:毫秒) 可为空 **/ public T get(String key, Function function, Long expireTime) { T obj = get(key, function, key, expireTime); return obj; } /** * 查询缓存 * * @param key 缓存键 不可为空 * @param function 如没有缓存,调用该callable函数返回对象 可为空 * @param funcParm function函数的调用参数 * @param expireTime 过期时间(单位:毫秒) 可为空 **/ public T get(String key, Function function, M funcParm, Long expireTime) { T obj = null; if (StringUtils.isEmpty(key) == true) { return obj; } expireTime = getExpireTime(expireTime); try { ValueOperations operations = redisTemplate.opsForValue(); obj = (T) operations.get(key); if (function != null && obj == null) { obj = function.apply(funcParm); if (obj != null) { set(key, obj, expireTime);//设置缓存信息 } } } catch (Exception e) { e.printStackTrace(); } return obj; } /** * 设置缓存键值 直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值 * * @param key 缓存键 不可为空 * @param obj 缓存值 不可为空 **/ public void set(String key, T obj) { set(key, obj, AppConst.CACHE_MINUTE); } /** * 设置缓存键值 直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值 * * @param key 缓存键 不可为空 * @param obj 缓存值 不可为空 * @param expireTime 过期时间(单位:毫秒) 可为空 **/ public void set(String key, T obj, Long expireTime) { if (StringUtils.isEmpty(key) == true) { return; } if (obj == null) { return; } expireTime = getExpireTime(expireTime); ValueOperations operations = redisTemplate.opsForValue(); operations.set(key, obj); redisTemplate.expire(key, expireTime, TimeUnit.MILLISECONDS); } /** * 移除缓存 * * @param key 缓存键 不可为空 **/ public void remove(String key) { if (StringUtils.isEmpty(key) == true) { return; } redisTemplate.delete(key); } /** * 是否存在缓存 * * @param key 缓存键 不可为空 **/ public boolean contains(String key) { boolean exists = false; if (StringUtils.isEmpty(key) == true) { return exists; } Object obj = get(key); if (obj != null) { exists = true; } return exists; } /** * 获取过期时间 单位:毫秒 * * @param expireTime 传人的过期时间 单位毫秒 如小于1分钟,默认为10分钟 **/ private Long getExpireTime(Long expireTime) { Long result = expireTime; if (expireTime == null || expireTime < AppConst.CACHE_MINUTE / 10) { result = AppConst.CACHE_MINUTE; } return result; } } 注意:很多教程里都讲到通过注解的方式(@Cacheable,@CachePut、@CacheEvict和@Caching)实现数据缓存,根据实践,我个人是不推崇这种使用方式的。 四、缓存"及时"过期问题 这个也是开发和运维过程中非常经典的问题。 有些公司写缓存客户端的时候,会给每个团队分别定义一个Area,但是这个只能做到缓存键的分布区分,不能保证缓存"实时"有效的过期。 多年以前我写过一篇结合实际情况的文章,也就是加上缓存版本,请猛击这里 ,算是提供了一种相对有效的方案,不过高并发站点要慎重,防止发生雪崩效应。 Redis还有一些其他常见问题,比如:Redis的字符串类型Key和Value都有限制,且都是不能超过512M,请猛击这里。还有最大连接数和超时时间设置等问题,本文就不再一一列举了。 五、二级缓存 在配置文件中,加上缓存提供者开关: ##是否启用本地缓存 spring.power.isuselocalcache=1 ##是否启用Redis缓存 spring.power.isuserediscache=1 缓存提供者程序都实现好了,我们会再包装一个调用外观类PowerCacheBuilder,加上缓存版本控制,可以轻松自如地控制和切换缓存,code talks: package com.power.demo.cache; import com.google.common.collect.Lists; import com.power.demo.cache.contract.CacheProviderService; import com.power.demo.common.AppConst; import com.power.demo.common.AppField; import com.power.demo.util.ConfigUtil; import com.power.demo.util.PowerLogger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.util.StringUtils; import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Function; /* * 支持多缓存提供程序多级缓存的缓存帮助类 * */ @Configuration @ComponentScan(basePackages = AppConst.BASE_PACKAGE_NAME) public class PowerCacheBuilder { @Autowired @Qualifier("localCacheService") private CacheProviderService localCacheService; @Autowired @Qualifier("redisCacheService") private CacheProviderService redisCacheService; private static List _listCacheProvider = Lists.newArrayList(); private static final Lock providerLock = new ReentrantLock(); /** * 初始化缓存提供者 默认优先级:先本地缓存,后分布式缓存 **/ private List getCacheProviders() { if (_listCacheProvider.size() >Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.
Views: 0
*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.
Continue with the installation of the previous hadoop.First, install zookooper1. Decompress zookoope
"Every 5-10 years, there's a rare product, a really special, very unusual product that's the most un
© 2024 shulou.com SLNews company. All rights reserved.