From 478d992bad33c28afc283f813c4e28e1bf4544cc Mon Sep 17 00:00:00 2001 From: Wenbo han Date: Wed, 8 Feb 2023 21:22:45 +0800 Subject: [PATCH] Redis add Memory driver --- README.md | 1 - README_zh.md | 1 - cache/application.go | 10 ++ cache/application_test.go | 227 +++++++++++++++++++++++--------------- cache/memory.go | 148 +++++++++++++++++++++++++ cache/redis.go | 16 +-- cache/utils.go | 9 ++ go.mod | 1 + go.sum | 2 + 9 files changed, 311 insertions(+), 104 deletions(-) create mode 100644 cache/memory.go create mode 100644 cache/utils.go diff --git a/README.md b/README.md index 024c949e0..af57f1bb9 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,6 @@ Golang developers quickly build their own applications. - [ ] Custom .env path - [ ] Database read-write separation -- [ ] Extend Redis driver ## Documentation diff --git a/README_zh.md b/README_zh.md index c19cc6737..d4ba130df 100644 --- a/README_zh.md +++ b/README_zh.md @@ -30,7 +30,6 @@ Goravel 是一个功能完备、具有良好扩展能力的 Web 应用程序框 - [ ] 自定义 .env 路径 - [ ] 数据库读写分离 -- [ ] 扩展 Redis 驱动 ## 文档 diff --git a/cache/application.go b/cache/application.go index 8c3f14992..c2d83e31e 100644 --- a/cache/application.go +++ b/cache/application.go @@ -25,6 +25,16 @@ func (app *Application) Init() cache.Store { return redis } + if driver == "memory" { + memory, err := NewMemory() + if err != nil { + color.Redf("[Cache] Init memory driver error: %v\n", err) + return nil + } + + return memory + } + if driver == "custom" { if custom, ok := facades.Config.Get("cache.stores." + defaultStore + ".via").(cache.Store); ok { return custom diff --git a/cache/application_test.go b/cache/application_test.go index 7edd23cc9..989c007f9 100644 --- a/cache/application_test.go +++ b/cache/application_test.go @@ -16,19 +16,24 @@ import ( type ApplicationTestSuite struct { suite.Suite - stores []cache.Store + stores map[string]cache.Store redisDocker *dockertest.Resource } func TestApplicationTestSuite(t *testing.T) { redisPool, redisDocker, redisStore, err := getRedisDocker() if err != nil { - log.Fatalf("Get redis error: %s", err) + log.Fatalf("Get redis store error: %s", err) + } + memoryStore, err := getMemoryStore() + if err != nil { + log.Fatalf("Get memory store error: %s", err) } suite.Run(t, &ApplicationTestSuite{ - stores: []cache.Store{ - redisStore, + stores: map[string]cache.Store{ + "redis": redisStore, + "memory": memoryStore, }, redisDocker: redisDocker, }) @@ -88,145 +93,171 @@ func (s *ApplicationTestSuite) TestInitRedis() { } func (s *ApplicationTestSuite) TestAdd() { - for _, store := range s.stores { - s.Nil(store.Put("name", "Goravel", 1*time.Second)) - s.False(store.Add("name", "World", 1*time.Second)) - s.True(store.Add("name1", "World", 1*time.Second)) - s.True(store.Has("name1")) - time.Sleep(2 * time.Second) - s.False(store.Has("name1")) - s.True(store.Flush()) + for name, store := range s.stores { + s.Run(name, func() { + s.Nil(store.Put("name", "Goravel", 1*time.Second)) + s.False(store.Add("name", "World", 1*time.Second)) + s.True(store.Add("name1", "World", 1*time.Second)) + s.True(store.Has("name1")) + time.Sleep(2 * time.Second) + s.False(store.Has("name1")) + s.True(store.Flush()) + }) } } func (s *ApplicationTestSuite) TestForever() { - for _, store := range s.stores { - s.True(store.Forever("name", "Goravel")) - s.Equal("Goravel", store.Get("name", "").(string)) - s.True(store.Flush()) + for name, store := range s.stores { + s.Run(name, func() { + s.True(store.Forever("name", "Goravel")) + s.Equal("Goravel", store.Get("name", "").(string)) + s.True(store.Flush()) + }) } } func (s *ApplicationTestSuite) TestForget() { - for _, store := range s.stores { - val := store.Forget("test-forget") - s.True(val) - - err := store.Put("test-forget", "goravel", 5*time.Second) - s.Nil(err) - s.True(store.Forget("test-forget")) + for name, store := range s.stores { + s.Run(name, func() { + val := store.Forget("test-forget") + s.True(val) + + err := store.Put("test-forget", "goravel", 5*time.Second) + s.Nil(err) + s.True(store.Forget("test-forget")) + }) } } func (s *ApplicationTestSuite) TestFlush() { - for _, store := range s.stores { - s.Nil(store.Put("test-flush", "goravel", 5*time.Second)) - s.Equal("goravel", store.Get("test-flush", nil).(string)) + for name, store := range s.stores { + s.Run(name, func() { + s.Nil(store.Put("test-flush", "goravel", 5*time.Second)) + s.Equal("goravel", store.Get("test-flush", nil).(string)) - s.True(store.Flush()) - s.False(store.Has("test-flush")) + s.True(store.Flush()) + s.False(store.Has("test-flush")) + }) } } func (s *ApplicationTestSuite) TestGet() { - for _, store := range s.stores { - s.Nil(store.Put("name", "Goravel", 1*time.Second)) - s.Equal("Goravel", store.Get("name", "").(string)) - s.Equal("World", store.Get("name1", "World").(string)) - s.Equal("World1", store.Get("name2", func() any { - return "World1" - }).(string)) - s.True(store.Forget("name")) - s.True(store.Flush()) + for name, store := range s.stores { + s.Run(name, func() { + s.Nil(store.Put("name", "Goravel", 1*time.Second)) + s.Equal("Goravel", store.Get("name", "").(string)) + s.Equal("World", store.Get("name1", "World").(string)) + s.Equal("World1", store.Get("name2", func() any { + return "World1" + }).(string)) + s.True(store.Forget("name")) + s.True(store.Flush()) + }) } } func (s *ApplicationTestSuite) TestGetBool() { - for _, store := range s.stores { - s.Equal(true, store.GetBool("test-get-bool", true)) - s.Nil(store.Put("test-get-bool", true, 2*time.Second)) - s.Equal(true, store.GetBool("test-get-bool", false)) + for name, store := range s.stores { + s.Run(name, func() { + s.Equal(true, store.GetBool("test-get-bool", true)) + s.Nil(store.Put("test-get-bool", true, 2*time.Second)) + s.Equal(true, store.GetBool("test-get-bool", false)) + }) } } func (s *ApplicationTestSuite) TestGetInt() { - for _, store := range s.stores { - s.Equal(2, store.GetInt("test-get-int", 2)) - s.Nil(store.Put("test-get-int", 3, 2*time.Second)) - s.Equal(3, store.GetInt("test-get-int", 2)) + for name, store := range s.stores { + s.Run(name, func() { + s.Equal(2, store.GetInt("test-get-int", 2)) + s.Nil(store.Put("test-get-int", 3, 2*time.Second)) + s.Equal(3, store.GetInt("test-get-int", 2)) + }) } } func (s *ApplicationTestSuite) TestGetString() { - for _, store := range s.stores { - s.Equal("2", store.GetString("test-get-string", "2")) - s.Nil(store.Put("test-get-string", "3", 2*time.Second)) - s.Equal("3", store.GetString("test-get-string", "2")) + for name, store := range s.stores { + s.Run(name, func() { + s.Equal("2", store.GetString("test-get-string", "2")) + s.Nil(store.Put("test-get-string", "3", 2*time.Second)) + s.Equal("3", store.GetString("test-get-string", "2")) + }) } } func (s *ApplicationTestSuite) TestHas() { - for _, store := range s.stores { - s.False(store.Has("test-has")) - s.Nil(store.Put("test-has", "goravel", 5*time.Second)) - s.True(store.Has("test-has")) + for name, store := range s.stores { + s.Run(name, func() { + s.False(store.Has("test-has")) + s.Nil(store.Put("test-has", "goravel", 5*time.Second)) + s.True(store.Has("test-has")) + }) } } func (s *ApplicationTestSuite) TestPull() { - for _, store := range s.stores { - s.Nil(store.Put("name", "Goravel", 1*time.Second)) - s.True(store.Has("name")) - s.Equal("Goravel", store.Pull("name", "").(string)) - s.False(store.Has("name")) + for name, store := range s.stores { + s.Run(name, func() { + s.Nil(store.Put("name", "Goravel", 1*time.Second)) + s.True(store.Has("name")) + s.Equal("Goravel", store.Pull("name", "").(string)) + s.False(store.Has("name")) + }) } } func (s *ApplicationTestSuite) TestPut() { - for _, store := range s.stores { - s.Nil(store.Put("name", "Goravel", 1*time.Second)) - s.True(store.Has("name")) - s.Equal("Goravel", store.Get("name", "").(string)) - time.Sleep(2 * time.Second) - s.False(store.Has("name")) + for name, store := range s.stores { + s.Run(name, func() { + s.Nil(store.Put("name", "Goravel", 1*time.Second)) + s.True(store.Has("name")) + s.Equal("Goravel", store.Get("name", "").(string)) + time.Sleep(2 * time.Second) + s.False(store.Has("name")) + }) } } func (s *ApplicationTestSuite) TestRemember() { - for _, store := range s.stores { - s.Nil(store.Put("name", "Goravel", 1*time.Second)) - value, err := store.Remember("name", 1*time.Second, func() any { - return "World" - }) - s.Nil(err) - s.Equal("Goravel", value) - - value, err = store.Remember("name1", 1*time.Second, func() any { - return "World1" + for name, store := range s.stores { + s.Run(name, func() { + s.Nil(store.Put("name", "Goravel", 1*time.Second)) + value, err := store.Remember("name", 1*time.Second, func() any { + return "World" + }) + s.Nil(err) + s.Equal("Goravel", value) + + value, err = store.Remember("name1", 1*time.Second, func() any { + return "World1" + }) + s.Nil(err) + s.Equal("World1", value) + time.Sleep(2 * time.Second) + s.False(store.Has("name1")) + s.True(store.Flush()) }) - s.Nil(err) - s.Equal("World1", value) - time.Sleep(2 * time.Second) - s.False(store.Has("name1")) - s.True(store.Flush()) } } func (s *ApplicationTestSuite) TestRememberForever() { - for _, store := range s.stores { - s.Nil(store.Put("name", "Goravel", 1*time.Second)) - value, err := store.RememberForever("name", func() any { - return "World" + for name, store := range s.stores { + s.Run(name, func() { + s.Nil(store.Put("name", "Goravel", 1*time.Second)) + value, err := store.RememberForever("name", func() any { + return "World" + }) + s.Nil(err) + s.Equal("Goravel", value) + + value, err = store.RememberForever("name1", func() any { + return "World1" + }) + s.Nil(err) + s.Equal("World1", value) + s.True(store.Flush()) }) - s.Nil(err) - s.Equal("Goravel", value) - - value, err = store.RememberForever("name1", func() any { - return "World1" - }) - s.Nil(err) - s.Equal("World1", value) - s.True(store.Flush()) } } @@ -271,6 +302,18 @@ func getRedisDocker() (*dockertest.Pool, *dockertest.Resource, cache.Store, erro return pool, resource, store, nil } +func getMemoryStore() (*Memory, error) { + mockConfig := mock.Config() + mockConfig.On("GetString", "cache.prefix").Return("goravel_cache").Once() + + memory, err := NewMemory() + if err != nil { + return nil, err + } + + return memory, nil +} + type Store struct { } diff --git a/cache/memory.go b/cache/memory.go new file mode 100644 index 000000000..00c4e101f --- /dev/null +++ b/cache/memory.go @@ -0,0 +1,148 @@ +package cache + +import ( + "context" + "time" + + "github.com/patrickmn/go-cache" + + cachecontract "github.com/goravel/framework/contracts/cache" +) + +type Memory struct { + ctx context.Context + prefix string + instance *cache.Cache +} + +func NewMemory() (*Memory, error) { + memory := cache.New(5*time.Minute, 10*time.Minute) + + return &Memory{ + prefix: prefix(), + instance: memory, + }, nil +} + +func (r *Memory) WithContext(ctx context.Context) cachecontract.Store { + return r +} + +//Add Store an item in the cache if the key does not exist. +func (r *Memory) Add(key string, value any, seconds time.Duration) bool { + if err := r.instance.Add(r.prefix+key, value, seconds); err != nil { + return false + } + + return true +} + +//Forever Store an item in the cache indefinitely. +func (r *Memory) Forever(key string, value any) bool { + if err := r.Put(key, value, cache.NoExpiration); err != nil { + return false + } + + return true +} + +//Forget Remove an item from the cache. +func (r *Memory) Forget(key string) bool { + r.instance.Delete(r.prefix + key) + + return true +} + +//Flush Remove all items from the cache. +func (r *Memory) Flush() bool { + r.instance.Flush() + + return true +} + +//Get Retrieve an item from the cache by key. +func (r *Memory) Get(key string, def any) any { + val, exist := r.instance.Get(r.prefix + key) + if exist { + return val + } + + switch s := def.(type) { + case func() any: + return s() + default: + return def + } +} + +func (r *Memory) GetBool(key string, def bool) bool { + res := r.Get(key, def) + + return res.(bool) +} + +func (r *Memory) GetInt(key string, def int) int { + res := r.Get(key, def) + + return res.(int) +} + +func (r *Memory) GetString(key string, def string) string { + return r.Get(key, def).(string) +} + +//Has Check an item exists in the cache. +func (r *Memory) Has(key string) bool { + _, exist := r.instance.Get(r.prefix + key) + + return exist +} + +//Pull Retrieve an item from the cache and delete it. +func (r *Memory) Pull(key string, def any) any { + res := r.Get(key, def) + r.Forget(key) + + return res +} + +//Put Store an item in the cache for a given number of seconds. +func (r *Memory) Put(key string, value any, seconds time.Duration) error { + r.instance.Set(r.prefix+key, value, seconds) + + return nil +} + +//Remember Get an item from the cache, or execute the given Closure and store the result. +func (r *Memory) Remember(key string, seconds time.Duration, callback func() any) (any, error) { + val := r.Get(key, nil) + + if val != nil { + return val, nil + } + + val = callback() + + if err := r.Put(key, val, seconds); err != nil { + return nil, err + } + + return val, nil +} + +//RememberForever Get an item from the cache, or execute the given Closure and store the result forever. +func (r *Memory) RememberForever(key string, callback func() any) (any, error) { + val := r.Get(key, nil) + + if val != nil { + return val, nil + } + + val = callback() + + if err := r.Put(key, val, cache.NoExpiration); err != nil { + return nil, err + } + + return val, nil +} diff --git a/cache/redis.go b/cache/redis.go index df50dff68..0560d2c96 100644 --- a/cache/redis.go +++ b/cache/redis.go @@ -42,7 +42,7 @@ func NewRedis(ctx context.Context) (*Redis, error) { return &Redis{ ctx: ctx, - prefix: facades.Config.GetString("cache.prefix") + ":", + prefix: prefix(), redis: client, }, nil } @@ -149,14 +149,10 @@ func (r *Redis) Has(key string) bool { //Pull Retrieve an item from the cache and delete it. func (r *Redis) Pull(key string, def any) any { - val, err := r.redis.Get(r.ctx, r.prefix+key).Result() - r.redis.Del(r.ctx, r.prefix+key) - - if err != nil { - return def - } + res := r.Get(key, def) + r.Forget(key) - return val + return res } //Put Store an item in the cache for a given number of seconds. @@ -170,7 +166,7 @@ func (r *Redis) Put(key string, value any, seconds time.Duration) error { } //Remember Get an item from the cache, or execute the given Closure and store the result. -func (r *Redis) Remember(key string, ttl time.Duration, callback func() any) (any, error) { +func (r *Redis) Remember(key string, seconds time.Duration, callback func() any) (any, error) { val := r.Get(key, nil) if val != nil { @@ -179,7 +175,7 @@ func (r *Redis) Remember(key string, ttl time.Duration, callback func() any) (an val = callback() - if err := r.Put(key, val, ttl); err != nil { + if err := r.Put(key, val, seconds); err != nil { return nil, err } diff --git a/cache/utils.go b/cache/utils.go new file mode 100644 index 000000000..cc13f6df1 --- /dev/null +++ b/cache/utils.go @@ -0,0 +1,9 @@ +package cache + +import ( + "github.com/goravel/framework/facades" +) + +func prefix() string { + return facades.Config.GetString("cache.prefix") + ":" +} diff --git a/go.mod b/go.mod index 3268bd10b..c0385d01b 100644 --- a/go.mod +++ b/go.mod @@ -127,6 +127,7 @@ require ( github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.0.2 // indirect github.com/opencontainers/runc v1.1.4 // indirect + github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pelletier/go-toml v1.9.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect diff --git a/go.sum b/go.sum index 8c06a3c70..c5f0c30f5 100644 --- a/go.sum +++ b/go.sum @@ -568,6 +568,8 @@ github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYr github.com/ory/dockertest/v3 v3.9.1 h1:v4dkG+dlu76goxMiTT2j8zV7s4oPPEppKT8K8p2f1kY= github.com/ory/dockertest/v3 v3.9.1/go.mod h1:42Ir9hmvaAPm0Mgibk6mBPi7SFvTXxEcnztDYOJ//uM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc= +github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=