46.go实现一个本地内存缓存系统

代码地址:https://gitee.com/lymgoforIT/golang-trick/tree/master/27-memCache

一、简介

主要功能:

  • 支持设定过期时间,精度到秒
  • 支持定期删除过期key
  • 支持设定最大内存,当内存超出时做出合适的处理
  • 支持并发安全

接口概览

package cache

import "time"

type Cache interface {
	// size: 1KB 100KB 1MB 2MB 1GB 1TB
	// 设置最大缓存,支持传入KB,MB,GB...等内存单位
	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、定义结构体实现接口

如下代码,定义了结构体memCache,且实现了Cache接口,但是我们可能看到,该结构体还没有任何字段,方法也基本都是空方法,接下来我们会一步一步补全。

package cache

import "time"

type MemCache struct {
}

func (mc *MemCache) SetMaxMemory(size string) bool {
	return false
}

func (mc *MemCache) Set(key string, val interface{}, expire time.Duration) bool {
	return false
}

func (mc *MemCache) Get(key string) (interface{}, bool) {
	return nil, false
}

func (mc *MemCache) Del(key string) bool {
	return false
}

func (mc *MemCache) Exists(key string) bool {
	return false
}

func (mc *MemCache) Flush() bool {
	return false
}

func (mc *MemCache) Keys() int64 {
	return 0
}

2、设置最大内存方法SetMaxMemory的实现

  • 首先要使用内存缓存,第一步肯定是需要拿到内存缓存MemCache对象,所以我们可以提供一个NewMemCache方法。
  • 此外,需要能够设置最大内存,那么MemCache就需要有能记录最大内存的字段maxMemorySize,同时也需要有一个字段currMemorySize记录当前已经使用了多少内存,从而可以和最大内存比较,达到最大内存时根据指定的策略做相应的处理,如LRU,LFU等。
  • 最后,从接口层面来看,设置最大内存时传入的是带KB、MB等单位的内存大小,所以我们可以提供一个maxMemorySizeStr字段,用于存储最大内存的字符串表示,用于设置最大内存,以及后续如果有需要获取最大内存表示,可以直接返回这个字符串表示,而不需要用maxMemorySize去算出一个带单位的内存表示了。

代码如下:其他暂无变更的代码省略

type MemCache struct {
	// 最大内存
	maxMemorySize int64
	// 最大内存的字符串表示
	maxMemorySizeStr string
	// 当前已经使用的内存
	currMemorySize int64
}

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

接下来具体实现设置最大内存的方法,传入带单位的大小,然后我们需要在内部转换为数字字节表示的大小,由于输入的内存大小格式可能不对,此时我们应该给一个默认的内存大下,如下:

// size: 1KB 100KB 1MB 2MB 1GB 1TB
// 设置最大缓存,支持传入KB,MB,GB...等内存单位
func (mc *MemCache) SetMaxMemory(size string) bool {
	mc.maxMemorySize, mc.maxMemorySizeStr = ParseSize(size)
	//fmt.Println(mc.maxMemorySize)
	//fmt.Println(mc.maxMemorySizeStr)
	return true
}

主要逻辑在ParseSize函数中,我们将其放到util包中,目前文件目录结构如下:
在这里插入图片描述

解析设置的内存大小的函数逻辑如下,里面有几个知识点可以学习借鉴:

  • 正则表达式与字符串替换
  • 常量值定义
  • 防御式编程,外部输入错误时,定义默认处理,如此处默认的100MB

详细信息可看代码注释:

package cache

import (
	"log"
	"regexp"
	"strconv"
	"strings"
)

// 内存单位之间的量级都是2^10次方,所以很适合用iota定义常量
const (
	B = 1 << (iota * 10)
	KB
	MB
	GB
	TB
	PB
)

const DefaultMaxMemorySizeStr = "100MB"

func ParseSize(size string) (int64, string) {
	// 默认大小为100MB
	// 取出数字部分
	re, _ := regexp.Compile("[0-9]+")
	// 将匹配的数字部分替换为空字符串后便是单位
	unit := string(re.ReplaceAll([]byte(size), []byte("")))
	// 同样,将单位部分替换为空字符串就是数字部分,第四个参数为替换几处,输入正确时肯定是1处,输入错误则按默认100MB处理
	// 转换过程中如果出现错误也可以忽略,num会为0,后面按默认100MB处理
	num, _ := strconv.ParseInt(strings.Replace(size, unit, "", 1), 10, 64)

	// 将单位统一转换为大写后,进行switch匹配
	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:
		num = 0
	}

	// 输入有误,num解析不出来会为0,单位错误,num也会为0
	if num == 0 {
		log.Println("ParseSize 仅支持 B、KB、MB、GB、TB、PB")
		byteNum = 100 * MB
		size = DefaultMaxMemorySizeStr
	}
	return byteNum, size
}

不用等代码全部写完才测试,我们可以写完一个小功能就测试一下,如现在就可以测试一下设置最大内存的方法是否成功。

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

}

输出如下:符合预期,实际测了更多case,这里就不贴所有图了。
在这里插入图片描述

3、设置值Set方法的实现

设置值时,我们第一反应就应该是值存到那里去呢?用什么结构存呢?很明显内存是key-value结构,那么用map存是最自然的,keystring类型,valueinterface{}。此外,在并发情况下map是不安全的,所以需要考虑加锁,且用到缓存的场景一般是读多写少,所以考虑用读写锁。(PS:如果读和写的量级差不多,读写锁对性能帮助不大,与互斥锁性能相差无几)

valueinterface{}类型真的OK吗?

  • 首先,这样的话过期时间存哪呢?过期时间可是和key绑定的,所以需要和key对应的value存一起才对
  • 此外,我们的内存对象时支持设置过期时间,但是也可以不设置过期时间呀,比如设置过期时间为0表示永久有效。
  • 最后,我们还需要记录这个value占的内存大小,从而在设置和删除key时更新内存对象的当前所用内存。
  • 综上,我们应该定义一个结构体类型作为value的类型,该结构体包含val值,val所占内存大小,过期时间点,过期时间大小等字段。

根据上述描述,结构体内容字段增长如下,注意NewMemCache方法中也加入了对values这个map的初始化,不然使用未初始化的map是会panic

type MemCache struct {
	// 最大内存
	maxMemorySize int64
	// 最大内存的字符串表示
	maxMemorySizeStr string
	// 当前已经使用的内存
	currMemorySize int64
	// 缓存键值对
	values map[string]*MemCacheValue
	// 保证并发安全的读写锁
	locker sync.RWMutex
}

type MemCacheValue struct {
	// value 值
	val interface{}
	// value所占内存大小
	size int64
	// 过期时间点
	expireTime time.Time
	// 过期时间时长,单位:秒 0表示永久有效
	expire int64
}

func NewMemCache() Cache {
	return &MemCache{
		values: make(map[string]*MemCacheValue),
	}
}

字段想清楚后,我们可以来看设置值的Set方法了。Set方法包含了新增和更新两个操作,对于新增不必过多解释。对于更新我们有个编程中常用的技巧,因为最里层的value是interface,可能是map,slice,struct等,如果实际去更新里面的某个元素或字段,需要断言,遍历,反射等,且要准确更新整个内存对象MemCache最新的所使用内存也很复杂。有个技巧就是直接删除原来的key,然后设置一个新key,就相当于是更新操作了。即修改 = 删除 + 添加

注意:如下代码中我们添加了三个本包可见的辅助方法,get,del,add,缓存其他的方法可能也会用到这几个方法的

// 将value写入缓存,支持传入过期时间
func (mc *MemCache) Set(key string, val interface{}, expire time.Duration) bool {
	mc.locker.Lock()
	defer mc.locker.Unlock()
	// 构建Value
	v := &MemCacheValue{
		val:        val,
		size:       GetValSize(val),
		expireTime: time.Now().Add(expire),
		expire:     int64(expire),
	}

	mc.del(key)
	mc.add(key,v)
	// 每次设置值,所占内存大小都可能变化,所以每次设置完后,需要判断是否超出了最大内存
	// 如果超出,需要根据策略做相应处理,如LRU,LFU等,这里为了方便演示,直接删除当前key,类似丢弃策略
	// 等定时任务删除过期了的key后,后续的新key就还是可以设置成功的
	if mc.currMemorySize > mc.maxMemorySize {
		mc.del(key)
		log.Println(fmt.Sprintf("max memory size limit maxMemorySize:%d,curMemorySize:%d",mc.maxMemorySize,mc.currMemorySize))
		return false
	}
	return true
}

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

func (mc *MemCache) del(key string)  {
	val, ok := mc.get(key)
	if ok && val != nil {
		delete(mc.values,key)
		// 更新内存对象当前所占空间大小
		mc.currMemorySize -= val.size
	}
	
}

func (mc *MemCache) add(key string,val *MemCacheValue ) {
	mc.values[key] = val
	// 更新内存对象当前所占空间大小
	mc.currMemorySize += val.size
	
}

在上面代码中,我们还用到了一个计算val所占内存大小的工具方法GetValSize,现在我们来实现一下,val是里面包含interface{},准确计算其所占字节是比较复杂的,我们不需要那么准确,所以有一个可替代的方案。那就是将原对象序列化为JSON,计算JSON字符串所占用字节数即可,计算结果就是会比原对象多出一些逗号,引号,大括号的大小,但是在可接受范围内。

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

4、获取值Get方法的实现

获取值有三个小知识点:

  • 获取值使用读锁
  • 使用快乐路径原则,让代码更优雅
  • 获取时,如果值有过期时间,需要判断key是否已经过期,如果过期了可以删除key,而不能将一个过期的key返回出去哦(虽然我们后面会写定时任务去删除过期key,但是比如定时时间为每隔5秒执行一次,但是在下一次定时任务执行之前,就来读取了一个过期的key,这个key就是还没有被定时任务删除,那么我们可以在此次读取中就将其删除,不必等到定时任务才来删除它了,毕竟它确实已经过期该删除了)
func (mc *MemCache) Get(key string) (interface{}, bool) {
	mc.locker.RLock()
	defer mc.locker.RUnlock()
	val ,ok := mc.values[key]
	// 快乐路径原则,将一些错误提前返回退出
	if !ok { 
		return nil,false
	}
	// val并非永久有效,即设置了过期时间,且过期时间已经过了,应该删除该key
	if val.expire != 0 && val.expireTime.Before(time.Now()){
		mc.del(key)
		return nil ,false
	}
	return val.val, true
}

5、删除值Del方法的实现

删除操作比较简单,但是要注意加写锁哦

func (mc *MemCache) Del(key string) bool {
	mc.locker.Lock()
	defer mc.locker.Unlock()
	mc.del(key)
	return true
}

6、判断key是否存在

比较简单,直接看代码吧

func (mc *MemCache) Exists(key string) bool {
	mc.locker.RLock()
	defer mc.locker.RUnlock()
	_, ok := mc.get(key)
	return ok
}

7、清空所有key

values置为空的map即可,老的map会被垃圾回收机制回收。然后更新下当前所占内存为0就行了。

func (mc *MemCache) Flush() bool {
	mc.locker.Lock()
	defer mc.locker.Unlock()
	mc.values = make(map[string]*MemCacheValue)
	mc.currMemorySize = 0
	return true
}

8、获取总key数量

func (mc *MemCache) Keys() int64 {
	mc.locker.RLock()
	defer mc.locker.RUnlock()
	return int64(len(mc.values))
}

9、开启协程定期清空过期key

  • 要定期清空key,那么这定期是多久呢?所以需要继续给MemCache添加字段clearExpiredKeyTimeInterval,这里为了方便直接使用默认值1秒了,实际工作中,可以根据需要让使用者自己设置,就像设置最大内存一样。
  • 既然是定期清理,那就需要开启单独的协程去处理,类似一个守护协程。在创建对象的时候就启动这个协程。

结构体MemCache MemCacheValue方法 更新如下:

type MemCache struct {
	// 最大内存
	maxMemorySize int64
	// 最大内存的字符串表示
	maxMemorySizeStr string
	// 当前已经使用的内存
	currMemorySize int64
	// 缓存键值对
	values map[string]*MemCacheValue
	// 保证并发安全的读写锁
	locker sync.RWMutex
	// 清楚过期缓存key的时间间隔
	clearExpiredKeyTimeInterval time.Duration
}

type MemCacheValue struct {
	// value 值
	val interface{}
	// value所占内存大小
	size int64
	// 过期时间点
	expireTime time.Time
	// 过期时间时长,单位:秒 0表示永久有效
	expire int64
}

func NewMemCache() Cache {
	memCache :=  &MemCache{
		clearExpiredKeyTimeInterval: time.Second,
		values: make(map[string]*MemCacheValue),
	}
	// 开启协程清除过期key
	go memCache.clearExpiredKey()
	return memCache
}

清空过期key的方法

func (mc *MemCache) clearExpiredKey() {
	timeTicker := time.NewTicker(mc.clearExpiredKeyTimeInterval)
	defer timeTicker.Stop()

	for {
		select {
		case <-timeTicker.C:
			for key, val := range mc.values {
				if val.expire != 0 && val.expireTime.Before(time.Now()) {
					mc.locker.Lock()
					mc.del(key)
					// 注意这里不能写成defer mc.locker.Unlock(),因为defer是在return前执行的,但是我们这个函数时无限循环的,defer没有机会执行
					mc.locker.Unlock()
				}
			}
		}
	}
}

测试:

func main() {
	memCache := cache.NewMemCache()
	memCache.SetMaxMemory("300MB")
	// 设置name : zhangsan 过期时间3秒
	memCache.Set("name", "zhangsan", 3 * time.Second)
	// 获取key,并打印
	val, _ := memCache.Get("name")
	fmt.Printf("%+v\n", val)
	fmt.Println(memCache.Keys())

	// 休眠4秒后,name应该已经被过期删除了
	time.Sleep(4 * time.Second)
	val, _ = memCache.Get("name")
	fmt.Printf("%#v\n", val)
	fmt.Println(memCache.Keys())

}

输出:
在这里插入图片描述

三、如何使用

在第二部分的时候,已经简单的使用了一下,即我们开发的缓存系统已经是可以使用的了,但是这里为什么还要介绍如何使用呢?原因是想介绍一下适配器模式。如:需要按照如下方式使用我们的缓存怎么办呢?其中的Set方法只传了两个参数,但是我们开发的缓存系统要求是要传三个参数的呀(这里省略的第三个参数为过期时间,即想实现不传过期时间则认为是永久有效)

memCache := cache.NewMemCache()
memCache.SetMaxMemory("200MB")
memCache.Set("int",1)
memCache.Set("bool",false)
memCache.Set("mapData",map[string]interface{}{"a":1})
memCache.Get("int")
memCache.Del("int")
memCache.Flush()
memCache.Keys()

适配器模式

适配器模式很常见的一个场景就是,在不能修改第三方库的情况下,又想扩展第三方库的功能,此时可以定义一个结构体,包含第三方库的对象作为自身字段,然后很多事情就可以委托这个第三方库的对象做了。具体可参考本人博客:结构型之适配器模式

这里我们也是假设在不能修改已经开发好的缓存系统的情况下,要实现上面的用法,那就可以使用适配器模式实现了。

注意看下面代码的注释即可:其中Set方法用到了可变长参惯用技巧

package cache_server

import (
	"golang-trick/27-memCache/cache"
	"time"
)

// 注意:本案例中CacheServer并没有与MemCache实现相同的接口Cache
// 因为CacheServer 的方法需要可变参数,与Cache接口的Set方法不一样
type CacheServer struct {
	// 1. 包含一个要适配的结构体对象(实际也是赋值给了接口),后期很多工作委托它做
	memCache cache.Cache
}

// 2. 提供构造方法
func NewCacheServer() *CacheServer {
	// memCache是cache.Cache类型,但是底层实际工作的cache.MemCache
	return &CacheServer{memCache: cache.NewMemCache()}
}

func (c CacheServer) SetMaxMemory(size string) bool {
	// 3. 委托
	return c.memCache.SetMaxMemory(size)
}

// Go 语言中不支持默认参数,所以想要实现参数可传可不传的解决办法是使用可变长参数
func (c CacheServer) Set(key string, val interface{}, expire ...time.Duration) bool {
	expireTs := time.Second * 0 // 默认是0,没有过期时间
	if len(expire) > 0 {
		expireTs = expire[0] // 即使传了多个参数,我们也只取第一个
	}
	// 3. 委托
	return c.memCache.Set(key, val, expireTs)
}

func (c CacheServer) Get(key string) (interface{}, bool) {
	// 3. 委托
	return c.memCache.Get(key)
}

func (c CacheServer) Del(key string) bool {
	// 3. 委托
	return c.memCache.Del(key)
}

func (c CacheServer) Exists(key string) bool {
	// 3. 委托
	return c.memCache.Exists(key)
}

func (c CacheServer) Flush() bool {
	// 3. 委托
	return c.memCache.Flush()
}

func (c CacheServer) Keys() int64 {
	// 3. 委托
	return c.memCache.Keys()
}

运行看看不报错

package main

import (
	"golang-trick/27-memCache/cache_server"
)

func main() {
	//memCache := cache.NewMemCache()
	//memCache.SetMaxMemory("300MB")
	 设置name : zhangsan 过期时间3秒
	//memCache.Set("name", "zhangsan", 3)
	 获取key,并打印
	//val, _ := memCache.Get("name")
	//fmt.Printf("%+v\n", val)
	//fmt.Println(memCache.Keys())
	//
	 休眠4秒后,name应该已经被过期删除了
	//time.Sleep(4 * time.Second)
	//val, _ = memCache.Get("name")
	//fmt.Printf("%#v\n", val)
	//fmt.Println(memCache.Keys())

	memCache := cache_server.NewCacheServer()
	memCache.SetMaxMemory("200MB")
	memCache.Set("int", 1)
	memCache.Set("bool", false)
	memCache.Set("mapData", map[string]interface{}{"a": 1})
	memCache.Get("int")
	memCache.Del("int")
	memCache.Flush()
	memCache.Keys()

}

至此,我们的缓存系统就介绍完啦!!

四、完整代码

本缓存系统代码结构如下:
在这里插入图片描述

cache/cache.go

主要是定义了接口

package cache

import "time"

type Cache interface {
	// size: 1KB 100KB 1MB 2MB 1GB 1TB
	// 设置最大缓存,支持传入KB,MB,GB...等内存单位
	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
}

cache/memCache.go

是接口的具体实现

package cache

import (
	"fmt"
	"log"
	"sync"
	"time"
)

type MemCache struct {
	// 最大内存
	maxMemorySize int64
	// 最大内存的字符串表示
	maxMemorySizeStr string
	// 当前已经使用的内存
	currMemorySize int64
	// 缓存键值对
	values map[string]*MemCacheValue
	// 保证并发安全的读写锁
	locker sync.RWMutex
	// 清楚过期缓存key的时间间隔
	clearExpiredKeyTimeInterval time.Duration
}

type MemCacheValue struct {
	// value 值
	val interface{}
	// value所占内存大小
	size int64
	// 过期时间点
	expireTime time.Time
	// 过期时间时长,单位:秒 0表示永久有效
	expire int64
}

func NewMemCache() Cache {
	memCache := &MemCache{
		clearExpiredKeyTimeInterval: time.Second,
		values:                      make(map[string]*MemCacheValue),
	}
	go memCache.clearExpiredKey()
	return memCache
}

// size: 1KB 100KB 1MB 2MB 1GB 1TB
// 设置最大缓存,支持传入KB,MB,GB...等内存单位
func (mc *MemCache) SetMaxMemory(size string) bool {
	mc.maxMemorySize, mc.maxMemorySizeStr = ParseSize(size)
	//fmt.Println(mc.maxMemorySize)
	//fmt.Println(mc.maxMemorySizeStr)
	return true
}

// 将value写入缓存,支持传入过期时间
func (mc *MemCache) Set(key string, val interface{}, expire time.Duration) bool {
	mc.locker.Lock()
	defer mc.locker.Unlock()
	// 构建Value
	v := &MemCacheValue{
		val:        val,
		size:       GetValSize(val),
		expireTime: time.Now().Add(expire),
		expire:     int64(expire),
	}

	mc.del(key)
	mc.add(key, v)
	// 每次设置值,所占内存大小都可能变化,所以每次设置完后,需要判断是否超出了最大内存
	// 如果超出,需要根据策略做相应处理,如LRU,LFU等,这里为了方便演示,直接删除当前key,类似丢弃策略
	// 等定时任务删除过期了的key后,后续的新key就还是可以设置成功的
	if mc.currMemorySize > mc.maxMemorySize {
		mc.del(key)
		log.Println(fmt.Sprintf("max memory size limit maxMemorySize:%d,curMemorySize:%d", mc.maxMemorySize, mc.currMemorySize))
		return false
	}
	return true
}

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

func (mc *MemCache) del(key string) {
	val, ok := mc.get(key)
	if ok && val != nil {
		delete(mc.values, key)
		// 更新内存对象当前所占空间大小
		mc.currMemorySize -= val.size
	}

}

func (mc *MemCache) add(key string, val *MemCacheValue) {
	mc.values[key] = val
	// 更新内存对象当前所占空间大小
	mc.currMemorySize += val.size

}

func (mc *MemCache) Get(key string) (interface{}, bool) {
	mc.locker.RLock()
	defer mc.locker.RUnlock()
	val, ok := mc.get(key)
	// 快乐路径原则,将一些错误提前返回退出
	if !ok {
		return nil, false
	}
	// val并非永久有效,即设置了过期时间,且过期时间已经过了,应该删除该key
	if val.expire != 0 && val.expireTime.Before(time.Now()) {
		mc.del(key)
		return nil, false
	}
	return val.val, true
}

func (mc *MemCache) Del(key string) bool {
	mc.locker.Lock()
	defer mc.locker.Unlock()
	mc.del(key)
	return true
}

func (mc *MemCache) Exists(key string) bool {
	mc.locker.RLock()
	defer mc.locker.RUnlock()
	_, ok := mc.get(key)
	return ok
}

func (mc *MemCache) Flush() bool {
	mc.locker.Lock()
	defer mc.locker.Unlock()
	mc.values = make(map[string]*MemCacheValue)
	mc.currMemorySize = 0
	return true
}

func (mc *MemCache) Keys() int64 {
	mc.locker.RLock()
	defer mc.locker.RUnlock()
	return int64(len(mc.values))
}

func (mc *MemCache) clearExpiredKey() {
	timeTicker := time.NewTicker(mc.clearExpiredKeyTimeInterval)
	defer timeTicker.Stop()

	for {
		select {
		case <-timeTicker.C:
			for key, val := range mc.values {
				if val.expire != 0 && val.expireTime.Before(time.Now()) {
					mc.locker.Lock()
					mc.del(key)
					// 注意这里不能写成defer mc.locker.Unlock(),因为defer是在return前执行的,但是我们这个函数时无限循环的,defer没有机会执行
					mc.locker.Unlock()
				}
			}
		}
	}
}

cache/util.go

接口具体实现中抽象出来的工具函数

package cache

import (
	"encoding/json"
	"log"
	"regexp"
	"strconv"
	"strings"
)

// 内存单位之间的量级都是2^10次方,所以很适合用iota定义常量
const (
	B = 1 << (iota * 10)
	KB
	MB
	GB
	TB
	PB
)

const DefaultMaxMemorySizeStr = "100MB"

func ParseSize(size string) (int64, string) {
	// 默认大小为100MB
	// 取出数字部分
	re, _ := regexp.Compile("[0-9]+")
	// 将匹配的数字部分替换为空字符串后便是单位
	unit := string(re.ReplaceAll([]byte(size), []byte("")))
	// 同样,将单位部分替换为空字符串就是数字部分,第四个参数为替换几处,输入正确时肯定是1处,输入错误则按默认100MB处理
	// 转换过程中如果出现错误也可以忽略,num会为0,后面按默认100MB处理
	num, _ := strconv.ParseInt(strings.Replace(size, unit, "", 1), 10, 64)

	// 将单位统一转换为大写后,进行switch匹配
	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:
		num = 0
	}

	// 输入有误,num解析不出来会为0,单位错误,num也会为0
	if num == 0 {
		log.Println("ParseSize 仅支持 B、KB、MB、GB、TB、PB")
		byteNum = 100 * MB
		size = DefaultMaxMemorySizeStr
	}
	return byteNum, size
}

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

cache_server/cache.go

介绍适配器模式,对memCache在使用上进行了有一层封装,主要是针对Set方法,可以不传过期时间,作为永久有效

package cache_server

import (
	"golang-trick/27-memCache/cache"
	"time"
)

// 注意:本案例中CacheServer并没有与MemCache实现相同的接口Cache
// 因为CacheServer 的方法需要可变参数,与Cache接口的Set方法不一样
type CacheServer struct {
	// 1. 包含一个要适配的结构体对象(实际也是赋值给了接口),后期很多工作委托它做
	memCache cache.Cache
}

// 2. 提供构造方法
func NewCacheServer() *CacheServer {
	// memCache是cache.Cache类型,但是底层实际工作的cache.MemCache
	return &CacheServer{memCache: cache.NewMemCache()}
}

func (c CacheServer) SetMaxMemory(size string) bool {
	// 3. 委托
	return c.memCache.SetMaxMemory(size)
}

// Go 语言中不支持默认参数,所以想要实现参数可传可不传的解决办法是使用可变长参数
func (c CacheServer) Set(key string, val interface{}, expire ...time.Duration) bool {
	expireTs := time.Second * 0 // 默认是0,没有过期时间
	if len(expire) > 0 {
		expireTs = expire[0] // 即使传了多个参数,我们也只取第一个
	}
	// 3. 委托
	return c.memCache.Set(key, val, expireTs)
}

func (c CacheServer) Get(key string) (interface{}, bool) {
	// 3. 委托
	return c.memCache.Get(key)
}

func (c CacheServer) Del(key string) bool {
	// 3. 委托
	return c.memCache.Del(key)
}

func (c CacheServer) Exists(key string) bool {
	// 3. 委托
	return c.memCache.Exists(key)
}

func (c CacheServer) Flush() bool {
	// 3. 委托
	return c.memCache.Flush()
}

func (c CacheServer) Keys() int64 {
	// 3. 委托
	return c.memCache.Keys()
}

main.go

用于简单测试

package main

import (
	"golang-trick/27-memCache/cache_server"
)

func main() {
	//memCache := cache.NewMemCache()
	//memCache.SetMaxMemory("300MB")
	 设置name : zhangsan 过期时间3秒
	//memCache.Set("name", "zhangsan", 3)
	 获取key,并打印
	//val, _ := memCache.Get("name")
	//fmt.Printf("%+v\n", val)
	//fmt.Println(memCache.Keys())
	//
	 休眠4秒后,name应该已经被过期删除了
	//time.Sleep(4 * time.Second)
	//val, _ = memCache.Get("name")
	//fmt.Printf("%#v\n", val)
	//fmt.Println(memCache.Keys())

	memCache := cache_server.NewCacheServer()
	memCache.SetMaxMemory("200MB")
	memCache.Set("int", 1)
	memCache.Set("bool", false)
	memCache.Set("mapData", map[string]interface{}{"a": 1})
	memCache.Get("int")
	memCache.Del("int")
	memCache.Flush()
	memCache.Keys()

}

五、单元测试

package cache

import (
	"fmt"
	"testing"
	"time"
)

func TestMemCache(t *testing.T) {
	testData := []struct {
		key    string
		val    interface{}
		expire time.Duration
	}{
		// key的过期时间故意设置成10S以上,从而在10S前测试其他case,避免有key自动过期被删除了
		{"int", 678, time.Second * 10},
		{"bool", false, time.Second * 11},
		{"map", map[string]interface{}{"a": 3, "b": false}, time.Second * 15},
		{"string", "该val是字符串", time.Second * 16},
	}
	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 != "map" && item.val != val {
			fmt.Println(item.val)
			fmt.Println(val)
			t.Error("存入的值与取出的值不一样")
		}
		// map 类型无法直接比较值,所以我们这里就简单判断下,是否能断言成map
		if item.key == "map" {
			_, ok1 := val.(map[string]interface{})
			if !ok1 {
				t.Error("存入的值与取出的值不一样")
			}
		}
	}

	// 测试key的数量是否一致
	if int64(len(testData)) != c.Keys() {
		t.Error("缓存key的数量不一致")
	}

	// 删除一个key后继续比较key的数量,从而判断是否删除成功
	c.Del(testData[0].key)
	if int64(len(testData)) != c.Keys()+1 {
		t.Error("缓存key的数量不一致")
	}

	// 休眠12秒后,应该只会剩下过期时间为15以及16的两个key了,12S前的两个key会被过期自动删除
	time.Sleep(12 * time.Second)
	if c.Keys() != 2 {
		t.Error("过期自动清除功能有bug")
	}

	// 清空所有key,验证清空功能
	c.Flush()
	if c.Keys() != 0 {
		t.Error("清除所有key的功能有bug")
	}
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值