├── .gitignore ├── docs ├── images │ ├── mget.png │ ├── banner.png │ ├── stats.png │ ├── autorefresh.png │ └── singleflight.png ├── CN │ ├── Plugin.md │ ├── Stat.md │ ├── Embedded.md │ ├── CacheAPI.md │ ├── GettingStarted.md │ └── Config.md └── EN │ ├── Plugin.md │ ├── Stat.md │ ├── Embedded.md │ ├── GettingStarted.md │ ├── CacheAPI.md │ └── Config.md ├── local ├── size.go ├── local.go ├── tinylfu.go ├── tinylfu_test.go ├── freecache.go └── freecache_test.go ├── .github ├── dependabot.yml └── workflows │ └── go.yml ├── util ├── recovery_test.go ├── recovery.go ├── zerocopy.go ├── merge.go ├── saferand.go ├── saferand_test.go ├── merge_test.go └── zerocopy_test.go ├── Makefile ├── encoding ├── json │ ├── json.go │ └── json_test.go ├── sonic │ ├── sonic.go │ └── sonic_test.go ├── msgpack │ ├── msgpack_test.go │ └── msgpack.go ├── encoding.go └── encoding_test.go ├── remote ├── remote.go ├── goredisv9adapter.go └── goredisv9adapter_test.go ├── logger ├── default.go ├── logger.go ├── default_test.go └── logger_test.go ├── LICENSE ├── go.mod ├── item_test.go ├── stats ├── stats_test.go ├── stats.go ├── statslogger_test.go └── statslogger.go ├── item.go ├── example_cache_test.go ├── cacheopt_test.go ├── bench_test.go ├── README_zh.md ├── cacheopt.go ├── README.md ├── cachegeneric.go └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | /.idea 3 | /.vscode 4 | -------------------------------------------------------------------------------- /docs/images/mget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgtv-tech/jetcache-go/HEAD/docs/images/mget.png -------------------------------------------------------------------------------- /docs/images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgtv-tech/jetcache-go/HEAD/docs/images/banner.png -------------------------------------------------------------------------------- /docs/images/stats.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgtv-tech/jetcache-go/HEAD/docs/images/stats.png -------------------------------------------------------------------------------- /docs/images/autorefresh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgtv-tech/jetcache-go/HEAD/docs/images/autorefresh.png -------------------------------------------------------------------------------- /docs/images/singleflight.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mgtv-tech/jetcache-go/HEAD/docs/images/singleflight.png -------------------------------------------------------------------------------- /local/size.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | type Size int64 4 | 5 | const ( 6 | Byte Size = 1 7 | KB = 1024 * Byte 8 | MB = 1024 * KB 9 | GB = 1024 * MB 10 | TB = 1024 * GB 11 | ) 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "gomod" # See documentation for possible values 5 | directory: "/" # Location of package manifests 6 | schedule: 7 | interval: "weekly" 8 | -------------------------------------------------------------------------------- /util/recovery_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestWithRecover(t *testing.T) { 10 | t.Run("test not panic", func(t *testing.T) { 11 | assert.NotPanics(t, func() { 12 | WithRecover(func() { 13 | panic("panic") 14 | }) 15 | }) 16 | }) 17 | } 18 | -------------------------------------------------------------------------------- /util/recovery.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "runtime/debug" 5 | "strings" 6 | 7 | "github.com/mgtv-tech/jetcache-go/logger" 8 | ) 9 | 10 | func WithRecover(fn func()) { 11 | defer func() { 12 | if err := recover(); err != nil { 13 | logger.Error("%+v\n\n%s", err, strings.TrimSpace(string(debug.Stack()))) 14 | } 15 | }() 16 | 17 | fn() 18 | } 19 | -------------------------------------------------------------------------------- /util/zerocopy.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "unsafe" 5 | ) 6 | 7 | // String converts byte slice to string. 8 | func String(b []byte) string { 9 | return *(*string)(unsafe.Pointer(&b)) 10 | } 11 | 12 | // Bytes converts string to byte slice. 13 | func Bytes(s string) []byte { 14 | return *(*[]byte)(unsafe.Pointer( 15 | &struct { 16 | string 17 | Cap int 18 | }{s, len(s)}, 19 | )) 20 | } 21 | -------------------------------------------------------------------------------- /local/local.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | type Local interface { 4 | // Set stores the given data with the specified key. 5 | Set(key string, data []byte) 6 | 7 | // Get retrieves the data associated with the specified key. 8 | // It returns the data and a boolean indicating whether the key was found. 9 | Get(key string) ([]byte, bool) 10 | 11 | // Del deletes the data associated with the specified key. 12 | Del(key string) 13 | } 14 | -------------------------------------------------------------------------------- /util/merge.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | // MergeMap merge maps, next key will overwrite previous key. 4 | func MergeMap[K comparable, V any](maps ...map[K]V) map[K]V { 5 | if len(maps) == 0 { 6 | return nil 7 | } 8 | 9 | if len(maps) == 1 { 10 | return maps[0] 11 | } 12 | 13 | result := maps[0] 14 | if result == nil { 15 | result = make(map[K]V) 16 | } 17 | 18 | for _, m := range maps[1:] { 19 | for k, v := range m { 20 | result[k] = v 21 | } 22 | } 23 | 24 | return result 25 | } 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := test 2 | 3 | # Run tests and generates html coverage file 4 | cover: test 5 | @go tool cover -html=./cover.text -o ./cover.html 6 | @test -f ./cover.out && rm ./cover.out; 7 | .PHONY: cover 8 | 9 | # Run linters 10 | lint: 11 | @golangci-lint run ./... 12 | .PHONY: lint 13 | 14 | # Run test 15 | test: 16 | @go test ./... 17 | @go test ./... -short -race 18 | @go test ./... -run=NONE -bench=. -benchmem 19 | .PHONY: test 20 | 21 | # Run test-coverage 22 | test-coverage: 23 | @go test ./... -cpu=4 -race -coverprofile=coverage.txt -covermode=atomic 24 | .PHONY: test-coverage 25 | -------------------------------------------------------------------------------- /encoding/json/json.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/mgtv-tech/jetcache-go/encoding" 7 | ) 8 | 9 | // Name is the name registered for the json codec. 10 | const Name = "json" 11 | 12 | func init() { 13 | encoding.RegisterCodec(codec{}) 14 | } 15 | 16 | // codec is a Codec implementation with json. 17 | type codec struct{} 18 | 19 | func (codec) Marshal(v any) ([]byte, error) { 20 | return json.Marshal(v) 21 | } 22 | 23 | func (codec) Unmarshal(data []byte, v any) error { 24 | return json.Unmarshal(data, v) 25 | } 26 | 27 | func (codec) Name() string { 28 | return Name 29 | } 30 | -------------------------------------------------------------------------------- /encoding/sonic/sonic.go: -------------------------------------------------------------------------------- 1 | package sonic 2 | 3 | import ( 4 | "github.com/bytedance/sonic" 5 | "github.com/mgtv-tech/jetcache-go/encoding" 6 | ) 7 | 8 | // Name is the name registered for the json codec. 9 | const Name = "sonic" 10 | 11 | func init() { 12 | encoding.RegisterCodec(codec{}) 13 | } 14 | 15 | // codec is a Codec implementation with json. 16 | type codec struct{} 17 | 18 | func (codec) Marshal(v interface{}) ([]byte, error) { 19 | return sonic.Marshal(v) 20 | } 21 | 22 | func (codec) Unmarshal(data []byte, v interface{}) error { 23 | return sonic.Unmarshal(data, v) 24 | } 25 | 26 | func (codec) Name() string { 27 | return Name 28 | } 29 | -------------------------------------------------------------------------------- /util/saferand.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "math/rand" 6 | "sync" 7 | "time" 8 | ) 9 | 10 | type SafeRand struct { 11 | mu *sync.Mutex 12 | rand *rand.Rand 13 | } 14 | 15 | func NewSafeRand() *SafeRand { 16 | return &SafeRand{ 17 | mu: new(sync.Mutex), 18 | rand: rand.New(rand.NewSource(time.Now().UnixNano())), 19 | } 20 | } 21 | 22 | func (r *SafeRand) Int63n(n int64) int64 { 23 | r.mu.Lock() 24 | val := r.rand.Int63n(n) 25 | r.mu.Unlock() 26 | return val 27 | } 28 | 29 | func (r *SafeRand) RandN(n int) string { 30 | r.mu.Lock() 31 | randBytes := make([]byte, n/2) 32 | r.rand.Read(randBytes) 33 | val := fmt.Sprintf("%x", randBytes) 34 | r.mu.Unlock() 35 | return val 36 | } 37 | -------------------------------------------------------------------------------- /util/saferand_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestRandSafe_Int63n(t *testing.T) { 10 | rand := NewSafeRand() 11 | for i := 0; i < 1000; i++ { 12 | val := rand.Int63n(1000) 13 | assert.True(t, val >= 0) 14 | assert.True(t, val < 1000) 15 | } 16 | } 17 | 18 | func TestSafeRand_RandN(t *testing.T) { 19 | rand := NewSafeRand() 20 | assert.True(t, len(rand.RandN(8)) > 0) 21 | 22 | const size = 10 23 | assert.True(t, len(rand.RandN(size)) == size) 24 | } 25 | 26 | func BenchmarkInt63ThreadSafe(b *testing.B) { 27 | rand := NewSafeRand() 28 | for n := b.N; n > 0; n-- { 29 | rand.Int63n(1000) 30 | } 31 | } 32 | 33 | func BenchmarkInt63ThreadSafeParallel(b *testing.B) { 34 | rand := NewSafeRand() 35 | b.RunParallel(func(pb *testing.PB) { 36 | for pb.Next() { 37 | rand.Int63n(1000) 38 | } 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /util/merge_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestMergeMap(t *testing.T) { 10 | t.Run("merge nil", func(t *testing.T) { 11 | actual := MergeMap[string, int]() 12 | assert.Nil(t, actual) 13 | }) 14 | 15 | t.Run("merge one map", func(t *testing.T) { 16 | m1 := map[int]string{ 17 | 1: "a", 18 | 2: "b", 19 | } 20 | expected := map[int]string{ 21 | 1: "a", 22 | 2: "b", 23 | } 24 | actual := MergeMap(m1) 25 | assert.Equal(t, expected, actual) 26 | }) 27 | 28 | t.Run("merge when map1 nil", func(t *testing.T) { 29 | var m1 map[int]string = nil 30 | m2 := map[int]string{ 31 | 1: "1", 32 | 3: "2", 33 | } 34 | expected := map[int]string{ 35 | 1: "1", 36 | 3: "2", 37 | } 38 | actual := MergeMap(m1, m2) 39 | assert.Equal(t, expected, actual) 40 | }) 41 | 42 | t.Run("merge maps", func(t *testing.T) { 43 | m1 := map[int]string{ 44 | 1: "a", 45 | 2: "b", 46 | } 47 | m2 := map[int]string{ 48 | 1: "1", 49 | 3: "2", 50 | } 51 | 52 | expected := map[int]string{ 53 | 1: "1", 54 | 2: "b", 55 | 3: "2", 56 | } 57 | actual := MergeMap(m1, m2) 58 | 59 | assert.Equal(t, expected, actual) 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /remote/remote.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | type Remote interface { 9 | // SetEX sets the expiration value for a key. 10 | SetEX(ctx context.Context, key string, value any, expire time.Duration) error 11 | 12 | // SetNX sets the value of a key if it does not already exist. 13 | SetNX(ctx context.Context, key string, value any, expire time.Duration) (val bool, err error) 14 | 15 | // SetXX sets the value of a key if it already exists. 16 | SetXX(ctx context.Context, key string, value any, expire time.Duration) (val bool, err error) 17 | 18 | // Get retrieves the value of a key. It returns errNotFound (e.g., redis.Nil) when the key does not exist. 19 | Get(ctx context.Context, key string) (val string, err error) 20 | 21 | // Del deletes the cached value associated with a key. 22 | Del(ctx context.Context, key string) (val int64, err error) 23 | 24 | // MGet retrieves the values of multiple keys. 25 | MGet(ctx context.Context, keys ...string) (map[string]any, error) 26 | 27 | // MSet sets multiple key-value pairs in the cache. 28 | MSet(ctx context.Context, value map[string]any, expire time.Duration) error 29 | 30 | // Nil returns an error indicating that the key does not exist. 31 | Nil() error 32 | } 33 | -------------------------------------------------------------------------------- /logger/default.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | "os" 7 | ) 8 | 9 | var _ Logger = (*localLogger)(nil) 10 | 11 | // SetDefaultLogger sets the default logger. 12 | // This is not concurrency safe, which means it should only be called during init. 13 | func SetDefaultLogger(l Logger) { 14 | if l == nil { 15 | panic("logger must not be nil") 16 | } 17 | defaultLogger = l 18 | } 19 | 20 | var defaultLogger Logger = &localLogger{ 21 | logger: log.New(os.Stderr, "", log.LstdFlags|log.Lshortfile|log.Lmicroseconds), 22 | } 23 | 24 | type localLogger struct { 25 | logger *log.Logger 26 | } 27 | 28 | func (ll *localLogger) logf(lv Level, format *string, v ...any) { 29 | if level > lv { 30 | return 31 | } 32 | msg := lv.String() + fmt.Sprintf(*format, v...) 33 | ll.logger.Output(4, msg) 34 | } 35 | 36 | func (ll *localLogger) Debug(format string, v ...any) { 37 | ll.logf(LevelDebug, &format, v...) 38 | } 39 | 40 | func (ll *localLogger) Info(format string, v ...any) { 41 | ll.logf(LevelInfo, &format, v...) 42 | } 43 | 44 | func (ll *localLogger) Warn(format string, v ...any) { 45 | ll.logf(LevelWarn, &format, v...) 46 | } 47 | 48 | func (ll *localLogger) Error(format string, v ...any) { 49 | ll.logf(LevelError, &format, v...) 50 | } 51 | -------------------------------------------------------------------------------- /docs/CN/Plugin.md: -------------------------------------------------------------------------------- 1 | 2 | # Overview 3 | [jetcache-go-plugin](https://github.com/mgtv-tech/jetcache-go-plugin) 是 [jetcache-go](https://github.com/mgtv-tech/jetcache-go) 维护的插件项目。 4 | 5 | # Getting started 6 | 7 | ## Remote Adapter 8 | 9 | ### [redis/go-redis v8](https://github.com/go-redis/redis/v8) 10 | ```go 11 | import ( 12 | "github.com/mgtv-tech/jetcache-go" 13 | "github.com/mgtv-tech/jetcache-go-plugin/remote" 14 | ) 15 | 16 | mycache := cache.New(cache.WithName("any"), 17 | cache.WithRemote(remote.NewGoRedisV8Adapter(ring)), 18 | cache.WithLocal(local.NewFreeCache(256*local.MB, time.Minute)), 19 | // ... 20 | ) 21 | ``` 22 | 23 | ## Local 24 | 25 | TODO 26 | 27 | ## Stats 28 | 29 | ### [prometheus](https://prometheus.io/) 30 | ```go 31 | import ( 32 | "github.com/mgtv-tech/jetcache-go" 33 | "github.com/mgtv-tech/jetcache-go-plugin/remote" 34 | pstats "github.com/mgtv-tech/jetcache-go-plugin/stats" 35 | "github.com/mgtv-tech/jetcache-go/stats" 36 | ) 37 | 38 | cacheName := "demo" 39 | jetcache := cache.New(cache.WithRemote(remote.NewGoRedisV8Adapter(ring)), 40 | cache.WithStatsHandler( 41 | stats.NewHandles(false, 42 | stats.NewStatsLogger(cacheName), 43 | pstats.NewPrometheus(cacheName)))) 44 | ``` 45 | > 同时集成日志统计和Prometheus统计。 46 | 47 | ## Encoding 48 | 49 | TODO 50 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2024, daoshenzzg 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 25 | -------------------------------------------------------------------------------- /encoding/msgpack/msgpack_test.go: -------------------------------------------------------------------------------- 1 | package msgpack 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | type testMessage struct { 12 | Id int64 13 | Name string 14 | Hobby []string 15 | } 16 | 17 | func TestName(t *testing.T) { 18 | c := new(codec) 19 | if !reflect.DeepEqual(c.Name(), "msgpack") { 20 | t.Errorf("no expect float_key value: %v, but got: %v", c.Name(), "msgpack") 21 | } 22 | } 23 | 24 | func TestMsgpackCodec(t *testing.T) { 25 | tests := []testMessage{ 26 | { 27 | Id: 1, 28 | Name: "jetcache-go", 29 | }, 30 | { 31 | Id: 1, 32 | Name: strings.Repeat("my very large string", 10), 33 | Hobby: []string{"study", "eat", "play"}, 34 | }, 35 | } 36 | 37 | for _, v := range tests { 38 | data, err := (codec{}).Marshal(&v) 39 | assert.Nilf(t, err, "Marshal() should be nil, but got %s", err) 40 | 41 | var res testMessage 42 | err = (codec{}).Unmarshal(data, &res) 43 | assert.Nilf(t, err, "Unmarshal() should be nil, but got %s", err) 44 | if !reflect.DeepEqual(res.Id, v.Id) { 45 | t.Errorf("ID should be %d, but got %d", res.Id, v.Id) 46 | } 47 | if !reflect.DeepEqual(res.Name, v.Name) { 48 | t.Errorf("Name should be %s, but got %s", res.Name, v.Name) 49 | } 50 | if !reflect.DeepEqual(res.Hobby, v.Hobby) { 51 | t.Errorf("Hobby should be %s, but got %s", res.Hobby, v.Hobby) 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /encoding/encoding.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "strings" 5 | ) 6 | 7 | // Codec defines the interface Transport uses to encode and decode messages. 8 | // Note that implementations of this interface must be thread safe; a Codec's 9 | // methods can be called from concurrent goroutines. 10 | type Codec interface { 11 | // Marshal returns the wire format of v. 12 | Marshal(v any) ([]byte, error) 13 | // Unmarshal parses the wire format into v. 14 | Unmarshal(data []byte, v any) error 15 | // Name returns the name of the Codec implementation. The returned string 16 | // will be used as part of content type in transmission. The result must be 17 | // static; the result cannot change between calls. 18 | Name() string 19 | } 20 | 21 | var registeredCodecs = make(map[string]Codec) 22 | 23 | // RegisterCodec registers the provided Codec for use with all Transport clients and 24 | // servers. 25 | func RegisterCodec(codec Codec) { 26 | if codec == nil { 27 | panic("cannot register a nil Codec") 28 | } 29 | if codec.Name() == "" { 30 | panic("cannot register Codec with empty string result for Name()") 31 | } 32 | contentSubtype := strings.ToLower(codec.Name()) 33 | registeredCodecs[contentSubtype] = codec 34 | } 35 | 36 | // GetCodec gets a registered Codec by content-subtype, or nil if no Codec is 37 | // registered for the content-subtype. 38 | // 39 | // The content-subtype is expected to be lowercase. 40 | func GetCodec(contentSubtype string) Codec { 41 | return registeredCodecs[contentSubtype] 42 | } 43 | -------------------------------------------------------------------------------- /docs/EN/Plugin.md: -------------------------------------------------------------------------------- 1 | # Overview 2 | 3 | The [jetcache-go-plugin](https://github.com/mgtv-tech/jetcache-go-plugin) project is a plugin maintained for [jetcache-go](https://github.com/mgtv-tech/jetcache-go). 4 | 5 | 6 | # Getting started 7 | 8 | ## Remote Adapter 9 | 10 | ### [redis/go-redis v8](https://github.com/go-redis/redis/v8) 11 | ```go 12 | import ( 13 | "github.com/mgtv-tech/jetcache-go" 14 | "github.com/mgtv-tech/jetcache-go-plugin/remote" 15 | ) 16 | 17 | mycache := cache.New(cache.WithName("any"), 18 | cache.WithRemote(remote.NewGoRedisV8Adapter(ring)), 19 | cache.WithLocal(local.NewFreeCache(256*local.MB, time.Minute)), 20 | // ... 21 | ) 22 | ``` 23 | 24 | ## Local 25 | 26 | TODO 27 | 28 | ## Stats 29 | 30 | ### [prometheus](https://prometheus.io/) 31 | ```go 32 | import ( 33 | "github.com/mgtv-tech/jetcache-go" 34 | "github.com/mgtv-tech/jetcache-go-plugin/remote" 35 | pstats "github.com/mgtv-tech/jetcache-go-plugin/stats" 36 | "github.com/mgtv-tech/jetcache-go/stats" 37 | ) 38 | 39 | cacheName := "demo" 40 | jetcache := cache.New(cache.WithRemote(remote.NewGoRedisV8Adapter(ring)), 41 | cache.WithStatsHandler( 42 | stats.NewHandles(false, 43 | stats.NewStatsLogger(cacheName), 44 | pstats.NewPrometheus(cacheName)))) 45 | ``` 46 | > This example demonstrates how to integrate Prometheus metrics collection with jetcache-go using the jetcache-go-plugin. It shows how to simultaneously use both logging and Prometheus for statistics. 47 | 48 | ## Encoding 49 | 50 | TODO 51 | -------------------------------------------------------------------------------- /util/zerocopy_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | var ( 12 | testStr = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 13 | testBytes = []byte(testStr) 14 | ) 15 | 16 | func TestByteStringConvert(t *testing.T) { 17 | b := Bytes(testStr) 18 | s := String(b) 19 | assert.Equal(t, testStr, s) 20 | } 21 | 22 | func TestWithGC(t *testing.T) { 23 | b := test() 24 | assert.Equal(t, string(b), "hello") 25 | fmt.Printf("%v\n", b) 26 | fmt.Printf("%v\n", b) 27 | } 28 | 29 | func test() []byte { 30 | defer runtime.GC() 31 | x := make([]byte, 5) 32 | x[0] = 'h' 33 | x[1] = 'e' 34 | x[2] = 'l' 35 | x[3] = 'l' 36 | x[4] = 'o' 37 | return Bytes(string(x)) 38 | } 39 | 40 | func BenchmarkBytesSafe(b *testing.B) { 41 | b.ResetTimer() 42 | b.ReportAllocs() 43 | b.RunParallel(func(pb *testing.PB) { 44 | for pb.Next() { 45 | _ = []byte(testStr) 46 | } 47 | }) 48 | } 49 | 50 | func BenchmarkStringSafe(b *testing.B) { 51 | b.ResetTimer() 52 | b.ReportAllocs() 53 | b.RunParallel(func(pb *testing.PB) { 54 | for pb.Next() { 55 | _ = string(testBytes) 56 | } 57 | }) 58 | } 59 | 60 | func BenchmarkBytesUnSafe(b *testing.B) { 61 | b.ResetTimer() 62 | b.ReportAllocs() 63 | b.RunParallel(func(pb *testing.PB) { 64 | for pb.Next() { 65 | Bytes(testStr) 66 | } 67 | }) 68 | } 69 | 70 | func BenchmarkStringUnSafe(b *testing.B) { 71 | b.ResetTimer() 72 | b.ReportAllocs() 73 | b.RunParallel(func(pb *testing.PB) { 74 | for pb.Next() { 75 | String(testBytes) 76 | } 77 | }) 78 | } 79 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | strategy: 15 | matrix: 16 | go: [ 1.22.x, 1.23.x, 1.24.x ] 17 | name: build & test 18 | runs-on: ubuntu-latest 19 | services: 20 | redis: 21 | image: redis 22 | options: >- 23 | --health-cmd "redis-cli ping" 24 | --health-interval 10s 25 | --health-timeout 5s 26 | --health-retries 5 27 | ports: 28 | - 6379:6379 29 | 30 | steps: 31 | - uses: actions/checkout@v4 32 | - name: Set up Go 33 | uses: actions/setup-go@v5 34 | with: 35 | go-version: ${{ matrix.go }} 36 | 37 | - name: Cache Go modules 38 | uses: actions/cache@v4 39 | with: 40 | path: | 41 | ~/.cache/go-build 42 | ~/go/pkg/mod 43 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 44 | restore-keys: | 45 | ${{ runner.os }}-go- 46 | 47 | - name: Build 48 | run: go build -v ./... 49 | 50 | - name: Test 51 | run: make test-coverage 52 | 53 | - name: Upload coverage reports to Codecov 54 | uses: codecov/codecov-action@v4 55 | env: 56 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 57 | with: 58 | file: ./coverage.txt 59 | flags: unittests 60 | -------------------------------------------------------------------------------- /encoding/msgpack/msgpack.go: -------------------------------------------------------------------------------- 1 | package msgpack 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/klauspost/compress/s2" 7 | "github.com/vmihailenco/msgpack/v5" 8 | 9 | "github.com/mgtv-tech/jetcache-go/encoding" 10 | ) 11 | 12 | // Name is the name registered for the json codec. 13 | const ( 14 | Name = "msgpack" 15 | 16 | compressionThreshold = 64 17 | timeLen = 4 18 | 19 | noCompression = 0x0 20 | s2Compression = 0x1 21 | ) 22 | 23 | func init() { 24 | encoding.RegisterCodec(codec{}) 25 | } 26 | 27 | // codec is a Codec implementation with json. 28 | type codec struct{} 29 | 30 | func (codec) Marshal(v any) ([]byte, error) { 31 | b, err := msgpack.Marshal(v) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | return compress(b), nil 37 | } 38 | 39 | func (codec) Unmarshal(data []byte, v any) error { 40 | switch c := data[len(data)-1]; c { 41 | case noCompression: 42 | data = data[:len(data)-1] 43 | case s2Compression: 44 | data = data[:len(data)-1] 45 | 46 | var err error 47 | data, err = s2.Decode(nil, data) 48 | if err != nil { 49 | return err 50 | } 51 | default: 52 | return fmt.Errorf("unknown compression method: %x", c) 53 | } 54 | 55 | return msgpack.Unmarshal(data, v) 56 | } 57 | 58 | func (codec) Name() string { 59 | return Name 60 | } 61 | 62 | func compress(data []byte) []byte { 63 | if len(data) < compressionThreshold { 64 | n := len(data) + 1 65 | b := make([]byte, n, n+timeLen) 66 | copy(b, data) 67 | b[len(b)-1] = noCompression 68 | return b 69 | } 70 | 71 | n := s2.MaxEncodedLen(len(data)) + 1 72 | b := make([]byte, n, n+timeLen) 73 | b = s2.Encode(b, data) 74 | b = append(b, s2Compression) 75 | return b 76 | } 77 | -------------------------------------------------------------------------------- /local/tinylfu.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/dgraph-io/ristretto/v2" 7 | "github.com/mgtv-tech/jetcache-go/util" 8 | ) 9 | 10 | const ( 11 | numCounters = 1e7 // number of keys to track frequency of (10M). 12 | bufferItems = 64 // number of keys per Get buffer. 13 | ) 14 | 15 | var _ Local = (*TinyLFU)(nil) 16 | 17 | type TinyLFU struct { 18 | rand *util.SafeRand 19 | cache *ristretto.Cache[string, []byte] 20 | ttl time.Duration 21 | offset time.Duration 22 | } 23 | 24 | func NewTinyLFU(size int, ttl time.Duration) *TinyLFU { 25 | const maxOffset = 10 * time.Second 26 | 27 | offset := ttl / 10 28 | if offset > maxOffset { 29 | offset = maxOffset 30 | } 31 | 32 | cache, err := ristretto.NewCache[string, []byte](&ristretto.Config[string, []byte]{ 33 | NumCounters: numCounters, 34 | MaxCost: int64(size), 35 | BufferItems: bufferItems, 36 | }) 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | return &TinyLFU{ 42 | rand: util.NewSafeRand(), 43 | cache: cache, 44 | ttl: ttl, 45 | offset: offset, 46 | } 47 | } 48 | 49 | func (c *TinyLFU) UseRandomizedTTL(offset time.Duration) { 50 | c.offset = offset 51 | } 52 | 53 | func (c *TinyLFU) Set(key string, b []byte) { 54 | ttl := c.ttl 55 | if c.offset > 0 { 56 | ttl += time.Duration(c.rand.Int63n(int64(c.offset))) 57 | } 58 | 59 | c.cache.SetWithTTL(key, b, 1, ttl) 60 | 61 | // wait for value to pass through buffers 62 | c.cache.Wait() 63 | } 64 | 65 | func (c *TinyLFU) Get(key string) ([]byte, bool) { 66 | val, ok := c.cache.Get(key) 67 | if !ok { 68 | return nil, false 69 | } 70 | 71 | return val, true 72 | } 73 | 74 | func (c *TinyLFU) Del(key string) { 75 | c.cache.Del(key) 76 | } 77 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mgtv-tech/jetcache-go 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/alicebob/miniredis/v2 v2.35.0 7 | github.com/bytedance/sonic v1.13.3 8 | github.com/coocood/freecache v1.2.4 9 | github.com/dgraph-io/ristretto/v2 v2.1.0 10 | github.com/klauspost/compress v1.17.11 11 | github.com/onsi/ginkgo v1.16.5 12 | github.com/onsi/gomega v1.34.1 13 | github.com/redis/go-redis/v9 v9.11.0 14 | github.com/stretchr/testify v1.10.0 15 | github.com/vmihailenco/msgpack/v5 v5.4.1 16 | golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e 17 | golang.org/x/sync v0.11.0 18 | ) 19 | 20 | require ( 21 | github.com/bytedance/sonic/loader v0.2.4 // indirect 22 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 23 | github.com/cloudwego/base64x v0.1.5 // indirect 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 26 | github.com/dustin/go-humanize v1.0.1 // indirect 27 | github.com/fsnotify/fsnotify v1.4.9 // indirect 28 | github.com/google/go-cmp v0.6.0 // indirect 29 | github.com/klauspost/cpuid/v2 v2.0.9 // indirect 30 | github.com/nxadm/tail v1.4.8 // indirect 31 | github.com/pkg/errors v0.9.1 // indirect 32 | github.com/pmezard/go-difflib v1.0.0 // indirect 33 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 34 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 35 | github.com/yuin/gopher-lua v1.1.1 // indirect 36 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect 37 | golang.org/x/net v0.27.0 // indirect 38 | golang.org/x/sys v0.29.0 // indirect 39 | golang.org/x/text v0.16.0 // indirect 40 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect 41 | gopkg.in/yaml.v3 v3.0.1 // indirect 42 | ) 43 | -------------------------------------------------------------------------------- /item_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestItemOptions(t *testing.T) { 12 | t.Run("default item options", func(t *testing.T) { 13 | o := newItemOptions(context.TODO(), "key") 14 | assert.Nil(t, o.value) 15 | assert.Nil(t, o.do) 16 | assert.Equal(t, defaultRemoteExpiry, o.getTtl(defaultRemoteExpiry)) 17 | assert.False(t, o.setXX) 18 | assert.False(t, o.setNX) 19 | assert.False(t, o.skipLocal) 20 | assert.False(t, o.refresh) 21 | }) 22 | 23 | t.Run("nil context", func(t *testing.T) { 24 | o := newItemOptions(nil, "key") 25 | assert.Equal(t, context.Background(), o.Context()) 26 | }) 27 | 28 | t.Run("with item options", func(t *testing.T) { 29 | o := newItemOptions(context.TODO(), "key", Value("getValue"), 30 | TTL(time.Minute), SetXX(true), SetNX(true), SkipLocal(true), 31 | Refresh(true), Do(func(context.Context) (any, error) { 32 | return "any", nil 33 | })) 34 | assert.Equal(t, "getValue", o.value) 35 | assert.NotNil(t, o.do) 36 | assert.Equal(t, time.Minute, o.getTtl(defaultRemoteExpiry)) 37 | assert.True(t, o.setXX) 38 | assert.True(t, o.setNX) 39 | assert.True(t, o.skipLocal) 40 | assert.True(t, o.refresh) 41 | }) 42 | } 43 | 44 | func TestItemTTL(t *testing.T) { 45 | tests := []struct { 46 | input time.Duration 47 | expect time.Duration 48 | }{ 49 | { 50 | input: -1, 51 | expect: 0, 52 | }, 53 | { 54 | input: time.Millisecond, 55 | expect: defaultRemoteExpiry, 56 | }, 57 | { 58 | input: time.Minute, 59 | expect: time.Minute, 60 | }, 61 | } 62 | 63 | for _, v := range tests { 64 | item := &item{ttl: v.input} 65 | assert.Equal(t, v.expect, item.getTtl(defaultRemoteExpiry)) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | // Logger is a logger anyhat provides logging function with levels. 4 | type Logger interface { 5 | Debug(format string, v ...any) 6 | Info(format string, v ...any) 7 | Warn(format string, v ...any) 8 | Error(format string, v ...any) 9 | } 10 | 11 | // Level defines the priority of a log message. 12 | // When a logger is configured with a level, any log message with a lower 13 | // log level (smaller by integer comparison) will not be output. 14 | type Level int 15 | 16 | // The levels of logs. 17 | const ( 18 | LevelDebug Level = iota 19 | LevelInfo 20 | LevelWarn 21 | LevelError 22 | ) 23 | 24 | // SetLevel sets the level of logs below which logs will not be output. 25 | // The default log level is LevelDebug. 26 | func SetLevel(lv Level) { 27 | if lv < LevelDebug || lv > LevelError { 28 | panic("invalid level") 29 | } 30 | level = lv 31 | } 32 | 33 | // Error calls the default logger's Error method. 34 | func Error(format string, v ...any) { 35 | if level > LevelError { 36 | return 37 | } 38 | defaultLogger.Error(format, v...) 39 | } 40 | 41 | // Warn calls the default logger's Warn method. 42 | func Warn(format string, v ...any) { 43 | if level > LevelWarn { 44 | return 45 | } 46 | defaultLogger.Warn(format, v...) 47 | } 48 | 49 | // Info calls the default logger's Info method. 50 | func Info(format string, v ...any) { 51 | if level > LevelInfo { 52 | return 53 | } 54 | defaultLogger.Info(format, v...) 55 | } 56 | 57 | // Debug calls the default logger's Debug method. 58 | func Debug(format string, v ...any) { 59 | if level > LevelDebug { 60 | return 61 | } 62 | defaultLogger.Debug(format, v...) 63 | } 64 | 65 | var level Level 66 | 67 | var levelNames = map[Level]string{ 68 | LevelDebug: "[DEBUG] ", 69 | LevelInfo: "[INFO] ", 70 | LevelWarn: "[WARN] ", 71 | LevelError: "[ERROR] ", 72 | } 73 | 74 | // String implementation. 75 | func (lv Level) String() string { 76 | return levelNames[lv] 77 | } 78 | -------------------------------------------------------------------------------- /docs/CN/Stat.md: -------------------------------------------------------------------------------- 1 | 2 | * [介绍](#介绍) 3 | * [LogStats 日志默认输出如下格式信息:](#logstats-日志默认输出如下格式信息) 4 | * [Prometheus 统计插件可视化大盘](#prometheus-统计插件可视化大盘) 5 | 6 | 7 | # 介绍 8 | 9 | `jetcache-go` 默认提供了内嵌 `LogStats` 及 [jetcache-go-plugin](https://github.com/mgtv-tech/jetcache-go-plugin) 提供的`Prometheus`统计插件。 10 | 11 | # LogStats 日志默认输出如下格式信息: 12 | 13 | ```shell 14 | 2024/09/25 18:45:49 jetcache-go stats last 1ms. 15 | cache | qpm| hit_ratio| hit| miss| query| query_fail 16 | ------------------------+------------+------------+------------+------------+------------+------------ 17 | any | 2| 50.00%| 1| 1| 1| 1 18 | any_local | 2| 50.00%| 1| 1| -| - 19 | any_remote | 2| 50.00%| 1| 1| -| - 20 | test_lang_cache_0 | 2| 50.00%| 1| 1| 1| 1 21 | test_lang_cache_0_local | 2| 50.00%| 1| 1| -| - 22 | test_lang_cache_0_remote| 2| 50.00%| 1| 1| -| - 23 | test_lang_cache_1 | 2| 50.00%| 1| 1| 1| 1 24 | test_lang_cache_1_local | 2| 50.00%| 1| 1| -| - 25 | test_lang_cache_1_remote| 2| 50.00%| 1| 1| -| - 26 | test_lang_cache_2 | 2| 50.00%| 1| 1| 1| 1 27 | test_lang_cache_2_local | 2| 50.00%| 1| 1| -| - 28 | test_lang_cache_2_remote| 2| 50.00%| 1| 1| -| - 29 | ------------------------+------------+------------+------------+------------+------------+------------ 30 | ``` 31 | 32 | # Prometheus 统计插件可视化大盘 33 | 34 | ![stats](/docs/images/stats.png) 35 | -------------------------------------------------------------------------------- /stats/stats_test.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import ( 4 | "errors" 5 | "sync/atomic" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | type testHandler struct { 12 | Hit uint64 13 | Miss uint64 14 | LocalHit uint64 15 | LocalMiss uint64 16 | RemoteHit uint64 17 | RemoteMiss uint64 18 | Query uint64 19 | QueryFail uint64 20 | } 21 | 22 | func TestNewHandles(t *testing.T) { 23 | tests := []struct { 24 | input bool 25 | expect uint64 26 | }{ 27 | { 28 | input: false, 29 | expect: 1, 30 | }, 31 | { 32 | input: true, 33 | expect: 0, 34 | }, 35 | } 36 | for _, v := range tests { 37 | var handler testHandler 38 | h := NewHandles(v.input, &handler) 39 | h.IncrHit() 40 | h.IncrMiss() 41 | h.IncrLocalHit() 42 | h.IncrLocalMiss() 43 | h.IncrRemoteHit() 44 | h.IncrRemoteMiss() 45 | h.IncrQuery() 46 | h.IncrQueryFail(errors.New("any")) 47 | 48 | assert.Equal(t, v.expect, handler.Hit) 49 | assert.Equal(t, v.expect, handler.Miss) 50 | assert.Equal(t, v.expect, handler.LocalHit) 51 | assert.Equal(t, v.expect, handler.LocalMiss) 52 | assert.Equal(t, v.expect, handler.RemoteHit) 53 | assert.Equal(t, v.expect, handler.RemoteMiss) 54 | assert.Equal(t, v.expect, handler.Query) 55 | assert.Equal(t, v.expect, handler.QueryFail) 56 | } 57 | } 58 | 59 | func (h *testHandler) IncrHit() { 60 | atomic.AddUint64(&h.Hit, 1) 61 | } 62 | 63 | func (h *testHandler) IncrMiss() { 64 | atomic.AddUint64(&h.Miss, 1) 65 | } 66 | 67 | func (h *testHandler) IncrLocalHit() { 68 | atomic.AddUint64(&h.LocalHit, 1) 69 | } 70 | 71 | func (h *testHandler) IncrLocalMiss() { 72 | atomic.AddUint64(&h.LocalMiss, 1) 73 | } 74 | 75 | func (h *testHandler) IncrRemoteHit() { 76 | atomic.AddUint64(&h.RemoteHit, 1) 77 | } 78 | 79 | func (h *testHandler) IncrRemoteMiss() { 80 | atomic.AddUint64(&h.RemoteMiss, 1) 81 | } 82 | 83 | func (h *testHandler) IncrQuery() { 84 | atomic.AddUint64(&h.Query, 1) 85 | } 86 | 87 | func (h *testHandler) IncrQueryFail(err error) { 88 | atomic.AddUint64(&h.QueryFail, 1) 89 | } 90 | -------------------------------------------------------------------------------- /stats/stats.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | type ( 4 | // Handler defines the interface that the Transport uses to collect cache metrics. 5 | // Note that implementations of this interface must be thread-safe; the methods of a Handler 6 | // can be called from concurrent goroutines. 7 | Handler interface { 8 | IncrHit() 9 | IncrMiss() 10 | IncrLocalHit() 11 | IncrLocalMiss() 12 | IncrRemoteHit() 13 | IncrRemoteMiss() 14 | IncrQuery() 15 | IncrQueryFail(err error) 16 | } 17 | 18 | Handlers struct { 19 | disable bool 20 | handlers []Handler 21 | } 22 | ) 23 | 24 | // NewHandles creates a new instance of Handlers. 25 | func NewHandles(disable bool, handlers ...Handler) Handler { 26 | return &Handlers{ 27 | disable: disable, 28 | handlers: handlers, 29 | } 30 | } 31 | 32 | func (hs *Handlers) IncrHit() { 33 | if hs.disable { 34 | return 35 | } 36 | 37 | for _, h := range hs.handlers { 38 | h.IncrHit() 39 | } 40 | } 41 | 42 | func (hs *Handlers) IncrMiss() { 43 | if hs.disable { 44 | return 45 | } 46 | 47 | for _, h := range hs.handlers { 48 | h.IncrMiss() 49 | } 50 | } 51 | 52 | func (hs *Handlers) IncrLocalHit() { 53 | if hs.disable { 54 | return 55 | } 56 | 57 | for _, h := range hs.handlers { 58 | h.IncrLocalHit() 59 | } 60 | } 61 | 62 | func (hs *Handlers) IncrLocalMiss() { 63 | if hs.disable { 64 | return 65 | } 66 | 67 | for _, h := range hs.handlers { 68 | h.IncrLocalMiss() 69 | } 70 | } 71 | 72 | func (hs *Handlers) IncrRemoteHit() { 73 | if hs.disable { 74 | return 75 | } 76 | 77 | for _, h := range hs.handlers { 78 | h.IncrRemoteHit() 79 | } 80 | } 81 | 82 | func (hs *Handlers) IncrRemoteMiss() { 83 | if hs.disable { 84 | return 85 | } 86 | 87 | for _, h := range hs.handlers { 88 | h.IncrRemoteMiss() 89 | } 90 | } 91 | 92 | func (hs *Handlers) IncrQuery() { 93 | if hs.disable { 94 | return 95 | } 96 | 97 | for _, h := range hs.handlers { 98 | h.IncrQuery() 99 | } 100 | } 101 | 102 | func (hs *Handlers) IncrQueryFail(err error) { 103 | if hs.disable { 104 | return 105 | } 106 | 107 | for _, h := range hs.handlers { 108 | h.IncrQueryFail(err) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /docs/EN/Stat.md: -------------------------------------------------------------------------------- 1 | 2 | * [Introduction](#introduction) 3 | * [LogStats Default Output Format](#logstats-default-output-format) 4 | * [Prometheus Plugin Visualization Dashboard](#prometheus-plugin-visualization-dashboard) 5 | 6 | 7 | # Introduction 8 | 9 | `jetcache-go` provides built-in `LogStats` and a `Prometheus` statistics plugin via the [jetcache-go-plugin](https://github.com/mgtv-tech/jetcache-go-plugin). 10 | 11 | 12 | # LogStats Default Output Format 13 | 14 | The default log output from LogStats follows this format: 15 | 16 | ```shell 17 | 2024/09/25 18:45:49 jetcache-go stats last 1ms. 18 | cache | qpm| hit_ratio| hit| miss| query| query_fail 19 | ------------------------+------------+------------+------------+------------+------------+------------ 20 | any | 2| 50.00%| 1| 1| 1| 1 21 | any_local | 2| 50.00%| 1| 1| -| - 22 | any_remote | 2| 50.00%| 1| 1| -| - 23 | test_lang_cache_0 | 2| 50.00%| 1| 1| 1| 1 24 | test_lang_cache_0_local | 2| 50.00%| 1| 1| -| - 25 | test_lang_cache_0_remote| 2| 50.00%| 1| 1| -| - 26 | test_lang_cache_1 | 2| 50.00%| 1| 1| 1| 1 27 | test_lang_cache_1_local | 2| 50.00%| 1| 1| -| - 28 | test_lang_cache_1_remote| 2| 50.00%| 1| 1| -| - 29 | test_lang_cache_2 | 2| 50.00%| 1| 1| 1| 1 30 | test_lang_cache_2_local | 2| 50.00%| 1| 1| -| - 31 | test_lang_cache_2_remote| 2| 50.00%| 1| 1| -| - 32 | ------------------------+------------+------------+------------+------------+------------+------------ 33 | ``` 34 | 35 | # Prometheus Plugin Visualization Dashboard 36 | 37 | ![stats](/docs/images/stats.png) 38 | -------------------------------------------------------------------------------- /local/tinylfu_test.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestNewTinyLFU(t *testing.T) { 14 | cache := NewTinyLFU(1000, time.Second) 15 | assert.Equal(t, time.Second/10, cache.offset) 16 | cache.UseRandomizedTTL(time.Millisecond) 17 | assert.Equal(t, time.Millisecond, cache.offset) 18 | 19 | key1 := "key1" 20 | val, exists := cache.Get(key1) 21 | assert.False(t, exists) 22 | assert.Equal(t, []byte(nil), val) 23 | 24 | cache.Set(key1, []byte("value1")) 25 | val, exists = cache.Get(key1) 26 | assert.True(t, exists) 27 | assert.Equal(t, []byte("value1"), val) 28 | 29 | cache.Del(key1) 30 | val, exists = cache.Get(key1) 31 | assert.False(t, exists) 32 | assert.Equal(t, []byte(nil), val) 33 | } 34 | 35 | // fix: https://github.com/go-redis/cache/issues/105 36 | func TestTinyLFU_SetAndGet(t *testing.T) { 37 | lfu := NewTinyLFU(100, time.Second) 38 | lfu.Set("a", []byte("a")) 39 | lfu.Set("a", []byte("b")) 40 | lfu.Set("a", []byte("c")) 41 | value, ok := lfu.Get("a") 42 | 43 | if !ok { 44 | t.Errorf("expected=true got=false") 45 | } 46 | if string(value) != "c" { 47 | t.Errorf("expected=c got=%s", value) 48 | } 49 | } 50 | 51 | func TestTinyLFUGetCorruptionOnExpiry(t *testing.T) { 52 | strFor := func(i int) string { 53 | return fmt.Sprintf("a string %d", i) 54 | } 55 | keyName := func(i int) string { 56 | return fmt.Sprintf("key-%00000d", i) 57 | } 58 | 59 | cache := NewTinyLFU(1000, 1*time.Second) 60 | size := 50000 61 | // Put a bunch of stuff in the cache with a TTL of 1 second 62 | for i := 0; i < size; i++ { 63 | key := keyName(i) 64 | cache.Set(key, []byte(strFor(i))) 65 | } 66 | 67 | // Read stuff for a bit longer than the TTL - that's when the corruption occurs 68 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 69 | defer cancel() 70 | 71 | done := ctx.Done() 72 | loop: 73 | for { 74 | select { 75 | case <-done: 76 | // this is expected 77 | break loop 78 | default: 79 | i := rand.Intn(size) 80 | key := keyName(i) 81 | 82 | b, ok := cache.Get(key) 83 | if !ok { 84 | continue loop 85 | } 86 | 87 | got := string(b) 88 | expected := strFor(i) 89 | if got != expected { 90 | t.Fatalf("expected=%q got=%q key=%q", expected, got, key) 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /remote/goredisv9adapter.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "time" 7 | 8 | "github.com/redis/go-redis/v9" 9 | ) 10 | 11 | var _ Remote = (*GoRedisV9Adapter)(nil) 12 | 13 | type GoRedisV9Adapter struct { 14 | client redis.Cmdable 15 | } 16 | 17 | // NewGoRedisV9Adapter is 18 | func NewGoRedisV9Adapter(client redis.Cmdable) Remote { 19 | return &GoRedisV9Adapter{ 20 | client: client, 21 | } 22 | } 23 | 24 | func (r *GoRedisV9Adapter) SetEX(ctx context.Context, key string, value any, expire time.Duration) error { 25 | return r.client.SetEx(ctx, key, value, expire).Err() 26 | } 27 | 28 | func (r *GoRedisV9Adapter) SetNX(ctx context.Context, key string, value any, expire time.Duration) (val bool, err error) { 29 | return r.client.SetNX(ctx, key, value, expire).Result() 30 | } 31 | 32 | func (r *GoRedisV9Adapter) SetXX(ctx context.Context, key string, value any, expire time.Duration) (val bool, err error) { 33 | return r.client.SetXX(ctx, key, value, expire).Result() 34 | } 35 | 36 | func (r *GoRedisV9Adapter) Get(ctx context.Context, key string) (val string, err error) { 37 | return r.client.Get(ctx, key).Result() 38 | } 39 | 40 | func (r *GoRedisV9Adapter) Del(ctx context.Context, key string) (val int64, err error) { 41 | return r.client.Del(ctx, key).Result() 42 | } 43 | 44 | func (r *GoRedisV9Adapter) MGet(ctx context.Context, keys ...string) (map[string]any, error) { 45 | pipeline := r.client.Pipeline() 46 | keyIdxMap := make(map[int]string, len(keys)) 47 | ret := make(map[string]any, len(keys)) 48 | 49 | for idx, key := range keys { 50 | keyIdxMap[idx] = key 51 | pipeline.Get(ctx, key) 52 | } 53 | 54 | cmder, err := pipeline.Exec(ctx) 55 | if err != nil && !errors.Is(err, r.Nil()) { 56 | return nil, err 57 | } 58 | 59 | for idx, cmd := range cmder { 60 | if strCmd, ok := cmd.(*redis.StringCmd); ok { 61 | key := keyIdxMap[idx] 62 | if val, _ := strCmd.Result(); len(val) > 0 { 63 | ret[key] = val 64 | } 65 | } 66 | } 67 | 68 | return ret, nil 69 | } 70 | 71 | func (r *GoRedisV9Adapter) MSet(ctx context.Context, value map[string]any, expire time.Duration) error { 72 | pipeline := r.client.Pipeline() 73 | 74 | for key, val := range value { 75 | pipeline.SetEx(ctx, key, val, expire) 76 | } 77 | _, err := pipeline.Exec(ctx) 78 | 79 | return err 80 | } 81 | 82 | func (r *GoRedisV9Adapter) Nil() error { 83 | return redis.Nil 84 | } 85 | -------------------------------------------------------------------------------- /encoding/encoding_test.go: -------------------------------------------------------------------------------- 1 | package encoding 2 | 3 | import ( 4 | "encoding/xml" 5 | "fmt" 6 | "runtime/debug" 7 | "testing" 8 | ) 9 | 10 | type codec struct{} 11 | 12 | func (c codec) Marshal(v any) ([]byte, error) { 13 | panic("implement me") 14 | } 15 | 16 | func (c codec) Unmarshal(data []byte, v any) error { 17 | panic("implement me") 18 | } 19 | 20 | func (c codec) Name() string { 21 | return "" 22 | } 23 | 24 | // codec2 is a Codec implementation with xml. 25 | type codec2 struct{} 26 | 27 | func (codec2) Marshal(v any) ([]byte, error) { 28 | return xml.Marshal(v) 29 | } 30 | 31 | func (codec2) Unmarshal(data []byte, v any) error { 32 | return xml.Unmarshal(data, v) 33 | } 34 | 35 | func (codec2) Name() string { 36 | return "xml" 37 | } 38 | 39 | func TestRegisterCodec(t *testing.T) { 40 | f := func() { RegisterCodec(nil) } 41 | funcDidPanic, panicValue, _ := didPanic(f) 42 | if !funcDidPanic { 43 | t.Fatalf(fmt.Sprintf("func should panic\n\tPanic value:\t%#v", panicValue)) 44 | } 45 | if panicValue != "cannot register a nil Codec" { 46 | t.Fatalf("panic error got %s want cannot register a nil Codec", panicValue) 47 | } 48 | f = func() { 49 | RegisterCodec(codec{}) 50 | } 51 | funcDidPanic, panicValue, _ = didPanic(f) 52 | if !funcDidPanic { 53 | t.Fatalf(fmt.Sprintf("func should panic\n\tPanic value:\t%#v", panicValue)) 54 | } 55 | if panicValue != "cannot register Codec with empty string result for Name()" { 56 | t.Fatalf("panic error got %s want cannot register Codec with empty string result for Name()", panicValue) 57 | } 58 | codec := codec2{} 59 | RegisterCodec(codec) 60 | got := GetCodec("xml") 61 | if got != codec { 62 | t.Fatalf("RegisterCodec(%v) want %v got %v", codec, codec, got) 63 | } 64 | } 65 | 66 | // PanicTestFunc defines a func that should be passed to the assert.Panics and assert.NotPanics 67 | // methods, and represents a simple func that takes no arguments, and returns nothing. 68 | type PanicTestFunc func() 69 | 70 | // didPanic returns true if the function passed to it panics. Otherwise, it returns false. 71 | func didPanic(f PanicTestFunc) (bool, any, string) { 72 | didPanic := false 73 | var message any 74 | var stack string 75 | func() { 76 | defer func() { 77 | if message = recover(); message != nil { 78 | didPanic = true 79 | stack = string(debug.Stack()) 80 | } 81 | }() 82 | 83 | // call the target function 84 | f() 85 | }() 86 | 87 | return didPanic, message, stack 88 | } 89 | -------------------------------------------------------------------------------- /local/freecache.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "sync" 7 | "time" 8 | 9 | "github.com/coocood/freecache" 10 | 11 | "github.com/mgtv-tech/jetcache-go/logger" 12 | "github.com/mgtv-tech/jetcache-go/util" 13 | ) 14 | 15 | var _ Local = (*FreeCache)(nil) 16 | 17 | var ( 18 | innerCache *freecache.Cache 19 | once sync.Once 20 | ) 21 | 22 | type ( 23 | FreeCache struct { 24 | safeRand *util.SafeRand 25 | ttl time.Duration 26 | offset time.Duration 27 | innerKeyPrefix string 28 | } 29 | // Option defines the method to customize an Options. 30 | Option func(o *FreeCache) 31 | ) 32 | 33 | // NewFreeCache Create a new cache instance, but the internal cache instances are shared, 34 | // and they will only be initialized once. 35 | func NewFreeCache(size Size, ttl time.Duration, innerKeyPrefix ...string) *FreeCache { 36 | prefix := "" 37 | if len(innerKeyPrefix) > 0 { 38 | prefix = innerKeyPrefix[0] 39 | } 40 | 41 | // avoid "expireSeconds <= 0 means no expire" 42 | if ttl > 0 && ttl < time.Second { 43 | ttl = time.Second 44 | } 45 | 46 | const maxOffset = 10 * time.Second 47 | offset := ttl / 10 48 | if offset > maxOffset { 49 | offset = maxOffset 50 | } 51 | 52 | once.Do(func() { 53 | if size < 512*KB || size > 8*GB { 54 | size = 256 * MB 55 | } 56 | innerCache = freecache.NewCache(int(size)) 57 | }) 58 | 59 | return &FreeCache{ 60 | innerKeyPrefix: prefix, 61 | safeRand: util.NewSafeRand(), 62 | ttl: ttl, 63 | offset: offset, 64 | } 65 | } 66 | 67 | func (c *FreeCache) UseRandomizedTTL(offset time.Duration) { 68 | c.offset = offset 69 | } 70 | 71 | func (c *FreeCache) Set(key string, b []byte) { 72 | ttl := c.ttl 73 | if c.offset > 0 { 74 | ttl += time.Duration(c.safeRand.Int63n(int64(c.offset))) 75 | } 76 | 77 | if err := innerCache.Set(util.Bytes(c.Key(key)), b, int(ttl.Seconds())); err != nil { 78 | logger.Error("freeCache set(%s) error(%v)", key, err) 79 | } 80 | } 81 | 82 | func (c *FreeCache) Get(key string) ([]byte, bool) { 83 | b, err := innerCache.Get(util.Bytes(c.Key(key))) 84 | if err != nil { 85 | if errors.Is(err, freecache.ErrNotFound) { 86 | return nil, false 87 | } 88 | logger.Error("freeCache get(%s) error(%v)", key, err) 89 | return nil, false 90 | } 91 | 92 | return b, true 93 | } 94 | 95 | func (c *FreeCache) Del(key string) { 96 | innerCache.Del(util.Bytes(c.Key(key))) 97 | } 98 | 99 | func (c *FreeCache) Key(key string) string { 100 | if c.innerKeyPrefix == "" { 101 | return key 102 | } 103 | 104 | return fmt.Sprintf("%s:%s", c.innerKeyPrefix, key) 105 | } 106 | -------------------------------------------------------------------------------- /remote/goredisv9adapter_test.go: -------------------------------------------------------------------------------- 1 | package remote 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/alicebob/miniredis/v2" 9 | "github.com/redis/go-redis/v9" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestGoRedisV9Adaptor_MGet(t *testing.T) { 14 | client := NewGoRedisV9Adapter(newRdb()) 15 | 16 | if err := client.SetEX(context.Background(), "key1", "value1", time.Minute); err != nil { 17 | t.Fatal(err) 18 | } 19 | 20 | val, err := client.Get(context.Background(), "key1") 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | assert.Equal(t, "value1", val) 25 | 26 | result, err := client.MGet(context.Background(), "key1", "key2") 27 | assert.Nil(t, err) 28 | assert.Equal(t, map[string]any{"key1": "value1"}, result) 29 | } 30 | 31 | func TestGoRedisV9Adaptor_MSet(t *testing.T) { 32 | client := NewGoRedisV9Adapter(newRdb()) 33 | 34 | err := client.MSet(context.Background(), map[string]any{"key1": "value1", "key2": 2}, time.Minute) 35 | assert.Nil(t, err) 36 | 37 | val, err := client.Get(context.Background(), "key1") 38 | assert.Nil(t, err) 39 | assert.Equal(t, "value1", val) 40 | 41 | val, err = client.Get(context.Background(), "key2") 42 | assert.Nil(t, err) 43 | assert.Equal(t, "2", val) 44 | 45 | result, err := client.MGet(context.Background(), "key1", "key2") 46 | assert.Nil(t, err) 47 | assert.Equal(t, map[string]any{"key1": "value1", "key2": "2"}, result) 48 | } 49 | 50 | func TestGoRedisV9Adaptor_Del(t *testing.T) { 51 | client := NewGoRedisV9Adapter(newRdb()) 52 | 53 | err := client.SetEX(context.Background(), "key1", "value1", time.Minute) 54 | assert.Nil(t, err) 55 | val, err := client.Get(context.Background(), "key1") 56 | assert.Nil(t, err) 57 | assert.Equal(t, "value1", val) 58 | _, err = client.Del(context.Background(), "key1") 59 | assert.Nil(t, err) 60 | _, err = client.Get(context.Background(), "key1") 61 | assert.NotNil(t, err) 62 | assert.Equal(t, err, client.Nil()) 63 | } 64 | 65 | func TestGoRedisV9Adaptor_SetXxNx(t *testing.T) { 66 | client := NewGoRedisV9Adapter(newRdb()) 67 | 68 | _, err := client.SetXX(context.Background(), "key1", "value1", time.Minute) 69 | assert.Nil(t, err) 70 | _, err = client.Get(context.Background(), "key1") 71 | assert.NotNil(t, err) 72 | assert.Equal(t, err, client.Nil()) 73 | 74 | _, err = client.SetNX(context.Background(), "key1", "value1", time.Minute) 75 | assert.Nil(t, err) 76 | val, err := client.Get(context.Background(), "key1") 77 | assert.Nil(t, err) 78 | assert.Equal(t, "value1", val) 79 | } 80 | 81 | func newRdb() *redis.Client { 82 | s, err := miniredis.Run() 83 | if err != nil { 84 | panic(err) 85 | } 86 | 87 | return redis.NewClient(&redis.Options{ 88 | Addr: s.Addr(), 89 | }) 90 | } 91 | -------------------------------------------------------------------------------- /local/freecache_test.go: -------------------------------------------------------------------------------- 1 | package local 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "math/rand" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestFreeCache(t *testing.T) { 14 | t.Run("Test limited ttl", func(t *testing.T) { 15 | cache := NewFreeCache(10*MB, time.Millisecond) 16 | assert.Equal(t, time.Second, cache.ttl) 17 | }) 18 | 19 | t.Run("Test limited offset", func(t *testing.T) { 20 | cache := NewFreeCache(200*KB, time.Hour) 21 | assert.Equal(t, 10*time.Second, cache.offset) 22 | }) 23 | 24 | t.Run("Test default", func(t *testing.T) { 25 | cache := NewFreeCache(10*MB, time.Second) 26 | assert.Equal(t, time.Second/10, cache.offset) 27 | cache.UseRandomizedTTL(time.Millisecond) 28 | assert.Equal(t, time.Millisecond, cache.offset) 29 | assert.Equal(t, "", cache.innerKeyPrefix) 30 | }) 31 | 32 | t.Run("Test GET/SET/DEL ", func(t *testing.T) { 33 | cache := NewFreeCache(10*MB, time.Second) 34 | key1 := "key1" 35 | val, exists := cache.Get(key1) 36 | assert.False(t, exists) 37 | assert.Equal(t, []byte(nil), val) 38 | 39 | cache.Set(key1, []byte("value1")) 40 | val, exists = cache.Get(key1) 41 | assert.True(t, exists) 42 | assert.Equal(t, []byte("value1"), val) 43 | 44 | cache.Del(key1) 45 | val, exists = cache.Get(key1) 46 | assert.False(t, exists) 47 | assert.Equal(t, []byte(nil), val) 48 | }) 49 | } 50 | 51 | func TestNewFreeCacheWithInnerKeyPrefix(t *testing.T) { 52 | innerKeyPrefix := "any" 53 | cache := NewFreeCache(10*MB, time.Second, innerKeyPrefix) 54 | assert.Equal(t, "any", cache.innerKeyPrefix) 55 | assert.Equal(t, "any:key", cache.Key("key")) 56 | } 57 | 58 | func TestFreeCacheGetCorruptionOnExpiry(t *testing.T) { 59 | strFor := func(i int) string { 60 | return fmt.Sprintf("a string %d", i) 61 | } 62 | keyName := func(i int) string { 63 | return fmt.Sprintf("key-%00000d", i) 64 | } 65 | 66 | cache := NewFreeCache(10*MB, time.Second) 67 | size := 50000 68 | // Put a bunch of stuff in the cache with a TTL of 1 second 69 | for i := 0; i < size; i++ { 70 | key := keyName(i) 71 | cache.Set(key, []byte(strFor(i))) 72 | } 73 | 74 | // Read stuff for a bit longer than the TTL - that's when the corruption occurs 75 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 76 | defer cancel() 77 | 78 | done := ctx.Done() 79 | loop: 80 | for { 81 | select { 82 | case <-done: 83 | // this is expected 84 | break loop 85 | default: 86 | i := rand.Intn(size) 87 | key := keyName(i) 88 | 89 | b, ok := cache.Get(key) 90 | if !ok { 91 | continue loop 92 | } 93 | 94 | got := string(b) 95 | expected := strFor(i) 96 | if got != expected { 97 | t.Fatalf("expected=%q got=%q key=%q", expected, got, key) 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /logger/default_test.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "log" 7 | "strings" 8 | "testing" 9 | ) 10 | 11 | func TestLocalLogger(t *testing.T) { 12 | t.Run("TestDebug", func(t *testing.T) { 13 | buffer := &bytes.Buffer{} 14 | ll := &localLogger{ 15 | logger: log.New(buffer, "", log.LstdFlags|log.Lshortfile|log.Lmicroseconds), 16 | } 17 | ll.Debug("debug message") 18 | if got, want := buffer.String(), "[DEBUG] debug message\n"; !strings.Contains(got, want) { 19 | t.Errorf("got %q, want %q", got, want) 20 | } 21 | }) 22 | 23 | t.Run("TestInfo", func(t *testing.T) { 24 | buffer := &bytes.Buffer{} 25 | ll := &localLogger{ 26 | logger: log.New(buffer, "", log.LstdFlags|log.Lshortfile|log.Lmicroseconds), 27 | } 28 | ll.Info("info message") 29 | if got, want := buffer.String(), "[INFO] info message\n"; !strings.Contains(got, want) { 30 | t.Errorf("got %q, want %q", got, want) 31 | } 32 | }) 33 | 34 | t.Run("TestWarn", func(t *testing.T) { 35 | buffer := &bytes.Buffer{} 36 | ll := &localLogger{ 37 | logger: log.New(buffer, "", log.LstdFlags|log.Lshortfile|log.Lmicroseconds), 38 | } 39 | ll.Warn("warn message") 40 | if got, want := buffer.String(), "[WARN] warn message\n"; !strings.Contains(got, want) { 41 | t.Errorf("got %q, want %q", got, want) 42 | } 43 | }) 44 | 45 | t.Run("TestError", func(t *testing.T) { 46 | buffer := &bytes.Buffer{} 47 | ll := &localLogger{ 48 | logger: log.New(buffer, "", log.LstdFlags|log.Lshortfile|log.Lmicroseconds), 49 | } 50 | ll.Error("error message") 51 | if got, want := buffer.String(), "[ERROR] error message\n"; !strings.Contains(got, want) { 52 | t.Errorf("got %q, want %q", got, want) 53 | } 54 | }) 55 | 56 | t.Run("TestLogf", func(t *testing.T) { 57 | buffer := &bytes.Buffer{} 58 | ll := &localLogger{ 59 | logger: log.New(buffer, "", log.LstdFlags|log.Lshortfile|log.Lmicroseconds), 60 | } 61 | testCases := []struct { 62 | level Level 63 | format string 64 | expected string 65 | shouldLog bool 66 | }{ 67 | {LevelDebug, "debug message %s", "[DEBUG] debug message test\n", true}, 68 | {LevelInfo, "info message %s", "[INFO] info message test\n", true}, 69 | {LevelWarn, "warn message %s", "[WARN] warn message test\n", true}, 70 | {LevelError, "error message %s", "[ERROR] error message test\n", true}, 71 | } 72 | 73 | for _, testCase := range testCases { 74 | t.Run(fmt.Sprintf("Level%s", testCase.level), func(t *testing.T) { 75 | buffer.Reset() 76 | ll.logf(testCase.level, &testCase.format, "test") 77 | if testCase.shouldLog { 78 | if !strings.Contains(buffer.String(), testCase.expected) { 79 | t.Errorf("Expected output: %s, but got: %s", testCase.expected, buffer.String()) 80 | } 81 | } else { 82 | if buffer.String() != "" { 83 | t.Errorf("Expected no output, but got: %s", buffer.String()) 84 | } 85 | } 86 | }) 87 | } 88 | }) 89 | } 90 | -------------------------------------------------------------------------------- /encoding/json/json_test.go: -------------------------------------------------------------------------------- 1 | package json 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | type testEmbed struct { 11 | Level1a int `json:"a"` 12 | Level1b int `json:"b"` 13 | Level1c int `json:"c"` 14 | } 15 | 16 | type testMessage struct { 17 | Field1 string `json:"a"` 18 | Field2 string `json:"b"` 19 | Field3 string `json:"c"` 20 | Embed *testEmbed `json:"embed,omitempty"` 21 | } 22 | 23 | type mock struct { 24 | value int 25 | } 26 | 27 | const ( 28 | Unknown = iota 29 | Gopher 30 | Zebra 31 | ) 32 | 33 | func (a *mock) UnmarshalJSON(b []byte) error { 34 | var s string 35 | if err := json.Unmarshal(b, &s); err != nil { 36 | return err 37 | } 38 | switch strings.ToLower(s) { 39 | default: 40 | a.value = Unknown 41 | case "gopher": 42 | a.value = Gopher 43 | case "zebra": 44 | a.value = Zebra 45 | } 46 | 47 | return nil 48 | } 49 | 50 | func (a *mock) MarshalJSON() ([]byte, error) { 51 | var s string 52 | switch a.value { 53 | default: 54 | s = "unknown" 55 | case Gopher: 56 | s = "gopher" 57 | case Zebra: 58 | s = "zebra" 59 | } 60 | 61 | return json.Marshal(s) 62 | } 63 | 64 | func TestJSONMarshal(t *testing.T) { 65 | tests := []struct { 66 | input any 67 | expect string 68 | }{ 69 | { 70 | input: &testMessage{}, 71 | expect: `{"a":"","b":"","c":""}`, 72 | }, 73 | { 74 | input: &testMessage{Field1: "a", Field2: "b", Field3: "c"}, 75 | expect: `{"a":"a","b":"b","c":"c"}`, 76 | }, 77 | { 78 | input: &mock{value: Gopher}, 79 | expect: `"gopher"`, 80 | }, 81 | } 82 | for _, v := range tests { 83 | data, err := (codec{}).Marshal(v.input) 84 | if err != nil { 85 | t.Errorf("marshal(%#v): %s", v.input, err) 86 | } 87 | if got, want := string(data), v.expect; strings.ReplaceAll(got, " ", "") != want { 88 | if strings.Contains(want, "\n") { 89 | t.Errorf("marshal(%#v):\nHAVE:\n%s\nWANT:\n%s", v.input, got, want) 90 | } else { 91 | t.Errorf("marshal(%#v):\nhave %#q\nwant %#q", v.input, got, want) 92 | } 93 | } 94 | } 95 | } 96 | 97 | func TestJSONUnmarshal(t *testing.T) { 98 | p := testMessage{} 99 | p4 := &mock{} 100 | tests := []struct { 101 | input string 102 | expect any 103 | }{ 104 | { 105 | input: `{"a":"","b":"","c":""}`, 106 | expect: &testMessage{}, 107 | }, 108 | { 109 | input: `{"a":"a","b":"b","c":"c"}`, 110 | expect: &p, 111 | }, 112 | { 113 | input: `"zebra"`, 114 | expect: p4, 115 | }, 116 | } 117 | for _, v := range tests { 118 | want := []byte(v.input) 119 | err := (codec{}).Unmarshal(want, v.expect) 120 | if err != nil { 121 | t.Errorf("marshal(%#v): %s", v.input, err) 122 | } 123 | got, err := codec{}.Marshal(v.expect) 124 | if err != nil { 125 | t.Errorf("marshal(%#v): %s", v.input, err) 126 | } 127 | if !reflect.DeepEqual(strings.ReplaceAll(string(got), " ", ""), strings.ReplaceAll(string(want), " ", "")) { 128 | t.Errorf("marshal(%#v):\nhave %#q\nwant %#q", v.input, got, want) 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /encoding/sonic/sonic_test.go: -------------------------------------------------------------------------------- 1 | package sonic 2 | 3 | import ( 4 | "reflect" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/bytedance/sonic" 9 | ) 10 | 11 | type testEmbed struct { 12 | Level1a int `json:"a"` 13 | Level1b int `json:"b"` 14 | Level1c int `json:"c"` 15 | } 16 | 17 | type testMessage struct { 18 | Field1 string `json:"a"` 19 | Field2 string `json:"b"` 20 | Field3 string `json:"c"` 21 | Embed *testEmbed `json:"embed,omitempty"` 22 | } 23 | 24 | type mock struct { 25 | value int 26 | } 27 | 28 | const ( 29 | Unknown = iota 30 | Gopher 31 | Zebra 32 | ) 33 | 34 | func (a *mock) UnmarshalJSON(b []byte) error { 35 | var s string 36 | if err := sonic.Unmarshal(b, &s); err != nil { 37 | return err 38 | } 39 | switch strings.ToLower(s) { 40 | default: 41 | a.value = Unknown 42 | case "gopher": 43 | a.value = Gopher 44 | case "zebra": 45 | a.value = Zebra 46 | } 47 | 48 | return nil 49 | } 50 | 51 | func (a *mock) MarshalJSON() ([]byte, error) { 52 | var s string 53 | switch a.value { 54 | default: 55 | s = "unknown" 56 | case Gopher: 57 | s = "gopher" 58 | case Zebra: 59 | s = "zebra" 60 | } 61 | 62 | return sonic.Marshal(s) 63 | } 64 | 65 | func TestJSONMarshal(t *testing.T) { 66 | tests := []struct { 67 | input interface{} 68 | expect string 69 | }{ 70 | { 71 | input: &testMessage{}, 72 | expect: `{"a":"","b":"","c":""}`, 73 | }, 74 | { 75 | input: &testMessage{Field1: "a", Field2: "b", Field3: "c"}, 76 | expect: `{"a":"a","b":"b","c":"c"}`, 77 | }, 78 | { 79 | input: &mock{value: Gopher}, 80 | expect: `"gopher"`, 81 | }, 82 | } 83 | for _, v := range tests { 84 | data, err := (codec{}).Marshal(v.input) 85 | if err != nil { 86 | t.Errorf("marshal(%#v): %s", v.input, err) 87 | } 88 | if got, want := string(data), v.expect; strings.ReplaceAll(got, " ", "") != want { 89 | if strings.Contains(want, "\n") { 90 | t.Errorf("marshal(%#v):\nHAVE:\n%s\nWANT:\n%s", v.input, got, want) 91 | } else { 92 | t.Errorf("marshal(%#v):\nhave %#q\nwant %#q", v.input, got, want) 93 | } 94 | } 95 | } 96 | } 97 | 98 | func TestJSONUnmarshal(t *testing.T) { 99 | p := testMessage{} 100 | p4 := &mock{} 101 | tests := []struct { 102 | input string 103 | expect interface{} 104 | }{ 105 | { 106 | input: `{"a":"","b":"","c":""}`, 107 | expect: &testMessage{}, 108 | }, 109 | { 110 | input: `{"a":"a","b":"b","c":"c"}`, 111 | expect: &p, 112 | }, 113 | { 114 | input: `"zebra"`, 115 | expect: p4, 116 | }, 117 | } 118 | for _, v := range tests { 119 | want := []byte(v.input) 120 | err := (codec{}).Unmarshal(want, v.expect) 121 | if err != nil { 122 | t.Errorf("marshal(%#v): %s", v.input, err) 123 | } 124 | got, err := codec{}.Marshal(v.expect) 125 | if err != nil { 126 | t.Errorf("marshal(%#v): %s", v.input, err) 127 | } 128 | if !reflect.DeepEqual(strings.ReplaceAll(string(got), " ", ""), strings.ReplaceAll(string(want), " ", "")) { 129 | t.Errorf("marshal(%#v):\nhave %#q\nwant %#q", v.input, got, want) 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /item.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/mgtv-tech/jetcache-go/logger" 8 | ) 9 | 10 | type ( 11 | // ItemOption defines the method to customize an Options. 12 | ItemOption func(o *item) 13 | 14 | // DoFunc returns getValue to be cached. 15 | DoFunc func(ctx context.Context) (any, error) 16 | 17 | item struct { 18 | ctx context.Context 19 | key string 20 | value any // value gets the value for the given key and fills into value. 21 | ttl time.Duration // ttl is the remote cache expiration time. Default ttl is 1 hour. 22 | do DoFunc // do is DoFunc 23 | setXX bool // setXX only sets the key if it already exists. 24 | setNX bool // setNX only sets the key if it does not already exist. 25 | skipLocal bool // skipLocal skips local cache as if it is not set. 26 | refresh bool // refresh open cache async refresh. 27 | } 28 | 29 | refreshTask struct { 30 | key string 31 | ttl time.Duration 32 | do DoFunc 33 | setXX bool 34 | setNX bool 35 | skipLocal bool 36 | lastAccessTime time.Time 37 | } 38 | ) 39 | 40 | func newItemOptions(ctx context.Context, key string, opts ...ItemOption) *item { 41 | var item = item{ctx: ctx, key: key} 42 | for _, opt := range opts { 43 | opt(&item) 44 | } 45 | 46 | return &item 47 | } 48 | 49 | func Value(value any) ItemOption { 50 | return func(o *item) { 51 | o.value = value 52 | } 53 | } 54 | 55 | func TTL(ttl time.Duration) ItemOption { 56 | return func(o *item) { 57 | o.ttl = ttl 58 | } 59 | } 60 | 61 | func Do(do DoFunc) ItemOption { 62 | return func(o *item) { 63 | o.do = do 64 | } 65 | } 66 | 67 | func SetXX(setXx bool) ItemOption { 68 | return func(o *item) { 69 | o.setXX = setXx 70 | } 71 | } 72 | 73 | func SetNX(setNx bool) ItemOption { 74 | return func(o *item) { 75 | o.setNX = setNx 76 | } 77 | } 78 | 79 | func SkipLocal(skipLocal bool) ItemOption { 80 | return func(o *item) { 81 | o.skipLocal = skipLocal 82 | } 83 | } 84 | 85 | func Refresh(refresh bool) ItemOption { 86 | return func(o *item) { 87 | o.refresh = refresh 88 | } 89 | } 90 | 91 | func (item *item) Context() context.Context { 92 | if item.ctx == nil { 93 | return context.Background() 94 | } 95 | return item.ctx 96 | } 97 | 98 | func (item *item) getValue() (any, error) { 99 | if item.do != nil { 100 | return item.do(item.Context()) 101 | } 102 | if item.value != nil { 103 | return item.value, nil 104 | } 105 | return nil, nil 106 | } 107 | 108 | func (item *item) getTtl(defaultTTL time.Duration) time.Duration { 109 | if item.ttl < 0 { 110 | return 0 111 | } 112 | 113 | if item.ttl != 0 { 114 | if item.ttl < time.Second { 115 | logger.Warn("too short ttl for key=%q: %s", item.key, item.ttl) 116 | return defaultTTL 117 | } 118 | return item.ttl 119 | } 120 | 121 | return defaultTTL 122 | } 123 | 124 | func (item *item) toRefreshTask() *refreshTask { 125 | return &refreshTask{ 126 | key: item.key, 127 | ttl: item.ttl, 128 | do: item.do, 129 | skipLocal: item.skipLocal, 130 | lastAccessTime: time.Now(), 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /docs/CN/Embedded.md: -------------------------------------------------------------------------------- 1 | 2 | * [介绍](#介绍) 3 | * [多种序列化方式](#多种序列化方式) 4 | * [多种本地缓存、远程缓存](#多种本地缓存远程缓存) 5 | * [指标采集统计](#指标采集统计) 6 | * [自定义接管日志](#自定义接管日志) 7 | 8 | 9 | # 介绍 10 | 11 | `jetcache-go` 通过通用接口实现了许多内嵌组件,方便开发者集成,以及参考实现自己的组件。 12 | 13 | 14 | # 多种序列化方式 15 | 16 | | codec方式 | 说明 | 优势 | 17 | |---------------------------------------------------|---------------------------|------------| 18 | | native json | golang自带的序列化工具 | 兼容性好 | 19 | | [msgpack](https://github.com/vmihailenco/msgpack) | msgpack+snappy压缩(内容>64字节) | 性能较强,内存占用小 | 20 | | [sonic](https://github.com/go-sonic/sonic) | 字节开源的高性能json序列化工具 | 性能强 | 21 | 22 | 你也可以通过实现 `encoding.Codec` 接口来自定义自己的序列化,并通过 `encoding.RegisterCodec` 注册进来。 23 | 24 | ```go 25 | 26 | import ( 27 | _ "github.com/mgtv-tech/jetcache-go/encoding/yourCodec" 28 | ) 29 | 30 | // Register your codec 31 | encoding.RegisterCodec(yourCodec.Name) 32 | 33 | mycache := cache.New(cache.WithName("any"), 34 | cache.WithRemote(...), 35 | cache.WithCodec(yourCodec.Name)) 36 | ``` 37 | 38 | 39 | # 多种本地缓存、远程缓存 40 | 41 | | 名称 | 类型 | 特点 | 42 | |-----------------------------------------------------|--------|-------------------| 43 | | [ristretto](https://github.com/dgraph-io/ristretto) | Local | 高性能、高命中率 | 44 | | [freecache](https://github.com/coocood/freecache) | Local | 零垃圾收集负荷、严格限制内存使用 | 45 | | [go-redis](https://github.com/redis/go-redis) | Remote | 最流行的 GO Redis 客户端 | 46 | 47 | 你也可以通过实现 `remote.Remote`、`local.Local` 接口来实现自己的本地、远程缓存。 48 | 49 | > FreeCache 使用注意事项: 50 | > 51 | > 缓存key的大小需要小于65535,否则无法存入到本地缓存中(The key is larger than 65535) 52 | > 缓存value的大小需要小于缓存总容量的1/1024,否则无法存入到本地缓存中(The entry size need less than 1/1024 of cache size) 53 | > 内嵌的FreeCache实例内部共享了一个 `innerCache` 实例,防止当多个缓存实例都使用 FreeCache 时内存占用过多。因此,共享 `innerCache` 会以第一次创建的配置的内存容量和过期时间为准。 54 | 55 | 56 | # 指标采集统计 57 | 58 | | 名称 | 类型 | 说明 | 59 | |-----------------|----|-------------------------------------------------------------------------------| 60 | | logStats | 内嵌 | 默认的指标采集统计器,统计信息打印到日志 | 61 | | PrometheusStats | 插件 | [jetcache-go-plugin](https://github.com/mgtv-tech/jetcache-go-plugin) 提供的统计插件 | 62 | 63 | 你也可以通过实现 `stats.Handler` 接口来自定义自己的指标采集器。 64 | 65 | 示例:同时使用多种指标采集器 66 | 67 | ```go 68 | import ( 69 | "context" 70 | "time" 71 | 72 | "github.com/mgtv-tech/jetcache-go" 73 | "github.com/mgtv-tech/jetcache-go/remote" 74 | "github.com/redis/go-redis/v9" 75 | pstats "github.com/mgtv-tech/jetcache-go-plugin/stats" 76 | "github.com/mgtv-tech/jetcache-go/stats" 77 | ) 78 | 79 | mycache := cache.New(cache.WithName("any"), 80 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 81 | cache.WithStatsHandler( 82 | stats.NewHandles(false, 83 | stats.NewStatsLogger(cacheName), 84 | pstats.NewPrometheus(cacheName)))) 85 | 86 | obj := struct { 87 | Name string 88 | Age int 89 | }{Name: "John Doe", Age: 30} 90 | 91 | err := mycache.Set(context.Background(), "mykey", cache.Value(&obj), cache.TTL(time.Hour)) 92 | if err != nil { 93 | // 错误处理 94 | } 95 | ``` 96 | 97 | # 自定义接管日志 98 | 99 | ```go 100 | import "github.com/mgtv-tech/jetcache-go/logger" 101 | 102 | // Set your Logger 103 | logger.SetDefaultLogger(l logger.Logger) 104 | ``` 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /logger/logger_test.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestSetDefaultLogger(t *testing.T) { 12 | t.Run("Test SetDefaultLogger", func(t *testing.T) { 13 | tl := &testLogger{} 14 | SetDefaultLogger(tl) 15 | 16 | assert.Equal(t, tl, defaultLogger) 17 | }) 18 | 19 | t.Run("Test Set Invalid", func(t *testing.T) { 20 | defer func() { 21 | if r := recover(); r == nil { 22 | t.Errorf("expected panic, got nil") 23 | } 24 | }() 25 | SetDefaultLogger(nil) 26 | }) 27 | } 28 | 29 | func TestLogger(t *testing.T) { 30 | t.Run("TestDebug", func(t *testing.T) { 31 | buffer := &bytes.Buffer{} 32 | testLogger := &testLogger{buffer: buffer} 33 | SetDefaultLogger(testLogger) 34 | Debug("debug message") 35 | if got, want := buffer.String(), "[DEBUG] debug message\n"; got != want { 36 | t.Errorf("got %q, want %q", got, want) 37 | } 38 | }) 39 | 40 | t.Run("TestInfo", func(t *testing.T) { 41 | buffer := &bytes.Buffer{} 42 | testLogger := &testLogger{buffer: buffer} 43 | SetDefaultLogger(testLogger) 44 | Info("info message") 45 | if got, want := buffer.String(), "[INFO] info message\n"; got != want { 46 | t.Errorf("got %q, want %q", got, want) 47 | } 48 | }) 49 | 50 | t.Run("TestWarn", func(t *testing.T) { 51 | buffer := &bytes.Buffer{} 52 | testLogger := &testLogger{buffer: buffer} 53 | SetDefaultLogger(testLogger) 54 | Warn("warn message") 55 | if got, want := buffer.String(), "[WARN] warn message\n"; got != want { 56 | t.Errorf("got %q, want %q", got, want) 57 | } 58 | }) 59 | 60 | t.Run("TestError", func(t *testing.T) { 61 | buffer := &bytes.Buffer{} 62 | testLogger := &testLogger{buffer: buffer} 63 | SetDefaultLogger(testLogger) 64 | Error("error message") 65 | if got, want := buffer.String(), "[ERROR] error message\n"; got != want { 66 | t.Errorf("got %q, want %q", got, want) 67 | } 68 | }) 69 | 70 | t.Run("TestLevelError", func(t *testing.T) { 71 | buffer := &bytes.Buffer{} 72 | testLogger := &testLogger{buffer: buffer} 73 | SetDefaultLogger(testLogger) 74 | 75 | SetLevel(LevelError) 76 | Debug("debug message") 77 | Info("info message") 78 | Warn("warn message") 79 | Error("error message") 80 | if got, want := buffer.String(), "[ERROR] error message\n"; got != want { 81 | t.Errorf("got %q, want %q", got, want) 82 | } 83 | }) 84 | 85 | t.Run("TestLevelWarn", func(t *testing.T) { 86 | buffer := &bytes.Buffer{} 87 | testLogger := &testLogger{buffer: buffer} 88 | SetDefaultLogger(testLogger) 89 | 90 | SetLevel(LevelWarn) 91 | Debug("debug message") 92 | Info("info message") 93 | Warn("warn message") 94 | Error("error message") 95 | if got, want := buffer.String(), "[WARN] warn message\n[ERROR] error message\n"; got != want { 96 | t.Errorf("got %q, want %q", got, want) 97 | } 98 | }) 99 | 100 | t.Run("TestLevelInfo", func(t *testing.T) { 101 | buffer := &bytes.Buffer{} 102 | testLogger := &testLogger{buffer: buffer} 103 | SetDefaultLogger(testLogger) 104 | 105 | SetLevel(LevelInfo) 106 | Debug("debug message") 107 | Info("info message") 108 | Warn("warn message") 109 | Error("error message") 110 | if got, want := buffer.String(), "[INFO] info message\n[WARN] warn message\n[ERROR] error message\n"; got != want { 111 | t.Errorf("got %q, want %q", got, want) 112 | } 113 | }) 114 | 115 | t.Run("TestInvalidLevel", func(t *testing.T) { 116 | defer func() { 117 | if r := recover(); r == nil { 118 | t.Errorf("expected panic, got nil") 119 | } 120 | }() 121 | SetLevel(Level(-1)) 122 | }) 123 | } 124 | 125 | func TestLevelString(t *testing.T) { 126 | tests := []struct { 127 | level Level 128 | want string 129 | }{ 130 | {LevelDebug, "[DEBUG] "}, 131 | {LevelInfo, "[INFO] "}, 132 | {LevelWarn, "[WARN] "}, 133 | {LevelError, "[ERROR] "}, 134 | } 135 | for _, tt := range tests { 136 | t.Run(tt.want, func(t *testing.T) { 137 | if got := tt.level.String(); got != tt.want { 138 | t.Errorf("Level.String() = %v, want %v", got, tt.want) 139 | } 140 | }) 141 | } 142 | } 143 | 144 | type testLogger struct { 145 | buffer *bytes.Buffer 146 | } 147 | 148 | func (tl *testLogger) Debug(format string, v ...any) { 149 | fmt.Fprintf(tl.buffer, "[DEBUG] "+format+"\n", v...) 150 | } 151 | 152 | func (tl *testLogger) Info(format string, v ...any) { 153 | fmt.Fprintf(tl.buffer, "[INFO] "+format+"\n", v...) 154 | } 155 | 156 | func (tl *testLogger) Warn(format string, v ...any) { 157 | fmt.Fprintf(tl.buffer, "[WARN] "+format+"\n", v...) 158 | } 159 | 160 | func (tl *testLogger) Error(format string, v ...any) { 161 | fmt.Fprintf(tl.buffer, "[ERROR] "+format+"\n", v...) 162 | } 163 | -------------------------------------------------------------------------------- /stats/statslogger_test.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "testing" 9 | "time" 10 | 11 | "github.com/mgtv-tech/jetcache-go/logger" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestNewStatsLogger(t *testing.T) { 16 | t.Run("default stats interval", func(t *testing.T) { 17 | sl := NewStatsLogger("any") 18 | stats := sl.(*Stats) 19 | assert.Equal(t, defaultStatsInterval, stats.statsInterval) 20 | }) 21 | 22 | t.Run("custom stats interval", func(t *testing.T) { 23 | sl := NewStatsLogger("any", WithStatsInterval(time.Millisecond)) 24 | stats := sl.(*Stats) 25 | assert.Equal(t, stats.statsInterval, time.Millisecond, stats.statsInterval) 26 | }) 27 | } 28 | 29 | func TestStatLogger_statLoop(t *testing.T) { 30 | t.Run("stat loop stats 0", func(t *testing.T) { 31 | _ = NewStatsLogger("any", WithStatsInterval(time.Millisecond)) 32 | time.Sleep(10 * time.Millisecond) 33 | }) 34 | 35 | t.Run("stat loop total not 0", func(t *testing.T) { 36 | stat := NewStatsLogger("any", WithStatsInterval(time.Millisecond)) 37 | stat.IncrHit() 38 | stat.IncrMiss() 39 | stat.IncrLocalHit() 40 | stat.IncrLocalMiss() 41 | stat.IncrRemoteHit() 42 | stat.IncrRemoteMiss() 43 | stat.IncrQuery() 44 | stat.IncrQueryFail(errors.New("any")) 45 | 46 | for i := 0; i < 3; i++ { 47 | stat := NewStatsLogger(fmt.Sprintf("test_lang_cache_%d", i), WithStatsInterval(time.Millisecond)) 48 | stat.IncrHit() 49 | stat.IncrMiss() 50 | stat.IncrLocalHit() 51 | stat.IncrLocalMiss() 52 | stat.IncrRemoteHit() 53 | stat.IncrRemoteMiss() 54 | stat.IncrQuery() 55 | stat.IncrQueryFail(errors.New("any")) 56 | } 57 | time.Sleep(10 * time.Millisecond) 58 | }) 59 | 60 | t.Run("stat loop query not 0", func(t *testing.T) { 61 | stat := NewStatsLogger("any", WithStatsInterval(time.Millisecond)) 62 | stat.IncrQuery() 63 | time.Sleep(10 * time.Millisecond) 64 | }) 65 | } 66 | 67 | func TestStatLogger_logStatSummary(t *testing.T) { 68 | var logBuffer = &bytes.Buffer{} 69 | logger.SetDefaultLogger(&testLogger{}) 70 | log.SetOutput(logBuffer) 71 | 72 | stats := []*Stats{ 73 | {Name: "cache1", Hit: 10, Miss: 2, RemoteHit: 5, RemoteMiss: 1, LocalHit: 5, LocalMiss: 1, Query: 100, QueryFail: 5}, 74 | {Name: "cache2", Hit: 5, Miss: 0, Query: 50}, 75 | {Name: "cache3", Hit: 0, Miss: 0, Query: 0}, 76 | } 77 | inner := &innerStats{ 78 | stats: stats, 79 | statsInterval: time.Minute, 80 | } 81 | 82 | inner.logStatSummary() 83 | 84 | expected := `jetcache-go stats last 1m0s. 85 | cache | qpm| hit_ratio| hit| miss| query| query_fail 86 | -------------+------------+------------+------------+------------+------------+------------ 87 | cache1 | 12| 83.33%| 10| 2| 100| 5 88 | cache1_local | 6| 83.33%| 5| 1| -| - 89 | cache1_remote| 6| 83.33%| 5| 1| -| - 90 | cache2 | 5| 100.00%| 5| 0| 50| 0 91 | -------------+------------+------------+------------+------------+------------+------------` 92 | 93 | assert.Contains(t, logBuffer.String(), expected) 94 | } 95 | 96 | func TestFormatHeader(t *testing.T) { 97 | maxLenStr := "12" 98 | expected := fmt.Sprintf("%-12s|%12s|%12s|%12s|%12s|%12s|%12s\n", "cache", "qpm", "hit_ratio", "hit", "miss", "query", "query_fail") 99 | actual := formatHeader(maxLenStr) 100 | if actual != expected { 101 | t.Errorf("formatHeader failed. Expected: %s, Actual: %s", expected, actual) 102 | } 103 | } 104 | 105 | func TestFormatSepLine(t *testing.T) { 106 | header := "cache | qpm| hit_ratio| hit| miss| query| query_fail\n" 107 | expected := "-------------+------------+------------+------------+------------+------------+-----------" 108 | actual := formatSepLine(header) 109 | assert.Equal(t, expected, actual) 110 | } 111 | 112 | func TestStatLogger_race(t *testing.T) { 113 | testCases := []struct { 114 | count uint64 115 | total uint64 116 | expect string 117 | }{ 118 | {10, 100, "10.00"}, 119 | {0, 100, "0.00"}, 120 | {10, 0, "0.00"}, 121 | {50, 50, "100.00"}, 122 | } 123 | for _, tc := range testCases { 124 | actual := rate(tc.count, tc.total) 125 | assert.Equal(t, tc.expect, actual) 126 | } 127 | } 128 | 129 | func TestStatLogger_getName(t *testing.T) { 130 | assert.Equal(t, "cache_local", getName("cache", "local")) 131 | } 132 | 133 | type testLogger struct{} 134 | 135 | func (l *testLogger) Debug(format string, v ...any) { 136 | log.Println(fmt.Sprintf(format, v...)) 137 | } 138 | 139 | func (l *testLogger) Info(format string, v ...any) { 140 | log.Println(fmt.Sprintf(format, v...)) 141 | } 142 | 143 | func (l *testLogger) Warn(format string, v ...any) { 144 | log.Println(fmt.Sprintf(format, v...)) 145 | } 146 | 147 | func (l *testLogger) Error(format string, v ...any) { 148 | log.Println(fmt.Sprintf(format, v...)) 149 | } 150 | -------------------------------------------------------------------------------- /example_cache_test.go: -------------------------------------------------------------------------------- 1 | package cache_test 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "time" 10 | 11 | "github.com/mgtv-tech/jetcache-go" 12 | "github.com/mgtv-tech/jetcache-go/local" 13 | "github.com/mgtv-tech/jetcache-go/remote" 14 | "github.com/redis/go-redis/v9" 15 | ) 16 | 17 | var errRecordNotFound = errors.New("mock gorm.errRecordNotFound") 18 | 19 | type object struct { 20 | Str string 21 | Num int 22 | } 23 | 24 | func mockDBGetObject(id int) (*object, error) { 25 | if id > 100 { 26 | return nil, errRecordNotFound 27 | } 28 | return &object{Str: "mystring", Num: 42}, nil 29 | } 30 | 31 | func mockDBMGetObject(ids []int) (map[int]*object, error) { 32 | ret := make(map[int]*object) 33 | for _, id := range ids { 34 | if id == 3 { 35 | continue 36 | } 37 | ret[id] = &object{Str: "mystring", Num: id} 38 | } 39 | return ret, nil 40 | } 41 | 42 | func Example_basicUsage() { 43 | ring := redis.NewRing(&redis.RingOptions{ 44 | Addrs: map[string]string{ 45 | "localhost": ":6379", 46 | }, 47 | }) 48 | 49 | mycache := cache.New(cache.WithName("any"), 50 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 51 | cache.WithLocal(local.NewFreeCache(256*local.MB, time.Minute)), 52 | cache.WithErrNotFound(errRecordNotFound)) 53 | 54 | ctx := context.TODO() 55 | key := "mykey:1" 56 | obj, _ := mockDBGetObject(1) 57 | if err := mycache.Set(ctx, key, cache.Value(obj), cache.TTL(time.Hour)); err != nil { 58 | panic(err) 59 | } 60 | 61 | var wanted object 62 | if err := mycache.Get(ctx, key, &wanted); err == nil { 63 | fmt.Println(wanted) 64 | } 65 | // Output: {mystring 42} 66 | 67 | mycache.Close() 68 | } 69 | 70 | func Example_advancedUsage() { 71 | ring := redis.NewRing(&redis.RingOptions{ 72 | Addrs: map[string]string{ 73 | "localhost": ":6379", 74 | }, 75 | }) 76 | 77 | mycache := cache.New(cache.WithName("any"), 78 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 79 | cache.WithLocal(local.NewFreeCache(256*local.MB, time.Minute)), 80 | cache.WithErrNotFound(errRecordNotFound), 81 | cache.WithRefreshDuration(time.Minute)) 82 | 83 | ctx := context.TODO() 84 | key := "mykey:1" 85 | obj := new(object) 86 | if err := mycache.Once(ctx, key, cache.Value(obj), cache.TTL(time.Hour), cache.Refresh(true), 87 | cache.Do(func(ctx context.Context) (any, error) { 88 | return mockDBGetObject(1) 89 | })); err != nil { 90 | panic(err) 91 | } 92 | fmt.Println(obj) 93 | // Output: &{mystring 42} 94 | 95 | mycache.Close() 96 | } 97 | 98 | func Example_mGetUsage() { 99 | ring := redis.NewRing(&redis.RingOptions{ 100 | Addrs: map[string]string{ 101 | "localhost": ":6379", 102 | }, 103 | }) 104 | 105 | mycache := cache.New(cache.WithName("any"), 106 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 107 | cache.WithLocal(local.NewFreeCache(256*local.MB, time.Minute)), 108 | cache.WithErrNotFound(errRecordNotFound), 109 | cache.WithRemoteExpiry(time.Minute), 110 | ) 111 | cacheT := cache.NewT[int, *object](mycache) 112 | 113 | ctx := context.TODO() 114 | key := "mget" 115 | ids := []int{1, 2, 3} 116 | 117 | ret := cacheT.MGet(ctx, key, ids, func(ctx context.Context, ids []int) (map[int]*object, error) { 118 | return mockDBMGetObject(ids) 119 | }) 120 | 121 | var b bytes.Buffer 122 | for _, id := range ids { 123 | b.WriteString(fmt.Sprintf("%v", ret[id])) 124 | } 125 | fmt.Println(b.String()) 126 | // Output: &{mystring 1}&{mystring 2} 127 | 128 | cacheT.Close() 129 | } 130 | 131 | func Example_syncLocalUsage() { 132 | ring := redis.NewRing(&redis.RingOptions{ 133 | Addrs: map[string]string{ 134 | "localhost": ":6379", 135 | }, 136 | }) 137 | 138 | sourceID := "12345678" // Unique identifier for this cache instance 139 | channelName := "syncLocalChannel" 140 | pubSub := ring.Subscribe(context.Background(), channelName) 141 | 142 | mycache := cache.New(cache.WithName("any"), 143 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 144 | cache.WithLocal(local.NewFreeCache(256*local.MB, time.Minute)), 145 | cache.WithErrNotFound(errRecordNotFound), 146 | cache.WithRemoteExpiry(time.Minute), 147 | cache.WithSourceId(sourceID), 148 | cache.WithSyncLocal(true), 149 | cache.WithEventHandler(func(event *cache.Event) { 150 | // Broadcast local cache invalidation for the received keys 151 | bs, _ := json.Marshal(event) 152 | ring.Publish(context.Background(), channelName, string(bs)) 153 | }), 154 | ) 155 | obj, _ := mockDBGetObject(1) 156 | if err := mycache.Set(context.TODO(), "mykey", cache.Value(obj), cache.TTL(time.Hour)); err != nil { 157 | panic(err) 158 | } 159 | 160 | go func() { 161 | for { 162 | msg := <-pubSub.Channel() 163 | var event *cache.Event 164 | if err := json.Unmarshal([]byte(msg.Payload), &event); err != nil { 165 | panic(err) 166 | } 167 | fmt.Println(event.Keys) 168 | 169 | // Invalidate local cache for received keys (except own events) 170 | if event.SourceID != sourceID { 171 | for _, key := range event.Keys { 172 | mycache.DeleteFromLocalCache(key) 173 | } 174 | } 175 | } 176 | }() 177 | 178 | // Output: [mykey] 179 | mycache.Close() 180 | time.Sleep(time.Second) 181 | } 182 | -------------------------------------------------------------------------------- /cacheopt_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | 9 | "github.com/mgtv-tech/jetcache-go/encoding/json" 10 | "github.com/mgtv-tech/jetcache-go/stats" 11 | ) 12 | 13 | func TestCacheOptions(t *testing.T) { 14 | t.Run("default options", func(t *testing.T) { 15 | o := newOptions() 16 | assert.Equal(t, defaultName, o.name) 17 | assert.Equal(t, defaultRemoteExpiry, o.remoteExpiry) 18 | assert.Equal(t, defaultNotFoundExpiry, o.notFoundExpiry) 19 | assert.Equal(t, defaultNotFoundExpiry/10, o.offset) 20 | assert.Equal(t, defaultRefreshConcurrency, o.refreshConcurrency) 21 | assert.Equal(t, defaultCodec, o.codec) 22 | assert.NotNil(t, o.statsHandler) 23 | assert.Equal(t, defaultRandSourceIdLen, len(o.sourceID)) 24 | assert.False(t, o.syncLocal) 25 | assert.Equal(t, defaultEventChBufSize, o.eventChBufSize) 26 | assert.Nil(t, o.eventHandler) 27 | assert.Equal(t, defaultSeparator, o.separator) 28 | assert.Equal(t, false, o.separatorDisabled) 29 | }) 30 | 31 | t.Run("with name", func(t *testing.T) { 32 | o := newOptions(WithName("any")) 33 | assert.Equal(t, "any", o.name) 34 | assert.Equal(t, defaultNotFoundExpiry/10, o.offset) 35 | assert.Equal(t, defaultRefreshConcurrency, o.refreshConcurrency) 36 | assert.Equal(t, defaultCodec, o.codec) 37 | }) 38 | 39 | t.Run("with remote expiry", func(t *testing.T) { 40 | o := newOptions(WithRemoteExpiry(time.Second)) 41 | assert.Equal(t, time.Second, o.remoteExpiry) 42 | }) 43 | 44 | t.Run("with not found expiry", func(t *testing.T) { 45 | o := newOptions(WithNotFoundExpiry(time.Second)) 46 | assert.Equal(t, time.Second/10, o.offset) 47 | assert.Equal(t, defaultRefreshConcurrency, o.refreshConcurrency) 48 | assert.Equal(t, defaultCodec, o.codec) 49 | }) 50 | 51 | t.Run("with offset", func(t *testing.T) { 52 | o := newOptions(WithOffset(time.Second)) 53 | assert.Equal(t, time.Second, o.offset) 54 | }) 55 | 56 | t.Run("with max offset", func(t *testing.T) { 57 | o := newOptions(WithOffset(30 * time.Second)) 58 | assert.Equal(t, maxOffset, o.offset) 59 | }) 60 | 61 | t.Run("with refresh duration", func(t *testing.T) { 62 | o := newOptions(WithRefreshDuration(time.Second)) 63 | assert.Equal(t, time.Second, o.refreshDuration) 64 | }) 65 | 66 | t.Run("with refresh concurrency", func(t *testing.T) { 67 | o := newOptions(WithRefreshConcurrency(16)) 68 | assert.Equal(t, defaultNotFoundExpiry, o.notFoundExpiry) 69 | assert.Equal(t, 16, o.refreshConcurrency) 70 | assert.Equal(t, defaultCodec, o.codec) 71 | }) 72 | 73 | t.Run("with mock decode", func(t *testing.T) { 74 | o := newOptions(WithCodec(json.Name)) 75 | assert.Equal(t, defaultNotFoundExpiry, o.notFoundExpiry) 76 | assert.Equal(t, defaultRefreshConcurrency, o.refreshConcurrency) 77 | assert.Equal(t, json.Name, o.codec) 78 | }) 79 | 80 | t.Run("with stats handler", func(t *testing.T) { 81 | stat := stats.NewStatsLogger("any") 82 | o := newOptions(WithStatsHandler(stat)) 83 | assert.Equal(t, stat, o.statsHandler) 84 | }) 85 | 86 | t.Run("with stats disabled", func(t *testing.T) { 87 | o := newOptions(WithStatsDisabled(true)) 88 | assert.Equal(t, true, o.statsDisabled) 89 | }) 90 | 91 | t.Run("with source id", func(t *testing.T) { 92 | sourceId := "12345678" 93 | o := newOptions(WithSourceId(sourceId)) 94 | assert.Equal(t, sourceId, o.sourceID) 95 | }) 96 | 97 | t.Run("with sync local", func(t *testing.T) { 98 | o := newOptions(WithSyncLocal(true)) 99 | assert.True(t, o.syncLocal) 100 | }) 101 | 102 | t.Run("with event chan buffer size", func(t *testing.T) { 103 | o := newOptions(WithEventChBufSize(10)) 104 | assert.Equal(t, o.eventChBufSize, 10) 105 | }) 106 | 107 | t.Run("with event handler", func(t *testing.T) { 108 | o := newOptions(WithEventHandler(func(event *Event) { 109 | })) 110 | assert.NotNil(t, o.eventHandler) 111 | }) 112 | 113 | t.Run("with separator", func(t *testing.T) { 114 | o := newOptions(WithSeparator(":")) 115 | assert.Equal(t, defaultSeparator, o.separator) 116 | }) 117 | 118 | t.Run("with disable default separator", func(t *testing.T) { 119 | o := newOptions(WithSeparatorDisabled(true)) 120 | assert.Equal(t, "", o.separator) 121 | }) 122 | 123 | t.Run("with disable customized separator", func(t *testing.T) { 124 | o := newOptions(WithSeparatorDisabled(true), WithSeparator(":")) 125 | assert.Equal(t, "", o.separator) 126 | }) 127 | 128 | t.Run("with registered codec", func(t *testing.T) { 129 | assert.NotPanics(t, func() { newOptions(WithCodec("sonic")) }) 130 | assert.NotPanics(t, func() { newOptions(WithCodec("json")) }) 131 | assert.NotPanics(t, func() { newOptions(WithCodec("msgpack")) }) 132 | }) 133 | 134 | t.Run("with not registered codec", func(t *testing.T) { 135 | assert.Panics(t, func() { newOptions(WithCodec("not-registered")) }) 136 | }) 137 | } 138 | 139 | func TestCacheOptionsRefreshDuration(t *testing.T) { 140 | tests := []struct { 141 | input time.Duration 142 | expect time.Duration 143 | }{ 144 | { 145 | input: 0, 146 | expect: 0, 147 | }, 148 | { 149 | input: time.Millisecond, 150 | expect: minEffectRefreshDuration, 151 | }, 152 | { 153 | input: time.Minute, 154 | expect: time.Minute, 155 | }, 156 | } 157 | 158 | for _, v := range tests { 159 | o := newOptions(WithRefreshDuration(v.input)) 160 | assert.Equal(t, v.expect, o.refreshDuration) 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /bench_test.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "context" 5 | "math/rand" 6 | "strconv" 7 | "strings" 8 | "sync" 9 | "testing" 10 | "time" 11 | 12 | "github.com/mgtv-tech/jetcache-go/local" 13 | "github.com/mgtv-tech/jetcache-go/logger" 14 | "github.com/mgtv-tech/jetcache-go/remote" 15 | "github.com/redis/go-redis/v9" 16 | ) 17 | 18 | var ( 19 | tOnce sync.Once 20 | rdb *redis.Client 21 | ) 22 | 23 | func tInit() { 24 | tOnce.Do(func() { 25 | rdb = newRdb() 26 | }) 27 | } 28 | 29 | func BenchmarkOnceWithTinyLFU(b *testing.B) { 30 | tInit() 31 | 32 | cache := newBoth(rdb, tinyLFU) 33 | obj := &object{ 34 | Str: strings.Repeat("my very large string", 10), 35 | Num: 42, 36 | } 37 | 38 | b.ResetTimer() 39 | 40 | b.RunParallel(func(pb *testing.PB) { 41 | for pb.Next() { 42 | var dst object 43 | err := cache.Once(context.TODO(), "bench-once", Value(&dst), Do(func(context.Context) (any, error) { 44 | return obj, nil 45 | })) 46 | if err != nil { 47 | b.Fatal(err) 48 | } 49 | if dst.Num != 42 { 50 | b.Fatalf("%d != 42", dst.Num) 51 | } 52 | } 53 | }) 54 | } 55 | 56 | func BenchmarkSetWithTinyLFU(b *testing.B) { 57 | tInit() 58 | 59 | cache := newBoth(rdb, tinyLFU) 60 | obj := &object{ 61 | Str: strings.Repeat("my very large string", 10), 62 | Num: 42, 63 | } 64 | 65 | b.ResetTimer() 66 | 67 | b.RunParallel(func(pb *testing.PB) { 68 | for pb.Next() { 69 | if err := cache.Set(context.TODO(), "bench-set", Value(obj)); err != nil { 70 | b.Fatal(err) 71 | } 72 | } 73 | }) 74 | } 75 | 76 | func BenchmarkOnceWithFreeCache(b *testing.B) { 77 | tInit() 78 | 79 | cache := newBoth(rdb, freeCache) 80 | obj := &object{ 81 | Str: strings.Repeat("my very large string", 10), 82 | Num: 42, 83 | } 84 | 85 | b.ResetTimer() 86 | 87 | b.RunParallel(func(pb *testing.PB) { 88 | for pb.Next() { 89 | var dst object 90 | err := cache.Once(context.TODO(), "bench-once", Value(&dst), 91 | Do(func(context.Context) (any, error) { 92 | return obj, nil 93 | })) 94 | if err != nil { 95 | b.Fatal(err) 96 | } 97 | if dst.Num != 42 { 98 | b.Fatalf("%d != 42", dst.Num) 99 | } 100 | } 101 | }) 102 | } 103 | 104 | func BenchmarkSetWithFreeCache(b *testing.B) { 105 | tInit() 106 | 107 | cache := newBoth(rdb, freeCache) 108 | obj := &object{ 109 | Str: strings.Repeat("my very large string", 10), 110 | Num: 42, 111 | } 112 | 113 | b.ResetTimer() 114 | 115 | b.RunParallel(func(pb *testing.PB) { 116 | for pb.Next() { 117 | if err := cache.Set(context.TODO(), "bench-set", Value(obj)); err != nil { 118 | b.Fatal(err) 119 | } 120 | } 121 | }) 122 | } 123 | 124 | var ( 125 | asyncCache Cache 126 | newOnce sync.Once 127 | ) 128 | 129 | func BenchmarkOnceWithStats(b *testing.B) { 130 | cache := newRefreshBoth() 131 | obj := &object{ 132 | Str: strings.Repeat("my very large string", 10), 133 | Num: 42, 134 | } 135 | 136 | b.ReportAllocs() 137 | b.ResetTimer() 138 | b.RunParallel(func(pb *testing.PB) { 139 | for pb.Next() { 140 | var dst object 141 | err := cache.Once(context.TODO(), "bench-once_"+strconv.Itoa(rand.Intn(256)), 142 | Value(&dst), Do(func(context.Context) (any, error) { 143 | time.Sleep(50 * time.Millisecond) 144 | return obj, nil 145 | })) 146 | if err != nil { 147 | b.Fatal(err) 148 | } 149 | if dst.Num != 42 { 150 | b.Fatalf("%d != 42", dst.Num) 151 | } 152 | } 153 | }) 154 | } 155 | 156 | func BenchmarkOnceRefreshWithStats(b *testing.B) { 157 | logger.SetLevel(logger.LevelInfo) 158 | cache := newRefreshBoth() 159 | obj := &object{ 160 | Str: strings.Repeat("my very large string", 10), 161 | Num: 42, 162 | } 163 | 164 | b.ReportAllocs() 165 | b.ResetTimer() 166 | b.RunParallel(func(pb *testing.PB) { 167 | for pb.Next() { 168 | var dst object 169 | err := cache.Once(context.TODO(), "bench-refresh_"+strconv.Itoa(rand.Intn(256)), 170 | Value(&dst), Do(func(context.Context) (any, error) { 171 | time.Sleep(50 * time.Millisecond) 172 | return obj, nil 173 | }), 174 | Refresh(true)) 175 | if err != nil { 176 | b.Fatal(err) 177 | } 178 | if dst.Num != 42 { 179 | b.Fatalf("%d != 42", dst.Num) 180 | } 181 | } 182 | }) 183 | } 184 | 185 | func BenchmarkMGetWithStats(b *testing.B) { 186 | logger.SetLevel(logger.LevelInfo) 187 | mycache := NewT[int, *object](newRefreshBoth()) 188 | ids := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10} 189 | 190 | b.ReportAllocs() 191 | b.ResetTimer() 192 | b.RunParallel(func(pb *testing.PB) { 193 | for pb.Next() { 194 | mycache.MGet(context.TODO(), "key", ids, func(ctx context.Context, ids []int) (map[int]*object, error) { 195 | return mockDBMGetObject(ids) 196 | }) 197 | } 198 | }) 199 | } 200 | 201 | func mockDBMGetObject(ids []int) (map[int]*object, error) { 202 | ret := make(map[int]*object) 203 | for _, id := range ids { 204 | if id == 3 { 205 | continue 206 | } 207 | ret[id] = &object{Str: "mystring", Num: id} 208 | } 209 | return ret, nil 210 | } 211 | 212 | func newRefreshBoth() Cache { 213 | tInit() 214 | 215 | newOnce.Do(func() { 216 | name := "bench" 217 | asyncCache = New(WithName(name), 218 | WithRemote(remote.NewGoRedisV9Adapter(rdb)), 219 | WithLocal(local.NewFreeCache(256*local.MB, 3*time.Second)), 220 | WithErrNotFound(errTestNotFound), 221 | WithRefreshDuration(2*time.Second), 222 | WithStopRefreshAfterLastAccess(3*time.Second), 223 | WithRefreshConcurrency(1000)) 224 | }) 225 | return asyncCache 226 | } 227 | -------------------------------------------------------------------------------- /docs/EN/Embedded.md: -------------------------------------------------------------------------------- 1 | 2 | * [Introduction](#introduction) 3 | * [Multiple Serialization Methods](#multiple-serialization-methods) 4 | * [Local and Remote Cache Options](#local-and-remote-cache-options) 5 | * [Metrics Collection and Statistics](#metrics-collection-and-statistics) 6 | * [Custom Logger](#custom-logger) 7 | 8 | 9 | # Introduction 10 | 11 | `jetcache-go` provides a unified interface for various built-in components, simplifying integration and providing examples for developers to create their own components. 12 | 13 | 14 | # Multiple Serialization Methods 15 | 16 | `jetcache-go` supports several serialization methods, offering flexibility and performance optimization depending on your needs. Here's a comparison: 17 | 18 | | Codec Method | Description | Advantages | 19 | |---------------|-------------------------------------------------------|----------------------------------------| 20 | | `native json` | Golang's built-in JSON serialization tool | Simplicity, readily available | 21 | | `msgpack` | msgpack with snappy compression (for data > 64 bytes) | High performance, low memory footprint | 22 | | `sonic` | ByteDance's high-performance JSON serialization tool | High performance | 23 | 24 | 25 | You can also customize your serialization by implementing the `encoding.Codec` interface and registering it using `encoding.RegisterCodec`. This allows for integration with other serialization libraries or custom serialization logic tailored to your specific data structures. 26 | 27 | 28 | ```go 29 | 30 | import ( 31 | _ "github.com/mgtv-tech/jetcache-go/encoding/yourCodec" 32 | ) 33 | 34 | // Register your codec 35 | encoding.RegisterCodec(yourCodec.Name) 36 | 37 | mycache := cache.New(cache.WithName("any"), 38 | cache.WithRemote(...), 39 | cache.WithCodec(yourCodec.Name)) 40 | ``` 41 | 42 | 43 | # Local and Remote Cache Options 44 | 45 | `jetcache-go` offers a variety of local and remote cache implementations, providing flexibility to choose the best option for your application's needs. 46 | 47 | | Name | Type | Features | 48 | |-----------------------------------------------------|--------|--------------------------------------------------------------| 49 | | [ristretto](https://github.com/dgraph-io/ristretto) | Local | High performance, high hit ratio | 50 | | [freecache](https://github.com/coocood/freecache) | Local | Zero garbage collection overhead, strict memory usage limits | 51 | | [go-redis](https://github.com/redis/go-redis) | Remote | Popular Go Redis client | 52 | 53 | 54 | You can also implement your own local and remote caches by implementing the `remote.Remote` and `local.Local` interfaces respectively. 55 | 56 | 57 | > **FreeCache Usage Notes:** 58 | > 59 | > * Keys must be less than 65535 bytes. Larger keys will result in an error ("The key is larger than 65535"). 60 | > * Values must be less than 1/1024 of the total cache size. Larger values will result in an error ("The entry size needs to be less than 1/1024 of the cache size"). 61 | > * Embedded FreeCache instances share an internal `innerCache` instance. This prevents excessive memory consumption when multiple cache instances use FreeCache. Therefore, the memory capacity and expiration time will be determined by the configuration of the first created instance. 62 | 63 | 64 | # Metrics Collection and Statistics 65 | 66 | `jetcache-go` provides several ways to collect and report cache metrics: 67 | 68 | | Name | Type | Description | 69 | |-------------------|----------|---------------------------------------------------------------------------------------------------------------------------------| 70 | | `logStats` | Built-in | Default metrics collector; statistics are printed to the log. | 71 | | `PrometheusStats` | Plugin | Statistics plugin provided by [jetcache-go-plugin](https://github.com/mgtv-tech/jetcache-go-plugin) for Prometheus integration. | 72 | 73 | 74 | You can also create custom metrics collectors by implementing the `stats.Handler` interface. 75 | 76 | 77 | Example: Using multiple Metrics collectors simultaneously 78 | 79 | ```go 80 | import ( 81 | "context" 82 | "time" 83 | 84 | "github.com/mgtv-tech/jetcache-go" 85 | "github.com/mgtv-tech/jetcache-go/remote" 86 | "github.com/redis/go-redis/v9" 87 | pstats "github.com/mgtv-tech/jetcache-go-plugin/stats" 88 | "github.com/mgtv-tech/jetcache-go/stats" 89 | ) 90 | 91 | mycache := cache.New(cache.WithName("any"), 92 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 93 | cache.WithStatsHandler( 94 | stats.NewHandles(false, 95 | stats.NewStatsLogger(cacheName), 96 | pstats.NewPrometheus(cacheName)))) 97 | 98 | obj := struct { 99 | Name string 100 | Age int 101 | }{Name: "John Doe", Age: 30} 102 | 103 | err := mycache.Set(context.Background(), "mykey", cache.Value(&obj), cache.TTL(time.Hour)) 104 | if err != nil { 105 | // Handle error 106 | } 107 | ``` 108 | 109 | # Custom Logger 110 | 111 | ```go 112 | import "github.com/mgtv-tech/jetcache-go/logger" 113 | 114 | // Set your Logger 115 | logger.SetDefaultLogger(l logger.Logger) 116 | ``` 117 | 118 | -------------------------------------------------------------------------------- /docs/CN/CacheAPI.md: -------------------------------------------------------------------------------- 1 | 2 | * [缓存接口](#缓存接口) 3 | * [Set 接口](#set-接口) 4 | * [Once 接口](#once-接口) 5 | * [泛型接口](#泛型接口) 6 | * [MGet批量查询](#mget批量查询) 7 | 8 | 9 | 10 | # 缓存接口 11 | 12 | 以下是 Cache 提供的接口,这些方法和签名和 [go-redis/cache](https://github.com/go-redis/cache) 基本一致。但某些接口我们提供了更强大的能力。 13 | 14 | ```go 15 | // Set 通过 ItemOption 设置缓存 16 | func Set(ctx context.Context, key string, opts ...ItemOption) error 17 | 18 | // Once 通过 ItemOption 查询缓存。单飞模式、可开启缓存自动刷新 19 | func Once(ctx context.Context, key string, opts ...ItemOption) error 20 | 21 | // Delete 删除缓存 22 | func Delete(ctx context.Context, key string) error 23 | 24 | // DeleteFromLocalCache 删除本地缓存 25 | func DeleteFromLocalCache(key string) 26 | 27 | // Exists 判断缓存是否存在 28 | func Exists(ctx context.Context, key string) bool 29 | 30 | // Get 查询缓存,并将查询结果序列化到 val 31 | func Get(ctx context.Context, key string, val any) error 32 | 33 | // GetSkippingLocal查询远程缓存(跳过本地缓存) 34 | func GetSkippingLocal(ctx context.Context, key string, val any) error 35 | 36 | // TaskSize 自动刷新缓存的任务数量(本实例本进程) 37 | func TaskSize() int 38 | 39 | // CacheType 缓存类型。共 Both、Remote、Local 三种类型 40 | func CacheType() string 41 | 42 | // Close 关闭缓存资源,当开启了缓存自动刷新且不再需要的时候,需要关闭 43 | func Close() 44 | ``` 45 | 46 | ## Set 接口 47 | 48 | 该接口用于设置缓存。它支持多种选项,例如设置值(`Value`)、远程过期时间(`TTL`)、回源函数(`Do`)、以及针对 `Remote` 缓存的原子操作。 49 | 50 | 函数签名: 51 | ```go 52 | func Set(ctx context.Context, key string, opts ...ItemOption) error 53 | ``` 54 | 55 | 参数: 56 | - `ctx`: `context.Context`,请求上下文。用于取消操作或设置超时。 57 | - `key`: `string`,缓存键。 58 | - `opts`: `...ItemOption`,可变参数列表,用于配置缓存项的各种选项。 支持以下选项: 59 | - `Value(value any)`: 设置缓存值。 60 | - `TTL(duration time.Duration)`: 设置远程缓存项的过期时间。(本地缓存过期时间在构建 Local 缓存实例的时候统一设置) 61 | - `Do(fn func(context.Context) (any, error))`: 给定的回源函数 `fn` 来获取值,优先级高于 `Value`。 62 | - `SetNX(flag bool)`: 仅当键不存在时才设置缓存项。 适用于远程缓存,防止覆盖已存在的值。 63 | - `SetXX(flag bool)`: 仅当键存在时才设置缓存项。适用于远程缓存,确保只更新已存在的值。 64 | 65 | 返回值: 66 | - `error`: 如果设置缓存失败,则返回错误。 67 | 68 | 示例1:使用 `Value` 设置缓存值 69 | ```go 70 | obj := struct { 71 | Name string 72 | Age int 73 | }{Name: "John Doe", Age: 30} 74 | 75 | err := cache.Set(ctx, key, Value(&obj), TTL(time.Hour)) 76 | if err != nil { 77 | // 处理错误 78 | } 79 | ``` 80 | 81 | 示例 2: 使用 `Do` 函数获取并设置缓存值 82 | ```go 83 | err := cache.Once(ctx, key, TTL(time.Hour), Do(func(ctx context.Context) (any, error) { 84 | return fetchData(ctx) 85 | })) 86 | if err != nil { 87 | // 处理错误 88 | } 89 | ``` 90 | 91 | 示例 3: 使用 `SetNX` 进行原子操作 (仅当键不存在时设置) 92 | 93 | ```go 94 | err := cache.Set(ctx, key, TTL(time.Hour), Value(obj), SetNX(true)) 95 | if err != nil { 96 | // 处理错误,例如键已存在 97 | } 98 | ``` 99 | 100 | ## Once 接口 101 | 102 | 该接口从缓存中获取给定 `Key` 的值。若未命中缓存,则执行 `Do` 函数,然后将结果缓存并返回。它确保对于给定的 `Key`,同一时间只有一个执行在进行中。如果出现重复请求,重复的调用者将等待原始请求完成,并接收相同的结果。 103 | 104 | 通过设置 `Refresh(true)` 可开启缓存自动刷新。 105 | 106 | `Once` 接口:分布式-防缓存击穿利器 - `singleflight` 107 | 108 | ![singleflight](/docs/images/singleflight.png) 109 | 110 | > singlefilght ,在go标准库中("golang.org/x/sync/singleflight")提供了可重复的函数调用抑制机制。通过给每次函数调用分配一个key, 111 | > 相同key的函数并发调用时,只会被执行一次,返回相同的结果。其本质是对函数调用的结果进行复用。 112 | 113 | 114 | `Once` 接口:分布式-防缓存击穿利器 - `auto refresh` 115 | 116 | ![autoRefresh](/docs/images/autorefresh.png) 117 | 118 | > `Once`接口提供了自动刷新缓存的能力,目的是为了防止缓存失效时造成的雪崩效应打爆数据库。对一些key比较少,实时性要求不高,加载开销非常大的缓存场景, 119 | > 适合使用自动刷新。下面的代码(示例1)指定每分钟刷新一次,1小时如果没有访问就停止刷新。如果缓存是redis或者多级缓存最后一级是redis,缓存加载行为是全局唯一的, 120 | > 也就是说不管有多少台服务器,同时只有一个服务器在刷新,目的是为了降低后端的加载负担。 121 | 122 | > 关于自动刷新功能的使用场景(“适用于按键数量少、实时性要求低、加载开销非常大的场景”),其中“按键数量少”需要进一步说明。 123 | > 为了确定合适的按键数量,可以建立一个模型。例如,当 refreshConcurrency=10,有 5 台应用服务器,每个按键的平均加载时间为 2 秒,refreshDuration=30s 时,理论上可刷新的最大按键数量为 30 / 2 * 10 * 5 = 750。 124 | 125 | 126 | 127 | 函数签名: 128 | ```go 129 | func Once(ctx context.Context, key string, opts ...ItemOption) error 130 | ``` 131 | 132 | 参数: 133 | - `ctx`: `context.Context`,请求上下文。用于取消操作或设置超时。 134 | - `key`: `string`,缓存键。 135 | - `opts`: `...ItemOption`,可变参数列表,用于配置缓存项的各种选项。 支持以下选项: 136 | - `Value(value any)`: 设置缓存值。 137 | - `TTL(duration time.Duration)`: 设置远程缓存项的过期时间。(本地缓存过期时间在构建 Local 缓存实例的时候统一设置) 138 | - `Do(fn func(context.Context) (any, error))`: 给定的回源函数 `fn` 来获取值,优先级高于 `Value`。 139 | - `SkipLocal(flag bool)`: 是否跳过本地缓存。 140 | - `Refresh(refresh bool)`: 是否开启缓存自动刷新。配合 Cache 配置参数 `config.refreshDuration` 设置刷新周期。 141 | 142 | 返回值: 143 | - `error`: 如果设置缓存失败,则返回错误。 144 | 145 | 示例1:使用 `Once` 查询数据,并开启缓存自动刷新 146 | 147 | ```go 148 | mycache := cache.New(cache.WithName("any"), 149 | // ... 150 | // cache.WithRefreshDuration 设置异步刷新时间间隔 151 | cache.WithRefreshDuration(time.Minute), 152 | // cache.WithStopRefreshAfterLastAccess 设置缓存 key 没有访问后的刷新任务取消时间 153 | cache.WithStopRefreshAfterLastAccess(time.Hour)) 154 | 155 | // `Once` 接口通过 `cache.Refresh(true)` 开启自动刷新 156 | err := mycache.Once(ctx, key, cache.Value(obj), cache.Refresh(true), cache.Do(func(ctx context.Context) (any, error) { 157 | return fetchData(ctx) 158 | })) 159 | if err != nil { 160 | // 处理错误 161 | } 162 | 163 | mycache.Close() 164 | ``` 165 | 166 | # 泛型接口 167 | 168 | ```go 169 | // Set 泛型设置缓存 170 | func (w *T[K, V]) Set(ctx context.Context, key string, id K, v V) error 171 | 172 | // Get 泛型查询缓存 (底层调用Once接口) 173 | func (w *T[K, V]) Get(ctx context.Context, key string, id K, fn func(context.Context, K) (V, error)) (V, error) 174 | 175 | // MGet 泛型批量查询缓存 176 | func (w *T[K, V]) MGet(ctx context.Context, key string, ids []K, fn func(context.Context, []K) (map[K]V, error)) (result map[K]V) 177 | ``` 178 | 179 | ## MGet批量查询 180 | 181 | `MGet` 通过 `golang` 的泛型机制 + `Load` 函数,非常友好的多级缓存批量查询ID对应的实体。如果缓存是 `redis` 或者多级缓存最后一级是 `redis`, 182 | 查询时采用 `pipeline`实现读写操作,提升性能。查询未命中本地缓存,需要去查询Redis和DB时,会对Key排序,并采用单飞模式(`singleflight`)调用。 183 | 需要说明是,针对异常场景(IO异常、序列化异常等),我们设计思路是尽可能提供有损服务,防止穿透。 184 | 185 | ![mget](/docs/images/mget.png) 186 | 187 | 函数签名: 188 | ```go 189 | func (w *T[K, V]) MGet(ctx context.Context, key string, ids []K, fn func(context.Context, []K) (map[K]V, error)) (result map[K]V) 190 | func (w *T[K, V]) MGetWithErr(ctx context.Context, key string, ids []K, fn func(context.Context, []K) (map[K]V, error)) (result map[K]V, err error) 191 | ``` 192 | 193 | 参数: 194 | - `ctx`: `context.Context`,请求上下文。用于取消操作或设置超时。 195 | - `key`: `string`,缓存键。 196 | - `ids`: `[]K`,缓存对象的ID。 197 | - `fn func(context.Context, []K) (map[K]V, error)`:回源函数。用于给未命中缓存的ID去查询数据并设置缓存。 198 | 199 | 返回值: 200 | - `map[K]V`: 返回有值键值对 `map`。 201 | -------------------------------------------------------------------------------- /stats/statslogger.go: -------------------------------------------------------------------------------- 1 | package stats 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | "sync" 9 | "sync/atomic" 10 | "time" 11 | 12 | "github.com/mgtv-tech/jetcache-go/logger" 13 | ) 14 | 15 | const defaultStatsInterval = time.Minute 16 | 17 | var ( 18 | once sync.Once 19 | inner *innerStats 20 | _ Handler = (*Stats)(nil) 21 | ) 22 | 23 | type ( 24 | Stats struct { 25 | Options 26 | Name string 27 | Hit uint64 28 | Miss uint64 29 | LocalHit uint64 30 | LocalMiss uint64 31 | RemoteHit uint64 32 | RemoteMiss uint64 33 | Query uint64 34 | QueryFail uint64 35 | } 36 | 37 | Options struct { 38 | statsInterval time.Duration 39 | } 40 | 41 | // Option defines the method to customize an Options. 42 | Option func(o *Options) 43 | 44 | innerStats struct { 45 | statsInterval time.Duration 46 | stats []*Stats 47 | } 48 | ) 49 | 50 | func WithStatsInterval(statsInterval time.Duration) Option { 51 | return func(o *Options) { 52 | o.statsInterval = statsInterval 53 | } 54 | } 55 | 56 | func NewStatsLogger(name string, opts ...Option) Handler { 57 | var o Options 58 | for _, opt := range opts { 59 | opt(&o) 60 | } 61 | if o.statsInterval <= 0 { 62 | o.statsInterval = defaultStatsInterval 63 | } 64 | once.Do(func() { 65 | inner = &innerStats{ 66 | statsInterval: o.statsInterval, 67 | stats: make([]*Stats, 0), 68 | } 69 | 70 | go func() { 71 | ticker := time.NewTicker(o.statsInterval) 72 | defer ticker.Stop() 73 | 74 | inner.statLoop(ticker) 75 | }() 76 | }) 77 | 78 | stat := &Stats{ 79 | Name: name, 80 | Options: o, 81 | } 82 | 83 | inner.stats = append(inner.stats, stat) 84 | 85 | return stat 86 | } 87 | 88 | func (s *Stats) IncrHit() { 89 | atomic.AddUint64(&s.Hit, 1) 90 | } 91 | 92 | func (s *Stats) IncrMiss() { 93 | atomic.AddUint64(&s.Miss, 1) 94 | } 95 | 96 | func (s *Stats) IncrLocalHit() { 97 | atomic.AddUint64(&s.LocalHit, 1) 98 | } 99 | 100 | func (s *Stats) IncrLocalMiss() { 101 | atomic.AddUint64(&s.LocalMiss, 1) 102 | } 103 | 104 | func (s *Stats) IncrRemoteHit() { 105 | atomic.AddUint64(&s.RemoteHit, 1) 106 | } 107 | 108 | func (s *Stats) IncrRemoteMiss() { 109 | atomic.AddUint64(&s.RemoteMiss, 1) 110 | } 111 | 112 | func (s *Stats) IncrQuery() { 113 | atomic.AddUint64(&s.Query, 1) 114 | } 115 | 116 | func (s *Stats) IncrQueryFail(err error) { 117 | atomic.AddUint64(&s.QueryFail, 1) 118 | } 119 | 120 | func (inner *innerStats) statLoop(ticker *time.Ticker) { 121 | for range ticker.C { 122 | inner.logStatSummary() 123 | } 124 | } 125 | 126 | func (inner *innerStats) logStatSummary() { 127 | stats := make([]Stats, len(inner.stats)) 128 | var maxNameLen int 129 | for i, s := range inner.stats { 130 | stats[i] = Stats{ 131 | Name: s.Name, 132 | Hit: atomic.SwapUint64(&s.Hit, 0), 133 | Miss: atomic.SwapUint64(&s.Miss, 0), 134 | RemoteHit: atomic.SwapUint64(&s.RemoteHit, 0), 135 | RemoteMiss: atomic.SwapUint64(&s.RemoteMiss, 0), 136 | LocalHit: atomic.SwapUint64(&s.LocalHit, 0), 137 | LocalMiss: atomic.SwapUint64(&s.LocalMiss, 0), 138 | Query: atomic.SwapUint64(&s.Query, 0), 139 | QueryFail: atomic.SwapUint64(&s.QueryFail, 0), 140 | } 141 | if len(s.Name) > maxNameLen { 142 | maxNameLen = len(s.Name) 143 | } 144 | } 145 | maxLenStr := strconv.Itoa(maxNameLen + 7) 146 | rows := formatRows(stats, maxLenStr) 147 | if len(rows) > 0 { 148 | var sb strings.Builder 149 | header := formatHeader(maxLenStr) 150 | sb.WriteString(fmt.Sprintf("jetcache-go stats last %s.\n", inner.statsInterval)) 151 | sb.WriteString(header) 152 | sb.WriteString(formatSepLine(header)) 153 | sb.WriteString("\n") 154 | sb.WriteString(rows) 155 | sb.WriteString(formatSepLine(header)) 156 | logger.Info(sb.String()) 157 | } 158 | } 159 | 160 | func formatHeader(maxLenStr string) string { 161 | return fmt.Sprintf("%-"+maxLenStr+"s|%12s|%12s|%12s|%12s|%12s|%12s\n", "cache", "qpm", "hit_ratio", "hit", "miss", "query", "query_fail") 162 | } 163 | 164 | func formatRows(stats []Stats, maxLenStr string) string { 165 | var rows strings.Builder 166 | for _, s := range stats { 167 | total := s.Hit + s.Miss 168 | remoteTotal := s.RemoteHit + s.RemoteMiss 169 | localTotal := s.LocalHit + s.LocalMiss 170 | if total == 0 && s.Query == 0 && s.QueryFail == 0 { 171 | continue 172 | } 173 | // All 174 | rows.WriteString(fmt.Sprintf("%-"+maxLenStr+"s|", s.Name)) 175 | rows.WriteString(fmt.Sprintf("%12d|", total)) 176 | rows.WriteString(fmt.Sprintf("%11s", rate(s.Hit, total))) 177 | rows.WriteString("%%|") 178 | rows.WriteString(fmt.Sprintf("%12d|", s.Hit)) 179 | rows.WriteString(fmt.Sprintf("%12d|", s.Miss)) 180 | rows.WriteString(fmt.Sprintf("%12d|", s.Query)) 181 | rows.WriteString(fmt.Sprintf("%12d", s.QueryFail)) 182 | rows.WriteString("\n") 183 | // Local 184 | if localTotal > 0 { 185 | rows.WriteString(fmt.Sprintf("%-"+maxLenStr+"s|", getName(s.Name, "local"))) 186 | rows.WriteString(fmt.Sprintf("%12d|", localTotal)) 187 | rows.WriteString(fmt.Sprintf("%11s", rate(s.LocalHit, localTotal))) 188 | rows.WriteString("%%|") 189 | rows.WriteString(fmt.Sprintf("%12d|", s.LocalHit)) 190 | rows.WriteString(fmt.Sprintf("%12d|", s.LocalMiss)) 191 | rows.WriteString(fmt.Sprintf("%12s|", "-")) 192 | rows.WriteString(fmt.Sprintf("%12s", "-")) 193 | rows.WriteString("\n") 194 | } 195 | // Remote 196 | if remoteTotal > 0 { 197 | rows.WriteString(fmt.Sprintf("%-"+maxLenStr+"s|", getName(s.Name, "remote"))) 198 | rows.WriteString(fmt.Sprintf("%12d|", remoteTotal)) 199 | rows.WriteString(fmt.Sprintf("%11s", rate(s.RemoteHit, remoteTotal))) 200 | rows.WriteString("%%|") 201 | rows.WriteString(fmt.Sprintf("%12d|", s.RemoteHit)) 202 | rows.WriteString(fmt.Sprintf("%12d|", s.RemoteMiss)) 203 | rows.WriteString(fmt.Sprintf("%12s|", "-")) 204 | rows.WriteString(fmt.Sprintf("%12s", "-")) 205 | rows.WriteString("\n") 206 | } 207 | } 208 | 209 | return rows.String() 210 | } 211 | 212 | func formatSepLine(header string) string { 213 | var b bytes.Buffer 214 | var l = len(header) 215 | for i, c := range header { 216 | if i+1 == l { 217 | continue 218 | } 219 | if c == '|' { 220 | b.WriteString("+") 221 | } else { 222 | b.WriteString("-") 223 | } 224 | } 225 | return b.String() 226 | } 227 | 228 | func rate(count, total uint64) string { 229 | if total == 0 { 230 | return "0.00" 231 | } 232 | 233 | return fmt.Sprintf("%2.2f", float64(count*100)/float64(total)) 234 | } 235 | 236 | func getName(name, typ string) string { 237 | return fmt.Sprintf("%s_%s", name, typ) 238 | } 239 | -------------------------------------------------------------------------------- /README_zh.md: -------------------------------------------------------------------------------- 1 | # jetcache-go 2 | ![banner](docs/images/banner.png) 3 | 4 |

5 | Build Status 6 | codeCov 7 | Go Repport Card 8 | License 9 | Release 10 |

11 | 12 | Translations: [English](README.md) | [简体中文](README_zh.md) 13 | 14 | # 简介 15 | [jetcache-go](https://github.com/mgtv-tech/jetcache-go)是基于[go-redis/cache](https://github.com/go-redis/cache)拓展的通用缓存访问框架。 16 | 实现了类似Java版[JetCache](https://github.com/alibaba/jetcache)的核心功能,包括: 17 | 18 | - ✅ 二级缓存自由组合:本地缓存、分布式缓存、本地缓存+分布式缓存 19 | - ✅ `Once`接口采用单飞(`singleflight`)模式,高并发且线程安全 20 | - ✅ 默认采用[MsgPack](https://github.com/vmihailenco/msgpack)来编解码Value。可选[sonic](https://github.com/bytedance/sonic)、原生`json` 21 | - ✅ 本地缓存默认实现了[Ristretto](https://github.com/dgraph-io/ristretto)和[FreeCache](https://github.com/coocood/freecache) 22 | - ✅ 分布式缓存默认实现了[go-redis/v9](https://github.com/redis/go-redis)的适配器,你也可以自定义实现 23 | - ✅ 可以自定义`errNotFound`,通过占位符替换,缓存空结果防止缓存穿透 24 | - ✅ 支持开启分布式缓存异步刷新 25 | - ✅ 指标采集,默认实现了通过日志打印各级缓存的统计指标(QPM、Hit、Miss、Query、QueryFail) 26 | - ✅ 分布式缓存查询故障自动降级 27 | - ✅ `MGet`接口支持`Load`函数。带分布缓存场景,采用`Pipeline`模式实现 (v1.1.0+) 28 | - ✅ 支持拓展缓存更新后所有GO进程的本地缓存失效 (v1.1.1+) 29 | 30 | # 详细文档 31 | 32 | Visit [documentation](/docs/CN/GettingStarted.md) for more details. 33 | 34 | # 安装 35 | 36 | 使用最新版本的jetcache-go,您可以在项目中导入该库: 37 | 38 | ```shell 39 | go get github.com/mgtv-tech/jetcache-go 40 | ``` 41 | 42 | # 快速开始 43 | 44 | ```go 45 | import ( 46 | "bytes" 47 | "context" 48 | "encoding/json" 49 | "errors" 50 | "fmt" 51 | "time" 52 | 53 | "github.com/mgtv-tech/jetcache-go" 54 | "github.com/mgtv-tech/jetcache-go/local" 55 | "github.com/mgtv-tech/jetcache-go/remote" 56 | "github.com/redis/go-redis/v9" 57 | ) 58 | 59 | var errRecordNotFound = errors.New("mock gorm.errRecordNotFound") 60 | 61 | type object struct { 62 | Str string 63 | Num int 64 | } 65 | 66 | func Example_basicUsage() { 67 | ring := redis.NewRing(&redis.RingOptions{ 68 | Addrs: map[string]string{ 69 | "localhost": ":6379", 70 | }, 71 | }) 72 | 73 | mycache := cache.New(cache.WithName("any"), 74 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 75 | cache.WithLocal(local.NewFreeCache(256*local.MB, time.Minute)), 76 | cache.WithErrNotFound(errRecordNotFound)) 77 | 78 | ctx := context.TODO() 79 | key := "mykey:1" 80 | obj, _ := mockDBGetObject(1) 81 | if err := mycache.Set(ctx, key, cache.Value(obj), cache.TTL(time.Hour)); err != nil { 82 | panic(err) 83 | } 84 | 85 | var wanted object 86 | if err := mycache.Get(ctx, key, &wanted); err == nil { 87 | fmt.Println(wanted) 88 | } 89 | // Output: {mystring 42} 90 | 91 | mycache.Close() 92 | } 93 | 94 | func Example_advancedUsage() { 95 | ring := redis.NewRing(&redis.RingOptions{ 96 | Addrs: map[string]string{ 97 | "localhost": ":6379", 98 | }, 99 | }) 100 | 101 | mycache := cache.New(cache.WithName("any"), 102 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 103 | cache.WithLocal(local.NewFreeCache(256*local.MB, time.Minute)), 104 | cache.WithErrNotFound(errRecordNotFound), 105 | cache.WithRefreshDuration(time.Minute)) 106 | 107 | ctx := context.TODO() 108 | key := "mykey:1" 109 | obj := new(object) 110 | if err := mycache.Once(ctx, key, cache.Value(obj), cache.TTL(time.Hour), cache.Refresh(true), 111 | cache.Do(func(ctx context.Context) (any, error) { 112 | return mockDBGetObject(1) 113 | })); err != nil { 114 | panic(err) 115 | } 116 | fmt.Println(obj) 117 | // Output: &{mystring 42} 118 | 119 | mycache.Close() 120 | } 121 | 122 | func Example_mGetUsage() { 123 | ring := redis.NewRing(&redis.RingOptions{ 124 | Addrs: map[string]string{ 125 | "localhost": ":6379", 126 | }, 127 | }) 128 | 129 | mycache := cache.New(cache.WithName("any"), 130 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 131 | cache.WithLocal(local.NewFreeCache(256*local.MB, time.Minute)), 132 | cache.WithErrNotFound(errRecordNotFound), 133 | cache.WithRemoteExpiry(time.Minute), 134 | ) 135 | cacheT := cache.NewT[int, *object](mycache) 136 | 137 | ctx := context.TODO() 138 | key := "mget" 139 | ids := []int{1, 2, 3} 140 | 141 | ret := cacheT.MGet(ctx, key, ids, func(ctx context.Context, ids []int) (map[int]*object, error) { 142 | return mockDBMGetObject(ids) 143 | }) 144 | 145 | var b bytes.Buffer 146 | for _, id := range ids { 147 | b.WriteString(fmt.Sprintf("%v", ret[id])) 148 | } 149 | fmt.Println(b.String()) 150 | // Output: &{mystring 1}&{mystring 2} 151 | 152 | cacheT.Close() 153 | } 154 | 155 | func Example_syncLocalUsage() { 156 | ring := redis.NewRing(&redis.RingOptions{ 157 | Addrs: map[string]string{ 158 | "localhost": ":6379", 159 | }, 160 | }) 161 | 162 | sourceID := "12345678" // Unique identifier for this cache instance 163 | channelName := "syncLocalChannel" 164 | pubSub := ring.Subscribe(context.Background(), channelName) 165 | 166 | mycache := cache.New(cache.WithName("any"), 167 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 168 | cache.WithLocal(local.NewFreeCache(256*local.MB, time.Minute)), 169 | cache.WithErrNotFound(errRecordNotFound), 170 | cache.WithRemoteExpiry(time.Minute), 171 | cache.WithSourceId(sourceID), 172 | cache.WithSyncLocal(true), 173 | cache.WithEventHandler(func(event *cache.Event) { 174 | // Broadcast local cache invalidation for the received keys 175 | bs, _ := json.Marshal(event) 176 | ring.Publish(context.Background(), channelName, string(bs)) 177 | }), 178 | ) 179 | obj, _ := mockDBGetObject(1) 180 | if err := mycache.Set(context.TODO(), "mykey", cache.Value(obj), cache.TTL(time.Hour)); err != nil { 181 | panic(err) 182 | } 183 | 184 | go func() { 185 | for { 186 | msg := <-pubSub.Channel() 187 | var event *cache.Event 188 | if err := json.Unmarshal([]byte(msg.Payload), &event); err != nil { 189 | panic(err) 190 | } 191 | fmt.Println(event.Keys) 192 | 193 | // Invalidate local cache for received keys (except own events) 194 | if event.SourceID != sourceID { 195 | for _, key := range event.Keys { 196 | mycache.DeleteFromLocalCache(key) 197 | } 198 | } 199 | } 200 | }() 201 | 202 | // Output: [mykey] 203 | mycache.Close() 204 | time.Sleep(time.Second) 205 | } 206 | 207 | func mockDBGetObject(id int) (*object, error) { 208 | if id > 100 { 209 | return nil, errRecordNotFound 210 | } 211 | return &object{Str: "mystring", Num: 42}, nil 212 | } 213 | 214 | func mockDBMGetObject(ids []int) (map[int]*object, error) { 215 | ret := make(map[int]*object) 216 | for _, id := range ids { 217 | if id == 3 { 218 | continue 219 | } 220 | ret[id] = &object{Str: "mystring", Num: id} 221 | } 222 | return ret, nil 223 | } 224 | ``` 225 | 226 | # 贡献 227 | 228 | 欢迎大家一起来完善jetcache-go。如果您有任何疑问、建议或者想添加其他功能,请直接提交issue或者PR。 229 | 230 | 请按照以下步骤来提交PR: 231 | 232 | - 克隆仓库 233 | - 创建一个新分支:如果是新功能分支,则命名为feature-xxx;如果是修复bug,则命名为bug-xxx 234 | - 在PR中详细描述更改的内容 235 | 236 | # 联系 237 | 238 | 如果您有任何问题,请联系 `daoshenzzg@gmail.com`。 239 | 240 | -------------------------------------------------------------------------------- /docs/CN/GettingStarted.md: -------------------------------------------------------------------------------- 1 | ![banner](/docs/images/banner.png) 2 | 3 | 4 | * [简介](#简介) 5 | * [产品对比](#产品对比) 6 | * [详细文档](#详细文档) 7 | * [安装](#安装) 8 | * [快速开始](#快速开始) 9 | 10 | 11 | # 简介 12 | 13 | `jetcache-go` 是基于 [go-redis/cache](https://github.com/go-redis/cache) 拓展的通用缓存框架。实现了类似Java版[JetCache](https://github.com/alibaba/jetcache)的核心功能,包括: 14 | 15 | - ✅ 二级缓存自由组合:本地缓存、分布式缓存、本地缓存+分布式缓存 16 | - ✅ `Once`接口采用单飞(`singleflight`)模式,高并发且线程安全 17 | - ✅ 默认采用[MsgPack](https://github.com/vmihailenco/msgpack)来编解码Value。可选[sonic](https://github.com/bytedance/sonic)、原生`json` 18 | - ✅ 本地缓存默认实现了[Ristretto](https://github.com/dgraph-io/ristretto)和[FreeCache](https://github.com/coocood/freecache) 19 | - ✅ 分布式缓存默认实现了[go-redis/v9](https://github.com/redis/go-redis)的适配器,你也可以自定义实现 20 | - ✅ 可以自定义`errNotFound`,通过占位符替换,缓存空结果防止缓存穿透 21 | - ✅ 支持开启分布式缓存异步刷新 22 | - ✅ 指标采集,默认实现了通过日志打印各级缓存的统计指标(QPM、Hit、Miss、Query、QueryFail) 23 | - ✅ 分布式缓存查询故障自动降级 24 | - ✅ `MGet`接口支持`Load`函数。带分布缓存场景,采用`Pipeline`模式实现 (v1.1.0+) 25 | - ✅ 支持拓展缓存更新后所有GO进程的本地缓存失效 (v1.1.1+) 26 | 27 | # 产品对比 28 | 29 | | 特性 | eko/gocache | go-redis/cache | mgtv-tech/jetcache-go | 30 | |----------------|-------------|----------------|-----------------------| 31 | | 多级缓存 | Yes | Yes | Yes | 32 | | 缓存旁路(loadable) | Yes | Yes | Yes | 33 | | 泛型支持 | Yes | No | Yes | 34 | | 单飞模式 | Yes | Yes | Yes | 35 | | 缓存更新监听器 | No | No | Yes | 36 | | 自动刷新 | No | No | Yes | 37 | | 指标采集 | Yes | Yes (simple) | Yes | 38 | | 缓存空对象 | No | No | Yes | 39 | | 批量查询 | No | No | Yes | 40 | | 稀疏列表缓存 | No | No | Yes | 41 | 42 | 43 | # 详细文档 44 | 45 | - 快速开始 46 | - [缓存 API](/docs/CN/CacheAPI.md) 47 | - [配置选项](/docs/CN/Config.md) 48 | - [内嵌组件](/docs/CN/Embedded.md) 49 | - [指标统计](/docs/CN/Stat.md) 50 | - [插件项目](/docs/CN/Plugin.md) 51 | 52 | # 安装 53 | 54 | 使用最新版本的jetcache-go,您可以在项目中导入该库: 55 | 56 | ```shell 57 | go get github.com/mgtv-tech/jetcache-go 58 | ``` 59 | # 快速开始 60 | 61 | ```go 62 | import ( 63 | "bytes" 64 | "context" 65 | "encoding/json" 66 | "errors" 67 | "fmt" 68 | "time" 69 | 70 | "github.com/mgtv-tech/jetcache-go" 71 | "github.com/mgtv-tech/jetcache-go/local" 72 | "github.com/mgtv-tech/jetcache-go/remote" 73 | "github.com/redis/go-redis/v9" 74 | ) 75 | 76 | var errRecordNotFound = errors.New("mock gorm.ErrRecordNotFound") 77 | 78 | type object struct { 79 | Str string 80 | Num int 81 | } 82 | 83 | func Example_basicUsage() { 84 | ring := redis.NewRing(&redis.RingOptions{ 85 | Addrs: map[string]string{ 86 | "localhost": ":6379", 87 | }, 88 | }) 89 | 90 | mycache := cache.New(cache.WithName("any"), 91 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 92 | cache.WithLocal(local.NewFreeCache(256*local.MB, time.Minute)), 93 | cache.WithErrNotFound(errRecordNotFound)) 94 | 95 | ctx := context.TODO() 96 | key := "mykey:1" 97 | obj, _ := mockDBGetObject(1) 98 | if err := mycache.Set(ctx, key, cache.Value(obj), cache.TTL(time.Hour)); err != nil { 99 | panic(err) 100 | } 101 | 102 | var wanted object 103 | if err := mycache.Get(ctx, key, &wanted); err == nil { 104 | fmt.Println(wanted) 105 | } 106 | // Output: {mystring 42} 107 | 108 | mycache.Close() 109 | } 110 | 111 | func Example_advancedUsage() { 112 | ring := redis.NewRing(&redis.RingOptions{ 113 | Addrs: map[string]string{ 114 | "localhost": ":6379", 115 | }, 116 | }) 117 | 118 | mycache := cache.New(cache.WithName("any"), 119 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 120 | cache.WithLocal(local.NewFreeCache(256*local.MB, time.Minute)), 121 | cache.WithErrNotFound(errRecordNotFound), 122 | cache.WithRefreshDuration(time.Minute)) 123 | 124 | ctx := context.TODO() 125 | key := "mykey:1" 126 | obj := new(object) 127 | if err := mycache.Once(ctx, key, cache.Value(obj), cache.TTL(time.Hour), cache.Refresh(true), 128 | cache.Do(func(ctx context.Context) (any, error) { 129 | return mockDBGetObject(1) 130 | })); err != nil { 131 | panic(err) 132 | } 133 | fmt.Println(obj) 134 | // Output: &{mystring 42} 135 | 136 | mycache.Close() 137 | } 138 | 139 | func Example_mGetUsage() { 140 | ring := redis.NewRing(&redis.RingOptions{ 141 | Addrs: map[string]string{ 142 | "localhost": ":6379", 143 | }, 144 | }) 145 | 146 | mycache := cache.New(cache.WithName("any"), 147 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 148 | cache.WithLocal(local.NewFreeCache(256*local.MB, time.Minute)), 149 | cache.WithErrNotFound(errRecordNotFound), 150 | cache.WithRemoteExpiry(time.Minute), 151 | ) 152 | cacheT := cache.NewT[int, *object](mycache) 153 | 154 | ctx := context.TODO() 155 | key := "mget" 156 | ids := []int{1, 2, 3} 157 | 158 | ret := cacheT.MGet(ctx, key, ids, func(ctx context.Context, ids []int) (map[int]*object, error) { 159 | return mockDBMGetObject(ids) 160 | }) 161 | 162 | var b bytes.Buffer 163 | for _, id := range ids { 164 | b.WriteString(fmt.Sprintf("%v", ret[id])) 165 | } 166 | fmt.Println(b.String()) 167 | // Output: &{mystring 1}&{mystring 2} 168 | 169 | cacheT.Close() 170 | } 171 | 172 | func Example_syncLocalUsage() { 173 | ring := redis.NewRing(&redis.RingOptions{ 174 | Addrs: map[string]string{ 175 | "localhost": ":6379", 176 | }, 177 | }) 178 | 179 | sourceID := "12345678" // Unique identifier for this cache instance 180 | channelName := "syncLocalChannel" 181 | pubSub := ring.Subscribe(context.Background(), channelName) 182 | 183 | mycache := cache.New(cache.WithName("any"), 184 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 185 | cache.WithLocal(local.NewFreeCache(256*local.MB, time.Minute)), 186 | cache.WithErrNotFound(errRecordNotFound), 187 | cache.WithRemoteExpiry(time.Minute), 188 | cache.WithSourceId(sourceID), 189 | cache.WithSyncLocal(true), 190 | cache.WithEventHandler(func(event *cache.Event) { 191 | // Broadcast local cache invalidation for the received keys 192 | bs, _ := json.Marshal(event) 193 | ring.Publish(context.Background(), channelName, string(bs)) 194 | }), 195 | ) 196 | obj, _ := mockDBGetObject(1) 197 | if err := mycache.Set(context.TODO(), "mykey", cache.Value(obj), cache.TTL(time.Hour)); err != nil { 198 | panic(err) 199 | } 200 | 201 | go func() { 202 | for { 203 | msg := <-pubSub.Channel() 204 | var event *cache.Event 205 | if err := json.Unmarshal([]byte(msg.Payload), &event); err != nil { 206 | panic(err) 207 | } 208 | fmt.Println(event.Keys) 209 | 210 | // Invalidate local cache for received keys (except own events) 211 | if event.SourceID != sourceID { 212 | for _, key := range event.Keys { 213 | mycache.DeleteFromLocalCache(key) 214 | } 215 | } 216 | } 217 | }() 218 | 219 | // Output: [mykey] 220 | mycache.Close() 221 | time.Sleep(time.Second) 222 | } 223 | 224 | func mockDBGetObject(id int) (*object, error) { 225 | if id > 100 { 226 | return nil, errRecordNotFound 227 | } 228 | return &object{Str: "mystring", Num: 42}, nil 229 | } 230 | 231 | func mockDBMGetObject(ids []int) (map[int]*object, error) { 232 | ret := make(map[int]*object) 233 | for _, id := range ids { 234 | if id == 3 { 235 | continue 236 | } 237 | ret[id] = &object{Str: "mystring", Num: id} 238 | } 239 | return ret, nil 240 | } 241 | ``` 242 | -------------------------------------------------------------------------------- /cacheopt.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/mgtv-tech/jetcache-go/encoding" 8 | _ "github.com/mgtv-tech/jetcache-go/encoding/json" 9 | "github.com/mgtv-tech/jetcache-go/encoding/msgpack" 10 | _ "github.com/mgtv-tech/jetcache-go/encoding/sonic" 11 | "github.com/mgtv-tech/jetcache-go/local" 12 | "github.com/mgtv-tech/jetcache-go/remote" 13 | "github.com/mgtv-tech/jetcache-go/stats" 14 | "github.com/mgtv-tech/jetcache-go/util" 15 | ) 16 | 17 | const ( 18 | defaultName = "default" 19 | defaultRefreshConcurrency = 4 20 | defaultRemoteExpiry = time.Hour 21 | defaultNotFoundExpiry = time.Minute 22 | defaultCodec = msgpack.Name 23 | defaultRandSourceIdLen = 16 24 | defaultEventChBufSize = 100 25 | defaultSeparator = ":" 26 | minEffectRefreshDuration = time.Second 27 | maxOffset = 10 * time.Second 28 | ) 29 | 30 | const ( 31 | EventTypeSet EventType = 1 32 | EventTypeSetByOnce EventType = 2 33 | EventTypeSetByRefresh EventType = 3 34 | EventTypeSetByMGet EventType = 4 35 | EventTypeDelete EventType = 5 36 | ) 37 | 38 | type ( 39 | // Options are used to store cache options. 40 | Options struct { 41 | name string // Cache name, used for log identification and metric reporting 42 | remote remote.Remote // Remote is distributed cache, such as Redis. 43 | local local.Local // Local is memory cache, such as FreeCache. 44 | codec string // Value encoding and decoding method. Default is "msgpack.Name". You can also customize it. 45 | errNotFound error // Error to return for cache miss. Used to prevent cache penetration. 46 | remoteExpiry time.Duration // Remote cache ttl, Default is 1 hour. 47 | notFoundExpiry time.Duration // Duration for placeholder cache when there is a cache miss. Default is 1 minute. 48 | offset time.Duration // Expiration time jitter factor for cache misses. 49 | refreshDuration time.Duration // Interval for asynchronous cache refresh. Default is 0 (refresh is disabled). 50 | stopRefreshAfterLastAccess time.Duration // Duration for cache to stop refreshing after no access. Default is refreshDuration + 1 second. 51 | refreshConcurrency int // Maximum number of concurrent cache refreshes. Default is 4. 52 | statsDisabled bool // Flag to disable cache statistics. 53 | statsHandler stats.Handler // Metrics statsHandler collector. 54 | sourceID string // Unique identifier for cache instance. 55 | syncLocal bool // Enable events for syncing local cache (only for "Both" cache type). 56 | eventChBufSize int // Buffer size for event channel (default: 100). 57 | eventHandler func(event *Event) // Function to handle local cache invalidation events. 58 | separatorDisabled bool // Disable separator for cache key. Default is false. If true, the cache key will not be split into multiple parts. 59 | separator string // Separator for cache key. Default is ":". 60 | } 61 | 62 | // Option defines the method to customize an Options. 63 | Option func(o *Options) 64 | 65 | EventType int 66 | 67 | Event struct { 68 | CacheName string 69 | SourceID string 70 | EventType EventType 71 | Keys []string 72 | } 73 | ) 74 | 75 | func newOptions(opts ...Option) Options { 76 | var o Options 77 | for _, opt := range opts { 78 | opt(&o) 79 | } 80 | if o.name == "" { 81 | o.name = defaultName 82 | } 83 | if o.codec == "" { 84 | o.codec = defaultCodec 85 | } 86 | if o.remoteExpiry <= 0 { 87 | o.remoteExpiry = defaultRemoteExpiry 88 | } 89 | if o.notFoundExpiry <= 0 { 90 | o.notFoundExpiry = defaultNotFoundExpiry 91 | } 92 | if o.offset <= 0 { 93 | o.offset = o.notFoundExpiry / 10 94 | } 95 | if o.offset > maxOffset { 96 | o.offset = maxOffset 97 | } 98 | if o.refreshConcurrency <= 0 { 99 | o.refreshConcurrency = defaultRefreshConcurrency 100 | } 101 | if o.refreshDuration > 0 && o.refreshDuration < minEffectRefreshDuration { 102 | o.refreshDuration = minEffectRefreshDuration 103 | } 104 | if o.stopRefreshAfterLastAccess <= 0 { 105 | o.stopRefreshAfterLastAccess = o.refreshDuration + time.Second 106 | } 107 | if o.statsHandler == nil { 108 | o.statsHandler = stats.NewHandles(o.statsDisabled, stats.NewStatsLogger(o.name)) 109 | } 110 | if o.sourceID == "" { 111 | o.sourceID = util.NewSafeRand().RandN(defaultRandSourceIdLen) 112 | } 113 | if o.eventChBufSize <= 0 { 114 | o.eventChBufSize = defaultEventChBufSize 115 | } 116 | if o.separator == "" && !o.separatorDisabled { 117 | o.separator = defaultSeparator 118 | } 119 | if encoding.GetCodec(o.codec) == nil { 120 | panic(fmt.Sprintf("encoding %s is not registered, please register it first", o.codec)) 121 | } 122 | return o 123 | } 124 | 125 | func WithName(name string) Option { 126 | return func(o *Options) { 127 | o.name = name 128 | } 129 | } 130 | 131 | func WithRemote(remote remote.Remote) Option { 132 | return func(o *Options) { 133 | o.remote = remote 134 | } 135 | } 136 | 137 | func WithLocal(local local.Local) Option { 138 | return func(o *Options) { 139 | o.local = local 140 | } 141 | } 142 | 143 | func WithCodec(codec string) Option { 144 | return func(o *Options) { 145 | o.codec = codec 146 | } 147 | } 148 | 149 | func WithErrNotFound(err error) Option { 150 | return func(o *Options) { 151 | o.errNotFound = err 152 | } 153 | } 154 | 155 | func WithRemoteExpiry(remoteExpiry time.Duration) Option { 156 | return func(o *Options) { 157 | o.remoteExpiry = remoteExpiry 158 | } 159 | } 160 | 161 | func WithNotFoundExpiry(notFoundExpiry time.Duration) Option { 162 | return func(o *Options) { 163 | o.notFoundExpiry = notFoundExpiry 164 | } 165 | } 166 | 167 | func WithOffset(offset time.Duration) Option { 168 | return func(o *Options) { 169 | o.offset = offset 170 | } 171 | } 172 | 173 | func WithRefreshDuration(refreshDuration time.Duration) Option { 174 | return func(o *Options) { 175 | o.refreshDuration = refreshDuration 176 | } 177 | } 178 | 179 | func WithStopRefreshAfterLastAccess(stopRefreshAfterLastAccess time.Duration) Option { 180 | return func(o *Options) { 181 | o.stopRefreshAfterLastAccess = stopRefreshAfterLastAccess 182 | } 183 | } 184 | 185 | func WithRefreshConcurrency(refreshConcurrency int) Option { 186 | return func(o *Options) { 187 | o.refreshConcurrency = refreshConcurrency 188 | } 189 | } 190 | 191 | func WithStatsHandler(handler stats.Handler) Option { 192 | return func(o *Options) { 193 | o.statsHandler = handler 194 | } 195 | } 196 | 197 | func WithStatsDisabled(statsDisabled bool) Option { 198 | return func(o *Options) { 199 | o.statsDisabled = statsDisabled 200 | } 201 | } 202 | 203 | func WithSourceId(sourceId string) Option { 204 | return func(o *Options) { 205 | o.sourceID = sourceId 206 | } 207 | } 208 | 209 | func WithSyncLocal(syncLocal bool) Option { 210 | return func(o *Options) { 211 | o.syncLocal = syncLocal 212 | } 213 | } 214 | 215 | func WithEventChBufSize(eventChBufSize int) Option { 216 | return func(o *Options) { 217 | o.eventChBufSize = eventChBufSize 218 | } 219 | } 220 | 221 | func WithEventHandler(eventHandler func(event *Event)) Option { 222 | return func(o *Options) { 223 | o.eventHandler = eventHandler 224 | } 225 | } 226 | 227 | func WithSeparatorDisabled(separatorDisabled bool) Option { 228 | return func(o *Options) { 229 | o.separatorDisabled = separatorDisabled 230 | } 231 | } 232 | 233 | func WithSeparator(separator string) Option { 234 | return func(o *Options) { 235 | if !o.separatorDisabled { 236 | o.separator = separator 237 | } 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jetcache-go 2 | 3 | ![banner](docs/images/banner.png) 4 | 5 |

6 | Build Status 7 | codeCov 8 | Go Repport Card 9 | License 10 | Release 11 |

12 | 13 | Translate to: [简体中文](README_zh.md) 14 | 15 | # Overview 16 | 17 | [jetcache-go](https://github.com/mgtv-tech/jetcache-go) is a general-purpose cache access framework based on 18 | [go-redis/cache](https://github.com/go-redis/cache). It implements the core features of the Java version of 19 | [JetCache](https://github.com/alibaba/jetcache), including: 20 | 21 | - ✅ Flexible combination of two-level caching: You can use memory, Redis, or your own custom storage method. 22 | - ✅ The `Once` interface adopts the `singleflight` pattern, which is highly concurrent and thread-safe. 23 | - ✅ By default, [MsgPack](https://github.com/vmihailenco/msgpack) is used for encoding and decoding values. Optional [sonic](https://github.com/bytedance/sonic) and native json. 24 | - ✅ The default local cache implementation includes [Ristretto](https://github.com/dgraph-io/ristretto) and [FreeCache](https://github.com/coocood/freecache). 25 | - ✅ The default distributed cache implementation is based on [go-redis/v9](https://github.com/redis/go-redis), and you can also customize your own implementation. 26 | - ✅ You can customize the errNotFound error and use placeholders to prevent cache penetration by caching empty results. 27 | - ✅ Supports asynchronous refreshing of distributed caches. 28 | - ✅ Metrics collection: By default, it prints statistical metrics (QPM, Hit, Miss, Query, QueryFail) through logs. 29 | - ✅ Automatic degradation of distributed cache query failures. 30 | - ✅ The `MGet` interface supports the `Load` function. In a distributed caching scenario, the Pipeline mode is used to improve performance. (v1.1.0+) 31 | - ✅ Invalidate local caches (in all Go processes) after updates (v1.1.1+) 32 | 33 | # Learning jetcache-go 34 | 35 | Visit [documentation](/docs/EN/GettingStarted.md) for more details. 36 | 37 | # Installation 38 | 39 | To start using the latest version of jetcache-go, you can import the library into your project: 40 | 41 | ```shell 42 | go get github.com/mgtv-tech/jetcache-go 43 | ``` 44 | 45 | # Getting started 46 | 47 | ## Basic Usage 48 | 49 | ```go 50 | import ( 51 | "bytes" 52 | "context" 53 | "encoding/json" 54 | "errors" 55 | "fmt" 56 | "time" 57 | 58 | "github.com/mgtv-tech/jetcache-go" 59 | "github.com/mgtv-tech/jetcache-go/local" 60 | "github.com/mgtv-tech/jetcache-go/remote" 61 | "github.com/redis/go-redis/v9" 62 | ) 63 | 64 | var errRecordNotFound = errors.New("mock gorm.errRecordNotFound") 65 | 66 | type object struct { 67 | Str string 68 | Num int 69 | } 70 | 71 | func Example_basicUsage() { 72 | ring := redis.NewRing(&redis.RingOptions{ 73 | Addrs: map[string]string{ 74 | "localhost": ":6379", 75 | }, 76 | }) 77 | 78 | mycache := cache.New(cache.WithName("any"), 79 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 80 | cache.WithLocal(local.NewFreeCache(256*local.MB, time.Minute)), 81 | cache.WithErrNotFound(errRecordNotFound)) 82 | 83 | ctx := context.TODO() 84 | key := "mykey:1" 85 | obj, _ := mockDBGetObject(1) 86 | if err := mycache.Set(ctx, key, cache.Value(obj), cache.TTL(time.Hour)); err != nil { 87 | panic(err) 88 | } 89 | 90 | var wanted object 91 | if err := mycache.Get(ctx, key, &wanted); err == nil { 92 | fmt.Println(wanted) 93 | } 94 | // Output: {mystring 42} 95 | 96 | mycache.Close() 97 | } 98 | 99 | func Example_advancedUsage() { 100 | ring := redis.NewRing(&redis.RingOptions{ 101 | Addrs: map[string]string{ 102 | "localhost": ":6379", 103 | }, 104 | }) 105 | 106 | mycache := cache.New(cache.WithName("any"), 107 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 108 | cache.WithLocal(local.NewFreeCache(256*local.MB, time.Minute)), 109 | cache.WithErrNotFound(errRecordNotFound), 110 | cache.WithRefreshDuration(time.Minute)) 111 | 112 | ctx := context.TODO() 113 | key := "mykey:1" 114 | obj := new(object) 115 | if err := mycache.Once(ctx, key, cache.Value(obj), cache.TTL(time.Hour), cache.Refresh(true), 116 | cache.Do(func(ctx context.Context) (any, error) { 117 | return mockDBGetObject(1) 118 | })); err != nil { 119 | panic(err) 120 | } 121 | fmt.Println(obj) 122 | // Output: &{mystring 42} 123 | 124 | mycache.Close() 125 | } 126 | 127 | func Example_mGetUsage() { 128 | ring := redis.NewRing(&redis.RingOptions{ 129 | Addrs: map[string]string{ 130 | "localhost": ":6379", 131 | }, 132 | }) 133 | 134 | mycache := cache.New(cache.WithName("any"), 135 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 136 | cache.WithLocal(local.NewFreeCache(256*local.MB, time.Minute)), 137 | cache.WithErrNotFound(errRecordNotFound), 138 | cache.WithRemoteExpiry(time.Minute), 139 | ) 140 | cacheT := cache.NewT[int, *object](mycache) 141 | 142 | ctx := context.TODO() 143 | key := "mget" 144 | ids := []int{1, 2, 3} 145 | 146 | ret := cacheT.MGet(ctx, key, ids, func(ctx context.Context, ids []int) (map[int]*object, error) { 147 | return mockDBMGetObject(ids) 148 | }) 149 | 150 | var b bytes.Buffer 151 | for _, id := range ids { 152 | b.WriteString(fmt.Sprintf("%v", ret[id])) 153 | } 154 | fmt.Println(b.String()) 155 | // Output: &{mystring 1}&{mystring 2} 156 | 157 | cacheT.Close() 158 | } 159 | 160 | func Example_syncLocalUsage() { 161 | ring := redis.NewRing(&redis.RingOptions{ 162 | Addrs: map[string]string{ 163 | "localhost": ":6379", 164 | }, 165 | }) 166 | 167 | sourceID := "12345678" // Unique identifier for this cache instance 168 | channelName := "syncLocalChannel" 169 | pubSub := ring.Subscribe(context.Background(), channelName) 170 | 171 | mycache := cache.New(cache.WithName("any"), 172 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 173 | cache.WithLocal(local.NewFreeCache(256*local.MB, time.Minute)), 174 | cache.WithErrNotFound(errRecordNotFound), 175 | cache.WithRemoteExpiry(time.Minute), 176 | cache.WithSourceId(sourceID), 177 | cache.WithSyncLocal(true), 178 | cache.WithEventHandler(func(event *cache.Event) { 179 | // Broadcast local cache invalidation for the received keys 180 | bs, _ := json.Marshal(event) 181 | ring.Publish(context.Background(), channelName, string(bs)) 182 | }), 183 | ) 184 | obj, _ := mockDBGetObject(1) 185 | if err := mycache.Set(context.TODO(), "mykey", cache.Value(obj), cache.TTL(time.Hour)); err != nil { 186 | panic(err) 187 | } 188 | 189 | go func() { 190 | for { 191 | msg := <-pubSub.Channel() 192 | var event *cache.Event 193 | if err := json.Unmarshal([]byte(msg.Payload), &event); err != nil { 194 | panic(err) 195 | } 196 | fmt.Println(event.Keys) 197 | 198 | // Invalidate local cache for received keys (except own events) 199 | if event.SourceID != sourceID { 200 | for _, key := range event.Keys { 201 | mycache.DeleteFromLocalCache(key) 202 | } 203 | } 204 | } 205 | }() 206 | 207 | // Output: [mykey] 208 | mycache.Close() 209 | time.Sleep(time.Second) 210 | } 211 | 212 | func mockDBGetObject(id int) (*object, error) { 213 | if id > 100 { 214 | return nil, errRecordNotFound 215 | } 216 | return &object{Str: "mystring", Num: 42}, nil 217 | } 218 | 219 | func mockDBMGetObject(ids []int) (map[int]*object, error) { 220 | ret := make(map[int]*object) 221 | for _, id := range ids { 222 | if id == 3 { 223 | continue 224 | } 225 | ret[id] = &object{Str: "mystring", Num: id} 226 | } 227 | return ret, nil 228 | } 229 | ``` 230 | 231 | # Contributing 232 | 233 | Everyone is welcome to help improve jetcache-go. If you have any questions, suggestions, or want to add other features, please submit an issue or PR directly. 234 | 235 | Please follow these steps to submit a PR: 236 | 237 | - Clone the repository 238 | - Create a new branch: name it feature-xxx for new features or bug-xxx for bug fixes 239 | - Describe the changes in detail in the PR 240 | 241 | 242 | # Contact 243 | 244 | If you have any questions, please contact `daoshenzzg@gmail.com`. 245 | -------------------------------------------------------------------------------- /docs/EN/GettingStarted.md: -------------------------------------------------------------------------------- 1 | ![banner](/docs/images/banner.png) 2 | 3 | 4 | * [Overview](#overview) 5 | * [Product Comparison](#product-comparison) 6 | * [Learning jetcache-go](#learning-jetcache-go) 7 | * [Installation](#installation) 8 | * [Quick started](#quick-started) 9 | 10 | 11 | # Overview 12 | 13 | `jetcache-go` is a general-purpose caching framework built upon and extending [go-redis/cache](https://github.com/go-redis/cache). It implements core features similar to the Java version of [JetCache](https://github.com/alibaba/jetcache), including: 14 | 15 | - ✅ **Flexible Two-Level Caching:** Supports local cache, distributed cache, and a combination of both. 16 | - ✅ **`Once` Interface with Singleflight:** High concurrency and thread safety using the singleflight pattern. 17 | - ✅ **Multiple Encoding Options:** Defaults to [MsgPack](https://github.com/vmihailenco/msgpack) for value encoding/decoding. [sonic](https://github.com/bytedance/sonic) and native `json` are also supported. 18 | - ✅ **Built-in Local Cache Implementations:** Provides implementations using [Ristretto](https://github.com/dgraph-io/ristretto) and [FreeCache](https://github.com/coocood/freecache). 19 | - ✅ **Distributed Cache Adapter:** Defaults to an adapter for [go-redis/v9](https://github.com/redis/go-redis), but custom implementations are also supported. 20 | - ✅ **`errNotFound` Customization:** Prevents cache penetration by caching null results using a placeholder. 21 | - ✅ **Asynchronous Distributed Cache Refresh:** Supports enabling asynchronous refresh of distributed caches. 22 | - ✅ **Metrics Collection:** Provides default logging of cache statistics (QPM, Hit, Miss, Query, QueryFail). 23 | - ✅ **Automatic Distributed Cache Query Degradation:** Handles failures gracefully. 24 | - ✅ **`MGet` Interface with `Load` Function:** Supports pipeline mode for distributed cache scenarios (v1.1.0+). 25 | - ✅ **Support for Invalidating Local Caches Across All Go Processes:** After cache updates (v1.1.1+). 26 | 27 | 28 | # Product Comparison 29 | 30 | | Feature | eko/gocache | go-redis/cache | mgtv-tech/jetcache-go | 31 | |-----------------------|-------------|----------------|-----------------------| 32 | | Multi-level Caching | Yes | Yes | Yes | 33 | | Loadable Caching | Yes | Yes | Yes | 34 | | Generics Support | Yes | No | Yes | 35 | | Singleflight Pattern | Yes | Yes | Yes | 36 | | Cache Update Listener | No | No | Yes | 37 | | Auto Refresh | No | No | Yes | 38 | | Metrics Collection | Yes | Yes (simple) | Yes | 39 | | Null Object Caching | No | No | Yes | 40 | | Bulk Query | No | No | Yes | 41 | | Sparse List Cache | No | No | Yes | 42 | 43 | # Learning jetcache-go 44 | - GettingStarted 45 | - [Cache API](/docs/EN/CacheAPI.md) 46 | - [Config](/docs/EN/Config.md) 47 | - [Embedded](/docs/EN/Embedded.md) 48 | - [Metrics](/docs/EN/Stat.md) 49 | - [Plugin](/docs/EN/Plugin.md) 50 | 51 | # Installation 52 | 53 | To use the latest version of `jetcache-go`, import the library into your project: 54 | 55 | ```shell 56 | go get github.com/mgtv-tech/jetcache-go 57 | ``` 58 | 59 | # Quick started 60 | 61 | ```go 62 | import ( 63 | "bytes" 64 | "context" 65 | "encoding/json" 66 | "errors" 67 | "fmt" 68 | "time" 69 | 70 | "github.com/mgtv-tech/jetcache-go" 71 | "github.com/mgtv-tech/jetcache-go/local" 72 | "github.com/mgtv-tech/jetcache-go/remote" 73 | "github.com/redis/go-redis/v9" 74 | ) 75 | 76 | var errRecordNotFound = errors.New("mock gorm.ErrRecordNotFound") 77 | 78 | type object struct { 79 | Str string 80 | Num int 81 | } 82 | 83 | func Example_basicUsage() { 84 | ring := redis.NewRing(&redis.RingOptions{ 85 | Addrs: map[string]string{ 86 | "localhost": ":6379", 87 | }, 88 | }) 89 | 90 | mycache := cache.New(cache.WithName("any"), 91 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 92 | cache.WithLocal(local.NewFreeCache(256*local.MB, time.Minute)), 93 | cache.WithErrNotFound(errRecordNotFound)) 94 | 95 | ctx := context.TODO() 96 | key := "mykey:1" 97 | obj, _ := mockDBGetObject(1) 98 | if err := mycache.Set(ctx, key, cache.Value(obj), cache.TTL(time.Hour)); err != nil { 99 | panic(err) 100 | } 101 | 102 | var wanted object 103 | if err := mycache.Get(ctx, key, &wanted); err == nil { 104 | fmt.Println(wanted) 105 | } 106 | // Output: {mystring 42} 107 | 108 | mycache.Close() 109 | } 110 | 111 | func Example_advancedUsage() { 112 | ring := redis.NewRing(&redis.RingOptions{ 113 | Addrs: map[string]string{ 114 | "localhost": ":6379", 115 | }, 116 | }) 117 | 118 | mycache := cache.New(cache.WithName("any"), 119 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 120 | cache.WithLocal(local.NewFreeCache(256*local.MB, time.Minute)), 121 | cache.WithErrNotFound(errRecordNotFound), 122 | cache.WithRefreshDuration(time.Minute)) 123 | 124 | ctx := context.TODO() 125 | key := "mykey:1" 126 | obj := new(object) 127 | if err := mycache.Once(ctx, key, cache.Value(obj), cache.TTL(time.Hour), cache.Refresh(true), 128 | cache.Do(func(ctx context.Context) (any, error) { 129 | return mockDBGetObject(1) 130 | })); err != nil { 131 | panic(err) 132 | } 133 | fmt.Println(obj) 134 | // Output: &{mystring 42} 135 | 136 | mycache.Close() 137 | } 138 | 139 | func Example_mGetUsage() { 140 | ring := redis.NewRing(&redis.RingOptions{ 141 | Addrs: map[string]string{ 142 | "localhost": ":6379", 143 | }, 144 | }) 145 | 146 | mycache := cache.New(cache.WithName("any"), 147 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 148 | cache.WithLocal(local.NewFreeCache(256*local.MB, time.Minute)), 149 | cache.WithErrNotFound(errRecordNotFound), 150 | cache.WithRemoteExpiry(time.Minute), 151 | ) 152 | cacheT := cache.NewT[int, *object](mycache) 153 | 154 | ctx := context.TODO() 155 | key := "mget" 156 | ids := []int{1, 2, 3} 157 | 158 | ret := cacheT.MGet(ctx, key, ids, func(ctx context.Context, ids []int) (map[int]*object, error) { 159 | return mockDBMGetObject(ids) 160 | }) 161 | 162 | var b bytes.Buffer 163 | for _, id := range ids { 164 | b.WriteString(fmt.Sprintf("%v", ret[id])) 165 | } 166 | fmt.Println(b.String()) 167 | // Output: &{mystring 1}&{mystring 2} 168 | 169 | cacheT.Close() 170 | } 171 | 172 | func Example_syncLocalUsage() { 173 | ring := redis.NewRing(&redis.RingOptions{ 174 | Addrs: map[string]string{ 175 | "localhost": ":6379", 176 | }, 177 | }) 178 | 179 | sourceID := "12345678" // Unique identifier for this cache instance 180 | channelName := "syncLocalChannel" 181 | pubSub := ring.Subscribe(context.Background(), channelName) 182 | 183 | mycache := cache.New(cache.WithName("any"), 184 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 185 | cache.WithLocal(local.NewFreeCache(256*local.MB, time.Minute)), 186 | cache.WithErrNotFound(errRecordNotFound), 187 | cache.WithRemoteExpiry(time.Minute), 188 | cache.WithSourceId(sourceID), 189 | cache.WithSyncLocal(true), 190 | cache.WithEventHandler(func(event *cache.Event) { 191 | // Broadcast local cache invalidation for the received keys 192 | bs, _ := json.Marshal(event) 193 | ring.Publish(context.Background(), channelName, string(bs)) 194 | }), 195 | ) 196 | obj, _ := mockDBGetObject(1) 197 | if err := mycache.Set(context.TODO(), "mykey", cache.Value(obj), cache.TTL(time.Hour)); err != nil { 198 | panic(err) 199 | } 200 | 201 | go func() { 202 | for { 203 | msg := <-pubSub.Channel() 204 | var event *cache.Event 205 | if err := json.Unmarshal([]byte(msg.Payload), &event); err != nil { 206 | panic(err) 207 | } 208 | fmt.Println(event.Keys) 209 | 210 | // Invalidate local cache for received keys (except own events) 211 | if event.SourceID != sourceID { 212 | for _, key := range event.Keys { 213 | mycache.DeleteFromLocalCache(key) 214 | } 215 | } 216 | } 217 | }() 218 | 219 | // Output: [mykey] 220 | mycache.Close() 221 | time.Sleep(time.Second) 222 | } 223 | 224 | func mockDBGetObject(id int) (*object, error) { 225 | if id > 100 { 226 | return nil, errRecordNotFound 227 | } 228 | return &object{Str: "mystring", Num: 42}, nil 229 | } 230 | 231 | func mockDBMGetObject(ids []int) (map[int]*object, error) { 232 | ret := make(map[int]*object) 233 | for _, id := range ids { 234 | if id == 3 { 235 | continue 236 | } 237 | ret[id] = &object{Str: "mystring", Num: id} 238 | } 239 | return ret, nil 240 | } 241 | ``` 242 | -------------------------------------------------------------------------------- /cachegeneric.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "errors" 7 | "fmt" 8 | "sort" 9 | 10 | "golang.org/x/exp/constraints" 11 | 12 | "github.com/mgtv-tech/jetcache-go/logger" 13 | "github.com/mgtv-tech/jetcache-go/util" 14 | ) 15 | 16 | // T wrap Cache to support golang's generics 17 | type T[K constraints.Ordered, V any] struct { 18 | Cache 19 | } 20 | 21 | // NewT new a T 22 | func NewT[K constraints.Ordered, V any](cache Cache) *T[K, V] { 23 | return &T[K, V]{cache} 24 | } 25 | 26 | // Set sets the value `v` associated with the given `key` and `id` in the cache. 27 | // The expiration time of the cached value is determined by the cache configuration. 28 | func (w *T[K, V]) Set(ctx context.Context, key string, id K, v V) error { 29 | c := w.Cache.(*jetCache) 30 | return w.Cache.Set(ctx, w.combKey(c, key, id), Value(v)) 31 | } 32 | 33 | // Get retrieves the value associated with the given `key` and `id`. 34 | // 35 | // It first attempts to fetch the value from the cache. If a cache miss occurs, it calls the provided 36 | // `fn` function to fetch the value and stores it in the cache with an expiration time 37 | // determined by the cache configuration. 38 | // 39 | // A `Once` mechanism is employed to ensure only one fetch is performed for a given `key` and `id` 40 | // combination, even under concurrent access. 41 | func (w *T[K, V]) Get(ctx context.Context, key string, id K, fn func(context.Context, K) (V, error)) (V, error) { 42 | c := w.Cache.(*jetCache) 43 | 44 | var varT V 45 | err := w.Once(ctx, w.combKey(c, key, id), Value(&varT), Do(func(ctx context.Context) (any, error) { 46 | return fn(ctx, id) 47 | })) 48 | 49 | return varT, err 50 | } 51 | 52 | // MGet efficiently retrieves multiple values associated with the given `key` and `ids`. 53 | // It is a wrapper around MGetWithErr that logs any errors and returns only the results. 54 | func (w *T[K, V]) MGet(ctx context.Context, key string, ids []K, fn func(context.Context, []K) (map[K]V, error)) (result map[K]V) { 55 | var err error 56 | if result, err = w.MGetWithErr(ctx, key, ids, fn); err != nil { 57 | logger.Warn("MGet error(%v)", err) 58 | } 59 | 60 | return 61 | } 62 | 63 | // MGetWithErr efficiently retrieves multiple values associated with the given `key` and `ids`, 64 | // returning both the results and any errors encountered during the process. 65 | // 66 | // It first attempts to retrieve values from the local cache (if enabled), then from the remote cache (if enabled). 67 | // For any values not found in the caches, it calls the provided `fn` function to fetch them from the 68 | // underlying data source. The fetched values are then stored in both the local and remote caches for 69 | // future use. 70 | // 71 | // The results are returned as a map where the key is the `id` and the value is the corresponding data. 72 | // Any errors encountered during the cache retrieval or data fetching process are returned as a non-nil error. 73 | func (w *T[K, V]) MGetWithErr(ctx context.Context, key string, ids []K, fn func(context.Context, []K) (map[K]V, error)) (result map[K]V, errs error) { 74 | c := w.Cache.(*jetCache) 75 | 76 | miss := make(map[string]K, len(ids)) 77 | for _, missId := range ids { 78 | miss[w.combKey(c, key, missId)] = missId 79 | } 80 | 81 | if c.local != nil { 82 | result, errs = w.mGetLocal(miss, true) 83 | if len(miss) == 0 { 84 | return 85 | } 86 | } 87 | 88 | if c.remote == nil && fn == nil { 89 | return 90 | } 91 | 92 | missIds := make([]K, 0, len(miss)) 93 | for _, missId := range miss { 94 | missIds = append(missIds, missId) 95 | } 96 | 97 | sort.Slice(missIds, func(i, j int) bool { 98 | return missIds[i] < missIds[j] 99 | }) 100 | 101 | combKey := fmt.Sprintf("%s%s%v", key, c.separator, missIds) 102 | v, err, _ := c.group.Do(combKey, func() (interface{}, error) { 103 | var ret map[K]V 104 | 105 | process := func(r map[K]V, e error) { 106 | errs = errors.Join(errs, e) 107 | ret = util.MergeMap(ret, r) 108 | } 109 | 110 | if c.local != nil { 111 | process(w.mGetLocal(miss, false)) 112 | if len(miss) == 0 { 113 | return ret, nil 114 | } 115 | } 116 | 117 | if c.remote != nil { 118 | process(w.mGetRemote(ctx, miss)) 119 | if len(miss) == 0 { 120 | return ret, nil 121 | } 122 | } 123 | 124 | if fn != nil { 125 | process(w.mQueryAndSetCache(ctx, miss, fn)) 126 | } 127 | 128 | return ret, nil 129 | }) 130 | 131 | if err != nil { 132 | errs = errors.Join(errs, err) 133 | return 134 | } 135 | 136 | return util.MergeMap(result, v.(map[K]V)), errs 137 | } 138 | 139 | func (w *T[K, V]) mGetLocal(miss map[string]K, skipMissStats bool) (result map[K]V, errs error) { 140 | c := w.Cache.(*jetCache) 141 | 142 | result = make(map[K]V, len(miss)) 143 | for missKey, missId := range miss { 144 | if b, ok := c.local.Get(missKey); ok { 145 | delete(miss, missKey) 146 | c.statsHandler.IncrHit() 147 | c.statsHandler.IncrLocalHit() 148 | if bytes.Compare(b, notFoundPlaceholder) == 0 { 149 | continue 150 | } 151 | var varT V 152 | if err := c.Unmarshal(b, &varT); err != nil { 153 | errs = errors.Join(errs, fmt.Errorf("mGetLocal#c.Unmarshal(%s) error(%v)", missKey, err)) 154 | } else { 155 | result[missId] = varT 156 | } 157 | } else if !skipMissStats { 158 | c.statsHandler.IncrLocalMiss() 159 | if c.remote == nil { 160 | c.statsHandler.IncrMiss() 161 | } 162 | } 163 | } 164 | 165 | return 166 | } 167 | 168 | func (w *T[K, V]) mGetRemote(ctx context.Context, miss map[string]K) (result map[K]V, errs error) { 169 | c := w.Cache.(*jetCache) 170 | 171 | missKeys := make([]string, 0, len(miss)) 172 | for missKey := range miss { 173 | missKeys = append(missKeys, missKey) 174 | } 175 | 176 | cacheValues, err := c.remote.MGet(ctx, missKeys...) 177 | if err != nil { 178 | errs = errors.Join(errs, fmt.Errorf("mGetRemote#c.Remote.MGet error(%v)", err)) 179 | return 180 | } 181 | 182 | result = make(map[K]V, len(cacheValues)) 183 | for missKey, missId := range miss { 184 | if val, ok := cacheValues[missKey]; ok { 185 | delete(miss, missKey) 186 | c.statsHandler.IncrHit() 187 | c.statsHandler.IncrRemoteHit() 188 | b := util.Bytes(val.(string)) 189 | if bytes.Compare(b, notFoundPlaceholder) == 0 { 190 | continue 191 | } 192 | var varT V 193 | if err = c.Unmarshal(b, &varT); err != nil { 194 | errs = errors.Join(errs, fmt.Errorf("mGetRemote#c.Unmarshal(%s) error(%v)", missKey, err)) 195 | } else { 196 | result[missId] = varT 197 | if c.local != nil { 198 | c.local.Set(missKey, b) 199 | } 200 | } 201 | } else { 202 | c.statsHandler.IncrMiss() 203 | c.statsHandler.IncrRemoteMiss() 204 | } 205 | } 206 | 207 | return 208 | } 209 | 210 | func (w *T[K, V]) mQueryAndSetCache(ctx context.Context, miss map[string]K, fn func(context.Context, []K) (map[K]V, error)) (result map[K]V, errs error) { 211 | c := w.Cache.(*jetCache) 212 | 213 | missIds := make([]K, 0, len(miss)) 214 | for _, missId := range miss { 215 | missIds = append(missIds, missId) 216 | } 217 | 218 | c.statsHandler.IncrQuery() 219 | fnValues, err := fn(ctx, missIds) 220 | if err != nil { 221 | errs = errors.Join(errs, fmt.Errorf("mQueryAndSetCache#fn(%v) error(%v)", missIds, err)) 222 | c.statsHandler.IncrQueryFail(err) 223 | return 224 | } 225 | 226 | result = make(map[K]V, len(fnValues)) 227 | cacheValues := make(map[string]any, len(miss)) 228 | placeholderValues := make(map[string]any, len(miss)) 229 | for missKey, missId := range miss { 230 | if val, ok := fnValues[missId]; ok { 231 | result[missId] = val 232 | if b, err := c.Marshal(val); err != nil { 233 | placeholderValues[missKey] = notFoundPlaceholder 234 | errs = errors.Join(errs, fmt.Errorf("mQueryAndSetCache#c.Marshal error(%v)", err)) 235 | } else { 236 | cacheValues[missKey] = b 237 | } 238 | } else { 239 | placeholderValues[missKey] = notFoundPlaceholder 240 | } 241 | } 242 | 243 | if c.local != nil { 244 | if len(cacheValues) > 0 { 245 | for key, value := range cacheValues { 246 | c.local.Set(key, value.([]byte)) 247 | } 248 | } 249 | if len(placeholderValues) > 0 { 250 | for key, value := range placeholderValues { 251 | c.local.Set(key, value.([]byte)) 252 | } 253 | } 254 | } 255 | 256 | if c.remote != nil { 257 | if len(cacheValues) > 0 { 258 | if err = c.remote.MSet(ctx, cacheValues, c.remoteExpiry); err != nil { 259 | errs = errors.Join(errs, fmt.Errorf("mQueryAndSetCache#c.Remote.MSet error(%v)", err)) 260 | } 261 | } 262 | if len(placeholderValues) > 0 { 263 | if err = c.remote.MSet(ctx, placeholderValues, c.notFoundExpiry); err != nil { 264 | errs = errors.Join(errs, fmt.Errorf("mQueryAndSetCache#c.Remote.MSet error(%v)", err)) 265 | } 266 | } 267 | if c.isSyncLocal() { 268 | cacheKeys := make([]string, 0, len(miss)) 269 | for missKey := range miss { 270 | cacheKeys = append(cacheKeys, missKey) 271 | } 272 | c.send(EventTypeSetByMGet, cacheKeys...) 273 | } 274 | } 275 | 276 | return 277 | } 278 | 279 | // Delete deletes cached val with the given `key` and `id`. 280 | func (w *T[K, V]) Delete(ctx context.Context, key string, id K) error { 281 | c := w.Cache.(*jetCache) 282 | return c.Delete(ctx, w.combKey(c, key, id)) 283 | } 284 | 285 | // Exists reports whether val for the given `key` and `id` exists. 286 | func (w *T[K, V]) Exists(ctx context.Context, key string, id K) bool { 287 | c := w.Cache.(*jetCache) 288 | return c.Exists(ctx, w.combKey(c, key, id)) 289 | } 290 | 291 | func (w *T[K, V]) combKey(c *jetCache, key string, id K) string { 292 | return fmt.Sprintf("%s%s%v", key, c.separator, id) 293 | } 294 | -------------------------------------------------------------------------------- /docs/CN/Config.md: -------------------------------------------------------------------------------- 1 | 2 | * [Cache 配置项说明](#cache-配置项说明) 3 | * [Cache 缓存实例创建](#cache-缓存实例创建) 4 | * [示例1:创建二级缓存实例(Both)](#示例1创建二级缓存实例both) 5 | * [示例2:创建仅本地缓存实例(Local)](#示例2创建仅本地缓存实例local) 6 | * [示例3:创建仅远程缓存实例(Remote)](#示例3创建仅远程缓存实例remote) 7 | * [示例4:创建缓存实例,并配置jetcache-go-plugin Prometheus 统计插件](#示例4创建缓存实例并配置jetcache-go-plugin-prometheus-统计插件) 8 | * [示例5:创建缓存实例,并配置 `errNotFound` 防止缓存穿透](#示例5创建缓存实例并配置-errnotfound-防止缓存穿透) 9 | 10 | 11 | # Cache 配置项说明 12 | 13 | | 配置项名称 | 配置项类型 | 缺省值 | 说明 | 14 | |----------------------------|----------------------|----------------------|---------------------------------------------------------------------------------------------------------------------------------------------------| 15 | | name | string | default | 缓存名称,用于日志标识和指标报告 | 16 | | remote | `remote.Remote` 接口 | nil | remote 是分布式缓存,例如 Redis。也可以自定义,实现`remote.Remote`接口即可 | 17 | | local | `local.Local` 接口 | nil | local 是内存缓存,例如 FreeCache、TinyLFU。也可以自定义,实现`local.Local`接口即可 | 18 | | codec | string | msgpack | value的编码和解码方法。默认为 "msgpack"。可选:`json` \| `msgpack` \| `sonic`,也可以自定义,实现`encoding.Codec`接口并注册即可 | 19 | | errNotFound | error | nil | 回源记录不存在时返回的错误,例:`gorm.ErrRecordNotFound`。用于防止缓存穿透(即缓存空对象) | 20 | | remoteExpiry | `time.Duration` | 1小时 | 远程缓存 TTL,默认为 1 小时 | 21 | | notFoundExpiry | `time.Duration` | 1分钟 | 缓存未命中时占位符缓存的过期时间。默认为 1 分钟 | 22 | | offset | `time.Duration` | (0,10]秒 | 缓存未命中时的过期时间抖动因子 | 23 | | refreshDuration | `time.Duration` | 0 | 异步缓存刷新的间隔。默认为 0(禁用刷新) | 24 | | stopRefreshAfterLastAccess | `time.Duration` | refreshDuration + 1秒 | 缓存停止刷新之前的持续时间(上次访问后) | 25 | | refreshConcurrency | int | 4 | 刷新缓存任务池的并发刷新的最大数量 | 26 | | statsDisabled | bool | false | 禁用缓存统计的标志 | 27 | | statsHandler | `stats.Handler` 接口 | stats.NewStatsLogger | 指标统计收集器。默认内嵌实现了`log`统计,也可以使用[jetcache-go-plugin](https://github.com/mgtv-tech/jetcache-go-plugin) 的`Prometheus` 插件。或自定义实现,只要实现`stats.Handler`接口即可 | 28 | | sourceID | string | 16位随机字符串 | 【缓存事件广播】缓存实例的唯一标识符 | 29 | | syncLocal | bool | false | 【缓存事件广播】启用同步本地缓存的事件(仅适用于 "Both" 缓存类型) | 30 | | eventChBufSize | int | 100 | 【缓存事件广播】事件通道的缓冲区大小(默认为 100) | 31 | | eventHandler | `func(event *Event)` | nil | 【缓存事件广播】处理本地缓存失效事件的函数 | 32 | | separatorDisabled | bool | false | 禁用缓存键的分隔符。默认为false。如果为true,则缓存键不会使用分隔符。目前主要用于泛型接口的缓存key和ID拼接 | 33 | | separator | string | : | 缓存键的分隔符。默认为 ":"。目前主要用于泛型接口的缓存key和ID拼接 | 34 | 35 | # Cache 缓存实例创建 36 | 37 | ## 示例1:创建二级缓存实例(Both) 38 | 39 | ```go 40 | import ( 41 | "context" 42 | "time" 43 | 44 | "github.com/jinzhu/gorm" 45 | "github.com/mgtv-tech/jetcache-go" 46 | "github.com/mgtv-tech/jetcache-go/local" 47 | "github.com/mgtv-tech/jetcache-go/remote" 48 | "github.com/redis/go-redis/v9" 49 | ) 50 | ring := redis.NewRing(&redis.RingOptions{ 51 | Addrs: map[string]string{ 52 | "localhost": ":6379", 53 | }, 54 | }) 55 | 56 | // 创建二级缓存实例 57 | mycache := cache.New(cache.WithName("any"), 58 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 59 | cache.WithLocal(local.NewTinyLFU(10000, time.Minute)), // 本地缓存过期时间统一为 1 分钟 60 | cache.WithErrNotFound(gorm.ErrRecordNotFound)) 61 | 62 | obj := struct { 63 | Name string 64 | Age int 65 | }{Name: "John Doe", Age: 30} 66 | // 设置缓存,其中远程缓存过期时间 TTL 为 1 小时 67 | err := mycache.Set(context.Background(), "mykey", cache.Value(&obj), cache.TTL(time.Hour)) 68 | if err != nil { 69 | // 错误处理 70 | } 71 | ``` 72 | 73 | ## 示例2:创建仅本地缓存实例(Local) 74 | 75 | ```go 76 | import ( 77 | "context" 78 | "time" 79 | 80 | "github.com/jinzhu/gorm" 81 | "github.com/mgtv-tech/jetcache-go" 82 | "github.com/mgtv-tech/jetcache-go/local" 83 | ) 84 | ring := redis.NewRing(&redis.RingOptions{ 85 | Addrs: map[string]string{ 86 | "localhost": ":6379", 87 | }, 88 | }) 89 | 90 | // 创建仅本地缓存实例 91 | mycache := cache.New(cache.WithName("any"), 92 | cache.WithLocal(local.NewTinyLFU(10000, time.Minute)), 93 | cache.WithErrNotFound(gorm.ErrRecordNotFound)) 94 | 95 | obj := struct { 96 | Name string 97 | Age int 98 | }{Name: "John Doe", Age: 30} 99 | 100 | err := mycache.Set(context.Background(), "mykey", cache.Value(&obj)) 101 | if err != nil { 102 | // 错误处理 103 | } 104 | ``` 105 | 106 | ## 示例3:创建仅远程缓存实例(Remote) 107 | 108 | ```go 109 | import ( 110 | "context" 111 | "time" 112 | 113 | "github.com/jinzhu/gorm" 114 | "github.com/mgtv-tech/jetcache-go" 115 | "github.com/mgtv-tech/jetcache-go/remote" 116 | "github.com/redis/go-redis/v9" 117 | ) 118 | ring := redis.NewRing(&redis.RingOptions{ 119 | Addrs: map[string]string{ 120 | "localhost": ":6379", 121 | }, 122 | }) 123 | 124 | // 创建仅远程缓存实例 125 | mycache := cache.New(cache.WithName("any"), 126 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 127 | cache.WithErrNotFound(gorm.ErrRecordNotFound)) 128 | 129 | obj := struct { 130 | Name string 131 | Age int 132 | }{Name: "John Doe", Age: 30} 133 | 134 | err := mycache.Set(context.Background(), "mykey", cache.Value(&obj), cache.TTL(time.Hour)) 135 | if err != nil { 136 | // 错误处理 137 | } 138 | ``` 139 | 140 | ## 示例4:创建缓存实例,并配置[jetcache-go-plugin](https://github.com/mgtv-tech/jetcache-go-plugin) Prometheus 统计插件 141 | 142 | ```go 143 | import ( 144 | "context" 145 | "time" 146 | 147 | "github.com/mgtv-tech/jetcache-go" 148 | "github.com/mgtv-tech/jetcache-go/remote" 149 | "github.com/redis/go-redis/v9" 150 | pstats "github.com/mgtv-tech/jetcache-go-plugin/stats" 151 | "github.com/mgtv-tech/jetcache-go/stats" 152 | ) 153 | 154 | mycache := cache.New(cache.WithName("any"), 155 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 156 | cache.WithStatsHandler( 157 | stats.NewHandles(false, 158 | stats.NewStatsLogger(cacheName), 159 | pstats.NewPrometheus(cacheName)))) 160 | 161 | obj := struct { 162 | Name string 163 | Age int 164 | }{Name: "John Doe", Age: 30} 165 | 166 | err := mycache.Set(context.Background(), "mykey", cache.Value(&obj), cache.TTL(time.Hour)) 167 | if err != nil { 168 | // 错误处理 169 | } 170 | ``` 171 | > 示例4 同时集成了 `Log` 和 `Prometheus` 统计。效果见:[Stat](/docs/CN/Stat.md) 172 | 173 | ## 示例5:创建缓存实例,并配置 `errNotFound` 防止缓存穿透 174 | 175 | ```go 176 | import ( 177 | "context" 178 | "fmt" 179 | "time" 180 | 181 | "github.com/jinzhu/gorm" 182 | "github.com/mgtv-tech/jetcache-go" 183 | ) 184 | ring := redis.NewRing(&redis.RingOptions{ 185 | Addrs: map[string]string{ 186 | "localhost": ":6379", 187 | }, 188 | }) 189 | 190 | // 创建缓存实例,并配置 errNotFound 防止缓存穿透 191 | mycache := cache.New(cache.WithName("any"), 192 | // ... 193 | cache.WithErrNotFound(gorm.ErrRecordNotFound)) 194 | 195 | var value string 196 | err := mycache.Once(ctx, key, Value(&value), Do(func(context.Context) (any, error) { 197 | return nil, gorm.ErrRecordNotFound 198 | })) 199 | fmt.Println(err) 200 | 201 | // Output: record not found 202 | ``` 203 | 204 | `jetcache-go` 采取轻量级的 \[缓存空对象\] 方式来解决缓存穿透问题: 205 | 206 | - 创建cache实例时,指定未找到错误。例如:gorm.ErrRecordNotFound、redis.Nil 207 | - 查询如果遇到未找到错误,直接用*号作为缓存值缓存 208 | - 返回的时候,判断缓存值是否为*号,如果是,则返回对应的未找到错误 209 | -------------------------------------------------------------------------------- /docs/EN/CacheAPI.md: -------------------------------------------------------------------------------- 1 | 2 | * [Cache Interface](#cache-interface) 3 | * [Set Interface](#set-interface) 4 | * [Once Interface](#once-interface) 5 | * [Generic Interfaces](#generic-interfaces) 6 | * [MGet Bulk Query](#mget-bulk-query) 7 | 8 | 9 | 10 | # Cache Interface 11 | 12 | The following interface, provided by `Cache`, is largely consistent with [go-redis/cache](https://github.com/go-redis/cache). However, some interfaces offer enhanced capabilities. 13 | 14 | ```go 15 | // Set sets cache using ItemOption. 16 | func Set(ctx context.Context, key string, opts ...ItemOption) error 17 | 18 | // Once retrieves cache using ItemOption. Single-flight mode; automatic cache refresh can be enabled. 19 | func Once(ctx context.Context, key string, opts ...ItemOption) error 20 | 21 | // Delete deletes cache. 22 | func Delete(ctx context.Context, key string) error 23 | 24 | // DeleteFromLocalCache deletes the local cache. 25 | func DeleteFromLocalCache(key string) 26 | 27 | // Exists checks if cache exists. 28 | func Exists(ctx context.Context, key string) bool 29 | 30 | // Get retrieves cache and serializes the result to `val`. 31 | func Get(ctx context.Context, key string, val any) error 32 | 33 | // GetSkippingLocal retrieves remote cache (skipping local cache). 34 | func GetSkippingLocal(ctx context.Context, key string, val any) error 35 | 36 | // TaskSize returns the number of cache auto-refresh tasks (for this instance and process). 37 | func TaskSize() int 38 | 39 | // CacheType returns the cache type. Options are `Both`, `Remote`, and `Local`. 40 | func CacheType() string 41 | 42 | // Close closes cache resources. This should be called when automatic cache refresh is enabled and is no longer needed. 43 | func Close() 44 | ``` 45 | 46 | ## Set Interface 47 | 48 | This interface is used to set cache entries. It supports various options, such as setting the value (`Value`), remote expiration time (`TTL`), a fetch function (`Do`), and atomic operations for `Remote` caches. 49 | 50 | Function Signature: 51 | 52 | ```go 53 | func Set(ctx context.Context, key string, opts ...ItemOption) error 54 | ``` 55 | 56 | Parameters: 57 | 58 | - `ctx`: `context.Context`, the request context. Used for cancellation or timeout settings. 59 | - `key`: `string`, the cache key. 60 | - `opts`: `...ItemOption`, a variadic parameter list for configuring various options of the cache item. The following options are supported: 61 | - `Value(value any)`: Sets the cache value. 62 | - `TTL(duration time.Duration)`: Sets the expiration time for the remote cache item. (Local cache expiration time is uniformly set when building the Local cache instance.) 63 | - `Do(fn func(context.Context) (any, error))`: Uses the given fetch function `fn` to retrieve the value; this takes precedence over `Value`. 64 | - `SetNX(flag bool)`: Sets the cache item only if the key does not exist. Applicable to remote caches to prevent overwriting existing values. 65 | - `SetXX(flag bool)`: Sets the cache item only if the key exists. Applicable to remote caches to ensure only existing values are updated. 66 | 67 | Return Value: 68 | 69 | - `error`: Returns an error if setting the cache fails. 70 | 71 | Example 1: Setting a cache value using `Value` 72 | 73 | ```go 74 | obj := struct { 75 | Name string 76 | Age int 77 | }{Name: "John Doe", Age: 30} 78 | 79 | err := cache.Set(ctx, key, cache.Value(&obj), cache.TTL(time.Hour)) 80 | if err != nil { 81 | // Handle error 82 | } 83 | ``` 84 | 85 | 86 | Example 2: Retrieving and setting a cache value using the `Do` function 87 | 88 | ```go 89 | err := cache.Once(ctx, key, cache.TTL(time.Hour), cache.Do(func(ctx context.Context) (any, error) { 90 | return fetchData(ctx) 91 | })) 92 | if err != nil { 93 | // Handle error 94 | } 95 | ``` 96 | 97 | Example 3: Performing an atomic operation using `SetNX` (sets only if the key doesn't exist) 98 | 99 | ```go 100 | err := cache.Set(ctx, key, cache.TTL(time.Hour), cache.Value(obj), cache.SetNX(true)) 101 | if err != nil { 102 | // Handle error, e.g., key already exists 103 | } 104 | ``` 105 | 106 | 107 | ## Once Interface 108 | 109 | This interface retrieves the value associated with a given `key` from the cache. If a cache miss occurs, the `Do` function is executed, the result is cached, and then returned. It ensures that for a given `key`, only one execution is in progress at any time. If duplicate requests occur, subsequent callers will wait for the original request to complete and receive the same result. Automatic cache refresh can be enabled by setting `Refresh(true)`. 110 | 111 | `Once` Interface: Distributed Cache – A Weapon Against Cache Piercing - `singleflight` 112 | 113 | ![singleflight](/docs/images/singleflight.png) 114 | 115 | > `singleflight`, provided in the Go standard library ("golang.org/x/sync/singleflight"), offers a mechanism to suppress redundant function calls. By assigning a key to each function call, concurrent calls with the same key will only be executed once, returning the same result. Essentially, it reuses the results of function calls. 116 | 117 | 118 | `Once` Interface: Distributed Cache – A Weapon Against Cache Piercing - `auto refresh` 119 | 120 | ![autoRefresh](/docs/images/autorefresh.png) 121 | 122 | > The `Once` interface provides the ability to automatically refresh the cache. This is intended to prevent cascading failures that can overwhelm the database when caches expire. Automatic refresh is suitable for scenarios with a small number of keys, low real-time requirements, and very high loading overhead. The code below (Example 1) specifies a refresh every minute and stops refreshing after an hour without access. If the cache is Redis or a multi-level cache where the last level is Redis, the cache loading behavior is globally unique. That is, regardless of the number of servers, only one server refreshes at a time to reduce the load on the backend. 123 | 124 | > Regarding the use case for the auto-refresh feature ("suitable for scenarios with a small number of keys, low real-time requirements, and very high loading overhead"), the "small number of keys" requires further clarification. To determine the appropriate number of keys, a model can be established. For example, when `refreshConcurrency=10`, there are 5 application servers, the average loading time for each key is 2 seconds, and `refreshDuration=30s`, the theoretically maximum number of keys that can be refreshed is 30 / 2 * 10 * 5 = 750. 125 | 126 | 127 | > Regarding the AutoRefresh feature's usage scenario ("Suitable for scenarios with few keys, low real-time requirements, and very high loading overhead"), "few keys" requires clarification. 128 | > To determine an appropriate number of keys, you can create a model. For example, with refreshConcurrency=10, 5 application servers, an average loading time of 2 seconds per key, and refreshDuration=30s, the theoretical maximum number of refreshable keys is 30 / 2 * 10 * 5 = 750. 129 | 130 | Function Signature: 131 | 132 | ```go 133 | func Once(ctx context.Context, key string, opts ...ItemOption) error 134 | ``` 135 | 136 | Parameters: 137 | 138 | - `ctx`: `context.Context`, the request context. Used for cancellation or timeout settings. 139 | - `key`: `string`, the cache key. 140 | - `opts`: `...ItemOption`, a variadic parameter list for configuring various options of the cache item. The following options are supported: 141 | - `Value(value any)`: Sets the cache value. 142 | - `TTL(duration time.Duration)`: Sets the expiration time for the remote cache item. (Local cache expiration time is uniformly set when building the Local cache instance.) 143 | - `Do(fn func(context.Context) (any, error))`: Uses the given fetch function `fn` to retrieve the value; this takes precedence over `Value`. 144 | - `SkipLocal(flag bool)`: Whether to skip the local cache. 145 | - `Refresh(refresh bool)`: Whether to enable automatic cache refresh. Works with the Cache configuration parameter `config.refreshDuration` to set the refresh interval. 146 | 147 | Return Value: 148 | 149 | - `error`: Returns an error if retrieving the cache fails. 150 | 151 | Example 1: Querying data using `Once` and enabling automatic cache refresh 152 | 153 | ```go 154 | mycache := cache.New(cache.WithName("any"), 155 | // ... 156 | // cache.WithRefreshDuration sets the asynchronous refresh interval 157 | cache.WithRefreshDuration(time.Minute), 158 | // cache.WithStopRefreshAfterLastAccess sets the time to cancel refresh tasks after a cache key has not been accessed 159 | cache.WithStopRefreshAfterLastAccess(time.Hour)) 160 | 161 | // The `Once` interface enables automatic refresh via `cache.Refresh(true)` 162 | err := mycache.Once(ctx, key, cache.Value(obj), cache.Refresh(true), cache.Do(func(ctx context.Context) (any, error) { 163 | return fetchData(ctx) 164 | })) 165 | if err != nil { 166 | // Handle error 167 | } 168 | 169 | mycache.Close() 170 | ``` 171 | 172 | 173 | # Generic Interfaces 174 | 175 | ```go 176 | // Set generically sets cache entries. 177 | func (w *T[K, V]) Set(ctx context.Context, key string, id K, v V) error 178 | 179 | // Get generically retrieves cache entries (underlying call to Once interface). 180 | func (w *T[K, V]) Get(ctx context.Context, key string, id K, fn func(context.Context, K) (V, error)) (V, error) 181 | 182 | // MGet generically retrieves multiple cache entries. 183 | func (w *T[K, V]) MGet(ctx context.Context, key string, ids []K, fn func(context.Context, []K) (map[K]V, error)) (result map[K]V) 184 | ``` 185 | 186 | ## MGet Bulk Query 187 | 188 | `MGet`, leveraging Go generics and the `Load` function, provides a user-friendly mechanism for bulk querying entities by ID in a multi-level cache. If the cache is Redis or a multi-level cache where the last level is Redis, read/write operations are performed using pipelining to improve performance. When a cache miss occurs in the local cache and a query to Redis and the database is required, the keys are sorted, and a single-flight (`singleflight`) call is used. It's important to note that for exceptional scenarios (I/O errors, serialization errors, etc.), our design prioritizes providing a degraded service to prevent cache penetration. 189 | 190 | ![mget](/docs/images/mget.png) 191 | 192 | Function Signature: 193 | 194 | ```go 195 | func (w *T[K, V]) MGet(ctx context.Context, key string, ids []K, fn func(context.Context, []K) (map[K]V, error)) (result map[K]V) 196 | func (w *T[K, V]) MGetWithErr(ctx context.Context, key string, ids []K, fn func(context.Context, []K) (map[K]V, error)) (result map[K]V, err error) 197 | ``` 198 | 199 | Parameters: 200 | 201 | - `ctx`: `context.Context`, the request context. Used for cancellation or timeout settings. 202 | - `key`: `string`, the cache key. 203 | - `ids`: `[]K`, the IDs of the cache objects. 204 | - `fn func(context.Context, []K) (map[K]V, error)`: The fetch function. Used to query data and set the cache for IDs that miss the cache. 205 | 206 | Return Value: 207 | 208 | - `map[K]V`: Returns a map of key-value pairs with values. 209 | -------------------------------------------------------------------------------- /docs/EN/Config.md: -------------------------------------------------------------------------------- 1 | 2 | * [Cache Configuration Options](#cache-configuration-options) 3 | * [Cache Instance Creation](#cache-instance-creation) 4 | * [Example 1: Creating a Two-Level Cache Instance (Both)](#example-1-creating-a-two-level-cache-instance-both) 5 | * [Example 2: Creating a Local-Only Cache Instance (Local)](#example-2-creating-a-local-only-cache-instance-local) 6 | * [Example 3: Creating a Remote-Only Cache Instance (Remote)](#example-3-creating-a-remote-only-cache-instance-remote) 7 | * [Example 4: Creating a Cache Instance and Configuring the jetcache-go-plugin Prometheus Statistics Plugin](#example-4-creating-a-cache-instance-and-configuring-the-jetcache-go-plugin-prometheus-statistics-plugin) 8 | * [Example 5: Creating a Cache Instance and Configuring `errNotFound` to Prevent Cache Penetration](#example-5-creating-a-cache-instance-and-configuring-errnotfound-to-prevent-cache-penetration) 9 | 10 | 11 | # Cache Configuration Options 12 | 13 | | Configuration Item | Data Type | Default Value | Description | 14 | |----------------------------|---------------------------|----------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 15 | | name | string | default | Cache name, used for log identification and metrics reporting. | 16 | | remote | `remote.Remote` interface | nil | Distributed cache, such as Redis. Can be customized by implementing the `remote.Remote` interface. | 17 | | local | `local.Local` interface | nil | In-memory cache, such as FreeCache, TinyLFU. Can be customized by implementing the `local.Local` interface. | 18 | | codec | string | msgpack | Encoding and decoding method for values. Defaults to "msgpack". Options: `json` \| `msgpack` \| `sonic`. Can be customized by implementing the `encoding.Codec` interface and registering it. | 19 | | errNotFound | error | nil | Error returned when an origin record is not found, e.g., `gorm.ErrRecordNotFound`. Used to prevent cache penetration (i.e., caching empty objects). | 20 | | remoteExpiry | `time.Duration` | 1 hour | Remote cache TTL, defaults to 1 hour. | 21 | | notFoundExpiry | `time.Duration` | 1 minute | Expiration time for placeholder caches when a cache miss occurs. Defaults to 1 minute. | 22 | | offset | `time.Duration` | (0,10] seconds | Expiration time jitter factor for cache misses. | 23 | | refreshDuration | `time.Duration` | 0 | Interval for asynchronous cache refresh. Defaults to 0 (refresh disabled). | 24 | | stopRefreshAfterLastAccess | `time.Duration` | refreshDuration + 1 second | Duration before cache refresh stops (after last access). | 25 | | refreshConcurrency | int | 4 | Maximum number of concurrent refreshes in the cache refresh task pool. | 26 | | statsDisabled | bool | false | Flag to disable cache statistics. | 27 | | statsHandler | `stats.Handler` interface | stats.NewStatsLogger | Metrics collector. Defaults to an embedded `log` collector. Can use the [jetcache-go-plugin](https://github.com/mgtv-tech/jetcache-go-plugin) `Prometheus` plugin or a custom implementation that implements the `stats.Handler` interface. | 28 | | sourceID | string | 16-character random string | 【Cache Event Broadcasting】Unique identifier for the cache instance. | 29 | | syncLocal | bool | false | 【Cache Event Broadcasting】Enable events to synchronize local caches (only applicable to "Both" cache types). | 30 | | eventChBufSize | int | 100 | 【Cache Event Broadcasting】Buffer size of the event channel (defaults to 100). | 31 | | eventHandler | `func(event *Event)` | nil | 【Cache Event Broadcasting】Function to handle local cache invalidation events. | 32 | | separatorDisabled | bool | false | Disable the cache key separator. Defaults to false. If true, the cache key will not use a separator. Currently mainly used for concatenating cache keys and IDs in generic interfaces. | 33 | | separator | string | : | Cache key separator. Defaults to ":". Currently mainly used for concatenating cache keys and IDs in generic interfaces. | 34 | 35 | 36 | # Cache Instance Creation 37 | 38 | ## Example 1: Creating a Two-Level Cache Instance (Both) 39 | 40 | ```go 41 | import ( 42 | "context" 43 | "time" 44 | 45 | "github.com/jinzhu/gorm" 46 | "github.com/mgtv-tech/jetcache-go" 47 | "github.com/mgtv-tech/jetcache-go/local" 48 | "github.com/mgtv-tech/jetcache-go/remote" 49 | "github.com/redis/go-redis/v9" 50 | ) 51 | 52 | ring := redis.NewRing(&redis.RingOptions{ 53 | Addrs: map[string]string{ 54 | "localhost": ":6379", 55 | }, 56 | }) 57 | 58 | // Create a two-level cache instance 59 | mycache := cache.New(cache.WithName("any"), 60 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 61 | cache.WithLocal(local.NewTinyLFU(10000, time.Minute)), // Local cache expiration time is uniformly set to 1 minute 62 | cache.WithErrNotFound(gorm.ErrRecordNotFound)) 63 | 64 | obj := struct { 65 | Name string 66 | Age int 67 | }{Name: "John Doe", Age: 30} 68 | 69 | // Set cache, where the remote cache expiration time TTL is 1 hour 70 | err := mycache.Set(context.Background(), "mykey", cache.Value(&obj), cache.TTL(time.Hour)) 71 | if err != nil { 72 | // Error handling 73 | } 74 | ``` 75 | 76 | ## Example 2: Creating a Local-Only Cache Instance (Local) 77 | 78 | ```go 79 | import ( 80 | "context" 81 | "time" 82 | 83 | "github.com/jinzhu/gorm" 84 | "github.com/mgtv-tech/jetcache-go" 85 | "github.com/mgtv-tech/jetcache-go/local" 86 | ) 87 | 88 | // Create a local-only cache instance 89 | mycache := cache.New(cache.WithName("any"), 90 | cache.WithLocal(local.NewTinyLFU(10000, time.Minute)), 91 | cache.WithErrNotFound(gorm.ErrRecordNotFound)) 92 | 93 | obj := struct { 94 | Name string 95 | Age int 96 | }{Name: "John Doe", Age: 30} 97 | 98 | err := mycache.Set(context.Background(), "mykey", cache.Value(&obj)) 99 | if err != nil { 100 | // Error handling 101 | } 102 | ``` 103 | 104 | ## Example 3: Creating a Remote-Only Cache Instance (Remote) 105 | 106 | ```go 107 | import ( 108 | "context" 109 | "time" 110 | 111 | "github.com/jinzhu/gorm" 112 | "github.com/mgtv-tech/jetcache-go" 113 | "github.com/mgtv-tech/jetcache-go/remote" 114 | "github.com/redis/go-redis/v9" 115 | ) 116 | 117 | ring := redis.NewRing(&redis.RingOptions{ 118 | Addrs: map[string]string{ 119 | "localhost": ":6379", 120 | }, 121 | }) 122 | 123 | // Create a remote-only cache instance 124 | mycache := cache.New(cache.WithName("any"), 125 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 126 | cache.WithErrNotFound(gorm.ErrRecordNotFound)) 127 | 128 | obj := struct { 129 | Name string 130 | Age int 131 | }{Name: "John Doe", Age: 30} 132 | 133 | err := mycache.Set(context.Background(), "mykey", cache.Value(&obj), cache.TTL(time.Hour)) 134 | if err != nil { 135 | // Error handling 136 | } 137 | ``` 138 | 139 | ## Example 4: Creating a Cache Instance and Configuring the [jetcache-go-plugin](https://github.com/mgtv-tech/jetcache-go-plugin) Prometheus Statistics Plugin 140 | 141 | ```go 142 | import ( 143 | "context" 144 | "time" 145 | 146 | "github.com/mgtv-tech/jetcache-go" 147 | "github.com/mgtv-tech/jetcache-go/remote" 148 | "github.com/redis/go-redis/v9" 149 | pstats "github.com/mgtv-tech/jetcache-go-plugin/stats" 150 | "github.com/mgtv-tech/jetcache-go/stats" 151 | ) 152 | 153 | mycache := cache.New(cache.WithName("any"), 154 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), 155 | cache.WithStatsHandler( 156 | stats.NewHandles(false, 157 | stats.NewStatsLogger(cacheName), 158 | pstats.NewPrometheus(cacheName)))) 159 | 160 | obj := struct { 161 | Name string 162 | Age int 163 | }{Name: "John Doe", Age: 30} 164 | 165 | err := mycache.Set(context.Background(), "mykey", cache.Value(&obj), cache.TTL(time.Hour)) 166 | if err != nil { 167 | // Error handling 168 | } 169 | ``` 170 | 171 | > Example 4 integrates both `Log` and `Prometheus` statistics. See: [Stat](/docs/EN/Stat.md) 172 | 173 | 174 | ## Example 5: Creating a Cache Instance and Configuring `errNotFound` to Prevent Cache Penetration 175 | 176 | ```go 177 | import ( 178 | "context" 179 | "fmt" 180 | "time" 181 | 182 | "github.com/jinzhu/gorm" 183 | "github.com/mgtv-tech/jetcache-go" 184 | "github.com/redis/go-redis/v9" 185 | ) 186 | 187 | ring := redis.NewRing(&redis.RingOptions{ 188 | Addrs: map[string]string{ 189 | "localhost": ":6379", 190 | }, 191 | }) 192 | 193 | // Create a cache instance and configure errNotFound to prevent cache penetration 194 | mycache := cache.New(cache.WithName("any"), 195 | cache.WithRemote(remote.NewGoRedisV9Adapter(ring)), // Assuming you still want a remote cache 196 | cache.WithErrNotFound(gorm.ErrRecordNotFound)) 197 | 198 | var value string 199 | err := mycache.Once(ctx, key, cache.Value(&value), cache.Do(func(context.Context) (any, error) { 200 | return nil, gorm.ErrRecordNotFound 201 | })) 202 | fmt.Println(err) 203 | 204 | // Output: record not found 205 | ``` 206 | 207 | `jetcache-go` uses a lightweight approach of [caching null objects] to address cache penetration: 208 | 209 | - When creating a cache instance, specify a "not found" error. For example: `gorm.ErrRecordNotFound`, `redis.Nil`. 210 | - If a "not found" error is encountered during a query, a placeholder value (e.g., a special marker) is cached. 211 | - When retrieving the value, check if it's the placeholder. If so, return the corresponding "not found" error. 212 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= 2 | github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= 3 | github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= 4 | github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= 5 | github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= 6 | github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= 7 | github.com/bytedance/sonic v1.13.3 h1:MS8gmaH16Gtirygw7jV91pDCN33NyMrPbN7qiYhEsF0= 8 | github.com/bytedance/sonic v1.13.3/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4= 9 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 10 | github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY= 11 | github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= 12 | github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 13 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 14 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 15 | github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4= 16 | github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 17 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 18 | github.com/coocood/freecache v1.2.4 h1:UdR6Yz/X1HW4fZOuH0Z94KwG851GWOSknua5VUbb/5M= 19 | github.com/coocood/freecache v1.2.4/go.mod h1:RBUWa/Cy+OHdfTGFEhEuE1pMCMX51Ncizj7rthiQ3vk= 20 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 22 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 23 | github.com/dgraph-io/ristretto/v2 v2.1.0 h1:59LjpOJLNDULHh8MC4UaegN52lC4JnO2dITsie/Pa8I= 24 | github.com/dgraph-io/ristretto/v2 v2.1.0/go.mod h1:uejeqfYXpUomfse0+lO+13ATz4TypQYLJZzBSAemuB4= 25 | github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y= 26 | github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= 27 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= 28 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= 29 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 30 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 31 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 32 | github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= 33 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 34 | github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= 35 | github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 36 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= 37 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 38 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 39 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 40 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 41 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 42 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 43 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 44 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 45 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 46 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 47 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 48 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 49 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 50 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 51 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 52 | github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg= 53 | github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= 54 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 55 | github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc= 56 | github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 57 | github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= 58 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 59 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 60 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 61 | github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= 62 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 63 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 64 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 65 | github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= 66 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 67 | github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= 68 | github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= 69 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 70 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 71 | github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k= 72 | github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY= 73 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 74 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 75 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 76 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 77 | github.com/redis/go-redis/v9 v9.11.0 h1:E3S08Gl/nJNn5vkxd2i78wZxWAPNZgUNTp8WIJUAiIs= 78 | github.com/redis/go-redis/v9 v9.11.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= 79 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 80 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 81 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 82 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 83 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 84 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 85 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 86 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 87 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 88 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 89 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 90 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 91 | github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= 92 | github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= 93 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 94 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 95 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 96 | github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= 97 | github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= 98 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= 99 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 100 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 101 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 102 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 103 | golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk= 104 | golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= 105 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 106 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 107 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 108 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 109 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 110 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 111 | golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= 112 | golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 113 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 114 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 115 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 116 | golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= 117 | golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 118 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 119 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 120 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 121 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 122 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 123 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 124 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 125 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 126 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 127 | golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= 128 | golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 129 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 130 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 131 | golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= 132 | golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= 133 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 134 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 135 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 136 | golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24= 137 | golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ= 138 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 139 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 140 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 141 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 142 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 143 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 144 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 145 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 146 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 147 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 148 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= 149 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 150 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 151 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 152 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 153 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= 154 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 155 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 156 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 157 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 158 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 159 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 160 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 161 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 162 | --------------------------------------------------------------------------------