贼厉害,手撸的 SpringBoot 缓存系统,性能杠杠的!
一、通用缓存接口
二、本地缓存
三、分布式缓存
四、缓存“及时”过期问题
五、二级缓存
缓存是最直接有效提升系统性能的手段之一。个人认为用好用对缓存是优秀程序员的必备基本素质。
本文结合实际开发经验,从简单概念原理和代码入手,一步一步搭建一个简单的二级缓存系统。
一、通用缓存接口
1、缓存基础算法
(1)、FIFO(First In First Out),先进先出,和OS里的FIFO思路相同,如果一个数据最先进入缓存中,当缓存满的时候,应当把最先进入缓存的数据给移除掉。(2)、LFU(Least Frequently Used),最不经常使用,如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小。(3)、LRU(Least Recently Used),最近最少使用,如果一个数据在最近一段时间没有被访问到,那么在将来它被访问的可能性也很小。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据移除。
2、接口定义
简单定义缓存接口,大致可以抽象如下:
- package com.power.demo.cache.contract;
- import java.util.function.Function;
- /**
- * 缓存提供者接口
- **/
- public interface CacheProviderService {
- /**
- * 查询缓存
- *
- * @param key 缓存键 不可为空
- **/
- <T extends Object> T get(String key);
- /**
- * 查询缓存
- *
- * @param key 缓存键 不可为空
- * @param function 如没有缓存,调用该callable函数返回对象 可为空
- **/
- <T extends Object> T get(String key, Function<String, T> function);
- /**
- * 查询缓存
- *
- * @param key 缓存键 不可为空
- * @param function 如没有缓存,调用该callable函数返回对象 可为空
- * @param funcParm function函数的调用参数
- **/
- <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm);
- /**
- * 查询缓存
- *
- * @param key 缓存键 不可为空
- * @param function 如没有缓存,调用该callable函数返回对象 可为空
- * @param expireTime 过期时间(单位:毫秒) 可为空
- **/
- <T extends Object> T get(String key, Function<String, T> function, Long expireTime);
- /**
- * 查询缓存
- *
- * @param key 缓存键 不可为空
- * @param function 如没有缓存,调用该callable函数返回对象 可为空
- * @param funcParm function函数的调用参数
- * @param expireTime 过期时间(单位:毫秒) 可为空
- **/
- <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm, Long expireTime);
- /**
- * 设置缓存键值
- *
- * @param key 缓存键 不可为空
- * @param obj 缓存值 不可为空
- **/
- <T extends Object> void set(String key, T obj);
- /**
- * 设置缓存键值
- *
- * @param key 缓存键 不可为空
- * @param obj 缓存值 不可为空
- * @param expireTime 过期时间(单位:毫秒) 可为空
- **/
- <T extends Object> void set(String key, T obj, Long expireTime);
- /**
- * 移除缓存
- *
- * @param key 缓存键 不可为空
- **/
- void remove(String key);
- /**
- * 是否存在缓存
- *
- * @param key 缓存键 不可为空
- **/
- boolean contains(String key);
- }
注意,这里列出的只是常见缓存功能接口,一些在特殊场景下用到的统计类的接口、分布式锁、自增(减)等功能不在讨论范围之内。
Get相关方法,注意多个参数的情况,缓存接口里面传人的Function,这是Java8提供的函数式接口,虽然支持的入参个数有限(这里你会非常怀念.NET下的Func委托),但是仅对Java这个语言来说,这真是一个重大的进步^_^。
接口定义好了,下面就要实现缓存提供者程序了。按照存储类型的不同,本文简单实现最常用的两种缓存提供者:本地缓存和分布式缓存。
二、本地缓存
本地缓存,也就是JVM级别的缓存(本地缓存可以认为是直接在进程内通信调用,而分布式缓存则需要通过网络进行跨进程通信调用),一般有很多种实现方式,比如直接使用Hashtable、ConcurrentHashMap等天生线程安全的集合作为缓存容器,或者使用一些成熟的开源组件,如EhCache、Guava Cache等。本文选择上手简单的Guava缓存。
1、什么是Guava
Guava,简单来说就是一个开发类库,且是一个非常丰富强大的开发工具包,号称可以让使用Java语言更令人愉悦,主要包括基本工具类库和接口、缓存、发布订阅风格的事件总线等。在实际开发中,我用的最多的是集合、缓存和常用类型帮助类,很多人都对这个类库称赞有加。
2、添加依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
3、实现接口
- package com.power.demo.cache.impl;
- import com.google.common.cache.Cache;
- import com.google.common.cache.CacheBuilder;
- import com.google.common.collect.Maps;
- 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.util.StringUtils;
- import java.util.Map;
- import java.util.concurrent.TimeUnit;
- import java.util.concurrent.locks.Lock;
- import java.util.concurrent.locks.ReentrantLock;
- import java.util.function.Function;
- /*
- * 本地缓存提供者服务 (Guava Cache)
- * */
- @Configuration
- @ComponentScan(basePackages = AppConst.BASE_PACKAGE_NAME)
- @Qualifier("localCacheService")
- public class LocalCacheProviderImpl implements CacheProviderService {
- private static Map<String, Cache<String, Object>> _cacheMap = Maps.newConcurrentMap();
- static {
- Cache<String, Object> cacheContainer = CacheBuilder.newBuilder()
- .maximumSize(AppConst.CACHE_MAXIMUM_SIZE)
- .expireAfterWrite(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS)//最后一次写入后的一段时间移出
- //.expireAfterAccess(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS) //最后一次访问后的一段时间移出
- .recordStats()//开启统计功能
- .build();
- _cacheMap.put(String.valueOf(AppConst.CACHE_MINUTE), cacheContainer);
- }
- /**
- * 查询缓存
- *
- * @param key 缓存键 不可为空
- **/
- public <T extends Object> T get(String key) {
- T obj = get(key, null, null, AppConst.CACHE_MINUTE);
- return obj;
- }
- /**
- * 查询缓存
- *
- * @param key 缓存键 不可为空
- * @param function 如没有缓存,调用该callable函数返回对象 可为空
- **/
- public <T extends Object> T get(String key, Function<String, T> function) {
- T obj = get(key, function, key, AppConst.CACHE_MINUTE);
- return obj;
- }
- /**
- * 查询缓存
- *
- * @param key 缓存键 不可为空
- * @param function 如没有缓存,调用该callable函数返回对象 可为空
- * @param funcParm function函数的调用参数
- **/
- public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm) {
- T obj = get(key, function, funcParm, AppConst.CACHE_MINUTE);
- return obj;
- }
- /**
- * 查询缓存
- *
- * @param key 缓存键 不可为空
- * @param function 如没有缓存,调用该callable函数返回对象 可为空
- * @param expireTime 过期时间(单位:毫秒) 可为空
- **/
- public <T extends Object> T get(String key, Function<String, T> function, Long expireTime) {
- T obj = get(key, function, key, expireTime);
- return obj;
- }
- /**
- * 查询缓存
- *
- * @param key 缓存键 不可为空
- * @param function 如没有缓存,调用该callable函数返回对象 可为空
- * @param funcParm function函数的调用参数
- * @param expireTime 过期时间(单位:毫秒) 可为空
- **/
- public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm, Long expireTime) {
- T obj = null;
- if (StringUtils.isEmpty(key) == true) {
- return obj;
- }
- expireTime = getExpireTime(expireTime);
- Cache<String, Object> 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;
- }
- /**
- * 设置缓存键值 直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值
- *
- * @param key 缓存键 不可为空
- * @param obj 缓存值 不可为空
- **/
- public <T extends Object> void set(String key, T obj) {
- set(key, obj, AppConst.CACHE_MINUTE);
- }
- /**
- * 设置缓存键值 直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值
- *
- * @param key 缓存键 不可为空
- * @param obj 缓存值 不可为空
- * @param expireTime 过期时间(单位:毫秒) 可为空
- **/
- public <T extends Object> void set(String key, T obj, Long expireTime) {
- if (StringUtils.isEmpty(key) == true) {
- return;
- }
- if (obj == null) {
- return;
- }
- expireTime = getExpireTime(expireTime);
- Cache<String, Object> cacheContainer = getCacheContainer(expireTime);
- cacheContainer.put(key, obj);
- }
- /**
- * 移除缓存
- *
- * @param key 缓存键 不可为空
- **/
- public void remove(String key) {
- if (StringUtils.isEmpty(key) == true) {
- return;
- }
- long expireTime = getExpireTime(AppConst.CACHE_MINUTE);
- Cache<String, Object> cacheContainer = getCacheContainer(expireTime);
- cacheContainer.invalidate(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;
- }
- private static Lock lock = new ReentrantLock();
- private Cache<String, Object> getCacheContainer(Long expireTime) {
- Cache<String, Object> 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)
- .expireAfterWrite(expireTime, TimeUnit.MILLISECONDS)//最后一次写入后的一段时间移出
- //.expireAfterAccess(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS) //最后一次访问后的一段时间移出
- .recordStats()//开启统计功能
- .build();
- _cacheMap.put(mapKey, cacheContainer);
- } finally {
- lock.unlock();
- }
- return cacheContainer;
- }
- /**
- * 获取过期时间 单位:毫秒
- *
- * @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;
- }
- }
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、添加依赖
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-data-redis</artifactId>
- </dependency>
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<String, Object> redisTemplate(RedisConnectionFactory factory) {
- RedisTemplate<String, Object> 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<Serializable, Object> redisTemplate;
- /**
- * 查询缓存
- *
- * @param key 缓存键 不可为空
- **/
- public <T extends Object> T get(String key) {
- T obj = get(key, null, null, AppConst.CACHE_MINUTE);
- return obj;
- }
- /**
- * 查询缓存
- *
- * @param key 缓存键 不可为空
- * @param function 如没有缓存,调用该callable函数返回对象 可为空
- **/
- public <T extends Object> T get(String key, Function<String, T> function) {
- T obj = get(key, function, key, AppConst.CACHE_MINUTE);
- return obj;
- }
- /**
- * 查询缓存
- *
- * @param key 缓存键 不可为空
- * @param function 如没有缓存,调用该callable函数返回对象 可为空
- * @param funcParm function函数的调用参数
- **/
- public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm) {
- T obj = get(key, function, funcParm, AppConst.CACHE_MINUTE);
- return obj;
- }
- /**
- * 查询缓存
- *
- * @param key 缓存键 不可为空
- * @param function 如没有缓存,调用该callable函数返回对象 可为空
- * @param expireTime 过期时间(单位:毫秒) 可为空
- **/
- public <T extends Object> T get(String key, Function<String, T> function, Long expireTime) {
- T obj = get(key, function, key, expireTime);
- return obj;
- }
- /**
- * 查询缓存
- *
- * @param key 缓存键 不可为空
- * @param function 如没有缓存,调用该callable函数返回对象 可为空
- * @param funcParm function函数的调用参数
- * @param expireTime 过期时间(单位:毫秒) 可为空
- **/
- public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm, Long expireTime) {
- T obj = null;
- if (StringUtils.isEmpty(key) == true) {
- return obj;
- }
- expireTime = getExpireTime(expireTime);
- try {
- ValueOperations<Serializable, Object> 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 <T extends Object> void set(String key, T obj) {
- set(key, obj, AppConst.CACHE_MINUTE);
- }
- /**
- * 设置缓存键值 直接向缓存中插入值,这会直接覆盖掉给定键之前映射的值
- *
- * @param key 缓存键 不可为空
- * @param obj 缓存值 不可为空
- * @param expireTime 过期时间(单位:毫秒) 可为空
- **/
- public <T extends Object> void set(String key, T obj, Long expireTime) {
- if (StringUtils.isEmpty(key) == true) {
- return;
- }
- if (obj == null) {
- return;
- }
- expireTime = getExpireTime(expireTime);
- ValueOperations<Serializable, Object> 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<CacheProviderService> _listCacheProvider = Lists.newArrayList();
- private static final Lock providerLock = new ReentrantLock();
- /**
- * 初始化缓存提供者 默认优先级:先本地缓存,后分布式缓存
- **/
- private List<CacheProviderService> getCacheProviders() {
- if (_listCacheProvider.size() > 0) {
- return _listCacheProvider;
- }
- //线程安全
- try {
- providerLock.tryLock(1000, TimeUnit.MILLISECONDS);
- if (_listCacheProvider.size() > 0) {
- return _listCacheProvider;
- }
- String isUseCache = ConfigUtil.getConfigVal(AppField.IS_USE_LOCAL_CACHE);
- CacheProviderService cacheProviderService = null;
- //启用本地缓存
- if ("1".equalsIgnoreCase(isUseCache)) {
- _listCacheProvider.add(localCacheService);
- }
- isUseCache = ConfigUtil.getConfigVal(AppField.IS_USE_REDIS_CACHE);
- //启用Redis缓存
- if ("1".equalsIgnoreCase(isUseCache)) {
- _listCacheProvider.add(redisCacheService);
- resetCacheVersion();//设置分布式缓存版本号
- }
- PowerLogger.info("初始化缓存提供者成功,共有" + _listCacheProvider.size() + "个");
- } catch (Exception e) {
- e.printStackTrace();
- _listCacheProvider = Lists.newArrayList();
- PowerLogger.error("初始化缓存提供者发生异常:{}", e);
- } finally {
- providerLock.unlock();
- }
- return _listCacheProvider;
- }
- /**
- * 查询缓存
- *
- * @param key 缓存键 不可为空
- **/
- public <T extends Object> T get(String key) {
- T obj = null;
- //key = generateVerKey(key);//构造带版本的缓存键
- for (CacheProviderService provider : getCacheProviders()) {
- obj = provider.get(key);
- if (obj != null) {
- return obj;
- }
- }
- return obj;
- }
- /**
- * 查询缓存
- *
- * @param key 缓存键 不可为空
- * @param function 如没有缓存,调用该callable函数返回对象 可为空
- **/
- public <T extends Object> T get(String key, Function<String, T> function) {
- T obj = null;
- for (CacheProviderService provider : getCacheProviders()) {
- if (obj == null) {
- obj = provider.get(key, function);
- } else if (function != null && obj != null) {//查询并设置其他缓存提供者程序缓存
- provider.get(key, function);
- }
- //如果callable函数为空 而缓存对象不为空 及时跳出循环并返回
- if (function == null && obj != null) {
- return obj;
- }
- }
- return obj;
- }
- /**
- * 查询缓存
- *
- * @param key 缓存键 不可为空
- * @param function 如没有缓存,调用该callable函数返回对象 可为空
- * @param funcParm function函数的调用参数
- **/
- public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm) {
- T obj = null;
- for (CacheProviderService provider : getCacheProviders()) {
- if (obj == null) {
- obj = provider.get(key, function, funcParm);
- } else if (function != null && obj != null) {//查询并设置其他缓存提供者程序缓存
- provider.get(key, function, funcParm);
- }
- //如果callable函数为空 而缓存对象不为空 及时跳出循环并返回
- if (function == null && obj != null) {
- return obj;
- }
- }
- return obj;
- }
- /**
- * 查询缓存
- *
- * @param key 缓存键 不可为空
- * @param function 如没有缓存,调用该callable函数返回对象 可为空
- * @param expireTime 过期时间(单位:毫秒) 可为空
- **/
- public <T extends Object> T get(String key, Function<String, T> function, long expireTime) {
- T obj = null;
- for (CacheProviderService provider : getCacheProviders()) {
- if (obj == null) {
- obj = provider.get(key, function, expireTime);
- } else if (function != null && obj != null) {//查询并设置其他缓存提供者程序缓存
- provider.get(key, function, expireTime);
- }
- //如果callable函数为空 而缓存对象不为空 及时跳出循环并返回
- if (function == null && obj != null) {
- return obj;
- }
- }
- return obj;
- }
- /**
- * 查询缓存
- *
- * @param key 缓存键 不可为空
- * @param function 如没有缓存,调用该callable函数返回对象 可为空
- * @param funcParm function函数的调用参数
- * @param expireTime 过期时间(单位:毫秒) 可为空
- **/
- public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm, long expireTime) {
- T obj = null;
- for (CacheProviderService provider : getCacheProviders()) {
- if (obj == null) {
- obj = provider.get(key, function, funcParm, expireTime);
- } else if (function != null && obj != null) {//查询并设置其他缓存提供者程序缓存
- provider.get(key, function, funcParm, expireTime);
- }
- //如果callable函数为空 而缓存对象不为空 及时跳出循环并返回
- if (function == null && obj != null) {
- return obj;
- }
- }
- return obj;
- }
- /**
- * 设置缓存键值 直接向缓存中插入或覆盖值
- *
- * @param key 缓存键 不可为空
- * @param obj 缓存值 不可为空
- **/
- public <T extends Object> void set(String key, T obj) {
- //key = generateVerKey(key);//构造带版本的缓存键
- for (CacheProviderService provider : getCacheProviders()) {
- provider.set(key, obj);
- }
- }
- /**
- * 设置缓存键值 直接向缓存中插入或覆盖值
- *
- * @param key 缓存键 不可为空
- * @param obj 缓存值 不可为空
- * @param expireTime 过期时间(单位:毫秒) 可为空
- **/
- public <T extends Object> void set(String key, T obj, Long expireTime) {
- //key = generateVerKey(key);//构造带版本的缓存键
- for (CacheProviderService provider : getCacheProviders()) {
- provider.set(key, obj, expireTime);
- }
- }
- /**
- * 移除缓存
- *
- * @param key 缓存键 不可为空
- **/
- public void remove(String key) {
- //key = generateVerKey(key);//构造带版本的缓存键
- if (StringUtils.isEmpty(key) == true) {
- return;
- }
- for (CacheProviderService provider : getCacheProviders()) {
- provider.remove(key);
- }
- }
- /**
- * 是否存在缓存
- *
- * @param key 缓存键 不可为空
- **/
- public boolean contains(String key) {
- boolean exists = false;
- //key = generateVerKey(key);//构造带版本的缓存键
- if (StringUtils.isEmpty(key) == true) {
- return exists;
- }
- Object obj = get(key);
- if (obj != null) {
- exists = true;
- }
- return exists;
- }
- /**
- * 获取分布式缓存版本号
- **/
- public String getCacheVersion() {
- String version = "";
- boolean isUseCache = checkUseRedisCache();
- //未启用Redis缓存
- if (isUseCache == false) {
- return version;
- }
- version = redisCacheService.get(AppConst.CACHE_VERSION_KEY);
- return version;
- }
- /**
- * 重置分布式缓存版本 如果启用分布式缓存,设置缓存版本
- **/
- public String resetCacheVersion() {
- String version = "";
- boolean isUseCache = checkUseRedisCache();
- //未启用Redis缓存
- if (isUseCache == false) {
- return version;
- }
- //设置缓存版本
- version = String.valueOf(Math.abs(UUID.randomUUID().hashCode()));
- redisCacheService.set(AppConst.CACHE_VERSION_KEY, version);
- return version;
- }
- /**
- * 如果启用分布式缓存,获取缓存版本,重置查询的缓存key,可以实现相对实时的缓存过期控制
- * <p>
- * 如没有启用分布式缓存,缓存key不做修改,直接返回
- **/
- public String generateVerKey(String key) {
- String result = key;
- if (StringUtils.isEmpty(key) == true) {
- return result;
- }
- boolean isUseCache = checkUseRedisCache();
- //没有启用分布式缓存,缓存key不做修改,直接返回
- if (isUseCache == false) {
- return result;
- }
- String version = redisCacheService.get(AppConst.CACHE_VERSION_KEY);
- if (StringUtils.isEmpty(version) == true) {
- return result;
- }
- result = String.format("%s_%s", result, version);
- return result;
- }
- /**
- * 验证是否启用分布式缓存
- **/
- private boolean checkUseRedisCache() {
- boolean isUseCache = false;
- String strIsUseCache = ConfigUtil.getConfigVal(AppField.IS_USE_REDIS_CACHE);
- isUseCache = "1".equalsIgnoreCase(strIsUseCache);
- return isUseCache;
- }
- }
单元测试如下:
- @Test
- public void testCacheVerson() throws Exception {
- String version = cacheBuilder.getCacheVersion();
- System.out.println(String.format("当前缓存版本:%s", version));
- String cacheKey = cacheBuilder.generateVerKey("goods778899");
- GoodsVO goodsVO = new GoodsVO();
- goodsVO.setGoodsId(UUID.randomUUID().toString());
- goodsVO.setCreateTime(new Date());
- goodsVO.setCreateDate(new DateTime(new Date()));
- goodsVO.setGoodsType(1024);
- goodsVO.setGoodsCode("123456789");
- goodsVO.setGoodsName("我的测试商品");
- cacheBuilder.set(cacheKey, goodsVO);
- GoodsVO goodsVO1 = cacheBuilder.get(cacheKey);
- Assert.assertNotNull(goodsVO1);
- version = cacheBuilder.resetCacheVersion();
- System.out.println(String.format("重置后的缓存版本:%s", version));
- cacheKey = cacheBuilder.generateVerKey("goods112233");
- cacheBuilder.set(cacheKey, goodsVO);
- GoodsVO goodsVO2 = cacheBuilder.get(cacheKey);
- Assert.assertNotNull(goodsVO2);
- Assert.assertTrue("两个缓存对象的主键相同", goodsVO1.getGoodsId().equals(goodsVO2.getGoodsId()));
- }
一个满足基本功能的多级缓存系统就好了。
在Spring Boot应用中使用缓存则非常简洁,选择调用上面包装好的缓存接口即可。
- String cacheKey = _cacheBuilder.generateVerKey("com.power.demo.apiservice.impl.getgoodsbyid." + request.getGoodsId());
- GoodsVO goodsVO = _cacheBuilder.get(cacheKey, _goodsService::getGoodsByGoodsId, request.getGoodsId());
到这里Spring Boot业务系统开发中最常用到的ORM,缓存和队列三板斧就介绍完了。
在开发的过程中你会发现,Java真的是非常非常中规中矩的语言,你需要不断折腾并熟悉常见的开源中间件和工具,开源的轮子实在是太丰富,多尝试几个,实践出真知。
Java 的知识面非常广,面试问的涉及也非常广泛,重点包括:Java 基础、Java 并发,JVM、MySQL、数据结构、算法、Spring、微服务、MQ 等等,涉及的知识点何其庞大,所以我们在复习的时候也往往无从下手,今天小编给大家带来一套 Java 面试题,题库非常全面,包括 Java 基础、Java 集合、JVM、Java 并发、Spring全家桶、Redis、MySQL、Dubbo、Netty、MQ 等等,包含 Java 后端知识点 2000 + ,部分如下:
资料获取方式:关注公众号:“程序员白楠楠”获取上述资料
贼厉害,手撸的 SpringBoot 缓存系统,性能杠杠的!的更多相关文章
- 手撸一个SpringBoot的Starter,简单易上手
前言:今天介绍一SpringBoot的Starter,并手写一个自己的Starter,在SpringBoot项目中,有各种的Starter提供给开发者使用,Starter则提供各种API,这样使开发S ...
- 手游服务端框架之使用Guava构建缓存系统
缓存的作用与应用场景 缓存,在项目中的应用非常之广泛.诸如这样的场景,某些对象计算或者获取的代码比较昂贵,并且在程序里你不止一次要用到这些对象,那么,你就应该使用缓存. 缓存跟java的Coucurr ...
- 看了 Spring 官网脚手架真香,也撸一个 SpringBoot DDD 微服务的脚手架!
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 为什么我们要去造轮子? 造轮子的核心目的,是为了解决通用共性问题的凝练和复用. 虽然 ...
- 《Spring 手撸专栏》第 3 章:初显身手,运用设计模式,实现 Bean 的定义、注册、获取
作者:小傅哥 博客:https://bugstack.cn 沉淀.分享.成长,让自己和他人都能有所收获! 一.前言 你是否能预见复杂内容的设计问题? 讲道理,无论产品功能是否复杂,都有很大一部分程序员 ...
- 手撸基于swoole 的分布式框架 实现分布式调用(20)讲
最近看的一个swoole的课程,前段时间被邀请的参与的这个课程 比较有特点跟一定的深度,swoole的实战教程一直也不多,结合swoole构建一个新型框架,最后讲解如何实现分布式RPC的调用. 内容听 ...
- 手撸一个SpringBoot-Starter
1. 简介 通过了解SpringBoot的原理后,我们可以手撸一个spring-boot-starter来加深理解. 1.1 什么是starter spring官网解释 starters是一组方便的依 ...
- 手撸一个springsecurity,了解一下security原理
手撸一个springsecurity,了解一下security原理 转载自:www.javaman.cn 手撸一个springsecurity,了解一下security原理 今天手撸一个简易版本的sp ...
- 《Mybatis 手撸专栏》第7章:SQL执行器的定义和实现
作者:小傅哥 博客:https://bugstack.cn - <手写Mybatis系列> 一.前言 为什么,要读框架源码? 因为手里的业务工程代码太拉胯了!通常作为业务研发,所开发出来的 ...
- SpringBoot缓存之redis--最简单的使用方式
第一步:配置redis 这里使用的是yml类型的配置文件 mybatis: mapper-locations: classpath:mapping/*.xml spring: datasource: ...
随机推荐
- Apache Hudi助力nClouds加速数据交付
1. 概述 在nClouds上,当客户的业务决策取决于对近实时数据的访问时,客户通常会向我们寻求有关数据和分析平台的解决方案.但随着每天创建和收集的数据量都在增加,这使得使用传统技术进行数据分析成为一 ...
- win10 随记
昨天买的台式电脑,今天到了.有点小激动(用了5年的i3笔记本可以稍微休息下了,哈哈) 拿到电脑,和朋友一块,插线...最终连接成功. 记录下过程中的乌龙操作,,, 1.连接好线路后,显示器没反应,(显 ...
- Maven2 ---- 安装及配置
Maven详解(二)------ Maven的安装配置 目录 1.下载 Maven 2.配置 Maven 环境变量 3.查看 Maven 环境变量是否配置成功 4.在 eclipse 中集成 Ma ...
- Messenger实现进程间通信(IPC)
messenger内部也是实现aidl通信,所以可以看做一个轻量级aidl,但相对比较简单.首先开启一个服务并实现一个Handler用来处理消息,在onbind方法中返回IBinder对象,通过Ser ...
- B. Two Fairs 解析(思維、DFS、組合)
Codeforce 1276 B. Two Fairs 解析(思維.DFS.組合) 今天我們來看看CF1276B 題目連結 題目 給一個連通圖,並給兩個點(\(a,b\)),求有多少點對使得:任一路徑 ...
- 【转】Setting up SDL Extension Libraries on Visual Studio 2010 Ultimate
FROM:http://lazyfoo.net/tutorials/SDL/06_extension_libraries_and_loading_other_image_formats/windows ...
- ES6--数组部分基础知识
数组Array的相关方法 1.Array.from()方法 Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象 ...
- Java学习的第一天
1.今天安装了Java操作环境,并运行了helloworld的程序 2.在安装过程中,文件路径丢了,与c++和c之前的差异使我难堪 3.明天准备完成小学期任务以及继续往下学习Java语言读大道至简
- 300万运算/秒 :VoltDB在电信行业基准测试上可线性扩展性能
01 总 体 概 述 VoltDB受到全球电信软件解决方案提供商的信赖,后者将其作为首选内存数据库来驱动他们部署在全球100多家运营商处的任务关键型应用.VoltDB受到青睐的原因在于其性能和功能不仅 ...
- IOCP 模型1
// IOCP.cpp : Defines the entry point for the console application. // // #include "stdafx.h&quo ...