spring boot自定义缓存,使用fifo,lru,lfu算法实现

本文详细介绍了如何在SpringBoot中自定义缓存组件,包括Caching接口、基础缓存类(永久与有效期)、FIFO、LRU和LFU淘汰算法的实现,以及同步和日志功能的配置。通过注解和切面编程,展示了如何利用缓存提高应用性能并控制其复杂性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

概述

缓存是我们在开发中经常用到的,我们最常见的有redis和MemCached,但是这章我们暂时不说这两个中间件。那么我们为什么要用到缓存呢? 就是为了提高效率。

在我们平时运行程序时,程序是在内存中跑起来的,然后程序与内存的交互是非常快的,但是与数据库交互却是不一样,数据库是持久化到硬盘中的,频繁的操作数据库是一个非常耗时的过程,所以就有了缓存的存在,因为他就是在内存中的,既能存储数据,同时避免了频繁读取数据的耗时问题。

但是会有人问,为什么不直接存在内存中

  1. 内存不能持久化,重启服务就没了

  2. 成本高,内存容量本来就是比硬盘小很多,扩展内存的成本会比扩展硬盘高好几倍甚至更高

但是缓存并不能滥用,如果数据量过多就会导致服务器崩溃,大量缓存失效的后果是非常严重的,而且并不是所有数据都可以使用缓存的,需要在合适的场景去使用。

接下来我们是在spring boot内部写个自定义缓存组件,为什么不用redis或MemCached呢

  1. 小部分缓存使用redis是大财小用

  2. 为了让自己更加熟悉缓存和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两种不同的淘汰策略我就不一一演示,因为数据测试起来比较麻烦,有兴趣可以自己试试

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

nssnail

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值