LRU 算法(缓存淘汰)

170 阅读3分钟

背景

前阵子有个项目需要写个IP限制中间件,本想着用 AspNetCoreRateLimit 来解决就好,但要求挺独特需要在线状态什么,划分地区限制什么的,那这个中间件看起来就不太能满足我的需求了(研究不多)。没办法自己写个差不多的得了,因考虑到请求响应速率问题,肯定得使用缓存,请求多了使用缓存时不能让它无限膨胀,就要做大小的限制,所以就了解到了这个LRU算法,用以将超出大小限制的缓存进行淘汰,在此自己学完写完顺便整理下。

LRU描述

LRULeast recently used:最近最少使用),也看有称为最近最少使用页面置换算法。 

常作为缓存淘汰策略使用,当然除了 LRU,常见的缓存淘汰还有 FIFOfirst-in, first-out:先进先出) 和 LFULeast frequently used:最少使用),这里就不一一说了。

LRU 算法在缓存写满的时候,会根据所有数据的访问记录,淘汰掉未来被访问几率最低的数据。该算法认为,最近被访问过的数据,在将来被访问的几率最大。也就是说我们认为最近使用过的数据应该是“有用的”,很久都没用过的数据应该是“无用的”,内存满了就优先删那些很久没用过的数据。

LRU原理

首先我们可以把 cache 理解成一个有长度限制的队列,排序因子就是使用时间 假设左边是队头,右边是队尾 新元素直接压入队头,每当已有元素被使用,就直接提到左边队头,久而久之未使用的元素就在队尾,队列长度到达临界值就将队尾元素移除。

从上面的描述中就知道了我们需要的数据结构特性:

  • 有序:区分最近使用的和久未使用的数据;
  • 唯一Key:查找键是否已存在;容量满了要删除最后一个数据;每次访问要把数据插入到队头;
  • 查询效率快:用于缓存不快不行

那么,什么数据结构符合上述条件呢?

  • 链表:数据有序,插入删除快,但是查找慢;
  • 哈希表:数据无序,查找快,;

结合一下,形成一种新的数据结构:哈希链表。就是借助哈希表赋予了链表快速查找的特性嘛:可以快速查找某个 key 是否存在缓存(链表)中,同时可以快速删除、添加节点。这样读取,压入操作时间复杂度接近为 O(1);这种数据结构是不是很贴合 LRU 缓存的需求?

代码实现

基于C#的代码实现类(线程不安全


    public class LRUCache<TKey, TValue>
    {
        /// <summary>
        /// 双链表的Hash映射表
        /// </summary>
        private readonly Dictionary<TKey, LinkedNode<TKey, TValue>> _dicLinkedNode;
        /// <summary>
        /// 缓存临界值
        /// </summary>
        private readonly int _capacity;
        /// <summary>
        /// 链表头
        /// </summary>
        private readonly LinkedNode<TKey, TValue> _head;
        /// <summary>
        /// 链表尾
        /// </summary>
        private readonly LinkedNode<TKey, TValue> _tail;
        private int _count;

        public class LinkedNode<TNKey, TNValue>
        {
            public LinkedNode<TNKey, TNValue> Pre;
            public LinkedNode<TNKey, TNValue> Next;
            public TNKey Key;
            public TNValue Value;
        }

        public LRUCache(int capacity)
        {
            _dicLinkedNode = new Dictionary<TKey, LinkedNode<TKey, TValue>>();
            this._capacity = capacity;

            _head = new LinkedNode<TKey, TValue>();
            _tail = new LinkedNode<TKey, TValue>();
            _head.Next = _tail;
            _tail.Pre = _head;
            this._count = 0;
        }

        /// <summary>
        /// 添加到头部
        /// </summary> 
        private void AddToHead(LinkedNode<TKey, TValue> node)
        {
            node.Pre = _head;
            node.Next = _head.Next;

            _head.Next.Pre = node;
            _head.Next = node;
        }

        /// <summary>
        /// 移除
        /// </summary> 
        private static void RemoveNode(LinkedNode<TKey, TValue> node)
        {
            node.Pre.Next = node.Next;
            node.Next.Pre = node.Pre;
        }

        /// <summary>
        /// 从尾部移除
        /// </summary> 
        private LinkedNode<TKey, TValue> PopTail()
        {
            var node = _tail.Pre;
            RemoveNode(node);
            return node;
        }
         

        public TValue Get(TKey key)
        {
            if (_dicLinkedNode.ContainsKey(key))
            {
                var node = _dicLinkedNode[key];
                RemoveNode(node);
                AddToHead(node);
                return node.Value;
            }

            return default;
        }

        /// <summary>
        /// 压入
        /// </summary> 
        public void Push(TKey key, TValue value)
        {
            if (_dicLinkedNode.ContainsKey(key))
            {
                var node = _dicLinkedNode[key];
                node.Value = value;

                RemoveNode(node);
                AddToHead(node);
            }
            else
            {
                var node = new LinkedNode<TKey, TValue>();
                node.Value = value;
                node.Key = key;
                _dicLinkedNode.Add(key, node);

                if (_count < _capacity)
                {
                    AddToHead(node);
                    _count++;
                }
                else
                {
                    var delectNode = PopTail();
                    _dicLinkedNode.Remove(delectNode.Key);
                    AddToHead(node);
                }

            }

        }
    }

上面的例子只是简单演示了下根据原理的实现,实际上我们还要考虑线程安全的问题,众所周知C#的Dictionary集合非线程安全,大家可以自己根据提供的Demo实现下使用C#线程安全集合的LRU缓存类。

断断续续的整理归纳,感觉理解还不是很到位,权当记笔记了,毕竟好记性不如烂笔头。

C# 哈希表

C# 链表

C# 线程安全集合