diff --git a/cache/big_cache.go b/cache/big_cache.go new file mode 100644 index 0000000..f2ba813 --- /dev/null +++ b/cache/big_cache.go @@ -0,0 +1,47 @@ +package cache + +import ( + "errors" + "time" + + "github.com/allegro/bigcache" +) + +var _ MemCache = &bigCacheClient{} + +type BigCacheConfig struct { +} + +type bigCacheClient struct { + cache *bigcache.BigCache +} + +func NewBigCache(c *BigCacheConfig) (*bigCacheClient, error) { + bigCache, err := bigcache.NewBigCache(bigcache.Config{}) + if err != nil { + return nil, err + } + + return &bigCacheClient{ + cache: bigCache, + }, nil +} + +func (c *bigCacheClient) Get(key string) ([]byte, error) { + return c.cache.Get(key) +} + +func (c *bigCacheClient) Set(key string, entry []byte, expire time.Duration) error { + return c.cache.Set(key, entry) +} + +func (c *bigCacheClient) Delete(key string) error { + return c.cache.Delete(key) +} + +func (c *bigCacheClient) Exist(key string) { +} + +func (c *bigCacheClient) IsNotFound(err error) bool { + return errors.Is(err, bigcache.ErrEntryNotFound) +} diff --git a/cache/cache.go b/cache/cache.go new file mode 100644 index 0000000..7e5c20e --- /dev/null +++ b/cache/cache.go @@ -0,0 +1,113 @@ +package cache + +import ( + "errors" + "time" + + "github.com/charlienet/go-mixed/bytesconv" +) + +var ErrNotFound = errors.New("not found") + +type LoadFunc func() (any, error) + +type Cache struct { + prefix string // 键前缀 + mem MemCache // 内存缓存 + distributdCache DistributdCache // 分布式缓存 + publishSubscribe PublishSubscribe // 发布订阅 + qps *qps +} + +func NewCache(opts ...option) (*Cache, error) { + c := &Cache{ + qps: NewQps(), + } + + for _, f := range opts { + f(c) + } + + go c.subscribe() + + return c, nil +} + +func (c *Cache) Set(key string, value any, expiration time.Duration) error { + if c.mem != nil { + bytes, err := bytesconv.Encode(value) + if err != nil { + return err + } + + c.mem.Set(key, bytes, expiration) + } + + return nil +} + +func (c *Cache) Get(key string, out any) error { + if c.mem != nil { + c.getFromMem(key, out) + } + + if c.distributdCache != nil { + if err := c.distributdCache.Get(key, out); err != nil { + + } + } + + return nil +} + +func (c *Cache) GetFn(key string, out any, fn LoadFunc, expiration time.Duration) (bool, error) { + ret, err := fn() + if err != nil { + return false, err + } + + _ = ret + + return false, nil +} + +func (c *Cache) Exist(key string) (bool, error) { + return false, nil +} + +func (c *Cache) Delete(key string) error { + if c.mem != nil { + c.mem.Delete(key) + } + + if c.distributdCache != nil { + c.distributdCache.Delete(key) + } + + return nil +} + +func (c *Cache) getFromMem(key string, out any) error { + + bytes, err := c.mem.Get(key) + if err != nil { + return err + } + + if err := bytesconv.Decode(bytes, out); err != nil { + return err + } + + return nil +} + +func (c *Cache) subscribe() { +} + +func (c *Cache) genKey(key string) string { + if len(c.prefix) == 0 { + return key + } + + return c.prefix + "-" + key +} diff --git a/cache/cache_test.go b/cache/cache_test.go new file mode 100644 index 0000000..91d0796 --- /dev/null +++ b/cache/cache_test.go @@ -0,0 +1,85 @@ +package cache + +import ( + "testing" + "time" +) + +func TestNewCache(t *testing.T) { + r := NewRedis(&RedisConfig{}) + if err := r.Ping(); err != nil { + t.Fatal(err) + } + + c, err := NewCache( + WithDistributdCache(r), + WithPrefix("cache_test")) + if err != nil { + t.Fatal(err) + } + + c.Set("abc", "value", time.Minute*10) +} + +type SimpleUser struct { + FirstName string + LastName string +} + +func TestMem(t *testing.T) { + c, err := NewCache(WithFreeCache(10 * 1024 * 1024)) + if err != nil { + t.Fatal(err) + } + key := "u-000" + u := SimpleUser{FirstName: "Radomir", LastName: "Sohlich"} + + c.Set(key, u, time.Second) + + var u2 SimpleUser + c.Get(key, &u2) + + t.Logf("%+v", u2) +} + +func TestDistributedCache(t *testing.T) { + key := "key-001" + c := NewRedis(&RedisConfig{Addrs: []string{"192.168.2.222:6379"}, DB: 6, Password: "123456"}) + + if err := c.Ping(); err != nil { + t.Fatal(err) + } + u := SimpleUser{FirstName: "redis client"} + c.Set(key, u, time.Second) + + var u2 SimpleUser + if err := c.Get(key, &u2); err != nil { + t.Fatal("err:", err) + } + t.Logf("%+v", u2) +} + +func TestGetFn(t *testing.T) { + c, err := NewCache(WithBigCache(&BigCacheConfig{})) + if err != nil { + t.Fatal(err) + } + key := "u-000" + + var u2 SimpleUser + c.GetFn(key, &u2, func() (out any, err error) { + v := &u2 + v.FirstName = "abc" + + return nil, nil + }, time.Second) + + t.Logf("%+v", u2) +} + +func BenchmarkMemCache(b *testing.B) { +} + +func load() (any, error) { + return nil, nil +} diff --git a/cache/distributd_cache.go b/cache/distributd_cache.go new file mode 100644 index 0000000..5d7929a --- /dev/null +++ b/cache/distributd_cache.go @@ -0,0 +1,10 @@ +package cache + +import "time" + +type DistributdCache interface { + Get(key string, out any) error + Set(key string, value any, expiration time.Duration) + Delete(key string) error + Ping() error +} diff --git a/cache/free_cache.go b/cache/free_cache.go new file mode 100644 index 0000000..4e9a4ae --- /dev/null +++ b/cache/free_cache.go @@ -0,0 +1,55 @@ +package cache + +import ( + "errors" + "time" + + "github.com/coocood/freecache" +) + +const defaultSize = 10 * 1024 * 1024 // 10M + +var _ MemCache = &freeCache{} + +type freeCache struct { + cache *freecache.Cache +} + +func NewFreeCache(size int) *freeCache { + if size < defaultSize { + size = defaultSize + } + + // debug.SetGCPercent(20) + + c := freecache.NewCache(size) + return &freeCache{ + cache: c, + } +} + +func (c *freeCache) Get(key string) ([]byte, error) { + return c.cache.Get([]byte(key)) +} + +func (c *freeCache) Set(key string, value []byte, d time.Duration) error { + s := int(d.Seconds()) + return c.cache.Set([]byte(key), value, s) +} + +func (c *freeCache) Delete(key string) error { + affected := c.cache.Del([]byte(key)) + if !affected { + return errors.New("不存在") + } + + return nil +} + +func (c *freeCache) Exist(key string) error { + return nil +} + +func (c *freeCache) IsNotFound(err error) bool { + return errors.Is(err, freecache.ErrNotFound) +} diff --git a/cache/mem.go b/cache/mem.go new file mode 100644 index 0000000..5f8cadf --- /dev/null +++ b/cache/mem.go @@ -0,0 +1,9 @@ +package cache + +import "time" + +type MemCache interface { + Get(key string) ([]byte, error) + Set(key string, entry []byte, expire time.Duration) error + Delete(key string) error +} diff --git a/cache/options.go b/cache/options.go new file mode 100644 index 0000000..cf46d3e --- /dev/null +++ b/cache/options.go @@ -0,0 +1,31 @@ +package cache + +type option func(*Cache) + +type options struct { + Prefix string +} + +func WithPrefix(prefix string) option { + return func(o *Cache) { o.prefix = prefix } +} + +func WithDistributdCache(d DistributdCache) option { + return func(o *Cache) { o.distributdCache = d } +} + +func WithBigCache(config *BigCacheConfig) option { + return func(o *Cache) { + c, err := NewBigCache(config) + _ = err + o.mem = c + } +} + +func WithFreeCache(size int) option { + return func(o *Cache) { o.mem = NewFreeCache(size) } +} + +func WithPublishSubscribe(p PublishSubscribe) option { + return func(o *Cache) {} +} diff --git a/cache/publish_subscribe.go b/cache/publish_subscribe.go new file mode 100644 index 0000000..04cdd35 --- /dev/null +++ b/cache/publish_subscribe.go @@ -0,0 +1,4 @@ +package cache + +type PublishSubscribe interface { +} diff --git a/cache/qps.go b/cache/qps.go new file mode 100644 index 0000000..4f6dfb1 --- /dev/null +++ b/cache/qps.go @@ -0,0 +1,43 @@ +package cache + +import ( + "sync/atomic" + "time" +) + +type qps struct { + all qpsContent + memoryTotal qpsContent + memoryHit qpsContent + redisTotal qpsContent + redisHit qpsContent + sourceTotal qpsContent +} + +type qpsContent struct { + viewTotal int64 + total int64 +} + +func NewQps() *qps { + qps := &qps{} + go qps.statisticsTotal() + return qps +} + +func (q *qps) statisticsTotal() { + defer func() { + if e := recover(); e != nil { + } + }() + + ticker := time.NewTicker(time.Second) + for range ticker.C { + q.all.viewTotal = atomic.SwapInt64(&q.all.total, 0) + q.memoryTotal.viewTotal = atomic.SwapInt64(&q.memoryTotal.total, 0) + q.memoryHit.viewTotal = atomic.SwapInt64(&q.memoryHit.total, 0) + q.redisTotal.viewTotal = atomic.SwapInt64(&q.redisTotal.total, 0) + q.redisHit.viewTotal = atomic.SwapInt64(&q.redisHit.total, 0) + q.sourceTotal.viewTotal = atomic.SwapInt64(&q.sourceTotal.total, 0) + } +} diff --git a/cache/redis.go b/cache/redis.go new file mode 100644 index 0000000..1bac3e7 --- /dev/null +++ b/cache/redis.go @@ -0,0 +1,86 @@ +package cache + +import ( + "context" + "fmt" + "time" + + "github.com/charlienet/go-mixed/bytesconv" + "github.com/charlienet/go-mixed/json" + "github.com/charlienet/go-mixed/rand" + "github.com/go-redis/redis/v8" +) + +type RedisConfig struct { + Perfix string // key perfix + Addrs []string + + // Database to be selected after connecting to the server. + // Only single-node and failover clients. + DB int + + Username string + Password string + MaxRetries int + MinRetryBackoff time.Duration + MaxRetryBackoff time.Duration + + DialTimeout time.Duration + ReadTimeout time.Duration + WriteTimeout time.Duration +} + +type redisClient struct { + client redis.UniversalClient + emptyStamp string // 空对象标识,每个实例隔离 + perfix string // 缓存键前缀 +} + +func NewRedis(c *RedisConfig) *redisClient { + client := redis.NewUniversalClient(&redis.UniversalOptions{ + Addrs: c.Addrs, + DB: c.DB, + Username: c.Username, + Password: c.Password, + }) + + return &redisClient{ + emptyStamp: fmt.Sprintf("redis-empty-%d-%s", time.Now().Unix(), rand.Hex.Generate(6)), + perfix: c.Perfix, + client: client, + } +} + +func (c *redisClient) Get(key string, out any) error { + cmd := c.client.Get(context.Background(), key) + str, err := cmd.Result() + if err != nil { + return err + } + + err = json.Unmarshal(bytesconv.StringToBytes(str), out) + return err +} + +func (c *redisClient) Set(key string, value any, expiration time.Duration) { + j, _ := json.Marshal(value) + c.client.Set(context.Background(), key, j, expiration) +} + +func (c *redisClient) Exist(key string) (bool, error) { + return false, nil +} + +func (c *redisClient) Delete(key string) error { + cmd := c.client.Del(context.Background(), key) + if cmd.Err() != nil { + return cmd.Err() + } + + return nil +} + +func (c *redisClient) Ping() error { + _, err := c.client.Ping(context.Background()).Result() + return err +}