再谈LRU双链表内存管理

N年前我写了个双链表也发了博客,还添了代码。但是那个代码不但复杂,而且还有有问题的,一直懒得整理,放在空间误导别人。最近在写服务端,今天抽点空补一篇。

关于LRU网上随便搜,有过后端经验的人应该很多都研究过。所谓双链表一个是哈希表,用于通过Key来查数据,另一个表是用来表示顺序,越前面的元素越新(也可以理解为越接近当前系统时间)。我以前写那个LRU,用一个哈希和一个数组,查哈希没什么问题,但是查数组用了indexof和splice就问题大了,呵呵,每次get数据都splice一次,那效率烂shi了。

正确的做法只需要一个哈希数组就可以了,另一个链表并不需要另开数组存,只需要给入库的哈希对象包一个新对象,新对象有prev和next即上一个下一个两节点即可表示先后顺序。另外再需要top和bottom两个变量来存头尾。

用一行代码表达:map[key] = {target=target,key=key,prev=XX,next=XX}。

最近在写lua,贴一段lua版本的lru

 

  1 -- 双链表LRU内存管理
  2 -- 充分利用空间来换时间查找和删除过期数据
  3 -- 哈希cacheMap用于主键存取查找,另一个链表是每个节点的prev和next来表示时间先后
  4 -- Author: Pelephone
  5 -- Date:2016-04-16 16:53:36
  6 
  7 LRUMgr = class(LRUMgr)
  8 
  9 -- 初始
 10 function LRUMgr:__init()
 11     -- 过期时间(多少秒之后过期)
 12     self.expireTime = 60*60*2
 13 
 14     -- 顶部节点,最新访问
 15     self.top = nil
 16     -- 最后节点,最旧的元素
 17     self.bottom = nil
 18 
 19     -- 过期时间(多少秒之后过期)
 20     self.expireTime = 60*60*2
 21     -- 最大缓存个数
 22     self.maxLen = 9999
 23 
 24     -- 目标对象的映射
 25     self.cacheMap = {}
 26      setmetatable(self.cacheMap,{__mode = "k"})
 27 
 28     -- 总共缓存的数量
 29     self.totLen = 0
 30 end
 31 
 32 -- 添加一个缓存对象
 33 function LRUMgr:set(key,target)
 34     local cacheObj = self.cacheMap[key]
 35     if not cacheObj then
 36         cacheObj = {key=key,target=target}
 37         self.cacheMap[key] = cacheObj
 38 
 39         if not self.top and not self.bottom then
 40             self.top = cacheObj
 41             self.bottom = cacheObj
 42         end
 43         self.totLen = self.totLen + 1
 44     end
 45 
 46     -- get一下放直队顶
 47     self:get(key)
 48 
 49     -- 超过最大缓存量,移出一下队尾
 50     if self.totLen > self.maxLen then
 51         self:remove(self.bottom.key)
 52     end
 53 end
 54 
 55 -- 获取缓存,返回对象的同时把对象移动队顶
 56 function LRUMgr:get(key)
 57     local cacheObj = self.cacheMap[key]
 58     if not cacheObj then
 59         return nil
 60     end
 61 
 62     if cacheObj == self.top then
 63         cacheObj.time = self:getNowTime()
 64         return cacheObj.target
 65     end
 66 
 67     -- 上下节点连接,然后把当前节放到队顶
 68     if cacheObj.prev and cacheObj.next then
 69         local tmpNext = cacheObj.prev
 70         cacheObj.prev.next = cacheObj.next
 71         cacheObj.next.prev = tmpNext
 72     end
 73 
 74     -- 新对象插入队头,队头是最新命中的节点
 75     if self.top then
 76         self.top.prev = cacheObj
 77     end
 78     cacheObj.next = self.top
 79     cacheObj.prev = nil
 80     self.top = cacheObj
 81     cacheObj.time = self:getNowTime()
 82     return cacheObj.target
 83 end
 84 
 85 -- 移出缓存
 86 function LRUMgr:remove(key)
 87     local cacheObj = self.cacheMap[key]
 88     if not cacheObj then
 89         return nil
 90     end
 91 
 92     -- 上下节点连接,然后把当前节放到队顶
 93     if cacheObj == self.top then
 94         self.top = self.top.next
 95         if self.top then
 96             self.top.prev = nil
 97         end
 98         if self.totLen == 1 then
 99             self.bottom = nil
100         end
101     elseif cacheObj == self.bottom then
102         self.bottom = self.bottom.prev
103         if self.bottom then
104             self.bottom.next = nil
105         end
106         if self.totLen == 1 then
107             self.top = nil
108         end
109     else
110         local tmpNext = cacheObj.prev
111         cacheObj.prev.next = cacheObj.next
112         cacheObj.next.prev = tmpNext
113     end
114     self.totLen = self.totLen - 1
115     self.cacheMap[key] = nil
116     cacheObj.prev = nil
117     cacheObj.next = nil
118     cacheObj.target = nil
119 end
120 
121 -- 清理过期对象
122 function LRUMgr:clearExpire()
123     local nExpireTime = self:getNowTime() - self.expireTime
124     -- 从队尾开始删除缓存,直到删到没到期的对象
125     while self.totLen > 0 and self.bottom.time < nExpireTime do
126         local newBtm = self.bottom.prev
127         if newBtm then
128             newBtm.next = nil
129         end
130 
131         self.cacheMap[self.bottom.key] = nil
132         self.bottom.prev = nil
133         self.bottom.next = nil
134         self.bottom.target = nil
135 
136         self.totLen = self.totLen - 1
137         self.bottom = newBtm
138     end
139 end
140 
141 -- 清除所有缓存
142 function LRUMgr:removeALl()
143     -- for k,v in pairs(self.cacheMap) do
144     --     self.cacheMap[k] = nil
145     -- end
146     self.cacheMap = {}
147      setmetatable(self.cacheMap,{__mode = "k"})
148      self.top = nil
149      self.bottom = nil
150 end
151 
152 -- 获取当前时间点
153 function LRUMgr:getNowTime()
154     return os.time()
155 end
156 
157 -- 获取缓存长度
158 function LRUMgr:getLength()
159     return self.totLen
160 end
161 
162 -- 创建一次数组返回(此方法有性能问题,甚用,仅用于查看顺序)
163 function LRUMgr:getList()
164     if self.totLen == 0 then
165         return {}
166     end
167 
168     local ls = {}
169     local cacheObj = self.top
170     table.insert(ls,cacheObj.target)
171     while cacheObj.next ~= nil do
172         table.insert(ls,cacheObj.next.target)
173         cacheObj = cacheObj.next
174     end
175     return ls
176 end
lua lru

 

 

对象池的话也可以在这个的基础上封装,代码就懒得粘了。 

除了双链外我以前还搞过一种时间块三链的存储结构,性能效率也不错,不过算法有些复杂,也不知道是不是我独创,总之网是搜不到。思路是把缓存分时间块存取,例如十分钟内的缓存在第一块,十到二十分钟的缓存在第二块,类堆。每次访问缓存就把缓存对象放到最新的时间块,过期处理是把过期时间块里所有缓存对象清了,例如五十到六十分钟时间块过期了,就把时间块置空即可,时间块LRU的好处是十分钟内的缓存被访问是不需要进行上下节点处理的,而且清内存的时候不需要对多个对象进行置空清除,只需要对时间块清除即可。

具体做法是取当前时间戳除以一个时间段数值(例如十分钟是60*10),取整数部份做为时间块的id,用这个id做为这个时间段的内存块加入链表头。每调用对象就把对象放到放到最新的时间块去。这个方法不是判断对象过期,而是判断时间块过期。时间块过期就把块id对应的对象置空。懒筋抽搐,改天有空再弄上来。

转载于:https://www.cnblogs.com/pelephone/p/lru_lua.html

### 实现LRU缓存机制中双向链表的头尾指针变化 为了满足最常使用的操作(`get` 和 `put`)平均时间复杂度为O(1)[^5]的要求,LRU缓存通常采用哈希表配合双向链表的方式实现。其中: #### 双向链表的作用 - **头部节点**代表最近访问的数据项。 - **尾部节点**表示最久未被访问的数据项。 当执行特定的操作时,会涉及到调整这两个特殊节点的位置关系以及它们所指向的具体数据项的变化情况如下: #### 添加新元素至缓存内 如果当前容量尚未达到上限,则直接创建新的节点并将之插入到链表前端作为最新的访问记录;此时只需更新前驱结点该新增加单元之间的连接即可完成整个过程[^2]。 ```java // 插入节点到双向链表头部 private void addToHead(Node node) { node.prev = head; node.next = head.next; head.next.prev = node; head.next = node; } ``` #### 移除已有元素 对于超出设定大小限制的情况或是显式调用了删除接口的情形下,应当移除位于列表末端的那个条目——因为它对应着距离现在最远的一次读取/写入动作[^3]。 ```java // 删除指定节点 private void removeNode(Node node){ node.prev.next = node.next; node.next.prev = node.prev; } // 尾删法淘汰最不常用的页面 private void popTail(){ removeNode(tail.prev); } ``` #### 更新现有元素的状态 每当某个已存在于缓存里的key再次发生交互行为之后,就需要将其重新定位成最新一次触及的对象。这一步骤具体表现为先断开旧有的前后关联再依照前述方法挂载于队首位置之上[^4]。 ```java // 改变一个已经存在的节点成为最近使用过的节点 private void moveToHead(Node node){ removeNode(node); // 先把node从原来的地方摘下来 addToHead(node); // 再把它放到head后面去 } ``` 通过以上方式可以有效地管理内存资源分配的同时也保证了高效的查询性能表现[^1]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值