高并发下的分布式缓存 | 设计和实现LFU缓存

news2024/9/21 8:03:40

什么是 LFU 缓存?

最少使用频率 (LFU) 是一种用于管理计算机内存的缓存算法。在这种算法中,系统会跟踪缓存中每个数据被引用的次数。当缓存已满时,系统会删除引用频率最低的数据。

LFU 缓存问题描述

我们的目标是设计一个LFU 缓存,需要支持以下操作:

  • LFUCache(int capacity): 初始化具有特定容量的缓存对象。
  • int get(int key): 如果键存在于缓存中,则返回该键的值;否则,返回 -1。
  • void put(int key, int value): 如果键存在,则更新其值;如果键不存在,则插入该键。当缓存达到容量时,应在插入新项目之前使最少使用频率的键失效并删除它。如果存在多个频率相同的键(平局情况),则应使最近最少使用的键失效。

当对缓存中某个键执行 get 或 put 操作时,该键的频率将增加。get 和 put 操作的时间复杂度应为 O(1)。

LFU 缓存实现

使用暴力方法实现 LFU 缓存

我们初始化一个大小等于缓存大小的数组。每个元素存储键、值、频率以及该键被访问的时间戳。我们使用频率和时间戳来确定最少使用的元素。

class Element {
    int key;
    int val;
    int frequency;
    int timeStamp;
    
    public Element(int k, int v, int time) {
        key = k;
        val = v;
        frequency = 1;
        timeStamp = time;
    }
}

int get(int key): 我们遍历数组,并将缓存中每个元素的键与给定键进行比较。如果找到相等的键,则增加该元素的频率并更新时间戳。如果未找到,则返回 -1。时间复杂度为 O(n)。

void put(int key, int value): 如果数组未满,我们创建一个频率为 1 且时间戳为 0 的新元素,并增加数组中现有数据的时间戳。如果数组已满,我们必须删除最少使用的元素。为此,我们遍历数组并找到频率最低的元素。如果频率相同,则选择最近最少使用的元素(最旧的时间戳)。然后插入新元素。时间复杂度为 O(n)。

使用更高效的方法实现 LFU 缓存

首先,我们将实现 O(1) 时间复杂度的插入和访问操作。为此,我们需要两个映射,一个存储键值对,另一个存储访问次数/频率。

public class LFUCache {
    private Map<Integer, Integer> valueMap = new HashMap<>();
    private Map<Integer, Integer> frequencyMap = new HashMap<>();
    private final int size;

    public LFUCache(int capacity) {
        size = capacity;
    }
    
    public int get(int key) {
        if (!valueMap.containsKey(key)) {
            return -1;
        }
        frequencyMap.put(key, frequencyMap.get(key) + 1);
        return valueMap.get(key);
    }
    
    public void put(int key, int value) {
        if (!valueMap.containsKey(key)) {
            valueMap.put(key, value);
            frequencyMap.put(key, 1);
        } else {
            valueMap.put(key, value);
            frequencyMap.put(key, frequencyMap.get(key) + 1);
        }
    }
}

使用单链表实现 LFU 缓存

在上述代码中,我们需要实现缓存淘汰策略。当映射大小达到最大容量时,我们需要找到具有最低访问频率的数据。

在当前实现中,我们必须遍历 frequencyMap 的所有值,找到最低频率并从两个映射中删除相应的键。这需要 O(n) 时间。

此外,如果多个键具有相同的频率,在当前实现中我们无法找到最近最少使用的键,因为 HashMap 不存储插入顺序。为了解决这个问题,我们添加了一个新的数据结构,即一个有序映射,其中键是频率,值是具有相同频率的元素列表。

现在,新的数据可以添加到频率为 1 的链表末尾。由于映射按频率排序,我们可以在 O(1) 时间内找到最低频率的列表。此外,我们可以在 O(1) 时间内删除列表的第一个项目(最低频率的项目),因为它是最近最少使用的。

代码如下:

public class LFUCache {
    private Map<Integer, Integer> valueMap = new HashMap<>();
    private Map<Integer, Integer> countMap = new HashMap<>();
    private TreeMap<Integer, List<Integer>> frequencyMap = new TreeMap<>();
    private final int size;

    public LFUCache(int capacity) {
        size = capacity;
    }
    
    public int get(int key) {
        if (!valueMap.containsKey(key) || size == 0) {
            return -1;
        }
        int frequency = countMap.get(key);    
        frequencyMap.get(frequency).remove(new Integer(key));
        if (frequencyMap.get(frequency).size() == 0) {
            frequencyMap.remove(frequency);
        }
        frequencyMap.computeIfAbsent(frequency + 1, k -> new LinkedList<>()).add(key);
        countMap.put(key, frequency + 1);
        return valueMap.get(key);
    }
    
    public void put(int key, int value) {
        if (!valueMap.containsKey(key) && size > 0) {
            if (valueMap.size() == size) {
                int lowestCount = frequencyMap.firstKey();
                int keyToDelete = frequencyMap.get(lowestCount).remove(0);
                if (frequencyMap.get(lowestCount).size() == 0) {
                    frequencyMap.remove(lowestCount);
                }
                valueMap.remove(keyToDelete);
                countMap.remove(keyToDelete);
            }
            valueMap.put(key, value);
            countMap.put(key, 1);
            frequencyMap.computeIfAbsent(1, k -> new LinkedList<>()).add(key);
        } else if (size > 0) {
            valueMap.put(key, value);
            int frequency = countMap.get(key);
            frequencyMap.get(frequency).remove(new Integer(key));
            if (frequencyMap.get(frequency).size() == 0) {
                frequencyMap.remove(frequency);
            }
            frequencyMap.computeIfAbsent(frequency + 1, k -> new LinkedList<>()).add(key);
            countMap.put(key, frequency + 1);
        }
    }
}

因此,插入和删除操作都是 O(1),即常量时间操作。

使用双链表实现 LFU 缓存 (Java)

在解决缓存淘汰问题时,我们将访问操作的时间复杂度增加到了 O(n)。所有具有相同频率的数据元素都在一个链表中。如果其中一个元素被访问,我们需要将其移动到下一个频率的链表中。我们必须先遍历链表找到该元,这在最坏情况下需要 O(n) 操作。

为了解决这个问题,我们需要以某种方式直接在链表中访问该数据,如果我们能做到这一点,就可以在 O(1) 时间内从当前频率链表中删除该元素,并在 O(1) 时间内将其移动到下一个频率链表的末尾。

为此,我们需要一个双链表。我们将创建一个节点,存储元素的键、值和在链表中的位置。我们将把链表转换为双链表。

public class LFUCache {

    private Map<Integer, Node> valueMap = new HashMap<>();
    private Map<Integer, Integer> countMap = new HashMap<>();
    private TreeMap<Integer, DoubleLinkedList> frequencyMap = new TreeMap<>();
    private final int size;

    public LFUCache(int n) {
        size = n;
    }

    public int get(int key) {
        if (!valueMap.containsKey(key) || size == 0) {
            return -1;
        }

        Node nodeToDelete = valueMap.get(key);
        Node node = new Node(key, nodeToDelete.value());
        int frequency = countMap.get(key);
        frequencyMap.get(frequency).remove(nodeToDelete);
        removeIfListEmpty(frequency);
        valueMap.remove(key);
        countMap.remove(key);
        valueMap.put(key, node);
        countMap.put(key, frequency + 1);
        frequencyMap.computeIfAbsent(frequency + 1, k -> new DoubleLinkedList()).add(node);
        return valueMap.get(key).value;
    }

    public void put(int key, int value) {
        if (!valueMap.containsKey(key) && size > 0) {
            Node node = new Node(key, value);

            if (valueMap.size() == size) {
                int lowestCount = frequencyMap.firstKey();
                Node nodeToDelete = frequencyMap.get(lowestCount).head();
                frequencyMap.get(lowestCount).remove(nodeToDelete);
                removeIfListEmpty(lowestCount);

                int keyToDelete = nodeToDelete.key();
                valueMap.remove(keyToDelete);
                countMap.remove(keyToDelete);
            }
            frequencyMap.computeIfAbsent(1, k -> new DoubleLinkedList()).add(node);
            valueMap.put(key, node);
            countMap.put(key, 1);
        } else if (size > 0) {
            Node node = valueMap.get(key);
            Node nodeToInsert = new Node(key, value);
            int frequency = countMap.get(key);
            frequencyMap.get(frequency).remove(node);
            removeIfListEmpty(frequency);
            valueMap.remove(key);
            countMap.remove(key);
            valueMap.put(key, nodeToInsert);
            countMap.put(key, frequency + 1);
            frequencyMap.computeIfAbsent(frequency + 1, k -> new DoubleLinkedList()).add(nodeToInsert);
        }
    }

    private void removeIfListEmpty(int frequency) {
        if (frequencyMap.get(frequency).size() == 0) {
            frequencyMap.remove(frequency);
        }
    }
}

class Node {
    private final int key;
    private final int value;
    private Node previous;
    private Node next;

    public Node(int k, int v) {
        key = k;
        value = v;
    }

    public int key() {
        return key;
    }

    public int value() {
        return value;
    }
}

class DoubleLinkedList {
    private Node head = new Node(0, 0);
    private Node tail = new Node(0, 0);
    private int size = 0;

    public DoubleLinkedList() {
        head.next = tail;
        tail.previous = head;
    }

    public void add(Node node) {
        node.previous = tail.previous;
        node.previous.next = node;
        node.next = tail;
        tail.previous = node;
        size++;
    }

    public void remove(Node node) {
        node.previous.next = node.next;
        node.next.previous = node.previous;
        size--;
    }

    public Node head() {
        return head.next;
    }

    public int size() {
        return size;
    }
}

双链表确保我们可以在常数时间内删除节点和插入节点。总的来说,插入和访问操作都是 O(1) 时间复杂度。

LRU缓存的实际应用

假设我们有一个代理服务器,它位于用户和互联网之间。当用户请求网页内容时,这个代理服务器会在用户和互联网之间进行中转,同时缓存一些内容,比如图片、CSS 文件、JavaScript 代码等。以优化网络利用率和提高响应速度。

这样的缓存代理应该在其有限的存储或内存中,尽量缓存尽可能多的数据。但是代理服务器的存储空间有限,所以它需要决定哪些内容应该保留在缓存中,哪些内容应该删除。

这里就有一个问题:到底应该删除哪些内容?最好的策略是删除那些不常用的内容,而保留那些经常被访问的内容。

LFU(Least Frequently Used,最少使用频率) 缓存算法正是用来解决这个问题的。LFU 会跟踪每个内容被访问的次数,当缓存满了需要删除一些内容时,它会选择删除那些访问次数最少的内容。这样,可以确保那些最有用、最常被访问的内容能够保留在缓存中,提高系统的效率。

**相比之下,如果使用 LRU(Least Recently Used,最近最少使用) 缓存算法,**它会删除那些最近没有被访问过的内容。这在很多情况下也有效,但如果用户的请求模式是轮流访问多个内容,LRU 可能会不断地删除和添加内容,导致缓存命中率降低。

例如,如果用户依次访问 A、B、C、D 四个资源,而代理服务器的缓存只能存三个资源。LRU 会不断地删除最久没访问的资源,导致缓存一直在变化,无法有效利用缓存。而 LFU 会保留访问次数最多的资源,不会频繁地删除和添加,可以更好地提高缓存命中率。

总之,在需要频繁处理大量请求的场景中,比如网络代理服务器,使用 LFU 缓存算法可以更有效地利用缓存,提高系统的整体性能。

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

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

相关文章

手机号码归属地数据源,让您随时掌握通话对方位置!

手机号码归属地数据源&#xff0c;这是一个非常实用的数据源&#xff0c;可以帮助我们随时掌握通话对方的位置。无论是普通民众还是企业用户&#xff0c;都可以从中受益。 在这个数据源中&#xff0c;我们可以通过手机号码的前7位来查询该手机号码的归属地&#xff0c;包括省市…

美容院管理小程序的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;服务类型管理&#xff0c;产品服务管理&#xff0c;预约信息管理&#xff0c;产品分类管理&#xff0c;产品信息管理&#xff0c;订单管理&#xff0c;系统管理 微信端账号功能包…

笔试练习day2

目录 BC64 牛牛的快递题目解析解法模拟代码方法1方法2 DP4 最小花费爬楼梯题目解析解法动态规划状态表示状态转移方程代码 数组中两个字符串的最小距离题目解析解法方法1暴力解法(会超时)方法2贪心(动态规划)代码 感谢各位大佬对我的支持,如果我的文章对你有用,欢迎点击以下链接…

【数据脱敏】数据交换平台数据脱敏建设方案

1 概述 1.1 数据脱敏定义 1.2 数据脱敏原则 1.2.1基本原则 1.2.2技术原则 1.2.3管理原则 1.3 数据脱敏常用方法 3.1.1泛化技术 3.1.2抑制技术 3.1.3扰乱技术 3.1.4有损技术 1.4 数据脱敏全生命周期 2 制定数据脱敏规程 3 发现敏感数据 4 定义脱敏规则 5 执…

GD32 IAP升级——boot和app相互切换

GD32 IAP升级——boot和app相互切换 目录 GD32 IAP升级——boot和app相互切换1 Keil工程设置1.1 修改ROM1.2 Keil烧录配置 2 代码编写2.1 app跳转2.2 软件重启2.3 app中断向量表偏移 结束语 1 Keil工程设置 1.1 修改ROM GD32内部Flash是一整块连续的内存&#xff0c;但是因为…

数学计算之JS小数精度问题(java/python)

number 小数计算会出现精度不准确问题&#xff0c;js中number是64 位双精度浮点数。 其实&#xff0c;不仅仅只有javascript&#xff0c;还有java、python等都会有类似问题&#xff0c;因为计算机中存的都是二进制&#xff08;浮点数IEEE754是被普遍使用的标准&#xff09;&am…

Java:线程安全

引子 首先来看一段代码: private static int count 0;public static void main(String[] args) throws InterruptedException {Thread t1 new Thread(()->{for (int i 0; i < 50000; i) {count;}});Thread t2 new Thread(()->{for (int i 0; i < 50000; i) {…

Java语言程序设计——篇十一(4)

&#x1f33f;&#x1f33f;&#x1f33f;跟随博主脚步&#xff0c;从这里开始→博主主页&#x1f33f;&#x1f33f;&#x1f33f; 欢迎大家&#xff1a;这里是我的学习笔记、总结知识的地方&#xff0c;喜欢的话请三连&#xff0c;有问题可以私信&#x1f333;&#x1f333;&…

深入解析 KMZ 文件的处理与可视化:从数据提取到地图展示项目实战

文章目录 1. KMZ 文件与 KML 文件简介1.1 KMZ 文件1.2 KML 文件 2. Python 环境配置与依赖安装3. 代码实现详解3.1 查找 KMZ 文件3.2 解压 KMZ 文件3.3 解析 KML 文件3.4 可视化 KMZ 数据 4. 项目实战4.1. 数据采集4.2. 项目完整代码 5. 项目运行与结果展示6. 总结与展望 在处理…

2007-2023年上市公司国内外专利申请获得情况数据

2007-2023年上市公司国内外专利申请获得情况数据 1、时间&#xff1a;2007-2023年 2、来源&#xff1a;上市公司年报 3、指标&#xff1a;证券代码、统计截止日期、报表类型、地区、申请类型编码、申请类型、专利&#xff08;件&#xff09;、发明专利&#xff08;件&#x…

动态路由协议基础

一、动态路由协议简介 动态路由协议:路由器用来计算和维护路由信息的协议;通俗的说,就算路由器用来学习路由的协议。 二、动态路由与静态路由的区别 静态路由动态路由路由表手工配置自动生成路由维护人工维护自动收敛资源消耗路由表生成不占网络资源路哟表生成占用网络资源…

学习Java的日子 Day59 学生管理系统 web1.0版本

Day59 学生管理系统 web1.0 1.项目需求 有两个角色&#xff0c;老师和学生&#xff0c;相同的功能提取到父类用户角色 2.数据库搭建 设计学生表 设计老师表 插入数据 (超级管理员) 设计学科表 3.项目搭建 处理基础页面&#xff0c;分包&#xff0c;实体类&#xff0c;导入数据…

微软AI业务最新营收数据情况(2024年7月)

Azure AI 年度经常性收入 (ARR)&#xff1a;达到50亿美元客户数量&#xff1a;60,000家平均客户价值 (ACV) 中位数&#xff1a;83,000美元同比增长率&#xff1a;达到了惊人的900% GitHub Copilot 年度经常性收入 (ARR)&#xff1a;达到3亿美元客户数量&#xff1a;77,000家…

每日两题8

买卖股票的最佳时机 III class Solution { public:int maxProfit(vector<int>& prices) {int n prices.size();int INF 0x3f3f3f3f;vector<vector<int>> f(n, vector<int>(3, -INF));auto g f;g[0][0] 0;f[0][0] -prices[0];for (int i 1; i…

Leetcode3227. 字符串元音游戏

Every day a Leetcode 题目来源&#xff1a;3227. 字符串元音游戏 解法1&#xff1a;博弈论 分类讨论&#xff1a; 如果 s 不包含任何元音&#xff0c;小红输。如果 s 包含奇数个元音&#xff0c;小红可以直接把整个 s 移除&#xff0c;小红赢。如果 s 包含正偶数个元音&am…

10.Redis类型SortedSet

介绍 Redis的SortedSet是一个可排序的set集合。与java的TreeSet有些类似&#xff0c;但底层数据结构却差别很大。 SortedSet中的每个元素都带有一个score属性&#xff0c;可以基于score属性对元素排序&#xff0c;底层实现是一个跳表SkipList加hash表。 特点 可排序 元素不…

“银狐”团伙再度出击:利用易语言远控木马实施钓鱼攻击

PART ONE 概述 自2023年上半年“银狐”工具被披露以来&#xff0c;涌现了多个使用该工具的黑产团伙。这些团伙主要针对国内的金融、教育、医疗、高新技术等企事业单位&#xff0c;集中向管理、财务、销售等从业人员发起攻击&#xff0c;窃取目标资金和隐私信息。该团伙惯用微信…

多旋翼+四光吊舱:5Kg负载无人机技术详解

多旋翼无人机是一种具有三个及以上旋翼轴的无人驾驶飞行器。它通过每个轴上的电动机转动&#xff0c;带动旋翼&#xff0c;从而产生升推力。旋翼的总距固定&#xff0c;不像一般直升机那样可变。通过改变不同旋翼之间的相对转速&#xff0c;可以控制飞行器的运行轨迹。多旋翼无…

Js在线Eval加密混淆及解密运行

具体请前往&#xff1a;Js在线Eval加密混淆及解密运行

自动打电话软件的效果怎么样?

​​使用这个系统&#xff0c;机器人自动拨打电话&#xff0c;真人录制的语音与客户对话&#xff0c;整个过程非常顺畅。而且系统可以每天外呼数千乃至数万通电话&#xff0c;是人工的5-10倍&#xff0c;这样就不需要招聘大量员工来外呼&#xff0c;只需要留下一些优秀的销售人…