{"id":24838969,"url":"https://github.com/monitor1379/simplecache","last_synced_at":"2025-10-08T11:35:59.323Z","repository":{"id":57659362,"uuid":"267231609","full_name":"monitor1379/simplecache","owner":"monitor1379","description":"Golang implementation of Memory Cache.","archived":false,"fork":false,"pushed_at":"2020-05-28T04:05:18.000Z","size":20,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-01-31T06:36:57.025Z","etag":null,"topics":["cache","expire-cache","go","go-cache","golang","memory-cache"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/monitor1379.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-05-27T05:45:21.000Z","updated_at":"2022-05-16T11:27:16.000Z","dependencies_parsed_at":"2022-09-08T00:10:32.751Z","dependency_job_id":null,"html_url":"https://github.com/monitor1379/simplecache","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/monitor1379%2Fsimplecache","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/monitor1379%2Fsimplecache/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/monitor1379%2Fsimplecache/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/monitor1379%2Fsimplecache/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/monitor1379","download_url":"https://codeload.github.com/monitor1379/simplecache/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245589213,"owners_count":20640247,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cache","expire-cache","go","go-cache","golang","memory-cache"],"created_at":"2025-01-31T06:36:28.990Z","updated_at":"2025-10-08T11:35:54.274Z","avatar_url":"https://github.com/monitor1379.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# simplecache\n\nGolang implementation of Memory Cache.\n\nFeatures:\n- 接口使用简单。\n- 支持设置过期时间。采取Lazy Delete + 定期扫描的方式，高效删除过期键值。\n- 支持设置最大可用内存。内存淘汰策略目前仅支持`no-eviction`，即当内存占用超过最大限制时，插入操作会返回失败。后续逐步支持:\n    - `MaxMemoryPolicyTypeAllKeysRandom`: 从所有key中随机删除一个。\n\t- `MaxMemoryPolicyTypeVolatileRandom`: 从设置了过期时间的key中随机删除一个。\n    - `MaxMemoryPolicyTypeAllKeysLRU`: 从所有key中按照LRU删除最近最少使用的一个。\n\t- `MaxMemoryPolicyTypeVolatileLRU`: 从设置了过期时间的key中按照按照LRU删除最近最少使用的一个。\n\n## Installation \n\n```bash\ngo get -u -v github.com/monitor1379/simplecache\n```\n\n\n## Benchmark\n\n```bash\ncd ${GOPATH}/src/github.com/monitor1379/simplecache\ngo test -v -bench=. -run=none -benchmem mem_cache_test.go   \n```\n\n与内建`map`的插入操作比较:\n```\ngoos: linux\ngoarch: amd64\nBenchmarkMemCache_Set-8          9634270               113 ns/op              40 B/op          2 allocs/op\nBenchmarkMap_Set-8              43507314                26.2 ns/op             8 B/op          1 allocs/op\n```\n\n\n## Examples\n\n\n```go\npackage main\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/monitor1379/simplecache\"\n)\n\nfunc example01_SetGetDel() {\n\tcache := simplecache.New()\n\tcache.Set(\"k1\", \"v1\", 0)\n\n\tvalue, ok := cache.Get(\"k1\")\n\tfmt.Println(value, ok)\n\t// print: v1 true\n\n\tok = cache.Del(\"k1\")\n\tfmt.Println(ok)\n\t// print: true\n\n\tok = cache.Del(\"k1\")\n\tfmt.Println(ok)\n\t// print: false\n}\n\nfunc example02_Expire() {\n\tcache := simplecache.New()\n\tcache.Set(\"k1\", \"v1\", time.Millisecond*500) // expire 500ms\n\n\tvalue, ok := cache.Get(\"k1\")\n\tfmt.Println(value, ok)\n\t// print: v1 true\n\n\ttime.Sleep(time.Millisecond * 500)\n\tvalue, ok = cache.Get(\"k1\")\n\tfmt.Println(value, ok)\n\t// print: \u003cnil\u003e false\n}\n\nfunc example03_MaxMemory() {\n\tvar err error\n\n\t// default options: see github.com/monitor1379/simplecache/options.go::defaultOptions\n\tcache := simplecache.NewMemCacheWithOptions(simplecache.Options{\n\t\tIntervalOfProactivelyDeleteExpiredKey: time.Second * 1,\n\t\tMaxMemoryPolicyType:                   simplecache.MaxMemoryPolicyTypeNoeviction,\n\t})\n\tcache.SetMaxMemory(\"500B\")\n\n\tvalue := make([]byte, 1024) // 1KB\n\terr = cache.Set(\"k1\", value, 0)\n\tfmt.Println(err)\n\t// print: \"out of max memory\"\n\n\tcache.SetMaxMemory(\"1.2KB\")\n\terr = cache.Set(\"k1\", value, 1*time.Second)\n\tfmt.Println(err)\n\t// print: \u003cnil\u003e\n\n\terr = cache.Set(\"k2\", value, 0)\n\tfmt.Println(err)\n\t// print: \"out of max memory\"\n\n\ttime.Sleep(1 * time.Second)\n\n\terr = cache.Set(\"k2\", value, 0)\n\tfmt.Println(err)\n\t// print: \u003cnil\u003e\n\n}\n\nfunc example04_Others() {\n\tcache := simplecache.New()\n\tcache.Set(\"k1\", \"v1\", 0)\n\tcache.Set(\"k2\", \"v2\", 0)\n\n\tfmt.Println(cache.Keys())\n\t// print: 2\n\n\tcache.Flush()\n\n\tfmt.Println(cache.Keys())\n\t// print: 0\n}\n\nfunc main() {\n\texample01_SetGetDel()\n\texample02_Expire()\n\texample03_MaxMemory()\n\texample04_Others()\n}\n\n```\n\n\n\n# Development\n\n## 1. 数据结构\n\n`Cache`接口的实现为`MemCache`:\n```go\n\ntype MemCache struct {\n\toptions Options\n\n\t// 单位: bytes\n\tmaxMemory   int64\n\tmemoryUsage int64\n\n\t// 存储所有key-entry pair\n\tmu    sync.RWMutex\n\ttable map[string]*Entry\n\n\t// 存储所有设置了expire的key-entry pair，用于主动定期清理，所以用普通锁而不是读写锁\n\texpiredMu    sync.Mutex\n\texpiredTable map[string]*Entry\n}\n```\n\n\n`MemCache.table`为一个`map[string]*Entry`，其中，`Entry`定义为:\n\n```go\n\ntype Entry struct {\n\tvalue       interface{}\n\texpiredNano int64 // 过期时间戳。0表示永不过期\n\tvalueSize   int64 // sizeof(value)\n}\n\n```\n\n通过读写锁`sync.RWMutex`来实现对`MemCache`读写操作的并发安全。\n\n\n\n## 2. 过期时间的实现\n\n最简单的实现就是在`Set()`操作时，给需要检查过期时间的key加上一个`time.Ticker`，起一个goroutine去后台清理。大致的伪代码是:\n\n```go\n\nimport \"time\"\n\nfunc Set(key string, value interface{}, expire time.Duration) {\n\n    go func(expire int64) {\n        ticker := time.Ticker(expire)\n        for {\n            select {\n                case \u003c- ticker.C:\n                    Del(key)\n                    return \n            }\n        }\n\n    }(expire)\n}\n```\n\n但这种方法的缺点很明显，就是当key-value数量特别多的情况下（例如上百万个），就得起上百万个goroutine来执行定时器，明显不可行。\n\n所以`simplecache`中采取类似Redis的过期键值对删除方法，即Lazy Delete + 定期扫描。\n\n- Lazy Delete: 在`Get(key)`操作中，如果发现key已过期，则执行删除操作，然后返回`nil`。\n- 定期扫描: 维护另外一张表`expiredTable`，类型为`map[string]*Entry`。`Set()`操作中如果key设置了过期时间，则也插入到`expiredTable`中。后台协程每隔一定时间，锁上`table`和`expiredTable`，然后遍历`expiredTable`中的`Entry`并判断是否过期。如果过期，则从`table`和`expiredTable`中删除。\n\n\n至于这个时间间隔可以根据实际情况进行配置，默认是10秒:\n\n```go\n\nvar (\n\tdefaultOptions = Options{\n\t\tIntervalOfProactivelyDeleteExpiredKey: time.Second * 10,\n\t\tMaxMemoryPolicyType:                   MaxMemoryPolicyTypeNoeviction,\n\t}\n)\n\n\nfunc NewMemCache() *MemCache {\n\treturn NewMemCacheWithOptions(defaultOptions)\n}\n\nfunc NewMemCacheWithOptions(options Options) *MemCache {\n\tmc := new(MemCache)\n\tmc.options = options\n\tmc.table = make(map[string]*Entry)\n\tmc.expiredTable = make(map[string]*Entry)\n\n\tmc.SetMaxMemory(\"1MB\")\n\n\t// 后台协程主动定期清理过期key\n\tgo mc.backgroundCleanupExpiredKeys()\n\n\treturn mc\n}\n\n```\n\n当然这种方法也是有缺点的。不做分桶每次写入操作都得对整个`map`加写锁，当数据量大的时候瓶颈显而易见。一种改进的思路是hash+bucketing。就是实际上维护多个bucket，一个bucket一个map，通过对key进行hash来判断应该写入哪个bucket的map里，然后只对该bucket加写锁，这样可以提升整个`Cache`的写入性能。\n\n\n## 3. 最大内存占用限制与变量大小计算\n\n为了给Cache支持**最大内存占用限制**这个功能，我们需要能够获取变量的内存占用大小。\n\nGolang中没有直接提供获取变量占用内存大小的方法，但可以间接地计算一个大概的值:\n- 方法1: 将变量序列化成字节数组，以字节数组的长度作为变量的大致占用内存的大小。\n- 方法2: 递归地利用反射机制获取变量以及其成员变量（如果有的话）的大小。\n\n第一种方法可以用`gob`包实现，缺点是序列化耗时，且计算并不是非常准确。\n第二种方法对于层次结构较复杂的变量可能会递归耗时，但是计算相对准确。\n\n**Notes**: 此处的计算不考虑Golang内建的数据结构的内存占用。例如，对于一个`[1024]byte{}`，我们将他的内存占用大小看成是1024, 不考虑`slice`的底层实现。同理，对于`map`，我们只考虑累加所有key和value的字面意义上的大小，不考虑`map`内部底层实现所申请的内存。\n\n\n以下`Size()`函数返回一个变量的理论内存占用大小，单位为Byte。\n\n```go\n\n// file: github.com/monitor1379/simplecache/utils/variable.go\n\nfunc Sizeof(v interface{}) int64 {\n\tvv := reflect.ValueOf(v)\n\treturn sizeof(vv)\n}\n\nfunc sizeof(v reflect.Value) int64 {\n\tswitch v.Kind() {\n\tcase reflect.Invalid:\n\t\treturn 0\n\tcase reflect.Ptr:\n\t\treturn sizeof(v.Elem())\n\tcase reflect.Bool, reflect.Int8, reflect.Uint8:\n\t\treturn 1\n\tcase reflect.Int16, reflect.Uint16:\n\t\treturn 2\n\tcase reflect.Int, reflect.Uint, reflect.Int32, reflect.Uint32, reflect.Float32:\n\t\treturn 4\n\tcase reflect.Int64, reflect.Uint64, reflect.Float64, reflect.Complex64, reflect.Uintptr:\n\t\treturn 8\n\tcase reflect.Complex128:\n\t\treturn 16\n\tcase reflect.String:\n\t\treturn int64(v.Len())\n\tcase reflect.Slice, reflect.Array:\n\t\tif v.Len() \u003e 0 {\n\t\t\treturn int64(v.Len()) * sizeof(v.Index(0))\n\t\t}\n\t\treturn 0\n\tcase reflect.Map:\n\t\tvar s int64\n\t\tfor _, key := range v.MapKeys() {\n\t\t\ts += sizeof(key) + sizeof(v.MapIndex(key))\n\t\t}\n\t\treturn s\n\n\tcase reflect.Struct:\n\t\tvar s int64\n\t\tfor i := 0; i \u003c v.NumField(); i++ {\n\t\t\ts += sizeof(v.Field(i))\n\t\t}\n\t\treturn s\n\n\tdefault:\n\t\tpanic(\"unsupport variable type for calculating memory usage\")\n\t}\n\treturn 0\n}\n\n```\n\n\n\n## 内存淘汰策略\n\n\n当内存超过实现限制的最大内存数时，需要通过淘汰策略来保证`Cache`的继续可用。\n\n\n由于时间关系，目前实现先只支持`no-eviction`，即当内存占用超过最大限制时，插入操作会返回失败。\n\n后续逐步支持:\n- `MaxMemoryPolicyTypeAllKeysRandom`: 从所有key中随机删除一个。\n- `MaxMemoryPolicyTypeVolatileRandom`: 从设置了过期时间的key中随机删除一个。\n- `MaxMemoryPolicyTypeAllKeysLRU`: 从所有key中按照LRU删除最近最少使用的一个。\n- `MaxMemoryPolicyTypeVolatileLRU`: 从设置了过期时间的key中按照按照LRU删除最近最少使用的一个。\n\n\n\nLRU的方法实现不难，使用双向链表来维护key，每当`Get(key)`时，将该`key`对应的结点移动到双向链表的最末端。然后淘汰删除的时候，删除头结点即可。","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmonitor1379%2Fsimplecache","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmonitor1379%2Fsimplecache","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmonitor1379%2Fsimplecache/lists"}