Skip to content

monitor1379/simplecache

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

simplecache

Golang implementation of Memory Cache.

Features:

  • 接口使用简单。
  • 支持设置过期时间。采取Lazy Delete + 定期扫描的方式,高效删除过期键值。
  • 支持设置最大可用内存。内存淘汰策略目前仅支持no-eviction,即当内存占用超过最大限制时,插入操作会返回失败。后续逐步支持:
    • MaxMemoryPolicyTypeAllKeysRandom: 从所有key中随机删除一个。
    • MaxMemoryPolicyTypeVolatileRandom: 从设置了过期时间的key中随机删除一个。
    • MaxMemoryPolicyTypeAllKeysLRU: 从所有key中按照LRU删除最近最少使用的一个。
    • MaxMemoryPolicyTypeVolatileLRU: 从设置了过期时间的key中按照按照LRU删除最近最少使用的一个。

Installation

go get -u -v github.com/monitor1379/simplecache

Benchmark

cd ${GOPATH}/src/github.com/monitor1379/simplecache
go test -v -bench=. -run=none -benchmem mem_cache_test.go   

与内建map的插入操作比较:

goos: linux
goarch: amd64
BenchmarkMemCache_Set-8          9634270               113 ns/op              40 B/op          2 allocs/op
BenchmarkMap_Set-8              43507314                26.2 ns/op             8 B/op          1 allocs/op

Examples

package main

import (
	"fmt"
	"time"

	"github.com/monitor1379/simplecache"
)

func example01_SetGetDel() {
	cache := simplecache.New()
	cache.Set("k1", "v1", 0)

	value, ok := cache.Get("k1")
	fmt.Println(value, ok)
	// print: v1 true

	ok = cache.Del("k1")
	fmt.Println(ok)
	// print: true

	ok = cache.Del("k1")
	fmt.Println(ok)
	// print: false
}

func example02_Expire() {
	cache := simplecache.New()
	cache.Set("k1", "v1", time.Millisecond*500) // expire 500ms

	value, ok := cache.Get("k1")
	fmt.Println(value, ok)
	// print: v1 true

	time.Sleep(time.Millisecond * 500)
	value, ok = cache.Get("k1")
	fmt.Println(value, ok)
	// print: <nil> false
}

func example03_MaxMemory() {
	var err error

	// default options: see github.com/monitor1379/simplecache/options.go::defaultOptions
	cache := simplecache.NewMemCacheWithOptions(simplecache.Options{
		IntervalOfProactivelyDeleteExpiredKey: time.Second * 1,
		MaxMemoryPolicyType:                   simplecache.MaxMemoryPolicyTypeNoeviction,
	})
	cache.SetMaxMemory("500B")

	value := make([]byte, 1024) // 1KB
	err = cache.Set("k1", value, 0)
	fmt.Println(err)
	// print: "out of max memory"

	cache.SetMaxMemory("1.2KB")
	err = cache.Set("k1", value, 1*time.Second)
	fmt.Println(err)
	// print: <nil>

	err = cache.Set("k2", value, 0)
	fmt.Println(err)
	// print: "out of max memory"

	time.Sleep(1 * time.Second)

	err = cache.Set("k2", value, 0)
	fmt.Println(err)
	// print: <nil>

}

func example04_Others() {
	cache := simplecache.New()
	cache.Set("k1", "v1", 0)
	cache.Set("k2", "v2", 0)

	fmt.Println(cache.Keys())
	// print: 2

	cache.Flush()

	fmt.Println(cache.Keys())
	// print: 0
}

func main() {
	example01_SetGetDel()
	example02_Expire()
	example03_MaxMemory()
	example04_Others()
}

Development

1. 数据结构

Cache接口的实现为MemCache:

type MemCache struct {
	options Options

	// 单位: bytes
	maxMemory   int64
	memoryUsage int64

	// 存储所有key-entry pair
	mu    sync.RWMutex
	table map[string]*Entry

	// 存储所有设置了expire的key-entry pair,用于主动定期清理,所以用普通锁而不是读写锁
	expiredMu    sync.Mutex
	expiredTable map[string]*Entry
}

MemCache.table为一个map[string]*Entry,其中,Entry定义为:

type Entry struct {
	value       interface{}
	expiredNano int64 // 过期时间戳。0表示永不过期
	valueSize   int64 // sizeof(value)
}

通过读写锁sync.RWMutex来实现对MemCache读写操作的并发安全。

2. 过期时间的实现

最简单的实现就是在Set()操作时,给需要检查过期时间的key加上一个time.Ticker,起一个goroutine去后台清理。大致的伪代码是:

import "time"

func Set(key string, value interface{}, expire time.Duration) {

    go func(expire int64) {
        ticker := time.Ticker(expire)
        for {
            select {
                case <- ticker.C:
                    Del(key)
                    return 
            }
        }

    }(expire)
}

但这种方法的缺点很明显,就是当key-value数量特别多的情况下(例如上百万个),就得起上百万个goroutine来执行定时器,明显不可行。

所以simplecache中采取类似Redis的过期键值对删除方法,即Lazy Delete + 定期扫描。

  • Lazy Delete: 在Get(key)操作中,如果发现key已过期,则执行删除操作,然后返回nil
  • 定期扫描: 维护另外一张表expiredTable,类型为map[string]*EntrySet()操作中如果key设置了过期时间,则也插入到expiredTable中。后台协程每隔一定时间,锁上tableexpiredTable,然后遍历expiredTable中的Entry并判断是否过期。如果过期,则从tableexpiredTable中删除。

至于这个时间间隔可以根据实际情况进行配置,默认是10秒:

var (
	defaultOptions = Options{
		IntervalOfProactivelyDeleteExpiredKey: time.Second * 10,
		MaxMemoryPolicyType:                   MaxMemoryPolicyTypeNoeviction,
	}
)


func NewMemCache() *MemCache {
	return NewMemCacheWithOptions(defaultOptions)
}

func NewMemCacheWithOptions(options Options) *MemCache {
	mc := new(MemCache)
	mc.options = options
	mc.table = make(map[string]*Entry)
	mc.expiredTable = make(map[string]*Entry)

	mc.SetMaxMemory("1MB")

	// 后台协程主动定期清理过期key
	go mc.backgroundCleanupExpiredKeys()

	return mc
}

当然这种方法也是有缺点的。不做分桶每次写入操作都得对整个map加写锁,当数据量大的时候瓶颈显而易见。一种改进的思路是hash+bucketing。就是实际上维护多个bucket,一个bucket一个map,通过对key进行hash来判断应该写入哪个bucket的map里,然后只对该bucket加写锁,这样可以提升整个Cache的写入性能。

3. 最大内存占用限制与变量大小计算

为了给Cache支持最大内存占用限制这个功能,我们需要能够获取变量的内存占用大小。

Golang中没有直接提供获取变量占用内存大小的方法,但可以间接地计算一个大概的值:

  • 方法1: 将变量序列化成字节数组,以字节数组的长度作为变量的大致占用内存的大小。
  • 方法2: 递归地利用反射机制获取变量以及其成员变量(如果有的话)的大小。

第一种方法可以用gob包实现,缺点是序列化耗时,且计算并不是非常准确。 第二种方法对于层次结构较复杂的变量可能会递归耗时,但是计算相对准确。

Notes: 此处的计算不考虑Golang内建的数据结构的内存占用。例如,对于一个[1024]byte{},我们将他的内存占用大小看成是1024, 不考虑slice的底层实现。同理,对于map,我们只考虑累加所有key和value的字面意义上的大小,不考虑map内部底层实现所申请的内存。

以下Size()函数返回一个变量的理论内存占用大小,单位为Byte。

// file: github.com/monitor1379/simplecache/utils/variable.go

func Sizeof(v interface{}) int64 {
	vv := reflect.ValueOf(v)
	return sizeof(vv)
}

func sizeof(v reflect.Value) int64 {
	switch v.Kind() {
	case reflect.Invalid:
		return 0
	case reflect.Ptr:
		return sizeof(v.Elem())
	case reflect.Bool, reflect.Int8, reflect.Uint8:
		return 1
	case reflect.Int16, reflect.Uint16:
		return 2
	case reflect.Int, reflect.Uint, reflect.Int32, reflect.Uint32, reflect.Float32:
		return 4
	case reflect.Int64, reflect.Uint64, reflect.Float64, reflect.Complex64, reflect.Uintptr:
		return 8
	case reflect.Complex128:
		return 16
	case reflect.String:
		return int64(v.Len())
	case reflect.Slice, reflect.Array:
		if v.Len() > 0 {
			return int64(v.Len()) * sizeof(v.Index(0))
		}
		return 0
	case reflect.Map:
		var s int64
		for _, key := range v.MapKeys() {
			s += sizeof(key) + sizeof(v.MapIndex(key))
		}
		return s

	case reflect.Struct:
		var s int64
		for i := 0; i < v.NumField(); i++ {
			s += sizeof(v.Field(i))
		}
		return s

	default:
		panic("unsupport variable type for calculating memory usage")
	}
	return 0
}

内存淘汰策略

当内存超过实现限制的最大内存数时,需要通过淘汰策略来保证Cache的继续可用。

由于时间关系,目前实现先只支持no-eviction,即当内存占用超过最大限制时,插入操作会返回失败。

后续逐步支持:

  • MaxMemoryPolicyTypeAllKeysRandom: 从所有key中随机删除一个。
  • MaxMemoryPolicyTypeVolatileRandom: 从设置了过期时间的key中随机删除一个。
  • MaxMemoryPolicyTypeAllKeysLRU: 从所有key中按照LRU删除最近最少使用的一个。
  • MaxMemoryPolicyTypeVolatileLRU: 从设置了过期时间的key中按照按照LRU删除最近最少使用的一个。

LRU的方法实现不难,使用双向链表来维护key,每当Get(key)时,将该key对应的结点移动到双向链表的最末端。然后淘汰删除的时候,删除头结点即可。

Releases

No releases published

Packages

No packages published

Languages