Go缓存系统

news2024/9/19 9:56:22

1.缓存

缓存(Caching),用于提高数据访问速度和系统性能。它通过在快速的存储介质中保存数据的副本,使得数据可以被更快地检索,而不是每次都从较慢的原始数据源(如数据库或磁盘)中获取。缓存通常用于减少数据检索时间、降低系统负载、提高响应速度和改善用户体验。

缓存的工作原理基于这样一个事实:某些数据被频繁访问,而这些数据的变化频率相对较低。因此,将这些数据保存在快速访问的缓存中,可以减少对原始数据源的访问次数,从而提高整体性能。

不管是web应用,还是游戏应用,缓存都是非常重要的技术。

2.java使用缓存

java发展多年,生态已经非常完善。如果使用springboot全家桶,可以直接使用springcache,通过注解使用缓存功能。需要注意的是,springcache是一种声明式解决方案,本身只提供操作缓存的相关接口,至于缓存实现,可根据需要选择redis或者caffine等。

2.2.引入sprigcache

在Spring中,可以通过Spring Cache来使用缓存。下面是使用Spring Cache的一般步骤:

添加依赖:在项目的构建文件(如pom.xml)中添加Spring Cache的相关依赖。

配置缓存管理器:在Spring的配置文件(如applicationContext.xml)中配置缓存管理器。可以选择使用Spring提供的缓存管理器实现,如ConcurrentMapCacheManager、EhCacheCacheManager等,也可以自定义缓存管理器。

在需要缓存的方法上添加缓存注解:在需要进行缓存的方法上添加Spring Cache的缓存注解,如@Cacheable、@CachePut等。这些注解可以指定缓存的名称、缓存条目的键,以及在何时加入或刷新缓存条目。

配置缓存注解的属性:根据需求,可以为缓存注解添加一些属性,如缓存的失效时间、编写缓存的键生成器等。

启用缓存功能:在Spring的配置类上使用@EnableCaching注解,以启用Spring Cache的功能

SpringCache通过注解提供缓存服务,注解只是提供一个抽象的统一访问接口,而没有提供缓存的实现。对于每个版本的spring,其使用的缓存实现存在一定的差异性。例如springboot 3.X,提供以下的缓存实现。

2.3.SpringCache主要注解

@Cacheable:将方法的返回值缓存起来,并在下次调用时,直接从缓存中获取,而不执行方法体。

@CachePut:将方法的返回值缓存起来,与@Cacheable不同的是,@CachePut会每次都执行方法体,并将返回值放入缓存中。

@CacheEvict:从缓存中移除一个或多个条目。可以通过指定的key来删除具体的缓存条目,或者通过allEntries属性来删除所有的缓存条目。


3.Go使用缓存

说实在,Go缓存工具确定没Java的好用,特别是Go要1.18后的版本才支持泛型,而相当多的缓存库因为开发时间早,不支持或暂未支持泛型。

我们先来看看如何使用第三方缓存库。go的缓存工具还是比较多的,大致搜了一下,有bigcache,go-cache,freecache,groupcache,不胜枚举。

本文以bigcache作为案例演示,设计目标

  • 支持对不同的数据库表进行缓存,不同表的缓存相互独立
  • 允许对缓存进行读操作,若缓存无法命中,则从数据库加载,并写入缓存
  • 定时移除长时间未读写的数据项

3.1.bigcache版本

3.1.1.缓存容器

1.首先,引入依赖

go get github.com/allegro/bigcache

2.缓存结构定义

const (
	// Define the timeout period after which cache items should be removed
	cleanupInterval     = 1 * time.Minute
	inactivityThreshold = 5 * time.Minute
)

type CustomCache struct {
	cache      *bigcache.BigCache
	lastAccess map[string]time.Time
	mu         sync.RWMutex
	modelType  reflect.Type
}

func NewCustomCache(modelType reflect.Type) (*CustomCache, error) {
	cache, err := bigcache.NewBigCache(bigcache.DefaultConfig(10 * time.Minute))
	if err != nil {
		return nil, err
	}
	return &CustomCache{
		cache:      cache,
		lastAccess: make(map[string]time.Time),
		modelType:  modelType,
	}, nil
}

3.缓存写操作

CustomCache内部使用读写锁用于并发,多个读操作可以并发,但只要有一个写操作,则其他写操作或者读操作无法执行。Set()使用写锁

func (c *CustomCache) Set(key string, value []byte) error {
	c.mu.Lock()
	defer c.mu.Unlock()
	err := c.cache.Set(key, value)
	if err != nil {
		return err
	}
	c.lastAccess[key] = time.Now()
	return nil
}

4.缓存读操作

下面演示读操作,这里比较复杂,实现了,若没有命中缓存,则从数据库加载,并将对象对应的byte 数组写入缓存

// General function to query the database using GORM
func queryDatabase(modelType reflect.Type, key string) ([]byte, error) {
	// Create a new instance of the model
	result := reflect.New(modelType).Interface()

	// Perform the query
	if err := mysqldb.Db.Where("id = ?", key).First(result).Error; err != nil {
		return nil, err
	}

	// Serialize the result to a byte slice
	jsonData, err := json.Marshal(result)
	if err != nil {
		return nil, err
	}

	return jsonData, nil
}

func (c *CustomCache) Get(key string) (interface{}, error) {
	// First try to get the value from the cache
	c.mu.RLocker().Lock()
	value, err := c.cache.Get(key)
	if err != nil {
		c.mu.RLocker().Unlock()

		// If cache miss, load from database
		value, err = queryDatabase(c.modelType, key)
		if err != nil {
			return nil, err
		}

		err = c.Set(key, value)
		if err != nil {
			return nil, err
		}
	} else {
		c.mu.RLocker().Unlock()
	}

	// Deserialize the value
	modelInstance := reflect.New(c.modelType).Interface()
	err = json.Unmarshal(value, modelInstance)
	if err != nil {
		return nil, err
	}

	return modelInstance, nil
}

这里有一个与java的不同之处。在java里,锁是支持可重入的。这意味着,同一个线程,可以多次获得同一个锁,读写锁也支持。

然而,Go的锁不支持可重入。这意味着,即使是同一个goroutine,在获得同一个锁之前,必须先解锁。反映到这里的代码是, Get操作内部,如果命中不了缓存,从数据库加载之后,在set之前,需要先释放读锁,保证set内部的写锁可以工作。所以这里的代码比较复杂(恶心),也没有完美抑制并发,这里从数据库读取的时候没有加锁。 

5.定时移除不活跃数据

func (c *CustomCache) cleanup() {
	c.mu.Lock()
	defer c.mu.Unlock()

	now := time.Now()
	for key, lastAccess := range c.lastAccess {
		if now.Sub(lastAccess) > inactivityThreshold {
			c.cache.Delete(key)
			delete(c.lastAccess, key)
		}
	}
}

func (c *CustomCache) StartCleanupRoutine() {
	ticker := time.NewTicker(cleanupInterval)
	go func() {
		for {
			select {
			case <-ticker.C:
				c.cleanup()
			}
		}
	}()
}

3.1.2.缓存管理器

对于每张表,都用对应的容器来保存,实现表之间数据分隔,所以这里加多一层管理层。

import (
	"io/github/gforgame/examples/player"
	"reflect"
	"sync"
)

type CacheManager struct {
	caches map[string]*CustomCache
	mu     sync.Mutex
}

func NewCacheManager() *CacheManager {
	return &CacheManager{
		caches: make(map[string]*CustomCache),
	}
}

func (cm *CacheManager) GetCache(table string) (*CustomCache, error) {
	cm.mu.Lock()
	defer cm.mu.Unlock()

	// 这里可以加一个映射,表格名称与model
	// 为了简化,这里硬编码
	model := reflect.TypeOf(player.Player{})

	if cache, exists := cm.caches[table]; exists {
		return cache, nil
	}

	cache, err := NewCustomCache(model)
	if err != nil {
		return nil, err
	}
	cache.StartCleanupRoutine()
	cm.caches[table] = cache
	return cache, nil
}

2.1.3.单元测试

主要观察,第一次读取,从数据库加载,第二次读取,从缓存加载。

import (
	"fmt"
	"testing"
)

func TestCache(t *testing.T) {
	cacheManager := NewCacheManager()
	// Fetch data from users table
	userCache, err := cacheManager.GetCache("player")
	if err != nil {
		fmt.Println("Error getting cache:", err)
		return
	}

	_, err = userCache.Get("1001")
	if err != nil {
		fmt.Println("Error getting value:", err)
		return
	}
	value2, err := userCache.Get("1001")
	fmt.Println("Value from table", value2)
}

使用bigcache实现了大部分功能,但还是有瑕疵。最主要的原因是底层数据格式为byte数组,签名如下。这意味着无法缓存对象的泛型数据,频繁序列化反序列化会影响性能。

func (c *BigCache) Set(key string, entry []byte) error

如果说使用的是进程外缓存(例如redis),redis可能使用json或protobuf等数据编解码,由于跨进程,这种序列化反序列化操作无法避免。但如果是进程内缓存仍需要这种io操作,那真是“婶可忍叔不可忍”!

3.2.原生map版本

接下来我们试着使用Go原生的map数据结果实现上述的功能,并且底层存储的是interface{},而不是byte[]。

3.2.1.缓存容器

1.缓存容器定义

// CacheItem 表示缓存条目
type CacheItem struct {
	Value      interface{}
	LastAccess time.Time
}

// Cache 表示缓存
type Cache struct {
	mu              sync.RWMutex
	items           map[string]*CacheItem
	expiry          time.Duration
	cleanupInterval time.Duration
	loader          func(key string) (interface{}, error)
}

// NewCache 创建一个新的缓存实例
func NewCache(expiry time.Duration, cleanupInterval time.Duration, loader func(key string) (interface{}, error)) *Cache {
	cache := &Cache{
		items:           make(map[string]*CacheItem),
		expiry:          expiry,
		cleanupInterval: cleanupInterval,
		loader:          loader,
	}
	go cache.cleanup() // 启动定期清理线程
	return cache
}

这里Cache#loader是一个函数类型,用于针对不同的数据表执行相应的数据库读取操作

2.缓存读取操作

// Get 从缓存中获取数据
func (c *Cache) Get(key string) (interface{}, error) {
	c.mu.RLock()
	item, found := c.items[key]
	c.mu.RUnlock()

	if found {
		// 更新访问时间
		c.mu.Lock()
		item.LastAccess = time.Now()
		c.mu.Unlock()
		return item.Value, nil
	}

	// 如果缓存未命中,从数据库加载数据
	value, err := c.loader(key)
	if err != nil {
		return nil, err
	}

	c.mu.Lock()
	c.items[key] = &CacheItem{
		Value:      value,
		LastAccess: time.Now(),
	}
	c.mu.Unlock()

	return value, nil
}

这里由于每张表都使用唯一的读写锁,容易影响吞吐量,可以进一步优化,使用分段锁代替独占锁。这里不展开处理。

3.缓存写入操作

// Set 更新缓存中的数据
func (c *Cache) Set(key string, value interface{}) {
	c.mu.Lock()
	c.items[key] = &CacheItem{
		Value:      value,
		LastAccess: time.Now(),
	}
	c.mu.Unlock()
}

4.定时移除沉默数据

// cleanup 定期清理沉默缓存
func (c *Cache) cleanup() {
	for {
		time.Sleep(c.cleanupInterval) // 以指定的清理间隔进行清理

		c.mu.Lock()
		now := time.Now()
		for key, item := range c.items {
			if now.After(item.LastAccess.Add(c.expiry)) {
				delete(c.items, key)
			}
		}
		c.mu.Unlock()
	}
}

3.2.2.缓存管理器

对于每张表,都用对应的容器来保存,实现表之间数据分隔,所以这里加多一层管理层。内部绑定了每张表与对应的数据库读取操作。

import (
	"fmt"
	mysqldb "io/github/gforgame/db"
	"io/github/gforgame/examples/player"
	"sync"
	"time"
)

var (
	loaders = map[string]func(key string) (interface{}, error){}
)

func init() {
	loaders = map[string]func(string) (interface{}, error){}
	loaders["player"] = func(key string) (interface{}, error) {
		var p player.Player
		mysqldb.Db.First(&p, "id=?", key)
		return &p, nil
	}
}

type CacheManager struct {
	caches map[string]*Cache
	mu     sync.Mutex
}

func NewCacheManager() *CacheManager {
	return &CacheManager{
		caches: make(map[string]*Cache),
	}
}

func (cm *CacheManager) GetCache(table string) (*Cache, error) {
	cm.mu.Lock()
	defer cm.mu.Unlock()

	if cache, exists := cm.caches[table]; exists {
		return cache, nil
	}

	dbLoader, ok := loaders[table]
	if !ok {
		return nil, fmt.Errorf("cache table %s not found", table)
	}
	cache := NewCache(5*time.Second, 10*time.Second, dbLoader)
	cm.caches[table] = cache
	return cache, nil
}

3.2.3.单元测试

import (
	"fmt"
	"io/github/gforgame/examples/player"
	"testing"
)

func TestCache(t *testing.T) {
	cm := NewCacheManager()
	cache, err := cm.GetCache("player")
	if err != nil {
		t.Error(err)
	}

	// 测试缓存
	key := "1001"
	p, err := cache.Get(key)
	if err != nil {
		fmt.Println("Error:", err)
	}
	fmt.Printf("first query %s: %v\n", key, p)
	p2, ok := p.(*player.Player)
	if ok {
		p2.Name = "gforgam2"
		 使用 Set 方法更新缓存
		cache.Set(key, p2)
		p, err = cache.Get(key)
		fmt.Printf("second query %s: %v\n", key, p)
	}

}

完整代码请移步:

--> go游戏服务器

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

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

相关文章

如何在手机端跑大模型?

最近新入手了一台 arm 开发板&#xff0c;内置安装了 Android 13 系统。 昨天把网络问题给解决了&#xff1a;安卓连接 WIFI 但无法上网&#xff1f;盘点踩过的那些坑 今日分享&#xff0c;继续带大家实操&#xff1a;如何把大模型&#xff08;LLM&#xff09;部署到移动端&a…

文章资讯职场话题网站源码整站资源自带2000+数据

介绍&#xff1a; 数据有点多&#xff0c;数据资源包比较大&#xff0c;压缩后还有250m左右。值钱的是数据&#xff0c;网站上传后直接可用&#xff0c;爽飞了 环境&#xff1a;NGINX1.18 mysql5.6 php7.2 代码下载

JUC学习笔记(三)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 八、共享模型之工具--JUC8.1 AQS 原理1. 概述2 实现不可重入锁自定义同步器自定义锁 3.心得起源目标设计1) state 设计2&#xff09;阻塞恢复设计3&#xff09;队列…

学习笔记 韩顺平 零基础30天学会Java(2024.9.16)

P563 自定义泛型方法 当调用方法时&#xff0c;要传入参数&#xff0c;因为当传入参数时&#xff0c;编译器就可以确定泛型代表的类型 泛型方法和方法使用了泛型是不一样的 泛型方法可以使用类声明的泛型&#xff0c;也可以使用自己的泛型 P564 泛型方法练习 P565 泛型的继承和…

Python编码系列—Python适配器模式:无缝集成的桥梁

&#x1f31f;&#x1f31f; 欢迎来到我的技术小筑&#xff0c;一个专为技术探索者打造的交流空间。在这里&#xff0c;我们不仅分享代码的智慧&#xff0c;还探讨技术的深度与广度。无论您是资深开发者还是技术新手&#xff0c;这里都有一片属于您的天空。让我们在知识的海洋中…

二叉树OJ题——另一棵树的子树

文章目录 一、题目链接二、解题思路三、解题代码 一、题目链接 另一棵树的子树 题目描述&#xff1a;判断当前树A是否是树B的子树。 二、解题思路 时间复杂度&#xff1a;O(n*m) 三、解题代码

Learn ComputeShader 15 Grass

1.Using Blender to create a single grass clump 首先blender与unity的坐标轴不同&#xff0c;z轴向上&#xff0c;不是y轴 通过小键盘的数字键可以快速切换视图&#xff0c;选中物体以后按下小键盘的点可以将物体聚焦于屏幕中心 首先我们创建一个平面&#xff0c;宽度为0.2…

AI替代插画师跟设计师?不用焦虑!

一个固定的工作流&#xff0c; 一个训练好的lora模型 输入一段提示词 二三十秒的时间&#xff0c;就能生成一张精致美观有韵味的中秋国风插画 这张不喜欢&#xff0c;改下提示词重新生成一张不一样的。还是二十几秒 同样的插画&#xff0c;你用手绘&#xff0c;从起稿到上…

大数据新视界 --大数据大厂之MongoDB与大数据:灵活文档数据库的应用场景

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…

【设计模式-外观】

这里写自定义目录标题 定义UML图角色作用代码使用场景 定义 为子系统中一组相关接口提供一致界面&#xff0c;定义一个高级接口&#xff0c;使得子系统更加容易使用。 UML图 角色作用 外观&#xff08;Facade&#xff09;角色&#xff1a;这是外观模式的核心&#xff0c;它知…

MongoDB的详细安装教程

6、MongoDB安装 6.1 为什么使用MongoDB 性能好大规模数据存储&#xff08;可拓展性&#xff09;可靠安全&#xff08;本地复制、自动故障转移&#xff09;方便存储复杂数据结构 6.2 下载安装 【1】下载地址&#xff0c;这里下载的是5.0版本的&#xff0c;否则配置环境变量之…

【电路笔记】-差分运算放大器

差分运算放大器 文章目录 差分运算放大器1、概述2、差分运算放大器表示2.1 差分模式2.2 减法器模式3、差分放大器示例3.1 相关电阻3.2 惠斯通桥3.3 光/温度检测4、仪表放大器5、总结1、概述 在之前的文章中,我们讨论了反相运算放大器和同相运算放大器,我们考虑了在运算放大器…

revisiting拉普拉斯模板

二维向量的二阶微分是Hessian矩阵&#xff0c;拉普拉斯算子是将两个独立的二阶微分求和&#xff0c;对二阶微分的近似。 我不认同冈萨雷斯的8邻域拉普拉斯模板。 MATLAB图像处理工具箱中fspecial函数’laplacian’参数给的拉普拉斯模板&#xff1a; 对于数字滤波器&#xff…

中秋前夕-我居然使用技术来鞭策兄弟

中秋前夕-我居然使用技术来鞭策兄弟 前言 最近在带领一些小伙伴在完成功能&#xff0c;因为人数不少&#xff0c;那么我们如何统计大家有没有摸鱼偷懒呢&#xff1f; 聪明的朋友们可以想到&#xff0c;利用git的提交记录统计。 因为git提交时&#xff0c;会给我们带上一些关…

高德2.0 多边形覆盖物无法选中编辑

多边形覆盖物无法选中编辑。先检查一下数据的类型得是<number[]>,里面是字符串的虽然显示没问题&#xff0c;但是不能选中编辑。 &#xff08;在项目中排查了加载时机&#xff0c;事件监听…等等种种原因&#xff0c;就是没发现问题。突然想到可能是数据就有问题&#xf…

ROS组合导航笔记:融合传感器数据

使用机器人定位包&#xff08;robot_localization package&#xff09;来合并来自不同传感器的数据&#xff0c;以改进机器人定位时的姿态估计。 基本概念 在现实生活中操作机器人时&#xff0c;有时我们需要处理不够准确的传感器数据。如果我们想要实现机器人的高精度定位&am…

初探全同态加密1 —— FHE的定义与历史回顾

文章目录 一、加密体系1、什么是加密体系2、加密体系的属性 Properties 二、同态加密&#xff1a;偶然的特殊性质三、同态加密体系的分类四、部分同态加密 Partially Homomorphic Encryption1、加法同态加密算法 —— ElGamal 加密算法1.1、ElGamal 的大致步骤1.2、ElGamal 的加…

vue3+vite项目中使用阿里图标库(svg)图标

前端项目中有很多地方会用到小图标&#xff0c;阿里的 iconfont 是一个不错的选择&#xff0c;同时&#xff0c; 它上面的 svg 矢量图标占用资源更少加载更快是一个不错的选择&#xff0c; 下面我们就来说一说&#xff0c;项目中如何来使用 svg 图标 安装插件 vite-plugin-svg…

RP2040 C SDK SysTick滴答定时器功能使用

RP2040 C SDK SysTick滴答定时器功能使用 ✨更好的阅读体验请移步到&#xff1a;飞书-云文档&#xff1a;https://u1etqmuwl9z.feishu.cn/wiki/VkoHwPGqHierOEkY651cZvaOntg?fromfrom_copylink RP2040的SysTick滴答定时器为24位的&#xff0c;计数方式为倒计时&#xff0c;扩展…

基于对数变换的图像美白增强,Matlab实现

博主简介&#xff1a;matlab图像处理&#xff08;QQ:3249726188&#xff09; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 本次案例是基于对数变换的图像美白增强&#xff0c;用matlab实现。 一、案例背景和算法介绍 这次案例是美白算法&…