Golang项目:实现一个内存缓存系统

news2025/1/8 3:53:41

要求

  1. 支持设定过期时间,精确到秒
  2. 支持设定最大内存,当内存超过时做出合适的处理
  3. 支持并发安全
  4. 按照以下接口安全

type Cache interface{
	//size : 1KB 100KB 1MB 2MB 1GB
	SetMaxMemory(size string )bool

	//将value写入缓存
	Set(key string, val interface{},expire time.Duration)bool

	//根据key值获取value
	Get(key string )(interface{},bool)

	//删除key
	Del(key string)bool

	//判断key是否存在
	Exists(key string)bool

	//清空所有key
	Flush()bool

	//获取缓存中所有key的数量
	Keys()int64
}
  1. 使用示例
cache := NewMemCache()
cache.SetMaxMemory("100MB")
cache.Set("int",1)
cache.Set("bool",false)
cache.Set("data",map[string]interface(){"a":1})
cache.Get("int")
cache.Del("int")
cache.Flush()
cache.Keys()

首先创建对应文件夹
在这里插入图片描述

其中main.go中填入测试案例

package main

import (
	"memCache/cache"
	"time"
)

func main() {
	cache := cache.NewMemCache()
	cache.SetMaxMemory("200MB")

	cache.Set("int", 1, time.Second)
	cache.Set("bool", false, time.Second)
	cache.Set("data", map[string]interface{}{"a": 1}, time.Second)

	//cache.Set("int",1)
	//cache.Set("bool",false)
	//cache.Set("data",map[string]interface{}{"a":1})
	cache.Get("int")
	cache.Del("int")
	cache.Flush()
	cache.Keys()

	//num, str := cache.ParseSize("2KB")
	//fmt.Println(num, str)

}

定义cache.go的接口

package cache

import "time"

type Cache interface {
	//size : 1KB 100KB 1MB 2MB 1GB
	SetMaxMemory(size string) bool

	//将value写入缓存
	Set(key string, val interface{}, expire time.Duration) bool

	//根据key值获取value
	Get(key string) (interface{}, bool)

	//删除key
	Del(key string) bool

	//判断key是否存在
	Exists(key string) bool

	//清空所有key
	Flush() bool

	//获取缓存中所有key的数量
	Keys() int64
}

然后在memCache.go中实现

package cache

import (
	"fmt"
	"time"
)

type memCache struct {
	//最大内存 -- 单位字节
	maxMemorySize int64

	//最大内存字符串表示
	maxMemorySizeStr string

	//当前内存大小 -- 单位字节
	currentMemorySize int64
}

func NewMemCache() Cache {
	return &memCache{}
}

// size : 1KB 100KB 1MB 2MB 1GB
func (mc *memCache) SetMaxMemory(size string) bool {

	mc.maxMemorySize, mc.maxMemorySizeStr = ParseSize(size)
	fmt.Println(mc.maxMemorySize, mc.maxMemorySizeStr)

	fmt.Println("called set Maxmem")
	return false
}

// 将value写入缓存
func (mc *memCache) Set(key string, val interface{}, expire time.Duration) bool {
	fmt.Println("called set")
	return false
}

// 根据key值获取value
func (mc *memCache) Get(key string) (interface{}, bool) {
	fmt.Println("called get")

	return nil, false
}

// 删除key
func (mc *memCache) Del(key string) bool {
	fmt.Println("called del")
	return false
}

// 判断key是否存在
func (mc *memCache) Exists(key string) bool {
	return false
}

// 清空所有key
func (mc *memCache) Flush() bool {
	return false
}

// 获取缓存中所有key的数量
func (mc *memCache) Keys() int64 {
	return 0
}


实现设置最大内存数中

// size : 1KB 100KB 1MB 2MB 1GB
func (mc *memCache) SetMaxMemory(size string) bool {

	mc.maxMemorySize, mc.maxMemorySizeStr = ParseSize(size)
	fmt.Println(mc.maxMemorySize, mc.maxMemorySizeStr)

	fmt.Println("called set Maxmem")
	return false
}

ParseSize()方法在util.go文件中实现


const (
	B = 1 << (iota * 10)
	KB
	MB
	GB
	TB
	PB
)

func ParseSize(size string) (int64, string) {
	//默认大小为 100MB

	re, _ := regexp.Compile("[0-9]+")
	//fmt.Println(re)
	unit := string(re.ReplaceAll([]byte(size), []byte("")))
	//fmt.Println("unit: " + unit)
	num, _ := strconv.ParseInt(strings.Replace(size, unit, "", 1), 10, 64)
	//fmt.Println("num: ", num)
	unit = strings.ToUpper(unit)

	var byteNum int64 = 0
	switch unit {
	case "B":
		byteNum = num
	case "KB":
		byteNum = num * KB
	case "MB":
		byteNum = num * MB
	case "GB":
		byteNum = num * GB
	case "TB":
		byteNum = num * TB
	case "PB":
		byteNum = num * PB
	default:
		byteNum = 0
		num = 0
	}

	if num == 0 {
		log.Println("ParseSize 仅支持 B,KB,MB,GB,TB,PB")
		num = 100
		byteNum = num * MB
		unit = "MB"
	}

	sizeStr := strconv.FormatInt(num, 10) + unit

	return byteNum, sizeStr
}

初步测试

在这里插入图片描述
说明ParseSize实现无误


接下来是实现Set方法

在memCache.go中添加


type memCacheValue struct {
	//value值
	val interface{}
	//过期时间
	expiration time.Time
	//value 大小
	size int64
}

来存储每一块内存的信息,包括大小,值,过期时间

对于Set操作,我们需要创建一些方法来辅助执行
如:get,add,del操作

// 将value写入缓存
func (mc *memCache) Set(key string, val interface{}, expire time.Duration) bool {
	mc.locker.Lock()
	defer mc.locker.Unlock()
	v := &memCacheValue{
		val:        val,
		expireTime: time.Now().Add(expire),
		size:       GetValSize(val),
	}
	mc.del(key)
	mc.add(key, v)
	if mc.currentMemorySize > mc.maxMemorySize {
		mc.del(key)
		panic(fmt.Sprintf("max memory size %s", mc.maxMemorySize))
	}
	return false
}

func (mc *memCache)del(key string) {
	tmp,ok:=mc.get(key)
	if ok && tmp != nil {
		mc.currentMemorySize -= tmp.size
		delete(mc.values, key)
	}
	
}

func (mc *memCache)add(key string, val *memCacheValue)  {
	mc.values[key] = val
	mc.currentMemorySize += val.size
	
}

func (mc *memCache)get(key string) (*memCacheValue,bool) {
	val,ok := mc.values[key]
	return val,ok
}


同理也可以利用上面创建的add,del,get方法来实现Get操作


// 根据key值获取value
func (mc *memCache) Get(key string) (interface{}, bool) {
	mc.locker.RLock()
	defer mc.locker.RUnlock()
	mcv, ok := mc.get(key)
	if ok{
		//判断缓存是否过期
		if mcv.expireTime.Before(time.Now()) {
			mc.del(key)
			return nil, false
		}
		return mcv.val, true
	}
	fmt.Println("called get")
	return nil, false
}


Del操作的实现


// 删除key
func (mc *memCache) Del(key string) bool {
	mc.locker.Lock()
	defer mc.locker.Unlock()

	mc.del(key)
	fmt.Println("called del")
	return false
}

剩余其他操作

// 判断key是否存在
func (mc *memCache) Exists(key string) bool {
	mc.locker.RLock()
	defer mc.locker.RUnlock()
	_,ok := mc.values[key]
	return ok
}

// 清空所有key
func (mc *memCache) Flush() bool {
	mc.locker.Lock()
	defer mc.locker.Unlock()
	
	mc.values = make(map[string]*memCacheValue)
	mc.currentMemorySize = 0
	return false
}

// 获取缓存中所有key的数量
func (mc *memCache) Keys() int64 {
	mc.locker.RLock()
	defer mc.locker.RUnlock()
	
	
	return int64(len(mc.values))
}

现在的问题是,我们只是设置了,在过期之后,就不能访问了,但是实际上还占用着缓存,只有在再一次Get的时候,发现过期了,才会删除掉

所以现在我们做一个定期清空的轮询访问


// 定期清除缓存
func (mc *memCache) clearExpiredItem() {
	timeTicker := time.NewTicker(mc.clearExpiredItemTimeInerval)
	defer timeTicker.Stop()

	for {
		fmt.Println("轮询检查")
		select {
		case <-timeTicker.C:
			for key, item := range mc.values {
				if item.expireTime.Before(time.Now()) {
					mc.locker.Lock()
					mc.del(key)
					mc.locker.Unlock()
					fmt.Println("check")
				}
			}
		}
	}
}


测试案例

之后,我们创建一个代理层,带使用测试案例

在这里插入图片描述

在cache-server中的cache.go文件中

创建对象,添加代理

package cache_server

import (
	"memCache/cache"
	"time"
)

type cacheServer struct {
	memCache cache.Cache
}

func NewMemCache() *cacheServer {
	return &cacheServer{
		memCache: cache.NewMemCache(),
	}
}

// size : 1KB 100KB 1MB 2MB 1GB
func (cs *cacheServer) SetMaxMemory(size string) bool {
	return cs.memCache.SetMaxMemory(size)
}

// 将value写入缓存
func (cs *cacheServer) Set(key string, val interface{}, expire ...time.Duration) bool {
	expireTs := time.Second * 0

	if len(expire) > 0 {
		expireTs = expire[0]
	}

	return cs.memCache.Set(key, val, expireTs)
}

// 根据key值获取value
func (cs *cacheServer) Get(key string) (interface{}, bool) {
	return cs.memCache.Get(key)
}

// 删除key
func (cs *cacheServer) Del(key string) bool {
	return cs.memCache.Del(key)
}

// 判断key是否存在
func (cs *cacheServer) Exists(key string) bool {
	return cs.memCache.Exists(key)
}

// 清空所有key
func (cs *cacheServer) Flush() bool {
	return cs.memCache.Flush()
}

// 获取缓存中所有key的数量
func (cs *cacheServer) Keys() int64 {
	return cs.memCache.Keys()
}

可以注意到,我们在代理层中对Set方法进行了可变长参数化

使得我们的Set方法的time部分参数为可选择填写0-n个参数,但我们只使用第一个expire[0]作为我们使用的参数

此时main函数可改成测试案例

cache := cache_server.NewMemCache()
		cache.SetMaxMemory("200MB")

		cache.Set("int", 1, 20*time.Second)
		cache.Set("bool", false, 10*time.Second)
		cache.Set("data", map[string]interface{}{"a": 1}, time.Second)

		cache.Set("int", 1)
		cache.Set("bool", false)
		cache.Set("data", map[string]interface{}{"a": 1})
		cache.Get("int")
		cache.Del("int")
		cache.Flush()
		cache.Keys()
		time.Sleep(time.Second * 25)

GetValSize函数

我们使用GetValSize函数

func GetValSize(val interface{}) int64 {
	size := unsafe.Sizeof(val)
	fmt.Println(int64(size))
	return int64(size)
}
cache.GetValSize(1)
	cache.GetValSize(false)
	cache.GetValSize("adwaeqweqwr")
	cache.GetValSize(map[string]string{
		"a": "b",
		"c": "d",
	})

会发现
在这里插入图片描述
无论输入什么,size都是16
unsafe.Sizeof 来获取一个接口值(interface{})的大小时,它实际上返回的是接口结构体本身的大小,而不是接口中存储的具体值的大小

我们可以利用json序列化,来直接获得序列长度来代表val的大小

func GetValSize(val interface{}) int64 {
	byte, _ := json.Marshal(val)
	size := int64(len(byte))

	fmt.Println(size)
	return int64(size)
}

可以看出,我们就可以得到实际的一个大小
在这里插入图片描述


加分项,单元测试

我们在cache目录中,新建一个单元测试,memCache_test.go

package cache

import (
	"testing"
	"time"
)

func TestCacheOP(t *testing.T) {
	testData := []struct {
		key    string
		val    interface{}
		expire time.Duration
	}{
		{"slawe", 234623, time.Second * 10},
		{"sawe", false, time.Second * 11},
		{"serytje", true, time.Second * 12},
		{"w35wyhe", map[string]interface{}{"a": 2, "B": false}, time.Second * 13},
		{"swetwgb", "fiyu85", time.Second * 14},
	}

	c := NewMemCache()
	c.SetMaxMemory("10MB")

	for _, item := range testData {
		c.Set(item.key, item.val, item.expire)
		val, ok := c.Get(item.key)
		if !ok {
			t.Error("缓存取值失败")
		}
		if item.key != "w35wyhe" && val != item.val {
			t.Error("缓存取值数据与预期不一致")
		}

		_, ok1 := val.(map[string]interface{})
		if item.key == "w35wyhe" && !ok1 {
			t.Error("map缓存取值数据与预期不一致")
		}
	}

	if int64(len(testData)) != c.Keys() {
		t.Error("缓存数量不一致")
	}

	c.Del(testData[0].key)
	c.Del(testData[1].key)

	if int64(len(testData)) != c.Keys()+2 {
		t.Error("缓存数量不一致")
	}

	time.Sleep(17 * time.Second)

	if c.Keys() != 0 {
		t.Error("缓存未清空")
	}

}

在这里插入图片描述
单元测试通过!!!

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

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

相关文章

Softing线上研讨会 | Ethernet-APL:推动数字时代的过程自动化

| &#xff08;免费&#xff09;线上研讨会时间&#xff1a;2024年11月19日 16:00~16:30 / 23:00~23:30 Ethernet-APL以10Mb/s的传输速率为过程工业中的现场设备带来了无缝以太网连接和本质安全电源&#xff0c;这不仅革新了新建工厂&#xff0c;也适用于改造现有工厂。 与现…

《Deep Multimodal Learning with Missing Modality: A Survey》中文校对版

文章汉化系列目录 文章目录 文章汉化系列目录摘要1 引言2 方法论分类&#xff1a;概述2.1 数据处理方面2.2 策略设计方面 3 数据处理方面的方法3.1 模态填充3.1.1 模态组合方法3.1.2 模态生成方法 3.2 面向表示的模型3.2.1 协调表示方法3.2.2 表示组合方法。3.2.3 表示生成方法…

python爬虫案例——猫眼电影数据抓取之字体解密,多套字体文件解密方法(20)

文章目录 1、任务目标2、网站分析3、代码编写1、任务目标 目标网站:猫眼电影(https://www.maoyan.com/films?showType=2) 要求:抓取该网站下,所有即将上映电影的预约人数,保证能够获取到实时更新的内容;如下: 2、网站分析 进入目标网站,打开开发者模式,经过分析,我…

鸿蒙安全控件之位置控件简介

位置控件使用直观且易懂的通用标识&#xff0c;让用户明确地知道这是一个获取位置信息的按钮。这满足了授权场景需要匹配用户真实意图的需求。只有当用户主观愿意&#xff0c;并且明确了解使用场景后点击位置控件&#xff0c;应用才会获得临时的授权&#xff0c;获取位置信息并…

MATLAB矩阵元素的修改及删除

利用等号赋值来进行修改 A ( m , n ) c A(m,n)c A(m,n)c将将矩阵第 m m m行第 n n n列的元素改为 c c c&#xff0c;如果 m m m或 n n n超出原来的行或列&#xff0c;则会自动补充行或列&#xff0c;目标元素改为要求的&#xff0c;其余为 0 0 0 A ( m ) c A(m)c A(m)c将索引…

网络安全之内网安全

下面给出了应对企业内网安全挑战的10种策略。这10种策略即是内网的防御策略&#xff0c;同时也是一个提高大型企业网络安全的策略。 1、注意内网安全与网络边界安全的不同 内网安全的威胁不同于网络边界的威胁。网络边界安全技术防范来自Internet上的攻击&#xff0c;主要是防…

Python 爬虫入门教程:从零构建你的第一个网络爬虫

网络爬虫是一种自动化程序&#xff0c;用于从网站抓取数据。Python 凭借其丰富的库和简单的语法&#xff0c;是构建网络爬虫的理想语言。本文将带你从零开始学习 Python 爬虫的基本知识&#xff0c;并实现一个简单的爬虫项目。 1. 什么是网络爬虫&#xff1f; 网络爬虫&#x…

solr 远程命令执行 (CVE-2019-17558)

目录 漏洞描述 执行漏洞py脚本&#xff0c;取得shell连接 EXP 漏洞描述 Apache Velocity是一个基于Java的模板引擎&#xff0c;它提供了一个模板语言去引用由Java代码定义的对象。Velocity是Apache基金会旗下的一个开源软件项目&#xff0c;旨在确保Web应用程序在表示层和业…

数据库中的视图

数据库中的视图 什么是视图创建视图使⽤视图修改数据注意事项 删除视图视图的优点 什么是视图 视图是⼀个虚拟的表&#xff0c;它是基于⼀个或多个基本表或其他视图的查询结果集。视图本⾝不存储数 据&#xff0c;⽽是通过执⾏查询来动态⽣成数据。⽤户可以像操作普通表⼀样使…

爬虫实战:采集知乎XXX话题数据

目录 反爬虫的本意和其带来的挑战目标实战开发准备代码开发发现问题1. 发现问题[01]2. 发现问题[02] 解决问题1. 解决问题[01]2. 解决问题[02] 最终结果 结语 反爬虫的本意和其带来的挑战 在这个数字化时代社交媒体已经成为人们表达观点的重要渠道&#xff0c;对企业来说&…

springboot-vue excel上传导出

数据库 device_manage表 字段&#xff0c;id&#xff0c;workshop,device_number,device_name,device_model,warn_time,expired_time device_warn表 字段&#xff0c;id,warn_time,expired_time 后端 实体类格式 device_manage Data TableName("device_manage"…

【简单好抄保姆级教学】javascript调用本地exe程序(谷歌,edge,百度,主流浏览器都可以使用....)

javascript调用本地exe程序 详细操作步骤结果 详细操作步骤 在本地创建一个txt文件依次输入 1.指明所使用注册表编程器版本 Windows Registry Editor Version 5.00这是脚本的第一行&#xff0c;指明了所使用的注册表编辑器版本。这是必需的&#xff0c;以确保脚本能够被正确解…

Redis五大基本类型——Zset有序集合命令详解(命令用法详解+思维导图详解)

目录 一、Zset有序集合类型介绍 二、常见命令 1、ZADD 2、ZCARD 3、ZCOUNT 4、ZRANGE 5、ZREVRANGE 6、ZRANGEBYSCORE 7、ZREVRANGEBYSCORE 8、ZPOPMAX 9、ZPOPMIN 10、ZRANK 11、ZREVRANK 12、ZSCORE 13、ZREM 14、ZREMRANGEBYRANK 15、ZREMRANGEBYSCORE 16…

设计模式之 责任链模式

责任链模式&#xff08;Chain of Responsibility Pattern&#xff09;是一种行为型设计模式&#xff0c;旨在将多个处理对象通过链式结构连接起来&#xff0c;形成一条处理请求的链条。每个处理对象都有机会处理请求&#xff0c;或者将请求传递给链中的下一个对象。这样&#x…

新版布谷直播软件源码开发搭建功能更新明细

即将步入2025年也就是山东布谷科技专注直播系统开发,直播软件源码出售开发搭建等业务第9年,山东布谷科技不断更新直播软件功能&#xff0c;以适应当前新市场环境下的新要求。山东布谷科技始终秉承初心&#xff0c;做一款符合广大客户需求的直播系统软件。支持广大客户提交更多个…

VITE+VUE3+TS环境搭建

前言&#xff08;与搭建项目无关&#xff09;&#xff1a; 可以安装一个node管理工具&#xff0c;比如nvm&#xff0c;这样可以顺畅的切换vue2和vue3项目&#xff0c;以免出现项目跑不起来的窘境。我使用的nvm&#xff0c;当前node 22.11.0 目录 搭建项目 添加状态管理库&…

HTML飞舞的爱心

目录 系列文章 写在前面 完整代码 代码分析 写在后面 系列文章 序号目录1HTML满屏跳动的爱心&#xff08;可写字&#xff09;2HTML五彩缤纷的爱心3HTML满屏漂浮爱心4HTML情人节快乐5HTML蓝色爱心射线6HTML跳动的爱心&#xff08;简易版&#xff09;7HTML粒子爱心8HTML蓝色…

英伟达推出了全新的小型语言模型家族——Hymba 1.5B

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

低速接口项目之串口Uart开发(二)——FIFO实现串口数据的收发回环测试

本节目录 一、设计思路 二、loop环回模块 三、仿真模块 四、仿真验证 五、上板验证 六、往期文章链接本节内容 一、设计思路 串口数据的收发回环测试&#xff0c;最简单的硬件测试是把Tx和Rx连接在一起&#xff0c;然后上位机进行发送和接收测试&#xff0c;但是需要考虑到串…

《Java核心技术I》树集

树集 TreeSet类与散列类似&#xff0c;树集是一个有序集合(sorted collection)。 可以以任意顺序将元素插入到集合中&#xff0c;遍历集合时&#xff0c;自动按照排序后的顺序呈现。 插入5个字符串&#xff0c;访问添加的元素 package treeSet;import java.util.TreeSet;pu…