golang分布式缓存项目 Day2

news2024/11/25 0:29:30

:该项目原作者: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 project

import (
	"goLang/project/lru"
	"sync"
)

type cache struct {
	mu         sync.Mutex
	lru        *lru.Cache
	cacheBytes 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      string
	getter    Getter
	mainCache cache
}

var (
	mu     sync.RWMutex
	groups = 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] = g
	return 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] += 1
				return []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 function
		if _, 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.coloradmin.cn/o/2238075.html

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

相关文章

abap 可配置通用报表字段级日志监控

文章目录 1.功能需求描述1.1 功能1.2 效果展示2.数据库表解释2.1 表介绍3.数据库表及字段3.1.应用日志数据库抬头表:ZLOG_TAB_H3.2.应用日志数据库明细表:ZLOG_TAB_P3.3.应用日志维护字段配置表:ZLOG_TAB_F4.日志封装类5.代码6.调用方式代码7.调用案例程序demo1.功能需求描述 …

材质(三)——材质参数集和材质函数

a.之前是针对材质在材质蓝图里面 类似 于静态更改的方法&#xff0c; b.材质参数集 &#xff0c;对外开放参数&#xff0c;可以手动更改&#xff0c;已然是一种封闭的静态更改方法 c.那么材质函数&#xff0c;将参数集对外开放&#xff0c;可以在关卡蓝图 通过程序 算法 去动…

随机采样之接受拒绝采样

之前提到的逆变换采样&#xff08;Inverse Transform Sampling&#xff09;是一种生成随机样本的方法&#xff0c;它利用累积分布函数&#xff08;CDF&#xff09;的逆函数来生成具有特定分布的随机变量。以下是逆变换采样的缺点&#xff1a; 计算复杂性&#xff1a;对于某些分…

软件设计师:排序算法总结

一、直接插入 排序方式&#xff1a;从第一个数开始&#xff0c;拿两个数比较&#xff0c;把后面一位跟前面的数比较&#xff0c;把较小的数放在前面一位 二、希尔 排序方式&#xff1a;按“增量序列&#xff08;步长&#xff09;”分组比较&#xff0c;组内元素比较交换 假设…

信息安全工程师(78)网络安全应急响应技术与常见工具

前言 网络安全应急响应是指为应对网络安全事件&#xff0c;相关人员或组织机构对网络安全事件进行监测、预警、分析、响应和恢复等工作。 一、网络安全应急响应技术 网络安全应急响应组织 构成&#xff1a;网络安全应急响应组织主要由应急领导组和应急技术支撑组构成。领导组负…

Kafka 的一些问题,夺命15连问

kafka-中的组成员 kafka四大核心 生产者API 允许应用程序发布记录流至一个或者多个kafka的主题&#xff08;topics&#xff09;。 消费者API 允许应用程序订阅一个或者多个主题&#xff0c;并处理这些主题接收到的记录流 StreamsAPI 允许应用程序充当流处理器&#xff08;s…

精选5款小程序设计工具,助力设计之路璀璨前行

在当今数字化浪潮中&#xff0c;小程序的重要性日益凸显&#xff0c;无论是电商、社交还是服务领域&#xff0c;小程序都成为连接用户与品牌的关键桥梁。而一款优秀的小程序离不开精心的设计&#xff0c;以下 5 款小程序设计工具将成为你设计事业的得力助手。 一、即时设计 即…

亚马逊评论爬虫+数据分析

爬取评论 做分析首先得有数据&#xff0c;数据是核心&#xff0c;而且要准确&#xff01; 1、爬虫必要步骤&#xff0c;选好框架 2、开发所需数据 3、最后测试流程 这里我所选框架是seleniumrequest&#xff0c;很多人觉得selenium慢&#xff0c;确实不快&#xff0c;仅针对此…

量子计算及其在密码学中的应用

&#x1f493; 博客主页&#xff1a;瑕疵的CSDN主页 &#x1f4dd; Gitee主页&#xff1a;瑕疵的gitee主页 ⏩ 文章专栏&#xff1a;《热点资讯》 量子计算及其在密码学中的应用 量子计算及其在密码学中的应用 量子计算及其在密码学中的应用 引言 量子计算概述 定义与原理 发展…

论文笔记:no pose,no problem-基于dust3r输出GS参数实现unpose稀疏重建

1.摘要 我们引入了 NoPoSplat&#xff0c;这是一种前馈模型&#xff0c;能够从未设置的稀疏多视图图像中重建由 3D 高斯参数化的 3D 场景。 我们的模型专门使用光度损失进行训练&#xff0c;在推理过程中实现了实时 3D 高斯重建。 为了消除重建过程中对准确pose的需要&#xff…

godot--自定义边框/选中时样式 StyleBoxTexture

前提知识&#xff1a; stylebox就像一个贴图&#xff0c;把图案贴到控件是。多个stylebox同时生效的话&#xff0c;那当然也有层级之分&#xff0c;上层覆盖下层&#xff08;可以设置透明度来显示下层&#xff09; 关于主题的概念&#xff1a; godot——主题、Theme、StyleB…

ReactPress 安装指南:从 MySQL 安装到项目启动

ReactPress Github项目地址&#xff1a;https://github.com/fecommunity/reactpress 欢迎Star。 ReactPress 是一个基于 React 的开源发布平台&#xff0c;适用于搭建博客、网站或内容管理系统&#xff08;CMS&#xff09;。本文将详细介绍如何安装 ReactPress&#xff0c;包括…

BMC运维管理:IPMI实现服务器远控制

IPMI实现服务器远控制 实操一、使用IPMI重置BMC用户密码实操二、使用IPMI配置BMC的静态IP实操三、IPMI实现BMC和主机控制操作实操四、ipmitool查看服务器基本信息实操五、ipmitool实现问题定位BMC(Baseboard Management Controller,基板管理控制器)是服务器硬件的一个独立管…

手机上用什么方法可以切换ip

手机上用什么方法可以切换IP&#xff1f;在某些特定情境下&#xff0c;用户可能需要切换手机的IP地址&#xff0c;以满足网络安全、隐私保护或绕过地域限制等需求。下面以华为手机为例&#xff0c;将详细介绍手机IP地址切换的几种方法&#xff0c;帮助用户轻松实现这一目标。 一…

鸿蒙多线程开发——并发模型对比(Actor与内存共享)

1、概 述 并发是指在同一时间段内&#xff0c;能够处理多个任务的能力。为了提升应用的响应速度与帧率&#xff0c;以及防止耗时任务对主线程的干扰&#xff0c;HarmonyOS系统提供了异步并发和多线程并发两种处理策略。 异步并发&#xff1a;指异步代码在执行到一定程度后会被…

【NLP】使用 PyTorch 从头构建自己的大型语言模型 (LLM)

读完这篇文章后&#xff0c;你会取得什么成就&#xff1f;你将能够自己构建和训练大型语言模型 (LLM)&#xff0c;同时与我一起编写代码。虽然我们正在构建一个将任何给定文本从英语翻译成马来语的 LLM&#xff0c;但你可以轻松地修改此 LLM 架构以用于其他语言翻译任务。 LLM…

css:还是语法

emmet的使用 emmet是一个插件&#xff0c;Emmet 是 Zen Coding 的升级版&#xff0c;由 Zen Coding 的原作者进行开发&#xff0c;可以快速的编写 HTML、CSS 以及实现其他的功能。很多文本编辑器都支持&#xff0c;我们只是学会使用它&#xff1a; 生成html结构 <!-- emme…

YOLO即插即用---PConv

Run, Don’t Walk: Chasing Higher FLOPS for Faster Neural Networks 论文地址&#xff1a; 1. 论文解决的问题 2. 解决问题的方法 3. PConv 的适用范围 4. PConv 在目标检测中的应用 5. 评估方法 6. 潜在挑战 7. 未来研究方向 8.即插即用代码 论文地址&#xff1a; …

小白NAS磁盘规划实践:一次科学、高效的存储旅程

引言 如今,数字化生活正逐步渗透到我们生活的方方面面。从家庭影音到工作文件,从珍贵的照片到大型游戏库,数据的存储需求日益增加。许多朋友开始关注NAS(网络附加存储)设备。作为一个专为数据存储和管理设计的系统,NAS能为我们提供安全、高效的存储方案。但如何科学地规…

ADC前端控制与处理模块--AD7606_Module

总体框架 AD7606_Module主要由3个模块组成组成&#xff0c;AD7606_Data_Pkt和AD7606_Drive以及AD7606_ctrl。 1.AD7606_Data_Pkt主要作用是把AD芯片数据组好数据包&#xff0c;然后发送给上位机&#xff1b; 2.AD7606_Drive主要负责和芯片的交互部分 3.AD7606_ctrl控制模块的作…