Golang實(shí)現(xiàn)EasyCache緩存庫(kù)實(shí)例探究
引言
學(xué)了不吃虧,學(xué)了不上當(dāng),進(jìn)廠打釘必備基本功,看完絕對(duì)有很爽的感覺。核心代碼也就300多行,代碼雖少但是功能一點(diǎn)不打折
通過本項(xiàng)目學(xué)到什么?
Golang基礎(chǔ)語法
緩存數(shù)據(jù)結(jié)構(gòu)
鎖的使用(并發(fā)安全 & 分片減小鎖粒度)
LRU(緩存淘汰算法)
key過期刪除策略(定時(shí)刪除)
測(cè)試用例的編寫
代碼原理
New函數(shù)
負(fù)責(zé)創(chuàng)建 *EasyCache對(duì)象,對(duì)象的底層包含 conf.Shards個(gè)分片,目的在于減少鎖沖突
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ù)
用來初始化實(shí)際存放 k/v的數(shù)據(jù)結(jié)構(gòu)*cacheShard(也就是單個(gè)分片)。分片底層的存儲(chǔ)采用兩個(gè)map和一個(gè)list:
items負(fù)責(zé)保存所有的k/v(過期or不過期都有存)expireItems負(fù)責(zé)保存有過期時(shí)間的k/v,目的在于減少掃描key`的數(shù)據(jù)量list用作LRU記錄最近最少使用key的順序。LRU代碼實(shí)現(xiàn)看這篇文章 Leetcode LRU題解,有助于理解本項(xiàng)目中的LRU的細(xì)節(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
負(fù)責(zé)對(duì)本分片中過期的key進(jìn)行定期刪除:代碼理解的關(guān)鍵在于不同的key會(huì)有不同的過期時(shí)間,例如key=a 過期時(shí)間3s,key=b 過期時(shí)間5s。
定時(shí)器定時(shí)執(zhí)行間隔不能太長(zhǎng),例如10s,a/b都已經(jīng)過期了還不清理,太不及時(shí)
定時(shí)器定時(shí)執(zhí)行間隔不能太短,例如1s,執(zhí)行頻率又太高了,a/b都未過期,空轉(zhuǎn)
過期間隔肯定是動(dòng)態(tài)變化的,一開始為3s間隔,執(zhí)行后清理掉a,此時(shí)b還剩(5-3)=2s的存活時(shí)間,所以間隔再設(shè)定為2s。再執(zhí)行完以后,沒有數(shù)據(jù)了,那間隔就在設(shè)定一個(gè)大值
smallestInternal = defaultInternal處于休眠狀態(tài)
這里再思考一種情況,按照上述解釋一開始間隔設(shè)定3s,等到過期了就可以將a清理掉。那如果用戶這時(shí)又設(shè)定了key=c 過期時(shí)間1s,那如果定時(shí)器按照3s執(zhí)行又變成了間隔太長(zhǎng)了。所以我們需要發(fā)送信號(hào)cs.addChan:,重新設(shè)定過期間隔
/*
1.當(dāng)定時(shí)器到期,執(zhí)行過期清理
2.當(dāng)新增的key有過期時(shí)間,通過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()
// 記錄下一次定時(shí)器的最小間隔(目的: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 { // 沒有過期時(shí)間
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ù)理解
關(guān)鍵在于,用戶可以對(duì)同一個(gè)key重復(fù)設(shè)定:
cache.Set(key, 0, 5*time.Second) // expire 5s cache.Set(key, 0, 0*time.Second) // expire 0s
第一次設(shè)定為5s過期,立刻又修改為0s不過期,所以在代碼中需要判斷key是否之前已經(jīng)存在,
如果存在重復(fù)&有過期時(shí)間,需要從過期
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 { // 原來的有過期時(shí)間,新的沒有過期時(shí)間
delete(cs.expireItems, key)
}
if oldLifeSpan == 0 && lifeSpan > 0 { // 原有的無過期時(shí)間,當(dāng)前有過期時(shí)間
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實(shí)現(xiàn)EasyCache緩存庫(kù)實(shí)例探究的詳細(xì)內(nèi)容,更多關(guān)于Golang EasyCache緩存庫(kù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Golang報(bào)“import cycle not allowed”錯(cuò)誤的2種解決方法
這篇文章主要給大家介紹了關(guān)于Golang報(bào)"import cycle not allowed"錯(cuò)誤的2種解決方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以們下面隨著小編來一起看看吧2018-08-08
GoFrame基于性能測(cè)試得知grpool使用場(chǎng)景
這篇文章主要為大家介紹了GoFrame基于性能測(cè)試得知grpool使用場(chǎng)景示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06
go-zero源碼閱讀之布隆過濾器實(shí)現(xiàn)代碼
布隆過濾器可以用于檢索一個(gè)元素是否在一個(gè)集合中。它的優(yōu)點(diǎn)是空間效率和查詢時(shí)間都比一般的算法要好的多,缺點(diǎn)是有一定的誤識(shí)別率和刪除困難,這篇文章主要介紹了go-zero源碼閱讀-布隆過濾器,需要的朋友可以參考下2023-02-02
Golang中實(shí)現(xiàn)數(shù)據(jù)脫敏處理的go-mask包分享
這篇文章主要是來和大家分享一個(gè)在輸出中對(duì)敏感數(shù)據(jù)進(jìn)行脫敏的工作包:go-mask,可以將敏感信息輸出的時(shí)候替換成星號(hào)或其他字符,感興趣的小編可以跟隨小編一起了解下2023-05-05
golang調(diào)用shell命令(實(shí)時(shí)輸出,終止)
本文主要介紹了golang調(diào)用shell命令(實(shí)時(shí)輸出,終止),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02

