LRU淘汰算法

235 阅读4分钟

一. 缓存可以提高数据读取性能的技术, 比如 CPU 缓存、数据库缓存、浏览器缓存等等。

二. 缓存的大小有限,当缓空间存被用满时,哪些数据应该被清理出去,哪些数据应该被保留?这是需要缓存淘汰策略决定。我了解到的有如下几种:

  • 粗暴的先进先出策略 FIFO(First In,First Out)这里可以用类似浏览器的max-age, 或者干脆用队列保存push,shift.
  • 最少使用策略 LFU(Least Frequently Used)
  • 最近最少使用策略 LRU(Least Recently Used)

三. 核心思想是: “如果数据最近被访问过,那么将来被访问的几率也更高”

由于使用数组在头部插入元素需要的步数多,性能不好,所以采用链表来实现,顺带简单实现一下React Fiber的遍历思想。 首先我们先要实现功能完备一个链表:

  function NodeList () {
    this.head = new Node("head")
    this.length = 0 // 链表长度
  }
  NodeList.prototype.findNode = function (item) {
    let curNode = this.head
    while (curNode.data !== item) {
      curNode = curNode.next
    }
    return curNode
  }

  NodeList.prototype.deleteNode = function (item) {
    let curNode = this.head
    while (curNode.next !== null & curNode.next.data !== item) {
      curNode = curNode.next
    }
    if (curNode.next !== null) {
      curNode.next = curNode.next.next
    }
  }

  NodeList.prototype.insertNode = function (newData, data) {
    const newNode = new Node(newData)
    const findNode = this.findNode(data)
    newNode.next = findNode.next
    findNode.next = newNode
    this.length++
  }

  NodeList.prototype.consoleDisplay = function () {
    let curNode = this.head
    let num = 0
    while (curNode.next !== null) {
      num++
      console.log(num + '. ', curNode.next.data)
      curNode = curNode.next
    }
  }

  function Node (data) {
    this.data = data
    this.next = null
  }

  const nodelist = new NodeList()
  console.log('链表', nodelist)
  nodelist.insertNode("名字", 'head')
  nodelist.insertNode("性别", '名字')
  nodelist.insertNode("年龄 ", '性别')

  // 看看链表怎么排的
  nodelist.consoleDisplay()
  /**
   * 测试下打印结果:
   * 链表 NodeList { head: Node { data: 'head', next: null }, length: 0 }
   * 1.  名字
   * 2.  性别
   * 3.  年龄
   **/

四. 有了链表之后,我们再梳理下LRU缓存的过程。

  1. 如果数据存在于链表中,遍历获取这个节点,并将其从原位置删除,添加到链表的头部。
  2. 如果数据不存在于链表中,此时有两种可能:
  • 缓存未满,直接将新数据添加到链表头部。
  • 缓存已满,删除尾部节点,新数据添加到链表头部
  • 直接上代码,先声明一个LRU缓存类, 以及更新缓存空间的方法
  function LRUCache(maxSpace) {
  this.nodelist = new NodeList()
  this.cacheSpace = this.nodelist.length
  this.maxSpace = maxSpace
}
LRUCache.prototype.updateSpace = function () {
  this.cacheSpace = this.nodelist.length
  return this
}
  • 然后定义输入新内容的缓存方法cacheNode
LRUCache.prototype.cacheNode = function (item) {
  if (this.nodelist.findNode(item) !== null) {
    // 1. 如果数据存在于链表中,遍历获取这个节点,并将其从原位置删除,添加到链表的头部。
    this.nodelist.deleteNode(item).insertNode(item, 'head')
  } else {
    // 2. 如果数据不存在于链表中,此时有两种可能:
    if (this.cacheSpace < this.maxSpace) {
      // 2.1 缓存未满,直接将新数据添加到链表头部。
      this.nodelist.insertNode(item, 'head')
    } else {
      // 2.2 缓存已满,删除尾部节点,新数据添加到链表头部
      console.log("缓存已满,自动删除访问量最低的")
      this.deleteLast().insertNode(item, 'head')
    } 
  }
  // 更新缓存空间
  this.updateSpace()
  return this
}
  • 当遇到缓存空间满了的情况下,我们需要删除链表的尾部项
LRUCache.prototype.deleteLast = function () {
  let curNode = this.nodelist.head
  let preNode = null
  while(curNode.next !== null && curNode.next.data !== null) {
    preNode = curNode
    curNode = curNode.next
  }
  console.log(JSON.stringify(preNode), 1111111)
  // 倒数第二个节点next指针等于null
  preNode.next = null
  return this.nodelist
}
  • 最后定义打印缓存内容的方法
LRUCache.prototype.display = function () {
  console.log('缓存最大空间' + this.maxSpace + '个')
  this.nodelist.consoleDisplay()
}

const cache_LRU = new LRUCache(10) // 链表实体最长10个

五. 哈哈,代码搞定,到了我们最期待的验证环节

  1. 先输入名字,看打印结果。名字已经被缓存进入,并且打印最大缓存空间
  cache_LRU.cacheNode("名字")
  cache_LRU.display() 
  // 缓存最大空间10个
  // 1.  名字
  1. 输入性别,发现性别排在了名字的前边
  cache_LRU.cacheNode("名字")
  cache_LRU.cacheNode("性别")
  cache_LRU.display()
  // 缓存最大空间10个
  // 1.  性别
  // 2.  名字
  1. 输入年龄,发现年龄又被替换到了链表的头部,性别随后,最后是名字
  cache_LRU.cacheNode("名字")
  cache_LRU.cacheNode("性别")
  cache_LRU.cacheNode("年龄")
  cache_LRU.display() 
  // 缓存最大空间10个
  // 1.  年龄
  // 2.  性别
  // 3.  名字
  1. 这是输入一个已经有的名字, 看看名字是否会被从新加载到头部,变成Least Recently Used。
  cache_LRU.cacheNode("名字1")
  cache_LRU.cacheNode("名字2")
  cache_LRU.cacheNode("名字3")
  cache_LRU.cacheNode("名字4")
  cache_LRU.cacheNode("名字5")
  cache_LRU.cacheNode("名字6")
  cache_LRU.cacheNode("名字7")
  cache_LRU.cacheNode("名字8")
  cache_LRU.cacheNode("名字9")
  cache_LRU.cacheNode("名字10")
  cache_LRU.cacheNode("名字11")
  cache_LRU.display()
  // 缓存最大空间10个
  // 1.  名字
  // 2.  年龄
  // 3.  性别
  1. 最后一步,验证缓存满了会不会,淘汰最不常用的。发现名字1已经被缓存淘汰了,最后输入的名字11排在第一个
  cache_LRU.cacheNode("名字1")
  cache_LRU.cacheNode("名字2")
  cache_LRU.cacheNode("名字3")
  cache_LRU.cacheNode("名字4")
  cache_LRU.cacheNode("名字5")
  cache_LRU.cacheNode("名字6")
  cache_LRU.cacheNode("名字7")
  cache_LRU.cacheNode("名字8")
  cache_LRU.cacheNode("名字9")
  cache_LRU.cacheNode("名字10")
  cache_LRU.cacheNode("名字11")
  cache_LRU.display() 
  //  缓存最大空间10个
  // 1.  名字11
  // 2.  名字10
  // 3.  名字9
  // 4.  名字8
  // 5.  名字7
  // 6.  名字6
  // 7.  名字5
  // 8.  名字4
  // 9.  名字3
  // 10.  名字2