cache教程 2.单机并发缓存

0.对原教程的一些见解

个人认为原教程中两点知识的引入不够友好。

首先是只读数据结构 ByteView 的引入使用是有点迷茫的,可能不能很好理解为什么需要ByteView。

第二是主体结构 Group的引入也疑惑。其实要是熟悉groupcache,那对结构Group的使用是清晰明白的。而看该教程的人可能是没有了解过groupcache,直接就引入结构Group,可能不好理解。这一章节希望可以讲明白这两点。

1.统一的缓存的value对象

//该类型实现了NodeValue接口
type String string
 
func (d String) Len() int {
	return len(d)
}

在上节讲解中, 我们存入的每一个元素(键值对)都要计算大小。为了能计算大小,那存入缓存的 value 对象必须实现NodeValue接口的Len()方法。上一节的测试用例中存储的value对象是String(也即是string)。

那么问题来了, 我们存入的 value 可能是 string, int, 也可能自定义的结构体User等等。如果为每一种类型都实现一个 Len() 方法那确实是繁琐。因此,我们希望将存入的每个 value 都转化为统一的类型, 比如:字节数组 []byte。

我们可以抽象了一个只读数据结构 ByteView 用来表示缓存值

ByteView 只有一个数据成员,b []byte,b 将会存储真实的缓存值。

b 是只读的,使用 ByteSlice() 方法返回一个拷贝,防止缓存值被外部程序修改。

//缓存值的抽象与封装
type ByteView struct {
	b []byte
}

func (v ByteView) Len() int {
	return len(v.b)
}

func (v ByteView) ByteSlice() []byte {
	return cloneByte(v.b)
}

func cloneByte(b []byte) []byte {
	c := make([]byte, len(b))
	copy(c, b)
	return c
}

func (v ByteView) String() string {
	return string(v.b)
}

2.实现缓存并发读写

上一节实现的LRU算法是不支持并发读写的。Go中map不是线程安全的。要实现并发读写map,需要加锁,可以使用sync.Mutex。

sync.Mutex 是一个互斥锁,可以由不同的协程加锁和解锁。

先回顾下上一节定义的缓存的整体数据结构

type Cache struct {
	maxBytes  int64      //允许的能使用的最大内存
	nbytes    int64      //已使用的内存
	ll        *list.List //双向链表
	cache     map[string]*list.Element
	OnEvicted func(key string, value NodeValue)
}

要是想的简单点,我们可以在该结构体Cache内部加上sync.Mutex并修改其方法的部分原有逻辑来实现并发读写。但这样就破坏了对扩展开放,对修改关闭的面向对象原则。这是不好的。

 定义加锁的缓存对象

我们可以在Cache结构体基础上再封装一个可以支持并发读写的对象。

type cache struct {
	mutex      sync.Mutex
	lru        *lru.Cache
	cacheBytes int64
}

显然,该新对象中是需要有个互斥锁变量。而每个缓存对象都有能使用的最大内存量上限,使用cacheBytes 字段来存储这个值。

该cache对象也基于互斥锁和lru封装了 get 和 add 方法。

func (c *cache) add(key string, value ByteView) {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	if c.lru == nil {
		c.lru = lru.New(c.cacheBytes, nil)
	}

	c.lru.Add(key, value)
}

func (c *cache) get(key string) (value ByteView, ok bool) {
	c.mutex.Lock()
	defer c.mutex.Unlock()
	if c.lru == nil {
		return
	}

	if v, ok := c.lru.Get(key); ok {
		return v.(ByteView), ok
	}
	return
}

3.提升缓存并发读写能力

互斥锁引发的性能问题

引入锁之后,可能会引起性能问题,思考如下场景:

当有 A个线程访问库存的缓存数据时, 我们给 cache 对象加了锁, 如果此时有 B个线程来访问商品缓存数据,这 A + B 个线程就需要共同竞争一把锁。

要是线程数量大的话,对性能是有影响的,那是因为所有的缓存都被一把锁把持住。那要是我们可以把缓存进行分组,这样首先就可以不用所有的线程都去抢一把锁了。

将缓存数据进行分组

为了提高缓存系统的并发读写的性能(降低锁的竞争程度), 我们想想是否可以再细分锁的范围,分段锁的设计。

可以理解成是先分段再锁,将原本的所有缓存分成了若干段,分别将这若干段放在了不同的组中,每个组有各自的锁,以此提高效率。

如此设计之后, 不同组的存缓数据就隔离了起来, 访问同一组数据的线程才会互相竞争。

这就引出了Group这个结构。

4.Group结构

定义一个分组结构,从上图也可知道,要去访问缓存,就需去找到该组,那如何辨别是这个组呢,这里就是通过组的名字去辨别的,每个组都有个名字。

// 紧接着我们定义一个 分组 类型
type Group struct {
    name      string // 分组名称
    mainCache cache  // 单个缓存对象
}

这时有多个组后,那如何通过组名字快速找到该组了?还是要用map。那肯定又涉及到多个线程并发读写 groups 。这里是找到对应组名字的组而加锁的。我们可以考虑用 读写锁 来解决这个问题。

这里使用读写锁应该比使用互斥锁可以提高并发度。

来看看创建组和通过名字获取组的函数

var (
	rwMu   sync.RWMutex
	groups = make(map[string]*Group)
)

func NewGroup(name string, cacheBytes int64) *Group {
	rwMu.Lock()
	defer rwMu.Unlock()
	g := &Group{
		name:      name,
		mainCache: cache{cacheBytes: cacheBytes},
	}
	groups[name] = g
	return g
}

// 获取 Group 对象的方法
func GetGroup(name string) *Group {
	rwMu.RLock()
	defer rwMu.RUnlock()
	g := groups[name]
	return g
}

缓存查询回调方法

我们要考虑一种情况:如果缓存不存在,应从数据源(文件,数据库等)获取数据并添加到缓存中。

该Cache 是否应该支持多种数据源的配置呢?不应该,一是数据源的种类太多,没办法都实现;二是扩展性不好。如何从源头获取数据,应该是用户决定的事情,我们就把这件事交给用户好了。因此,我们设计了一个回调函数(callback),在缓存不存在时,就可以调用该函数,得到源数据。

这个回调方法我们可以直接定义在上面的 Get 方法的入参中,也可以放在 Group 对象中,为了方便,我们放在Group内。

type Group struct {
    name      string // 组名
    mainCache cache  // 单个缓存对象
		// 新增回调函数
    getter    Getter

}

type Getter interface {
	Get(key string) ([]byte, error)
}

type GetterFunc func(key string) ([]byte, error)

func (f GetterFunc) Get(key string) ([]byte, error) {
	return f(key)
}

 函数类型实现某一个接口,称之为接口型函数,那么该函数也是接口。

其好处:当一个函数的参数类型是接口,那使用者在调用时既能够传入函数作为参数,也能够传入实现了该接口的结构体作为参数

接口型函数不太理解的话,可以看Go接口型函数。

接口型函数在这章节的最后测试中也会进行讲解的,测试中有例子。

 Group 的 Get 方法

首先从本地缓存中查找,若是有则直接返回该缓存数据即可。

若是缓存不存在(即是没击中),则调用 load 方法,调用用户回调函数 g.getter.Get() 获取源数据,并且将源数据添加到缓存 mainCache 中。

func (g *Group) Get(key string) (ByteView, error) {
	if v, ok := g.mainCache.get(key); ok {
		return v, nil
	}
	return g.load(key)
}

func (g *Group) load(key string) (ByteView, error) {
	bytes, err := g.getter.Get(key)
	if err != nil {
		return ByteView{}, err
	}
	value := ByteView{b: cloneByte(bytes)}
	g.mainCache.add(key, value)    //将源数据添加到缓存mainCache
	return value, nil
}

至此,这一章节的单机并发缓存就已经完成了。

5.测试

// 缓存中没有的话,就从该db中查找
var db = map[string]string{
	"tom":  "100",
	"jack": "200",
	"sam":  "444",
}

// 统计某个键调用回调函数的次数
var loadCounts = make(map[string]int, len(db))

创建 group 实例,并测试 Get 方法。

主要测试了两种情况

  • 1)在缓存为空的情况下,能够通过回调函数获取到源数据。
  • 2)在缓存已经存在的情况下,是否直接从缓存中获取,为了实现这一点,使用 loadCounts 统计某个键调用回调函数的次数,如果次数大于1,则表示调用了多次回调函数,没有缓存。
func main() {
	//传函数入参    cache.GetterFunc(funcCbGet)是进行类型转换,不是执行函数
	cache := cache.NewGroup("scores", 2<<10, cache.GetterFunc(funcCbGet))
	//传结构体入参,也可以
	// cbGet := &search{}
	// cache := cache.NewGroup("scores", 2<<10, cbGet)

	for k, v := range db {
		if view, err := cache.Get(k); err != nil || view.String() != v {
			fmt.Println("failed to get value of ",k)
		}

		if _, err := cache.Get(k); err != nil || loadCounts[k] > 1 {
			fmt.Printf("cache %s miss", k)
		}
	}

	if view, err := cache.Get("unknown"); err == nil {
		fmt.Printf("the value of unknow should be empty, but %s got", view)
	}else {
		fmt.Println(err)
	}
}

// 函数的
func funcCbGet(key string) ([]byte, error) {
	fmt.Println("callback search key: ", key)
	if v, ok := db[key]; ok {
		if _, ok := loadCounts[key]; !ok {
			loadCounts[key] = 0
		}
		loadCounts[key] += 1
		return []byte(v), nil
	}
	return nil, fmt.Errorf("%s not exit", key)
}

// 结构体,实现了Getter接口的Get方法,
type search struct {
}

func (s *search) Get(key string) ([]byte, error) {
	fmt.Println("struct callback search key: ", key)
	if v, ok := db[key]; ok {
		if _, ok := loadCounts[key]; !ok {
			loadCounts[key] = 0
		}
		loadCounts[key] += 1
		return []byte(v), nil
	}
	return nil, fmt.Errorf("%s not exit", key)
}

讨论接口型函数

NewGroup中的最后一个参数类型是接口类型。

这里既可以传入函数,也可以传入结构体变量。

而按照这个例子,传入函数是很方便的。只写一个函数就行,而做成结构体的话,还需要新建一个结构体类型,再实现Get方法,这就是很麻烦的。

这里可能就有疑惑了,大家通过这个例子明白,这样做是既可以传入函数,也可以传入结构体变量。但从这例子来看,没必要这样做,就只是传函数就行啦,没必要把NewGroup的最后那个参数类型做成接口类型,只弄成函数类型就行啦。

这是这个例子的,要是在其他更加复杂的情况呢。比如:如果对数据库的操作需要很多信息,地址、用户名、密码,还有很多中间状态需要保持,比如超时、重连、加锁等等。这种情况下,更适合将其封装为一个结构体,再把该结构体传入更好。

既能够将普通的函数类型(需类型转换)作为参数,也可以将结构体作为参数,使用更为灵活,可读性也更好,这就是接口型函数的价值。

这样就不用等我们想要用结构体传参时候,发现类型不符合,传参失败就需要修改代码,这时候就麻烦了。

完整代码:https://github.com/liwook/Go-projects/tree/main/go-cache/2-single-node

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/238863.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

版本控制:让你的代码有迹可循

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

viple与物理机器人(一):线控模拟

为了检测viple程序与物理机器人是否能顺利连接上 如果能顺利连接上&#xff0c;那么&#xff0c;可以通过内建事件从而控制物理机器人的前进、后退、左转、右转以及暂停。 如果不能连接上&#xff0c;首先&#xff0c;程序无法控制物理机器人&#xff0c;其次&#xff0c;当vip…

c++STL使用时的迭代器失效问题

迭代器失效本质上有两种情况&#xff1a; 一是pos的意义变了&#xff08;指向的位置不是想要指向位置&#xff09;&#xff0c;二是pos变成了野指针&#xff08;使用了一块已经被释放了的空间&#xff09;。 迭代器失效会导致程序出现莫名其妙的越界访问、编译报错和获取的位置…

计算机网络:应用层(一)

我最近开了几个专栏&#xff0c;诚信互三&#xff01; > |||《算法专栏》&#xff1a;&#xff1a;刷题教程来自网站《代码随想录》。||| > |||《C专栏》&#xff1a;&#xff1a;记录我学习C的经历&#xff0c;看完你一定会有收获。||| > |||《Linux专栏》&#xff1…

uniapp,点击选中并改变颜色,第二次点击取消选中状态

一、效果图 二、代码实现 字符串的indexOf和数组的indexOf用法一致&#xff01; arr.indexOf(item) 该方法返回某个元素在数组中的位置。若没检索到&#xff0c;则返回 -1。 关键代码&#xff1a;(通过:class绑定) :class"selectList.indexOf(sub.type) ! -1 ? right_ite…

Linux Zabbix企业级监控平台本地部署并实现远程访问

前言 Zabbix是一个基于WEB界面的提供分布式系统监视以及网络监视功能的企业级的开源解决方案。能监视各种网络参数&#xff0c;保证服务器系统的安全运营&#xff1b;并提供灵活的通知机制以让系统管理员快速定位/解决存在的各种问题。 本地zabbix web管理界面限制在只能局域…

SD-WAN跨国网络加速的原理

许多企业需要在全球范围内高效传输和交流数据&#xff0c;然而&#xff0c;跨国网络连接面临着多种挑战&#xff0c;如网络延迟、拥塞和数据包丢失&#xff0c;这些问题可能会显著降低企业的运作效率和客户体验。为了克服这些问题&#xff0c;越来越多的企业正在采用SD-WAN跨国…

android悬浮窗气泡点击穿透事件

一个小众功能记录&#xff1a;新增气泡&#xff0c;拖动气泡&#xff0c;点击气泡事件传递到下层 文章底部附上demo 效果&#xff1a; 1、新建一个service&#xff0c;都在这里面实现 左侧悬浮窗&#xff1a; private void setFloatWinow() {floatingView LayoutInflater.…

第二证券:结构性行情或将延续 泛科技有望继续走强

展望未来&#xff0c;当时已进入重要的方针窗口期&#xff0c;能否有超预期的新方针推出是改变商场的要害。但复盘2023年的行情来看&#xff0c;过早买卖方针预期的成功率并不高&#xff0c;因而主张该方位以防御性资产为主&#xff0c;高股息资产从本年9月份至今现已调整了2个…

科研论文中PPT图片格式选择与转换:EPS、SVG 和 PDF 的比较

当涉及论文中的图片格式时&#xff0c;导师可能要求使用 EPS 格式的图片。EPS&#xff08;Encapsulated PostScript&#xff09;是一种矢量图格式&#xff0c;它以 PostScript 语言描述图像&#xff0c;能够无损地缩放并保持图像清晰度。与像素图像格式&#xff08;如 PNG 和 J…

Redis(三):常见数据类型:List、Set、Zset

List 列表 列表类型是用来存储多个有序的字符串&#xff0c; 如图&#xff1a; a、b、c、d、e 五个元素从左到右组成 了⼀个有序的列表&#xff0c;列表中的每个字符串称为元素&#xff08;element&#xff09;&#xff0c;⼀个列表最多可以存储个元素。在 Redis 中&#xff…

虹科分享 | CanEasy多场景应用,让汽车总线测试更简单

CanEasy是一个基于Windows的总线工具&#xff0c;用于分析和测试CAN、CAN FD和LIN以及汽车以太网系统。通过高度自动化和简单的配置模拟总线流量&#xff0c;CanEasy可用于分析真实网络、模拟虚拟系统&#xff0c;以及在整个开发过程中进行剩余总线模拟&#xff0c;实现从测试到…

Todesk、向日葵等访问“无显示器”主机黑屏问题解决

我的环境是 ubuntu 22.04 安装 要安装 video dummy&#xff0c;请在终端中运行以下命令&#xff1a; sudo apt install xserver-xorg-video-dummy配置 video dummy 的配置文件请自行搜索 使用任何文本编辑器打开此文件。 我的是 /etc/X11/xorg.conf 默认配置文件包含以下内…

vue chrome debugger 无效

昨天晚上debbger可以正常运行的&#xff0c;但是早上起来突然间所有的debugger都不会被命中&#xff0c;重装了vscode,也清了浏览器缓存&#xff0c;可是这个bitch还是不行&#xff01;整整折腾了一早上&#xff0c;就是无法解决&#xff0c;没办法只能找找资料 &#xff0c;搜…

dockerfile创建镜像 lNMP+wordpress

dockerfile创建镜像 lNMPwordpress nginx dockernginx mysql dockermysql php dockerphp nginx vim nginx.conf vim Dockerfile docker network create --subnet172.17.0.0/16 --opt "com.docker.network.bridge.name""docker1" mynetwork docker buil…

DeepDrive双转子径向磁通电机

DeepDrive公司开发的是一种高效、高性能、低成本的双转子径向磁通电机系统&#xff08;含控制器&#xff09;。该系统具有较高的成本效益和资源效率&#xff0c;并拥有更高的能效&#xff0c;能显著提升电动车续航能力&#xff0c;同时亦能有效控制生产成本&#xff0c;减少自然…

【LuatOS】简单案例网页点灯

材料 硬件&#xff1a;合宙ESP32C3简约版&#xff0c;BH1750光照度模块&#xff0c;0.96寸OLED(4P_IIC)&#xff0c;杜邦线若干 接线&#xff1a; ESP32C3.GND — OLED.GND — BH1750.GND ESP32C3.3.3V — OLED.VCC — BH1750.VCC ESP32C3.GPIO5 — OLED.SCL — BH1750.SCL E…

智能优化算法应用:基于猫群算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于猫群算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于猫群算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.猫群算法4.实验参数设定5.算法结果6.参考文献7.MA…

c++ map

unordered_map #include <iostream> #include <string> #include <unordered_map>int main() {// 创建包含三个字符串的&#xff08;映射到字符串的&#xff09;unordered_mapstd::unordered_map<std::string, std::string> u {{"red", &qu…

el-collapse 默认展开第一个(实测有效)

<el-collapse accordion v-model"activeCollapse"> <el-collapse-item v-for"(item, index) in assetList" :name"index" :key"item.id" > 我这个是通过循环, 只需要v-model 绑定的值和 name 相等,就可以实现展开 然后就…