1、缓存基础算法
FIFO(First In First Out),先进先出,和OS里的FIFO思路相同,如果一个数据最先进入缓存中,当缓存满的时候,应当把最先进入缓存的数据给移除掉。
LFU(Least Frequently Used),最不经常使用,如果一个数据在最近一段时间内使用次数很少,那么在将来一段时间内被使用的可能性也很小。
2、接口定义
简单定义缓存接口,大致可以抽象如下:
package com.power.demo.cache.contract;
import java.util.function.Function;
/**
* 缓存提供者接口
**/
public interface CacheProviderService {
/**
* 查询缓存
* @param key 缓存键 不可为空
**/
T get(String key);
/**
* 查询缓存
* @param key 缓存键 不可为空
* @param function 如没有缓存,调用该callable函数返回对象 可为空
**/
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 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 缓存值 不可为空
**/
void set(String key, T obj);
/**
* 设置缓存键值
* @param key 缓存键 不可为空
* @param obj 缓存值 不可为空
* @param expireTime 过期时间(单位:毫秒) 可为空
**/
void set(String key, T obj, Long expireTime);
/**
* 移除缓存
* @param key 缓存键 不可为空
**/
void remove(String key);
/**
* 是否存在缓存
* @param key 缓存键 不可为空
**/
boolean contains(String key);
}
注意,这里列出的只是常见缓存功能接口,一些在特殊场景下用到的统计类的接口、分布式锁、自增(减)等功能不在讨论范围之内。
接口定义好了,下面就要实现缓存提供者程序了。按照存储类型的不同,本文简单实现最常用的两种缓存提供者:本地缓存和分布式缓存。
二、本地缓存
1、什么是Guava
2、添加依赖
com.google.guava
guava
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 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<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 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 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);
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的数据类型:
[Keys:非二进制安全的字符类型( not binary-safe strings ),由于key不是binary safe的字符串,所以像“my key”和“mykey\n”这样包含空格和换行的key是不允许的。
Values:Strings、Hash、Lists、 Sets、 Sorted sets。考虑到Redis单线程操作模式,Value的粒度不应该过大,缓存的值越大,越容易造成阻塞和排队。]( )
同时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
接着定义一个继承自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();
// }
// };
// }
}
而redisTemplate方法,则主要是设置Redis模板类,比如键和值的序列化器(从这里可以看出,Redis的键值对必须可序列化)、redis连接工厂等。
**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 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<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 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 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<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;
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
![img](https://img-blog.csdnimg.cn/img_convert/61b8fe130024011e8038377a822c50c3.jpeg)
最后
现在正是金三银四的春招高潮,前阵子小编一直在搭建自己的网站,并整理了全套的**【一线互联网大厂Java核心面试题库+解析】:包括Java基础、异常、集合、并发编程、JVM、Spring全家桶、MyBatis、Redis、数据库、中间件MQ、Dubbo、Linux、Tomcat、ZooKeeper、Netty等等**
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》,点击传送门即可获取!
y) {
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;
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。[外链图片转存中…(img-d0P5Nx0K-1712515144606)]
[外链图片转存中…(img-j2zJDDXU-1712515144606)]
[外链图片转存中…(img-N5jsknKQ-1712515144607)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
![img](https://img-blog.csdnimg.cn/img_convert/61b8fe130024011e8038377a822c50c3.jpeg)
最后
现在正是金三银四的春招高潮,前阵子小编一直在搭建自己的网站,并整理了全套的**【一线互联网大厂Java核心面试题库+解析】:包括Java基础、异常、集合、并发编程、JVM、Spring全家桶、MyBatis、Redis、数据库、中间件MQ、Dubbo、Linux、Tomcat、ZooKeeper、Netty等等**
[外链图片转存中…(img-uaJ3yRcq-1712515144607)]
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》,点击传送门即可获取!