目录
概述
缓存是我们在开发中经常用到的,我们最常见的有redis和MemCached,但是这章我们暂时不说这两个中间件。那么我们为什么要用到缓存呢? 就是为了提高效率。
在我们平时运行程序时,程序是在内存中跑起来的,然后程序与内存的交互是非常快的,但是与数据库交互却是不一样,数据库是持久化到硬盘中的,频繁的操作数据库是一个非常耗时的过程,所以就有了缓存的存在,因为他就是在内存中的,既能存储数据,同时避免了频繁读取数据的耗时问题。
但是会有人问,为什么不直接存在内存中
-
内存不能持久化,重启服务就没了
-
成本高,内存容量本来就是比硬盘小很多,扩展内存的成本会比扩展硬盘高好几倍甚至更高
但是缓存并不能滥用,如果数据量过多就会导致服务器崩溃,大量缓存失效的后果是非常严重的,而且并不是所有数据都可以使用缓存的,需要在合适的场景去使用。
接下来我们是在spring boot内部写个自定义缓存组件,为什么不用redis或MemCached呢
-
小部分缓存使用redis是大财小用
-
为了让自己更加熟悉缓存和spring
注:在内部使用缓存有个比较大的缺点,就是服务耦合,如果业务服务器宕机,缓存就是失效,但是这章节是重点为了提升自己
功能实现
项目结构
以下为项目的结构,现在我们来开始尝试下吧
1.创建Cahce接口
此为基础接口,任何缓存都要基于此接口实现
public interface Cache {
/**
* 存储数据
* @param key
* @param value
*/
void putObject(Object key, Object value);
/**
* 取数据
* @param key
* @return
*/
Object getObject(Object key);
/**
* 基于key移除元素
* @param key
* @return
*/
Object removeObject(Object key);
}
2.创建基础缓存类
基础缓存类,永久缓存,一般我们缓存使用hashmap进行封装
public class PerpetualCache implements Cache {
private Map<Object, Object> cache=
new HashMap<>();
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return cache.get(key);
}
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}
@Override
public String toString() {
return cache.toString();
}
}
有效期缓存,使用ExpiryMap,自定义实现,继承与hashmap
public class ExpiryCache implements Cache {
private Map<Object, Object> cache=new ExpiryMap<>();
//默认过期时间
public ExpiryCache(){
cache=new ExpiryMap<>(CacheConstant.DEFAULT_TIME);
}
//自定义过期时间
public ExpiryCache(Long expiryTime){
this.cache=new ExpiryMap<>(expiryTime);
}
@Override
public void putObject(Object key, Object value) {
cache.put(key, value);
}
@Override
public Object getObject(Object key) {
return cache.get(key);
}
@Override
public Object removeObject(Object key) {
return cache.remove(key);
}
@Override
public String toString() {
return cache.toString();
}
}
ExpiryMap实现,有兴趣可以自己研究下,这个我也是参考到其他博客的,毕竟重点不是这里,我就不多讲解了
public class ExpiryMap<K, V> extends HashMap<K, V> implements Serializable {
private static final long serialVersionUID = 1802035957266164823L;
/**
* default expiry time 2m
*/
private long EXPIRY = 1000 * 60 * 2;
private HashMap<K, Long> expiryMap = new HashMap<>();
public ExpiryMap(){
super();
}
public ExpiryMap(long defaultExpiryTime){
this(1 << 4, defaultExpiryTime);
}
public ExpiryMap(int initialCapacity, long defaultExpiryTime){
super(initialCapacity);
this.EXPIRY = defaultExpiryTime;
}
public V put(K key, V value) {
expiryMap.put(key, System.currentTimeMillis() + EXPIRY);
return super.put(key, value);
}
public boolean containsKey(Object key) {
return !checkExpiry(key, true) && super.containsKey(key);
}
public V put(K key, V value, long expiryTime) {
expiryMap.put(key, System.currentTimeMillis() + expiryTime);
return super.put(key, value);
}
public int size() {
return entrySet().size();
}
public boolean isEmpty() {
return entrySet().size() == 0;
}
public boolean containsValue(Object value) {
if (value == null) return Boolean.FALSE;
Set<Entry<K, V>> set = super.entrySet();
Iterator<Entry<K, V>> iterator = set.iterator();
while (iterator.hasNext()) {
Entry<K, V> entry = iterator.next();
if(value.equals(entry.getValue())){
if(checkExpiry(entry.getKey(), false)) {
iterator.remove();
return Boolean.FALSE;
}else return Boolean.TRUE;
}
}
return Boolean.FALSE;
}
public Collection<V> values() {
Collection<V> values = super.values();
if(values == null || values.size() < 1) return values;
Iterator<V> iterator = values.iterator();
while (iterator.hasNext()) {
V next = iterator.next();
if(!containsValue(next)) iterator.remove();
}
return values;
}
public V get(Object key) {
if (key == null)
return null;
if(checkExpiry(key, true))
return null;
return super.get(key);
}
public V delete(Object key){
if (key == null)
return null;
expiryMap.remove(key);
return super.remove(key);
}
public Object isInvalid(Object key) {
if (key == null)
return null;
if(!expiryMap.containsKey(key)){
return null;
}
long expiryTime = expiryMap.get(key);
boolean flag = System.currentTimeMillis() > expiryTime;
if(flag){
super.remove(key);
expiryMap.remove(key);
return -1;
}
return super.get(key);
}
public void putAll(Map<? extends K, ? extends V> m) {
for (Entry<? extends K, ? extends V> e : m.entrySet())
expiryMap.put(e.getKey(), System.currentTimeMillis() + EXPIRY);
super.putAll(m);
}
public Set<Entry<K,V>> entrySet() {
Set<Entry<K, V>> set = super.entrySet();
Iterator<Entry<K, V>> iterator = set.iterator();
while (iterator.hasNext()) {
Entry<K, V> entry = iterator.next();
if(checkExpiry(entry.getKey(), false)) iterator.remove();
}
return set;
}
private boolean checkExpiry(Object key, boolean isRemoveSuper){
if(!expiryMap.containsKey(key)){
return Boolean.FALSE;
}
long expiryTime = expiryMap.get(key);
boolean flag = System.currentTimeMillis() > expiryTime;
if(flag){
if(isRemoveSuper)
super.remove(key);
expiryMap.remove(key);
}
return flag;
}
}
3.实现FIFO,LFU,LRU三种淘汰算法
3.1 FIFO(先进入先淘汰)算法
最早进入的最先被淘汰的策略
一样实现cache接口
,使用队列来存储缓存,设置最大容量maxCap
,当达到最大容量的时候将先进来的缓存移出队列
public class FifoCache implements Cache {
/**组合Cache接口*/
private Cache cache;
/**借助此对象存储key的添加顺序*/
private Deque<Object> keyOrderList;
/**最大容量*/
private int maxCap;
public FifoCache(int maxCap, Cache cache) {
this.maxCap=maxCap;
this.cache=cache;
keyOrderList=new LinkedList<Object>();
}
@Override
public void putObject(Object key, Object value) {
//1.记录key
keyOrderList.addLast(key);
//2.移除老元素
if(keyOrderList.size()>maxCap) {
Object oldKey=
keyOrderList.removeFirst();
cache.removeObject(oldKey);
}
//3.放新元素
cache.putObject(key, value);
}
@Override
public Object getObject(Object key) {
return cache.getObject(key);
}
@Override
public Object removeObject(Object key) {
return cache.removeObject(key);
}
@Override
public String toString() {
return cache.toString();
}
}
3.2 LRU(最近最少使用)算法
最近最少使用的缓存被淘汰
一样定义一个队列和最大容量maxCap
,在访问的时候,如果该缓存存在于队列中,就讲他放在队列的最前面,那么最后面的缓存必然是最少访问的
public class LruCache implements Cache {
/**组合Cache接口*/
private Cache cache;
/**借助此对象存储key的添加顺序*/
private Deque<Object> keyOrderList;
/**最大容量*/
private int maxCap;
public LruCache(int maxCap, Cache cache) {
this.maxCap=maxCap;
this.cache=cache;
keyOrderList=new LinkedList<Object>();
}
@Override
public void putObject(Object key, Object value) {
//1.记录key
keyOrderList.addLast(key);
//2.移除老元素
if(keyOrderList.size()>maxCap) {
Object oldKey=
keyOrderList.removeFirst();
cache.removeObject(oldKey);
}
//3.放新元素
cache.putObject(key, value);
}
@Override
public Object getObject(Object key) {
Object obj=cache.getObject(key);
if(obj!=null) {
keyOrderList.remove(key);
keyOrderList.addLast(key);
}
return obj;
}
@Override
public Object removeObject(Object key) {
return cache.removeObject(key);
}
@Override
public String toString() {
return cache.toString();
}
}
3.3 LFU(历史访问率最低)算法
定义list集合,和最大容量maxCap
,内部类HitRate用来存储key值的访问率,如果达到最大容量,则删除HitRate中hitCount最小的数值
public class LfuCache implements Cache {
private Cache cache;
/**借助此对象存储key的添加顺序*/
private List<Object> keyOrderList;
private Map<Object, HitRate> count ;
/**组合Cache接口*/
/**最大容量*/
private int maxCap;
public LfuCache(int maxCap, Cache cache) {
this.maxCap=maxCap;
this.cache=cache;
count = new HashMap<>();
keyOrderList=new ArrayList<>();
}
//移除元素
private Object removeElement() {
HitRate hr = Collections.min(count.values());
keyOrderList.remove(hr.key);
count.remove(hr.key);
return cache.removeObject(hr.key);
}
//更新访问元素状态
private void addHitCount(Object key) {
HitRate hitRate = count.get(key);
hitRate.hitCount = hitRate.hitCount + 1;
hitRate.lastTime = System.nanoTime();
}
@Override
public void putObject(Object key, Object value) {
Object v = cache.getObject(key);
if (v == null) {
if (keyOrderList.size() == maxCap) {
removeElement();
}
count.put(key, new HitRate(key, 1, System.nanoTime()));
} else {
addHitCount(key);
}
keyOrderList.add(key);
cache.putObject(key, value);
}
@Override
public Object getObject(Object key) {
Object value = cache.getObject(key);
if (value != null) {
addHitCount(key);
return value;
}
return removeElement();
}
@Override
public Object removeObject(Object key) {
Object value = cache.getObject(key);
if(value==null){
return null;
}
return removeElement();
}
//内部类
class HitRate implements Comparable<HitRate> {
private Object key;
private int hitCount;
private long lastTime;
private HitRate(Object key, int hitCount, long lastTime) {
this.key = key;
this.hitCount = hitCount;
this.lastTime = lastTime;
}
@Override
public int compareTo(HitRate o) {
int compare = Integer.compare(this.hitCount, o.hitCount);
return compare == 0 ? Long.compare(this.lastTime, o.lastTime) : compare;
}
}
@Override
public String toString() {
return cache.toString();
}
}
4.定义缓存同步,日志
4.1定义同步缓存
public class SynchronizedCache implements Cache {
private Cache cache;
public SynchronizedCache(Cache cache) {
this.cache=cache;
}
@Override
public synchronized void putObject(Object key, Object value) {
cache.putObject(key, value);
}
public synchronized Object getObject(Object key) {
return cache.getObject(key);
}
@Override
public synchronized Object removeObject(Object key) {
return cache.removeObject(key);
}
}
4.2 定义日志缓存
这里仅仅打印日志,实际项目中可以将其持久化,或者写在内部缓存中
public class LogCache implements Cache {
private Cache cache;
/**请求次数*/
private int requests;
/**命中次数*/
private int hit;
public LogCache(Cache cache) {
this.cache=cache;
}
@Override
public void putObject(Object key,
Object value) {
cache.putObject(key, value);
}
@Override
public Object getObject(Object key) {
//1.记录请求次数
requests++;
//2.从cache取数据
Object obj=cache.getObject(key);
//3.记录命中次数
if(obj!=null)hit++;
//4.输出命中率
System.out.println("request hits "+hit*1.0/requests);
return obj;
}
@Override
public Object removeObject(Object key) {
return cache.removeObject(key);
}
}
5.定义缓存注解,缓存切面等核心实现
5.1 定义缓存注解
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Cacheable {
//缓存名
@AliasFor("cacheName")
String value() default "";
@AliasFor("value")
String cacheName() default "";
//缓存key
String key() default "";
//缓存策略,有fifo,lru,lfu三种,默认fifo
String type() default CacheConstant.FIFO;
//缓存容量,默认32
int capacity() default CacheConstant.MAX_CAP;
//是否开启日志
boolean log() default false;
//是否同步
boolean sync() default false;
//是否开启有效期缓存
boolean expire() default false;
//设置有效时长,默认五分钟
long expireTime() default CacheConstant.DEFAULT_TIME;
}
5.2 定义缓存常量
public class CacheConstant {
//默认初始容量
public static final int MAX_CAP = 32;
//一分钟
public static final long ONE_MINUTES = 1000 * 60L;
//十分钟
public static final long TEN_MINUTES = ONE_MINUTES * 10L;
//半小时
public static final long HALF_AN_HOUR = ONE_MINUTES * 30L;
//一小时
public static final long ONE_HOUR = ONE_MINUTES * 60L;
//六小时
public static final long SIX_HOUR = ONE_HOUR * 6L;
//一天
public static final long ONE_DAY = ONE_HOUR * 12L;
//默认时间(五分钟)
public static final long DEFAULT_TIME = ONE_MINUTES * 5L;
public static final String FIFO="fifo";
public static final String LRU="lru";
public static final String LFU="lfu";
}
5.3 定义缓存异常类
public class CacheException extends RuntimeException {
private static final long serialVersionUID = -234204541003512526L;
public CacheException() {
super();
}
public CacheException(String message) {
super(message);
}
public CacheException(Throwable cause) {
super(cause);
}
}
5.4 定义Vo缓存实体
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
@Accessors(chain = true)
public class CacheVo {
private String cacheName;
private String key;
private String type;
private Integer capacity;
private Boolean log;
private Boolean sync;
private Boolean expire;
private Long ExpireTime;
}
5.5 定义缓存工厂类
缓存定义采用装饰器模式
@Component
public class CacheFactory {
private final ConcurrentHashMap<String, Cache> cachePools = new ConcurrentHashMap<>();
//生产缓存
public Cache produce(CacheVo cacheVo) {
if(cachePools.containsKey(cacheVo.getCacheName())){
return cachePools.get(cacheVo.getCacheName());
}
//定义缓存
Cache cache=null;
//有效期缓存还是永久缓存
cache=cacheVo.getExpire()?new ExpiryCache(cacheVo.getExpireTime()):new PerpetualCache();
//缓存淘汰策略
switch (cacheVo.getType().toLowerCase()){
case CacheConstant.FIFO:
cache=new FifoCache(cacheVo.getCapacity(),cache);break;
case CacheConstant.LRU:
cache=new LruCache(cacheVo.getCapacity(),cache);break;
case CacheConstant.LFU:
cache=new LfuCache(cacheVo.getCapacity(),cache);break;
default:
throw new CacheException("请输入fifo,lru,lfu其中一种策略");
}
//是否开启日志
if(cacheVo.getLog())cache=new LogCache(cache);
//是否开启同步
if(cacheVo.getSync())cache=new SynchronizedCache(cache);
cachePools.put(cacheVo.getCacheName(), cache);
return cache;
}
}
5.6 定义缓存切面
@Aspect
@Component
@Slf4j
public class CacheAspect {
@Autowired
private CacheFactory cacheFactory;
//拦截Cacheable注解
@Pointcut("@annotation( cgncopm.annotation.Cacheable)")
public void doPointCut() {
}
@Around("doPointCut()")
public Object around(ProceedingJoinPoint jp) throws Throwable {
try {
//获取缓存
Object cache = getCache(jp);
//如果缓存不为空直接返回值,不执行目标方法(jp.proceed())
if (cache != null) {
return cache;
} else {
//如果缓存为空,则执行目标方法,拿到方法返回值设置缓存
Object result = jp.proceed();
setCache(jp, result);
return result;
}
} catch (Throwable e) {
log.error(e.getMessage());
throw e;
}
}
//获取缓存
private Object getCache(ProceedingJoinPoint jp) {
CacheVo cacheVo = getCacheItem(jp);
Cache cache = cacheFactory.produce(cacheVo);
return cache.getObject(cacheVo.getKey());
}
//设置缓存
private void setCache(ProceedingJoinPoint jp, Object res) throws NoSuchFieldException {
CacheVo cacheVo = getCacheItem(jp);
Cache cache = cacheFactory.produce(cacheVo);
cache.putObject(cacheVo.getKey(),res);
}
//如果参数中带有#,则将方法参数的值作为缓存key,否则为常量key
//例如test(String Id),如果设置的key为Id,则缓存的key为Id,如果设置的key为#Id,则缓存的key为Id的值
private CacheVo getCacheItem(ProceedingJoinPoint jp) {
Class<?> targetCls = jp.getTarget().getClass();
String group = targetCls.getName();
MethodSignature ms = (MethodSignature) jp.getSignature();
Method method = ms.getMethod();
Cacheable annotation = method.getAnnotation(Cacheable.class);
String cacheName = annotation.cacheName();
String key = annotation.key();
if (StringUtils.isEmpty(cacheName)) {
cacheName = group;
}
String fieldName = "";
boolean iSField = key.contains("#");
if (iSField) {
fieldName = key.replaceAll("#", "");
}
Object[] args = jp.getArgs();
if (args.length == 0 || StringUtils.isEmpty(key)) {
key = method.getName();
}
if (iSField) {
String[] parameterNames = ms.getParameterNames();
int index = Arrays.binarySearch(parameterNames, fieldName);
Object object = args[index];
key = String.valueOf(object);
}
return CacheVo.builder()
.cacheName(cacheName)
.key(key)
.capacity(annotation.capacity())
.type(annotation.type())
.log(annotation.log())
.sync(annotation.sync())
.expire(annotation.expire())
.ExpireTime(annotation.expireTime())
.build();
}
}
6.测试缓存
6.1 定义测试实体
@Data
@AllArgsConstructor
public class CacheItem<T> {
private T data;
private long executeTime;
}
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String name;
}
6.2 定义测试业务
public interface TestService {
User getList(int id);
}
@Service
public class TestServiceImpl implements TestService {
//设置缓存名为user,key为id的值,最大容量为3,不开启有效缓存,默认FIFO淘汰策略
@Override
@Cacheable(cacheName = "user",key = "#id",capacity = 3)
public User getList(int id) {
User user = null;
switch (id) {
case 1:
user = new User(id, "张三");
break;
case 2:
user = new User(id, "李四");
break;
case 3:
user = new User(id, "王五");
break;
case 4:
user = new User(id, "赵六");
break;
}
try {
//模拟延时处理
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
return user;
}
}
6.3 定义测试接口
@RestController
@RequestMapping("/test")
public class TestController {
@Autowired
private TestService testService;
@RequestMapping("/get")
public SysResult getList(int id){
Long l1=System.currentTimeMillis();
User user = testService.getList(id);
Long l2=System.currentTimeMillis();
return new SysResult(new CacheItem<>(user,l2-l1));
}
}
6.4 测试
业务层已模拟5秒延时
- 第一次获取id为1的用户
-
第二次获取id为1的用户
-
获取了id为2,3,4的用户后再次获取id为1的用户,时间再次变回5s,因为缓存容量超过三个,先进去的缓存被淘汰
-
-
开启日志,每次请求时打印缓存命中率
@Cacheable(cacheName = "user",key = "#id",capacity = 3,log = true)
有效期缓存,同步缓存,还有LFU,LRU两种不同的淘汰策略我就不一一演示,因为数据测试起来比较麻烦,有兴趣可以自己试试