1、支持Put、Get的LRU实现

想要实现一个带过期时间的LRU,从易到难,我们需要先学会如何实现一个普通的LRU,做到O(1)的Get、Put。

想要做到O(1)的Get,我们很容易想到使用哈希表来存储每个key对应的value;要想实现O(1)的Put,并且能当容量满了的时候自动弹出最久未使用的元素,单纯使用哈希表是比较难实现的,因此我们可以使用一个双向链表头部存放最新被使用的节点尾部存放最久未使用的节点。那么哈希表只需要记录key到node的映射,就能让我们快速的追踪到节点在双向链表中的位置。

为了使得双向链表易于维护,我们可以增加两个哨兵节点,分别代表头部和尾部

到这里,我们就能确定实现一个简单操作的LRU需要涉及的数据结构:双向链表、哈希表

1.1、数据结构定义

现在,我们将基本的数据结构定义出来,并且定义个构造器函数

type Node struct {
Next, Pre *Node
key, val int
} type DoubleLinkList struct {
Head *Node
Tail *Node
} type LRUcache struct {
doubleList DoubleLinkList
KeyToNode map[int]*Node
cap, cnt int
} 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),
}
return lru
}

1.2、方法分析

我们可以先来考虑Put方法。当Put一个(key,value)对的时候,需要先检查是否存在对应的key,若存在,我们就只需要更新这个节点的值并且将节点移动至头部就可以了;若不存在,就需要创建一个节点,添加到头部,若LRUcache满了,就需要移除最久没使用的尾部节点

再来考虑Get方法,假若我们能获取到key对应的节点,那么就需要将这个节点更新至链表的头部,然后返回值即可;否则,直接返回-1.

从上面的分析我们不难发现,实现Get和Put方法,需要实现三个基本操作:移除一个节点、移除尾部节点、添加节点至头部

上面的方法,涉及到哈希表的值改变的,也需要一一处理。

接下来我们一一实现。

1.3、添加节点至头部

func (c *LRUcache) AddNode(node *Node) {
//哈希表注册这个节点
c.KeyToNode[node.key] = node
//添加到链表中,头节点之后
node.Next = c.doubleList.Head.Next
node.Pre = c.doubleList.Head
c.doubleList.Head.Next.Pre = node
c.doubleList.Head.Next = node
//更新表记录节点数
c.cnt++
}

执行过程:

  • 注册该节点到哈希表中
  • 将该节点的前后指针正确设置
  • 将原本的第一个节点的Pre指针设置为node
  • 将头节点的Next设置为node
  • 更新cnt计数器

1.4、移除节点

func (c *LRUcache) RemoveNode(node *Node) {
//哈希表删除节点记录
delete(c.KeyToNode, node.key)
//更新链表
node.Next.Pre = node.Pre
node.Pre.Next = node.Next
//更新计数器
c.cnt--
}

1.5、移除尾部节点

func (c *LRUcache) RemoveTail() {
//获取尾部节点
node := c.doubleList.Tail.Pre
//移除
c.RemoveNode(node)
}

1.6、Get()

接下来,就可以实现Get和Put方法了。先来实现一下Get

func (c *LRUcache) Get(key int) int{
//查询节点是否存在
if node, ok := c.KeyToNode[key]; ok{
//存在:从链表移除、添加到链表头部
c.RemoveNode(node)
c.AddNode(node)
return node.val
}else{
//不存在,返回-1
return -1
}
}

1.7、Put()

Put的执行流程也比较简单:

func (c *LRUcache) Put(key int, val int) {
//检查节点是否存在
if node, ok := c.KeyToNode[key]; ok {
//存在:更新节点的值、移除节点、添加节点到头部
node.val = val
c.RemoveNode(node)
c.AddNode(node)
} else {
//不存在,先检查容量是否上限
if c.cnt == c.cap {
c.RemoveTail() //移除尾部节点
}
//容量足够,添加节点
newNode := &Node{key: key, val: val}
c.AddNode(newNode)
}
}

至此,一个简单的LRU就实现了。

2、优雅实现带过期时间的LRU

现在,我们来考虑如何引入过期时间。

首先,我们当然要为每个node添加一个过期时间字段,来记录它什么时候会过期。

对于用户来说,为了保证数据是有效的,每次Get一个值,是不允许用户获取到一个过期对象的。这里可以采用一个懒更新的策略,当调用Get获取到一个节点的时候,应该检查是否已经过期,过期了就应该去除掉这个节点,给用户返回-1。

这样子,我们就已经对用户的值获取有了个最基本的保障,至少不会获取到已经过期的数据。接下来我们就要考虑,如何去实现节点的自动过期删除呢

要快速的获取到最早要过期的节点,我们可以引入一个过期时间最小堆,位于堆顶的是最早将要过期的节点;然后为了实现“自动管理”,我们可以引入一个协程去打理这个堆,每次发现节点过期了,就让这个协程自己去把节点清理掉就好了。这样子,可以做到当有节点过期了,只需要O(logn)的时间去清理掉节点,Put/Get主流程仍然只需要O(1)的时间复杂度,做到了“优雅高效”。

以为我们引入了协程,这就会有线程安全的问题,所以需要对LRU添加一把锁,来实现对操作的安全访问。

接着,我们希望当LRU存在节点的时候,再进行检查是否过期,为了防止协程长期自旋检查,我们可以为LRUcache添加一个sycn.Cond做到当没有节点的时候,就可以沉睡等待被唤醒。(一个优化的点)

接下来,我们一一修改时间。

2.1、数据结构修改

原本的node需要添加过期时间的记录,以及为了实现最小堆,需要添加index下标,记录在堆的位置。

type Node struct {
Next, Pre *Node
key, val int
index int
expire time.Time
}

接着是LRUcache,添加一个最小堆结构,然后还需要添加锁,以及sync.Cond。

附带实现这个最小堆(使用container/heap)

// 最小堆实现
type MinHeap []*Node func (h MinHeap) Less(i, j int) bool { return h[i].expire.Before(h[j].expire) }
func (h MinHeap) Len() int { return len(h) }
func (h MinHeap) Swap(i, j int) {
h[i], h[j] = h[j], h[i]
h[i].index, h[j].index = i, j
}
func (h *MinHeap) Push(x interface{}) {
n := h.Len()
node := x.(*Node)
node.index = n
*h = append(*h, node)
}
func (h *MinHeap) Pop() interface{} {
old := *h
n := old.Len()
node := old[n-1]
node.index = -1
*h = old[:n-1]
return node
}

然后是结构的修改,和构造器修改

type LRUcache struct {
doubleList DoubleLinkList
KeyToNode map[int]*Node
cap, cnt int expHeap MinHeap
mu sync.Mutex
expCond sync.Cond
}
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
}

2.2、清理协程实现

func (c *LRUcache) expirer() {
for {
//获取锁
c.mu.Lock()
//若列表没有节点,就沉睡等到被put唤醒
for c.expHeap.Len() == 0 {
c.expCond.Wait() //会自动释放锁,等待被唤醒后又尝试获取锁。
}
//获取堆顶节点
node := c.expHeap[0]
now := time.Now()
wait := node.expire.Sub(now)
//如果wait>0,说明还没有过期
if wait > 0 {
//沉睡到该节点过期
c.mu.Unlock()
time.Sleep(wait)
//醒来后,要重新执行一次流程
continue
}
//清理这个节点
heap.Pop(&c.expHeap)
c.RemoveNode(node)
c.mu.Unlock()
}
}

流程:

  • 获取锁
  • 检查队列是否为空,为空则等待被put唤醒
  • 获取堆顶节点,检查是否已经到达过期时间
    • 未到达过期时间,则沉睡,当被唤醒的时候,重新检查堆顶。
  • 到达了过期时间,进行清除操作

2.3、Get修改

现在引入了过期机制后,就需要检查是否过期,以及获取锁才能操作。

func (c *LRUcache) Get(key int) int {
//修改1
c.mu.Lock()
defer c.mu.Unlock()
//查询节点是否存在
if node, ok := c.KeyToNode[key]; ok {
//修改2,检查是否过期
if time.Now().After(node.expire) {
//过期了,协程还没有发现,手动帮助删除
heap.Remove(&c.expHeap, node.index)
c.RemoveNode(node)
return -1
}
//存在:从链表移除、添加到链表头部
c.RemoveNode(node)
c.AddNode(node)
return node.val
} else {
//不存在,返回-1
return -1
}
}

修改的点已经标记

2.4、Put修改

func (c *LRUcache) Put(key int, val int, ttl time.Duration) {
//修改1
c.mu.Lock()
defer c.mu.Unlock()
//修改2,获取过期时间
exp := time.Now().Add(ttl)
//检查节点是否存在
if node, ok := c.KeyToNode[key]; ok {
//存在:更新节点的值、移除节点、添加节点到头部
//修改3,重新设置过期时间
node.expire = exp node.val = val
c.RemoveNode(node)
c.AddNode(node) //修改4,heap需要fix
heap.Fix(&c.expHeap, node.index)
} else {
//不存在,先检查容量是否上限
if c.cnt == c.cap {
c.RemoveTail() //移除尾部节点
}
//容量足够,添加节点
newNode := &Node{key: key, val: val} //修改4,设置节点过期时间,添加到堆,唤醒协程
newNode.expire = exp
c.AddNode(newNode)
heap.Push(&c.expHeap, newNode)
c.expCond.Signal()
}
}

Put方法的流程:

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

于是,我们就完成了带过期时间的LRU。

2.5、代码全览

package main

import (
"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
} // 最小堆实现
type MinHeap []*Node func (h MinHeap) Less(i, j int) bool { return h[i].expire.Before(h[j].expire) }
func (h MinHeap) Len() int { return len(h) }
func (h MinHeap) Swap(i, j int) {
h[i], h[j] = h[j], h[i]
h[i].index, h[j].index = i, j
}
func (h *MinHeap) Push(x interface{}) {
n := h.Len()
node := x.(*Node)
node.index = n
*h = append(*h, node)
}
func (h *MinHeap) Pop() interface{} {
old := *h
n := old.Len()
node := old[n-1]
node.index = -1
*h = old[:n-1]
return node
} type LRUcache struct {
doubleList DoubleLinkList
KeyToNode map[int]*Node
cap, cnt int expHeap MinHeap
mu sync.Mutex
expCond sync.Cond
} 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() {
for {
//获取锁
c.mu.Lock()
//若列表没有节点,就沉睡等到被put唤醒
for c.expHeap.Len() == 0 {
c.expCond.Wait() //会自动释放锁,等待被唤醒后又尝试获取锁。
}
//获取堆顶节点
node := c.expHeap[0]
now := time.Now()
wait := node.expire.Sub(now)
//如果wait>0,说明还没有过期
if wait > 0 {
//沉睡到该节点过期
c.mu.Unlock()
time.Sleep(wait)
//醒来后,要重新执行一次流程
continue
}
//清理这个节点
heap.Pop(&c.expHeap)
c.RemoveNode(node)
c.mu.Unlock()
}
}
func (c *LRUcache) AddNode(node *Node) {
//哈希表注册这个节点
c.KeyToNode[node.key] = node
//添加到链表中,头节点之后
node.Next = c.doubleList.Head.Next
node.Pre = c.doubleList.Head
c.doubleList.Head.Next.Pre = node
c.doubleList.Head.Next = node
//更新表记录节点数
c.cnt++
} func (c *LRUcache) RemoveNode(node *Node) {
//哈希表删除节点记录
delete(c.KeyToNode, node.key)
//更新链表
node.Next.Pre = node.Pre
node.Pre.Next = node.Next
//更新计数器
c.cnt--
} func (c *LRUcache) RemoveTail() {
//获取尾部节点
node := c.doubleList.Tail.Pre
//移除
c.RemoveNode(node)
} func (c *LRUcache) Get(key int) int {
//修改1
c.mu.Lock()
defer c.mu.Unlock()
//查询节点是否存在
if node, ok := c.KeyToNode[key]; ok {
//修改2,检查是否过期
if time.Now().After(node.expire) {
//过期了,协程还没有发现,手动帮助删除
heap.Remove(&c.expHeap, node.index)
c.RemoveNode(node)
return -1
}
//存在:从链表移除、添加到链表头部
c.RemoveNode(node)
c.AddNode(node)
return node.val
} else {
//不存在,返回-1
return -1
}
} func (c *LRUcache) Put(key int, val int, ttl time.Duration) {
//修改1
c.mu.Lock()
defer c.mu.Unlock()
//修改2,获取过期时间
exp := time.Now().Add(ttl)
//检查节点是否存在
if node, ok := c.KeyToNode[key]; ok {
//存在:更新节点的值、移除节点、添加节点到头部
//修改3,重新设置过期时间
node.expire = exp node.val = val
c.RemoveNode(node)
c.AddNode(node) //修改4,heap需要fix
heap.Fix(&c.expHeap, node.index)
} else {
//不存在,先检查容量是否上限
if c.cnt == c.cap {
c.RemoveTail() //移除尾部节点
}
//容量足够,添加节点
newNode := &Node{key: key, val: val} //修改4,设置节点过期时间,添加到堆,唤醒协程
newNode.expire = exp
c.AddNode(newNode)
heap.Push(&c.expHeap, newNode)
c.expCond.Signal()
}
}
func (c *LRUcache) Print() {
for node := c.doubleList.Head; node != nil; node = node.Next {
fmt.Printf("%d -> ", node.key)
}
fmt.Println()
}
func main() {
cache := Constructor(2)
cache.Put(10, 101, time.Second)
cache.Print() time.Sleep(time.Second)
fmt.Println(cache.Get(10)) cache.Put(9, 99, time.Second*2)
cache.Print() cache.Put(7, 77, time.Second)
cache.Print() time.Sleep(2 * time.Second)
cache.Print()
}

3、测试

func (c *LRUcache) Print() {
for node := c.doubleList.Head; node != nil; node = node.Next {
fmt.Printf("%d -> ", node.key)
}
fmt.Println()
}
func main() {
cache := Constructor(2)
cache.Put(10, 101, time.Second)
cache.Print() time.Sleep(time.Second)
fmt.Println(cache.Get(10)) cache.Put(9, 99, time.Second*2)
cache.Print() cache.Put(7, 77, time.Second)
cache.Print() time.Sleep(2 * time.Second)
cache.Print()
}

输出如下:

0 -> 10 -> 0 ->
-1
0 -> 9 -> 0 ->
0 -> 7 -> 9 -> 0 ->
0 -> 0 ->

可以看到,过期的节点已经被成功自动删除了,同时原本的LRU功能保持不变。

Golang从0到1实现简易版expired LRU cache带图解的更多相关文章

  1. Golang简易版 网站路径扫描demo

    package main import ( "bufio" "fmt" "net/http" "os" "re ...

  2. .NET Core的文件系统[5]:扩展文件系统构建一个简易版“云盘”

    FileProvider构建了一个抽象文件系统,作为它的两个具体实现,PhysicalFileProvider和EmbeddedFileProvider则分别为我们构建了一个物理文件系统和程序集内嵌文 ...

  3. 简易版的TimSort排序算法

    欢迎探讨,如有错误敬请指正 如需转载,请注明出处http://www.cnblogs.com/nullzx/ 1. 简易版本TimSort排序算法原理与实现 TimSort排序算法是Python和Ja ...

  4. Python写地铁的到站的原理简易版

    Python地铁的到站流程及原理(个人理解) 今天坐地铁看着站牌就莫名的想如果用Python写其工作原理 是不是很简单就小试牛刀了下大佬们勿喷纯属小弟个人理解 首先来看看地铁上显示的站牌如下: 就想这 ...

  5. C+命令行+方向键=简易版扫雷

    前言: 想起来做这个是因为那时候某天知道了原来黑框框里面的光标是可以控制的,而且又经常听人说起这个,就锻炼一下好了. 之前就完成了那1.0的版本,现在想放上来分享却发现有蛮多问题的,而且最重要的是没什 ...

  6. MVC5+EF6 简易版CMS(非接口) 第三章:数据存储和业务处理

    目录 简易版CMS后台管理系统开发流程 MVC5+EF6 简易版CMS(非接口) 第一章:新建项目 MVC5+EF6 简易版CMS(非接口) 第二章:建数据模型 MVC5+EF6 简易版CMS(非接口 ...

  7. hdoj 2083 简易版之最短距离

    简易版之最短距离 Time Limit: 1000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Su ...

  8. Android学习之路——简易版微信为例(三)

    最近好久没有更新博文,一则是因为公司最近比较忙,另外自己在Android学习过程和简易版微信的开发过程中碰到了一些绊脚石,所以最近一直在学习充电中.下面来列举一下自己所走过的弯路: (1)本来打算前端 ...

  9. Android学习之路——简易版微信为例(二)

    1 概述 从这篇博文开始,正式进入简易版微信的开发.深入学习前,想谈谈个人对Android程序开发一些理解,不一定正确,只是自己的一点想法.Android程序开发不像我们在大学时候写C控制台程序那样, ...

  10. Android学习之路——简易版微信为例(一)

    这是“Android学习之路”系列文章的开篇,可能会让大家有些失望——这篇文章中我们不介绍简易版微信的实现(不过不是标题党哦,我会在后续博文中一步步实现这个应用程序的).这里主要是和广大园友们聊聊一个 ...

随机推荐

  1. 使用SOUI4中的STreeView控件

    STreeView控件是一个基于虚表技术实现的高性能树形控件. 和STreeCtrl这种传统的树形控件将数据和控件固定在一起不同,STreeView数据和控件分离,使用一个adapter进行连接. 用 ...

  2. rbd常用的配置参数

    本文分享自天翼云开发者社区<rbd常用的配置参数>,作者:l****n rbd的基本介绍 rbd的架构如下图所示: rbd采用CRUSH算法实现数据的随机分布.CRUSH算法,即Contr ...

  3. 服务器通用背板管理(UBM)实现

    本文分享自天翼云开发者社区<服务器通用背板管理(UBM)实现>,作者: 乘风 一 UBM概述 通过SGPIO 进行 SAS 和 SATA 背板管理的 SCSI 机箱服务 (SES) 标准于 ...

  4. IDEA中创建Spring Boot项目(SSM框架)

    一.IDEA创建新Maven项目 创建maven项目完成 因为创建多模块项目,删除根目录src目录 二.maven多模块项目配置 需要创建的模块 umetric-web  控制层 umetric-we ...

  5. Arduino语法--运算符

    本节介绍最常用的一些Arduino运算符,包括赋值运算符.算数运算符.关系运算符.逻辑运算符和递增/减运算符. 一. 赋值运算符 =(等于)为指定某个变量的值,例如:A=x,将x变量的值放入A变量. ...

  6. 解决黑群晖 Docker 日志八小时时间差的有效方法

    步骤一:登录黑群晖控制台 首先,我们需要登录到黑群晖控制台.可以通过SSH登录,或是直接在黑群晖控制台界面上操作. 步骤二:停止相关的Docker容器 在解决时间差问题之前,我们需要停止相关的Dock ...

  7. 生成式 AI 的发展方向,是 Chat 还是 Agent?

    一.整体介绍 生成式 AI 在当今科技领域的发展可谓是日新月异,其在对话系统(Chat)和自主代理(Agent)两个领域都取得了显著的成果. 在对话系统(Chat)方面,发展现状令人瞩目.当前,众多智 ...

  8. git上传大文件!git push 报错 ! [remote rejected] main -&gt; main (pre-receive hook declined) error_ failed to push some refs to &#39;xxx

    前言 今天在用git push项目的时候,出现了一个报错,记录一下解决方案,以后报同样的错误可以回来看. 错误信息 下面是git push的详细报错信息: 20866@DESKTOP-7R0VL04 ...

  9. 值得推荐的IT公司名单(国企篇)

    大家好,今天我们来盘点一下值得推荐的国企,这些企业在行业内具有举足轻重的地位,不仅主营业务突出,福利待遇优厚,尤其是研发岗位的薪资区间,更是让人眼前一亮. 十大顶尖央企国企,待遇优厚如天花板级别!(排 ...

  10. php处理跨域

    1.允许所有域名访问 header('Access-Control-Allow-Origin: *'); 2.允许单个域名访问 header('Access-Control-Allow-Origin: ...