当前位置: 首页 > article >正文

golang分布式缓存项目 Day 1

:该项目原作者:https://geektutu.com/post/geecache-day1.html。本文旨在记录本人做该项目时的一些疑惑解答以及部分的测试样例以便于本人复习。

LRU缓存淘汰策略

三种缓存淘汰策略

  1. FIFO(First In, First Out)先进先出
    原理:FIFO是一种最简单的缓存淘汰算法,它基于“先进先出”的原则。当缓存满了之后,最早进入缓存的数据将被首先淘汰。
    适用场景:适用于数据访问模式中,数据的访问顺序与数据进入缓存的顺序一致的场景。
    优点:实现简单,公平性较好,每个数据都有相同的淘汰机会。
    缺点:可能会淘汰掉一些频繁访问的数据,导致缓存命中率降低。
  2. LFU(Least Frequently Used)最少使用
    原理:LFU算法淘汰那些在最近一段时间内访问次数最少的数据。它的核心思想是,如果数据在过去被访问得少,那么在未来被访问的可能性也低。
    适用场景:适用于那些访问模式中,某些数据被频繁访问,而其他数据则很少被访问的场景。
    优点:可以有效地保留那些频繁访问的数据,提高缓存的命中率。
    缺点:实现相对复杂,需要跟踪每个数据的访问频率,并且在数据访问模式变化时可能需要调整策略。
  3. LRU(Least Recently Used)最近最少使用
    原理:LRU算法淘汰那些最近最少被使用的数据。它基于这样一个假设:如果数据最近被访问过,那么在未来它被访问的可能性也更高。
    适用场景:适用于那些数据访问模式中,最近访问过的数据在未来很可能再次被访问的场景。
    优点:可以有效地利用缓存空间,提高缓存命中率,是实践中最常用的缓存淘汰算法之一。
    缺点:实现相对复杂,需要维护一个数据访问的时间顺序,以便快速找到最近最少使用的数据。

LRU算法实现

算法图解

在这里插入图片描述
这张图很好地表示了 LRU 算法最核心的 2 个数据结构

  • 绿色的是字典(map),存储键和值的映射关系。这样根据某个键(key)查找对应的值(value)的复杂是O(1),在字典中插入一条记录的复杂度也是O(1)。
  • 红色的是双向链表(double linked
    list)实现的队列。将所有的值放到双向链表中,这样,当访问到某个值时,将其移动到队尾的复杂度是O(1),在队尾新增一条记录以及删除一条记录的复杂度均为O(1)。

具体代码实现

建立缓存池结构体

package project

import (
	"container/list"
)

type Cache struct {
	maxBytes  int64
	nbytes    int64
	ll        *list.List
	cache     map[string]*list.Element
	OnEvicted func(key string, value Value)
}

type entry struct {
	key   string
	value Value
}

type Value interface {
	Len() int
}

  • 在这里我们直接使用 Go 语言标准库实现的双向链表list.List。
  • 字典的定义是 map[string]*list.Element,键是字符串,值是双向链表中对应节点的指针。
  • maxBytes 是允许使用的最大内存,nbytes 是当前已使用的内存,OnEvicted 是某条记录被移除时的回调函数,可以为nil。
  • 键值对 entry 是双向链表节点的数据类型,在链表中仍保存每个值对应的 key 的好处在于,淘汰队首节点时,需要用 key从字典中删除对应的映射。
  • 为了通用性,我们允许值是实现了 Value 接口的任意类型,该接口只包含了一个方法 Len() int,用于返回值所占用的内存大小

PS:list.Element是一个结构体其定义为

type Element struct {
	// Next and previous pointers in the doubly-linked list of elements.
	// To simplify the implementation, internally a list l is implemented
	// as a ring, such that &l.root is both the next element of the last
	// list element (l.Back()) and the previous element of the first list
	// element (l.Front()).
	next, prev *Element

	// The list to which this element belongs.
	list *List

	// The value stored with this element.
	Value any
}

所以element.Value的值可以是任何类型的,需要我们自己具体实现的时候定义下来.

方便实例化 Cache,实现 New() 函数:

func New(maxBytes int64, onEvicted func(string, Value)) *Cache {
	return &Cache{
		maxBytes:  maxBytes,
		ll:        list.New(),
		cache:     make(map[string]*list.Element),
		OnEvicted: onEvicted,
	}
}

查找功能

实现查找功能,具体步骤如下:

找到了
没找到
查找缓存池
是否找到
从字典中找到对应的双向链表的节点
将其放到双链表队列的队尾代表最近刚刚使用
返回空

代码实现

func (c *Cache) Get(Key string) (value Value, ok bool) {
	if ele, ok := c.cache[Key]; ok {
		c.ll.MoveToFront(ele)
		kv := ele.Value.(*entry)
		return kv.value, ok
	}
	return
}

删除功能

具体步骤如下:

找到双向链表中队首元素
更新内存池中所用的空间nbytes
删掉链表和map对应的元素
如果回调函数不空则调用

代码实现

func (c *Cache) RemoveOldest() {
	ele := c.ll.Back()
	if ele != nil {
		c.ll.Remove(ele)
		kv := ele.Value.(*entry)
		delete(c.cache, kv.key)
		c.nbytes -= int64(len(kv.key)) + int64(kv.value.Len())
		if c.OnEvicted != nil {
			c.OnEvicted(kv.key, kv.value)
		}
	}
}

新增/修改功能

具体步骤:

存在
不存在
超出
查找缓存池中的键是否存在
更新对应节点的值并将其移到链表队尾
对应更新所用内存nbytes
判断是否超出最大内存
将其添加到缓存池和链表队尾中
对应更新所用内存nbytes
循环调用删除函数直至小于最大内存

代码实现

func (c *Cache) Add(key string, value Value) {
	if ele, ok := c.cache[key]; ok {
		c.ll.MoveToFront(ele)
		kv := ele.Value.(*entry)
		c.nbytes += int64(value.Len()) - int64(kv.value.Len())
		kv.value = value
	} else {
		ele := c.ll.PushFront(&entry{key, value})
		c.cache[key] = ele
		c.nbytes += int64(len(key)) + int64(value.Len())
	}
	for c.maxBytes != 0 && c.maxBytes < c.nbytes {
		c.RemoveOldest()
	}
}

测试

测试所用样例及其Len()方法实现

package project

import (
	"fmt"
	"testing"
)

type String string

func (d String) Len() int {
	return len(String(d))
}

type test = []struct {
	name       string
	id         int
	key        string
	value      String
	expectedOk bool
}

var tests = test{
	{
		id:         1,
		name:       "test existed key",
		key:        "key1",
		value:      "6666",
		expectedOk: true,
	},
	{
		id:         2,
		name:       "test existed key",
		key:        "key2",
		value:      "1234",
		expectedOk: true,
	},
	{
		id:         3,
		name:       "test existed key",
		key:        "key3",
		value:      "1111",
		expectedOk: true,
	},
	{
		id:         4,
		name:       "test existed key",
		key:        "key4",
		value:      "1314",
		expectedOk: true,
	},
}

测试增加/修改和删除功能

代码实现

func TestGetAndRemove(t *testing.T) {
	lru := New(0, nil)
	for _, tt := range tests {
		lru.Add(tt.key, tt.value)
		if findkey, ok := lru.Get(tt.key); ok == tt.expectedOk && findkey == tt.value {
			fmt.Printf("find key%d successfully\n", tt.id)
		} else {
			fmt.Printf("find key%d failed\n", tt.id)
		}
	}

	lru.RemoveOldest()
	for _, tt := range tests {
		if findkey, ok := lru.Get(tt.key); ok == tt.expectedOk && findkey == tt.value {
			fmt.Printf("find key%d successfully\n", tt.id)
		} else {
			fmt.Printf("find key%d failed\n", tt.id)
		}
	}
}

测试结果
在这里插入图片描述

测试超出最大容量是能否删除最久未使用的节点

代码实现

func TestOverCapacity(t *testing.T) {
	lru := New(int64(24), nil)
	for _, tt := range tests {
		lru.Add(tt.key, tt.value)
		fmt.Printf("add key%d successfully, prsent lru usedcap = %d\n", tt.id, lru.nbytes)
	}

	for _, tt := range tests {
		if findkey, ok := lru.Get(tt.key); ok == tt.expectedOk && findkey == tt.value {
			fmt.Printf("find key%d successfully\n", tt.id)
		} else {
			fmt.Printf("find key%d failed\n", tt.id)
		}
	}
}

测试结果
在这里插入图片描述

测试回调函数

代码实现

func (c *Cache) CallBack(key string, value Value) {
	if _, ok := c.Get(key); ok {
		return
	} else {
		c.Add(key, value)
		c.ll.MoveToFront(c.cache[key])
	}
}

func TestOnEvicted(t *testing.T) {

	LoseKeys := make([]string, 0)
	lru := New(int64(8), func(key string, value Value) {
		LoseKeys = append(LoseKeys, key)
	})

	for _, tt := range tests {
		lru.Add(tt.key, tt.value)
		fmt.Printf("add key%d successfully, prsent lru usedcap = %d\n", tt.id, lru.nbytes)
	}

	for _, tt := range LoseKeys {
		fmt.Printf("%s被丢弃并保存在日志中\n", tt)
	}
}

测试结果
在这里插入图片描述


http://www.kler.cn/a/383840.html

相关文章:

  • 云集电商:如何通过 OceanBase 实现降本 87.5%|OceanBase案例
  • git撤销commit和add
  • Python决策树、随机森林、朴素贝叶斯、KNN(K-最近邻居)分类分析银行拉新活动挖掘潜在贷款客户附数据代码
  • 为什么Uptime+Kuma本地部署与远程使用是网站监控新选择?
  • 【网易云插件】听首歌放松放松
  • Python 学习完基础语法知识后,如何进一步提高?
  • rust编写的系统监测器
  • jvm学习笔记-轻量级锁内存模型
  • Vue2 与 Vue3 的区别
  • C++ -- 继承
  • day52 图论章节刷题Part04(110.字符串接龙、105.有向图的完全可达性、106.岛屿的周长 )
  • promise的用法以及注意事项,看了这篇你就会了
  • 100种算法【Python版】第52篇——无损压缩之霍夫曼编码
  • 查看网路信息-ifconfig命令
  • Tomasulo算法介绍
  • 介绍一下memcpy(c基础)
  • 文本语义分块、RAG 系统的分块难题:小型语言模型如何找到最佳断点?
  • 【Golang】区块链练习(一)
  • 2025天津市考8日报名,建议收藏好报名流程
  • 昆仑通态触摸屏学习路程
  • NFT Insider #154:The Sandbox Alpha 4 第四周开启,NBA Topshot NFT 销量激增至新高
  • WPF中的转换器
  • 机器学习—推理:做出预测(前向传播)
  • WPF+MVVM案例实战(二十二)- 制作一个侧边弹窗栏(CD类)
  • AWS S3在客户端应用不能使用aws-sdk场景下的文件上传与下载
  • 七.numpy模块