背景
前阵子有个项目需要写个IP限制中间件,本想着用 AspNetCoreRateLimit 来解决就好,但要求挺独特需要在线状态什么,划分地区限制什么的,那这个中间件看起来就不太能满足我的需求了(研究不多)。没办法自己写个差不多的得了,因考虑到请求响应速率问题,肯定得使用缓存,请求多了使用缓存时不能让它无限膨胀,就要做大小的限制,所以就了解到了这个LRU算法,用以将超出大小限制的缓存进行淘汰,在此自己学完写完顺便整理下。
LRU描述
LRU (Least recently used:最近最少使用),也看有称为最近最少使用页面置换算法。
常作为缓存淘汰策略使用,当然除了 LRU,常见的缓存淘汰还有 FIFO(first-in, first-out:先进先出) 和 LFU(Least 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缓存类。
断断续续的整理归纳,感觉理解还不是很到位,权当记笔记了,毕竟好记性不如烂笔头。