Go分布式缓存 单机并发缓存(day2)

2023/11/30 9:40:08

Go分布式缓存 单机并发缓存(day2)

1 支持并发读写

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

day2-single-node/geecache/byteview.go - github

package geecache

// 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 添加并发特性了。

day2-single-node/geecache/cache.go - github

package geecache

import (
	"geecache/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),一个对象的延迟初始化意味着该对象的创建将会延迟至第一次使用该对象时。主要用于提高性能,并减少程序内存要求。

2 主体结构 Group

Group 是 GeeCache 最核心的数据结构,负责与用户的交互,并且控制缓存值存储和获取的流程。

                            是
接收 key --> 检查是否被缓存 -----> 返回缓存值 ⑴
                |  否                         是
                |-----> 是否应当从远程节点获取 -----> 与远程节点交互 --> 返回缓存值 ⑵
                            ||-----> 调用`回调函数`,获取值并添加到缓存 --> 返回缓存值 ⑶

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

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

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

2.1 回调 Getter

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

day2-single-node/geecache/geecache.go - github

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

了解接口型函数的使用场景,可以参考 Go 接口型函数的使用场景 - 7days-golang Q & A

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

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 的常用技巧。

2.2 Group 的定义

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

day2-single-node/geecache/geecache.go - github

// 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(),因为不涉及任何冲突变量的写操作。

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

	if 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 方法)

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

3 测试

可以写测试用例,也可以写 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] += 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,则表示调用了多次回调函数,没有缓存。

测试结果如下:

$ go test -run TestGet
2020/02/11 22:07:31 [SlowDB] search key Sam
2020/02/11 22:07:31 [GeeCache] hit
2020/02/11 22:07:31 [SlowDB] search key Tom
2020/02/11 22:07:31 [GeeCache] hit
2020/02/11 22:07:31 [SlowDB] search key Jack
2020/02/11 22:07:31 [GeeCache] hit
2020/02/11 22:07:31 [SlowDB] search key unknown
PASS
ok      geecache        0.008s

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


4 总结

支持并发通过互斥锁实现。

通过Group的回调函数进行实现若缓存不击中,从远程数据库中读取。


http://www.jnnr.cn/a/129728.html

相关文章

vue实战中的一些小技巧

能让你首次加载更快的路由懒加载&#xff0c;怎么能忘&#xff1f; 路由懒加载可以让我们的包不需要一次把所有的页面的加载进来&#xff0c;只加载当前页面的路由组件就行。 举个&#x1f330;&#xff0c;如果这样写&#xff0c;加载的时候会全部都加载进来。 const route…

智慧公厕擦手纸洗手液余量实时在线统计

由于公共厕所的建设一般都是公益性的&#xff0c;免费供市民使用&#xff0c;所以大部分使用者在资源节约这一块还是很缺乏自觉性的。尤其是擦手纸、厕纸、以及洗手液等这些公共消耗物品的使用上毫无顾忌&#xff0c;造成了不必要的铺张浪费。讯鹏科技智慧公厕资源消耗余量监测…

尚医通_第13章_手机验证码登录

尚医通_第13章_手机验证码登录 文章目录尚医通_第13章_手机验证码登录第1节、手机验证码登录&#xff08;需求和登录接口&#xff09;一、登录需求分析二、搭建service-user模块1、在service下创建service_user模块3、创建数据库和表4、创建启动类5、配置GateWay网关三、登录接…

MySQL十秒插入百万条数据

mysql数据库准备 private String Driver "com.mysql.cj.jdbc.Driver";private String url "jdbc:mysql://localhost:3306/mp?serverTimezoneAsia/Shanghai&rewriteBatchedStatementstrue";private String user "root";private String pa…

【Linux】基础IO(万字详解) —— 系统文件IO | 文件描述符fd | 重定向原理

&#x1f308;欢迎来到Linux专栏~~基础IO (꒪ꇴ꒪(꒪ꇴ꒪ )&#x1f423;,我是Scort目前状态&#xff1a;大三非科班啃C中&#x1f30d;博客主页&#xff1a;张小姐的猫~江湖背景快上车&#x1f698;&#xff0c;握好方向盘跟我有一起打天下嘞&#xff01;送给自己的一句鸡汤&a…

【C++】初识STL

文章目录STL简介1.1 什么是STL1.2 STL的诞生1.3 STL的版本1.4 STL六大组件1.5 STL中容器、算法、迭代器1.6 STL的缺陷STL简介 1.1 什么是STL STL&#xff1a;是C标准库的重要组成部分&#xff0c;不仅是一个可复用的组件库&#xff0c;而且是一个包罗数据结构与算法的软件框架…

LCT 的基本操作

一 点睛 LCT 有 7 种基本操作 access(x) makeroot(x) findroot(x) split(x , y) link(x , y) cut(x , y) isroot(x) 二 access(x) 1 定义 access(x) 是动态树所有操作的基础&#xff0c;用于打通 x 到原树根节点的一条实链。 2 图解 a 原树变化 access(L) 指将 …

Python画爱心——谁能拒绝用代码敲出会跳动的爱心呢~

还不快把这份浪漫拿走&#xff01;&#xff01;节日就快到来了&#xff0c;给Ta一个惊喜吧~ 今天给大家分享一个浪漫小技巧&#xff0c;利用Python中的 HTML 制作一个立体会动的心动小爱心 成千上百个爱心汇成一个大爱心&#xff0c;从里到外形成一个立体状&#xff0c;给人视…

vs code添加C51关键字及C51头文件

文章目录1、vs code准备设置2、keil新建一个工程3、用 vs code进行单片机编程4、对vs code一些语句的报错处理vs code是一个轻量级的代码编辑器&#xff0c;因为单片机使用的是C51编程&#xff0c;基于C语言。使用vs code编程就会出现C51的关键字报错&#xff0c;这篇文章是我仅…

Mybatis整合达梦数据库

陈老老老板&#x1f9b8;&#x1f468;‍&#x1f4bb;本文专栏&#xff1a;国产数据库-达梦数据库&#xff08;主要讲一些达梦数据库相关的内容&#xff09;&#x1f468;‍&#x1f4bb;本文简述&#xff1a;本文讲一下SpringBoot整合Mybatis与达梦数据库&#xff0c;就是简单…

软件缺陷的定义

软件缺陷是指存在于软件&#xff08;程序、数据、文档&#xff09;中的那些不符合用户需求的问题。 软件未达到需求规格说明书表明的功能软件出现了需求规格说明书指明不会出现的错误软件的功能超出了需求规格说明书指明的范围软件未达到需求规格说明书虽未指明而应该达到的目…

时序分析 47 -- 时序数据转为空间数据 (六) 马尔可夫转换场 python 实践(中)

时序分析 47 时序数据转为空间数据 (六) 马尔可夫转换场 python 实践&#xff08;中&#xff09; …接上 Step 6. MTF聚合压缩 正如理论部分所讨论的&#xff0c;我们为了可视化效果或者计算效率经常需要对MTF图像进行聚合压缩。详细信息请参见理论部分马尔可夫转换场。 i…

Spring Cloud框架(原生Hoxton版本与Spring Cloud Alibaba)初级篇 ---- 服务注册与发现

目录一、Eureka服务注册与发现Eureka基础服务治理服务注册Eureka组件单继Eureka构建IDEA生成EurekaServer端服务注册中心将EurekaClient端8001注册进EurekaServer成为服务提供者provider将EurekaClient端80注册进EurekaServer成为服务消费者consumer集群Eureka构建构建EurekaSe…

Rsync分布式应用

一.rsync ( Rcmotc sync&#xff0c;远程同步&#xff09; 1.是一个开源的快速备份工具&#xff0c;可以在不同主机之间镜像同步整个H录树&#xff0c;支持增量备份&#xff0c;并保持链接和权限&#xff0c;且采用优化的同步算法&#xff0c;传输前执行压缩&#xff0c;因此非…

Selenium基础 — Selenium中的expected_conditions模块(二)

3、expected_conditions模块独立使用 我们练习expected_conditions模块中两个功能&#xff0c;其他功能参照即可。 # 1.导入selenium from selenium import webdriver from time import sleep from selenium.webdriver.support import expected_conditions as EC# 2.打开Chro…

辽宁2022农民丰收节 国稻种芯:4个主会场31个分会场同步

辽宁2022农民丰收节 国稻种芯&#xff1a;4个主会场31个分会场同步 农民日报中国农网记者于险峰 新闻中国采编网 中国新闻采编网 谋定研究中国智库网 中国农民丰收节国际贸易促进会 国稻种芯中国水稻节 中国三农智库网-功能性农业农业大健康大会报道&#xff1a;金秋好时节&am…

【Java】JDBC编程实现对数据库表的增删改查操作

目录 一、准备工作 二、准备数据 代码 三、存放MySQL驱动jar包 四、编程步骤 五、代码实现 1.增 代码 执行结果 ​2.改 代码 执行结果 3.查 代码 执行结果 4.删 代码 执行结果 一、准备工作 下载MySQL驱动jar包&#xff0c;资源直达&#xff1a;http://t.c…

JavaEE——Http请求和响应

请求 报头 里面是一系列键值对&#xff0c;有的是标准定义的&#xff0c;有的是自定义的 典型的有以下几个 Host 代表服务器的主机地址和端口 也就是当我们访问浏览器时&#xff0c;可以知道从哪里获取数据 端口号如果省略就代表是默认值&#xff0c;http是80&#xff0c;h…

为什么 Spring 的构造器注入不需要 @Autowired 注解?

Spring的三种注入方式 在讨论这个问题之前&#xff0c;我们可以先来回忆一下Spring的依赖注入的三种方式。 分别是——属性注入、setter 注入、构造器注入 一、属性注入 这种方式是最常用的&#xff0c;我们可以使用 Autowired 或者是 Resource 进行注入 RestController Re…

elementUI select组件value注意事项(value失效问题)

第一:提出问题的前提场景: form表单初始数据state是 数值 情景1: select组件v-model state 如果value是 :value的情况下 组件正常显示: 情景2: value 不是:value 组件显示为: form表单初始数据state是 字符串 情景1 : value 是value’ ’ 组件正常显示: 情景2: valu…
最新文章