前言

之前也用过一些缓存中间件,框架,也想着自己是不是也能用Java写一个出来,于是就有了这个想法,打算在写的过程中同步进行总结。

源码:weloe/Java-Distributed-Cache (github.com)

本篇代码:

Java-Distributed-Cache/src/main/java/com/weloe/cache/outstrategy at master · weloe/Java-Distributed-Cache (github.com)

Java-Distributed-Cache/src/test/java/com/weloe/cache/outstrategy at master · weloe/Java-Distributed-Cache (github.com)

我们可以想想几个问题,什么是缓存?为什么需要缓存?

什么是缓存?将之前请求的数据暂存,遇到同样的请求/状况直接返回,这就是缓存。

为什么需要?同样的情况下,直接返回数据,无需其他操作,能加快服务器反应速度,减轻服务器压力。

那么缓存怎么存?简单的缓存为键值对,可以用Map存储。这就完了吗?如果我们一直往Map中存储数据,占用的内存会越来越大,这时候怎么办?

这就是本篇需要解决的问题。

要使用缓存,就必然会面临到缓存使用空间达到上限的问题,这个时候就需要从已有的缓存数据中淘汰一部分去维持缓存的可用性。

LRU

力扣上的相关题 https://leetcode.cn/problems/lru-cache/

LRU,缓存淘汰算法,最近最少使用(Least Recently Used),就是一种选择淘汰数据的策略

原理:为最近被访问的数据进行缓存,淘汰不常被访问的数据。

也就是说我们认为最近使用过的数据应该是有用的,很久都没用过的数据应该是无用的,内存满了就优先删那些很久没用过的数据。

举一个我们最常见的例子,手机可用把软件放到后台运行,比如我们先后打开了日历,设置,闹钟。后台的顺序是 日历->设置->闹钟,如果这台手机只能打开三个应用,再打开 应用商城 ,后台的顺序会变成 应用商城->日历->设置。

从这个案例可以知道LRU的主要两个操作的具体思路,一个数据结构存值,一个数据结构存储后台顺序。

缓存一般以key,value形式存储,因此选择map存储,而存储顺序的数据结构由于要不断改动节点顺序,选择双向链表

public class LRUCache<K, V> implements CacheStrategy<K, V> {
private Map<K, V> map;
private int capacity;
private Deque<K> queue;
private Callback callback; public LRUCache(int capacity) {
this.capacity = capacity;
this.map = new HashMap();
this.queue = new LinkedList();
}
}

put(key,value)

如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。

   	public void put(int key, int value) {
if (map.containsKey(key)) {
queue.remove(key);
}
queue.addFirst(key);
map.put(key, value); // 缓存达到上限
if (queue.size() > capacity) { // 移除
K last = queue.removeLast();
V removeValue = map.remove(last); // 回调
if (callback != null) {
callback.callback(last, removeValue);
}
}
return value; }

get(key)

如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1

    public V get(K key) {
// 如果已经缓存过该数据
if (map.containsKey(key)) {
queue.remove(key);
queue.addFirst(key);
return map.get(key);
}
return null;
}

弊端,容易出现缓存污染问题

(k1,v1) (k2,v2),(k3,v3),(k4,v4)

(k2,v2),(k4,v4),(k1,v1),(k3,v3)

LRU-K

LRU-K算法是对LRU算法的改进,将原先进入缓存队列的评判标准从访问一次改为访问K次。

LRU-K算法有两个队列,一个是缓存队列,一个是数据访问历史队列。当访问一个数据时,首先先在访问历史队列中累加访问次数,当历史访问记录超过K次后,才将数据缓存至缓存队列,从而避免缓存队列被污染。同时访问历史队列中的数据可以按照LRU的规则进行淘汰。具体如下:

public class LRUKCache<K, V> extends LRUCache<K, V> {

    // 进入缓存队列的评判标准
private int putStandard; // 访问数据历史记录
private LRUCache<Object, Integer> historyList; public LRUKCache(int cacheSize, int historyCapacity, int putStandard) {
super(cacheSize);
this.putStandard = putStandard;
this.historyList = new LRUCache(historyCapacity);
} @Override
public V get(K key) {
// 记录数据访问次数
Integer historyCount = historyList.get(key);
historyCount = historyCount == null ? 0 : historyCount;
historyList.put(key, ++historyCount);
return super.get(key);
} @Override
public V put(K key, V value) {
if (value == null) {
return null;
}
// 如果已经在缓存里则直接返回
if (super.get(key) != null) {
return super.put(key, value);
}
// 如果数据历史访问次数达到上限,则加入缓存
Integer historyCount = historyList.get(key);
historyCount = (historyCount == null) ? 0 : historyCount;
if (removeCache(historyCount)) {
// 移除历史访问记录,加入缓存
historyList.remove(key);
return super.put(key, value);
} return value;
} private boolean removeCache(Integer historyCount) {
return historyCount >= putStandard;
} public void setPutStandard(int putStandard) {
this.putStandard = putStandard;
} @Override
public void setCallback(Callback<K, V> callback) {
super.setCallback(callback);
} public void setHistoryListCallback(Callback<K, V> callback) {
historyList.setCallback((Callback<Object, Integer>) callback);
} }

LRU-K能降低缓存污染发生的概率,但是需要额外记录对象访问次数,内存消耗较大。

测试

class LRUCacheTest {

    @Test
void lru(){
CacheStrategy<Integer, Integer> lruCache = new LRUCache<>(5);
lruCache.setCallback((integer, integer2) -> System.out.println("淘汰"+integer+"="+integer2));
lruCache.put(1,1);
lruCache.put(2,2);
lruCache.put(3,3);
lruCache.put(4,4);
lruCache.put(5,5);
lruCache.put(6,6);
List list = lruCache.list();
System.out.println(list);
} }
淘汰1=1
[2=2, 3=3, 4=4, 5=5, 6=6]
class LRUKCacheTest {

    @Test
void lrukCacheTest() {
LRUKCache<Integer, Integer> lrukCache = new LRUKCache<>(2,3,1);
lrukCache.setHistoryListCallback((integer, integer2) -> System.out.println("记录队列淘汰"+integer+"="+integer2));
lrukCache.setCallback((integer, integer2) -> System.out.println("缓存淘汰"+integer+"="+integer2));
lrukCache.get(1);
lrukCache.get(1);
lrukCache.get(1);
lrukCache.get(2);
lrukCache.get(2);
lrukCache.get(2);
lrukCache.get(3);
lrukCache.get(3);
lrukCache.get(3);
lrukCache.get(4);
lrukCache.get(4);
lrukCache.get(4);
lrukCache.put(1,2);
lrukCache.put(2,2);
lrukCache.put(3,2);
lrukCache.put(4,2);
List list = lrukCache.list();
System.out.println(list);
}
}
记录队列淘汰1=3
缓存淘汰2=2
[3=2, 4=2]

用Java写一个分布式缓存——缓存淘汰算法的更多相关文章

  1. 用JAVA写一个函数,功能例如以下: 随意给定一组数, 找出随意数相加之后的结果为35(随意设定)的情况

    用JAVA写一个函数.功能例如以下:随意给定一组数,比如{12,60,-8,99,15,35,17,18},找出随意数相加之后的结果为35(随意设定)的情况. 能够递归算法来解: package te ...

  2. 五:用JAVA写一个阿里云VPC Open API调用程序

    用JAVA写一个阿里云VPC Open API调用程序 摘要:用JAVA拼出来Open API的URL 引言 VPC提供了丰富的API接口,让网络工程是可以通过API调用的方式管理网络资源.用程序和软 ...

  3. 用java写一个servlet,可以将放在tomcat项目根目录下的文件进行下载

    用java写一个servlet,可以将放在tomcat项目根目录下的文件进行下载,将一个完整的项目进行展示,主要有以下几个部分: 1.servlet部分   Export 2.工具类:TxtFileU ...

  4. 用JAVA写一个多线程程序,写四个线程,其中二个对一个变量加1,另外二个对一个变量减1

    package com.ljn.base; /** * @author lijinnan * @date:2013-9-12 上午9:55:32 */ public class IncDecThrea ...

  5. 使用JAVA写一个简单的日历

    JAVA写一个简单的日历import java.text.DateFormat;import java.text.ParseException;import java.text.SimpleDateF ...

  6. Java实现一个简单的缓存方法

    缓存是在web开发中经常用到的,将程序经常使用到或调用到的对象存在内存中,或者是耗时较长但又不具有实时性的查询数据放入内存中,在一定程度上可以提高性能和效率.下面我实现了一个简单的缓存,步骤如下. 创 ...

  7. 【redis前传】自己手写一个LRU策略 | redis淘汰策略

    title: 自己手写一个LRU策略 date: 2021-06-18 12:00:30 tags: - [redis] - [lru] categories: - [redis] permalink ...

  8. java 写一个"HelloJavaWorld你好世界"输出到操作系统文件Hello.txt文件中

    package com.beiwo.homework; import java.io.File; import java.io.FileOutputStream; import java.io.IOE ...

  9. Java写一个简单学生管理系统

    其实作为一名Java的程序猿,无论你是初学也好,大神也罢,学生管理系统一直都是一个非常好的例子,初学者主要是用数组.List等等来写出一个简易的学生管理系统,二.牛逼一点的大神则用数据库+swing来 ...

  10. 用java写一个用户登陆界面

    一.课堂测试源代码及其结果截图 用java的swing写一个用户登录界面,采用网格布局.源代码如下: /** * */package LiuLijia; import java.awt.CardLay ...

随机推荐

  1. Vue学习之--------全局事件总线(2022/8/22)

    文章目录 1.全局事件总线基础知识(GlobalEventBus) 2.图解过程 3.代码实例 3.1 main.js 3.1 App.vue 3.2 School.vue 3.3 Student.v ...

  2. 齐博x1页面不直接报错,如何排查

    有的页面是不会直接报错的,比如像下面这个,这个时候需要你用谷歌或火狐浏览器打开,按F12键进入开发者模式,然后选择Network选项,刷新一下当前的网页,就会看到红色的请求.单独打开他.就可以看到错误 ...

  3. 动词时态=>3.现在时态和过去时态构成详解

    现在时态构成详解 一般现在时态 最容易构成的时态,直接加动词原形(字典当中显示的词条)就可以 第三人称"单数"的话需要加s 这是最容易出错的时态:容易将 现在的时间,和一般的状态: ...

  4. python基础之数据类型总结

    一.列表 1.作用:列表主要用于存储多个数据. 2.空列表表示:li=[]或者li=list() 3.列表的索引和切片:同字符串的索引和切片,索引超出范围报错,切片超出范围不报错. list3 = [ ...

  5. Tesla-E380,4K eDP一键点屏神器问世

    eDP屏快速点亮,EDID回读, eDP屏调试 是否为点屏的准备工作感到烦躁: 1)查找LCD模组的数据手册(常常还未必能找着) 2)在上位机软件或者单片机程序里设置一大堆的LCD屏参,这个频率,那个 ...

  6. LcdTools如何编写MIPI指令(初始化代码)

    在LcdTools帮助文档中查看MIPI读写指令描述,如下图 编写LCM初始化代码就是配置LCM Driver IC寄存器值,一般只需用MipiWrite()指令写参数即可:下面介绍MipiWrite ...

  7. 十一、Pod的健康检查-探针

    Pod 的健康检查-探针 一.Pod 的健康检查-探针 1.1.探针基本概念 ​探针是由 kubelet 对容器执行的定期诊断.要执行诊断,kubelet 调用由容器实现的 Handler 有三种类型 ...

  8. 创建.NET程序Dump的几种姿势

    当一个应用程序运行的有问题时,生成一个Dump文件来调试它可能会很有用.在Windows.Linux或Azure上有许多方法可以生成转储文件. Windows平台 dotnet-dump (Windo ...

  9. Python基础之面向对象:3、继承与派生

    面向对象 一.三大特征之继承 python三大特征: 封装.继承.多态 三者中继承最为核心,实际应用对,感受较为直观 封装和多态略微抽象 1.继承的概念 继承的含义: ​ 在现实生活中,继承表示人与人 ...

  10. RHCE习题

    RHCE习题 考试说明: RH294系统信息 在练习期间,您将操作下列虚拟系统: 真实机: foundation: kiosk:redhat root: Asimov workstation.lab. ...