cache 2.单机并发缓存

0.对原教程的一些见解

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

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

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

1.统一的缓存的value对象

//该类型实现了NodeValue接口
type String stringfunc (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.ElementOnEvicted func(key string, value NodeValue)
}

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

 定义加锁的缓存对象

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

type cache struct {mutex      sync.Mutexlru        *lru.CachecacheBytes 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.RWMutexgroups = 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] = greturn 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)    //将源数据添加到缓存mainCachereturn 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] += 1return []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] += 1return []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.mzph.cn/news/208468.shtml

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

相关文章

QT作业2

使用手动连接&#xff0c;将登录框中的取消按钮使用qt4版本的连接到自定义的槽函数中&#xff0c;在自定义的槽函数中调用关闭函数 将登录按钮使用qt5版本的连接到自定义的槽函数中&#xff0c;在槽函数中判断ui界面上输入的账号是否为"admin"&#xff0c;密码是否为…

Navicat 技术指引 | 适用于 GaussDB 分布式的数据查看器

Navicat Premium&#xff08;16.3.3 Windows 版或以上&#xff09;正式支持 GaussDB 分布式数据库。GaussDB 分布式模式更适合对系统可用性和数据处理能力要求较高的场景。Navicat 工具不仅提供可视化数据查看和编辑功能&#xff0c;还提供强大的高阶功能&#xff08;如模型、结…

微服务学习:Nacos微服务架构中的服务注册、服务发现和动态配置Nacos下载

Nacos的主要用途包括&#xff1a; 服务注册与发现&#xff1a;Nacos提供了服务注册和发现的功能&#xff0c;服务提供者可以将自己的服务注册到Nacos服务器上&#xff0c;服务消费者则可以通过Nacos来发现可用的服务实例&#xff0c;从而实现服务调用。 动态配置管理&#xff…

聚观早报 |华为畅享 70正式开售;梦饷科技双12玩法

【聚观365】12月8日消息 华为畅享 70正式开售 梦饷科技双12玩法 华为Mate X5应对火海挑战 谷歌发布AI模型Gemini 字节跳动开启新一轮回购 华为畅享 70正式开售 精致外观与创新科技兼具的华为畅享 70正式开售&#xff0c;1199元起搭载6000mAh超大电池&#xff0c;带来超强…

机器视觉相机镜头光源选型

镜头选型工具 - HiTools - 海康威视 Hikvisionhttps://www.hikvision.com/cn/support/tools/hitools/cl8a9de13648c56d7f/ 海康机器人-机器视觉产品页杭州海康机器人股份有限公司海康机器人HIKROBOT是面向全球的机器视觉和移动机器人产品及解决方案提供商&#xff0c;业务聚焦于…

Navicat 技术指引 | 适用于 GaussDB 分布式的数据生成功能

Navicat Premium&#xff08;16.3.3 Windows 版或以上&#xff09;正式支持 GaussDB 分布式数据库。GaussDB 分布式模式更适合对系统可用性和数据处理能力要求较高的场景。Navicat 工具不仅提供可视化数据查看和编辑功能&#xff0c;还提供强大的高阶功能&#xff08;如模型、结…

GPTs的创建与使用,自定义GPTs中的Actions示例用法 定义和执行特定任务的功能模块 通过API与外部系统或服务的交互

Name 等 Logo:自动生成 Name 介绍 Description 介绍 Instructions 要求或命令等 比如用中文回复&#xff0c;角色。 Knowledge 上传你的知识库&#xff0c;如果你有某一垂直行业的数据&#xff0c;基于数据来回答。比如我有某个芯片的指令集。 Capabilities 都要 Actions&…

Flink 使用场景

Apache Flink 功能强大&#xff0c;支持开发和运行多种不同种类的应用程序。它的主要特性包括&#xff1a;批流一体化、精密的状态管理、事件时间支持以及精确一次的状态一致性保障等。Flink 不仅可以运行在包括 YARN、 Mesos、K8s 在内的多种资源管理框架上&#xff0c;还支持…

vue3递归组件---树形组件

第一种方式&#xff0c;直接自己调用自己 Tree.vue <template><div class"tree"><div v-for"(item, index) in data" :key"item.name">每一层 {{ item.name }}<Tree v-if"item?.children?.length" :dataitem…

AGM离线下载器使用说明

AGM专用离线下载器示意图&#xff1a; 供电方式&#xff1a; 通过 USB 接口给下载器供电&#xff0c;跳线 JP 断开。如果客户 PCB 的 JTAG 口不能提供 3.3V 电源&#xff0c;或仅需烧写下载器&#xff0c;尚未连接用户 PCB 时&#xff0c;采用此种方式供电。 或者&#xff1a…

Linux中的网络时间服务器

本章主要介绍网络时间的服务器 使用chrony配置时间服务器配置chrony客户端服务器同步时间 1.1 时间同步的重要性 一些服务对时间要求非常严格&#xff0c;例如如图所示的由三台服务器搭建的ceph集群 这三台服务器的时间必须保持一致&#xff0c;如果不一致&#xff0c;就会显…

Django讲课笔记01:初探Django框架

文章目录 一、学习目标二、课程导入&#xff08;一&#xff09;课程简介&#xff08;二&#xff09;课程目标&#xff08;三&#xff09;适用人群&#xff08;四&#xff09;教学方式&#xff08;五&#xff09;评估方式&#xff08;六&#xff09;参考教材 三、新课讲授&#…

android项目实战之编辑器集成

引言 项目需要用到编辑器&#xff0c;采用RichEditor&#xff0c;如下效果 实现 1. 引入库2 implementation jp.wasabeef:richeditor-android:2.0.0 2. XML <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_width&q…

如何使用 Wordpress?托管, 网站, 插件, 缓存

这是该系列教程的第一个教程&#xff0c;最终将在运行高性能 LEMP 堆栈的阿里云 ECS 实例上运行一个新的 WordPress 站点。 在本教程中&#xff0c;我们将创建一个运行 Ubuntu 16.04 的实例&#xff0c;然后通过创建超级用户并禁用 root 登录来保护服务器&#xff0c;最后配置…

持续集成交付CICD:使用Maven命令下载Nexus制品

目录 一、实验 1.Maven安装 2.Nexus搭建公共组仓库及Maven全局配置文件 3.使用Maven命令下载Nexus制品 一、实验 1.Maven安装 &#xff08;1&#xff09;CentOS环境安装步骤 tar -xf apache-maven-3.8.6-bin.tar.gz #解压 mv apache-maven-3.8.6 /usr/local/maven #移动…

如何进行更好的面试回复之缓存函数在项目中的性能优化?

缓存函数是一种提高函数性能的技术&#xff0c;在函数被调用时&#xff0c;会将计算结果缓存起来&#xff0c;以便在后续的调用中直接返回缓存的结果&#xff0c;从而减少了重复计算的时间。 缓存函数的实现通常包括两个步骤&#xff1a; 判断缓存是否存在&#xff1a;在函数被…

理解传统模式与互联网时代 消费行为模型 AIDMA , AISAS , SICAS

1 AIDMA与AISAS 消费行为模型&#xff0c;以及所诞生的IT岗位 1.1 传统市场营销消费行为模型 AIDMA模型&#xff1a;Attention&#xff08;吸引&#xff09;&#xff0c;Interest &#xff08;兴趣&#xff09;&#xff0c;Desire&#xff08;欲望&#xff09; &#xff0c;Me…

❤ Mac IDEA使用并运行项目

❤ IDEA导入项目并运行 Mac IDEA使用 (1) 仓库导入 通过获取giett仓库包的url&#xff0c;在idea中导入项目 在gitee里获取项目的ur打开idea&#xff0c;点击 File->new->Project from Version Control (2) 创建数据库ry并导入数据脚本 &#xff08;3&#xff09;修改配…

华为配置Smart Link主备备份示例

定义 Smart Link&#xff0c;又叫做备份链路。一个Smart Link由两个接口组成&#xff0c;其中一个接口作为另一个的备份。Smart Link常用于双上行组网&#xff0c;提供可靠高效的备份和快速的切换机制。 Monitor Link是一种接口联动方案&#xff0c;它通过监控设备的上行接口…

npm私有源构建项目下载依赖报错

Jenkins构建项目报错&#xff0c;依赖找不到 Error: Couldnt find any versions for "babel/helper-module-imports" that matches "^7.22.15"at MessageError.ExtendableBuiltin (/data1/jenkins/tools/jenkins.plugins.nodejs.tools.NodeJSInstallation/…