day2 单机并发缓存

文章目录

  • 1 sync.Mutex
  • 2 支持并发读写
  • 3 主体结构 Group
    • 3.1 回调 Getter
    • 3.2 Group 的定义
    • 3.3 Group 的 Get 方法
  • 4 测试

本文代码地址: https://gitee.com/lymgoforIT/gee-cache/tree/master/day2-single-node

本文是7天用Go从零实现分布式缓存GeeCache的第二篇。

  • 介绍 sync.Mutex 互斥锁的使用,并实现 LRU 缓存的并发控制。
  • 实现 GeeCache 核心数据结构 Group,缓存不存在时,调用回调函数获取源数据,代码约150

1 sync.Mutex

多个协程(goroutine)同时读写同一个变量,在并发度较高的情况下,会发生冲突。确保一次只有一个协程(goroutine)可以访问该变量以避免冲突,这称之为互斥,互斥锁可以解决这个问题。

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

sync.MutexGo 语言标准库提供的一个互斥锁,当一个协程(goroutine)获得了这个锁的拥有权后,其它请求锁的协程(goroutine) 就会阻塞在 Lock() 方法的调用上,直到调用 Unlock() 锁被释放。

接下来举一个简单的例子,假设有10个并发的协程打印了同一个数字100,为了避免重复打印,实现了printOnce(num int) 函数,使用集合 set 记录已打印过的数字,如果数字已打印过,则不再打印。

var set = make(map[int]bool, 0)func printOnce(num int) {if _, exist := set[num]; !exist {fmt.Println(num)}set[num] = true
}func main() {for i := 0; i < 10; i++ {go printOnce(100)}time.Sleep(time.Second)
}

我们运行 go run . 会发生什么情况呢?

$ go run .
100
100

有时候打印 2 次,有时候打印 4 次,有时候还会触发 panic,因为对同一个数据结构map的访问冲突了。接下来用互斥锁的Lock()Unlock() 方法将冲突的部分包裹起来:

var m sync.Mutex
var set = make(map[int]bool, 0)func printOnce(num int) {m.Lock()if _, exist := set[num]; !exist {fmt.Println(num)}set[num] = truem.Unlock()
}func main() {for i := 0; i < 10; i++ {go printOnce(100)}time.Sleep(time.Second)
}
$ go run .
100

相同的数字只会被打印一次。当一个协程调用了 Lock() 方法时,其他协程被阻塞了,直到Unlock()调用将锁释放。因此被包裹部分的代码就能够避免冲突,实现互斥。

Unlock()释放锁还有另外一种写法:

func printOnce(num int) {m.Lock()defer m.Unlock()if _, exist := set[num]; !exist {fmt.Println(num)}set[num] = true
}

2 支持并发读写

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

day2-single-node/geecache/byteview.go

package geecachetype ByteView struct {b []byte
}// 实现Value接口,从而可以作为lru缓存entry中的value值
func (v ByteView) Len() int {return len(v.b)
}func (v ByteView) ByteSlice() []byte {return cloneBytes(v.b)
}func (v ByteView) String() string {return string(v.b)
}// 用于复制一个切片返回出去,避免外部直接修改了缓存中的[]byte内容
func cloneBytes(b []byte) []byte {c := make([]byte, len(b))copy(c, b)return c
}
  • ByteView 只有一个数据成员,b []byteb 将会存储真实的缓存值。选择 byte 类型是为了能够支持任意的数据类型的存储,例如字符串、图片等。
  • 实现 Len() int 方法,我们在 lru.Cache 的实现中,要求被缓存对象必须实现 Value 接口,即 Len() int 方法,返回其所占的内存大小。
  • b 是只读的,使用 ByteSlice() 方法返回一个拷贝,防止缓存值被外部程序修改。

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

day2-single-node/geecache/cache.go

package geecacheimport ("geecache/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)}// 底层实际办事的还是委托给了lruc.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 方法(底层实际是委托的lru在工作),并添加互斥锁 mu
  • add 方法中,判断了 c.lru 是否为 nil,如果等于 nil 再创建实例。这种方法称之为延迟初始化(Lazy Initialization),一个对象的延迟初始化意味着该对象的创建将会延迟至第一次使用该对象时。主要用于提高性能,并减少程序内存要求。

3 主体结构 Group

GroupGeeCache 最核心的数据结构,负责与用户的交互,并且控制缓存值存储和获取的流程。
在这里插入图片描述

我们将在 geecache.go 中实现主体结构 Group,那么 GeeCache 的代码结构的雏形已经形成了。

geecache/|--lru/|--lru.go  // lru 缓存淘汰策略|--byteview.go // 缓存值的抽象与封装|--cache.go    // 并发控制|--geecache.go // 负责与外部交互,控制缓存存储和获取的主流程

接下来我们将实现流程,远程交互的部分后续再实现。

3.1 回调 Getter

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

day2-single-node/geecache/geecache.go

// 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 方法。
  • 函数类型实现某一个接口,称之为接口型函数,方便使用者在调用时既能够传入函数作为参数,也能够传入实现了该接口的结构体作为参数。

了解接口型函数的使用场景,可以参考 day2加餐 Go 接口型函数的使用场景

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

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")}
}

在这个测试用例中,我们借助 GetterFunc 的类型转换,将一个匿名回调函数转换成了接口 f Getter

调用该接口的方法 f.Get(key string),实际上就是在调用匿名回调函数。

定义一个函数类型 F,并且实现接口 A 的方法,然后在这个方法中调用自己。这是 Go 语言中将其他函数(参数返回值定义与 F 一致)转换为接口 A 的常用技巧。从而方便使用者在调用时既能够传入函数作为参数,也能够传入实现了该接口的结构体作为参数

3.2 Group 的定义

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

day2-single-node/geecache/geecache.go

// A Group is a cache namespace and associated data loaded spread over
type Group struct {name      stringgetter    GettermainCache cache
}var (mu     sync.RWMutex // 控制groups的并发安全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] = 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(),因为不涉及任何冲突变量的写操作。

3.3 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")}// Group主要是与用户交互的,底层实际做事的是mainCache(cache),而cache底层实际做事的是lru.Cacheif v, ok := g.mainCache.get(key); ok {log.Println("[GeeCache] hit")return v, nil}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 方法)

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

4 测试

可以写测试用例,也可以写 main 函数来测试这一章节实现的功能。那我们通过测试用例来看一下,如何使用我们实现的单机并发缓存吧。

首先,用一个 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("[SlowDB] 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,则表示调用了多次回调函数,没有缓存。

测试结果如下:

$ go test -run TestGet
=== RUN   TestGet
2024/07/20 14:33:58 [SlowDB] search key Tom
2024/07/20 14:33:58 [GeeCache] hit
2024/07/20 14:33:58 [SlowDB] search key Jack
2024/07/20 14:33:58 [GeeCache] hit
2024/07/20 14:33:58 [SlowDB] search key Sam
2024/07/20 14:33:58 [GeeCache] hit
2024/07/20 14:33:58 [SlowDB] search key unknown
--- PASS: TestGet (0.00s)
PASS

可以很清晰地看到,缓存为空时,调用了回调函数,第二次访问时,则直接从缓存中读取。

原文地址:https://geektutu.com/post/geecache-day2.html

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

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

相关文章

【LeetCode】80.删除有序数组中的重复项II

1. 题目 2. 分析 3. 代码 class Solution:def removeDuplicates(self, nums: List[int]) -> int:if len(nums) < 3:return len(nums)i 0j 1k 2while(k < len(nums)):if (nums[i] nums[j]):while(k < len(nums) and nums[j] nums[k] ):k1if (k < len(nums…

校验deb、rpm、apt、yum安装文件完整性测试

简介&#xff1a;deb包在Linux操作系统中类似于windows中的软件包&#xff08;msi&#xff09;&#xff0c;几乎不需要什么复杂的编译即可通过鼠标点击安装使用。此外,deb广泛应用于越狱后iOS软件及MeeGo&#xff08;含Maemo软件&#xff09;中。deb 格式是 Debian 系统(包含 D…

StringBuilder和StringBuffer

目录 &#xff08;一&#xff09;为什么要引入StringBuilder和StringBuffer &#xff08;二&#xff09;StringBuilder和StringBuffer &#xff08;1&#xff09;底层数组长度 (2)StringBuilder与StringBuffer扩容机制 &#xff08;3&#xff09;StringBuilder和StringBuf…

并发编程面试题1

并发编程 1、线程池中提交一个任务的流程是怎样的&#xff1f; 1、提交任务&#xff1a;首先&#xff0c;一个任务被提交到线程池。这个任务通常是一个实现了Runnable或Callable接口的对象&#xff1b; 2、检测线程池状态&#xff1a;线程池会首先检测其运行状态。如果线程池…

javafx使用发现的问题

1.按钮的方法 如果在fxml按钮的方法报错&#xff0c;并且你已在lei中添加了它的按钮及其按钮方法&#xff0c;那么可能是FXML和控制器类未正确关联&#xff1a; 确保你的FXML文件通过 fx:controller 属性正确指定了与之关联的控制器类。例如&#xff0c;fx:controller"c…

数据库之存储引擎

目录 一、MySQL支持的存储引擎 二、查看MySQL默认存储引擎 三、修改MySQL默认存储引擎 四、常用的存储引擎 1.InnoDB 2.MyISAM 3.MEMORY 一、MySQL支持的存储引擎 使用SHOW ENGINES \G; 命令查看 以“\G”结尾&#xff0c;其作用是将查询结果按列显示。 Engine&#xff…

更加深入Mysql-04-MySQL 多表查询与事务的操作

文章目录 多表查询内连接隐式内连接显示内连接 外连接左外连接右外连接 子查询 事务事务隔离级别 多表查询 有时我们不仅需要一个表的数据&#xff0c;数据可能关联到俩个表或者三个表&#xff0c;这时我们就要进行夺标查询了。 数据准备&#xff1a; 创建一个部门表并且插入…

Fiddler 导出请求为curl格式

来自:https://www.cnblogs.com/yudongdong/p/15418181.html Fiddler 下载地址: https://downloads.getfiddler.com/fiddler-classic/FiddlerSetup.5.0.20243.10853-latest.exe 这段代码加到类中 public static RulesOption("关闭请求体转代码", "生成代码&qu…

达梦数据库系列—29. DTS迁移ORACLE到DM

目录 1.ORACLE源端信息 2.DM目的端信息 3.DTS 迁移评估 4.数据库迁移 4.1 Oracle 源端数据库准备 4.2 目的端达梦数据库准备 初始化参数设置 兼容性参数设置 表空间规划 用户规划 创建迁移用户和表空间 4.3迁移步骤 创建迁移 配置数据源 配置迁移对象及策略 开…

django-vue-admin项目运行

文本主要对django-vue-admin项目进行了简要介绍&#xff0c;并且对前后端进行了源码安装和运行。在此基础上可作为管理系统二次开发的基础框架。 一.django-vue-admin简介和安装 1.简介 django-vue-admin项目是基于RBAC模型权限控制的中小型应用的基础开发平台&#xff0c;采…

昇思MindSpore学习总结十六 —— 基于MindSpore的GPT2文本摘要

1、mindnlp 版本要求 !pip install tokenizers0.15.0 -i https://pypi.tuna.tsinghua.edu.cn/simple # 该案例在 mindnlp 0.3.1 版本完成适配&#xff0c;如果发现案例跑不通&#xff0c;可以指定mindnlp版本&#xff0c;执行!pip install mindnlp0.3.1 !pip install mindnlp …

使用Amazon Web Services Lambda把天气预报推送到微信

最近北京开始下雨&#xff0c;开始和同事打赌几点能够雨停&#xff0c;虽然Iphone已经提供了实时天气&#xff0c;但是还是想用国内的API试试看看是不是更加准确些。 以下是我使用的服务&#xff1a; 地图SDK/APP获取 经纬度彩云天气API 通过地理位置获取天气信息Lambda 作为…

关于Mysql的面试题(实时更新中~)

一、主键约束与“not null unique”区别 1、作为Primary Key的域/域组不能为null&#xff0c;而Unique Key可以。 2、在一个表中只能有一个Primary Key&#xff0c;而多个Unique Key可以同时存在。unique not null 可以 将表的一列或多列定义为唯一性属性&#xff0c;而prima…

buu做题(6)

目录 [GWCTF 2019]我有一个数据库 [WUSTCTF2020]朴实无华 [GWCTF 2019]我有一个数据库 什么都没有, 尝试用dirsearch扫一下目录 可以扫到一个 /phpmyadmin 可以直接进入到数据库里面 但里面没什么东西 可以看到它的版本不是最新的, 搜一下相关的漏洞 phpMyAdmin 4.8.1后台文…

go关于string与[]byte再学深一点

目标&#xff1a;充分理解string与[]bytes零拷贝转换的实现 先回顾下string与[]byte的基本知识 1. string与[]byte的数据结构 reflect包中关于字符串的数据结构 // StringHeader is the runtime representation of a string.type StringHeader struct {Data uintptrLen int} …

ClickHouse 入门(一)【基本特点、数据类型与表引擎】

前言 今天开始学习 ClickHouse &#xff0c;一种 OLAP 数据库&#xff0c;实时数仓中用到的比较多&#xff1b; 1、ClickHouse 入门 ClickHouse 是俄罗斯的 Yandex&#xff08;搜索引擎公司&#xff09;在 2016 年开源的列式存储数据库&#xff08;HBase 也是列式存储&#xf…

某宝同款度盘不限速后台系统源码

简介&#xff1a; 某宝同款度盘不限速后台系统源码&#xff0c;验证已被我去除&#xff0c;两个后端系统&#xff0c;账号和卡密系统 第一步安装宝塔&#xff0c;部署卡密系统&#xff0c;需要环境php7.4 把源码丢进去&#xff0c;设置php7.4&#xff0c;和伪静态为thinkphp…

山东济南十大杰出人物起名大师颜廷利:影响世界的思想家哲学家教育家

在宇宙的广袤舞台上&#xff0c;各类智者以他们独特的方式揭示着世界的奥秘。数学家们在无尽的符号与公式中穿梭&#xff0c;像探索者般解锁着自然界的深层逻辑。考古学家们则跋涉于古老的土地&#xff0c;用他们的双手拂去岁月的尘埃&#xff0c;让沉睡的历史重见天日。 二十一…

spss是什么软件?spss有什么用

spss是什么软件&#xff1f; SPSS是一款数据统计、分析软件&#xff0c;它由IBM公司出品&#xff0c;这款软件平台提供了文本分析、大量的机器学习算法、数据分析模型、高级统计分析功能等&#xff0c;软件易学且功能非常强大&#xff0c;可以使用SPSS制作图表&#xff0c;例如…

汽车免拆诊断案例 | 2017 款林肯大陆车发动机偶尔无法起动

故障现象 一辆2017款林肯大陆车&#xff0c;搭载2.0T发动机&#xff0c;累计行驶里程约为7.5万km。车主进厂反映&#xff0c;有时按下起动按钮&#xff0c;起动机不工作&#xff0c;发动机无法起动&#xff0c;组合仪表点亮正常&#xff1b;多次按下起动按钮&#xff0c;发动机…