我跟着极客兔兔的教程实现了分布式缓存,该系列文章是对实现过程的总结。
详细实现教程:7天用Go从零实现分布式缓存GeeCache
缓存的大小是有限的,当添加数据发现剩余缓存不够时,需要淘汰缓存中的部分数据。如何选择性的淘汰缓存中的数据呢?常用方法有以下三种。
LRU: 全称Least Recently Use,意为:最近最少使用的。当缓存到达最大值时,就会淘汰最近最久未使用的数据。
LFU: 全称Least Frequently User,意为:最少使用。注意与LRU的区别,LFU强调使用次数最少的。当缓存到达最大值时,就会淘汰最近最少使用的。
FIFO: 全称First In First Out,意为:最先进的最先出去。当缓存到达最大值时,就会淘汰最先进入缓存的那个数据。
这里我们只实现LRU淘汰策略。
lru的实现需要用到两个数据结构:双向循环链表和一个字典map,双向循环链表是缓存内容的主要存放位置,并且借助双向循环链表 来实现数据淘汰。字典map存放的是指向链表节点的指针,便于在链表中查询缓存。
每当我们添加数据时,将数据插入链表头部,同时将节点的指针存储到map中。当我们查询数据时,通过key在map中获取对应节点的指针,然后将该节点放到链表的头部,并返回数据。当缓存淘汰数据时,只需要将链表尾部的数据剔除,并删除map中对应节点的指针。
通过以上方案可以实现lru淘汰策略。但是lru淘汰策略还存在很大问题,数据的剔除是由lru算法决定的,使用者并不能决定何时淘汰。如果旧数据一直不被剔除,就可能造成数据并不一致问题。所以接下来在lru的基础上实现超时淘汰。
实现超时淘汰我们需要增加一个字典map来维护,当我们添加数据时需要指定数据的过期时间,设置为负数表示永不过期。设置了过期时间的数据(过期时间为非负数)也要加入这个map。
接下来要考略的就是什么时候剔除过期数据,这里有三种方案:
这里为参考了Redis的超时淘汰策略,将惰性删除和定期删除结合使用。每当获取数据时会查看该数据是否过期,如果过期就将该数据删除。与此同时,会另外开启一个协程,定期的从缓存中抽取部分数据,删除过期数据。
my-cache2/evict/cache.go
type Value interface {
Len() int
}
type entity struct {
key string
v Value
ddl int64
}
type Cache struct {
maxBytes uint64
uBytes uint64
ll *list.List
allDataMap map[string]*list.Element
expireDataMap map[string]*list.Element
}
func NewCache(maxBytes uint64) *Cache {
return &Cache{
maxBytes: maxBytes,
ll: new(list.List),
allDataMap: make(map[string]*list.Element),
}
}
在实例化缓存时并没有初始化expireDataMap,是因为这里会使用延迟加载,当第一个设置了过期时间的数据被添加时,这个map才会被初始化。
func (c *Cache) Add(key string, v Value, expire int64) error {
if c.allDataMap[key] != nil{
c.remove(key)
}
vBytes := uint64(v.Len()) + uint64(len([]byte(key)))
if vBytes > c.maxBytes-c.uBytes {
if vBytes > c.maxBytes {
return fmt.Errorf("%s is not find in cache", key)
}
c.RemoveOldest()
return c.Add(key, v, expire)
}
var ddl int64 = -1
if expire > 0 {
ddl = time.Now().Unix() + expire
}
e := c.ll.PushFront(&entity{key, v, ddl})
c.uBytes += vBytes
c.allDataMap[key] = e
if expire > 0 {
if c.expireDataMap == nil {
c.expireDataMap = make(map[string]*list.Element)
}
c.expireDataMap[key] = e
}
return nil
}
func (c *Cache) RemoveOldest() {
back := c.ll.Back()
c.ll.Remove(back)
e := back.Value.(*entity)
delete(c.allDataMap, e.key)
delete(c.expireDataMap, e.key)
c.uBytes -= uint64(back.Value.(*entity).v.Len()) + uint64(len(e.key))
}
func (c *Cache) remove(key string) {
element := c.allDataMap[key]
c.ll.MoveToBack(element)
c.RemoveOldest()
}
func (c *Cache) Get(key string) (Value, error) {
element := c.allDataMap[key]
if element == nil {
return nil, fmt.Errorf("%s is not find in cache", key)
}
entity := element.Value.(*entity)
if entity.ddl > 0 && entity.ddl < time.Now().Unix() {
c.remove(key)
return nil, fmt.Errorf("%s is not find in cache", key)
}
c.ll.MoveToFront(element)
return entity.v, nil
}
func (c *Cache) DeleteExpired() {
go func() {
for true {
if c.expireDataMap == nil {
time.Sleep(1 * time.Second)
continue
}
count := 20
expired := 0
for _, v := range c.expireDataMap {
if count <= 0 {
break
}
e := v.Value.(*entity)
if e.ddl <= time.Now().Unix() {
expired++
c.remove(e.key)
}
count--
}
if expired < 5 {
time.Sleep(1 * time.Second)
}
}
}()
}
func (c *Cache) Print() {
fmt.Println("allDataMap:")
for _, v := range c.allDataMap {
fmt.Printf("%v ", v.Value.(*entity).key)
}
fmt.Println("\nexpireDataMap")
for _, v := range c.expireDataMap {
fmt.Printf("%v ", v.Value.(*entity).key)
}
fmt.Println()
}
func (c *Cache) Len() int {
return len(c.allDataMap)
}
在cache.go中实现这两个函数是为了更好的进行测试。
my-cache2/test/cache_test.go
type v struct {
s string
}
func (v v) Len() int {
return len(v.s)
}
// 测试对url剔除、对设置超时时间和未设置超时时间进行分组
func TestCache1(t *testing.T) {
cache := evict.NewCache(20)
cache.Add("12", v{"ab"}, 2)
cache.Add("34", v{"ab"}, 2)
cache.Add("56", v{"ab"}, 2)
cache.Add("78", v{"ab"}, 2)
cache.Add("90", v{"ab"}, 2)
cache.Add("91", v{"ab"}, 2)
cache.Add("92", v{"ab"}, 2)
cache.Add("93", v{"ab"}, -1)
cache.Add("94", v{"ab"}, -1)
cache.Add("95", v{"ab"}, -1)
cache.Print()
}
// 测式定时随机剔除
func TestCache2(t *testing.T) {
cache := evict.NewCache(20)
cache.DeleteExpired()
cache.Add("12", v{"ab"}, 2)
cache.Add("34", v{"ab"}, 2)
cache.Add("56", v{"ab"}, 2)
cache.Add("78", v{"ab"}, 2)
cache.Add("90", v{"ab"}, 2)
cache.Add("95", v{"ab"}, -1)
cache.Print()
time.Sleep(4 * time.Second)
cache.Print()
}
//测试Get不会删除未过期数据,但是会删除过期数据
func TestCache3(t *testing.T) {
cache := evict.NewCache(20)
cache.Add("12", v{"ab"}, 2)
cache.Add("34", v{"ab"}, 2)
cache.Add("56", v{"ab"}, 2)
cache.Add("78", v{"ab"}, 2)
cache.Add("90", v{"ab"}, 2)
fmt.Println(cache.Get("12"))
fmt.Println(cache.Get("34"))
fmt.Println(cache.Get("56"))
fmt.Println(cache.Get("78"))
fmt.Println(cache.Get("90"))
time.Sleep(4 * time.Second)
fmt.Println(cache.Get("12"))
fmt.Println(cache.Get("34"))
fmt.Println(cache.Get("56"))
fmt.Println(cache.Get("78"))
fmt.Println(cache.Get("90"))
cache.Print()
}
=== RUN TestCache1
allDataMap:
93 94 95 91 92
expireDataMap
91 92
--- PASS: TestCache1 (0.00s)
PASS
=== RUN TestCache2
allDataMap:
56 78 90 95 34
expireDataMap
34 56 78 90
allDataMap:
95
expireDataMap
--- PASS: TestCache2 (4.00s)
PASS
=== RUN TestCache3
{ab}
{ab}
{ab}
{ab}
{ab}
12 is not find in cache
34 is not find in cache
56 is not find in cache
78 is not find in cache
90 is not find in cache
allDataMap:
expireDataMap
--- PASS: TestCache3 (4.01s)
PASS