大家好,我是Go学堂的渔夫子。今天给大家介绍一下在Go项目中在数据量小、读取频繁的场景中如何实现基于本地内存缓存的方法以提高系统性能。
对于缓存,大家都不陌生。百度百科的定义是这样的:
缓存是指可以进行高速数据交换的存储器,它先于内存与CPU交换数据,因此速率很快。
由此可知,缓存是用来提高数据交换速度的。我们今天要讲的缓存不是CPU中的缓存,而是在应用程序中对数据库的缓存。应用程序先于数据库,从缓存中读取数据,以降低数据库的压力,提高应用程序的读取性能。
在实际项目中,相信大家也都遇到过类似的情景:数据量小,但访问又较频繁(例如国家标准行政区域数据),想将其完全存放于本地内存中。这样就可以避免直接访问mysql或redis,减少网络传输,提高访问速度。那具体应该怎么实现呢?
本文就介绍一种Go项目中经常使用到的方法:将数据从数据库中加载到本地文件,然后再将文件中的数据加载到内存中,内存中的数据直接供应用程序使用。如下图所示:
本文会忽略数据库到本地文件的过程,因为这个环节就是一个文件上传和下载到本地的过程。所以我们会重点讲解如何从本地文件加载数据到内存中这个环节。
01 目标
在Go语言的项目中,将本地文件的数据加载到应用程序的内存中,以供应用程序直接使用。
我们再将目标拆解成两个目标:
1、程序启动时,将本地文件的数据初始化到内存中,即冷启动
2、程序运行期间,本地文件有更新时,将数据更新到内存中。
02 代码实现
本文主要是目的就是给大家讲解目标的实现,所以不会带大家一步步分析,而是通过讲解已实现的代码来给大家提供一种参考实现。
所以,我们先给出我们设计的类图:
从类图中可知,有两个主要的结构体:FileDoubleBuffer和LocalFileLoader。下面我们一一讲解这两个结构体的属性和方法实现。
2.1 场景假设
我们以城市的天气状况为示例,将每个城市的实时温度和风力以json格式存储在文件中,当城市的温度或风力有变化时,再更新该文件。如下:
1 | json复制代码{ |
2.2 main的调用
这里,先给出main函数的调用示例,根据main函数中的实现,我们一步步看图中两个主要结构体的实现,代码如下:
1 | golang复制代码//第一步,定义装载文件中数据的结构体 |
2.3 FileDoubleBuffer结构体及实现
该结构体的作用主要是面向应用程序(我们这里是main函数),供应用程序直接从内存即bufferData中获取数据的。该结构体的定义如下:
1 | golang复制代码// main应用主要面向该结构体获取数据 |
首先看该结构体的属性:
**Loader:**是一个LocalFileLoader类型(后面会定义该结构体),用于从具体的文件中加载数据到bufferData中。
**bufferData切片:**接收文件中数据的变量。一方面会将文件中的数据加载到该变量中。另一方面,应用程序直接从该变量中获取想要的数据信息,而非文件或数据库。该变量的数据类型是interface{},说明可以加载任何类型的数据结构。另外,我们注意该变量是一个切片,该切片只有2个元素,两个元素具有相同的数据结构,结合curIndex属性使用。
**curIndex:**该属性是指定当前bufferData正在使用哪个索引中的数据,该属性的值在0和1之间循环,用于新老数据的切换。例如,当前对外使用的是curIndex=1这个索引元素的数据,当文件中有新数据时,先将文件的数据加载到索引0这个元素中,当将文件的数据完全加载完后,再将curIndex的值指向0。这样,当文件中有新数据进行刷新内存中的数据时,不会影响应用程序对老数据的使用。
再来看FileDoubleBuffer中的函数:
Data()函数
应用程序通过该函数来获取FileDoubleBuffer中的dataBuffer数据。具体实现如下:
1 | golang复制代码func (buffer *FileDoubleBuffer) Data() interface{} { |
load函数
该函数是用于加载文件中的数据到bufferData中。代码实现如下:
1 | golang复制代码func (buffer *FileDoubleBuffer) load() { |
reload函数
用于从文件中加载新的数据到bufferData中。实际上是一个for循环,每隔一定的时间执行一次load函数,代码如下:
1 | golang复制代码func (buffer *FileDoubleBuffer) reload() { |
StartFileBuffer函数
该函数的作用是启动数据的加载和更新,代码如下:
1 | golang复制代码func (buffer *FileDoubleBuffer) StartFileBuffer() { |
**NewFileDoubleBuffer(loader LocalFileLoader) FileDoubleBuffer 函数
该函数的作用是初始化FileDoubleBuffer实例,代码如下:
1 | golang复制代码func NewFileDoubleBuffer(loader *LocalFileLoader) *FileDoubleBuffer { |
2.4 LocalFileLoader结构体及实现
由于我们是将数据先从数据库加载到本地文件上,然后再将文件的数据加载到内存缓冲区中,故有了LocalFileLoader结构体。该结构体的作用是执行具体的文件数据加载和检测文件更新的任务。LocalFileLoader的定义如下:
1 | golang复制代码type LocalFileLoader struct { |
首先来看该结构体的属性:
**filename:**指定具体的文件名,说明从该文件中加载数据
**modifyTime:**最后一次加载文件的时间。如果文件的更新时间大于该时间,则说明文件有更新
再来看LocalFileLoader中的函数:
Load(filename string, i interface)函数
该函数用于将filename文件中的数据加载到变量i中。该变量i实际上是从FileDoubleBuffer中传进来的bufferData中的元素,代码如下:
1 | golang复制代码// 这里i变量实际上是从FileDoubleBuffer结构的load方法中传入的dataBuffer中的一个元素 |
DetectNewFile()函数
该函数用于检测filename文件是否有更新,如果文件的修改时间大于modifyTime,则FileDoubleBuffer会将新的数据加载到dataBuffer中。代码如下:
1 | golang复制代码// 该函数检查文件是否有更新,如果有更新 则返回true,否则返回false |
**Alloc() interface{} **
用于分配具体的变量,以供装载文件中的数据。这里分配的变量最终会存储到FileDoubleBuffer中的dataBuffer数据中。代码如下:
1 | golang复制代码// 分配具体的变量,来承载文件中的具体内容,变量结构体需要和文件中的结构体保持一致 |
同样需要一个初始化LocalFileLoader实例的函数:
1 | golang复制代码//指定需要加载的文件路径path |
总结
这种方式一般适用于数据量较小、频繁读的场景。在文章开始的图中我们可以看到,因为是服务器往往是集群,所以每台机器上的文件内容可能会有短暂的差异,所以该实现也不适用于对数据具有强一致要求的场景中。
本文转载自: 掘金