首页 用go实现一个LRU缓存
文章
取消

用go实现一个LRU缓存

LRU(Least Recently Used),是一种常用的缓存机制和算法,在数据数量达到缓存容量时最久没有访问的会优先淘汰。

简介

其形式如下:

lru机制.jpg

它有以下的需求:

  • 数据插入后,其优先级最高,不会被淘汰。
  • 数据查询后,由于最近被访问,优先级变最高,不会被淘汰。
  • 空间不足时,淘汰最久没有被访问的数据,直到空间充足。

常用的应用有:

  • 操作系统内存管理
  • redis 数据的淘汰
  • 手机 App 使用记录

实现设计

上面的特点可以哈希表和双链表这两种数据结构。哈希表的查询和删除的效率很高,都是\(O(1)\);双链表的数据插入、移动和删除效率很高,时间复杂度只有\(O(1)\)。

其实际形式如下:

lru组成.jpg

实现

先定义双链表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// LRU 双链表节点
type lruNode struct {
	key   string
	value interface{}
	pre   *lruNode
	next  *lruNode
}

// LRU 双链表,数据从尾部追加
type lruDoubleLinkedList struct {
	head *lruNode // 虚拟节点,不存数据
	tail *lruNode // 虚拟节点,不存数据
	size int
}

// 添加节点,加到头部
func (list *lruDoubleLinkedList) add(node *lruNode) {
	node.pre = list.tail.pre
	node.next = list.tail
	list.tail.pre.next = node
	list.tail.pre = node
	// 非空,在尾部添加
	list.size++
}

// 删除节点
func (list *lruDoubleLinkedList) remove(node *lruNode) {
	node.pre.next = node.next
	node.next.pre = node.pre
	list.size--
}

// 移除第一个节点head.next
func (list *lruDoubleLinkedList) removeFirst() *lruNode {
	if list.head.next == nil {
		// 没有
		return nil
	}
	// 删除第一个
	result := list.head.next
	list.remove(result)
	return result
}

定义LRU缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
type LRUCache struct {
	dataMap  map[string]*lruNode // map数据部分
	dataList lruDoubleLinkedList // 双链表部分
	capacity int                 // 容量
}

// 将指定key置为最近访问的
func (cache *LRUCache) makeRecent(key string) {
	if node, ok := cache.dataMap[key]; ok {
		cache.dataList.remove(node)
		cache.dataList.add(node)
	}
}

func (cache *LRUCache) Init(capacity int) {
	cache.capacity = capacity
	// 创建两个虚拟节点
	head := &lruNode{
		key:   "",
		value: nil,
		pre:   nil,
		next:  nil,
	}
	tail := &lruNode{
		key:   "",
		value: nil,
		pre:   nil,
		next:  nil,
	}
	head.next = tail
	tail.pre = head
	// 初始化双链表
	cache.dataList = lruDoubleLinkedList{
		head: head,
		tail: tail,
		size: 0,
	}
	// 初始化map
	cache.dataMap = make(map[string]*lruNode)
}

// 添加数据
func (cache *LRUCache) Put(key string, value interface{}) {

	if node, ok := cache.dataMap[key]; ok {
		// key 已经存在,更新
		node.value = value
		// 移到tail,表示最近使用的
		cache.makeRecent(key)
	} else {
		// 不存在,添加
		node := &lruNode{
			key:   key,
			value: value,
			pre:   nil,
			next:  nil,
		}
		cache.dataMap[key] = node
		cache.dataList.add(node)
		if cache.dataList.size > cache.capacity {
			// 超出容量,删除最旧的
			delNode := cache.dataList.removeFirst()
			delete(cache.dataMap, delNode.key)
		}
	}

}

// 读取指定数据
func (cache *LRUCache) Get(key string) interface{} {
	if node, ok := cache.dataMap[key]; ok {
		// 标记为最近使用
		cache.makeRecent(key)
		return node.value
	}
	return nil
}

// 获取所有数据
func (cache *LRUCache) ToList() []interface{} {
	result := make([]interface{}, cache.Length())
	node := cache.dataList.head.next
	idx := 0
	for node != cache.dataList.tail {
		result[idx] = node.value
		node = node.next
		idx++
	}
	return result
}

// 获取数据容量
func (cache *LRUCache) Capacity() int {
	return cache.capacity
}

// 获取数据长度
func (cache *LRUCache) Length() int {
	return cache.dataList.size
}

测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func main() {
	var cache LRUCache
	cache.Init(5)
	cache.Put("a", "apple")
	cache.Put("b", "big")
	cache.Put("c", "car")
	fmt.Printf("lru cache content:%v,length:%v \n", cache.ToList(), cache.Length())
	cache.Put("c", "cab") // 覆盖上面的2
	cache.Put("d", "dog")
	cache.Put("e", "eye")
	fmt.Printf("lru cache content:%v,length:%v \n", cache.ToList(), cache.Length())
	fmt.Printf("get c from cache:%v \n", cache.Get("c"))
	fmt.Printf("lru cache content:%v,length:%v \n", cache.ToList(), cache.Length())
	cache.Put("f", "fog") // 淘汰上面的 a
	fmt.Printf("lru cache content:%v,length:%v \n", cache.ToList(), cache.Length())
	fmt.Printf("get a non-exist key:x from cache:%v \n", cache.Get("x"))
}

输出:

1
2
3
4
5
6
lru cache content:[apple big car],length:3
lru cache content:[apple big cab dog eye],length:5
get c from cache:cab
lru cache content:[apple big dog eye cab],length:5
lru cache content:[big dog eye cab fog],length:5
get a non-exist key:x from cache:<nil>

这样一个基本的LRU缓存就做好了。

缩容改进

实际使用中可能需要在缓存容量满的时候缩减数据量到一个程度,以减少存储空间的长时间占用。我们可以设定一个比率来表示缩减后的最佳 数据长度/数据容量 比值。

结构体改进如下:

1
2
3
4
5
6
type LRUCache struct {
	dataMap   map[string]*lruNode // map数据部分
	dataList  lruDoubleLinkedList // 双链表部分
	capacity  int                 // 容量
	spaceRate float64             // 最佳 数据长度/容量 比例
}

初始化方法改进下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
func (cache *LRUCache) Init(capacity int, spaceRate float64) {
	if spaceRate <= 0 || spaceRate > 1 {
		panic("spaceRate should be in (0,1] interval")
	}
	cache.capacity = capacity
	// 创建两个虚拟节点
	head := &lruNode{
		key:   "",
		value: 0,
		pre:   nil,
		next:  nil,
	}
	tail := &lruNode{
		key:   "",
		value: 0,
		pre:   nil,
		next:  nil,
	}
	head.next = tail
	tail.pre = head
	// 初始化双链表
	cache.dataList = lruDoubleLinkedList{
		head: head,
		tail: tail,
		size: 0,
	}
	// 初始化map
	cache.dataMap = make(map[string]*lruNode, capacity)
}

添加数据方法改进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func (cache *LRUCache) Put(key string, value interface{}) {

	if node, ok := cache.dataMap[key]; ok {
		// key 已经存在,更新
		node.value = value
		// 移到tail,表示最近使用的
		cache.makeRecent(key)
	} else {
		// 不存在,添加
		node := &lruNode{
			key:   key,
			value: value,
			pre:   nil,
			next:  nil,
		}
		cache.dataMap[key] = node
		cache.dataList.add(node)
		if cache.dataList.size > cache.capacity {
			// 超出容量,删除最旧的
			properLength := int(float64(cache.capacity) * cache.spaceRate)
			for cache.Length() > properLength {
				delNode := cache.dataList.removeFirst()
				delete(cache.dataMap, delNode.key)
			}
		}
	}
}
本文由作者按照 CC BY 4.0 进行授权