在 Go 項目中基于本地內(nèi)存緩存的實現(xiàn)及應(yīng)用
大家好,我是 Go 學(xué)堂的漁夫子。今天給大家介紹一下在 Go 項目中在數(shù)據(jù)量小、讀取頻繁的場景中如何實現(xiàn)基于本地內(nèi)存緩存的方法以提高系統(tǒng)性能。
對于緩存,大家都不陌生。百度百科的定義是這樣的:
緩存是指可以進行高速數(shù)據(jù)交換的存儲器,它先于內(nèi)存與 CPU 交換數(shù)據(jù),因此速率很快。
由此可知,緩存是用來提高數(shù)據(jù)交換速度的。我們今天要講的緩存不是 CPU 中的緩存,而是在應(yīng)用程序中對數(shù)據(jù)庫的緩存。應(yīng)用程序先于數(shù)據(jù)庫,從緩存中讀取數(shù)據(jù),以降低數(shù)據(jù)庫的壓力,提高應(yīng)用程序的讀取性能。
在實際項目中,相信大家也都遇到過類似的情景:數(shù)據(jù)量小,但訪問又較頻繁(例如國家標(biāo)準(zhǔn)行政區(qū)域數(shù)據(jù)),想將其完全存放于本地內(nèi)存中。這樣就可以避免直接訪問 mysql 或 redis,減少網(wǎng)絡(luò)傳輸,提高訪問速度。那具體應(yīng)該怎么實現(xiàn)呢?
本文就介紹一種 Go 項目中經(jīng)常使用到的方法:將數(shù)據(jù)從數(shù)據(jù)庫中加載到本地文件,然后再將文件中的數(shù)據(jù)加載到內(nèi)存中,內(nèi)存中的數(shù)據(jù)直接供應(yīng)用程序使用。如下圖所示:

本文會忽略數(shù)據(jù)庫到本地文件的過程,因為這個環(huán)節(jié)就是一個文件上傳和下載到本地的過程。所以我們會重點講解如何從本地文件加載數(shù)據(jù)到內(nèi)存中這個環(huán)節(jié)。
01
目標(biāo)
在 Go 語言的項目中,將本地文件的數(shù)據(jù)加載到應(yīng)用程序的內(nèi)存中,以供應(yīng)用程序直接使用。
我們再將目標(biāo)拆解成兩個目標(biāo):
1、程序啟動時,將本地文件的數(shù)據(jù)初始化到內(nèi)存中,即冷啟動
2、程序運行期間,本地文件有更新時,將數(shù)據(jù)更新到內(nèi)存中。
02
代碼實現(xiàn)
本文主要是目的就是給大家講解目標(biāo)的實現(xiàn),所以不會帶大家一步步分析,而是通過講解已實現(xiàn)的代碼來給大家提供一種參考實現(xiàn)。
所以,我們先給出我們設(shè)計的類圖:

從類圖中可知,有兩個主要的結(jié)構(gòu)體:FileDoubleBuffer 和 LocalFileLoader。下面我們一一講解這兩個結(jié)構(gòu)體的屬性和方法實現(xiàn)。
2.1 場景假設(shè)
我們以城市的天氣狀況為示例,將每個城市的實時溫度和風(fēng)力以 json 格式存儲在文件中,當(dāng)城市的溫度或風(fēng)力有變化時,再更新該文件。如下:
{
"beijing": {
"temperature": 23,
"wind": 3
},
"tianjin": {
"temperature": 20,
"wind": 2
},
"shanghai": {
"temperature": 20,
"wind": 20
},
"chongqing": {
"temperature": 30,
"wind": 10
}}2.2 main 的調(diào)用
這里,先給出 main 函數(shù)的調(diào)用示例,根據(jù) main 函數(shù)中的實現(xiàn),我們一步步看圖中兩個主要結(jié)構(gòu)體的實現(xiàn),代碼如下:
//第一步,定義裝載文件中數(shù)據(jù)的結(jié)構(gòu)體
type WeatherContainer struct {
Weathers map[string]*Weather //每個城市對應(yīng)的實況天氣
}
//文件數(shù)據(jù)中每個城市的天氣狀況
type Weather struct {
Temperature int //當(dāng)前氣溫 `json:"temperature"`
Wind int //當(dāng)前風(fēng)力 `json:"wind"`
}
func main() {
pwd, _ := os.Getwd()
//加載的文件路徑
filename := pwd + "/cache/cache.json"
//初始化本地文件加載器
localFileLoader := NewLocalFileLoader(filename)
//初始化文件緩沖實例,將localFileLoader作為底層的文件緩沖
fileDoubleBuffer := NewFileDoubleBuffer(localFileLoader)
// 開始將文件中的內(nèi)容加載到緩沖變量中,本質(zhì)上就是通過load和reload加載文件數(shù)據(jù)
fileDoubleBuffer.StartFileBuffer()
//獲取數(shù)據(jù)
weathersConfig := fileDoubleBuffer.Data().(*WeatherContainer)
fmt.Println("weathers:", weathersConfig.Weathers["beijing"])
blockCh := make(chan int)
//該通道用于阻塞進程不結(jié)束,這樣reload的協(xié)程就可以執(zhí)行了
<-blockCh
}
2.3 FileDoubleBuffer 結(jié)構(gòu)體及實現(xiàn)
該結(jié)構(gòu)體的作用主要是面向應(yīng)用程序(我們這里是 main 函數(shù)),供應(yīng)用程序直接從內(nèi)存即 bufferData 中獲取數(shù)據(jù)的。該結(jié)構(gòu)體的定義如下:
// main應(yīng)用主要面向該結(jié)構(gòu)體獲取數(shù)據(jù)
type FileDoubleBuffer struct {
Loader *LocalFileLoader
bufferData []interface{}
curIndex int32
mutex sync.Mutex
}
首先看該結(jié)構(gòu)體的屬性:
Loader:是一個 LocalFileLoader 類型(后面會定義該結(jié)構(gòu)體),用于從具體的文件中加載數(shù)據(jù)到 bufferData 中。
bufferData 切片:接收文件中數(shù)據(jù)的變量。一方面會將文件中的數(shù)據(jù)加載到該變量中。另一方面,應(yīng)用程序直接從該變量中獲取想要的數(shù)據(jù)信息,而非文件或數(shù)據(jù)庫。該變量的數(shù)據(jù)類型是 interface{},說明可以加載任何類型的數(shù)據(jù)結(jié)構(gòu)。另外,我們注意該變量是一個切片,該切片只有 2 個元素,兩個元素具有相同的數(shù)據(jù)結(jié)構(gòu),結(jié)合 curIndex 屬性使用。
curIndex:該屬性是指定當(dāng)前 bufferData 正在使用哪個索引中的數(shù)據(jù),該屬性的值在 0 和 1 之間循環(huán),用于新老數(shù)據(jù)的切換。例如,當(dāng)前對外使用的是 curIndex=1 這個索引元素的數(shù)據(jù),當(dāng)文件中有新數(shù)據(jù)時,先將文件的數(shù)據(jù)加載到索引 0 這個元素中,當(dāng)將文件的數(shù)據(jù)完全加載完后,再將 curIndex 的值指向 0。這樣,當(dāng)文件中有新數(shù)據(jù)進行刷新內(nèi)存中的數(shù)據(jù)時,不會影響應(yīng)用程序?qū)蠑?shù)據(jù)的使用。
再來看 FileDoubleBuffer 中的函數(shù):
Data() 函數(shù)
應(yīng)用程序通過該函數(shù)來獲取 FileDoubleBuffer 中的 dataBuffer 數(shù)據(jù)。具體實現(xiàn)如下:
func (buffer *FileDoubleBuffer) Data() interface{} {
// bufferData實際上存儲了兩個相同結(jié)構(gòu)的元素,用于切換新老數(shù)據(jù)
index := atomic.LoadInt32(&buffer.curIndex)
return buffer.bufferData[index]
}
load 函數(shù)
該函數(shù)是用于加載文件中的數(shù)據(jù)到 bufferData 中。代碼實現(xiàn)如下:
func (buffer *FileDoubleBuffer) load() {
buffer.mutex.Lock()
defer buffer.mutex.Unlock()
//判斷當(dāng)前使用的是bufferData數(shù)組哪個元素
// 因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 函數(shù)
用于從文件中加載新的數(shù)據(jù)到 bufferData 中。實際上是一個 for 循環(huán),每隔一定的時間執(zhí)行一次 load 函數(shù),代碼如下:
func (buffer *FileDoubleBuffer) reload() {
for {
time.Sleep(time.Duration(5) * time.Second)
fmt.Println("開始加載...")
buffer.load()
}
}
StartFileBuffer 函數(shù)
該函數(shù)的作用是啟動數(shù)據(jù)的加載和更新,代碼如下:
func (buffer *FileDoubleBuffer) StartFileBuffer() {
buffer.load()
go buffer.reload()
}
NewFileDoubleBuffer(loader *LocalFileLoader) *FileDoubleBuffer 函數(shù)
該函數(shù)的作用是初始化 FileDoubleBuffer 實例,代碼如下:
func NewFileDoubleBuffer(loader *LocalFileLoader) *FileDoubleBuffer {
buffer := &FileDoubleBuffer{
Loader: loader,
curIndex: 0,
}
//這里分配內(nèi)存空間,以便將文件中的值加載到該變量中,供應(yīng)用程序使用
buffer.bufferData = append(buffer.bufferData, loader.Alloc(), loader.Alloc())
return buffer
}
2.4 LocalFileLoader 結(jié)構(gòu)體及實現(xiàn)?由于我們是將數(shù)據(jù)先從數(shù)據(jù)庫加載到本地文件上,然后再將文件的數(shù)據(jù)加載到內(nèi)存緩沖區(qū)中,故有了 LocalFileLoader 結(jié)構(gòu)體。該結(jié)構(gòu)體的作用是執(zhí)行具體的文件數(shù)據(jù)加載和檢測文件更新的任務(wù)。LocalFileLoader 的定義如下:
type LocalFileLoader struct {
filename string //需要加載的文件,完整路徑
lastModifyTime int64 //文件最近一次的修改時間
}
首先來看該結(jié)構(gòu)體的屬性:
filename:指定具體的文件名,說明從該文件中加載數(shù)據(jù)
modifyTime:最后一次加載文件的時間。如果文件的更新時間大于該時間,則說明文件有更新
再來看 LocalFileLoader 中的函數(shù):
Load(filename string, i interface) 函數(shù)
該函數(shù)用于將 filename 文件中的數(shù)據(jù)加載到變量 i 中。該變量 i 實際上是從 FileDoubleBuffer 中傳進來的 bufferData 中的元素,代碼如下:
// 這里i變量實際上是從FileDoubleBuffer結(jié)構(gòu)的load方法中傳入的dataBuffer中的一個元素
func (loader *LocalFileLoader) Load(i interface{}) error {
// WeatherContainer結(jié)構(gòu)體是依據(jù)文件中具體存儲的數(shù)據(jù)定義的,后面會講到
weatherContainer := i.(*WeatherContainer)
fileHandler, _ := os.Open(loader.filename)
defer fileHandler.Close()
body, _ := ioutil.ReadAll(fileHandler)
_ := json.Unmarshal(body, &weatherContainer.Weathers)
// 這里我們省略了那些err的判斷
return nil
}
DetectNewFile() 函數(shù)
該函數(shù)用于檢測 filename 文件是否有更新,如果文件的修改時間大于 modifyTime,則 FileDoubleBuffer 會將新的數(shù)據(jù)加載到 dataBuffer 中。代碼如下:
// 該函數(shù)檢查文件是否有更新,如果有更新 則返回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{} *
用于分配具體的變量,以供裝載文件中的數(shù)據(jù)。這里分配的變量最終會存儲到 FileDoubleBuffer 中的 dataBuffer 數(shù)據(jù)中。代碼如下:
// 分配具體的變量,來承載文件中的具體內(nèi)容,變量結(jié)構(gòu)體需要和文件中的結(jié)構(gòu)體保持一致
func (loader *LocalFileLoader) Alloc() interface{} {
return &WeatherContainer{
Weathers: make(map[string]*Weather),
}
}
同樣需要一個初始化 LocalFileLoader 實例的函數(shù):
//指定需要加載的文件路徑path
func NewLocalFileLoader(path string) *LocalFileLoader {
return &LocalFileLoader{
filename: path,
}
}03
總結(jié)
這種方式一般適用于數(shù)據(jù)量較小、頻繁讀的場景。在文章開始的圖中我們可以看到,因為是服務(wù)器往往是集群,所以每臺機器上的文件內(nèi)容可能會有短暫的差異,所以該實現(xiàn)也不適用于對數(shù)據(jù)具有強一致要求的場景中。
想要了解關(guān)于 Go 的更多資訊,還可以通過掃描的方式,進群一起探討哦~
