说明
本文用于记录学习 go
语言过程中的笔记, 文中的代码都是在文本中敲出来的伪代码, 并不能直接运行, 如有需要可以参考原文链接.
本文的整体思路是对原系列教程阅读后的复盘.
关于本文参考的 学习教程 可以访问原教程链接:
7天用Go从零实现分布式缓存GeeCache
本文如有侵占原文博主的权益, 还请指出, 本人尽可能及时做出调整.
笔记大纲
-
第一天,为了解决资源限制的问题,实现了 LRU 缓存淘汰算法;
-
第二天实现了单机并发,并给用户提供了自定义数据源的回调函数;
-
第三天实现了 HTTP 服务端;
-
第四天实现了一致性哈希算法,解决远程节点的挑选问题;
-
第五天创建 HTTP 客户端,实现了多节点间的通信;
-
第六天实现了 singleflight 解决缓存击穿的问题;
-
第七天,使用 protobuf 库,优化了节点间通信的性能。
第一天 LRU 缓存淘汰
一、 定义缓存的存储容量以及缓存容量达到上限时的解决方案
当我们设计缓存时, 我们需要考虑到一个问题, 我们缓存容量一定要是可控的, 不能无限量存储, 因此需要定义一个最大存储容量, 这个我们定义为 maxBytes
,为了和最大存储容量进行比较, 我们还需要计算已占用的字节大小, 我们定义为 nbytes
.
当我们定义完最大容量后, 我们要考虑到, 如果存储容量达到设定的最大值时, 想要继续存储数据时, 改怎么办?
有如下两种办法:
-
抛异常提示, 容量达到上限, 让管理员自己去清理.
这个我们通过对外暴露接口, 让客户端调用对应增删改查的 API 就能实现.
-
使用缓存淘汰策略, 淘汰掉 “不需要的已缓存数据”.
这时我们就需要考虑具体要提供哪几种缓存淘汰策略. 这里就先不多说, 我们直接采用 LRU(最近最少使用). 详情参考原文的博客.
二、定义缓存对象
定义缓存数据的数据结构, 以及 LRU 算法(这里省略思考过程, 直接说答案, 具体可参考原文连接)
巧妙结合 list
和 map
实现缓存数据的存储问题,以及 LRU
算法
定义数据结构
var ll = list.New()
var m = make(map[string]*list.Element)
因为我们是 k-v 存储, key 的类型已经确定了, 就是 string, 然后结合上一章节的问题, 因为我们要实时统计存储大小, 所以存储的 元素 都要能够计算大小.所以我们抽象出如下接口, 定义要存储的 value:
type Value interface {
Length() int// 存储元素的大小
}
所以我们整个要存储的元素也就定下来了:
type entry struct {
key string
value Value
}
综合上面的描述, 我们定义如下的 缓存 对象:
type Cache struct {
maxBytes int64 // 最大存储容量
nBytes int64 // 已占用的容量
ll *list.List // 双向链表
cache map[string]*list.Element // map
OnEvicted func(key string, value Value) // 缓存删除后的回调方法
}
三、定义创建缓存对象的方法
func New(maxBytes int64, OnEvicted func(key string, value Value)) (c *Cache) {
return &Cache{
maxBytes: maxBytes,
nBytes: 0,
ll: list.New(),
cache: make(map[string]*list.Element),
OnEvicted: OnEvicted
}
}
四、定义 LRU 缓存淘汰策略
func (c *Cache) RemoveOldest() {
// 拿出队尾元素
e := c.ll.Back()
// 从队列中删除
c.ll.Remove(e)
entry := e.Value.(*Entry)
// 删除缓存中的 key
delete(c.cache, entry.key)
// 已使用的大小 - 删除的元素大小
c.cache.nBytes -= (entry.Value.length + len(entry.key))
// callback
OnEvicted(entry.key, entry.Value)
}
五、定义增删改查的方法:
// 添加或更新缓存
func (c *Cache) Add(key string, value Value) {
if c == nil {
panic("breaking error, cache object is nil!")
}
// 如果缓存已经存在
if e, ok := c.cache[key], ok {
// 将元素移到队首
c.ll.MoveToFront(e)
// 覆盖之前的值
c.cache[key] = value
// 减去原先的元素占用的大小
c.nBytes -= e.Value.(*Entry).Value.Length() + len(key)
} else {
// 如果元素不存在, 就创建一个
e := &entry{key, value}
// 放入队首
ll.PushFront(e)
// 放入缓存
c.cache[key] = e
}
// 要存储的元素大小
entryLength := len(key) + value.Length()
// 新元素进入缓存后, 重新计算已占用大小
c.nBytes += entryLength
// 触发缓存淘汰策略, 一直到已占用容量小于最大值
for (c.nBytes + entryLength) > maxBytes {
c.RemoveOldest()
}
}
// 查询缓存
func (c *Cache) Get(key String) (val Value, ok bool) {
// 取出元素
e := c.cache[key]
// 如果查询到了
if e != nil {
// 因为成为了热点数据, 所以要往队首放
c.ll.PushFront(e)
return e.Value.(*Entry).Value, true
}
}