Shaula Blog

美丽的东西都是肤浅的

Swift - LRU 缓存

什么是 LRU 缓存?

LRU(Least Recently Used,最近最少使用)是一种缓存淘汰策略。当缓存满了,它会优先删除最久没被访问的数据,保留最近刚用过的

举个生活例子:你有一个只能放3本书的书架。每次读书都会把书放在最顺手的位置。当要放第4本书时,就把最角落(最久没读)的那本拿走。

核心设计:为什么用“双向链表 + 哈希表”?

  • 哈希表:让你能用 key 快速找到对应的数据(O(1))。
  • 双向链表:帮你记住数据的访问顺序,方便把刚访问的节点移到头部,并快速删除尾部的节点(O(1))。

两者结合,就能做到所有操作都在常数时间内完成。

链表结构(带虚拟头尾节点)

head <-> [最近使用] <-> ... <-> [最久未使用] <-> tail
  • headtail 是虚拟节点,不存数据,只为了方便操作,省去判断边界。

核心操作实现(Swift 示例)

1. 节点定义

private final class LRUNode<Key: Hashable, Value> {
    var key: Key?
    var value: Value?
    var prev: LRUNode<Key, Value>?
    var next: LRUNode<Key, Value>?

    init() {}
}

2. 缓存类

/// LRU(Least Recently Used)缓存
/// 当缓存容量达到上限时,淘汰最久未被使用的元素
public final class LRUCache<Key: Hashable, Value>: @unchecked Sendable {
    private let capacity: Int
    private var dict: [Key: LRUNode<Key, Value>] = [:]
    private let head: LRUNode<Key, Value>
    private let tail: LRUNode<Key, Value>
    private let lock = NSLock()

    /// 初始化 LRU 缓存
    /// - Parameter capacity: 缓存最大容量,必须大于 0
    public init(capacity: Int) {
        guard capacity > 0 else {
            fatalError("LRU 缓存容量必须大于 0")
        }
        self.capacity = capacity
        self.head = LRUNode<Key, Value>()
        self.tail = LRUNode<Key, Value>()
        head.next = tail
        tail.prev = head
    }
}

3. get(key) – 获取数据

/// 获取缓存值(存在则移到链表头部,标记为最近使用)
public func get(_ key: Key) -> Value? {
    lock.lock()
    defer { lock.unlock() }
    guard let node = dict[key] else {
        return nil
    }
    moveNodeToHead(node)
    return node.value
}

4. put(key, value) – 放入数据

/// 添加/更新缓存(满了则淘汰最久未使用的节点)
public func put(_ key: Key, value: Value) {
    lock.lock()
    defer { lock.unlock() }
    if let existingNode = dict[key] {
        existingNode.value = value
        moveNodeToHead(existingNode)
    } else {
        let newNode = LRUNode<Key, Value>()
        newNode.key = key
        newNode.value = value
        dict[key] = newNode
        addNodeToHead(newNode)
        if dict.count > capacity {
            if let removedNode = removeTailNode() {
                if let removedKey = removedNode.key {
                    dict.removeValue(forKey: removedKey)
                }
            }
        }
    }
}

5. 链表操作(核心原语)

/// 移除指定缓存
public func remove(_ key: Key) {
    lock.lock()
    defer { lock.unlock() }
    guard let node = dict[key] else { return }
    removeNode(node)
    dict.removeValue(forKey: key)
}

private func addNodeToHead(_ node: LRUNode<Key, Value>) {
    let temp = head.next
    head.next = node
    node.prev = head
    node.next = temp
    temp?.prev = node
}

private func removeNode(_ node: LRUNode<Key, Value>) {
    let prevNode = node.prev
    let nextNode = node.next
    prevNode?.next = nextNode
    nextNode?.prev = prevNode
}

private func moveNodeToHead(_ node: LRUNode<Key, Value>) {
    removeNode(node)
    addNodeToHead(node)
}

private func removeTailNode() -> LRUNode<Key, Value>? {
    guard let targetNode = tail.prev, targetNode !== head else {
        return nil
    }
    removeNode(targetNode)
    return targetNode
}

实际应用小贴士

  1. 容量设置:根据数据量合理设置,避免内存过大。
  2. 可监控:添加命中率、淘汰次数等统计,便于调试。
  3. 与系统缓存搭配:LRU 可作为 NSCache 的补充,实现更精准的淘汰策略。

总结

LRU 缓存通过哈希表 + 双向链表,实现了 O(1) 的读写和淘汰操作,是工程中最常用的缓存算法之一。理解它的原理,能让你在需要自己实现缓存时更有底气,也能更明智地选择已有的缓存方案。