LRU算法介绍及简单实现

LRU算法(Least Recently Used)

最少最少使用是一种常见的页面置换算法。

LRU原理

原文链接

1 什么HASH算法

简单解释:哈希(Hash)算法,即散列函数。它是一种单向密码体制,即它是一个从明文到密文的不可逆的映射,只有加密过程,没有解密过程。 哈希算法可以将任意长度的二进制值引用为较短的且固定长度的二进制值,把这个小的二进制值称为哈希值。
常见的hash算法有

  • MD4
  • MD5
  • SHA-1

JAVA 中使用的Object.hashCode() 默认计算

public native int hashCode();

2 HASH一致性

什么是HASH一致性
  • 百度百科
    一致性哈希算法在1997年由麻省理工学院提出(参见扩展阅读[1]),设计目标是为了解决因特网中的热点(Hot spot)问题,初衷和CARP十分类似。一致性哈希修正了CARP使用的简单哈希算法带来的问题,使得DHT可以在P2P环境中真正得到应用。
  • 维基百科
    是一种特殊的哈希算法。在使用一致哈希算法后,哈希表槽位数(大小)的改变平均只需要对K/n个关键字重新映射,其中K是关键字的数量,n是槽位数量。然而在传统的哈希表中,添加或删除一个槽位的几乎需要对所有关键字进行重新映射。
HASH一致性解决什么问题

解决传统哈希扩容或者缩容时造成的hash映射关系的变化,本文主要以分布式系统设计来理解一致性算法。
[外链图片转存失败(img-mMjWyCJ5-1566527986485)(http://www.lgwen.cn/hash%E4%B8%80%E8%87%B4%E6%80%A7.png)]

数据存储,假设有三台服务器编号0,1,2
一条数据存储在哪里可以通过如下算法计算
h = hash(key) % 3
根据hash值对服务器数量取余,确定数据去那一台机器。
抽象成公式 h = Hash(key) % N 其中N代表服务器的数量,N=[0,N-1]。

这个算法的问题在于容错性和扩展性不好。所谓容错性是指当系统中某一个或几个服务器变得不可用时,整个系统是否可以正确高效运行;而扩展性是指当加入新的服务器后,整个系统是否可以正确高效运行。比如宕机了一台 0 号机,那么根据算法所得要去0号机的数据无法正常存储。那如何处理:

  1. 将宕机的的服务器从列表中移除使原先 N 变成 N-1 。
  2. 每个key按照h = hash(key) % (N-1) 从新计算一遍。
  3. 将数据按照重新计算的h在N-1台机器上重新分布。

如果增加一个节点也是一样的道理,需要从新计算分配数据。

实现原理

简单的描述一致性哈希,将所有的数据散列在一个[0,2^32)范围内一个圆圈上。以12点位置为0顺时针分配数据和节点,所有的node(节点)也分布在这个圆上。

1
一个圆圈上分布了个[0,2^32)个点,也就是最多存储Integer.MAX个hash值。

用n0-n2代表三个节点具体这三个节点怎么分布,可以通过编号0,1,2或者IP什么的反正可以计算出三个hash(编号/ip)分别插入到这个圆圈对应hash值的位置。
[外链图片转存失败(img-zhrg7Xyq-1566527986491)(http://lgwen.cn/upload/2019/8/2-41cf10b70e5149f591b43872de7c33c5.png)]

假设有两条数据分别是D1,D2。 要保存到对应的node,计算hash值后按照顺时针方向找到的第一个node节点即为存放数据的节点,(我觉得逆时针也行,反正就是朝着一个方向就行)。
[外链图片转存失败(img-MgbXFAwi-1566527986495)(http://lgwen.cn/upload/2019/8/3-d6f83a63c6304a1eae6fa58c53b03b21.png)]

删除增加删除节
  • 删除节点。假设删除节点node1 则需要将node上的数据顺时针方向迁移到下一个节点node2,D2->node2
    [外链图片转存失败(img-QHFhLRnL-1566527986498)(http://lgwen.cn/upload/2019/8/4-f81f291e40f74ec49e960bf58377e5ef.png)]
  • 增加节点。如图插入一个节点node3 将node1上hash值小于node4的数据迁移至node4上
    [外链图片转存失败(img-2cCaXIAH-1566527986499)(http://lgwen.cn/upload/2019/8/5-84f7148a142a477aa2f94b4460b1e12c.png)]
特性

一致性哈希满足以下四种性质。

  • 均衡性(Balance)
    均衡性也就是说负载均衡,是指客户端hash后的请求应该能够分散到不同的服务器上去。一致性hash可以做到每个服务器都进行处理请求,但是不能保证每个服务器处理的请求的数量大致相同。
  • 单调性(Monotonicity)
    单调性是指如果已经有一些请求通过哈希分派到了相应的服务器进行处理,又有新的服务器加入到系统中时候,应保证原有的请求可以被映射到原有的或者新的服务器中去,而不会被映射到原来的其它服务器上去。 这个通过上面新增服务器ip5可以证明,新增ip5后,原来被ip1处理的user6现在还是被ip1处理,原来被ip1处理的user5现在被新增的ip5处理。
  • 分散性(Spread)
    分布式环境中,客户端请求时候可能不知道所有服务器的存在,可能只知道其中一部分服务器,在客户端看来他看到的部分服务器会形成一个完整的hash环。如果多个客户端都把部分服务器作为一个完整hash环,那么可能会导致,同一个用户的请求被路由到不同的服务器进行处理。这种情况显然是应该避免的,因为它不能保证同一个用户的请求落到同一个服务器。所谓分散性是指上述情况发生的严重程度。
  • 负载(Load)
    负载问题实际上是从另一个角度看待分散性问题。既然不同的终端可能将相同的内容映射到不同的缓冲区中,那么对于一个特定的缓冲区而言,也可能被不同的用户映射为不同的内容。与分散性一样,这种情况也是应当避免的,因此好的哈希算法应能够尽量降低缓冲的负荷。
总结

相比于文章开头提到的算法,一致性hash算法能较好的解决节点的增加和删除,适应分布式系统。
核心解决节点变化带来的数据迁移问题。

发散思考

解决问题的同时,这个算法也会带来一些问题。数据分布不均匀。

  1. node无法按照设想的那样按照合适的间均匀分布在圆圈上。
  2. 对于节点变化(增/删)同样也会带来数据分布不均。

LRU工程实现

设计一个 即要取(get)元素,又需要插入(put)元素的缓存系统,我们可以结合hash(get 时间复杂度O(1) )+ 链表(insert 时间复杂度O(1) ) 的方式来实现。

public class LRUCache<K, V> {

    private Map<K, V> map = new HashMap<>();

    private LinkedList<K> linkedList = new LinkedList<>();

    //最大缓存
    private Integer maxCacheCapacity;

    public LRUCache(Integer maxCacheCapacity) {
        this.maxCacheCapacity = maxCacheCapacity;
    }

    public void put(K key, V value) {
        if (map.get(key) != null) {
            // 如果是已经存在的元素
            //将 队列中的key 从队列里面删除 然后添加到队首
            moveToHead(key);
        } else {
            //先判缓存是否满了
            if (linkedList.size() < maxCacheCapacity){
                linkedList.addFirst(key);
                map.put(key, value);
            } else {
                //剔除链表尾部的key,插入新的key
                K val = linkedList.removeLast();
                map.remove(val);
                map.put(key,value);
                linkedList.addFirst(key);
            }

        }
    }

    public V get (K key) {
        V value;
        if ((value = map.get(key)) != null) {
            //将 命中的value 从队列里面删除 然后添加到队首
            moveToHead(key);
        }
        return value;
    }

    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("{");
        if (map != null) {
            map.forEach( (x, v) ->
                    sb.append("[").append(x.toString()).append(":")
                            .append(v.toString())
                            .append("]").append(",")
            );
        }
        sb.append("}");
        return sb.toString();
    }

    // 移动命中的key到链表的顶端
    private void moveToHead(K key) {
        linkedList.remove(key);
        linkedList.addFirst(key);
    }
}

写个方法测试一下

   public static void main(String[] args) {
        LRUCache<Integer, String> cache = new LRUCache<>(5);
        cache.put(1, "1");
        cache.put(2, "2");
        cache.put(3, "3");
        System.out.println("get key 3: " + cache.get(3));
        System.out.println(cache.toString());
        cache.put(4, "4");
        cache.put(5, "5");
        cache.put(6, "6");
        System.out.println(cache.toString());
        System.out.println("get key 2: " + cache.get(2));
        cache.put(7, "7");
        System.out.println(cache.toString());
    }

输出

get key 3: 3
{[1:1],[2:2],[3:3],}
{[2:2],[3:3],[4:4],[5:5],[6:6],}
get key 2: 2
{[2:2],[4:4],[5:5],[6:6],[7:7],}

待完善的地方

  • 线程不安全

关注我的公众号

畅所欲言,共同进步
qrcode_for_gh_eac3d4651e58_344

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
LRU(Least Recently Used)算法是一种缓存淘汰算法,它的基本思想是根据数据的使用频率来淘汰最近最少使用的数据。以下是用C语言实现LRU算法的示例代码: ```c #include <stdio.h> #include <stdlib.h> // 定义LRU结点的结构体 typedef struct Node { int key; // 结点的键 int value; // 结点的值 struct Node* next; // 指向下一个结点的指针 struct Node* prev; // 指向前一个结点的指针 } Node; // 定义LRU缓存的结构体 typedef struct LRUCache { int capacity; // 缓存的容量 int size; // 缓存的当前大小 Node* head; // 指向链表头节点的指针 Node* tail; // 指向链表尾节点的指针 Node** hashmap; // 哈希表,保存结点的指针 } LRUCache; // 初始化LRU缓存 LRUCache* lRUCacheCreate(int capacity) { LRUCache* cache = (LRUCache*)malloc(sizeof(LRUCache)); cache->capacity = capacity; cache->size = 0; cache->head = NULL; cache->tail = NULL; cache->hashmap = (Node**)calloc(capacity, sizeof(Node*)); return cache; } // 获取LRU缓存中指定key对应的value int lRUCacheGet(LRUCache* obj, int key) { if (obj->hashmap[key] == NULL) { return -1; } // 将访问的结点移到链表头部 Node* node = obj->hashmap[key]; if (node != obj->head) { if (node == obj->tail) { obj->tail = obj->tail->prev; } else { node->next->prev = node->prev; node->prev->next = node->next; } node->next = obj->head; node->prev = NULL; obj->head->prev = node; obj->head = node; } return node->value; } // 向LRU缓存中插入新的key-value对 void lRUCachePut(LRUCache* obj, int key, int value) { if (obj->hashmap[key] == NULL) { // 如果缓存已满,删除最近最少使用的结点 if (obj->size == obj->capacity) { Node* tail = obj->tail; obj->hashmap[tail->key] = NULL; obj->tail = tail->prev; if (obj->tail != NULL) { obj->tail->next = NULL; } else { obj->head = NULL; } free(tail); obj->size--; } // 创建新的结点并加入链表头部 Node* node = (Node*)malloc(sizeof(Node)); node->key = key; node->value = value; node->next = obj->head; node->prev = NULL; if (obj->head != NULL) { obj->head->prev = node; } else { obj->tail = node; } obj->head = node; obj->hashmap[key] = node; obj->size++; } else { // 更新结点的值并移到链表头部 Node* node = obj->hashmap[key]; node->value = value; if (node != obj->head) { if (node == obj->tail) { obj->tail = obj->tail->prev; } else { node->next->prev = node->prev; node->prev->next = node->next; } node->next = obj->head; node->prev = NULL; obj->head->prev = node; obj->head = node; } } } // 释放LRU缓存的内存 void lRUCacheFree(LRUCache* obj) { Node* curr = obj->head; while (curr != NULL) { Node* tmp = curr->next; free(curr); curr = tmp; } free(obj->hashmap); free(obj); } int main() { LRUCache* cache = lRUCacheCreate(2); lRUCachePut(cache, 1, 1); lRUCachePut(cache, 2, 2); printf("%d\n", lRUCacheGet(cache, 1)); // 输出1 lRUCachePut(cache, 3, 3); printf("%d\n", lRUCacheGet(cache, 2)); // 输出-1 lRUCachePut(cache, 4, 4); printf("%d\n", lRUCacheGet(cache, 1)); // 输出-1 printf("%d\n", lRUCacheGet(cache, 3)); // 输出3 printf("%d\n", lRUCacheGet(cache, 4)); // 输出4 lRUCacheFree(cache); return 0; } ``` 以上代码是用C语言实现LRU算法简单示例,包括创建缓存、获取键对应的值、插入新的键值对以及释放内存的功能。代码中使用了双向链表和哈希表来实现LRU缓存,保证了缓存的快速查找和结点的插入/删除操作。在main函数中演示了LRU缓存的使用过程,并输出了一些操作的结果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值