Go實戰 | 基於本地記憶體的快取的應用及實現

yudotyang發表於2021-11-20

大家好,我是Go學堂的漁夫子。

對於快取,大家都不陌生。百度百科的定義是這樣的:

快取是指可以進行高速資料交換的儲存器,它先於記憶體與CPU交換資料,因此速率很快。

由此可知,快取是用來提高資料交換速度的。我們今天要講的快取不是CPU中的快取,而是在應用程式中對資料庫的快取。應用程式先於資料庫,從快取中讀取資料,以降低資料庫的壓力,提高應用程式的讀取效能。

在實際專案中,相信大家也都遇到過類似的情景:資料量小,但訪問又較頻繁(例如國家標準行政區域資料),想將其完全存放於本地記憶體中。這樣就可以避免直接訪問mysql或redis,減少網路傳輸,提高訪問速度。那具體應該怎麼實現呢?

本文就介紹一種Go專案中經常使用到的方法:將資料從資料庫中載入到本地檔案,然後再將檔案中的資料載入到記憶體中,記憶體中的資料直接供應用程式使用。如下圖所示:

本文會忽略資料庫到本地檔案的過程,因為這個環節就是一個檔案上傳和下載到本地的過程。所以我們會重點講解如何從本地檔案載入資料到記憶體中這個環節。

01 目標

在Go語言的專案中,將本地檔案的資料載入到應用程式的記憶體中,以供應用程式直接使用。

我們再將目標拆解成兩個目標:

1、程式啟動時,將本地檔案的資料初始化到記憶體中,即冷啟動

2、程式執行期間,本地檔案有更新時,將資料更新到記憶體中。

02 程式碼實現

本文主要是目的就是給大家講解目標的實現,所以不會帶大家一步步分析,而是透過講解已實現的程式碼來給大家提供一種參考實現。

所以,我們先給出我們設計的類圖:

從類圖中可知,有兩個主要的結構體:FileDoubleBuffer和LocalFileLoader。下面我們一一講解這兩個結構體的屬性和方法實現。

2.1 場景假設

我們以城市的天氣狀況為示例,將每個城市的實時溫度和風力以json格式儲存在檔案中,當城市的溫度或風力有變化時,再更新該檔案。如下:

{
    "beijing": {
        "temperature": 23,
        "wind": 3
    },
    "tianjin": {
        "temperature": 20,
        "wind": 2
    },
    "shanghai": {
        "temperature": 20,
        "wind": 20
    },
    "chongqing": {
        "temperature": 30,
        "wind": 10
    }
}

2.2 main的呼叫

這裡,先給出main函式的呼叫示例,根據main函式中的實現,我們一步步看圖中兩個主要結構體的實現,程式碼如下:

//第一步,定義裝載檔案中資料的結構體
type WeatherContainer struct {
    Weathers map[string]*Weather //每個城市對應的實況天氣
}
//檔案資料中每個城市的天氣狀況
type Weather struct {
    Temperature int //當前氣溫 `json:"temperature"`
    Wind        int //當前風力 `json:"wind"`
}
func main() {
    pwd, _ := os.Getwd()
    //載入的檔案路徑
    filename := pwd + "/cache/cache.json"
    //初始化本地檔案載入器
    localFileLoader := NewLocalFileLoader(filename)
    //初始化檔案緩衝例項,將localFileLoader作為底層的檔案緩衝
    fileDoubleBuffer := NewFileDoubleBuffer(localFileLoader)

    // 開始將檔案中的內容載入到緩衝變數中,本質上就是透過load和reload載入檔案資料    
    fileDoubleBuffer.StartFileBuffer()

    //獲取資料
    weathersConfig := fileDoubleBuffer.Data().(*WeatherContainer)
    fmt.Println("weathers:", weathersConfig.Weathers["beijing"])

    blockCh := make(chan int)
    //該通道用於阻塞程式不結束,這樣reload的協程就可以執行了
    <-blockCh
}

2.3 FileDoubleBuffer結構體及實現

該結構體的作用主要是面向應用程式(我們這裡是main函式),供應用程式直接從記憶體即bufferData中獲取資料的。該結構體的定義如下:

// main應用主要面向該結構體獲取資料
type FileDoubleBuffer struct {
    Loader     *LocalFileLoader
    bufferData []interface{}
    curIndex   int32
    mutex      sync.Mutex
}

首先看該結構體的屬性:

Loader:是一個LocalFileLoader型別(後面會定義該結構體),用於從具體的檔案中載入資料到bufferData中。

bufferData切片:接收檔案中資料的變數。一方面會將檔案中的資料載入到該變數中。另一方面,應用程式直接從該變數中獲取想要的資料資訊,而非檔案或資料庫。該變數的資料型別是interface{},說明可以載入任何型別的資料結構。另外,我們注意該變數是一個切片,該切片只有2個元素,兩個元素具有相同的資料結構,結合curIndex屬性使用。

curIndex:該屬性是指定當前bufferData正在使用哪個索引中的資料,該屬性的值在0和1之間迴圈,用於新老資料的切換。例如,當前對外使用的是curIndex=1這個索引元素的資料,當檔案中有新資料時,先將檔案的資料載入到索引0這個元素中,當將檔案的資料完全載入完後,再將curIndex的值指向0。這樣,當檔案中有新資料進行重新整理記憶體中的資料時,不會影響應用程式對老資料的使用。

再來看FileDoubleBuffer中的函式:

Data()函式

應用程式透過該函式來獲取FileDoubleBuffer中的dataBuffer資料。具體實現如下:

func (buffer *FileDoubleBuffer) Data() interface{} {
    // bufferData實際上儲存了兩個相同結構的元素,用於切換新老資料
    index := atomic.LoadInt32(&buffer.curIndex)
    return buffer.bufferData[index]
}

load函式

該函式是用於載入檔案中的資料到bufferData中。程式碼實現如下:

func (buffer *FileDoubleBuffer) load() {
  buffer.mutex.Lock()
  defer buffer.mutex.Unlock()
  //判斷當前使用的是bufferData陣列哪個元素
  // 因bufferData中只有兩個元素,所以要麼是0,要麼是1
  curIndex := 1 - atomic.LoadInt32(&buffer.curIndex)

  err := buffer.Loader.Load(buffer.bufferData[curIndex])
  if err == nil {
    atomic.StoreInt32(&buffer.curIndex, curIndex)
  }
}

reload函式

用於從檔案中載入新的資料到bufferData中。實際上是一個for迴圈,每隔一定的時間執行一次load函式,程式碼如下:

func (buffer *FileDoubleBuffer) reload() {
  for {
    time.Sleep(time.Duration(5) * time.Second)
    fmt.Println("開始載入...")
    buffer.load()
  }
}

StartFileBuffer函式

該函式的作用是啟動資料的載入和更新,程式碼如下:

func (buffer *FileDoubleBuffer) StartFileBuffer() {
  buffer.load()
  go buffer.reload()
}

NewFileDoubleBuffer(loader *LocalFileLoader) *FileDoubleBuffer 函式

該函式的作用是初始化FileDoubleBuffer例項,程式碼如下:

func NewFileDoubleBuffer(loader *LocalFileLoader) *FileDoubleBuffer {
  buffer := &FileDoubleBuffer{
    Loader:   loader,
    curIndex: 0,
  }

  //這裡分配記憶體空間,以便將檔案中的值載入到該變數中,供應用程式使用
  buffer.bufferData = append(buffer.bufferData, loader.Alloc(), loader.Alloc())
  return buffer
}

2.4 LocalFileLoader結構體及實現
由於我們是將資料先從資料庫載入到本地檔案上,然後再將檔案的資料載入到記憶體緩衝區中,故有了LocalFileLoader結構體。該結構體的作用是執行具體的檔案資料載入和檢測檔案更新的任務。LocalFileLoader的定義如下:

type LocalFileLoader struct {
  filename       string //需要載入的檔案,完整路徑
  lastModifyTime int64  //檔案最近一次的修改時間
}

首先來看該結構體的屬性:

filename:指定具體的檔名,說明從該檔案中載入資料

modifyTime:最後一次載入檔案的時間。如果檔案的更新時間大於該時間,則說明檔案有更新

再來看LocalFileLoader中的函式:

Load(filename string, i interface)函式

該函式用於將filename檔案中的資料載入到變數i中。該變數i實際上是從FileDoubleBuffer中傳進來的bufferData中的元素,程式碼如下:

// 這裡i變數實際上是從FileDoubleBuffer結構的load方法中傳入的dataBuffer中的一個元素
func (loader *LocalFileLoader) Load(i interface{}) error {
    // WeatherContainer結構體是依據檔案中具體儲存的資料定義的,後面會講到
    weatherContainer := i.(*WeatherContainer)
    fileHandler, _ := os.Open(loader.filename)
    defer fileHandler.Close()
    body, _ := ioutil.ReadAll(fileHandler)
    _ := json.Unmarshal(body, &weatherContainer.Weathers)
    // 這裡我們省略了那些err的判斷
    return nil
}

DetectNewFile()函式

該函式用於檢測filename檔案是否有更新,如果檔案的修改時間大於modifyTime,則FileDoubleBuffer會將新的資料載入到dataBuffer中。程式碼如下:

// 該函式檢查檔案是否有更新,如果有更新 則返回true,否則返回false
func (loader *LocalFileLoader) DetectNewFile() bool {
    fileInfo, _ := os.Stat(loader.filename)
    //檔案的修改時間比上次修改時間大,說明檔案有更新
    if fileInfo.ModTime().Unix() > loader.lastModifyTime {
        loader.lastModifyTime = fileInfo.ModTime().Unix()
        return true
    }
    return false
}

*Alloc() interface{} *

用於分配具體的變數,以供裝載檔案中的資料。這裡分配的變數最終會儲存到FileDoubleBuffer中的dataBuffer資料中。程式碼如下:

// 分配具體的變數,來承載檔案中的具體內容,變數結構體需要和檔案中的結構體保持一致
func (loader *LocalFileLoader) Alloc() interface{} {
    return &WeatherContainer{
        Weathers: make(map[string]*Weather),
    }
}

同樣需要一個初始化LocalFileLoader例項的函式:

//指定需要載入的檔案路徑path
func NewLocalFileLoader(path string) *LocalFileLoader {
    return &LocalFileLoader{
        filename: path,
    }
}

總結

這種方式一般適用於資料量較小、頻繁讀的場景。在文章開始的圖中我們可以看到,因為是伺服器往往是叢集,所以每臺機器上的檔案內容可能會有短暫的差異,所以該實現也不適用於對資料具有強一致要求的場景中。

本作品採用《CC 協議》,轉載必須註明作者和本文連結
大家好,我是Go學堂的漁夫子,歡迎大家關注Go學堂,一起系統化的分享、學習Go相關的知識。

相關文章