LRU
LRU算法可以用于内存淘汰策略,策略是最近最少使用,使用双线链表可以轻松满足这个要求,新元素插入表头,元素被get将其移动到表头,淘汰表尾元素,缺陷在于会有大量的内存碎片和指针内存消耗、内存拷贝,查找、删除时间复杂度都为O(n),因为要遍历查找相应元素。双向链表+hash的方式可以解决这些问题,使得插入、查找、删除都是O(1)
LRU实现
// 双向链表结点
type dllNode struct {
value interface{}
prev, next *dllNode
}
// LRU
// hash指向node+链表使得插入、删除、查找都是O(1)
type LRUCache struct {
head, tail *dllNode // 头尾结点
elen int // 当前元素个数
ecap int // 缓存大小
hash map[string]*dllNode // key:node
}
func NewLRUCache(maxCacheLen int) *LRUCache {
head, tail := new(dllNode), new(dllNode)
head.next = tail
tail.prev = head
return &LRUCache{
head: head,
tail: tail,
elen: 0,
ecap: maxCacheLen,
hash: make(map[string]*dllNode, maxCacheLen),
}
}
func newdllNode(value interface{}) *dllNode {
return &dllNode{value: value}
}
// 插入
func (lru *LRUCache) Save(key string, value interface{}) {
node, ok := lru.hash[key]
// 已经存在了
if ok {
lru.moveToHead(node)
return
}
// 已经满了则删除一个
if lru.elen == lru.ecap {
lru.removetail()
}
newNode := newdllNode(value)
lru.hash[key] = newNode
lru.elen++
// 放在表头
newNode.next = lru.head.next
lru.head.next.prev = newNode
newNode.prev = lru.head
lru.head.next = newNode
}
// 当缓存满了,移除tail
func (lru *LRUCache) removetail() {
if lru.elen == 0 {
return
}
lru.tail.prev = lru.tail.prev.prev
lru.tail.prev.next = lru.tail
}
// 删除
func (lru *LRUCache) Delete(key string) {
}
// 获取,应该移动到表头
func (lru *LRUCache) Get(key string) interface{} {
movenode, ok := lru.hash[key]
if !ok {
return nil
}
lru.moveToHead(movenode)
return movenode.value
}
// 移动到表头
func (lru *LRUCache) moveToHead(node *dllNode) {
node.prev.next = node.next
node.next.prev = node.prev
node.next = lru.head.next
lru.head.next.prev = node
lru.head.next = node
node.prev = lru.head
}
func (lru *LRUCache) print() {
node := lru.head.next
for node != lru.tail {
fmt.Print(node.value, " ")
node = node.next
}
}
func main() {
lru := NewLRUCache(4)
lru.Save("key1", 1)
lru.Save("key2", 2)
lru.Save("key3", 3)
lru.Save("key4", 4)
lru.print() // 4 3 2 1
fmt.Println()
lru.Save("key5", 5)
lru.print() // 5 4 3 2
fmt.Println()
lru.Get("key2")
lru.print() // 2 5 4 3
}
Redis中的LRU
双向链表+hash的方式仍然会造成大量的内存消耗,redis追求极致速度,采用的是近似LRU的方案,随机选取几个键,删除最久未使用的元素。偶然使用一次的元素可能也不会被删除,考虑到这Redis5.0推出LFU的内存淘汰策略,使用最近访问频率来标定热度,考虑是否淘汰