Go缓存系统

devtools/2024/9/23 1:33:13/

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缓存工具还是比较多的,大致搜了一下,有cache>bigcache,go-cache,freecache,groupcache,不胜枚举。

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

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

3.1.cache>bigcache版本

3.1.1.缓存容器

1.首先,引入依赖

go get github.com/allegro/cache>bigcache

2.缓存结构定义

const (// Define the timeout period after which cache items should be removedcleanupInterval     = 1 * time.MinuteinactivityThreshold = 5 * time.Minute
)type CustomCache struct {cache      *cache>bigcache.BigCachelastAccess map[string]time.Timemu         sync.RWMutexmodelType  reflect.Type
}func NewCustomCache(modelType reflect.Type) (*CustomCache, error) {cache, err := cache>bigcache.NewBigCache(cache>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 modelresult := reflect.New(modelType).Interface()// Perform the queryif err := mysqldb.Db.Where("id = ?", key).First(result).Error; err != nil {return nil, err}// Serialize the result to a byte slicejsonData, 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 cachec.mu.RLocker().Lock()value, err := c.cache.Get(key)if err != nil {c.mu.RLocker().Unlock()// If cache miss, load from databasevalue, 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 valuemodelInstance := 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]*CustomCachemu     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] = cachereturn cache, nil
}

2.1.3.单元测试

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

import ("fmt""testing"
)func TestCache(t *testing.T) {cacheManager := NewCacheManager()// Fetch data from users tableuserCache, 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)
}

使用cache>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.RWMutexitems           map[string]*CacheItemexpiry          time.DurationcleanupInterval time.Durationloader          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.Playermysqldb.Db.First(&p, "id=?", key)return &p, nil}
}type CacheManager struct {caches map[string]*Cachemu     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] = cachereturn 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.ppmy.cn/devtools/115734.html

相关文章

数据结构-树(基础,分类,遍历)

数据结构-树 1.什么是树&#xff1f; 在计算机科学中&#xff0c;树是一种常用的非线性数据结构&#xff0c;用于表示具有层次关系的数据。与线性数据结构&#xff08;如数组和链表&#xff09;不同&#xff0c;树结构以节点&#xff08;Nodes&#xff09;和边&#xff08;Ed…

搭建 PHP

快速搭建 PHP 环境指南 PHP 是一种广泛用于 Web 开发的后端脚本语言&#xff0c;因其灵活性和易用性而受到开发者的青睐。无论是开发个人项目还是企业级应用&#xff0c;PHP 环境的搭建都是一个不可忽视的基础步骤。本指南将带您快速学习如何在不同平台上搭建 PHP 环境&#x…

CentOS入门宝典:从零到一构建你的Linux服务器帝国

目录 引言 一、CentOS简介与版本选择 1.1 CentOS是什么&#xff1f; 1.2 版本选择 二、安装CentOS 2.1 准备安装介质 2.2 安装过程 三、基础配置与优化 3.1 更新系统 3.2 配置防火墙 3.3 配置SELinux 3.4 系统监控与日志 四、网络配置与管理 4.1 配置静态IP 4.…

Kotlin-Flow学习笔记

Channel 和 Flow 都是数据流&#xff0c;Channel 是“热”的&#xff0c;Flow 则是“冷”的。这里的冷&#xff0c;代表着 Flow 不仅是“冷淡”的&#xff0c;而且还是“懒惰”的。 Flow 从 API 的角度分类&#xff0c;主要分为&#xff1a;构造器、中间操作符、终止操作符。今…

深入了解 Maven 和 Redis

在现代软件开发中&#xff0c;工具的选择对于项目的成功至关重要。Maven 和 Redis 是两个在不同领域发挥着重要作用的工具&#xff0c;本文将对它们进行详细介绍。 一、Maven&#xff1a;强大的项目管理工具 &#xff08;一&#xff09;什么是 Maven&#xff1f; Maven 是一个基…

ARM驱动学习之 IOremap实现GPIO 读

ARM驱动学习之 IOremap实现GPIO 读 前面介绍了虚拟地址和物理地址。 读写GPIO&#xff0c;控制GPIO的寄存器都是使用系统做好的虚拟地址 本期介绍如何自己实现物理地址到虚拟地址的转化 iounmap和ioremap函数可以实现物理地址到虚拟地址的转化1.根据原理图找核心板对应的寄存器…

【HTTP】请求“报头”,Referer 和 Cookie

Referer 描述了当前这个页面是从哪里来的&#xff08;从哪个页面跳转过来的&#xff09; 浏览器中&#xff0c;直接输入 URL/点击收藏夹打开的网页&#xff0c;此时是没有 referer。当你在 sogou 页面进行搜索时&#xff0c;新进入的网页就会有 referer 有一个非常典型的用…

2024“华为杯”中国研究生数学建模竞赛(A题)深度剖析_数学建模完整过程+详细思路+代码全解析

问题一详细解答过程 2. 简化疲劳损伤计算模型 2.1 累积损伤的Palmgren-Miner理论 根据Palmgren-Miner线性累积损伤理论&#xff0c;疲劳损伤是通过在一定的应力循环下累积的。对于给定应力幅值 S i S_i Si​&#xff0c;累积损伤值 D D D 是由经历的应力循环次数 n i n_i…