golang分布式缓存项目 Day2 单机并发缓存

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

支持并发读写

接下来我们使用 sync.Mutex 封装 LRU 的几个方法,使之支持并发的读写。在这之前,我们抽象了一个只读数据结构 ByteView 用来表示缓存值,是 GeeCache 主要的数据结构之一。

package project// A ByteView holds an immutable view of bytes.
type ByteView struct {b []byte
}// Len returns the view's length
func (v ByteView) Len() int {return len(v.b)
}// ByteSlice returns a copy of the data as a byte slice.
func (v ByteView) ByteSlice() []byte {return cloneBytes(v.b)
}// String returns the data as a string, making a copy if necessary.
func (v ByteView) String() string {return string(v.b)
}func cloneBytes(b []byte) []byte {c := make([]byte, len(b))copy(c, b)return c
}
  • ByteView 只有一个数据成员,b []byte,b 将会存储真实的缓存值。选择 byte类型是为了能够支持任意的数据类型的存储,例如字符串、图片等。

  • 实现 Len() int 方法,我们在 lru.Cache的实现中,要求被缓存对象必须实现 Value 接口,即 Len() int
    方法,返回其所占的内存大小。

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

接下来就可以为 lru.Cache 添加并发特性了。

package projectimport ("goLang/project/lru""sync"
)type cache struct {mu         sync.Mutexlru        *lru.CachecacheBytes int64
}func (c *cache) add(key string, value ByteView) {c.mu.Lock()defer c.mu.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.mu.Lock()defer c.mu.Unlock()if c.lru == nil {return}if v, ok := c.lru.Get(key); ok {return v.(ByteView), ok}return
}···
  • cache.go 的实现非常简单,实例化 lru,封装 get 和 add 方法,并添加互斥锁 mu。
  • 在 add 方法中,判断了 c.lru 是否为 nil,如果等于 nil 再创建实例。这种方法称之为延迟初始化(Lazy Initialization),一个对象的延迟初始化意味着该对象的创建将会延迟至第一次使用该对象时。主要用于提高性能,并减少程序内存要求。

主体结构Group

Group 是 GeeCache 最核心的数据结构,负责与用户的交互,并且控制缓存值存储和获取的流程。
在这里插入图片描述
我们将在 geecache.go 中实现主体结构 Group,那么 GeeCache 的代码结构的雏形已经形成了。
在这里插入图片描述

回调Getter

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

// A Getter loads data for a key.
type Getter interface {Get(key string) ([]byte, error)
}// A GetterFunc implements Getter with a function.
type GetterFunc func(key string) ([]byte, error)// Get implements Getter interface function
func (f GetterFunc) Get(key string) ([]byte, error) {return f(key)
}
  • 定义接口 Getter 和 回调函数 Get(key string)([]byte, error),参数是 key,返回值是[]byte。
  • 定义函数类型 GetterFunc,并实现 Getter 接口的 Get 方法。
  • 函数类型实现某一个接口,称之为接口型函数,方便使用者在调用时既能够传入函数作为参数,也能够传入实现了该接口的结构体作为参数。

我们可以写一个测试用例来保证回调函数能够正常工作。

func TestGetter(t *testing.T) {var f Getter = GetterFunc(func(key string) ([]byte, error) {return []byte(key), nil})expect := []byte("key")if v, _ := f.Get("key"); !reflect.DeepEqual(v, expect) {t.Errorf("callback failed")}
}

Group 的定义

接下来是最核心数据结构 Group 的定义:

// A Group is a cache namespace and associated data loaded spread over
type Group struct {name      stringgetter    GettermainCache cache
}var (mu     sync.RWMutexgroups = make(map[string]*Group)
)// NewGroup create a new instance of Group
func NewGroup(name string, cacheBytes int64, getter Getter) *Group {if getter == nil {panic("nil Getter")}mu.Lock()defer mu.Unlock()g := &Group{name:      name,getter:    getter,mainCache: cache{cacheBytes: cacheBytes},}groups[name] = greturn g
}// GetGroup returns the named group previously created with NewGroup, or
// nil if there's no such group.
func GetGroup(name string) *Group {mu.RLock()g := groups[name]mu.RUnlock()return g
}
  • 一个 Group 可以认为是一个缓存的命名空间,每个 Group 拥有一个唯一的名称 name。比如可以创建三个
    Group,缓存学生的成绩命名为 scores,缓存学生信息的命名为 info,缓存学生课程的命名为 courses。
  • 第二个属性是 getter Getter,即缓存未命中时获取源数据的回调(callback)。
  • 第三个属性是 mainCache cache,即一开始实现的并发缓存。 构建函数 NewGroup 用来实例化 Group,并且将group 存储在全局变量 groups 中。
  • GetGroup 用来特定名称的 Group,这里使用了只读锁 RLock(),因为不涉及任何冲突变量的写操作。

Group 的 Get 方法

接下来是 GeeCache 最为核心的方法 Get:

// Get value for a key from cache
func (g *Group) Get(key string) (ByteView, error) {if key == "" {return ByteView{}, fmt.Errorf("key is required")}if v, ok := g.mainCache.get(key); ok {log.Println("[GeeCache] hit")return v, nil} else {log.Println("[cache] unhit")return 	g.load(key)}
}func (g *Group) load(key string) (value ByteView, err error) {return g.getLocally(key)
}func (g *Group) getLocally(key string) (ByteView, error) {bytes, err := g.getter.Get(key)if err != nil {return ByteView{}, err}value := ByteView{b: cloneBytes(bytes)}g.populateCache(key, value)return value, nil
}func (g *Group) populateCache(key string, value ByteView) {g.mainCache.add(key, value)
}
  • Get 方法实现了上述所说的流程 ⑴ 和 ⑶。
  • 流程 ⑴ :从 mainCache 中查找缓存,如果存在则返回缓存值。
  • 流程 ⑶ :缓存不存在,则调用 load 方法,load 调用 getLocally(分布式场景下会调用 getFromPeer 从其他节点获取),getLocally 调用用户回调函数 g.getter.Get() 获取源数据,并且将源数据添加到缓存 mainCache 中(通过 populateCache 方法)

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

PS
为什么load方法定义为调用getlocally方法,不能直接调用getlocally方法吗?
:分布式场景下,load 会先从远程节点获取 getFromPeer失败了再回退到 getLocally,设计时预留了。(为后面的分布式场景下设计预留空间)

测试

首先,用一个 map 模拟耗时的数据库。

var db = map[string]string{"Tom":  "630","Jack": "589","Sam":  "567",
}

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

func TestGet(t *testing.T) {loadCounts := make(map[string]int, len(db))gee := NewGroup("scores", 2<<10, GetterFunc(func(key string) ([]byte, error) {log.Println("[slow db] 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 exist", key)}))for k, v := range db {if view, err := gee.Get(k); err != nil || view.String() != v {t.Fatal("failed to get value of Tom")} // load from callback functionif _, err := gee.Get(k); err != nil || loadCounts[k] > 1 {t.Fatalf("cache %s miss", k)} // cache hit}if view, err := gee.Get("unknown"); err == nil {t.Fatalf("the value of unknow should be empty, but %s got", view)}
}
  • 在这个测试用例中,我们主要测试了 2 种情况
  • 1)在缓存为空的情况下,能够通过回调函数获取到源数据
  • 2)在缓存已经存在的情况下,是否直接从缓存中获取,为了实现这一点,使用 loadCounts 统计某个键调用回调函数的次数,如果次数大于1,则表示调用了多次回调函数,没有缓存。

测试结果如下:
在这里插入图片描述
上述测试的基本逻辑
第一次搜索db中的键–>不存在,打印[cache] unhit,并调用load(key) ->再调用getlocally(key)方法 ->再调用Getter接口的Get方法,也就是GetterFunc函数,打印[slow db] search key…->调用populateCache->调用add方法将键值对加入缓存池中 ->第二次搜索db中的键 ->存在,打印[Cache] hit–>结束一次循环

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

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

相关文章

2024 年将 Swagger 导入 Postman 图文教程

2024 年将 Swagger 导入 Postman 图文教程

从入门到精通:hello-algo开源项目助你系统学习数据结构与算法

文章目录 前言1.关于hello-algo2.安装Docker和Docker compose3.本地部署hello-algo4. hello-algo本地访问5.cpolar内网穿透工具安装6.创建远程连接公网地址7.固定Uptime Kuma公网地址 前言 本文将探讨如何在本地环境中部署hello-algo这一算法学习必备项目&#xff0c;并利用cp…

SystemVerilog学习笔记(七):函数与任务

函数 函数的主要用途是编写一段可以随时调用n次的代码&#xff0c;只需调用函数名即可&#xff0c;不需要任何模拟时间来执行。函数是返回类型&#xff0c;仅返回函数声明中提到的单个值&#xff0c;如果未声明则返回一个位的值。 语法&#xff1a; initial begin functio…

地下水数值模拟、 地下水环评、Visual modflow Flex、Modflow

地下水数值模拟软件Visual modflow Flex实践技术应用 地下水数值模拟软件的应用&#xff0c;主要围绕的目前应用较为广泛的Visual Modflow Flex 6.1软件版本开展&#xff0c;结合具体应用场景&#xff0c;实例讲解软件的全流程应用过程&#xff0c;包括数据处理分析、数值模型…

丹摩征文活动|Llama3.1:从安装到熟练使用的全方位教程

0.前言 目前关于 Llama 3.1 的详细安装和使用指南在网络上较为分散&#xff0c;对于许多想要深入了解和应用该模型的人来说&#xff0c;缺乏一个系统、全面的指导资料。为了填补这一空白&#xff0c;本文应运而生。旨在为广大读者提供从 Llama 3.1 的安装到熟练使用的全方位指…

UI自动化测试|CSS元素定位实践

前言 自动化测试元素定位是指在自动化测试过程中&#xff0c;通过特定的方法或策略来准确识别和定位页面上的元素&#xff0c;以便对这些元素进行进一步的操作或断言。这些元素可以是文本框、按钮、链接、图片等HTML页面上的任何可见或不可见的组件。 在自动化测试中&#xf…

软件架构与模式分析

软件架构模式分析 软件架构模式和架构风格是两个相关但不同的概念。 软件架构模式&#xff08;Software Architecture Patterns&#xff09;是一种在软件工程领域广泛应用的规范化、可复用的架构设计方案。它是通过抽象和提炼出解决特定问题所需的结构、组件、关系和规则等&am…

npm完整发包流程(亲测可验证)

1. 准备工作 &#xff08;1&#xff09; 在npm官网上注册一个账号 &#xff08;2&#xff09; 注册成功之后&#xff0c;npm会发送一封邮件给你&#xff0c;点击邮件里面的链接&#xff0c;做确认关联操作&#xff08;必需&#xff09; 2. 创建自己的npm包 &#xff08;…

无插件直播流媒体音视频播放器EasyPlayer.js播放器多分屏超过6路不能播放如何解决

EasyPlayer.js H5播放器&#xff0c;是一款能够同时支持HTTP、HTTP-FLV、HLS&#xff08;m3u8&#xff09;、WS、WEBRTC、FMP4视频直播与视频点播等多种协议&#xff0c;支持H.264、H.265、AAC、G711A、Mp3等多种音视频编码格式&#xff0c;支持MSE、WASM、WebCodec等多种解码方…

从零开始使用YOLOv11——Yolo检测detect数据集自建格式转换为模型训练格式:20w+图片1w+类别代码测试成功

在之前的文章中记录了YOLO环境的配置安装和基本命令的一些使用&#xff0c;上一篇博文的地址快速链接&#xff1a;从零开始使用YOLOv8——环境配置与极简指令&#xff08;CLI&#xff09;操作&#xff1a;1篇文章解决—直接使用&#xff1a;模型部署 and 自建数据集&#xff1a…

【HAProxy06】企业级反向代理HAProxy调度算法之其他算法

HAProxy 调度算法 HAProxy通过固定参数 balance 指明对后端服务器的调度算法&#xff0c;该参数可以配置在listen或backend选项中。 HAProxy的调度算法分为静态和动态调度算法&#xff0c;但是有些算法可以根据不同的参数实现静态和动态算法 相互转换。 官方文档&#xff1…

Leetcode 检测相邻递增子数组

3349. 检测相邻递增子数组 I 给你一个由 n 个整数组成的数组 nums &#xff0c;请你找出 k 的 最大值&#xff0c;使得存在 两个 相邻 且长度为 k 的 严格递增 子数组 。具体来说&#xff0c;需要检查是否存在从下标 a 和 b (a < b) 开始的 两个 子数组&#xff0c;并满…

【STL栈和队列】:高效数据结构的应用秘籍

前言&#xff1a; C 标准模板库&#xff08;STL&#xff09;为我们提供了多种容器&#xff0c;其中 stack&#xff08;栈&#xff09;和 queue&#xff08;队列&#xff09;是非常常用的两种容器。 根据之前C语言实现的栈和队列&#xff0c;&#xff08;如有遗忘&#xff0c;…

香江电器从A股到港股7年漫长上市路,收入后退停滞不前

《港湾商业观察》施子夫 9月29日&#xff0c;湖北香江电器股份有限公司&#xff08;以下简称&#xff0c;香江电器&#xff09;递表港交所引起外界关注&#xff0c;公司的独家保荐机构为国金证券。 回顾香江电器的IPO之旅&#xff0c;可以说是颇为坎坷&#xff0c;多次尝试A股…

从python源码到可自动更新软件

相关阅读 标题链接如何打包python程序为exebczl【auto-py-to-exe 可视化打包python到exe】51CTO ZATA 1. python源码 打包时需要特别注意的源码编写规范 除了基本的 Python 编码规范之外,在准备程序进行打包时,还需要特别注意以下几点: 1.1 依赖管理 确保 requirements.t…

2024智能视觉与数据建模国际学术会议(ICIVD 2024)

重要信息 主会官网&#xff1a;www.iccaid.net 大会时间&#xff1a;2024年12月13-15日 大会地点&#xff1a;中国南昌 大会简介 2024智能视觉与数据建模国际学术会议&#xff08;ICIVD 2024&#xff09;作为第四届计算机图形学、人工智能与数据处理国际学术会议&#xff…

Linux磁盘分区

文章目录 磁盘分区 &#x1f3e1;作者主页&#xff1a;点击&#xff01; &#x1f916;Linux专栏&#xff1a;点击 ⏰️创作时间&#xff1a;2024年11月12日13点20分 磁盘分区 MBR 主启动记录分区方案指定了运行BIOS固件的系统上应如何对磁盘进行分区&#xff0c;存在与驱动开…

2. Spring Cloud 微服务基础环境搭建

2. Spring Cloud 微服务基础环境搭建 文章目录 2. Spring Cloud 微服务基础环境搭建前言1. 微服务需求解析2. 具体搭建微服务步骤&#xff1a;2.1 创建父工程 &#xff0c;用于聚合其它微服务模块2.1.1 需求说明/图解2.1.2 具体实现步骤2.1.3 注意事项和具体细节 2.2 创建会员中…

微信朋友圈营销

朋友圈营销4567法则

【赵渝强老师】MySQL InnoDB的表空间

InnoDB存储引擎目前是MySQL默认的存储引擎&#xff0c;它主要由三部分组成&#xff0c;分别是&#xff1a;存储结构、内存结构和线程结构。InnoDB的存储结构又可以分为逻辑存储结构和物理存储结构。InnoDB存储引擎的逻辑存储结构和Oracle大致相同&#xff0c;所有数据都被逻辑地…