ehcache缓存原理_贼厉害,手撸的 SpringBoot缓存系统,性能杠杠的!

b540fe7010261f6b2f989d6bb84e2347.png

缓存是最直接有效提升系统性能的手段之一。个人认为用好用对缓存是优秀程序员的必备基本素质。

本文结合实际开发经验,从简单概念原理和代码入手,一步一步搭建一个简单的二级缓存系统。

一、通用缓存接口

1、缓存基础算法

FIFO(First In First Out),先进先出,和OS里的FIFO思路相同,如果一个数据最先进入缓存中,当缓存满的时候,应当把最先进入缓存的数据给移除掉。

LFU(Least Frequently Used),最不经常使用,如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小。

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、实现接口

/*  * 本地缓存提供者服务 (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”和“mykeyn”这样包含空格和换行的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吗?),见如下效果:

7fb84771cf65b04b470b460b3bffcc3d.png

而值序列化器则要复杂的多,很多人推荐使用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真的是非常非常中规中矩的语言,你需要不断折腾并熟悉常见的开源中间件和工具,开源的轮子实在是太丰富,多尝试几个,实践出真知。

预告一下,后面的文章我将继续学习分享介绍常用中间件和工具,如定时任务,MongoDB,ES,分布式文件系统以及各种实用工具。总之Java下工具链非常完备,我们要给自己更多动力。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/506354.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

angular 字符串转换成数字_Angular日期在TypeScript中格式化转换应用

组件模板中我们常常使用Angular内置管道DatePipe将其日期格式化&#xff0c;如同{{ startTime | date:yyyy-MM-dd }}这般操作&#xff0c;即可转换为字符串格式。那么在TS文件中我们也可以通过DatePipe或formatDate方法将其格式化。Angular日期在TypeScript中格式化转换应用​w…

python罗马数字转换,Python3.5实现的罗马数字转换成整数功能示例

本文实例讲述了Python3.5实现的罗马数字转换成整数功能。分享给大家供大家参考&#xff0c;具体如下&#xff1a;问题概述&#xff1a;给定一个罗马数字 &#xff0c;将罗马数字转换成整数。如罗马数字I&#xff0c;II&#xff0c;III&#xff0c;IV&#xff0c;V分别代表数字 …

js在一个指定元素前添加内容_WebAR开发指南(1)---使用AR.js实现第一个WebAR demo...

前面有一篇文章万字干货介绍WebAR的实现与应用 分析了目前流行的WebAR框架并简单的介绍一些实现方法&#xff0c;这个专栏我们具体的来通过一些框架实现WebAR效果。关于AR.jsAR.js是一个轻量级的增强现实类JavaScript库&#xff0c;支持基于标记和位置的增强现实。开发人员可以…

python 最小二乘回归 高斯核_从简单数学建模开始:08最小二乘准则的应用(附python代码)...

模型拟合一般来说有这么三种&#xff1a;切比雪夫近似准则极小化绝对偏差之和最小二乘准则这几个原则各有各的适用范围。其中最小二乘准则是比较容易计算的。接下来我将简要的介绍最小二乘准则以及举例说明如何用python实现。最小二乘准则定义&#xff1a;给定某种函数类型 和 …

oracle 时间集合,oracle 日期函数集合(集中版本)第2/2页

oracle 日期函数集合(集中版本)第2/2页更新时间&#xff1a;2009年06月16日 23:45:55 作者&#xff1a;oracle 日期函数网上已经有了不少&#xff0c;特我们跟集中一下&#xff0c;免得大家麻烦。一、 常用日期数据格式1.Y或YY或YYY 年的最后一位&#xff0c;两位或三位SQL&g…

中缀表达式转后缀表达式两位数_再见,正则表达式!

从一段指定的字符串中&#xff0c;取得期望的数据&#xff0c;正常人都会想到正则表达式吧&#xff1f;写过正则表达式的人都知道&#xff0c;正则表达式入门不难&#xff0c;写起来也容易。但是正则表达式几乎没有可读性可言&#xff0c;维护起来&#xff0c;真的会让人抓狂&a…

php 腾讯云 文字识别_腾讯科技(成都)有限公司

成都IT内推圈是由IT行业老兵组建的民间组织&#xff0c;我们希望能为广大IT同仁提供力所能及的帮助&#xff0c;我们不仅提供靠谱的职位&#xff0c;我们更是有温度的圈子&#xff01;为了帮助更多的朋友寻找到靠谱的内推职位&#xff0c;老农在此号召大家&#xff1a;1. 发布职…

oracle插入性能优化,Oracle- insert性能优化

看见朋友导入数据&#xff0c;花了很长时间都没完成&#xff01;其实有很多快速的方法&#xff0c;整理下&#xff01;向表中插入数据有很多办法,但是方法不同&#xff0c;性能差别很大.----1.原始语句drop table t1 purge;create table t1(sid number,sname varchar2(20)) tab…

winscp 自动断开无法连接_winscp教程,winscp教程,看完就学会的winscp教程

作为一个站长&#xff0c;当你的网站流量逐渐变大时&#xff0c;就会发现目前的主机无法满足正常需要&#xff0c;就要更换更高一级的云主机或VPS主机&#xff0c;新手就需要使用WinSCP和Putty&#xff0c;来管理主机。IIS7服务器管理工具可以批量管理、定时上传下载、同步操作…

oracle客户端数据恢复,AnyBackup-Oracle 数据恢复任务恢复到异客户端失败,并提示错误:数据源为空,请检查恢复环境是否授权...

关键字Oracle、数据源适用产品AnyBackup Express 7.0.6 - 7.0.8AnyBackup CDM 7.0.6 - 7.0.8问题描述登录 AnyBackup 管理控制台&#xff0c;依次点击定时数据保护 > 数据恢复 > 新建&#xff0c;新建 Oracle 数据恢复任务&#xff0c;选择恢复数据到异客户端&#xff0c…

linux祖先进程,Linux下的几种特殊进程

1、Linux的登录环境Linux是一个多任务多用户的操作系统&#xff0c;其设计初衷: 就是要达成多用户同时使用单个计算机大的任务。多用户&#xff1a;早期计算机资源紧张&#xff0c;为了让更多的人都可以使用。多任务&#xff1a;服务于多用户&#xff0c;同时提高计算机的吞吐量…

@select注解_SSM框架(十三):Spring框架中的IoC(3)新注解,完全摆脱xml文件

spring中的新注解1、Configuration作用&#xff1a;指定当前类是一个配置类细节&#xff1a;当配置类作为AnnotationConfigApplicationContext对象创建的参数时&#xff0c;该注解可以不写。2、ComponentScan作用&#xff1a;用于通过注解指定spring在创建容器时要扫描的包属性…

element ui 多个子组件_ElementUI 技术揭秘(2) 组件库的整体设计

需求分析当我们去实现一个组件库的时候&#xff0c;并不会一上来就撸码&#xff0c;而是把它当做产品一样&#xff0c;思考一下我们的组件库的需求。那么对于 element-ui&#xff0c;除了基于 Vue.js 技术栈开发组件&#xff0c;它还有哪些方面的需求呢。丰富的 feature&#x…

arraylist线程安全吗_Java的线程安全、单例模式、JVM内存结构等知识梳理

java技术总结知其然&#xff0c;不知其所以然 &#xff01;在技术的海洋里&#xff0c;遨游&#xff01;做一个积极的人编码、改bug、提升自己我有一个乐园&#xff0c;面向编程&#xff0c;春暖花开&#xff01;本篇以一些问题开头&#xff0c;请先不看答案&#xff0c;自己思…

【GitHub精选项目】抖音/ TikTok 视频下载:TikTokDownloader 操作指南

前言 本文为大家带来的是 JoeanAmier 开发的 TikTokDownloader 项目&#xff0c;这是一个高效的下载 抖音/ TikTok 视频的开源工具。特别适合用户们保存他们喜欢的视频或分享给其他人。 TikTokDownloader 是一个专门设计用于下载 TikTok 视频的工具&#xff0c;旨在为用户提供一…

python的界面文字翻译_一个把网站全英文转成中文的方法,让你轻松看懂python官网...

近日&#xff0c;在看python官网&#xff08;英文网站&#xff09;时&#xff0c;使用了有道翻译工具查看了几个专有名词。无意间&#xff0c;有了是不是可以输入网站地址进行翻译的想法&#xff0c;翻译后的网站地址点击进去后&#xff0c;又是一个什么情况呢&#xff1f;小编…

drools规则引擎技术指南_物联网规则引擎技术

物联网应用程序设计与典型的IT解决方案大不相同&#xff0c;因为它将物理操作技术&#xff08;OT&#xff09;与传感器、致动器和通信设备连接起来&#xff0c;并将数字信息技术&#xff08;IT&#xff09;与数据、分析和工作流连接起来。在企业环境中&#xff0c;物联网非常复…

linux 2G内存够用吗,不管2G还是6G运存,为什么使用手机时都会先占用一半?

原标题&#xff1a;不管2G还是6G运存&#xff0c;为什么使用手机时都会先占用一半&#xff1f;以前安卓手机最大的缺陷就是运存问题&#xff0c;手机玩着玩着突然就运行内存不足了&#xff0c;为了运行更流畅手机运存从1G到2G…到8G&#xff0c;手机运存越来越大。但大家有没有…

c++ 写x64汇编 5参数_怀念9年前用纯C和汇编写的入侵检测软件

在翻出12年前用C#写的自我管理软件之后&#xff0c;进一步激发了本猫的怀旧情怀。上一篇在此:竟然无意间翻出12年前自己用C#写的程序这不&#xff0c;昨天竟然又找出2010年写的一款Windows系统入侵检测及防御小工具&#xff0c;当时命名是:NtInfoGuy!对于Windows的内部&#xf…

python怎么导入os模块_python之os模块

在自动化测试中&#xff0c;经常需要查找操作文件&#xff0c;比如说查找配置文件&#xff08;从而读取配置文件的信息&#xff09;&#xff0c;查找测试报告&#xff08;从而发送测试报告邮件&#xff09;&#xff0c;经常要对大量文件和大量路径进行操作&#xff0c;这就依赖…