使用Python实现类似redis的缓存,原文是使用go实现的,本文使用python实现,用来比较两者的区别,方便从python转go的开发者比较二者的不同。
PS:原文链接是:https://geektutu.com/post/geecache-day1.html
PS: 预计在完成前还会对本文多次修改 仅作参考
PS: 测试代码也会在后续补充
原文使用的是LRU算法,这里改成LRU-K算法。先说明一下两者区别。
LRU(最近最少使用)算法在处理周期性或突发查询时会导致缓存命中率下降,主要原因在于其工作机制和数据访问模式之间的不匹配。
LRU算法的工作机制:
LRU算法的基本思想是,当缓存空间不足时,淘汰最近最少使用的缓存项。具体步骤如下:
1、访问缓存项:
如果缓存项存在,则将其标记为最新使用。
如果缓存项不存在,则将其加载到缓存中,并可能会淘汰最久未使用的缓存项。
2、淘汰策略:
当缓存空间不足时,选择最久未使用的缓存项进行淘汰。
LRU-K 是一种先进的缓存替换算法,它结合了最近最少使用(LRU)和频率(K)的概念。LRU-K 算法会记录每个缓存项最近的 K 次访问时间,并根据这些时间来决定缓存项的替换顺序。与传统的 LRU 算法相比,LRU-K 算法能够更好地捕捉缓存项的访问模式,从而提高缓存命中率。
工作原理
1、初始化:
设置缓存的大小和 K 的值(例如,K=2 表示记录最近两次访问时间)。
2、访问缓存项:
如果缓存项在缓存中,更新其最近 K 次访问时间。
如果缓存项不在缓存中,加载缓存项并记录其访问时间。如果缓存已满,按照 LRU-K 的替换策略进行替换。
3、替换缓存项:
选择最近 K 次访问时间中最早的缓存项进行替换。
LRU-K 算法的优点
LRU-K 算法的应用场景
LRU-K 算法适用于访问模式复杂且需要高缓存命中率的场景,例如数据库缓冲池、网页缓存等。在这些场景中,LRU-K 算法能够有效地提高缓存性能,减少缓存替换次数。
周期性查询指的是数据访问具有明显的周期性,比如每隔一段时间会重新访问某些数据。这种访问模式的特点是,数据在每个周期内是高度集中的,但在周期之间的间隔期可能不被访问。
1、缓存项被淘汰:
在周期的间隔期内,缓存中的这些周期性数据可能会被淘汰,因为它们在此期间不被访问,导致其在LRU队列中位置靠后。
当下一周期开始时,这些数据需要重新加载到缓存中,导致缓存命中率下降。
2、缓存空间占用:
如果缓存空间有限,周期性数据可能会占用大量缓存空间,导致其他数据无法被缓存,进一步降低缓存命中率。
突发查询指的是数据访问在某个时刻突然增加,随后又恢复正常。这种访问模式的特点是短时间内大量访问某些数据,而其他时间访问量较少。
1、缓存污染:
突发查询会导致大量数据短时间内被加载到缓存中,原本的缓存数据可能被淘汰。这种现象被称为缓存污染。
当突发查询结束后,这些新加载的数据可能不再被访问,而原本的缓存数据已经被淘汰,导致缓存命中率下降。
2、缓存稳定性降低:
突发查询会导致缓存内容频繁变化,缓存项在短时间内被频繁替换,无法稳定保持较高的缓存命中率。
ps:双向链表的算法可以根据另一个文章内容进行实现
from Dlist import DLinkedList
class PCache:
def __init__(self, max_bytes, nbytes, func=None):
self.max_bytes = max_bytes
self.nbytes = nbytes
self.ll = DLinkedList()
self.cache = dict()
self.OnEvicted = func # 回调函数
def add(self, key, value):
if self.cache.get(key):
# 更新
entry = self.cache.get(key)
self.ll.move_to_front(entry)
self.cache[key] = Entry(key, value)
self.nbytes += len(value) - len(entry.value)
else:
# 新增
entry = Entry(key, value)
self.ll.append(entry)
self.cache[key] = entry
self.nbytes = len(key) + len(value)
while self.max_bytes != 0 and self.max_bytes < self.nbytes:
self.remove_oldest()
def remove_oldest(self):
# lru算法
ele = self.ll.back()
del self.cache[ele.key]
self.nbytes -= len(ele.key) + ele.value
if self.OnEvicted:
self.OnEvicted()
def query(self, key):
ele = self.cache.get(key)
self.ll.move_to_front(ele)
if ele:
return ele.value, True
class Entry:
def __init__(self, key, value):
self.key = key
self.value = value
import time
from collections import defaultdict, deque
from Dlist import DLinkedList
class PCache:
def __init__(self, max_bytes, nbytes, k=2, func=None):
self.max_bytes = max_bytes
self.nbytes = nbytes
# lru 需要的数据结构 双向链表
self.ll = DLinkedList()
self.cache = dict()
self.OnEvicted = func # 回调函数
# lru-k 所需数据结构 deque是双向队列
self.k = k
self.history = defaultdict(deque)
def _current_time(self):
return time.time()
def add(self, key, value):
if self.cache.get(key):
# 更新
entry = self.cache.get(key)
self.history[key].append(self._current_time())
if len(self.history[key]) > self.k:
self.history[key].popleft()
self.cache[key] = Entry(key, value)
self.nbytes += len(value) - len(entry.value)
else:
# 新增
entry = Entry(key, value)
self.cache[key] = entry
self.nbytes = len(key) + len(value)
self.history[key].append(self._current_time())
while self.max_bytes != 0 and self.max_bytes < self.nbytes:
self.remove_oldest_k()
def remove_oldest(self):
# lru算法
ele = self.ll.back()
del self.cache[ele.key]
self.nbytes -= len(ele.key) + ele.value
if self.OnEvicted:
self.OnEvicted()
def remove_oldest_k(self):
# lru-k算法
oldest_key = None
oldest_time = float('inf')
for key, times in self.history.items():
# 先淘汰频率低的
if len(times) < self.k:
oldest_key = key
break
# 再淘汰最早使用的
if times[0] < oldest_time:
oldest_time = times[0]
oldest_key = key
if oldest_key is not None:
self.history.pop(oldest_key)
ele = self.cache[oldest_key]
del self.cache[oldest_key]
self.nbytes -= len(oldest_key) + ele.value
if self.OnEvicted:
self.OnEvicted()
def query(self, key):
ele = self.cache.get(key)
if ele:
self.history[key].append(self._current_time())
if len(self.history[key]) > self.k:
self.history[key].popleft()
return ele.value, True
class Entry:
def __init__(self, key, value):
self.key = key
self.value = value