手撕分布式缓存之一

项目参考GitHub上的高分项目7days-golang,文章内不一定展示完整代码,文章的目的是分享自己在实现分布式缓存的过程中对具体代码实现的解释与见解,文章的编写的时间均是在项目实现之后,可放心参考,有些地方会加上自己对具体实现的想法,具体会有特殊的标识。

本章目录

  • (1)定义缓存结构体与实现底层功能函数
    • (1.1)前言
    • (1.2)确定实现逻辑
    • (1.3)确定数据结构
    • (1.4)代码实现逻辑

(1)定义缓存结构体与实现底层功能函数

(1.1)前言

这一节的内容主要实现了缓存的底层逻辑,比如:空间占满时如何清理历史数据的算法;插入数据与获取数据;以及结构体的定义等。并没有实现分布式的效果,但后续的功能扩展以性能优化均是基于本章的底层函数和算法进行实现。

(1.2)确定实现逻辑

项目源码中的缓存淘汰算法使用的是LRU(最近最少使用)淘汰策略,区别于LFU(最少使用)淘汰策略,LRU是不考虑历史的访问频率的,是从时间维度考虑最少访问的;LFU是考虑历史访问频率的,因此LFU受到历史数据的影响很大;比如一个key在很早之前经常被访问,但是最近没有访问,采用LFU算法很难将其淘汰,但是使用LRU算法很快就会将其淘汰掉。后续整个项目完成之后,可以多实现几个底层算法逻辑。

LRU算法实现的关键在于如何去快速的查找最近最少使用的key,go的contains包提供了双向链表的数据结构,支持的函数功能可以满足之后的数据处理,比如:将数据移至链表头部(这里定义头部front的数据是最新访问的数据,因此尾部的数据就是最不经常访问的数据,在缓存空间用尽时可以直接删除处于尾部的数据)。

通过源码可以得知双向链表的Value类型声明为any,因此我们为便于记录(比如空间满后,删除数据时可以获取到被删除数据的key和value),可以在双向链表的value字段中存储数据的key和value,并且存储的数据的value必须是实现了Len()函数的类型,这是因为缓存不仅需要记录数据的键值,也需要实时的更新当前缓存占用的空间大小,否则淘汰算法将无法启动。

除了LRU算法的实现逻辑外,主要用于数据存储的数据结构是map,map中value的数据类型应该与双向列表的value的数据类型一致,否则通过map找到的值再去双向链表中定位时,还需要做一层数据转换。

综上: 我们的缓存结构就比较清晰了,分别是一个map,一个双向链表,一个包含key-value的已占用空间大小,一个规定的空间大小,除此之外,我们还可以做一些延伸,比如:源码里使用了删除数据时会触发的函数OnEvicted。

(1.3)确定数据结构

// 缓存整体的数据结构
type Cache struct {
	maxBytes int64
	nbytes   int64
	// list 是双向链表
	ll *list.List
	// cache 一个键值对 其中键是key,例如redis中的key,值是一个双向列表,相当于redis的一个key对应的值的列表值
	cache map[string]*list.Element
	// optional and executed when an entry is purged.
	OnEvicted func(key string, value Value)
}

// 实际存储在list的element中的value的数据结构
type entry struct {
	key   string
	value Value
}

// entry中的value必须要实现的接口Len()
type Value interface {
	Len() int
}

(1.4)代码实现逻辑

注释很详细,有问题评论区友善发问(不是义务分享,请摆正态度)

// 初始化Cache的结构体
func New(maxBytes int64, onEvicted func(string, Value)) *Cache {
	return &Cache{
		maxBytes:  maxBytes,
		ll:        list.New(),
		cache:     make(map[string]*list.Element),
		OnEvicted: onEvicted,
	}
}

// Add insert key-value pair into map and push it front of freqList
// 1. how to solve repeat key?
// cover it.
// 2. how to define front and back?
// people define it, in this, the most frequently visited key move to front, vice versa
func (c *CacheLRU) Add(key string, value Value) {
	// exist
	if ele, ok := c.meMap[key]; ok {
		// move value to front
		c.freqList.MoveToFront(ele)
		// bind it type
		oe := ele.Value.(*Entity)
		// resize
		c.occupyLength += int64(value.Len()) - int64(oe.value.Len())
		// revalue
		oe.value = value
	} else {
		// pushFront
		ele := c.freqList.PushFront(&Entity{key: key, value: value})
		// acc size
		c.occupyLength += int64(value.Len()) + int64(len(key))
		// insert ele
		c.meMap[key] = ele
	}
	//	 delete the key-value, if list oversize
	for c.occupyLength >= c.maxLength {
		c.Remove()
	}
}

// Remove if the key exist, remove it. if success finish it, return true
func (c *CacheLRU) Remove() bool {
	ele := c.freqList.Back()
	if ele != nil {
		c.freqList.Remove(ele)
		oe := ele.Value.(*Entity)
		delete(c.meMap, oe.key)
		c.occupyLength -= int64(len(oe.key)) + int64(oe.value.Len())
		if c.onEvicted != nil {
			c.onEvicted(oe.key, oe.value)
		}
	}
	return true
}

// Get if you don't hit the key, response is erred
func (c *CacheLRU) Get(key string) (Value, bool) {
	// exist
	if ele, ok := c.meMap[key]; ok {
		// put the key to front
		c.freqList.MoveToFront(ele)
		
		oe := ele.Value.(*Entity)
		return oe.value, true
	}
	return nil, false
}

// Len() calculate the list's length
func (c *CacheLRU) Len() int {
	return c.freqList.Len()
}

你可能感兴趣的:(分布式缓存,分布式,缓存)