简介
我们都知道,Redis会使用“淘汰策略”来进行热点数据的管理,其中大部分场景下都会使用LRU(Least Recently used)算法,本文从一个简单的使用dict缓存斐波那契数列的值为例引出LRU的使用场景并使用Python实现一个简单的LRUCache。
使用缓存减少计算或者主数据库的开销
在实际的业务场景中,我们常常会使用缓存来减少程序的计算或者用户频繁访问主数据库的开销。比如说,我这里有一个接口函数fib,用户使用某一个天数来请求数据时,接口将计算结果返回给用户:
def fib(n): if n <= 2: return 1 return fib(n-1) + fib(n-2)
为了减少程序计算的开销,我们可以使用一个字典去“缓存”对应的结果,也就是说,我们可以提前计算好用户请求的天数与对应结果的对应信息,如果用户请求的天数在“缓存”中的话我们可以直接将缓存中的结果返回给用户,这样就有效的减少了一次程序的开销!
装饰器方式实现“缓存效果”
# 字典中有的会从这里取值而不用计算,减少计算开销 dic = {1:1,2:1,3:2} def wrapper(func): global dic def inner(n): if n in dic: return dic[n] res = func(n) dic[n] = res return res return inner @wrapper def fib(n): if n <= 2: return 1 return fib(n-1) + fib(n-2) if __name__ == "__main__": for i in range(1,6): ret = fib(i) print(ret)
上面的代码有效的实现了“缓存”效果。
但是问题又来了:事实上缓存的大小由机器的内存大小决定的,因此缓存中的数据不可能无限大!如果任凭缓存中的数据无限的增加总有一个时刻会撑爆内存,影响服务的性能!
这就需要有一种缓存数据的管理策略,而LRU是管理缓存热点数据常用的算法。
简单的LRU实现
LRU(Least Recently used)策略是将最早访问的数据从缓存中删除(当然是在缓存超过阈值的情况)。
实现的思路
拿上面的缓存字典dic来讲解:
如果用户请求的数据在缓存中的话,我们直接在缓存中找到该数据对应的value值返回给用户,并且将刚刚访问的这个键值对放到后面;
如果请求的数据不在缓存中的话需要调用fib方法计算结果,并且还得判断一下缓存是否满:如果缓存没有满的话将结果返回并更新缓存空间——将刚刚访问的键值对放在后面,如果缓存满了的话需要将“最早”访问的那个键值对从dic中删掉,然后将新计算的结果放到dic的后面。
使用双端链表实现简单的LRUCache
# -*- coding:utf-8 -*- class Node(object): def __init__(self,prev=None,next=None,key=None,value=None): self.prev,self.next,self.key,self.value = prev,next,key,value # 双端链表 class CircularDoubeLinkedList(object): def __init__(self): node = Node() node.prev,node.next = node,node self.rootnode = node # 头节点 def headnode(self): return self.rootnode.next # 尾节点 def tailnode(self): return self.rootnode.prev # 移除一个节点 def remove(self,node): if node is self.rootnode: return node.prev.next = node.next node.next.prev = node.prev # 追加一个节点到链表的尾部 def append(self,node): tailnode = self.tailnode() tailnode.next = node node.next = self.rootnode # 需要把根节点的prev设置为node self.rootnode.prev = node # LRU Cache class LRUCache(object): def __init__(self,maxsize=10): self.maxsize = maxsize self.cache = {} self.access = CircularDoubeLinkedList() self.isfull = len(self.cache) >= self.maxsize ### 类装饰器的写法 def __call__(self,func): def inner(n): cachenode = self.cache.get(n,None) # 如果缓存中有,从缓存中获取并移动n对应的节点 if cachenode: self.access.remove(cachenode) self.access.append(cachenode) # 返回value值 return cachenode.value # 如果缓存中没有,需要先看一下缓存是否满了,再去处理 else: result = func(n) # 如果缓存没满往里面添加数据 if not self.isfull: tailnode = self.access.tailnode() new_node = Node(tailnode,self.access.rootnode,n,result) self.access.append(new_node) # 将新节点缓存下来 self.cache[n] = new_node self.isfull = len(self.cache) >= self.maxsize # return result # 如果缓存满了,删除lru_node else: lru_node = self.access.headnode() # 先把lru_node删除 del self.cache[lru_node.key] self.access.remove(lru_node) # 讲新节点放进去 tailnode = self.access.tailnode() new_node = Node(tailnode,self.access.rootnode,n,result) self.access.append(new_node) # 将新节点缓存下来 self.cache[n] = new_node return result # 装饰器,最后返回inner return inner @LRUCache() def fib(n): if n <= 2: return 1 return fib(n-1) + fib(n-2) for i in range(1,10): print(fib(i))
~~~