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.Mutex
是 Go
语言标准库提供的一个互斥锁,当一个协程(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 []byte
,b
将会存储真实的缓存值。选择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
Group
是 GeeCache
最核心的数据结构,负责与用户的交互,并且控制缓存值存储和获取的流程。
我们将在 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
相关文章:

day2 单机并发缓存
文章目录 1 sync.Mutex2 支持并发读写3 主体结构 Group3.1 回调 Getter3.2 Group 的定义3.3 Group 的 Get 方法 4 测试 本文代码地址: https://gitee.com/lymgoforIT/gee-cache/tree/master/day2-single-node 本文是7天用Go从零实现分布式缓存GeeCache的第二篇。 …...

ECMP等价多路由机制,大模型训练负载均衡流量极化冲突原因,万卡(大规模)集群语言模型(LLM)训练流量拥塞特点
大规模集群,大语言模型(LLM)训练流量特点,ECMP(Equal-Cost Multi-Path Routing)流量极化拥塞原因。 视频分享在这: 2.1 ECMP等价多路由,大模型训练流量特点,拥塞冲突极化产生原因_哔哩哔哩_bi…...

Linux 注意事项
Linux 与 Windows 是两个相互独立的操作系统,两者有较大差距: 1.1 Linux 严格区分大小写(Windows不严格区分大小写); 1.2 Linux 中所有内容,硬件设备都以文件形式保存在 /dev 目录下(万物皆文件…...

力扣SQL50 指定日期的产品价格 双重子查询 coalesce
Problem: 1164. 指定日期的产品价格 coalesce 的使用 简洁版 👨🏫 参考题解 select distinct p1.product_id,coalesce((select p2.new_pricefrom Products p2where p2.product_id p1.product_id and p2.change_date < 2019-08-16order by p2.…...
MySQL8的备份方案——全量(完全)备份(CentOS)
MySQL8的全量备份 一、安装备份工具二、备份数据三、恢复备份 点击跳转增量备份 点击跳转差异备份 点击跳转压缩备份 一、安装备份工具 官网 下载地址 备份所用工具为percona-xtrabackup 如果下方安装工具的教程失效,请点击上方下载地址转到官方文档查看 下载该工…...

JVM监控及诊断工具-命令行篇--jcmd命令介绍
JVM监控及诊断工具-命令行篇5-jcmd:多功能命令行 一 基本情况二 基本语法jcmd -ljcmd pid helpjcmd pid 具体命令 一 基本情况 在JDK 1.7以后,新增了一个命令行工具jcmd。它是一个多功能的工具,可以用来实现前面除了jstat之外所有命令的功能…...

c++信号和槽机制的轻量级实现,sigslot 库介绍及使用
Qt中的信号与槽机制很好用,然而只在Qt环境中。在现代 C 编程中,对象间的通信是一个核心问题。为了解决这个问题,许多库提供了信号和槽(Signals and Slots)机制。今天推荐分享一个轻量级的实现:sigslot 库。…...
云原生项目纪事系列 - 项目管理的鲜活事例
大规模云原生系统的新颖性、建设性和挑战性,吸引着许多有数学思想、哲学意识和美学观念的系统架构师,老模也是其中一员。 老模即是文史家庭出身,又有理工学业背景,他基于平时记录的翔实细节,秉持客观原则,使…...

【Vite】快速入门及其配置
概述 Vite是前端构建工具。vite 相较于webpack,vite采用了不同的运行方式: 开发时,并不对代码打包,而是直接采用ESM的方式来运行项目在项目打包部署时,使用 rollup 对项目进行打包除了速度外,vite使用起来也更加方便…...

Armv8/Armv9架构的学习大纲-学习方法-自学路线-付费学习路线
本文给大家列出了Arm架构的学习大纲、学习方法、自学路线、付费学习路线。有兴趣的可以关注,希望对您有帮助。 如果大家有需要的,欢迎关注我的CSDN课程:https://edu.csdn.net/lecturer/6964 ARM 64位架构介绍 ARM 64位架构介绍 ARM架构概况…...
vue 中 ui 组件二次封装后 ref 怎么穿透到子组件里
情景:element-ui 二次封装了 el-table 组件,使用封装组件时,想要调用 el-table 组件内置的一些方法。只在封装组件上定义 ref 是拿不到 el-table 内置方法的。解决方法如下。 1. vue2 封装组件 <template><el-table ref"inn…...

sourcetree中常用功能使用方法及gitlab冲突解决
添加至缓存:等于git add 提交:等于git commit 拉取/获取:等于git pull ,在每次要新增代码或者提交代码前需要先拉取一遍服务器中最新的代码,防止服务器有其他人更新了代码,但我们自己本地的代码在我们更新前跟服务器不…...
SQL Server分布式查询:跨数据库的无缝数据探索
SQL Server分布式查询:跨数据库的无缝数据探索 在当今的企业环境中,数据往往分散在不同的数据库和服务器上。SQL Server的分布式查询功能提供了一种强大的手段,允许用户编写单一的查询来访问和操作分散在不同SQL Server实例中的数据。本文将…...

【字少图多剖析微服务】深入理解Eureka核心原理
深入理解Eureka核心原理 Eureka整体设计Eureka服务端启动Eureka三级缓存Eureka客户端启动 Eureka整体设计 Eureka是一个经典的注册中心,通过http接收客户端的服务发现和服务注册请求,使用内存注册表保存客户端注册上来的实例信息。 Eureka服务端接收的…...
如何在 Linux 中解压 ZIP 文件
ZIP 是一种常用的压缩文件格式,用于存储和传输多个文件。在 Linux 系统中,解压 ZIP 文件非常简单。 使用 unzip 命令 unzip 是一个专用于解压 ZIP 文件的命令行工具。要使用它,请打开终端并输入以下命令: 例如,要解…...
IDEA的APIPost接口测试插件详解
APIPOST官方网址 一、安装APIPost插件 打开IntelliJ IDEA: 启动您的IntelliJ IDEA开发环境。 导航到插件设置: 在Windows或Linux上,点击 File > Settings。在macOS上,点击 IntelliJ IDEA > Preferences。 搜索并安装APIPo…...

[经验] 驰这个汉字的拼音是什么 #学习方法#其他#媒体
驰这个汉字的拼音是什么 驰,是一个常见的汉字,其拼音为“ch”,音调为第四声。它既可以表示动词,也可以表示形容词或副词,意义广泛,经常出现在生活和工作中。下面就让我们一起来了解一下“驰”的含义和用法。…...

生成式人工智能落地校园与课堂的15个场景
生成式人工智能正在重塑教育行业,为传统教学模式带来了革命性的变化。随着AI的不断演进,更多令人兴奋的应用场景将逐一显现,为学生提供更加丰富和多元的学习体验。 尽管AI在教学中的应用越来越广泛,但教师们也不必担心会被完全替代…...
C# 中的事件
1.事件的概念 在C#中,事件是一种特殊的委托类型,用于在对象之间提供一种基于观察者模式的通知机制。事件的发送方定义了一个委托,委托类型的声明包含了事件的签名,即事件处理器方法的签名。事件的订阅者可以通过运算符来注册事件…...

一、单例模式
文章目录 1 基本介绍2 实现方式2.1 饿汉式2.1.1 代码2.1.2 特性 2.2 懒汉式 ( 线程不安全 )2.2.1 代码2.2.2 特性 2.3 懒汉式 ( 线程安全 )2.3.1 代码2.3.2 特性 2.4 双重检查2.4.1 代码2.4.2 特性 2.5 静态内部类2.5.1 代码2.5.2 特性 2.6 枚举2.6.1 代码2.6.2 特性 3 实现的要…...
云原生核心技术 (7/12): K8s 核心概念白话解读(上):Pod 和 Deployment 究竟是什么?
大家好,欢迎来到《云原生核心技术》系列的第七篇! 在上一篇,我们成功地使用 Minikube 或 kind 在自己的电脑上搭建起了一个迷你但功能完备的 Kubernetes 集群。现在,我们就像一个拥有了一块崭新数字土地的农场主,是时…...
ubuntu搭建nfs服务centos挂载访问
在Ubuntu上设置NFS服务器 在Ubuntu上,你可以使用apt包管理器来安装NFS服务器。打开终端并运行: sudo apt update sudo apt install nfs-kernel-server创建共享目录 创建一个目录用于共享,例如/shared: sudo mkdir /shared sud…...

【OSG学习笔记】Day 18: 碰撞检测与物理交互
物理引擎(Physics Engine) 物理引擎 是一种通过计算机模拟物理规律(如力学、碰撞、重力、流体动力学等)的软件工具或库。 它的核心目标是在虚拟环境中逼真地模拟物体的运动和交互,广泛应用于 游戏开发、动画制作、虚…...
反向工程与模型迁移:打造未来商品详情API的可持续创新体系
在电商行业蓬勃发展的当下,商品详情API作为连接电商平台与开发者、商家及用户的关键纽带,其重要性日益凸显。传统商品详情API主要聚焦于商品基本信息(如名称、价格、库存等)的获取与展示,已难以满足市场对个性化、智能…...

前端导出带有合并单元格的列表
// 导出async function exportExcel(fileName "共识调整.xlsx") {// 所有数据const exportData await getAllMainData();// 表头内容let fitstTitleList [];const secondTitleList [];allColumns.value.forEach(column > {if (!column.children) {fitstTitleL…...

uniapp微信小程序视频实时流+pc端预览方案
方案类型技术实现是否免费优点缺点适用场景延迟范围开发复杂度WebSocket图片帧定时拍照Base64传输✅ 完全免费无需服务器 纯前端实现高延迟高流量 帧率极低个人demo测试 超低频监控500ms-2s⭐⭐RTMP推流TRTC/即构SDK推流❌ 付费方案 (部分有免费额度&#x…...

Linux-07 ubuntu 的 chrome 启动不了
文章目录 问题原因解决步骤一、卸载旧版chrome二、重新安装chorme三、启动不了,报错如下四、启动不了,解决如下 总结 问题原因 在应用中可以看到chrome,但是打不开(说明:原来的ubuntu系统出问题了,这个是备用的硬盘&a…...
WEB3全栈开发——面试专业技能点P2智能合约开发(Solidity)
一、Solidity合约开发 下面是 Solidity 合约开发 的概念、代码示例及讲解,适合用作学习或写简历项目背景说明。 🧠 一、概念简介:Solidity 合约开发 Solidity 是一种专门为 以太坊(Ethereum)平台编写智能合约的高级编…...

Java面试专项一-准备篇
一、企业简历筛选规则 一般企业的简历筛选流程:首先由HR先筛选一部分简历后,在将简历给到对应的项目负责人后再进行下一步的操作。 HR如何筛选简历 例如:Boss直聘(招聘方平台) 直接按照条件进行筛选 例如:…...

C++ Visual Studio 2017厂商给的源码没有.sln文件 易兆微芯片下载工具加开机动画下载。
1.先用Visual Studio 2017打开Yichip YC31xx loader.vcxproj,再用Visual Studio 2022打开。再保侟就有.sln文件了。 易兆微芯片下载工具加开机动画下载 ExtraDownloadFile1Info.\logo.bin|0|0|10D2000|0 MFC应用兼容CMD 在BOOL CYichipYC31xxloaderDlg::OnIni…...