cache教程 2.单机并发缓存

news2024/11/24 15:20:16

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

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

LangChain学习二:提示-实战

文章目录 上一节内容&#xff1a;LangChain学习一&#xff1a;模型-实战学习目标&#xff1a;提示词及提示词模板的运用学习内容一&#xff1a;什么是提示词&#xff1f;学习内容二&#xff1a;提示词模板2.1 聊天提示模板实战:首先需要声明和定义一个模板实战:把提示词模板放入…

分类预测 | GASF-CNN格拉姆角场-卷积神经网络的数据分类预测

分类预测 | GASF-CNN格拉姆角场-卷积神经网络的数据分类预测 目录 分类预测 | GASF-CNN格拉姆角场-卷积神经网络的数据分类预测分类效果基本描述模型描述程序设计参考资料 分类效果 基本描述 1.GASF-CNN格拉姆角场-卷积神经网络的数据分类预测&#xff08;完整源码和数据) 2.自…

Leetcode2477. 到达首都的最少油耗

Every day a Leetcode 题目来源&#xff1a;2477. 到达首都的最少油耗 解法1&#xff1a;贪心 深度优先搜索 题目等价于给出了一棵以节点 0 为根结点的树&#xff0c;并且初始树上的每一个节点上都有一个人&#xff0c;现在所有人都需要通过「车子」向结点 0 移动。 对于…

os.walk()遍历文件夹/文件

天行健&#xff0c;君子以自强不息&#xff1b;地势坤&#xff0c;君子以厚德载物。 每个人都有惰性&#xff0c;但不断学习是好好生活的根本&#xff0c;共勉&#xff01; 文章均为学习整理笔记&#xff0c;分享记录为主&#xff0c;如有错误请指正&#xff0c;共同学习进步。…

智能优化算法应用:基于人工蜂鸟算法无线传感器网络(WSN)覆盖优化 - 附代码

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

Qt槽函数不响应不执行的一种原因:ui提升导致重名

背景&#xff1a; 一个包含了组件提升的ui&#xff0c;有个按钮的槽函数就是不响应&#xff0c;于是找原因。 分析&#xff1a; 槽函数的对应一是通过connect函数绑定信号&#xff0c;二是on_XXX_signal的命名方式。界面上部件的槽函数通常是第二种。 我反复确认细节&#…

正则表达式:字符串处理的瑞士军刀

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

图的搜索(一):广度优先搜索算法和深度优先搜索算法

图的搜索&#xff08;一&#xff09;&#xff1a;广度优先搜索算法和深度优先搜索算法 本章主要记录了图的搜索算法&#xff0c;和可以解决图的基本问题——最短路径问题的算法。本章主要对图搜索的相关算法进行了介绍&#xff1a;广度优先搜索算法、深度优先搜索算法。 下一…

蓝牙物联网开发应用实例|「蓝牙道钉」规范共享单车停放

为进一步改善城市环境&#xff0c;解决共享单车乱停乱放、随意占道等顽疾&#xff0c;引导骑行者文明停放&#xff0c;近期&#xff0c;铜陵市试点启用共享单车“蓝牙道钉”技术。 共享单车“蓝牙道钉”是通过在人行便道的停放区域内安装“道钉”设备&#xff0c;凭借蓝牙高精…

电视节目中活动灭灯系统是如何实现的

活动灭灯系统主要用于各种需要亮灯或灭灯的活动节目&#xff0c;如招聘灭灯、相亲灭灯等。有多种灯光颜色供选择&#xff0c;本设备通过按钮灯软件组合实现&#xff0c;用户可以自己设置亮灯或灭灯规则。 软件功能&#xff1a; 1、后台统一控制亮灯&#xff0c;重新开始下轮…

three.js 入门三:buffergeometry贴图属性(position、index和uvs)

环境&#xff1a; three.js 0.159.0 一、基础知识 geometry&#xff1a;决定物体的几何形状、轮廓&#xff1b;material&#xff1a;决定物体呈现的色彩、光影特性、贴图皮肤&#xff1b;mesh&#xff1a;场景中的物体&#xff0c;由geometry和materia组成&#xff1b;textu…

虹科Pico汽车示波器 | 汽车免拆检修 | 2018款东风风神AX7车发动机怠速抖动、加速无力

一、故障现象 一辆2018款东风风神AX7车&#xff0c;搭载10UF01发动机&#xff0c;累计行驶里程约为5.3万km。该车因发动机怠速抖动、加速无力及发动机故障灯异常点亮而进厂维修&#xff0c;维修人员用故障检测仪检测&#xff0c;提示气缸3失火&#xff1b;与其他气缸对调点火线…

Shell变量的奇妙用法,让你的Shell脚本更简洁高效

当涉及到命令行工具和脚本编写时&#xff0c;Shell变量是一个非常重要的概念。利用Shell变量的一些奇妙用法&#xff0c;我们可以用一个简单的表达式实现复杂操作&#xff0c;使我们的命令更加简洁高效。 本文将介绍一些常用的Shell变量操作符&#xff0c;包括字符串操作、数组…

SAP UI5 walkthrough step5 Controllers

在这个章节&#xff0c;我们要做的是&#xff0c;将之前的text文本展示为一个按钮&#xff0c;并将声明绑定在点击按钮事件。 因为改的是外观&#xff0c;所以我们修改的是view.XML webapp/view/App.view.xml <mvc:ViewcontrollerName"ui5.walkthrough.controller.A…

Nacos注册中心客户端容灾

目前Nacos客户端有一个FailoverReactor来进行容灾文件的管理&#xff0c;可以通过在指定磁盘文件里写入容灾数据来进行客户端使用数据的覆盖。FailoverReactor目前会拦截Nacos客户端查询接口调用&#xff0c;以getAllInstances接口为例&#xff0c;目前FailoverReactor的工作流…

uni-app实现安卓原生态调用身份证阅读器读卡库读身份证和社保卡、银行卡、IC卡等功能

DONSEE系列多功能读写器Android Uniapp API接口规范V1.0.0 本项目Uniapp调用了身份证读卡器的库文件&#xff1a;DonseeDeviceLib-debug.aar&#xff0c;该库放到nativeplugins\donsee-card\android&#xff0c;然后会自动加载。SDK会自动检查是否拥有USB设备权限&#xff0c;…

2023五岳杯量子计算挑战赛数学建模思路+模型+代码+论文

赛题思路&#xff1a;12月6日晚开赛后第一时间更新&#xff0c;获取见文末名片 “五岳杯”量子计算挑战赛&#xff0c;是国内专业的量子计算大赛&#xff0c;也是玻色量子首次联合移动云、南方科技大学共同发起的一场“企校联名”的国际竞赛&#xff0c;旨在深度融合“量子计算…

人工智能教程(三):更多有用的 Python 库

目录 前言 推荐 JupyterLab 入门 复杂的矩阵运算 其它人工智能和机器学习的 Python 库 前言 在本系列的上一篇人工智能教程&#xff08;二&#xff09;&#xff1a;人工智能的历史以及再探矩阵中&#xff0c;我们回顾了人工智能的历史&#xff0c;然后详细地讨论了矩阵。在…

二维数组附近遍历所有值

二维数组附近遍历所有值 假如以56点为中心&#xff0c;上下左右近距离遍历附近值&#xff0c;看代码&#xff0c;代码把思路写出来了&#xff0c;边界问题暂不处理。 #include<iostream> using namespace std;void FindNearPos(int (*int_arr)[10] , int p_row , int …

Linux centos8安装JDK1.8、tomcat

一、安装jdk 1.如果之前安装过jdk&#xff0c;先卸载掉旧的 rpm -qa | grep -i jdk 2.检查yum中有没有java1.8的包 yum list java-1.8* 3.yum安装jdk yum install java-1.8.0-openjdk* -y 4.验证 二、安装tomcat Index of /tomcat 可以在这里选择你想要安装的tomcat版本…