青年IT男

个人从事金融行业,就职过易极付、思建科技等重庆一流技术团队,目前就职于某网约车平台负责整个支付系统建设。自身对金融行业有强烈的爱好。同时也实践大数据、数据存储、自动化集成和部署、分布式微服务、人工智能等领域。

go实现缓存组件的实现

go实现缓存组件的实现

一、实验介绍

1.1 实验内容

这一节实验中,我们将设计并使用 golang 语言实现一个缓存组件。

1.2 实验知识点

缓存的意义和缓存的设计
Go 语言 map 实现哈希方法
1.3 实验环境

Go 1.2.1
Xfce终端
1.4 适合人群

本课程属于中级级别课程,适合具有Go基础的用户。

1.5 代码获取

该实验的所有实验代码可通过在终端中输入入如下代码来获取

wget http://labfile.oss-cn-hangzhou.aliyuncs.com/courses/504/cache.tgz

二、实验原理

2.1 缓存

缓存(Cache)在计算机硬件中普遍存在。比如在 CPU 中就有一级缓存,二级缓存,甚至三级缓存。缓存的工作原理一般是 CPU 需要读取数据时,会首先从缓存中查找需要的数据,如果找到了就直接进行处理,如果没有找到则从内存中读取数据。由于 CPU 中的缓存工作速度比内存还要快,所以缓存的使用能加快 CPU 处理速度。缓存不仅仅存在于硬件中,在各种软件系统中也处处可见。比如在 Web 系统中,缓存存在于服务器端,客户端或者代理服务器中。广泛使用的 CDN 网络,也可以看作一个巨大的缓存系统。缓存在 Web 系统中的使用有很多好处,不仅可以减少网络流量,降低客户访问延迟,还可以减轻服务器负载。

目前已经存在很多高性能的缓存系统,比如 Memcache,Redis 等,尤其是 Redis,现已经广泛用于各种 Web 服务中。既然有了这些功能完善的缓存系统,那为什么我们还需要自己实现一个缓存系统呢?这么做主要有两个原因,第一,通过动手实现我们可以了解缓存系统的工作原理,这也是老掉牙的理由了。第二,Redis 之类的缓存系统都是独立存在的,如果只是开发一个简单的应用还需要单独使用 Redis 服务器,难免会过于复杂。这时候如果有一个功能完善的软件包实现了这些功能,只需要引入这个软件包就能实现缓存功能,而不需要单独使用 Redis 服务器,就最好不过了。

2.2缓存系统的设计

缓存系统中,缓存的数据一般都存储在内存中,所以我们设计的缓存系统要以某一种方式管理内存中的数据。既然缓存数据是存储在内存中的,那如果系统停机,那数据岂不就丢失了吗?其实一般情况下,缓存系统还支持将内存中的数据写入到文件中,在系统再次启动时,再将文件中的数据加载到内存中,这样一来就算系统停机,缓存数据也不会丢失。

同时缓存系统还提供过期数据清理机制,也就是说缓存的数据项是有生存时间的,如果数据项过期,则数据项会从内存中被删除,这样一来热数据会一直存在,而冷数据则会被删除,也没有必要进行缓存。

缓存系统还需要对外提供操作的接口,这样系统的其他组件才能使用缓存。一般情况下,缓存系统需要支持 CRUD 操作,也就算创建(添加),读取,更新和删除操作。

通过以上分析,可以总结出缓存系统需要有以下功能:

缓存数据的存储
过期数据项管理
内存数据导出,导入
提供 CRUD 接口
三、开发准备

首先,需要创建工作目录,同时设置 GOPATH 环境变量:

 cd /home/shiyanlou/
 mkdir -p golang/src
 cd golang
 export GOPATH='/home/shiyanlou/golang'

以上步骤中,我们创建了 /home/shiyanlou/golang目录,并将它设置为 GOPATH, 也是后面实验的工作目录。

四、实验步骤

4.1 缓存系统的实现

缓存数据需要存储在内存中,这样才可以被快速访问。那使用什么数据结构来存储数据项呢?一般情况下,我们使用哈希表来存储数据项,这样访问数据项将获得更好的性能。在 golang 语言中,我们不用自己实现哈希表,因为内建类型 map 已经实现了哈希表,所以我们可以直接将缓存数据项存储在 map 中。同时由于缓存系统支持过期数据清理,所以缓存数据项应该带有生存时间,这说明需要将缓存数据进行封装后,保存到缓存系统中。这样我们就需要先实现缓存数据项,其实现的代码如下:

type Item struct {
    Object     interface{} // 真正的数据项
    Expiration int64       // 生存时间
}

// 判断数据项是否已经过期

func (item Item) Expired() bool {
    if item.Expiration == 0 {
        return false
    }
    return time.Now().UnixNano() > item.Expiration
}

以上代码中,我们定义了一个 Item 结构,该结构包含两个字段,其中 Object 用于存储任意类型的数据对象,而 Expiration 字段则存储了该数据项的过期时间,同时我们为 Item 类型,提供了 Expired() 方法,该方法返回布尔值表示该数据项是否已经过期。需要注意的是,数据项的过期时间,是 Unix 时间戳,单位是纳秒。怎么样判断数据项有没有过期呢?其实非常简单。我们在每一个数据项中,记录数据项的过期时间,然后缓存系统将定期检查每一项数据项,如果发现数据项的过期时间小于当前时间,则将数据项从缓存系统中删除。这里我们将借助 time 模块来实现周期任务。到这里,我们可以实现缓存系统的框架了,见一下代码:

const (
    // 没有过期时间标志
    NoExpiration time.Duration = -1

    // 默认的过期时间
    DefaultExpiration time.Duration = 0
)
type Cache struct {
    defaultExpiration time.Duration
    items             map[string]Item // 缓存数据项存储在 map 中
    mu                sync.RWMutex    // 读写锁
    gcInterval        time.Duration   // 过期数据项清理周期
    stopGc            chan bool
}

// 过期缓存数据项清理

func (c *Cache) gcLoop() {
    ticker := time.NewTicker(c.gcInterval)
    for {
        select {
        case <-ticker.C:
            c.DeleteExpired()
        case <-c.stopGc:
            ticker.Stop()
            return
        }
    }
}

以上代码中,实现了 Cache 结构,该结构也就是缓存系统结构,其中 items 是一个 map,用于存储缓存数据项。同时可以看到,我们实现了 gcLoop() 方法,该方通过time.Ticker 定期执行 DeleteExpired() 方法,从而清理过期的数据项。通过 time.NewTicker() 方法创建的 ticker, 会通过指定的c.Interval 间隔时间,周期性的从 ticker.C 管道中发送数据过来,我们可以根据这一特性周期性的执行DeleteExpired() 方法。同时为使 gcLoop()函数能正常结束,我们通过监听`c.

0
1028826685@qq.com