文章目录
代码地址: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
存是最自然的,key
为string
类型,value
为interface{}
。此外,在并发情况下map
是不安全的,所以需要考虑加锁,且用到缓存的场景一般是读多写少,所以考虑用读写锁。(PS
:如果读和写的量级差不多,读写锁对性能帮助不大,与互斥锁性能相差无几)
value
为interface{}
类型真的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")
}
}