記事のディレクトリ
概念の理解
1.LRUは、仮想ページのストレージ管理サービスのための最低使用ページ置換アルゴリズムである最低使用頭字語は、ページングされたメモリの使用に基づいて意思決定を行うことです。唯一の「近未来」近似として「最近の過去」を使用することができ、各ページの将来の使用量を予測することができませんので、LRUアルゴリズムでは、最も最近使用されたページを段階的に廃止されます。
2.オペレーティングシステムのカリキュラムは学習している、古いコンテンツ戦略のうちの、シーンに十分なメモリではありません。LRU ...使用済み最小最近、頻繁に使用される最低を排除します。もう少し2は、最大かつ最も信頼性の高いストレージはハードディスクであるコンピュータアーキテクチャので、それは偉大な能力であり、メモリにロードされたコンテンツを使用する必要があるので、コンテンツを硬化させることができるが、アクセス速度が遅い、追加することができるよりも、メモリ高速だが容量が制限されており、内容は停電後に失われ、さらにまでのようなコンセプトのパフォーマンスだけでなく、内部のCPUのL1キャッシュ、L2キャッシュを改善しています。場所速いので、高いその単位コスト、小容量は、新しいコンテンツが常にロードされ、古いコンテンツは確かにそうなAの背景を使用することがあり、解消する必要があります。
LRU原則
あなたは、現在使用中のページのページ番号を保存するために、特別なスタックを使用することができます。ページにアクセスするための新しいプロセスは、ページ番号がスタックの最上部に圧入された場合、他のページ番号スタックシフトの下、そうでない場合は十分なメモリに、ページ番号の下を削除スタックします。このように、スタックは常に最も最近アクセスしたページ数であり、スタックの最下部には、少なくとも最近アクセスしたページのページ番号です。
オペレーティングシステムで一般的な標準的な教科書には、専用アクセスページで70120304の順に、3つのメモリページサイズに対応すると仮定すると、次のようにLRUの原理を実証するために使用されます。アクセスタイム方式に応じて、スタックメモリが、最近アクセスされ、次のLRU作業これにより、アクセスの遠い時間であり、上記で説明したと仮定する。
私たちは、非常に多くの可能な設計上の問題をデザインベースのLRUキャッシュを所有している場合でも、このメモリは、アクセス時間に従ってソートされ、そこにメモリコピー操作の多くも、そのパフォーマンスは確かに受け入れられないだろう。
だから、あなたは両方のO(1)の挿入および削除を行う、LRUキャッシュを設計んどのように、我々はそれらを維持するためのアクセス注文する必要がありますが、実際の反応のメモリにソートすることができない、解決策は、二重リンクリストを使用することであります。
LRUベースのHashMapと二重リンクリストを実現
ハッシュチェーンののLinkedHashMapにおけるJavaはうまくスレッドセーフを達成するために、我々は同期修飾子を追加する必要があり、これはスレッドセーフではないことに注意することが、達成されています。
HashMapの格納された鍵を使用することができる全体的な設計概念が、これは時間保存行われ、キーを取得しているO(1)、値およびハッシュマップは、図3に示すように、ノードは、実装ノードのLRU二重リンクリストを向けることができます。
RUストレージが二重にリンクされたリストに基づいて達成され、次の図は、それがどのように動作するかを示しています。前記hは二重リンクリストヘッダ、尾のTの代表を表します。新しいノードの最初、プリLRUセット容量メモリが満杯の場合、O(1)によって除去することができる時間毎に新しい及びアクセスデータの二重リンクリストのテールオフ、それはO(1)により、効率を向上させることができること頭部、または既存のチームヘッドへの移動ノード。
以下に示すように、デフォルトのサイズ、保存されたLRU 3を格納し、アクセスするプロセスの変化。図の複雑さを簡略化するために、図HashMapの変化部分に示されていない、図は変更のみLRU二重連結リストを示す図です。次のようにこの一連の動作上の私たちLRUキャッシュは次のとおりです。
(「KEY1」を、7)を保存
(「KEY2」を、0)を保存
(「KEY3」を、1)保存します
(「KEY4」を、2)保存
(「KEY2」)を取得します
(「KEY5を」、3)保存
(「KEY2」)を取得します
(「KEY6を」、4)セーブ
次のように一部の変更に対応するLRU二重リンクリスト:
のコアオペレーティング手順を要約すると:
- (キー、値)を保存、最初のノードに対応するノードが存在する場合、ノードは値を更新し、このノードのキューヘッドの移動、HashMapのキーに見出されます。新しいノード、ノードを構築し、詰めのチームヘッドをしようとする必要がない場合はスペースが不足している場合のHashMapでキーを除去しながら、LRUは、エンド・ノードのうちの尾が残しました。
- get(key),通过 HashMap 找到 LRU 链表节点,把节点插入到队头,返回缓存的值。
完整基于 Java 的代码参考如下
class DLinkedNode {
String key;
int value;
DLinkedNode pre;
DLinkedNode post;
}
LRU Cache
public class LRUCache {
private Hashtable<Integer, DLinkedNode>
cache = new Hashtable<Integer, DLinkedNode>();
private int count;
private int capacity;
private DLinkedNode head, tail;
public LRUCache(int capacity) {
this.count = 0;
this.capacity = capacity;
head = new DLinkedNode();
head.pre = null;
tail = new DLinkedNode();
tail.post = null;
head.post = tail;
tail.pre = head;
}
public int get(String key) {
DLinkedNode node = cache.get(key);
if(node == null){
return -1; // should raise exception here.
}
// move the accessed node to the head;
this.moveToHead(node);
return node.value;
}
public void set(String key, int value) {
DLinkedNode node = cache.get(key);
if(node == null){
DLinkedNode newNode = new DLinkedNode();
newNode.key = key;
newNode.value = value;
this.cache.put(key, newNode);
this.addNode(newNode);
++count;
if(count > capacity){
// pop the tail
DLinkedNode tail = this.popTail();
this.cache.remove(tail.key);
--count;
}
}else{
// update the value.
node.value = value;
this.moveToHead(node);
}
}
/**
* Always add the new node right after head;
*/
private void addNode(DLinkedNode node){
node.pre = head;
node.post = head.post;
head.post.pre = node;
head.post = node;
}
/**
* Remove an existing node from the linked list.
*/
private void removeNode(DLinkedNode node){
DLinkedNode pre = node.pre;
DLinkedNode post = node.post;
pre.post = post;
post.pre = pre;
}
/**
* Move certain node in between to the head.
*/
private void moveToHead(DLinkedNode node){
this.removeNode(node);
this.addNode(node);
}
// pop the current tail.
private DLinkedNode popTail(){
DLinkedNode res = tail.pre;
this.removeNode(res);
return res;
}
}
Redis的LRU实现
如果按照HashMap和双向链表实现,需要额外的存储存放 next 和 prev 指针,牺牲比较大的存储空间,显然是不划算的。所以Redis采用了一个近似的做法,就是随机取出若干个key,然后按照访问时间排序后,淘汰掉最不经常使用的,具体分析如下:
为了支持LRU,Redis 2.8.19中使用了一个全局的LRU时钟,server.lruclock,定义如下,
#define REDIS_LRU_BITS 24
unsigned lruclock:REDIS_LRU_BITS; /* Clock for LRU eviction */
默认的LRU时钟的分辨率是1秒,可以通过改变REDIS_LRU_CLOCK_RESOLUTION宏的值来改变,Redis会在serverCron()中调用updateLRUClock定期的更新LRU时钟,更新的频率和hz参数有关,默认为100ms一次,如下,
#define REDIS_LRU_CLOCK_MAX ((1<<REDIS_LRU_BITS)-1) /* Max value of obj->lru */
#define REDIS_LRU_CLOCK_RESOLUTION 1 /* LRU clock resolution in seconds */
void updateLRUClock(void) {
server.lruclock = (server.unixtime / REDIS_LRU_CLOCK_RESOLUTION) &
REDIS_LRU_CLOCK_MAX;
}
server.unixtime是系统当前的unix时间戳,当 lruclock 的值超出REDIS_LRU_CLOCK_MAX时,会从头开始计算,所以在计算一个key的最长没有访问时间时,可能key本身保存的lru访问时间会比当前的lrulock还要大,这个时候需要计算额外时间,如下,
/* Given an object returns the min number of seconds the object was never
* requested, using an approximated LRU algorithm. */
unsigned long estimateObjectIdleTime(robj *o) {
if (server.lruclock >= o->lru) {
return (server.lruclock - o->lru) * REDIS_LRU_CLOCK_RESOLUTION;
} else {
return ((REDIS_LRU_CLOCK_MAX - o->lru) + server.lruclock) *
REDIS_LRU_CLOCK_RESOLUTION;
}
}
Redis支持和LRU相关淘汰策略包括,
- volatile-lru 设置了过期时间的key参与近似的lru淘汰策略
- allkeys-lru 所有的key均参与近似的lru淘汰策略
当进行LRU淘汰时,Redis按如下方式进行的,
......
/* volatile-lru and allkeys-lru policy */
else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
{
for (k = 0; k < server.maxmemory_samples; k++) {
sds thiskey;
long thisval;
robj *o;
de = dictGetRandomKey(dict);
thiskey = dictGetKey(de);
/* When policy is volatile-lru we need an additional lookup
* to locate the real key, as dict is set to db->expires. */
if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
de = dictFind(db->dict, thiskey);
o = dictGetVal(de);
thisval = estimateObjectIdleTime(o);
/* Higher idle time is better candidate for deletion */
if (bestkey == NULL || thisval > bestval) {
bestkey = thiskey;
bestval = thisval;
}
}
}
......
Redis会基于server.maxmemory_samples配置选取固定数目的key,然后比较它们的lru访问时间,然后淘汰最近最久没有访问的key,maxmemory_samples的值越大,Redis的近似LRU算法就越接近于严格LRU算法,但是相应消耗也变高,对性能有一定影响,样本值默认为5。
实际运用
1. LRU算法也可以用于一些实际的应用中,如你要做一个浏览器,或类似于淘宝客户端的应用的就要用到这个原理。大家都知道浏览器在浏览网页的时候会把下载的图片临时保存在本机的一个文件夹里,下次再访问时就会,直接从本机临时文件夹里读取。但保存图片的临时文件夹是有一定容量限制的,如果你浏览的网页太多,就会一些你最不常使用的图像删除掉,只保留最近最久使用的一些图片。这时就可以用到LRU算法 了,这时上面算法里的这个特殊的栈就不是保存页面的序号了,而是每个图片的序号或大小;所以上面这个栈的元素都用Object类来表示,这样的话这个栈就可以保存的对像了。
2.漫画理解
用户信息当然是存在数据库里。但是由于我们对用户系统的性能要求比较高,显然不能每一次请求都去查询数据库。
所以,在内存中创建了一个哈希表作为缓存,每次查找一个用户的时候先在哈希表中查询,以此提高访问性能。
问题出现:
解决:
什么是哈希链表呢?
我们都知道,哈希表是由若干个Key-Value所组成。在“逻辑”上,这些Key-Value是无所谓排列顺序的,谁先谁后都一样。
在哈希链表当中,这些Key-Value不再是彼此无关的存在,而是被一个链条串了起来。每一个Key-Value都具有它的前驱Key-Value、后继Key-Value,就像双向链表中的节点一样。
这样一来,原本无序的哈希表拥有了固定的排列顺序。
让我们以用户信息的需求为例,来演示一下LRU算法的基本思路:
1.假设我们使用哈希链表来缓存用户信息,目前缓存了4个用户,这4个用户是按照时间顺序依次从链表右端插入的。
2.此时,业务方访问用户5,由于哈希链表中没有用户5的数据,我们从数据库中读取出来,插入到缓存当中。这时候,链表中最右端是最新访问到的用户5,最左端是最近最少访问的用户1。
3.次に、チェーンが存在し、ユーザー2、ユーザー2のデータのハッシュ営業サイドアクセス、私たちはどのように行うのですか?私たちは、ユーザ2が右端のリストに再挿入され、その前任者と後継者ノード間のノードから削除されて置きます。このとき、リストが最新2に極右のユーザーのアクセスとなり、ほとんどのユーザーは、まだ最近アクセス以上を残しました。
4.次に、ユーザサービスは、4情報の変更を要求します。同様に、我々は4人のユーザーがリストの右端、およびユーザ情報の更新の値に元の場所から移動しています。このとき、リストは右端ユーザ4のへの最新のアクセスまでで、一番左はまだ最近、ユーザ1がアクセスし、少なくともです。
5.その後、ビジネス側は、ユーザ6がキャッシュにない、味、とアクセスユーザ6を変更するには、ハッシュチェーンに挿入する必要があります。容量バッファこの時間と仮定すると限界に達している、あなたが最も最近アクセスしたデータを削除する必要があり、その後、ハッシュチェーンは、これまで1を削除した後、右端ユーザ6の中に挿入されますユーザーの左側に位置しています。
実用的なソリューション:Redisのキャッシュの使用は、ここでは原理を理解しています!
参考記事:
https://zhuanlan.zhihu.com/p/34133067
https://kuaibao.qq.com/s/20181105B0AS5O00?refer=cp_1026
https://blog.csdn.net/luoweifu/article/details/8297084