再谈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
对象池的话也可以在这个的基础上封装,代码就懒得粘了。
除了双链外我以前还搞过一种时间块三链的存储结构,性能效率也不错,不过算法有些复杂,也不知道是不是我独创,总之网是搜不到。思路是把缓存分时间块存取,例如十分钟内的缓存在第一块,十到二十分钟的缓存在第二块,类堆。每次访问缓存就把缓存对象放到最新的时间块,过期处理是把过期时间块里所有缓存对象清了,例如五十到六十分钟时间块过期了,就把时间块置空即可,时间块LRU的好处是十分钟内的缓存被访问是不需要进行上下节点处理的,而且清内存的时候不需要对多个对象进行置空清除,只需要对时间块清除即可。
具体做法是取当前时间戳除以一个时间段数值(例如十分钟是60*10),取整数部份做为时间块的id,用这个id做为这个时间段的内存块加入链表头。每调用对象就把对象放到放到最新的时间块去。这个方法不是判断对象过期,而是判断时间块过期。时间块过期就把块id对应的对象置空。懒筋抽搐,改天有空再弄上来。