diff --git a/cache/cache.go b/cache/cache.go index ea02f1a..c0a491d 100644 --- a/cache/cache.go +++ b/cache/cache.go @@ -7,6 +7,7 @@ import ( "time" "github.com/charlienet/go-mixed/bytesconv" + "github.com/charlienet/go-mixed/json" "github.com/charlienet/go-mixed/locker" "github.com/charlienet/go-mixed/logx" ) @@ -22,6 +23,7 @@ type Cache struct { distributdCache DistributdCache // 分布式缓存 publishSubscribe PublishSubscribe // 发布订阅 lock locker.ChanLocker // 资源锁 + stats *Stats // 缓存命中计数 qps *qps // 访问计数 logger logx.Logger // 日志记录 } @@ -148,3 +150,39 @@ func (c *Cache) getFromSource(ctx context.Context, key string, fn LoadFunc) erro return c.getFromSource(ctx, key, fn) } } + +func (c *Cache) marshal(value any) ([]byte, error) { + switch value := value.(type) { + case nil: + return nil, nil + case []byte: + return value, nil + case string: + return []byte(value), nil + } + + b, err := json.Marshal(value) + return b, err +} + +func (c *Cache) unmarshal(b []byte, value any) error { + if len(b) == 0 { + return nil + } + + switch value := value.(type) { + case nil: + return nil + case *[]byte: + clone := make([]byte, len(b)) + copy(clone, b) + *value = clone + return nil + case *string: + *value = string(b) + return nil + } + + err := json.Unmarshal(b, value) + return err +} diff --git a/cache/local_cache.go b/cache/local_cache.go new file mode 100644 index 0000000..6b4ff29 --- /dev/null +++ b/cache/local_cache.go @@ -0,0 +1,7 @@ +package cache + +type LocalCache interface { + Set(key string, data []byte) + Get(key string) ([]byte, bool) + Del(key string) +} diff --git a/cache/stats.go b/cache/stats.go new file mode 100644 index 0000000..fcb6fa6 --- /dev/null +++ b/cache/stats.go @@ -0,0 +1,20 @@ +package cache + +import "sync/atomic" + +type Stats struct { + Hits uint64 + Misses uint64 +} + +func (s *Stats) AddHits() { + atomic.AddUint64(&s.Hits, 1) +} + +func (s *Stats) AddMisses() { + atomic.AddUint64(&s.Misses, 1) +} + +func (c *Cache) Stats() *Stats { + return c.stats +} diff --git a/cache/tiny_lfu.go b/cache/tiny_lfu.go new file mode 100644 index 0000000..4479c12 --- /dev/null +++ b/cache/tiny_lfu.go @@ -0,0 +1,52 @@ +package cache + +import ( + "time" + + "github.com/charlienet/go-mixed/locker" + "github.com/vmihailenco/go-tinylfu" +) + +type TinyLFU struct { + mu locker.Locker + lfu *tinylfu.T + ttl time.Duration +} + +func NewTinyLFU(size int, ttl time.Duration) *TinyLFU { + return &TinyLFU{ + mu: locker.NewLocker(), + lfu: tinylfu.New(size, 100000), + ttl: ttl, + } +} + +func (c *TinyLFU) Set(key string, b []byte, expire time.Duration) { + c.mu.Lock() + defer c.mu.Unlock() + + c.lfu.Set(&tinylfu.Item{ + Key: key, + Value: b, + ExpireAt: time.Now().Add(c.ttl), + }) +} + +func (c *TinyLFU) Get(key string) ([]byte, bool) { + c.mu.Lock() + defer c.mu.Unlock() + + val, ok := c.lfu.Get(key) + if !ok { + return nil, false + } + + return val.([]byte), true +} + +func (c *TinyLFU) Del(key string) { + c.mu.Lock() + defer c.mu.Unlock() + + c.lfu.Del(key) +} diff --git a/cache/tiny_lfu_test.go b/cache/tiny_lfu_test.go new file mode 100644 index 0000000..850aa5d --- /dev/null +++ b/cache/tiny_lfu_test.go @@ -0,0 +1,57 @@ +package cache_test + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/charlienet/go-mixed/cache" + "github.com/charlienet/go-mixed/rand" +) + +func TestTinyGet(t *testing.T) { + strFor := func(i int) string { + return fmt.Sprintf("a string %d", i) + } + keyName := func(i int) string { + return fmt.Sprintf("key-%00000d", i) + } + + mycache := cache.NewTinyLFU(1000, 1*time.Second) + size := 50000 + // Put a bunch of stuff in the cache with a TTL of 1 second + for i := 0; i < size; i++ { + key := keyName(i) + mycache.Set(key, []byte(strFor(i)), time.Second*2) + } + + // Read stuff for a bit longer than the TTL - that's when the corruption occurs + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + + done := ctx.Done() + +loop: + for { + select { + case <-done: + // this is expected + break loop + default: + i := rand.Intn(size) + key := keyName(i) + + b, ok := mycache.Get(key) + if !ok { + continue loop + } + + got := string(b) + expected := strFor(i) + if got != expected { + t.Fatalf("expected=%q got=%q key=%q", expected, got, key) + } + } + } +}