Golang實現(xiàn)EasyCache緩存庫實例探究
引言
學了不吃虧,學了不上當,進廠打釘必備基本功,看完絕對有很爽的感覺。核心代碼也就300多行,代碼雖少但是功能一點不打折
通過本項目學到什么?
Golang基礎語法
緩存數(shù)據(jù)結構
鎖的使用(并發(fā)安全 & 分片減小鎖粒度)
LRU(緩存淘汰算法)
key過期刪除策略(定時刪除)
測試用例的編寫
代碼原理
New函數(shù)
負責創(chuàng)建 *EasyCache
對象,對象的底層包含 conf.Shards
個分片,目的在于減少鎖沖突
func New(conf Config) (*EasyCache, error) { if !utils.IsPowerOfTwo(conf.Shards) { returnnil, errors.New("shards number must be power of two") } if conf.Cap <= 0 { conf.Cap = defaultCap } // init cache object cache := &EasyCache{ shards: make([]*cacheShard, conf.Shards), conf: conf, hash: conf.Hasher, shardMask: uint64(conf.Shards - 1), // mask close: make(chanstruct{}), } var onRemove OnRemoveCallback if conf.OnRemoveWithReason != nil { onRemove = conf.OnRemoveWithReason } else { onRemove = cache.notProvidedOnRemove } // init shard for i := 0; i < conf.Shards; i++ { cache.shards[i] = newCacheShard(conf, i, onRemove, cache.close) } return cache, nil }
newCacheShard函數(shù)
用來初始化實際存放 k/v
的數(shù)據(jù)結構*cacheShard
(也就是單個分片)。分片底層的存儲采用兩個map和一個list:
items
負責保存所有的k/v
(過期or不過期都有存)expireItems
負責保存有過期時間的k/v
,目的在于減少掃描key`的數(shù)據(jù)量list
用作LRU
記錄最近最少使用key
的順序。LRU代碼實現(xiàn)看這篇文章 Leetcode LRU題解,有助于理解本項目中的LRU的細節(jié)。
func newCacheShard(conf Config, id int, onRemove OnRemoveCallback, close chan struct{}) *cacheShard { shard := &cacheShard{ items: make(map[string]*list.Element), expireItems: make(map[string]*list.Element), cap: conf.Cap, list: list.New(), logger: newLogger(conf.Logger), cleanupInterval: defaultInternal, cleanupTicker: time.NewTicker(defaultInternal), addChan: make(chanstring), isVerbose: conf.Verbose, id: id, onRemove: onRemove, close: close, } // goroutine clean expired key go shard.expireCleanup() return shard }
expireCleanup
負責對本分片中過期的key進行定期刪除:代碼理解的關鍵在于不同的key
會有不同的過期時間,例如key=a 過期時間3s,key=b 過期時間5s。
定時器定時執(zhí)行間隔不能太長,例如10s,a/b都已經(jīng)過期了還不清理,太不及時
定時器定時執(zhí)行間隔不能太短,例如1s,執(zhí)行頻率又太高了,a/b都未過期,空轉
過期間隔肯定是動態(tài)變化的,一開始為3s間隔,執(zhí)行后清理掉a,此時b還剩(5-3)=2s的存活時間,所以間隔再設定為2s。再執(zhí)行完以后,沒有數(shù)據(jù)了,那間隔就在設定一個大值
smallestInternal = defaultInternal
處于休眠狀態(tài)
這里再思考一種情況,按照上述解釋一開始間隔設定3s,等到過期了就可以將a清理掉。那如果用戶這時又設定了key=c 過期時間1s,那如果定時器按照3s執(zhí)行又變成了間隔太長了。所以我們需要發(fā)送信號cs.addChan:
,重新設定過期間隔
/* 1.當定時器到期,執(zhí)行過期清理 2.當新增的key有過期時間,通過addChan觸發(fā)執(zhí)行 */ func (cs *cacheShard) expireCleanup() { for { select { case <-cs.cleanupTicker.C: case <-cs.addChan: // 立即觸發(fā) case <-cs.close: // stop goroutine if cs.isVerbose { cs.logger.Printf("[shard %d] flush..", cs.id) } cs.flush() // free return } cs.cleanupTicker.Stop() // 記錄下一次定時器的最小間隔(目的:key過期了,盡快刪除) smallestInternal := 0 * time.Second now := time.Now() cs.lock.Lock() for key, ele := range cs.expireItems { // 遍歷過期key item := ele.Value.(*cacheItem) if item.LifeSpan() == 0 { // 沒有過期時間 cs.logger.Printf("warning wrong data\n") continue } if now.Sub(item.CreatedOn()) >= item.LifeSpan() { // 過期 // del delete(cs.items, key) delete(cs.expireItems, key) cs.list.Remove(ele) cs.onRemove(key, item.Value(), Expired) if cs.isVerbose { cs.logger.Printf("[shard %d]: expire del key <%s> createdOn:%v, lifeSpan:%d ms \n", cs.id, key, item.CreatedOn(), item.LifeSpan().Milliseconds()) } } else { d := item.LifeSpan() - now.Sub(item.CreatedOn()) if smallestInternal == 0 || d < smallestInternal { smallestInternal = d } } } if smallestInternal == 0 { smallestInternal = defaultInternal } cs.cleanupInterval = smallestInternal cs.cleanupTicker.Reset(cs.cleanupInterval) cs.lock.Unlock() } }
set 函數(shù)理解
關鍵在于,用戶可以對同一個key重復設定:
cache.Set(key, 0, 5*time.Second) // expire 5s cache.Set(key, 0, 0*time.Second) // expire 0s
第一次設定為5s過期,立刻又修改為0s不過期,所以在代碼中需要判斷key是否之前已經(jīng)存在,
如果存在重復&有過期時間,需要從過期
expireItems
中剔除如果不存在直接新增即可(前提:容量還有剩余)
LRU的基本規(guī)則
最新數(shù)據(jù)放到list的Front
如果超過最大容量,從list的Back刪除元素
func (cs *cacheShard) set(key string, value interface{}, lifeSpan time.Duration) error { cs.lock.Lock() defer cs.lock.Unlock() oldEle, ok := cs.items[key] if ok { // old item oldItem := oldEle.Value.(*cacheItem) oldLifeSpan := oldItem.LifeSpan() // modify oldEle.Value = newCacheItem(key, value, lifeSpan) cs.list.MoveToFront(oldEle) if oldLifeSpan > 0 && lifeSpan == 0 { // 原來的有過期時間,新的沒有過期時間 delete(cs.expireItems, key) } if oldLifeSpan == 0 && lifeSpan > 0 { // 原有的無過期時間,當前有過期時間 cs.expireItems[key] = oldEle if lifeSpan < cs.cleanupInterval { gofunc() { cs.addChan <- key }() } } } else { // new item iflen(cs.items) >= int(cs.cap) { // lru: No space delVal := cs.list.Remove(cs.list.Back()) item := delVal.(*cacheItem) delete(cs.items, item.Key()) if item.LifeSpan() > 0 { delete(cs.expireItems, item.Key()) } cs.onRemove(key, item.Value(), NoSpace) if cs.isVerbose { cs.logger.Printf("[shard %d] no space del key <%s>\n", cs.id, item.Key()) } } // add ele := cs.list.PushFront(newCacheItem(key, value, lifeSpan)) cs.items[key] = ele if lifeSpan > 0 { cs.expireItems[key] = ele if lifeSpan < cs.cleanupInterval { gofunc() { cs.addChan <- key }() } } } if cs.isVerbose { if lifeSpan == 0 { cs.logger.Printf("[shard %d]: set persist key <%s>\n", cs.id, key) } else { cs.logger.Printf("[shard %d]: set expired key <%s>", cs.id, key) } } returnnil }
以上就是Golang實現(xiàn)EasyCache緩存庫實例探究的詳細內(nèi)容,更多關于Golang EasyCache緩存庫的資料請關注腳本之家其它相關文章!
相關文章
Golang報“import cycle not allowed”錯誤的2種解決方法
這篇文章主要給大家介紹了關于Golang報"import cycle not allowed"錯誤的2種解決方法,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友可以們下面隨著小編來一起看看吧2018-08-08Golang中實現(xiàn)數(shù)據(jù)脫敏處理的go-mask包分享
這篇文章主要是來和大家分享一個在輸出中對敏感數(shù)據(jù)進行脫敏的工作包:go-mask,可以將敏感信息輸出的時候替換成星號或其他字符,感興趣的小編可以跟隨小編一起了解下2023-05-05golang調(diào)用shell命令(實時輸出,終止)
本文主要介紹了golang調(diào)用shell命令(實時輸出,終止),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2023-02-02