refactor: 使用 memory cache

This commit is contained in:
tiglog 2023-10-11 07:36:56 +08:00
parent 26c95c8fee
commit 2302575edf
14 changed files with 559 additions and 297 deletions

View File

@ -1,10 +0,0 @@
//
// adapter_file.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
// 本地文件缓存

View File

@ -1,48 +0,0 @@
//
// adapter_local.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// 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
}

View File

@ -1,52 +0,0 @@
//
// adapter_redis.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// 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()
}

View File

@ -1,57 +0,0 @@
//
// cache.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// 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...)
}

View File

@ -1,17 +0,0 @@
//
// cache_contact.go
// Copyright (C) 2022 tiglog <me@tiglog.com>
//
// 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
}

View File

@ -1,70 +0,0 @@
//
// cache_test.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// 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)
}

21
gcache/config.go Normal file
View File

@ -0,0 +1,21 @@
//
// config.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// 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}
}

42
gcache/hash.go Normal file
View File

@ -0,0 +1,42 @@
//
// hash.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// 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/FowlerNollVo_hash_function
func newDefaultHash() IHash {
return fnv64a{}
}
type fnv64a struct{}
const (
// offset64 FNVa offset basis. See https://en.wikipedia.org/wiki/FowlerNollVo_hash_function#FNV-1a_hash
offset64 = 14695981039346656037
// prime64 FNVa prime value. See https://en.wikipedia.org/wiki/FowlerNollVo_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
}

122
gcache/icache.go Normal file
View File

@ -0,0 +1,122 @@
//
// icache.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// 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
}

255
gcache/memory.go Normal file
View File

@ -0,0 +1,255 @@
//
// memory.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// 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)
}
}

31
gcache/memory_test.go Normal file
View File

@ -0,0 +1,31 @@
//
// memory_test.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// 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)
}

68
gcache/option.go Normal file
View File

@ -0,0 +1,68 @@
//
// option.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// 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
}
}

View File

@ -1,4 +1,4 @@
= 缓存设计 = 说明
:author: tiglog :author: tiglog
:experimental: :experimental:
:toc: left :toc: left
@ -11,51 +11,18 @@
:source-highlighter: rouge :source-highlighter: rouge
:rouge-style: github :rouge-style: github
:source-linenums-option: :source-linenums-option:
:revdate: 2022-11-30 :revdate: 2023-10-11
:imagesdir: ./img :imagesdir: ./img
== 实现 == 快速使用
已于 2023-06-21 实现。 [source,go]
可以基于 `redis` 进行一些基本使用。
== 设计
从使用倒推设计。
=== 场景1
自己管理 `key`:
[source,golang]
---- ----
ck := "key_foo" c := cache.NewMemCache()
data := cache.get(ck) c.Set("a", 1)
if !data { // <1> c.Set("b", 1, cache.WithEx(1*time.Second))
data = FETCH_DATA() time.sleep(1*time.Second)
cache.set(ck, data, 7200) // <2> c.Get("a") // 1, true
} c.Get("b") // nil, false
return data
---- ----
<1> `get` 值为 `false` 表示没有缓存或缓存已过期
<2> 7200 为缓存有效期(单位为秒),若指定为 0 表示不过期。
=== 场景2
程序自动管理 `key`:
[source,golang]
----
cache.get(func() {
return 'foo'
}, 7200)
----
这种方式一般情况下比较方便,要是需要手动使缓存失效,则要麻烦一些。因此,这种方
式暂时不实现。

10
gcache/redis.go Normal file
View File

@ -0,0 +1,10 @@
//
// redis.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
// TODO 暂时不需要,后面再实现