學習 go-cache

奇蹟師發表於2021-07-29

go-cache程式碼地址

Copy go-cache 學習快取的前置知識

1.什麼是讀鎖,什麼是寫鎖,sync.Mutex 和 sync.RWMutex 的區別

參考地址

2.go runtime 協程

3. Golang time 和 runtime 包

參考地址

寫 go-cache 的順序

1.確定資料需要儲存的內容和其過期時間
2.將cache封裝為一個結構體
3.New
4.編寫主方法:
新增,修改,刪除,查詢,過期快取回收
5.測試
6.驗收

開始編寫

1.確定快取結構體


type Item struct {
    // 能夠儲存的任何種類的資料
    Object interface{}
    // 原始碼中過期時間,為什麼是 int64 (因為 time.Duration 的代指就是 int64 )
    Expiration int64
}

// 順便設定常量
const (
    // 設定永久過期時間為 -1
    NoExpiration time.Duration = -1
    // 設定預設過期時間為 0
    DefaultExpiration time.Duration = 0
)

2.將 cache 封裝為一個結構體


// 該結構體方便於外部呼叫
type Cache struct {
    *cache
}

// 2.cache 中儲存的內容:
//      預設過期時間,儲存的 item, 讀寫鎖, janitor
type cache struct {
    defaultExpiration time.Duration             // 預設過期時間
    items             map[string]Item           // 內容
    mu                sync.RWMutex              // 讀寫鎖
    onEvicted         func(string, interface{}) // 對刪除資料二次操作
    janitor           *janitor                  // gc 垃圾回收
}

3.New

go-cache 中包含 1 個對外的New 和兩個內部建立結構體

// newCache 該方法真正返回 cache 結構體
// de 設定過期時間 ,m 儲存的資料
func newCache(de time.Duration, m map[string]Item) *cache {
    // 如果過期時間是0,建立永久時間
    if de == 0 {
        de = -1
    }
    return &cache{
        defaultExpiration: de,
        items:             m,
    }
}

// 該方法用來執行快取回收
func newCacheWithJanitor(de, ci time.Duration, m map[string]Item) *Cache {
    c := newCache(de, m)

    C := &Cache{c}

    if ci > 0 {
        // 執行垃圾回收方法,之後會寫
        runJanitor(c,ci)
        // 設定垃圾回收,stopJanitor 也是之後寫的停止執行快取回收
        runtime.SetFinalizer(C,stopJanitor)
    }
    return C
}

// New
// defaultExpiration 設定所有快取的預設過期時間
// cleanInterval 設定快取清理間隔時間
func New(defaultExpiration, cleanInterval time.Duration) *Cache {
    items := make(map[string]Item)
    return newCacheWithJanitor(defaultExpiration, cleanInterval, items)
}

4.編寫主方法

1.獲取


// Get 獲取 該資料為,開啟讀鎖是為了保證讀取資料的原子性
func (c *cache) Get(k string) (interface{}, bool) {
    c.mu.RLock()
    defer c.mu.RUnlock()
    item, found := c.items[k]
    if !found {
        return nil, false
    }
    // 如果設定了過期時間,判斷是否過期
    if item.Expiration > 0 {
        if time.Now().UnixNano() > item.Expiration {
            return nil, false
        }
    }
    return item.Object, true
}


// get 不需要讀寫鎖的原因是因為呼叫該方法的前置方法已經開啟了鎖
func (c *cache) get(k string) (interface{}, bool) {
    item, found := c.items[k]
    if !found {
        return nil, false
    }
    if item.Expiration > 0 { //設定了過期時間,但是過期了
        if time.Now().UnixNano() > item.Expiration {
            return nil, false
        }
    }
    return item.Object, true
}


2.新增

// Set (新增) 該方法因為在外部層面需要保證資料的一致性,防止多個操作
// 該方法偉倫是否有值,都會重新設定
// k - key , x - value , d - 過期時間
func (c cache) Set(k string, x interface{}, d time.Duration) {
    // 最後需要存入的資料內容
    var e int64
    // 如果為 0,則設定為之前設定的預設過期時間
    if d == DefaultExpiration {
        d = c.defaultExpiration
    }
    // 設定過期時間從當前時間開始
    if d > 0 {
        e = time.Now().Add(d).UnixNano()
    }
    // 新增寫鎖,保證資料的原子性,並寫入內容
    c.mu.Lock()
    defer c.mu.Unlock()
    c.items[k] = Item{
        Object:     x, //儲存內容
        Expiration: e, //過期時間
    }
}

// set 與 Set 相同,因為作用在 Add 中 已經加了鎖,所以不需要加鎖
func (c *cache) set(k string, x interface{}, d time.Duration) {
    var e int64
    if d == DefaultExpiration {
        d = c.defaultExpiration
    }
    if d > 0 {
        e = time.Now().Add(d).UnixNano()
    }
    c.items[k] = Item{
        Object:     x,
        Expiration: e,
    }
}

// Add 如果資料存在則不會進行操作
func (c cache) Add(k string, x interface{}, d time.Duration) error {
    c.mu.Lock()
    defer c.mu.Unlock()
    if _, found := c.get(k); found { // 已經儲存了資料
        return fmt.Errorf("Item %s already exists", k)
    }
    c.set(k, x, d)
    return nil
}

3.修改


// Replace 修改
func (c *cache) Replace(k string, x interface{}, d time.Duration) error {
    c.mu.Lock()
    defer c.mu.Unlock()
    if _, found := c.get(k); !found {
        return fmt.Errorf("Item %s doesn't exist", k)
    }
    // 否則進行設定
    c.set(k, x, d)
    return nil
}

4.刪除


// Delete 刪除快取, evicted 用來儲存驅逐資料
func (c *cache) Delete(k string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    v, evicted := c.delete(k)
    // 是否是需要做二次操作的資料
    if evicted {
        c.onEvicted(k, v)
    }
}

// delete 刪除快取
func (c *cache) delete(k string) (interface{}, bool) {
    if c.onEvicted != nil {
        if v, found := c.items[k]; found {
            delete(c.items, k)
            return v.Object, true
        }
    }
    delete(c.items, k)
    return nil, false
}

5.過期快取回收


type janitor struct {
    Interval time.Duration
    stop     chan bool
}

// Run
// select 作為阻塞程式
func (j *janitor) Run(c *cache) {
    // 設定的每過一段時間返回一個 channel 通道欄位
    ticker := time.NewTicker(j.Interval)
    for {
        select {
        // 如果返回的是時間,則執行刪除過期快取
        case <-ticker.C:
            c.DeleteExpired()
        // 如果j中確定停止回收
        case <-j.stop:
            ticker.Stop()
            return
    go j.Run(c)
}

// 對資料進行二次操作
type keyAndValue struct {
    key   string
    value interface{}
}

// DeleteExpired() 刪除過期快取
func (c *cache) DeleteExpired() {
    var evictedItems []keyAndValue
    now := time.Now().UnixNano()
    c.mu.Lock()
    defer c.mu.Unlock()
    for k, v := range c.items {
        if v.Expiration > 0 && now > v.Expiration {
            ov, eveicted := c.delete(k)
            // 將需要進行二次操作的資料存入evictedItems 中
            if eveicted {
                evictedItems = append(evictedItems, keyAndValue{k, ov})
            }
        }
    }
    // 將需要二次操作的資料儲存
    for _, v := range evictedItems {
        c.onEvicted(v.key, v.value)
    }
}


// 儲存二次操作的資料
func (c *cache) OnEvicted(f func(string, interface{})) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.onEvicted = f
}

6.幾個常用方法


// Items 獲取所有資料
func (c *cache) Items() map[string]Item {
    c.mu.RLock()
    defer c.mu.RUnlock()
    m := make(map[string]Item, len(c.items))
    now := time.Now().UnixNano()
    for k, v := range c.items {
        if v.Expiration > 0 && now > v.Expiration {
            continue
        }
        m[k] =v
    }
    return m
}

// 獲取資料總數
func (c cache) ItemCount() int {
    c.mu.RLock()
    defer c.mu.RUnlock()
    return len(c.items)
}

// Flush 刪除所有
func (c cache) Flush() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.items = map[string]Item{}
}

5.單元測試

結語

  • go-cache 其實常用在二次快取,同時go-cahce 不需要新增分散式快取,如果新增了為什麼不直接用redis的分散式快取

  • 我們其實並不需要經常重複造輪子,重複造輪子的目的是為了瞭解成熟框架的執行原理,如果有能力,可以同時進行優化

  • 同時也謝謝能看到最後

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章