找回密码
 立即注册
首页 业界区 业界 Golang从0到1实现简易版expired LRU cache带图解 ...

Golang从0到1实现简易版expired LRU cache带图解

峰邑 5 天前
1、支持Put、Get的LRU实现

想要实现一个带过期时间的LRU,从易到难,我们需要先学会如何实现一个普通的LRU,做到O(1)的Get、Put。
想要做到O(1)的Get,我们很容易想到使用哈希表来存储每个key对应的value;要想实现O(1)的Put,并且能当容量满了的时候自动弹出最久未使用的元素,单纯使用哈希表是比较难实现的,因此我们可以使用一个双向链表头部存放最新被使用的节点尾部存放最久未使用的节点。那么哈希表只需要记录key到node的映射,就能让我们快速的追踪到节点在双向链表中的位置。
为了使得双向链表易于维护,我们可以增加两个哨兵节点,分别代表头部和尾部
到这里,我们就能确定实现一个简单操作的LRU需要涉及的数据结构:双向链表、哈希表
1.png

1.1、数据结构定义

现在,我们将基本的数据结构定义出来,并且定义个构造器函数
  1. type Node struct {
  2.         Next, Pre *Node
  3.         key, val  int
  4. }
  5. type DoubleLinkList struct {
  6.         Head *Node
  7.         Tail *Node
  8. }
  9. type LRUcache struct {
  10.         doubleList DoubleLinkList
  11.         KeyToNode  map[int]*Node
  12.         cap, cnt   int
  13. }
  14. func Constructor(cap int) *LRUcache {
  15.         head := &Node{}
  16.         tail := &Node{}
  17.         head.Next = tail
  18.         tail.Pre = head
  19.         lru := &LRUcache{
  20.                 doubleList: DoubleLinkList{
  21.                         Head: head,
  22.                         Tail: tail,
  23.                 },
  24.                 cap:       cap,
  25.                 KeyToNode: make(map[int]*Node, cap),
  26.         }
  27.         return lru
  28. }
复制代码
1.2、方法分析

我们可以先来考虑Put方法。当Put一个(key,value)对的时候,需要先检查是否存在对应的key,若存在,我们就只需要更新这个节点的值并且将节点移动至头部就可以了;若不存在,就需要创建一个节点,添加到头部,若LRUcache满了,就需要移除最久没使用的尾部节点
再来考虑Get方法,假若我们能获取到key对应的节点,那么就需要将这个节点更新至链表的头部,然后返回值即可;否则,直接返回-1.
从上面的分析我们不难发现,实现Get和Put方法,需要实现三个基本操作:移除一个节点、移除尾部节点、添加节点至头部
上面的方法,涉及到哈希表的值改变的,也需要一一处理。
接下来我们一一实现。
1.3、添加节点至头部
  1. func (c *LRUcache) AddNode(node *Node) {
  2.         //哈希表注册这个节点
  3.         c.KeyToNode[node.key] = node
  4.         //添加到链表中,头节点之后
  5.         node.Next = c.doubleList.Head.Next
  6.         node.Pre = c.doubleList.Head
  7.         c.doubleList.Head.Next.Pre = node
  8.         c.doubleList.Head.Next = node
  9.         //更新表记录节点数
  10.         c.cnt++
  11. }
复制代码
执行过程:

  • 注册该节点到哈希表中
  • 将该节点的前后指针正确设置
  • 将原本的第一个节点的Pre指针设置为node
  • 将头节点的Next设置为node
  • 更新cnt计数器
1.4、移除节点
  1. func (c *LRUcache) RemoveNode(node *Node) {
  2.         //哈希表删除节点记录
  3.         delete(c.KeyToNode, node.key)
  4.         //更新链表
  5.         node.Next.Pre = node.Pre
  6.         node.Pre.Next = node.Next
  7.         //更新计数器
  8.         c.cnt--
  9. }
复制代码
1.5、移除尾部节点
  1. func (c *LRUcache) RemoveTail() {
  2.         //获取尾部节点
  3.         node := c.doubleList.Tail.Pre
  4.         //移除
  5.         c.RemoveNode(node)
  6. }
复制代码
1.6、Get()

接下来,就可以实现Get和Put方法了。先来实现一下Get
  1. func (c *LRUcache) Get(key int) int{
  2.         //查询节点是否存在
  3.         if node, ok := c.KeyToNode[key]; ok{
  4.                 //存在:从链表移除、添加到链表头部
  5.                 c.RemoveNode(node)
  6.                 c.AddNode(node)
  7.                 return node.val
  8.         }else{
  9.                 //不存在,返回-1
  10.                 return -1
  11.         }
  12. }
复制代码
1.7、Put()

Put的执行流程也比较简单:
  1. func (c *LRUcache) Put(key int, val int) {
  2.         //检查节点是否存在
  3.         if node, ok := c.KeyToNode[key]; ok {
  4.                 //存在:更新节点的值、移除节点、添加节点到头部
  5.                 node.val = val
  6.                 c.RemoveNode(node)
  7.                 c.AddNode(node)
  8.         } else {
  9.                 //不存在,先检查容量是否上限
  10.                 if c.cnt == c.cap {
  11.                         c.RemoveTail() //移除尾部节点
  12.                 }
  13.                 //容量足够,添加节点
  14.                 newNode := &Node{key: key, val: val}
  15.                 c.AddNode(newNode)
  16.         }
  17. }
复制代码
至此,一个简单的LRU就实现了。
2、优雅实现带过期时间的LRU

现在,我们来考虑如何引入过期时间。
首先,我们当然要为每个node添加一个过期时间字段,来记录它什么时候会过期。
对于用户来说,为了保证数据是有效的,每次Get一个值,是不允许用户获取到一个过期对象的。这里可以采用一个懒更新的策略,当调用Get获取到一个节点的时候,应该检查是否已经过期,过期了就应该去除掉这个节点,给用户返回-1。
这样子,我们就已经对用户的值获取有了个最基本的保障,至少不会获取到已经过期的数据。接下来我们就要考虑,如何去实现节点的自动过期删除呢
要快速的获取到最早要过期的节点,我们可以引入一个过期时间最小堆,位于堆顶的是最早将要过期的节点;然后为了实现“自动管理”,我们可以引入一个协程去打理这个堆,每次发现节点过期了,就让这个协程自己去把节点清理掉就好了。这样子,可以做到当有节点过期了,只需要O(logn)的时间去清理掉节点,Put/Get主流程仍然只需要O(1)的时间复杂度,做到了“优雅高效”。
以为我们引入了协程,这就会有线程安全的问题,所以需要对LRU添加一把锁,来实现对操作的安全访问。
接着,我们希望当LRU存在节点的时候,再进行检查是否过期,为了防止协程长期自旋检查,我们可以为LRUcache添加一个sycn.Cond做到当没有节点的时候,就可以沉睡等待被唤醒。(一个优化的点)
2.png

接下来,我们一一修改时间。
2.1、数据结构修改

原本的node需要添加过期时间的记录,以及为了实现最小堆,需要添加index下标,记录在堆的位置。
  1. type Node struct {
  2.         Next, Pre *Node
  3.         key, val  int
  4.         index int
  5.         expire time.Time
  6. }
复制代码
接着是LRUcache,添加一个最小堆结构,然后还需要添加锁,以及sync.Cond。
附带实现这个最小堆(使用container/heap)
  1. // 最小堆实现
  2. type MinHeap []*Node
  3. func (h MinHeap) Less(i, j int) bool { return h[i].expire.Before(h[j].expire) }
  4. func (h MinHeap) Len() int           { return len(h) }
  5. func (h MinHeap) Swap(i, j int) {
  6.         h[i], h[j] = h[j], h[i]
  7.         h[i].index, h[j].index = i, j
  8. }
  9. func (h *MinHeap) Push(x interface{}) {
  10.         n := h.Len()
  11.         node := x.(*Node)
  12.         node.index = n
  13.         *h = append(*h, node)
  14. }
  15. func (h *MinHeap) Pop() interface{} {
  16.         old := *h
  17.         n := old.Len()
  18.         node := old[n-1]
  19.         node.index = -1
  20.         *h = old[:n-1]
  21.         return node
  22. }
复制代码
然后是结构的修改,和构造器修改
  1. type LRUcache struct {
  2.         doubleList DoubleLinkList
  3.         KeyToNode  map[int]*Node
  4.         cap, cnt   int
  5.         expHeap MinHeap
  6.         mu      sync.Mutex
  7.         expCond sync.Cond
  8. }
复制代码
  1. func Constructor(cap int) *LRUcache {
  2.         head := &Node{}
  3.         tail := &Node{}
  4.         head.Next = tail
  5.         tail.Pre = head
  6.         lru := &LRUcache{
  7.                 doubleList: DoubleLinkList{
  8.                         Head: head,
  9.                         Tail: tail,
  10.                 },
  11.                 cap:       cap,
  12.                 KeyToNode: make(map[int]*Node, cap),
  13.         }
  14.         heap.Init(&lru.expHeap)//初始化堆
  15.         lru.expCond = *sync.NewCond(&lru.mu)//创建cond
  16.         go lru.expirer()
  17.         return lru
  18. }
复制代码
2.2、清理协程实现
  1. func (c *LRUcache) expirer() {
  2.         for {
  3.                 //获取锁
  4.                 c.mu.Lock()
  5.                 //若列表没有节点,就沉睡等到被put唤醒
  6.                 for c.expHeap.Len() == 0 {
  7.                         c.expCond.Wait() //会自动释放锁,等待被唤醒后又尝试获取锁。
  8.                 }
  9.                 //获取堆顶节点
  10.                 node := c.expHeap[0]
  11.                 now := time.Now()
  12.                 wait := node.expire.Sub(now)
  13.                 //如果wait>0,说明还没有过期
  14.                 if wait > 0 {
  15.                         //沉睡到该节点过期
  16.                         c.mu.Unlock()
  17.                         time.Sleep(wait)
  18.                         //醒来后,要重新执行一次流程
  19.                         continue
  20.                 }
  21.                 //清理这个节点
  22.                 heap.Pop(&c.expHeap)
  23.                 c.RemoveNode(node)
  24.                 c.mu.Unlock()
  25.         }
  26. }
复制代码
流程:

  • 获取锁
  • 检查队列是否为空,为空则等待被put唤醒
  • 获取堆顶节点,检查是否已经到达过期时间

    • 未到达过期时间,则沉睡,当被唤醒的时候,重新检查堆顶。

  • 到达了过期时间,进行清除操作
2.3、Get修改

现在引入了过期机制后,就需要检查是否过期,以及获取锁才能操作。
  1. func (c *LRUcache) Get(key int) int {
  2.         //修改1
  3.         c.mu.Lock()
  4.         defer c.mu.Unlock()
  5.         //查询节点是否存在
  6.         if node, ok := c.KeyToNode[key]; ok {
  7.                 //修改2,检查是否过期
  8.                 if time.Now().After(node.expire) {
  9.                         //过期了,协程还没有发现,手动帮助删除
  10.                         heap.Remove(&c.expHeap, node.index)
  11.                         c.RemoveNode(node)
  12.                         return -1
  13.                 }
  14.                 //存在:从链表移除、添加到链表头部
  15.                 c.RemoveNode(node)
  16.                 c.AddNode(node)
  17.                 return node.val
  18.         } else {
  19.                 //不存在,返回-1
  20.                 return -1
  21.         }
  22. }
复制代码
修改的点已经标记
2.4、RemoveTail修改

在将要修改的Put操作中,若节点未过期也需要被弹出链表,那么需要同步修改heap,防止到时候产生二次删除引发的指针越界问题。
  1. func (c *LRUcache) RemoveTail() {
  2.         //获取尾部节点
  3.         node := c.doubleList.Tail.Pre
  4.     heap.Remove(&c.expHeap, node.index) //从堆中删除这个节点
  5.         //移除
  6.         c.RemoveNode(node)
  7. }
复制代码
2.5、Put修改
  1. func (c *LRUcache) Put(key int, val int, ttl time.Duration) {
  2.         //修改1
  3.         c.mu.Lock()
  4.         defer c.mu.Unlock()
  5.         //修改2,获取过期时间
  6.         exp := time.Now().Add(ttl)
  7.         //检查节点是否存在
  8.         if node, ok := c.KeyToNode[key]; ok {
  9.                 //存在:更新节点的值、移除节点、添加节点到头部
  10.                 //修改3,重新设置过期时间
  11.                 node.expire = exp
  12.                 node.val = val
  13.                 c.RemoveNode(node)
  14.                 c.AddNode(node)
  15.                 //修改4,heap需要fix
  16.                 heap.Fix(&c.expHeap, node.index)
  17.         } else {
  18.                 //不存在,先检查容量是否上限
  19.                 if c.cnt == c.cap {
  20.                         c.RemoveTail() //移除尾部节点
  21.                 }
  22.                 //容量足够,添加节点
  23.                 newNode := &Node{key: key, val: val}
  24.                 //修改4,设置节点过期时间,添加到堆,唤醒协程
  25.                 newNode.expire = exp
  26.                 c.AddNode(newNode)
  27.                 heap.Push(&c.expHeap, newNode)
  28.                 c.expCond.Signal()
  29.         }
  30. }
复制代码
Put方法的流程:

  • 获取锁
  • 检查节点是否存在

    • 存在:进行节点的移除、节点值更新、节点添加、heap修复
    • 不存在:检查容量,容量超出则移除尾部节点;进行节点的创建,节点的添加,heap的添加,唤醒协程

于是,我们就完成了带过期时间的LRU。
2.6、代码全览
  1. package mainimport (        "container/heap"        "fmt"        "sync"        "time")type Node struct {        Next, Pre *Node        key, val  int        index     int        expire    time.Time}type DoubleLinkList struct {        Head *Node        Tail *Node}// 最小堆实现
  2. type MinHeap []*Node
  3. func (h MinHeap) Less(i, j int) bool { return h[i].expire.Before(h[j].expire) }
  4. func (h MinHeap) Len() int           { return len(h) }
  5. func (h MinHeap) Swap(i, j int) {
  6.         h[i], h[j] = h[j], h[i]
  7.         h[i].index, h[j].index = i, j
  8. }
  9. func (h *MinHeap) Push(x interface{}) {
  10.         n := h.Len()
  11.         node := x.(*Node)
  12.         node.index = n
  13.         *h = append(*h, node)
  14. }
  15. func (h *MinHeap) Pop() interface{} {
  16.         old := *h
  17.         n := old.Len()
  18.         node := old[n-1]
  19.         node.index = -1
  20.         *h = old[:n-1]
  21.         return node
  22. }type LRUcache struct {
  23.         doubleList DoubleLinkList
  24.         KeyToNode  map[int]*Node
  25.         cap, cnt   int
  26.         expHeap MinHeap
  27.         mu      sync.Mutex
  28.         expCond sync.Cond
  29. }func Constructor(cap int) *LRUcache {        head := &Node{}        tail := &Node{}        head.Next = tail        tail.Pre = head        lru := &LRUcache{                doubleList: DoubleLinkList{                        Head: head,                        Tail: tail,                },                cap:       cap,                KeyToNode: make(map[int]*Node, cap),        }        heap.Init(&lru.expHeap)              //初始化堆        lru.expCond = *sync.NewCond(&lru.mu) //创建cond        go lru.expirer()        return lru}func (c *LRUcache) expirer() {
  30.         for {
  31.                 //获取锁
  32.                 c.mu.Lock()
  33.                 //若列表没有节点,就沉睡等到被put唤醒
  34.                 for c.expHeap.Len() == 0 {
  35.                         c.expCond.Wait() //会自动释放锁,等待被唤醒后又尝试获取锁。
  36.                 }
  37.                 //获取堆顶节点
  38.                 node := c.expHeap[0]
  39.                 now := time.Now()
  40.                 wait := node.expire.Sub(now)
  41.                 //如果wait>0,说明还没有过期
  42.                 if wait > 0 {
  43.                         //沉睡到该节点过期
  44.                         c.mu.Unlock()
  45.                         time.Sleep(wait)
  46.                         //醒来后,要重新执行一次流程
  47.                         continue
  48.                 }
  49.                 //清理这个节点
  50.                 heap.Pop(&c.expHeap)
  51.                 c.RemoveNode(node)
  52.                 c.mu.Unlock()
  53.         }
  54. }func (c *LRUcache) AddNode(node *Node) {
  55.         //哈希表注册这个节点
  56.         c.KeyToNode[node.key] = node
  57.         //添加到链表中,头节点之后
  58.         node.Next = c.doubleList.Head.Next
  59.         node.Pre = c.doubleList.Head
  60.         c.doubleList.Head.Next.Pre = node
  61.         c.doubleList.Head.Next = node
  62.         //更新表记录节点数
  63.         c.cnt++
  64. }func (c *LRUcache) RemoveNode(node *Node) {
  65.         //哈希表删除节点记录
  66.         delete(c.KeyToNode, node.key)
  67.         //更新链表
  68.         node.Next.Pre = node.Pre
  69.         node.Pre.Next = node.Next
  70.         //更新计数器
  71.         c.cnt--
  72. }func (c *LRUcache) RemoveTail() {
  73.         //获取尾部节点
  74.         node := c.doubleList.Tail.Pre
  75.     heap.Remove(&c.expHeap, node.index) //从堆中删除这个节点
  76.         //移除
  77.         c.RemoveNode(node)
  78. }func (c *LRUcache) Get(key int) int {
  79.         //修改1
  80.         c.mu.Lock()
  81.         defer c.mu.Unlock()
  82.         //查询节点是否存在
  83.         if node, ok := c.KeyToNode[key]; ok {
  84.                 //修改2,检查是否过期
  85.                 if time.Now().After(node.expire) {
  86.                         //过期了,协程还没有发现,手动帮助删除
  87.                         heap.Remove(&c.expHeap, node.index)
  88.                         c.RemoveNode(node)
  89.                         return -1
  90.                 }
  91.                 //存在:从链表移除、添加到链表头部
  92.                 c.RemoveNode(node)
  93.                 c.AddNode(node)
  94.                 return node.val
  95.         } else {
  96.                 //不存在,返回-1
  97.                 return -1
  98.         }
  99. }func (c *LRUcache) Put(key int, val int, ttl time.Duration) {
  100.         //修改1
  101.         c.mu.Lock()
  102.         defer c.mu.Unlock()
  103.         //修改2,获取过期时间
  104.         exp := time.Now().Add(ttl)
  105.         //检查节点是否存在
  106.         if node, ok := c.KeyToNode[key]; ok {
  107.                 //存在:更新节点的值、移除节点、添加节点到头部
  108.                 //修改3,重新设置过期时间
  109.                 node.expire = exp
  110.                 node.val = val
  111.                 c.RemoveNode(node)
  112.                 c.AddNode(node)
  113.                 //修改4,heap需要fix
  114.                 heap.Fix(&c.expHeap, node.index)
  115.         } else {
  116.                 //不存在,先检查容量是否上限
  117.                 if c.cnt == c.cap {
  118.                         c.RemoveTail() //移除尾部节点
  119.                 }
  120.                 //容量足够,添加节点
  121.                 newNode := &Node{key: key, val: val}
  122.                 //修改4,设置节点过期时间,添加到堆,唤醒协程
  123.                 newNode.expire = exp
  124.                 c.AddNode(newNode)
  125.                 heap.Push(&c.expHeap, newNode)
  126.                 c.expCond.Signal()
  127.         }
  128. }func (c *LRUcache) Print() {
  129.         for node := c.doubleList.Head; node != nil; node = node.Next {
  130.                 fmt.Printf("%d -> ", node.key)
  131.         }
  132.         fmt.Println()
  133. }
  134. func main() {
  135.         cache := Constructor(2)
  136.         cache.Put(10, 101, time.Second)
  137.         cache.Print()
  138.         time.Sleep(time.Second)
  139.         fmt.Println(cache.Get(10))
  140.         cache.Put(9, 99, time.Second*2)
  141.         cache.Print()
  142.         cache.Put(7, 77, time.Second)
  143.         cache.Print()
  144.         time.Sleep(2 * time.Second)
  145.         cache.Print()
  146. }
复制代码
3、测试
  1. func (c *LRUcache) Print() {
  2.         for node := c.doubleList.Head; node != nil; node = node.Next {
  3.                 fmt.Printf("%d -> ", node.key)
  4.         }
  5.         fmt.Println()
  6. }
  7. func main() {
  8.         cache := Constructor(2)
  9.         cache.Put(10, 101, time.Second)
  10.         cache.Print()
  11.         time.Sleep(time.Second)
  12.         fmt.Println(cache.Get(10))
  13.         cache.Put(9, 99, time.Second*2)
  14.         cache.Print()
  15.         cache.Put(7, 77, time.Second)
  16.         cache.Print()
  17.         time.Sleep(2 * time.Second)
  18.         cache.Print()
  19. }
复制代码
输出如下:
  1. 0 -> 10 -> 0 ->
  2. -1
  3. 0 -> 9 -> 0 ->
  4. 0 -> 7 -> 9 -> 0 ->
  5. 0 -> 0 ->
复制代码
可以看到,过期的节点已经被成功自动删除了,同时原本的LRU功能保持不变。

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
您需要登录后才可以回帖 登录 | 立即注册