NodeJS中的LRU缓存(CLOCK-2-hand)实现
转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。
原文参考:https://www.codeproject.com/Articles/5299328/LRU-Cache-CLOCK-2-hand-Implementation-In-NodeJS
在文章的开始我们需要了解什么是缓存?缓存是预先根据数据列表准备一些重要数据。没有缓存的话,系统的吞吐量就取决于存储速度最慢的数据,因此保持应用程序高性能的一个重要优化就是缓存。web应用程序中有两项很重要的工作,分别是文件和视频Blob的缓存和快速访问页面模板。而在NodeJS中,非异步功能操作的延迟会决定系统什么时候为其他客户端提供服务,尽管操作系统有自己的文件缓存机制,但是同一个服务器中有多个web应用程序同时运行,且其中一个应用正在传输大量视频数据的时候,其他应用的缓存内容就可能会频繁失效,此时程序效率会大幅降低。
而针对应用程序资源的LRU算法能有效解决这个问题,使应用程序不被同一服务器中的其他应用程序缓存所影响。考虑到存储速度最慢数据决系统吞吐量的这一点,LRU缓存的存在能将系统性能提高2倍至100倍;同时,异步LRU会隐藏全部高速缓存未命中的延迟。
接下来我们一起来看具体实现的内容。
代码展示
- 首先构建一个用来构造LRU对象模块的文件:
1 'use strict';
2 let Lru = function(cacheSize,callbackBackingStoreLoad,elementLifeTimeMs=1000){
3 let me = this;
4 let maxWait = elementLifeTimeMs;
5 let size = parseInt(cacheSize,10);
6 let mapping = {};
7 let mappingInFlightMiss = {};
8 let buf = [];
9 for(let i=0;i<size;i++)
10 {
11 let rnd = Math.random();
12 mapping[rnd] = i;
13 buf.push({data:"",visited:false, key:rnd, time:0, locked:false});
14 }
15 let ctr = 0;
16 let ctrEvict = parseInt(cacheSize/2,10);
17 let loadData = callbackBackingStoreLoad;
18 this.get = function(key,callbackPrm){
19
20 let callback = callbackPrm;
21 if(key in mappingInFlightMiss)
22 {
23 setTimeout(function(){
24 me.get(key,function(newData){
25 callback(newData);
26 });
27 },0);
28 return;
29 }
30
31 if(key in mapping)
32 {
33 // RAM speed data
34 if((Date.now() - buf[mapping[key]].time) > maxWait)
35 {
36 if(buf[mapping[key]].locked)
37 {
38 setTimeout(function(){
39 me.get(key,function(newData){
40 callback(newData);
41 });
42 },0);
43 }
44 else
45 {
46 delete mapping[key];
47
48 me.get(key,function(newData){
49 callback(newData);
50 });
51 }
52 }
53 else
54 {
55 buf[mapping[key]].visited=true;
56 buf[mapping[key]].time = Date.now();
57 callback(buf[mapping[key]].data);
58 }
59 }
60 else
61 {
62 // datastore loading + cache eviction
63 let ctrFound = -1;
64 while(ctrFound===-1)
65 {
66 if(!buf[ctr].locked && buf[ctr].visited)
67 {
68 buf[ctr].visited=false;
69 }
70 ctr++;
71 if(ctr >= size)
72 {
73 ctr=0;
74 }
75
76 if(!buf[ctrEvict].locked && !buf[ctrEvict].visited)
77 {
78 // evict
79 buf[ctrEvict].locked = true;
80 ctrFound = ctrEvict;
81 }
82
83 ctrEvict++;
84 if(ctrEvict >= size)
85 {
86 ctrEvict=0;
87 }
88 }
89
90 mappingInFlightMiss[key]=true;
91 let f = function(res){
92 delete mapping[buf[ctrFound].key];
93 buf[ctrFound] =
94 {data: res, visited:false, key:key, time:Date.now(), locked:false};
95 mapping[key] = ctrFound;
96 callback(buf[ctrFound].data);
97 delete mappingInFlightMiss[key];
98 };
99 loadData(key,f);
100 }
101 };
102 };
103
104 exports.Lru = Lru;
- 文件缓存示例:
1 let Lru = require("./lrucache.js").Lru;
2 let fs = require("fs");
3 let path = require("path");
4
5 let fileCache = new Lru(500, async function(key,callback){
6 // cache-miss data-load algorithm
7 fs.readFile(path.join(__dirname,key),function(err,data){
8 if(err) {
9 callback({stat:404, data:JSON.stringify(err)});
10 }
11 else
12 {
13 callback({stat:200, data:data});
14 }
15 });
16 },1000 /* cache element lifetime */);
使用LRU构造函数获取参数(高速缓存大小、高速缓存未命中的关键字和回调、高速缓存要素生命周期)来构造CLOCK高速缓存。
- 异步缓存未命中回调的工作方式如下:
1.一些get()在缓存中找不到密钥
2.算法找到对应插槽
3.运行此回调:
在回调中,重要计算异步完成
回调结束时,将回调函数的回调返回到LRU缓存中
4. 再次访问同一密钥的数据来自RAM
该依赖的唯一实现方法get():
1 fileCache.get("./test.js",function(dat){
2 httpResponse.writeHead(dat.stat);
3 httpResponse.end(dat.data);
4 });
结果数据还有另一个回调,因此可以异步运行
工作原理
- 现在大多LRU的工作过程始终存在从键到缓存槽的“映射”对象,就缓存槽的数量而言实现O(1)键搜索时间复杂度。但是用JavaScript就简单多了:
映射对象:
1 let mapping = {};
在映射中找到一个(字符串/整数)键:
1 if(key in mapping)
2 {
3 // key found, get data from RAM
4 }
高效且简单
- 只要映射对应一个缓存插槽,就可以直接从其中获取数据:
1 buf[mapping[key]].visited=true;
2 buf[mapping[key]].time = Date.now();
3 callback(buf[mapping[key]].data);
visited用来通知CLOCK指针(ctr和ctrEvict)保存该插槽,避免它被驱逐。time字段用来管理插槽的生命周期。只要访问到高速缓存命中都会更新time字段,把它保留在高速缓存中。
用户使用callback函数给get()函数提供用于检索高速缓存插槽的数据。
- 想要直接从映射插槽获取数据之前,需要先查看它的生命周期,如果生命周期已经结束,需要删除映射并用相同键重试使高速缓存丢失:
1 if((Date.now() - buf[mapping[key]].time) > maxWait)
2 {
3 delete mapping[key];
4 me.get(key,function(newData){
5 callback(newData);
6 });
7 }
删除映射后其他异步访问不会再影响其内部状态
- 如果在映射对象中没找到密钥,就运行LRU逐出逻辑寻找目标:
1 let ctrFound = -1;
2 while(ctrFound===-1)
3 {
4 if(!buf[ctr].locked && buf[ctr].visited)
5 {
6 buf[ctr].visited=false;
7 }
8 ctr++;
9 if(ctr >= size)
10 {
11 ctr=0;
12 }
13
14 if(!buf[ctrEvict].locked && !buf[ctrEvict].visited)
15 {
16 // evict
17 buf[ctrEvict].locked = true;
18 ctrFound = ctrEvict;
19 }
20
21 ctrEvict++;
22 if(ctrEvict >= size)
23 {
24 ctrEvict=0;
25 }
26 }
第一个“ if”块检查“第二次机会”指针(ctr)指向的插槽状态,如果是未锁定并已访问会将其标记为未访问,而不是驱逐它。
第三“If”块检查由ctrEvict指针指向的插槽状态,如果是未锁定且未被访问,则将该插槽标记为“ locked”,防止异步访问get() 方法,并找到逐出插槽,然后循环结束。
对比可以发现ctr和ctrEvict的初始相位差为50%:
1 let ctr = 0;
2 let ctrEvict = parseInt(cacheSize/2,10);
并且在“ while”循环中二者均等递增。这意味着,这二者循环跟随另一方,互相检查。高速缓存插槽越多,对目标插槽搜索越有利。对每个键而言,每个键至少停留超过N / 2个时针运动才从从逐出中保存。
- 找到目标插槽后,删除映射防止异步冲突的发生,并在加载数据存储区后重新创建映射:
1 mappingInFlightMiss[key]=true;
2 let f = function(res){
3 delete mapping[buf[ctrFound].key];
4 buf[ctrFound] = {data: res, visited:false, key:key, time:Date.now(), locked:false};
5 mapping[key] = ctrFound;
6 callback(buf[ctrFound].data);
7 delete mappingInFlightMiss[key];
8 };
9
10 loadData(key,f);
由于用户提供的缓存缺失数据存储加载功能(loadData)可以异步进行,所以该缓存在运行中最多可以包含N个缓存缺失,最多可以隐藏N个缓存未命中延迟。隐藏延迟是影响吞吐量高低的重要因素,这一点在web应用中尤为明显。一旦应用中出现了超过N个异步缓存未命中/访问就会导致死锁,因此具有100个插槽的缓存可以异步服务多达100个用户,甚至可以将其限制为比N更低的值(M),并在多次(K)遍中进行计算(其中M x K =总访问次数)。
我们都知道高速缓存命中就是RAM的速度,但因为高速缓存未命中可以隐藏,所以对于命中和未命中而言,总体性能看起来的时间复杂度都是O(1)。当插槽很少时,每个访问可能有多个时钟指针迭代,但如果增加插槽数时,它接近O(1)。
在此loadData回调中,将新插槽数据的locked字段设置为false,可以使该插槽用于其他异步访问。
- 如果存在命中,并且找到的插槽生命周期结束且已锁定,则访问操作setTimeout将0 time参数延迟到JavaScript消息队列的末尾。锁定操作(cache-miss)在setTimeout之前结束的概率为100%,就时间复杂度而言,仍算作具有较大的延迟的O(1),但它隐藏在锁定操作延迟的延迟的之后。
1 if(buf[mapping[key]].locked)
2 {
3 setTimeout(function(){
4 me.get(key,function(newData){
5 callback(newData);
6 });
7 },0);
8 }
- 最后,如果某个键处于进行中的高速缓存未命中映射中,则通过setTimeout将其推迟到消息队列的末尾:
1 if(key in mappingInFlightMiss)
2 {
3
4 setTimeout(function(){
5 me.get(key,function(newData){
6 callback(newData);
7 });
8 },0);
9 return;
10 }
这样,就可以避免数据的重复。
标杆管理
- 异步高速缓存未命中基准
1 "use strict";
2 // number of asynchronous accessors(1000 here) need to be equal to or less than
3 // cache size(1000 here) or it makes dead-lock
4 let Lru = require("./lrucache.js").Lru;
5
6 let cache = new Lru(1000, async function(key,callback){
7 // cache-miss data-load algorithm
8 setTimeout(function(){
9 callback(key+" processed");
10 },1000);
11 },1000 /* cache element lifetime */);
12
13 let ctr = 0;
14 let t1 = Date.now();
15 for(let i=0;i<1000;i++)
16 {
17 cache.get(i,function(data){
18 console.log("data:"+data+" key:"+i);
19 if(i.toString()+" processed" !== data)
20 {
21 console.log("error: wrong key-data mapping.");
22 }
23 if(++ctr === 1000)
24 {
25 console.log("benchmark: "+(Date.now()-t1)+" miliseconds");
26 }
27 });
28 }
为了避免死锁的出现,可以将LRU大小选择为1000,或者for只允许循环迭代1000次。
输出:
1 benchmark: 1127 miliseconds
由于每个高速缓存未命中都有1000毫秒的延迟,因此同步加载1000个元素将花费15分钟,但是重叠的高速缓存未命中会更快。这在I / O繁重的工作负载(例如来自HDD或网络的流数据)中特别有用。
- 缓存命中率基准
10%的命中率:
密钥生成:随机,可能有10000个不同的值
1000个插槽
1 "use strict";
2 // number of asynchronous accessors(1000 here) need to be equal to or less than
3 // cache size(1000 here) or it makes dead-lock
4 let Lru = require("./lrucache.js").Lru;
5
6 let cacheMiss = 0;
7 let cache = new Lru(1000, async function(key,callback){
8 cacheMiss++;
9 // cache-miss data-load algorithm
10 setTimeout(function(){
11 callback(key+" processed");
12 },100);
13 },100000000 /* cache element lifetime */);
14
15 let ctr = 0;
16 let t1 = Date.now();
17 let asynchronity = 500;
18 let benchRepeat = 100;
19 let access = 0;
20
21 function test()
22 {
23 ctr = 0;
24 for(let i=0;i<asynchronity;i++)
25 {
26 let key = parseInt(Math.random()*10000,10); // 10% hit ratio
27 cache.get(key.toString(),function(data){
28 access++;
29 if(key.toString()+" processed" !== data)
30 {
31 console.log("error: wrong key-data mapping.");
32 }
33 if(++ctr === asynchronity)
34 {
35 console.log("benchmark: "+(Date.now()-t1)+" miliseconds");
36 console.log("cache hit: "+(access - cacheMiss));
37 console.log("cache miss: "+(cacheMiss));
38 console.log("cache hit ratio: "+((access - cacheMiss)/access));
39 if(benchRepeat>0)
40 {
41 benchRepeat--;
42 test();
43 }
44 }
45 });
46 }
47 }
48
49 test();
结果:
1 benchmark: 10498 miliseconds
2 cache hit: 6151
3 cache miss: 44349
4 cache hit ratio: 0.1218019801980198
由于基准测试是按100个步骤进行的,每个缓存丢失的延迟时间为100毫秒,因此产生了10秒的时间(接近100 x 100毫秒)。命中率接近预期值10%。
50%命中率测试
1 let key = parseInt(Math.random()*2000,10); // 50% hit ratio
2
3 Result:
4
5 benchmark: 10418 miliseconds
6 cache hit: 27541
7 cache miss: 22959
8 cache hit ratio: 0.5453663366336634
99%命中率测试
1 let key = parseInt(Math.random()*1010,10); // 99% hit ratio
2
3 Result:
4
5 benchmark: 10199 miliseconds
6 cache hit: 49156
7 cache miss: 1344
8 cache hit ratio: 0.9733861386138614
结果产生了0.9733比率的键的随机性
100%命中率测试
1 let key = parseInt(Math.random()*999,10); // 100% hit ratio
基准测试的第一步(无法逃避缓存未命中)之后,所有内容都来自RAM,并大大减少了总延迟。
总结:
文本详细介绍了NodeJS中LRU算法缓存的实现,希望可以为大家提供新的思路,更好的在开发中提升系统性能。
NodeJS中的LRU缓存(CLOCK-2-hand)实现的更多相关文章
- 在NodeJS中使用Redis缓存数据
Redis数据库采用极简的设计思想,最新版的源码包还不到2Mb.其在使用上也有别于一般的数据库. node_redis redis驱动程序多使用 node_redis 此模块可搭载官方的 hiredi ...
- 常见面试题之操作系统中的LRU缓存机制实现
LRU缓存机制,全称Least Recently Used,字面意思就是最近最少使用,是一种缓存淘汰策略.换句话说,LRU机制就是认为最近使用的数据是有用的,很久没用过的数据是无用的,当内存满了就优先 ...
- [原创]mybatis中整合ehcache缓存框架的使用
mybatis整合ehcache缓存框架的使用 mybaits的二级缓存是mapper范围级别,除了在SqlMapConfig.xml设置二级缓存的总开关,还要在具体的mapper.xml中开启二级缓 ...
- Redis学习笔记2-使用 Redis 作为 LRU 缓存
当 Redis 作为缓存使用时,当你添加新的数据时,有时候很方便使 Redis 自动回收老的数据.LRU 实际上是被唯一支持的数据移除方法.Redis 的 maxmemory 指令,用于限制内存使用到 ...
- 在mapreduce中做分布式缓存的问题
一.问题描述: 主要解决一个问题,就是两个表做join,两个表都够大,单个表都无法装入内存. 怎么做呢?思路就是对做join的字段做排序两个表都排序,然后针对一个表a逐行读取,希望能够在内存中加载到另 ...
- 转: LRU缓存介绍与实现 (Java)
引子: 我们平时总会有一个电话本记录所有朋友的电话,但是,如果有朋友经常联系,那些朋友的电话号码不用翻电话本我们也能记住,但是,如果长时间没有联系了,要再次联系那位朋友的时候,我们又不得不求助电话本, ...
- volley三种基本请求图片的方式与Lru的基本使用:正常的加载+含有Lru缓存的加载+Volley控件networkImageview的使用
首先做出全局的请求队列 package com.qg.lizhanqi.myvolleydemo; import android.app.Application; import com.android ...
- nodeJS中读写文件方法的区别
导言:nodejs中所有与文件相关的操作都在fs模块中,而读写操作又是我们会经常用到的操作,nodejs的fs模块针对读操作为我们提供了readFile,read, createReadStream三 ...
- nodejs中的require,exports使用说明
模块是一门语言编写大项目的基石,因此,了解如何组织.编写.编译.加载模块很重要.这里主要谈谈Node中的模块加载. 1.Node中的模块,主要使用require来加载模块,文件 require(&qu ...
随机推荐
- 解决浏览器点击button出现边框问题
发现问题 本人不懂浏览器的HTML代码 不知道怎么在chrome浏览器的F12之后点到了哪里 点击button的时候就会出现黑色边框 解决 终于发现不是因为动了调试页面,而是动了谷歌浏览器的高级选项, ...
- HDOJ-6645(简单题+贪心+树)
Stay Real HDOJ-6645 由小根堆的性质可以知道,当前最大的值就在叶节点上面,所以只需要排序后依次取就可以了. #include<iostream> #include< ...
- System.Net.Mail邮件发送抄送附件(多个)
/// <summary> /// 邮件发送抄送附件 /// </summary> /// <param name="mailTo">收件人(可 ...
- [个人总结]pytorch中model.eval()会对哪些函数有影响?
来源于知乎:pytorch中model.eval()会对哪些函数有影响? - 蔺笑天的回答 - 知乎 https://www.zhihu.com/question/363144860/answer/9 ...
- 关于PHP的isset()函数
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title></title> 5 <meta cha ...
- 进阶宝典一|SqlServer数据库自动备份设置
很多人都没机会接触到数据库备份,经常操作的要么是数据库管理员,要么是项目负责人.那是不是说数据库备份就不用学了? 不,其实作为开发人员应该要了解数据备份,数据备份的手段有很多:软件备份.脚本备份.其他 ...
- 【Azure 应用服务】Azure App Service 自带 FTP服务
问题描述 Azure PaaS服务是否有FTP/S服务呢? 回答问题 应用服务(Web App/App Service)在创建时候,默认创建了FTP服务并自动开启,用于应用部署.但它不是适合作为FTP ...
- CF524F And Yet Another Bracket Sequence 题解
题目链接 算法:后缀数组+ST表+贪心 各路题解都没怎么看懂,只会常数巨大的后缀数组+ST表,最大点用时 \(4s\), 刚好可以过... 确定合法序列长度 首先一个括号序列是合法的必须满足以 ...
- 生成元(JAVA语言)
package 第三章; import java.util.Scanner; public class 生成元 { public static void main(String[] args) { / ...
- (二)SpringBoot启动过程的分析-环境信息准备
-- 以下内容均基于2.1.8.RELEASE版本 由上一篇SpringBoot基本启动过程的分析可以发现在run方法内部启动SpringBoot应用时采用多个步骤来实现,本文记录启动的第二个环节:环 ...