diff --git a/gcache/adapter_file.go b/gcache/adapter_file.go deleted file mode 100644 index 3011c71..0000000 --- a/gcache/adapter_file.go +++ /dev/null @@ -1,10 +0,0 @@ -// -// adapter_file.go -// Copyright (C) 2022 tiglog -// -// Distributed under terms of the MIT license. -// - -package gcache - -// 本地文件缓存 diff --git a/gcache/adapter_local.go b/gcache/adapter_local.go deleted file mode 100644 index 4cd5c88..0000000 --- a/gcache/adapter_local.go +++ /dev/null @@ -1,48 +0,0 @@ -// -// adapter_local.go -// Copyright (C) 2022 tiglog -// -// Distributed under terms of the MIT license. -// - -package gcache - -import ( - "sync" - "time" - - "git.hexq.cn/tiglog/golib/helper" -) - -const default_cache_size = 1024 - -// 本地内存缓存 -type localCacheAdapter struct { - mu sync.Mutex - data map[string][]byte -} - -func NewLocalCacheAdapter() ICacheAdapter { - return &localCacheAdapter{ - data: make(map[string][]byte, default_cache_size), - } -} - -func (c *localCacheAdapter) Get(key string, dest interface{}) error { - val, ok := c.data[key] - if ok { - helper.Scan(val, dest) - } - return nil -} - -func (c *localCacheAdapter) Set(key string, val interface{}, ttl time.Duration) error { - return nil -} - -func (c *localCacheAdapter) Has(key string) bool { - return false -} -func (c *localCacheAdapter) Del(keys ...string) (int64, error) { - return 0, nil -} diff --git a/gcache/adapter_redis.go b/gcache/adapter_redis.go deleted file mode 100644 index adfaf3d..0000000 --- a/gcache/adapter_redis.go +++ /dev/null @@ -1,52 +0,0 @@ -// -// adapter_redis.go -// Copyright (C) 2022 tiglog -// -// Distributed under terms of the MIT license. -// - -package gcache - -import ( - "context" - "sync" - "time" - - "github.com/go-redis/redis/v8" -) - -// 使用 redis 服务缓存 -type redisCacheAdapter struct { - mu sync.Mutex - redis *redis.Client -} - -func NewRedisCacheAdapter(rds *redis.Client) ICacheAdapter { - return &redisCacheAdapter{ - redis: rds, - } -} - -func (c *redisCacheAdapter) Get(key string, dest interface{}) error { - cmd := c.redis.Get(context.Background(), key) - return cmd.Scan(dest) -} - -func (c *redisCacheAdapter) Set(key string, val interface{}, ttl time.Duration) error { - cmd := c.redis.Set(context.Background(), key, val, ttl) - return cmd.Err() -} - -func (c *redisCacheAdapter) Has(key string) bool { - cmd := c.redis.Exists(context.Background(), key) - result, _ := cmd.Result() - if result == 1 { - return true - } - return false -} - -func (c *redisCacheAdapter) Del(keys ...string) (int64, error) { - cmd := c.redis.Del(context.Background(), keys...) - return cmd.Result() -} diff --git a/gcache/cache.go b/gcache/cache.go deleted file mode 100644 index cdec02e..0000000 --- a/gcache/cache.go +++ /dev/null @@ -1,57 +0,0 @@ -// -// cache.go -// Copyright (C) 2022 tiglog -// -// Distributed under terms of the MIT license. -// - -package gcache - -import ( - "sync" - "time" - - "github.com/go-redis/redis/v8" -) - -type Engine struct { - client ICacheAdapter -} - -var once sync.Once - -var engine *Engine - -func New(adapter ICacheAdapter) *Engine { - once.Do(func() { - engine = &Engine{ - client: adapter, - } - }) - return engine -} - -func NewWithRedis(rds *redis.Client) *Engine { - once.Do(func() { - engine = &Engine{ - client: NewRedisCacheAdapter(rds), - } - }) - return engine -} - -func (e *Engine) Get(key string, dest interface{}) error { - return e.client.Get(key, dest) -} - -func (e *Engine) Set(key string, val interface{}, ttl time.Duration) error { - return e.client.Set(key, val, ttl) -} - -func (e *Engine) Has(key string) bool { - return e.client.Has(key) -} - -func (e *Engine) Del(keys ...string) (int64, error) { - return e.client.Del(keys...) -} diff --git a/gcache/cache_contract.go b/gcache/cache_contract.go deleted file mode 100644 index d513a8b..0000000 --- a/gcache/cache_contract.go +++ /dev/null @@ -1,17 +0,0 @@ -// -// cache_contact.go -// Copyright (C) 2022 tiglog -// -// Distributed under terms of the MIT license. -// - -package gcache - -import "time" - -type ICacheAdapter interface { - Get(key string, dest interface{}) error - Set(key string, val interface{}, exp time.Duration) error - Del(keys ...string) (int64, error) - Has(key string) bool -} diff --git a/gcache/cache_test.go b/gcache/cache_test.go deleted file mode 100644 index f492a12..0000000 --- a/gcache/cache_test.go +++ /dev/null @@ -1,70 +0,0 @@ -// -// cache_test.go -// Copyright (C) 2023 tiglog -// -// Distributed under terms of the MIT license. -// - -package gcache_test - -import ( - "context" - "fmt" - "os" - "testing" - "time" - - "git.hexq.cn/tiglog/golib/gcache" - "git.hexq.cn/tiglog/golib/gtest" - "github.com/go-redis/redis/v8" -) - -func getRedis() *redis.Client { - opt, _ := redis.ParseURL(os.Getenv("REDIS_URL")) - return redis.NewClient(opt) -} - -func TestRedis(t *testing.T) { - rds := getRedis() - cmd := rds.Ping(context.Background()) - ret, err := cmd.Result() - gtest.Nil(t, err) - fmt.Println(ret) -} - -func TestCacheNew(t *testing.T) { - rds := getRedis() - cm1 := gcache.NewWithRedis(rds) - cm2 := gcache.NewWithRedis(rds) - gtest.Equal(t, cm1, cm2) -} -func TestRedisAdapter(t *testing.T) { - rds := getRedis() - cm := gcache.NewWithRedis(rds) - key := "foo" - cm.Del(key) - r1 := cm.Has(key) - gtest.False(t, r1) - - var err error - val1 := "bar" - err = cm.Set(key, val1, time.Second) - gtest.Nil(t, err) - - var r2 string - err = cm.Get(key, &r2) - gtest.Nil(t, err) - gtest.Equal(t, val1, r2) - - val2 := 2 - err = cm.Set(key, val2, time.Hour) - gtest.Nil(t, err) - var r3 int - err = cm.Get(key, &r3) - gtest.Nil(t, err) - gtest.Equal(t, val2, r3) - - n, err := cm.Del(key) - gtest.Nil(t, err) - gtest.Equal(t, int64(1), n) -} diff --git a/gcache/config.go b/gcache/config.go new file mode 100644 index 0000000..d4e482b --- /dev/null +++ b/gcache/config.go @@ -0,0 +1,21 @@ +// +// config.go +// Copyright (C) 2023 tiglog +// +// Distributed under terms of the MIT license. +// + +package gcache + +import "time" + +type Config struct { + shards int + expiredCallback ExpiredCallback + hash IHash + clearInterval time.Duration +} + +func NewConfig() *Config { + return &Config{shards: 1024, hash: newDefaultHash(), clearInterval: 1 * time.Second} +} diff --git a/gcache/hash.go b/gcache/hash.go new file mode 100644 index 0000000..f45ab19 --- /dev/null +++ b/gcache/hash.go @@ -0,0 +1,42 @@ +// +// hash.go +// Copyright (C) 2023 tiglog +// +// Distributed under terms of the MIT license. +// + +package gcache + +// IHash is responsible for generating unsigned, 64-bit hash of provided string. IHash should minimize collisions +// (generating same hash for different strings) and while performance is also important fast functions are preferable (i.e. +// you can use FarmHash family). +type IHash interface { + Sum64(string) uint64 +} + +// newDefaultHash returns a new 64-bit FNV-1a IHash which makes no memory allocations. +// Its Sum64 method will lay the value out in big-endian byte order. +// See https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_function +func newDefaultHash() IHash { + return fnv64a{} +} + +type fnv64a struct{} + +const ( + // offset64 FNVa offset basis. See https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_function#FNV-1a_hash + offset64 = 14695981039346656037 + // prime64 FNVa prime value. See https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_function#FNV-1a_hash + prime64 = 1099511628211 +) + +// Sum64 gets the string and returns its uint64 hash value. +func (f fnv64a) Sum64(key string) uint64 { + var hash uint64 = offset64 + for i := 0; i < len(key); i++ { + hash ^= uint64(key[i]) + hash *= prime64 + } + + return hash +} diff --git a/gcache/icache.go b/gcache/icache.go new file mode 100644 index 0000000..4a839f1 --- /dev/null +++ b/gcache/icache.go @@ -0,0 +1,122 @@ +// +// icache.go +// Copyright (C) 2023 tiglog +// +// Distributed under terms of the MIT license. +// + +package gcache + +import "time" + +// ExpiredCallback Callback the function when the key-value pair expires +// Note that it is executed after expiration +type ExpiredCallback func(k string, v interface{}) error + +type ICache interface { + //Set key to hold the string value. If key already holds a value, it is overwritten, regardless of its type. + //Any previous time to live associated with the key is discarded on successful SET operation. + //Example: + //c.Set("demo", 1) + //c.Set("demo", 1, WithEx(10*time.Second)) + //c.Set("demo", 1, WithEx(10*time.Second), WithNx()) + Set(k string, v interface{}, opts ...SetIOption) bool + //Get the value of key. + //If the key does not exist the special value nil,false is returned. + //Example: + //c.Get("demo") //nil, false + //c.Set("demo", "value") + //c.Get("demo") //"value", true + Get(k string) (interface{}, bool) + //GetSet Atomically sets key to value and returns the old value stored at key. + //Returns nil,false when key not exists. + //Example: + //c.GetSet("demo", 1) //nil,false + //c.GetSet("demo", 2) //1,true + GetSet(k string, v interface{}, opts ...SetIOption) (interface{}, bool) + //GetDel Get the value of key and delete the key. + //This command is similar to GET, except for the fact that it also deletes the key on success. + //Example: + //c.Set("demo", "value") + //c.GetDel("demo") //"value", true + //c.GetDel("demo") //nil, false + GetDel(k string) (interface{}, bool) + //Del Removes the specified keys. A key is ignored if it does not exist. + //Return the number of keys that were removed. + //Example: + //c.Set("demo1", "1") + //c.Set("demo2", "1") + //c.Del("demo1", "demo2", "demo3") //2 + Del(keys ...string) int + //DelExpired Only delete when key expires + //Example: + //c.Set("demo1", "1") + //c.Set("demo2", "1", WithEx(1*time.Second)) + //time.Sleep(1*time.Second) + //c.DelExpired("demo1", "demo2") //true + DelExpired(k string) bool + //Exists Returns if key exists. + //Return the number of exists keys. + //Example: + //c.Set("demo1", "1") + //c.Set("demo2", "1") + //c.Exists("demo1", "demo2", "demo3") //2 + Exists(keys ...string) bool + //Expire Set a timeout on key. + //After the timeout has expired, the key will automatically be deleted. + //Return false if the key not exist. + //Example: + //c.Expire("demo", 1*time.Second) // false + //c.Set("demo", "1") + //c.Expire("demo", 1*time.Second) // true + Expire(k string, d time.Duration) bool + //ExpireAt has the same effect and semantic as Expire, but instead of specifying the number of seconds representing the TTL (time to live), + //it takes an absolute Unix Time (seconds since January 1, 1970). A Time in the past will delete the key immediately. + //Return false if the key not exist. + //Example: + //c.ExpireAt("demo", time.Now().Add(10*time.Second)) // false + //c.Set("demo", "1") + //c.ExpireAt("demo", time.Now().Add(10*time.Second)) // true + ExpireAt(k string, t time.Time) bool + //Persist Remove the existing timeout on key. + //Return false if the key not exist. + //Example: + //c.Persist("demo") // false + //c.Set("demo", "1") + //c.Persist("demo") // true + Persist(k string) bool + //Ttl Returns the remaining time to live of a key that has a timeout. + //Returns 0,false if the key does not exist or if the key exist but has no associated expire. + //Example: + //c.Set("demo", "1") + //c.Ttl("demo") // 0,false + //c.Set("demo", "1", WithEx(10*time.Second)) + //c.Ttl("demo") // 10*time.Second,true + Ttl(k string) (time.Duration, bool) +} + +type IItem interface { + Expired() bool + CanExpire() bool + SetExpireAt(t time.Time) +} + +type Item struct { + v interface{} + expire time.Time +} + +func (i *Item) Expired() bool { + if !i.CanExpire() { + return false + } + return time.Now().After(i.expire) +} + +func (i *Item) CanExpire() bool { + return !i.expire.IsZero() +} + +func (i *Item) SetExpireAt(t time.Time) { + i.expire = t +} diff --git a/gcache/memory.go b/gcache/memory.go new file mode 100644 index 0000000..75f9b53 --- /dev/null +++ b/gcache/memory.go @@ -0,0 +1,255 @@ +// +// memory.go +// Copyright (C) 2023 tiglog +// +// Distributed under terms of the MIT license. +// + +package gcache + +import ( + "runtime" + "sync" + "time" +) + +func NewMemCache(opts ...ICacheOption) ICache { + conf := NewConfig() + for _, opt := range opts { + opt(conf) + } + + c := &memCache{ + shards: make([]*memCacheShard, conf.shards), + closed: make(chan struct{}), + shardMask: uint64(conf.shards - 1), + config: conf, + hash: conf.hash, + } + for i := 0; i < len(c.shards); i++ { + c.shards[i] = newMemCacheShard(conf) + } + if conf.clearInterval > 0 { + go func() { + ticker := time.NewTicker(conf.clearInterval) + defer ticker.Stop() + for { + select { + case <-ticker.C: + for _, shard := range c.shards { + shard.checkExpire() + } + case <-c.closed: + return + } + } + }() + } + cache := &MemCache{c} + // Associated finalizer function with obj. + // When the obj is unreachable, close the obj. + runtime.SetFinalizer(cache, func(cache *MemCache) { close(cache.closed) }) + return cache +} + +type MemCache struct { + *memCache +} + +type memCache struct { + shards []*memCacheShard + hash IHash + shardMask uint64 + config *Config + closed chan struct{} +} + +func (c *memCache) Set(k string, v interface{}, opts ...SetIOption) bool { + item := Item{v: v} + for _, opt := range opts { + if pass := opt(c, k, &item); !pass { + return false + } + } + hashedKey := c.hash.Sum64(k) + shard := c.getShard(hashedKey) + shard.set(k, &item) + return true +} + +func (c *memCache) Get(k string) (interface{}, bool) { + hashedKey := c.hash.Sum64(k) + shard := c.getShard(hashedKey) + return shard.get(k) +} + +func (c *memCache) GetSet(k string, v interface{}, opts ...SetIOption) (interface{}, bool) { + defer c.Set(k, v, opts...) + return c.Get(k) +} + +func (c *memCache) GetDel(k string) (interface{}, bool) { + defer c.Del(k) + return c.Get(k) +} + +func (c *memCache) Del(ks ...string) int { + var count int + for _, k := range ks { + hashedKey := c.hash.Sum64(k) + shard := c.getShard(hashedKey) + count += shard.del(k) + } + return count +} + +// DelExpired Only delete when key expires +func (c *memCache) DelExpired(k string) bool { + hashedKey := c.hash.Sum64(k) + shard := c.getShard(hashedKey) + return shard.delExpired(k) +} + +func (c *memCache) Exists(ks ...string) bool { + for _, k := range ks { + if _, found := c.Get(k); !found { + return false + } + } + return true +} + +func (c *memCache) Expire(k string, d time.Duration) bool { + v, found := c.Get(k) + if !found { + return false + } + return c.Set(k, v, WithEx(d)) +} + +func (c *memCache) ExpireAt(k string, t time.Time) bool { + v, found := c.Get(k) + if !found { + return false + } + return c.Set(k, v, WithExAt(t)) +} + +func (c *memCache) Persist(k string) bool { + v, found := c.Get(k) + if !found { + return false + } + return c.Set(k, v) +} + +func (c *memCache) Ttl(k string) (time.Duration, bool) { + hashedKey := c.hash.Sum64(k) + shard := c.getShard(hashedKey) + return shard.ttl(k) +} + +func (c *memCache) getShard(hashedKey uint64) (shard *memCacheShard) { + return c.shards[hashedKey&c.shardMask] +} + +////////////// +// shard // +////////////// + +type memCacheShard struct { + hashmap map[string]Item + lock sync.RWMutex + expiredCallback ExpiredCallback +} + +func newMemCacheShard(conf *Config) *memCacheShard { + return &memCacheShard{expiredCallback: conf.expiredCallback, hashmap: map[string]Item{}} +} + +func (c *memCacheShard) set(k string, item *Item) { + c.lock.Lock() + c.hashmap[k] = *item + c.lock.Unlock() + return +} + +func (c *memCacheShard) get(k string) (interface{}, bool) { + c.lock.RLock() + item, exist := c.hashmap[k] + c.lock.RUnlock() + if !exist { + return nil, false + } + if !item.Expired() { + return item.v, true + } + if c.delExpired(k) { + return nil, false + } + return c.get(k) +} + +func (c *memCacheShard) getSet(k string, item *Item) (interface{}, bool) { + defer c.set(k, item) + return c.get(k) +} + +func (c *memCacheShard) getDel(k string) (interface{}, bool) { + defer c.del(k) + return c.get(k) +} + +func (c *memCacheShard) del(k string) int { + var count int + c.lock.Lock() + v, found := c.hashmap[k] + if found { + delete(c.hashmap, k) + if !v.Expired() { + count++ + } + } + c.lock.Unlock() + return count +} + +// delExpired Only delete when key expires +func (c *memCacheShard) delExpired(k string) bool { + c.lock.Lock() + item, found := c.hashmap[k] + if !found || !item.Expired() { + c.lock.Unlock() + return false + } + delete(c.hashmap, k) + c.lock.Unlock() + if c.expiredCallback != nil { + _ = c.expiredCallback(k, item.v) + } + return true +} + +func (c *memCacheShard) ttl(k string) (time.Duration, bool) { + c.lock.RLock() + v, found := c.hashmap[k] + c.lock.RUnlock() + if !found || !v.CanExpire() || v.Expired() { + return 0, false + } + return v.expire.Sub(time.Now()), true +} + +func (c *memCacheShard) checkExpire() { + var expiredKeys []string + c.lock.RLock() + for k, item := range c.hashmap { + if item.Expired() { + expiredKeys = append(expiredKeys, k) + } + } + c.lock.RUnlock() + for _, k := range expiredKeys { + c.delExpired(k) + } +} diff --git a/gcache/memory_test.go b/gcache/memory_test.go new file mode 100644 index 0000000..cdc5214 --- /dev/null +++ b/gcache/memory_test.go @@ -0,0 +1,31 @@ +// +// memory_test.go +// Copyright (C) 2023 tiglog +// +// Distributed under terms of the MIT license. +// + +package gcache_test + +import ( + "testing" + "time" + + "git.hexq.cn/tiglog/golib/gcache" + "git.hexq.cn/tiglog/golib/gtest" +) + +func TestUsage(t *testing.T) { + + c := gcache.NewMemCache() + c.Set("a", 1) + c.Set("b", 1, gcache.WithEx(1*time.Second)) + time.Sleep(1 * time.Second) + r1, ok := c.Get("a") // 1, true + gtest.True(t, ok) + gtest.Equal(t, 1, r1) + + r2, ok := c.Get("b") + gtest.False(t, ok) + gtest.Nil(t, r2) +} diff --git a/gcache/option.go b/gcache/option.go new file mode 100644 index 0000000..15e12d1 --- /dev/null +++ b/gcache/option.go @@ -0,0 +1,68 @@ +// +// option.go +// Copyright (C) 2023 tiglog +// +// Distributed under terms of the MIT license. +// + +package gcache + +import "time" + +// SetIOption The option used to cache set +type SetIOption func(ICache, string, IItem) bool + +// WithEx Set the specified expire time, in time.Duration. +func WithEx(d time.Duration) SetIOption { + return func(c ICache, k string, v IItem) bool { + v.SetExpireAt(time.Now().Add(d)) + return true + } +} + +// WithExAt Set the specified expire deadline, in time.Time. +func WithExAt(t time.Time) SetIOption { + return func(c ICache, k string, v IItem) bool { + v.SetExpireAt(t) + return true + } +} + +// ICacheOption The option used to create the cache object +type ICacheOption func(conf *Config) + +// WithShards set custom size of sharding. Default is 1024 +// The larger the size, the smaller the lock force, the higher the concurrency performance, +// and the higher the memory footprint, so try to choose a size that fits your business scenario +func WithShards(shards int) ICacheOption { + if shards <= 0 { + panic("Invalid shards") + } + return func(conf *Config) { + conf.shards = shards + } +} + +// WithExpiredCallback set custom expired callback function +// This callback function is called when the key-value pair expires +func WithExpiredCallback(ec ExpiredCallback) ICacheOption { + return func(conf *Config) { + conf.expiredCallback = ec + } +} + +// WithHash set custom hash key function +func WithHash(hash IHash) ICacheOption { + return func(conf *Config) { + conf.hash = hash + } +} + +// WithClearInterval set custom clear interval. +// Interval for clearing expired key-value pairs. The default value is 1 second +// If the d is 0, the periodic clearing function is disabled +func WithClearInterval(d time.Duration) ICacheOption { + return func(conf *Config) { + conf.clearInterval = d + } +} diff --git a/gcache/readme.adoc b/gcache/readme.adoc index e0215cd..13d3c36 100644 --- a/gcache/readme.adoc +++ b/gcache/readme.adoc @@ -1,4 +1,4 @@ -= 缓存设计 += 说明 :author: tiglog :experimental: :toc: left @@ -11,51 +11,18 @@ :source-highlighter: rouge :rouge-style: github :source-linenums-option: -:revdate: 2022-11-30 +:revdate: 2023-10-11 :imagesdir: ./img -== 实现 +== 快速使用 -已于 2023-06-21 实现。 - -可以基于 `redis` 进行一些基本使用。 - - - -== 设计 - -从使用倒推设计。 - -=== 场景1 - -自己管理 `key`: - -[source,golang] +[source,go] ---- -ck := "key_foo" -data := cache.get(ck) -if !data { // <1> - data = FETCH_DATA() - cache.set(ck, data, 7200) // <2> -} -return data +c := cache.NewMemCache() +c.Set("a", 1) +c.Set("b", 1, cache.WithEx(1*time.Second)) +time.sleep(1*time.Second) +c.Get("a") // 1, true +c.Get("b") // nil, false ---- - -<1> `get` 值为 `false` 表示没有缓存或缓存已过期 -<2> 7200 为缓存有效期(单位为秒),若指定为 0 表示不过期。 - -=== 场景2 - -程序自动管理 `key`: - -[source,golang] ----- -cache.get(func() { - return 'foo' -}, 7200) ----- - -这种方式一般情况下比较方便,要是需要手动使缓存失效,则要麻烦一些。因此,这种方 -式暂时不实现。 - diff --git a/gcache/redis.go b/gcache/redis.go new file mode 100644 index 0000000..a823503 --- /dev/null +++ b/gcache/redis.go @@ -0,0 +1,10 @@ +// +// redis.go +// Copyright (C) 2023 tiglog +// +// Distributed under terms of the MIT license. +// + +package gcache + +// TODO 暂时不需要,后面再实现