用 Go 实现一个 LRU cache

Go语言实现LRU缓存
本文介绍如何使用Go语言实现LRU缓存,并应用于性能测试工具ptg中,以存储和检索请求历史记录。

609fd41f0292c4df4cbde3aba98be3b2.png

前言

早在几年前写过关于 LRU cache 的文章:https://crossoverjie.top/2018/04/07/algorithm/LRU-cache/

当时是用 Java 实现的,最近我在完善 ptg 时正好需要一个最近最少使用的数据结构来存储历史记录。

ptg: Performance testing tool (Go), 用 Go 实现的 gRPC 客户端调试工具。

Go 官方库中并没有相关的实现,考虑到程序的简洁就不打算依赖第三方库,自己写一个;本身复杂度也不高,没有几行代码。

配合这个数据结构,我便在 ptg 中实现了请求历史记录的功能:

将每次的请求记录存储到 lru cache 中,最近使用到的历史记录排在靠前,同时也能提供相关的搜索功能;具体可见下图。

b21574554ea17285ee016c751d727fae.gif

实现

284a132867f24bf561d3477901a3315c.png

实现原理没什么好说的,和 Java 的一样:

  • 一个双向链表存储数据的顺序

  • 一个 map 存储最终的数据

  • 当数据达到上限时移除链表尾部数据

  • 将使用到的 Node 移动到链表的头结点

虽然 Go 比较简洁,但好消息是基本的双向链表结构还是具备的。

60cc39bf7de2c04f8d95d2867a28bcb2.png

所以基于此便定义了一个 LruCache:

391055a057f4b3c00f8a559f2aba1937.png

根据之前的分析:

  • size 存储缓存大小。

  • 链表存储数据顺序。

  • map 存储数据。

  • lock 用于控制并发安全。

9d180a8620d10e4b5487077140c67c5d.png

接下来重点是两个函数:写入、查询。

写入时判断是否达到容量上限,达到后删除尾部数据;否则就想数据写入头部。

而获取数据时,这会将查询到的结点移动到头结点。

这些结点操作都由 List 封装好了的。98854d20949f0a616c30f1c92457950c.png

所以使用起来也比较方便。

最终就是通过这个 LruCache 实现了上图的效果,想要了解更多细节的可以参考源码:

https://github.com/crossoverJie/ptg/blob/main/gui/lru.go

ba8c2c6ee0e9afaa568155e1c37fafd5.png

撸了一个可调试 gRPC 的 GUI 客户端


3bd0b2232c9e47e8a06ff03a78a8138c.png

编写一个接口压测工具


091f2b9514a592e96398d6c87e51a025.png

效率提高80%,Go开发必备的库与工具!


3e2af95616ef104b5517eff7995ec5e3.png

DDD 到底是银弹还是垃圾


955a2140350ff8f1ee7ddf2fb879cd74.png

观察者模式的实际应用


90e32aec9e4845182a07bf2236e3860a.png

[]*T *[]T *[]*T 傻傻分不清楚


3d63bfc7ec2b7691c7646382e3a70674.png

点个在看你最好看

### 回答1: 我可以给你一些简单的步骤来实现LRU缓存:1. 使用一个哈希表来存储键和值的映射。2. 使用一个双端链表来存储键,以按照访问顺序排列。3. 如果键存在于缓存中,将其移动到双端链表的头部,表示这个键最近被访问了。4. 如果缓存容量已满,则将尾部的键删除,以便空出空间。 ### 回答2: 要用Go语言实现一个LRU(Least Recently Used)缓存,可以使用该语言提供的一些数据结构和函数来简化操作。 首先,我们需要创建一个结构体来表示缓存项。每个缓存项包含键和值两个字段。 ```go type CacheItem struct { key string value interface{} } ``` 接下来,我们创建一个结构体来表示LRU缓存,其中包含一个哈希表和一个双向链表。 ```go type LRUCache struct { capacity int cacheMap map[string]*list.Element cacheList *list.List } ``` 在LRUCache结构体中,哈希表cacheMap用于存储键和对应链表元素的映射关系,以便在O(1)时间复杂度内快速访问缓存项。双向链表cacheList用于存储实际的缓存项,其中最近被访问的项放在链表头部,最久未被访问的项放在链表尾部。 接下来,我们来实现LRUCache的一些方法: 1. 初始化缓存。 ```go func NewLRUCache(capacity int) *LRUCache { return &LRUCache{ capacity: capacity, cacheMap: make(map[string]*list.Element), cacheList: list.New(), } } ``` 2. 获取缓存项。 ```go func (c *LRUCache) Get(key string) (interface{}, bool) { if element, ok := c.cacheMap[key]; ok { c.cacheList.MoveToFront(element) return element.Value.(*CacheItem).value, true } return nil, false } ``` 3. 插入缓存项。 ```go func (c *LRUCache) Put(key string, value interface{}) { if element, ok := c.cacheMap[key]; ok { c.cacheList.MoveToFront(element) element.Value.(*CacheItem).value = value } else { if c.cacheList.Len() >= c.capacity { tail := c.cacheList.Back() delete(c.cacheMap, tail.Value.(*CacheItem).key) c.cacheList.Remove(tail) } element := c.cacheList.PushFront(&CacheItem{ key: key, value: value, }) c.cacheMap[key] = element } } ``` 在插入缓存项时,首先检查是否已存在该键。如果存在,则将缓存项移到链表头部,同时更新对应的值。如果不存在,则检查缓存容量是否已满,如果已满,则删除链表尾部的缓存项,并从哈希表中删除对应的键;然后,插入新的缓存项到链表头部,并在哈希表中添加对应的键和链表元素的映射关系。 这样,就可以用Go语言实现一个LRU缓存了。 ### 回答3: 使用Go语言实现一个LRU(Least Recently Used)缓存可以通过使用哈希表和双向链表实现。 首先,我们需要定义一个缓存结构体,其中包含一个哈希表和两个链表指针,分别用于存储缓存的键值对和维护访问顺序。 ```go type LRUCache struct { capacity int cache map[int]*Node head *Node tail *Node } type Node struct { key int value int prev *Node next *Node } ``` 在初始化LRU缓存时,我们需要指定缓存的容量并创建一个空的哈希表。 ```go func NewLRUCache(capacity int) *LRUCache { return &LRUCache{ capacity: capacity, cache: make(map[int]*Node), head: nil, tail: nil, } } ``` 接下来,我们需要实现四个核心的功能方法:获取缓存值、添加新的缓存值、移动已存在的缓存值到链表头部以及移除最久未使用的缓存。 首先,我们实现Get方法用于获取缓存值。当需要获取某个缓存值时,如果该值存在于哈希表中,我们需要将其移动到链表头部表示最近被访问过,并返回其值。否则,返回-1表示该缓存值不存在。 ```go func (l *LRUCache) Get(key int) int { if node, ok := l.cache[key]; ok { l.moveToHead(node) return node.value } return -1 } ``` 其次,我们实现Put方法用于添加新的缓存值。当需要添加新的缓存值时,首先判断该值是否已存在于哈希表中,如果存在,则更新该值的节点,并将节点移动到链表头部。如果不存在,检查缓存是否已满,如果已满,则需要移除链表尾部的节点,并从哈希表中删除相应的键。最后,创建一个新节点,并将其添加到链表头部和哈希表中。 ```go func (l *LRUCache) Put(key int, value int) { if node, ok := l.cache[key]; ok { node.value = value l.moveToHead(node) return } if len(l.cache) == l.capacity { delete(l.cache, l.tail.key) l.removeTail() } newNode := &Node{ key: key, value: value, prev: nil, next: nil, } l.addToHead(newNode) l.cache[key] = newNode } ``` 最后,我们需要实现两个辅助方法moveToHead和removeTail,分别用于将某个节点移动到链表头部和移除链表尾部的节点。 ```go func (l *LRUCache) moveToHead(node *Node) { if node == l.head { return } l.removeNode(node) l.addToHead(node) } func (l *LRUCache) removeTail() { if l.tail == nil { return } if l.tail == l.head { l.head = nil l.tail = nil return } l.tail = l.tail.prev l.tail.next = nil } func (l *LRUCache) removeNode(node *Node) { if node.prev != nil { node.prev.next = node.next } else { l.head = node.next } if node.next != nil { node.next.prev = node.prev } else { l.tail = node.prev } } func (l *LRUCache) addToHead(node *Node) { node.prev = nil node.next = l.head if l.head != nil { l.head.prev = node } l.head = node if l.tail == nil { l.tail = node } } ``` 至此,我们通过使用哈希表和双向链表实现一个LRU缓存。这样,我们可以在O(1)的时间复杂度下对缓存进行读和写操作。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值