溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

如何提高進(jìn)程內(nèi)緩存的并發(fā)

發(fā)布時間:2021-09-14 14:32:54 來源:億速云 閱讀:142 作者:柒染 欄目:編程語言

本篇文章給大家分享的是有關(guān)如何提高進(jìn)程內(nèi)緩存的并發(fā),小編覺得挺實(shí)用的,因此分享給大家學(xué)習(xí),希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。

緩存,設(shè)計(jì)的初衷是為了減少繁重的IO操作,增加系統(tǒng)并發(fā)能力。不管是 CPU多級緩存page cache,還是我們業(yè)務(wù)中熟悉的 redis 緩存,本質(zhì)都是將有限的熱點(diǎn)數(shù)據(jù)存儲在一個存取更快的存儲介質(zhì)中。

計(jì)算機(jī)本身的緩存設(shè)計(jì)就是 CPU 采取多級緩存。那對我們服務(wù)來說,我們是不是也可以采用這種多級緩存的方式來組織我們的緩存數(shù)據(jù)。同時 redis 的存取都會經(jīng)過網(wǎng)絡(luò)IO,那我們能不能把熱點(diǎn)數(shù)據(jù)直接存在本進(jìn)程內(nèi),由進(jìn)程自己緩存一份最近最熱的這批數(shù)據(jù)呢?

這就引出了我們今天探討的:local cache,本地緩存,也叫進(jìn)程緩存。

快速入門

作為一個進(jìn)程存儲設(shè)計(jì),當(dāng)然是 crud 都有的:

  1. 我們先初始化 local cache

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

其中參數(shù)的含義:

  • expire:key統(tǒng)一的過期時間

  • CacheOption:cache設(shè)置。比如key的上限設(shè)置等

  1. 基礎(chǔ)操作緩存

// 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) 設(shè)置緩存

  • value, ok := Get(key) 讀取緩存

  • Del(key) 刪除緩存

  1. 高級操作

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 不存在時,執(zhí)行傳入的 fetch 方法,將具體讀取邏輯交給開發(fā)者實(shí)現(xiàn),并自動將結(jié)果放到緩存里。

到這里核心使用代碼基本就講完了,其實(shí)看起來還是挺簡單的。也可以到 https://github.com/tal-tech/go-zero/blob/master/core/collection/cache_test.go 去看 test 中的使用。

解決方案

如何提高進(jìn)程內(nèi)緩存的并發(fā)

首先緩存實(shí)質(zhì)是一個存儲有限熱點(diǎn)數(shù)據(jù)的介質(zhì),面臨以下的這些問題:

  1. 有限容量

  2. 熱點(diǎn)數(shù)據(jù)統(tǒng)計(jì)

  3. 多線程存取

下面來說說這3個方面我們的設(shè)計(jì)實(shí)踐。

有限容量

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

那淘汰怎么發(fā)生呢? 有幾個選擇:

  1. 開一個定時器,不斷循環(huán)所有key,等到了預(yù)設(shè)過期時間,執(zhí)行回調(diào)函數(shù)(這里是刪除map中過的key)

  2. 惰性刪除。訪問時判斷該鍵是否被刪除。缺點(diǎn)是:如果未訪問的話,會加重空間浪費(fèi)。

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

不斷循環(huán),空消耗CPU資源,即使在額外的協(xié)程中這么做,也是沒有必要的。

cache 中采取的是時間輪記錄額外過期通知,等過期 channel 中有通知時,然后觸發(fā)刪除回調(diào)。

> 有關(guān) 時間輪 更多的設(shè)計(jì)文章:https://go-zero.dev/cn/timing-wheel.html

熱點(diǎn)數(shù)據(jù)統(tǒng)計(jì)

對于緩存來說,我們需要知道這個緩存在使用額外空間和代碼的情況下是否有價值,以及我們想知道需不需要進(jìn)一步優(yōu)化過期時間或者緩存大小,所有這些我們就很依賴統(tǒng)計(jì)能力了, go-zerosqlcmongoc 也同樣提供了統(tǒng)計(jì)能力。所以我們在 cache 中也加入的緩存,為開發(fā)者提供本地緩存監(jiān)控的特性,在接入 ELK 時開發(fā)者可以更直觀的監(jiān)測到緩存的分布情況。

而設(shè)計(jì)其實(shí)也很簡單,就是:Get() 命中,就在統(tǒng)計(jì) 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
}

多線程存取

當(dāng)多個協(xié)程并發(fā)存取的時候,對于緩存來說,涉及的問題以下幾個:

  • 寫-寫沖突

  • LRU 中元素的移動過程沖突

  • 并發(fā)執(zhí)行寫入緩存時,造成流量沖擊或者無效流量

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

// 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()
  // 當(dāng)key存在時,則調(diào)整 LRU item 中的位置,這個過程也是加鎖的
  value, ok := c.data[key]
  if ok {
    c.lruCache.add(key)
  }

  return value, ok
}

而并發(fā)執(zhí)行寫入邏輯,這個邏輯主要是開發(fā)者自己傳入的。而這個過程:

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. 多協(xié)程中通過 sharedCalls 去獲取,一個協(xié)程獲取多個協(xié)程共享結(jié)果
  val, err := c.barrier.Do(key, func() (interface{}, error) {
    // double check,防止多次讀取
    if val, ok := c.doGet(key); ok {
      return val, nil
    }
    ...
    // 重點(diǎn)是執(zhí)行了傳入的緩存設(shè)置函數(shù)
    val, err := fetch()
    ...
    c.Set(key, val)
  })
  if err != nil {
    return nil, err
  }
  ...
  return val, nil
}

sharedCalls 通過共享返回結(jié)果,節(jié)省了多次執(zhí)行函數(shù),減少了協(xié)程競爭。

以上就是如何提高進(jìn)程內(nèi)緩存的并發(fā),小編相信有部分知識點(diǎn)可能是我們?nèi)粘9ぷ鲿姷交蛴玫降?。希望你能通過這篇文章學(xué)到更多知識。更多詳情敬請關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI