相親交友原始碼實現程式內快取,提升高併發能力!

雲豹科技程式設計師發表於2021-11-03

前言

快取,設計的初衷是為了減少繁重的IO操作,增加相親交友原始碼系統併發能力。不管是 CPU多級快取,page cache,還是我們業務中熟悉的 redis 快取,本質都是將有限的熱點資料儲存在一個存取更快的儲存介質中。

計算機本身的快取設計就是 CPU 採取多級快取。那對相親交友原始碼的服務來說,我們是不是也可以採用這種多級快取的方式來組織我們的快取資料。同時 redis 的存取都會經過網路IO,那我們能不能把熱點資料直接存在本程式內,由程式自己快取一份最近最熱的這批資料呢?

這就引出了我們今天探討的:local cache,本地快取,也叫程式快取。

本文帶你一起探討下 go-zero 中程式快取的設計。Let’s go!

快速入門

作為一個相親交友原始碼中的程式儲存設計,當然是 crud 都有的:

1、我們先初始化 local cache

// 先初始化 local cache
cache, err = collection.NewCache(time.Minute, collection.WithLimit(10))
if err != nil {
  log.Fatal(err)
}

其中引數的含義:

  • expire:key統一的過期時間
  • CacheOption:cache設定。比如key的上限設定等

** 2、基礎操作快取**

// 1. add/update 增加/修改都是該API
cache.Set("first", "first element")
// 2. get 獲取key下的value
value, ok := cache.Get("first")
// 3. del 刪除一個key
cache.Del("first")
  • Set(key, value) 設定快取
  • value, ok := Get(key) 讀取快取
  • Del(key) 刪除快取

** 3、高階操作**

cache.Take("first", func() (interface{}, error) {
  // 模擬邏輯寫入local cache
  time.Sleep(time.Millisecond * 100)
  return "first element", nil
})

前面的 Set(key, value) 是單純將 <key, value> 加入快取;Take(key, setFunc) 則是在 key 對於的 value 不存在時,執行傳入的 fetch 方法,將相親交友原始碼具體讀取邏輯交給開發者實現,並自動將結果放到快取裡。

到這裡相親交友原始碼核心使用程式碼基本就講完了,其實看起來還是挺簡單的。

解決方案

在這裡插入圖片描述

首先快取實質是一個儲存相親交友原始碼有限熱點資料的介質,面臨以下的這些問題:

1、有限容量
2、熱點資料統計
3、多執行緒存取

下面來說說這3個方面我們的設計實踐。

有限容量

有限就意味著滿了要淘汰,這個就涉及到淘汰策略。cache 中使用的是:LRU(最近最少使用)。

那淘汰怎麼發生呢? 有幾個選擇:

1、開一個定時器,不斷迴圈所有key,等到了預設過期時間,執行回撥函式(這裡是刪除map中過的key)
2、惰性刪除。訪問時判斷該鍵是否被刪除。缺點是:如果未訪問的話,會加重空間浪費。

而 cache 中採取的是第一種 主動刪除。但是,主動刪除中遇到最大的問題是:

不斷迴圈,空消耗相親交友原始碼CPU資源,即使在額外的協程中這麼做,也是沒有必要的。

cache 中採取的是時間輪記錄額外過期通知,等過期 channel 中有通知時,然後觸發刪除回撥。

熱點資料統計

對於快取來說,我們需要知道這個快取在使用相親交友原始碼額外空間和程式碼的情況下是否有價值,以及我們想知道需不需要進一步優化過期時間或者快取大小,所有這些我們就很依賴統計能力了, go-zero 中 sqlc 和 mongoc 也同樣提供了統計能力。所以我們在 cache 中也加入的快取,為開發者提供本地快取監控的特性,在接入 ELK 時開發者可以更直觀的監測到快取的分佈情況。

而設計其實也很簡單,就是:Get() 命中,就在統計 count 上加1即可。

func (c *Cache) Get(key string) (interface{}, bool) {
  value, ok := c.doGet(key)
  if ok {
    // 命中hit+1
    c.stats.IncrementHit()
  } else {
    // 未命中miss+1
    c.stats.IncrementMiss()
  }
  return value, ok
}

多執行緒存取

當多個協程併發存取的時候,對於快取來說,涉及的問題以下幾個:

  • 寫-寫衝突
  • LRU 中元素的移動過程衝突
  • 併發執行寫入快取時,造成流量衝擊或者無效流量

這種情況下,寫衝突好解決,最簡單的方法就是 加鎖 :

// Set(key, value)
func (c *Cache) Set(key string, value interface{}) {
  // 加鎖,然後將 <key, value> 作為鍵值對寫入 cache 中的 map
  c.lock.Lock()
  _, ok := c.data[key]
  c.data[key] = value
  // lru add key
  c.lruCache.add(key)
  c.lock.Unlock()
  ...
}
// 還有一個在操作 LRU 的地方時:Get()
func (c *Cache) doGet(key string) (interface{}, bool) {
  c.lock.Lock()
  defer c.lock.Unlock()
  // 當key存在時,則調整 LRU item 中的位置,這個過程也是加鎖的
  value, ok := c.data[key]
  if ok {
    c.lruCache.add(key)
  }
  return value, ok
}

而併發執行寫入相親交友原始碼邏輯,這個邏輯主要是開發者自己傳入的。而這個過程:

func (c *Cache) Take(key string, fetch func() (interface{}, error)) (interface{}, error) {
  // 1. 先獲取 doGet() 中的值
  if val, ok := c.doGet(key); ok {
    c.stats.IncrementHit()
    return val, nil
  }
  var fresh bool
  // 2. 多協程中通過 sharedCalls 去獲取,一個協程獲取多個協程共享結果
  val, err := c.barrier.Do(key, func() (interface{}, error) {
    // double check,防止多次讀取
    if val, ok := c.doGet(key); ok {
      return val, nil
    }
    ...
    // 重點是執行了傳入的快取設定函式
    val, err := fetch()
    ...
    c.Set(key, val)
  })
  if err != nil {
    return nil, err
  }
  ...
  return val, nil
}

而 sharedCalls 通過共享返回結果,節省了多次執行函式,減少了相親交友原始碼協程競爭。

總結

本篇文章講解了相親交友原始碼本地快取設計實踐。從使用到設計思路,你也可以根據你的業務動態修改快取的過期策略,加入你想要的統計指標,實現自己的本地快取。

本文轉載自網路,轉載僅為分享乾貨知識,如有侵權歡迎聯絡雲豹科技進行刪除處理
原文連結:


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69996194/viewspace-2840438/,如需轉載,請註明出處,否則將追究法律責任。

相關文章