Go语言实战案例-LRU缓存机制模拟

在高性能服务开发中,缓存是提升访问速度和减少后端负载的重要手段。常见的缓存淘汰策略中,**LRU(Least Recently Used,最近最少使用)**是应用最广的一种。本篇我们用Go语言手写一个LRU缓存机制的模拟实现。


一、LRU缓存机制简介

1. 定义

LRU缓存是一种固定容量的缓存结构。当缓存已满时,它会淘汰最近最少使用的那个数据。简单理解:

谁最久没被访问,就先删除谁。

2. 使用场景

  • • Web浏览器缓存
  • • 数据库查询结果缓存
  • • 操作系统页面置换

二、设计要求

LRU缓存应支持以下操作:

    1. Get(key):如果key存在,返回对应的value,并将该key标记为最近使用;否则返回-1。
    1. Put(key, value):插入或更新key。如果容量已满,需要删除最近最少使用的key。

要求两种操作都能在 O(1) 时间复杂度内完成。


三、核心数据结构

要实现 O(1) 操作,需要组合以下两种结构:

1. 哈希表(map)

  • • 用于存储 key → 节点 的映射;
  • • 可在 O(1) 时间内找到节点。

2. 双向链表

  • • 用于维护数据访问的顺序;
  • • 头部表示最近使用,尾部表示最久未使用;
  • • 插入、删除节点都是 O(1)。

四、Go语言实现

1. 节点结构

go 复制代码
type Node struct {
    key, value int
    prev, next *Node
}

2. LRU缓存结构

go 复制代码
type LRUCache struct {
    capacity int
    cache    map[int]*Node
    head     *Node
    tail     *Node
}
  • headtail 是哨兵节点(dummy),方便操作。

3. 初始化

bash 复制代码
func Constructor(capacity int) LRUCache {
    head := &Node{}
    tail := &Node{}
    head.next = tail
    tail.prev = head

    return LRUCache{
        capacity: capacity,
        cache:    make(map[int]*Node),
        head:     head,
        tail:     tail,
    }
}

4. 辅助方法

scss 复制代码
// moveToHead:将节点移动到头部
func (l *LRUCache) moveToHead(node *Node) {
    l.removeNode(node)
    l.addToHead(node)
}

// removeNode:删除链表中的节点
func (l *LRUCache) removeNode(node *Node) {
    prev := node.prev
    next := node.next
    prev.next = next
    next.prev = prev
}

// addToHead:在头部插入节点
func (l *LRUCache) addToHead(node *Node) {
    node.prev = l.head
    node.next = l.head.next
    l.head.next.prev = node
    l.head.next = node
}

// removeTail:删除尾部节点并返回它
func (l *LRUCache) removeTail() *Node {
    node := l.tail.prev
    l.removeNode(node)
    return node
}

5. 核心操作

Get

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)
    } else {
        newNode := &Node{key: key, value: value}
        l.cache[key] = newNode
        l.addToHead(newNode)

        if len(l.cache) > l.capacity {
            tail := l.removeTail()
            delete(l.cache, tail.key)
        }
    }
}

五、完整代码示例

go 复制代码
package main

import "fmt"

type Node struct {
    key, value int
    prev, next *Node
}

type LRUCache struct {
    capacity int
    cache    map[int]*Node
    head     *Node
    tail     *Node
}

func Constructor(capacity int) LRUCache {
    head := &Node{}
    tail := &Node{}
    head.next = tail
    tail.prev = head
    return LRUCache{
        capacity: capacity,
        cache:    make(map[int]*Node),
        head:     head,
        tail:     tail,
    }
}

func (l *LRUCache) Get(key int) int {
    if node, ok := l.cache[key]; ok {
        l.moveToHead(node)
        return node.value
    }
    return -1
}

func (l *LRUCache) Put(key int, value int) {
    if node, ok := l.cache[key]; ok {
        node.value = value
        l.moveToHead(node)
    } else {
        newNode := &Node{key: key, value: value}
        l.cache[key] = newNode
        l.addToHead(newNode)
        if len(l.cache) > l.capacity {
            tail := l.removeTail()
            delete(l.cache, tail.key)
        }
    }
}

func (l *LRUCache) moveToHead(node *Node) {
    l.removeNode(node)
    l.addToHead(node)
}

func (l *LRUCache) removeNode(node *Node) {
    prev := node.prev
    next := node.next
    prev.next = next
    next.prev = prev
}

func (l *LRUCache) addToHead(node *Node) {
    node.prev = l.head
    node.next = l.head.next
    l.head.next.prev = node
    l.head.next = node
}

func (l *LRUCache) removeTail() *Node {
    node := l.tail.prev
    l.removeNode(node)
    return node
}

func main() {
    cache := Constructor(2)
    cache.Put(1, 1)
    cache.Put(2, 2)
    fmt.Println(cache.Get(1)) // 1
    cache.Put(3, 3)           // 淘汰 key=2
    fmt.Println(cache.Get(2)) // -1
    cache.Put(4, 4)           // 淘汰 key=1
    fmt.Println(cache.Get(1)) // -1
    fmt.Println(cache.Get(3)) // 3
    fmt.Println(cache.Get(4)) // 4
}

六、复杂度分析

  • 时间复杂度:O(1),Get 和 Put 都只涉及哈希表查找和链表操作。
  • 空间复杂度:O(capacity),存储固定大小的map和链表节点。

七、工程实践与优化

    1. 线程安全
      在多协程环境中,需使用 sync.Mutexsync.RWMutex 保证安全。
    1. 泛型支持 (Go1.18+)
      可以用泛型实现支持任意类型的key/value。
    1. 监控统计
      可增加命中率统计、淘汰计数。

八、应用场景

  • 数据库缓存:Redis内部就支持LRU策略;
  • 浏览器缓存:网页资源加载优化;
  • API限速器:存储用户最近访问记录。

九、总结

  • • LRU缓存结合了 哈希表 + 双向链表
  • • 关键是 O(1) 时间内完成访问和淘汰
  • • 该思想可扩展到 LFU、ARC 等高级缓存策略。
相关推荐
毕业设计制作和分享20 分钟前
springboot150基于springboot的贸易行业crm系统
java·vue.js·spring boot·后端·毕业设计·mybatis
你的人类朋友5 小时前
【Node】认识multer库
前端·javascript·后端
lang201509286 小时前
Spring Boot 官方文档精解:构建与依赖管理
java·spring boot·后端
why技术7 小时前
从18w到1600w播放量,我的一点思考。
java·前端·后端
间彧7 小时前
Redis Cluster vs Sentinel模式区别
后端
间彧7 小时前
🛡️ 构建高可用缓存架构:Redis集群与Caffeine多级缓存实战
后端
间彧7 小时前
构建本地缓存(如Caffeine)+ 分布式缓存(如Redis集群)的二级缓存架构
后端
程序猿DD9 小时前
Java 25 中的 6 个新特性解读
java·后端
稻草猫.9 小时前
文件 IO
java·笔记·后端·java-ee·idea
掘金码甲哥9 小时前
有关CORS跨域访问,这事没完
后端