大家好,我是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 協議》,轉載必須註明作者和本文連結