实现 LRU 缓存
问题
如何用 Go 实现一个线程安全的 LRU(Least Recently Used)缓存?
答案
数据结构:双向链表 + 哈希表
- 哈希表: 查找
- 双向链表: 移动和删除
完整实现
type entry struct {
key string
value interface{}
prev *entry
next *entry
}
type LRUCache struct {
mu sync.Mutex
capacity int
items map[string]*entry
head *entry // 哨兵头
tail *entry // 哨兵尾
}
func NewLRUCache(capacity int) *LRUCache {
head := &entry{}
tail := &entry{}
head.next = tail
tail.prev = head
return &LRUCache{
capacity: capacity,
items: make(map[string]*entry),
head: head,
tail: tail,
}
}
func (c *LRUCache) Get(key string) (interface{}, bool) {
c.mu.Lock()
defer c.mu.Unlock()
if e, ok := c.items[key]; ok {
c.moveToFront(e) // 访问后移到头部
return e.value, true
}
return nil, false
}
func (c *LRUCache) Put(key string, value interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
if e, ok := c.items[key]; ok {
e.value = value
c.moveToFront(e)
return
}
// 新元素
e := &entry{key: key, value: value}
c.items[key] = e
c.addToFront(e)
// 超容量,淘汰最久未使用的
if len(c.items) > c.capacity {
c.removeLeast()
}
}
// 移到链表头部(最近访问)
func (c *LRUCache) moveToFront(e *entry) {
c.removeNode(e)
c.addToFront(e)
}
func (c *LRUCache) addToFront(e *entry) {
e.prev = c.head
e.next = c.head.next
c.head.next.prev = e
c.head.next = e
}
func (c *LRUCache) removeNode(e *entry) {
e.prev.next = e.next
e.next.prev = e.prev
}
func (c *LRUCache) removeLeast() {
e := c.tail.prev // 尾部前一个就是最久未使用的
c.removeNode(e)
delete(c.items, e.key)
}
带 TTL 的 LRU
type ttlEntry struct {
key string
value interface{}
expireAt time.Time
prev, next *ttlEntry
}
func (c *TTLLRUCache) Get(key string) (interface{}, bool) {
c.mu.Lock()
defer c.mu.Unlock()
e, ok := c.items[key]
if !ok {
return nil, false
}
// 检查是否过期
if time.Now().After(e.expireAt) {
c.removeNode(e)
delete(c.items, key)
return nil, false
}
c.moveToFront(e)
return e.value, true
}
func (c *TTLLRUCache) Put(key string, value interface{}, ttl time.Duration) {
// ... 同上,但设置 expireAt = time.Now().Add(ttl)
}
测试
func TestLRUCache(t *testing.T) {
cache := NewLRUCache(2)
cache.Put("a", 1)
cache.Put("b", 2)
v, ok := cache.Get("a")
assert.True(t, ok)
assert.Equal(t, 1, v)
cache.Put("c", 3) // 淘汰 b(最久未使用)
_, ok = cache.Get("b")
assert.False(t, ok) // b 已被淘汰
v, _ = cache.Get("c")
assert.Equal(t, 3, v)
}
常见面试问题
Q1: 为什么不直接用 map?
答案:map 没有顺序,无法知道哪个 key 最久未使用。链表维护访问顺序,map 提供快速查找,两者结合才能实现 的 LRU。
Q2: Go 的 container/list 能用吗?
答案:可以,但面试通常要求手写链表。container/list 使用 interface{},类型不安全且有拆箱开销,手写实现性能更好。
Q3: LRU 和 LFU 的区别?
答案:
- LRU(Least Recently Used):淘汰最久未访问的
- LFU(Least Frequently Used):淘汰访问次数最少的
- LRU 更简单常用,LFU 对频繁访问的数据更友好