Python技术栈 —— 一种超时LRU的实现方式

Python技术栈 —— 一种超时LRU的实现方式

  • 前言
  • 一、代码实现
  • 总结
  • 参考文章


前言

本题是Leetcode的LRU的变种实现

题目链接:LRU 缓存 - leetcode
题目描述:
请你设计并实现一个满足 LRU (最近最少使用) 缓存 约束的数据结构。
实现 LRUCache 类:

  • LRUCache(int capacity) 以 正整数 作为容量 capacity 初始化 LRU 缓存
  • int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1
  • void put(int key, int value) 如果关键字 key 已经存在,则变更其数据值 value ;如果不存在,则向缓存中插入该组 key-value 。如果插入操作导致关键字数量超过 capacity ,则应该 逐出 最久未使用的关键字。
  • 函数 getput 必须以 O ( 1 ) O(1) O(1) 的平均时间复杂度运行。

题目归纳:
LRU 缓存在计算机组成原理与OS课程中是常客,在OS中的Cache替换算法出现次数尤其频繁,先复习下 LRU 的定义
LRU(Least(最少) Recently(近) Used 最近最少使用)侧重于观察最近访问,实现起来比LFU更简单,LFU(Least Frequently Used 最不经常使用)侧重于根据数据的访问次数所得出的统计规律。
当场景满足以下两个特点时,使用LFU
(1) 长期的数据访问模式是稳定的。
(2) 重视数据项的频率 > 重视数据项的时效,也就是关注长期 > 关注短期

参考文章或视频链接
[1] LRU算法 - 百度百科
[2] LFU算法 - Wikipedia
[3] 【Java面试】LRU算法和LFU算法的本质区别?- bilibili
[4] Difference between LRU and LFU Page Replacement Algorithm
[5] What is the difference between LRU and LFU
[6] Cache replacement policies

解题思路:
解法: LRU缓存机制 - leetcode官方题解

一、代码实现

# 提前安装包
$ pip install timeloop
# coding:utf-8
# @Time: 2024/1/25 下午8:35
# @Author: 键盘国治理专家
# @File: LRU_with_timeout.py
# @Description: 设计一个LRU缓存,并且带有超时功能
import datetime
import threading
import time
import schedule
from timeloop import Timeloop
from datetime import timedelta

tl = Timeloop()


class DLinkNode:  # 双向链表不要和循环双向链表混淆了
    def __init__(self, key, value, add_time):
        self.key = key
        self.value = value

        self.prev = None
        self.next = None

        self.add_time = add_time  # 添加时的时间
        # 另一种设计超时的思路是,在初始化每个节点的时候,让每个节点就自带一个超时的类或者任务,到时间了就把自己销毁掉,但这种实现思路不觉得奇怪吗?


class LRUCache:
    # 哈希表+双向链表(DLinkNode)
    def __init__(self, capacity: int, expire_time: int):  # expire_time是过期时间
        self.cache = {}  # 相当于Java里的HashSet,因为Key值即便重复也没有存储两遍的道理
        self.dum_head = DLinkNode(-1, -1, datetime.datetime.now())  # 伪头
        self.dum_tail = DLinkNode(-1, -1, datetime.datetime.now())  # 伪尾

        self.dum_head.next = self.dum_tail
        self.dum_tail.prev = self.dum_head

        self.capacity = capacity
        self.size = 0

        self.expire_time = expire_time

    def get(self, key: int):  # 那么每次get也要更新expire_time
        # 如果在cache中,则直接返回
        if key in self.cache:
            node = self.cache[key]
            node.add_time = datetime.datetime.now()
            self.move2Head(node)  # 由于是LRU,返回之前必须移动到头部
            return node
        else:
            return -1

    def put(self, key: int, value: int):  # 那么每次put也要更新expire_time
        # 如果在cache中就更新值,再提到头部
        if key in self.cache:
            node = self.cache[key]
            node.add_time = datetime.datetime.now()
            node.value = value
            self.move2Head(node)
        else:  # 不在cache中,则需要put添加
            node = DLinkNode(key, value, datetime.datetime.now())
            self.cache[key] = node

            self.add2Head(node)
            self.size += 1

            # 超过容量限制
            if self.size > self.capacity:
                # 移除末尾
                removed = self.removeTaile()
                self.cache.pop(removed.key)  # 清除缓存
                self.size -= 1

    def move2Head(self, node: DLinkNode):
        # 拆除旧关系
        self.removeNode(node)
        # 建立新关系
        self.add2Head(node)

    def add2Head(self, node: DLinkNode):
        # (1)新节点与老节点构建关系
        node.prev = self.dum_head
        node.next = self.dum_head.next
        # (2)拆除旧有关系
        self.dum_head.next.prev = node
        self.dum_head.next = node

    def removeNode(self, node: DLinkNode):
        node.prev.next = node.next
        node.next.prev = node.prev
        return node
        # del node # 不能删除,要返回此信息消除cache中的缓存

    def removeTail(self):
        node = self.dum_tail.prev
        self.removeNode(node)
        return node

    # def __str__(self):
    #     p = self.dum_head.next
    #     while p and p != self.dum_tail:
    #         print(p.value)
    #         p = p.next
    #     return ""

    def __repr__(self):
        p = self.dum_head.next
        while p and p != self.dum_tail:
            print(p.value, end=" ")
            p = p.next
        return ""


# 如果要对LRU进行超时的限制,那么必须再单独开一个线程,对LRU里的内容进行扫描,才能有超时,必须用线程来完成这件事
@tl.job(interval=timedelta(seconds=1))
def scanning_LRU(LRU: LRUCache):
    print("scanning LRU", id(LRU))
    # 反向持续扫描LRU才是对的,因为越靠近tail,越少被用到
    # while True: # 应该是一个定时的扫描线程,而不是死循环执行
    p = LRU.dum_tail.prev
    while p and p != LRU.dum_head:
        # 判断是否超时
        if (datetime.datetime.now() - p.add_time).seconds > LRU.expire_time:  # 超时了,就需要调用删除函数
            print(f"\tnode({p.key},{p.value})超时{ (datetime.datetime.now() - p.add_time).seconds - LRU.expire_time}s", )
            pre = p.prev
            removed = LRU.removeNode(p)
            p = removed.prev
            # (1)清空缓存
            LRU.cache.pop(removed.key)
            # (2)物理删除该节点
            del removed
        else:
            p = p.prev


def test_time(a):
    start = datetime.datetime.now()
    print("----")
    time.sleep(2)  # 2 seconds
    end = datetime.datetime.now()
    print((end - start).seconds)
    return test_time


if __name__ == '__main__':
    capacity = 3
    expire_time = 3  # 3s 的超时时间

    LRU = LRUCache(capacity, expire_time)
    LRU.put(2, 2)
    LRU.put(3, 3)
    LRU.put(1, 1)
    print('LRU超时前的值', LRU)

    start = datetime.datetime.now()
    cnt = 1e8 * 2
    while cnt > 0:
        cnt -= 1
    print('消磨时间:',datetime.datetime.now() - start) # 大概5-7秒

    # 定时调度方式(1),结合timeloop包
    scanning_LRU(LRU)
    # 定时调度方式(2),schedule方式,该方式实现有问题
    # schedule.every(1).seconds.do(lambda: scanning_LRU(LRU))
    # 定时调度方式(3),threading.Timer()方式,该方式实现有问题
    # thread = threading.Timer(1, lambda: scanning_LRU(LRU))
    # thread = threading.Timer(1, scanning_LRU, LRU)
    # thread.start()

    print('LRU超时后的值', LRU)

总结

(1)思路一,开辟线程去定时循环扫描LRU链表的节点。这是本文的实现思路,定时任务的实现方式见参考文章。
(2)思路二,给每个DLinkNode节点本身带上一个计时器属性,超时就“自爆”销毁自己,但这种实现思路我是没想出来要怎么实现。

参考文章

参考文章或视频链接
[1] Pass parameters to schedule
[2] 《threading.Timer()定时器实现定时任务》
[3] 《我整理了8种方案:Python执行定时任务!》
[4] 《5种Python使用定时调度任务的方式》

你可能感兴趣的:(Python技术栈,Algorithm,python,redis,开发语言)