本系列参考于 极客兔兔-7天用Go从零实现分布式缓存GeeCache,将从零开始实现一个分布式缓存,称为 DawnCache。
在本节实现了 LRU 缓存删除策略,最终的代码目录结构如下:
1 | lru/ |
缓存淘汰策略
当有新数据加入缓存,缓存的容量已经超过了最大容量,就需要将之前加入的缓存数据替换出去。常见的缓存淘汰策略如下:
- FIFO(First In First Out)先进先出:淘汰最先进入缓存的数据,这种策略局部性不好。
- LFU(Least Frequently Used)最久未使用:LFU 需要为每一条缓存记录维护一个计数器,每一次命中会使得计数器+1,每次淘汰访问次数最少的即可。但是 LRU 有一个缺点,就是若某条缓存在过去被频繁访问,即使最近不再被访问也不会被立即淘汰。
- LRU(Least Recently Used)最近最少未使用:LRU 认为,如果数据最近被访问过,那么将来被访问的概率也会更高。LRU 维护一个队列,如果某条记录被访问了,则移动到队头,那么队尾则是最近最少访问的数据,淘汰该条记录即可。
LRU 数据结构
LRU 的数据结构如下:
- 双向链表的顺序表示最近被访问的顺序,队头是最近被访问的数据,队尾是最久未被访问的数据。
- 字典记录了键值 key 与链表节点的映射关系,这样保证了查找和增删的时间复杂度不高。
LRU
lru/lru.go
实现 LRU
首先定义 LRU 缓存的结构体、链表节点值的结构体、value 的结构体:
1 | // Cache LRU 缓存 |
下面定义 Cache 的查找操作,当查找命中时将对应节点移动至双向链表的头部。
1 | // Get 获取键值key对应的value |
Cache 的删除操作,即删除最近最久未使用的节点(双向链表的尾部节点):
1 | // DeleteOldest 删除最近最久未使用的节点 即链表尾部的节点 |
Cache 的增加数据操作:
- 如果缓存里已有键值为key的数据,则更新value
- 没有键值为key的数据,新增一个节点,插入链表头部
1 | // Add 在 lru 缓存中添加数据 |
测试
lru/lru_test.go
下面是对 LRU 缓存的测试:
1 | package lru |