造輪子 | golang | 支援過期時間的LRU快取
本文程式碼已上傳 github ,歡迎交流。
最近在學習go語言,正好有遇到需要使用快取的地方,於是決定自己造個輪子。主要特性如下:
- 執行緒安全;
- 支援 被動觸發 的過期時間;
- 支援key和value任意型別;
- 基於 雙向連結串列和hash表 實現;
雙向連結串列的插入、刪除和元素移動效率非常高,LRU快取通常都有大量的以上操作。使用hash表來儲存每個key對應的元素的指標,避免每次查詢快取都需要遍歷整個連結串列,提高效率。
被動的過期的時間表示並不會主動的刪除快取中已經過期的元素,而是在需要使用的時候才去檢查是否過期,如果過期的話再去刪除。
資料結構
每個快取的元素至少包含兩個:快取的關鍵字 key 、快取的資料 data ;為了支援過期時間,每個元素還要有一個值來表示其 過期時間 ;另外基於雙向連結串列實現,還需要指向前一個元素和後一個元素的指標;於是,每個快取元素的結構定義:
type elem struct { keyinterface{} datainterface{} expireTime int64 next*elem pre*elem }
那麼對於整個快取來說,事實上就是一個個元素組成的列表,但是為了更高效的查詢,使用一個hash表來存放key對應的元素的指標,提升查詢效率,於是cache的結構定義:
type lrucache struct { maxSizeint elemCount int elemListmap[interface{}]*elem first*elem last*elem musync.Mutex }
儲存連結串列首尾元素的指標是為了在淘汰元素和插入元素的時候更高效。
基本方法
一個快取基本的方法應該包括新建快取、新增元素、刪除元素、查詢元素。
新建快取
新建一個快取實際上就是新建一個lrucache結構體,並對裡面的元素進行初始化:
// New create a new lrucache // size: max number of element func New(size int) (*lrucache, error) { newCache := new(lrucache) newCache.maxSize = size newCache.elemCount = 0 newCache.elemList = make(map[interface{}]*elem) return newCache, nil }
入參表示這個快取最多能存放的元素的個數,當到達最大個數的時候就開始淘汰最久沒使用的元素。
新增元素
新增元素使用 Set
方法來實現,如果快取中已經存在該key,就更新值;否則新建一個快取元素並儲存。過期時間是可選的,如果沒傳入過期時間,這個元素就會一直存在知道被淘汰。
// Set create or update an element using key //key:The identity of an element //value:new value of the element //ttl:expire time, unit: second func (c *lrucache) Set(key interface{}, value interface{}, ttl ...int) error { // Ensure ttl are correct if len(ttl) > 1 { return errors.New("wrong para number, 2 or 3 expected but more than 3 received") } var elemTTL int64 if len(ttl) == 1 { elemTTL = int64(ttl[0]) } else { elemTTL = -1 } c.mu.Lock() defer c.mu.Unlock() if e, ok := c.elemList[key]; ok { e.data = value if elemTTL == -1 { e.expireTime = elemTTL } else { e.expireTime = time.Now().Unix() + elemTTL } c.mvKeyToFirst(key) } else { if c.elemCount+1 > c.maxSize { if c.checkExpired() <= 0 { c.eliminationOldest() } } newElem := &elem{ key:key, data:value, expireTime: -1, pre:nil, next:c.first, } if elemTTL != -1 { newElem.expireTime = time.Now().Unix() + elemTTL } if c.first != nil { c.first.pre = newElem } c.first = newElem c.elemList[key] = newElem c.elemCount++ } return nil }
如果一個key已經存在就更新它所對應的值,並將這個key對應的元素移動到連結串列的最前面;如果key不存在就需要新建一個連結串列元素,流程如下:

新增key流程圖
由於採用的是過期時間是被動觸發的方式,因此在元素滿的時候並不能確定是否存在過期的元素,因此目前採用的方式是,當滿了之後每次新增元素就去遍歷的檢查一次過期的元素,時間複雜度為O(n),感覺這種實現方式不太好,但是目前沒想到更好的實現方式。
上面使用到的內部方法實現如下:
// updateKeyPtr 更新對應key的指標,放到連結串列的第一個 func (c *lrucache) mvKeyToFirst(key interface{}) { elem := c.elemList[key] if elem.pre == nil { // 當key是第一個元素時,不做動作 return } else if elem.next == nil { // 當key不是第一個元素,但是是最後一個元素時,提到第一個元素去 elem.pre.next = nil c.last = elem.pre elem.pre = nil elem.next = c.first c.first = elem } else { elem.pre.next = elem.next elem.next.pre = elem.pre elem.next = c.first elem.pre = nil c.first = elem } } func (c *lrucache) eliminationOldest() { if c.last == nil { return } if c.last.pre != nil { c.last.pre.next = nil } key := c.last.key c.last = c.last.pre delete(c.elemList, key) } func (c *lrucache) deleteByKey(key interface{}) { if v, ok := c.elemList[key]; ok { if v.pre == nil && v.next == nil { // 當key是第一個元素時,清空元素列表,充值指標和元素計數 c.elemList = make(map[interface{}]*elem) c.elemCount = 0 c.last = nil c.first = nil return } else if v.next == nil { // 當key不是第一個元素,但是是最後一個元素時,修改前一個元素的next指標並修改c.last指標 v.pre.next = v.next c.last = v.pre } else if v.pre == nil { c.first = v.next c.first.pre = nil } else { // 中間元素,修改前後指標 v.pre.next = v.next v.next.pre = v.pre } delete(c.elemList, key) c.elemCount-- } } // 遍歷連結串列,檢查並刪除已經過期的元素 func (c *lrucache) checkExpired() int { now := time.Now().Unix() tmp := c.first count := 0 for tmp != nil { if tmp.expireTime != -1 && now > tmp.expireTime { c.deleteByKey(tmp.key) count++ } tmp = tmp.next } return count }
獲取元素
使用 Get
方法來獲取嘗試獲取一個快取的元素,在獲取的時候同時會檢查是否過期,如果過期的話會返回響應的錯誤並刪掉該元素:
// Get Get the value of a cached element by key. If key do not exist, this function will return nil and a error msg //key:The identity of an element //return: //value:the cached value, nil if key do not exist //err:error info, nil if value is not nil func (c *lrucache) Get(key interface{}) (value interface{}, err error) { if v, ok := c.elemList[key]; ok { if v.expireTime != -1 && time.Now().Unix() > v.expireTime { // 如果過期了 c.deleteByKey(key) return nil, errors.New("the key was expired") } c.mvKeyToFirst(key) return v.data, nil } return nil, errors.New("no value found") }
刪除元素
刪除元素通過 Delete
來實現,實際上在之前的內部方法中已經實現了刪除一個元素的功能,只需要封裝給外部呼叫即可:
// Delete delete an element func (c *lrucache) Delete(key interface{}) error { c.mu.Lock() defer c.mu.Unlock() if _, ok := c.elemList[key]; !ok { return errors.New(fmt.Sprintf("key %T do not exist", key)) } c.deleteByKey(key) return nil }
算是熟悉了go語言的基本使用,但是還有很多需要優化的地方,比如優化 Set
方法的效率,使用 讀寫鎖 替換互斥鎖。。。。
歡迎討論。