refactor: 使用 memory cache
This commit is contained in:
parent
26c95c8fee
commit
2302575edf
@ -1,10 +0,0 @@
|
|||||||
//
|
|
||||||
// adapter_file.go
|
|
||||||
// Copyright (C) 2022 tiglog <me@tiglog.com>
|
|
||||||
//
|
|
||||||
// Distributed under terms of the MIT license.
|
|
||||||
//
|
|
||||||
|
|
||||||
package gcache
|
|
||||||
|
|
||||||
// 本地文件缓存
|
|
@ -1,48 +0,0 @@
|
|||||||
//
|
|
||||||
// adapter_local.go
|
|
||||||
// Copyright (C) 2022 tiglog <me@tiglog.com>
|
|
||||||
//
|
|
||||||
// Distributed under terms of the MIT license.
|
|
||||||
//
|
|
||||||
|
|
||||||
package gcache
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.hexq.cn/tiglog/golib/helper"
|
|
||||||
)
|
|
||||||
|
|
||||||
const default_cache_size = 1024
|
|
||||||
|
|
||||||
// 本地内存缓存
|
|
||||||
type localCacheAdapter struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
data map[string][]byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewLocalCacheAdapter() ICacheAdapter {
|
|
||||||
return &localCacheAdapter{
|
|
||||||
data: make(map[string][]byte, default_cache_size),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *localCacheAdapter) Get(key string, dest interface{}) error {
|
|
||||||
val, ok := c.data[key]
|
|
||||||
if ok {
|
|
||||||
helper.Scan(val, dest)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *localCacheAdapter) Set(key string, val interface{}, ttl time.Duration) error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *localCacheAdapter) Has(key string) bool {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
func (c *localCacheAdapter) Del(keys ...string) (int64, error) {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
@ -1,52 +0,0 @@
|
|||||||
//
|
|
||||||
// adapter_redis.go
|
|
||||||
// Copyright (C) 2022 tiglog <me@tiglog.com>
|
|
||||||
//
|
|
||||||
// Distributed under terms of the MIT license.
|
|
||||||
//
|
|
||||||
|
|
||||||
package gcache
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 使用 redis 服务缓存
|
|
||||||
type redisCacheAdapter struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
redis *redis.Client
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRedisCacheAdapter(rds *redis.Client) ICacheAdapter {
|
|
||||||
return &redisCacheAdapter{
|
|
||||||
redis: rds,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *redisCacheAdapter) Get(key string, dest interface{}) error {
|
|
||||||
cmd := c.redis.Get(context.Background(), key)
|
|
||||||
return cmd.Scan(dest)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *redisCacheAdapter) Set(key string, val interface{}, ttl time.Duration) error {
|
|
||||||
cmd := c.redis.Set(context.Background(), key, val, ttl)
|
|
||||||
return cmd.Err()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *redisCacheAdapter) Has(key string) bool {
|
|
||||||
cmd := c.redis.Exists(context.Background(), key)
|
|
||||||
result, _ := cmd.Result()
|
|
||||||
if result == 1 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *redisCacheAdapter) Del(keys ...string) (int64, error) {
|
|
||||||
cmd := c.redis.Del(context.Background(), keys...)
|
|
||||||
return cmd.Result()
|
|
||||||
}
|
|
@ -1,57 +0,0 @@
|
|||||||
//
|
|
||||||
// cache.go
|
|
||||||
// Copyright (C) 2022 tiglog <me@tiglog.com>
|
|
||||||
//
|
|
||||||
// Distributed under terms of the MIT license.
|
|
||||||
//
|
|
||||||
|
|
||||||
package gcache
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Engine struct {
|
|
||||||
client ICacheAdapter
|
|
||||||
}
|
|
||||||
|
|
||||||
var once sync.Once
|
|
||||||
|
|
||||||
var engine *Engine
|
|
||||||
|
|
||||||
func New(adapter ICacheAdapter) *Engine {
|
|
||||||
once.Do(func() {
|
|
||||||
engine = &Engine{
|
|
||||||
client: adapter,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return engine
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewWithRedis(rds *redis.Client) *Engine {
|
|
||||||
once.Do(func() {
|
|
||||||
engine = &Engine{
|
|
||||||
client: NewRedisCacheAdapter(rds),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return engine
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Engine) Get(key string, dest interface{}) error {
|
|
||||||
return e.client.Get(key, dest)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Engine) Set(key string, val interface{}, ttl time.Duration) error {
|
|
||||||
return e.client.Set(key, val, ttl)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Engine) Has(key string) bool {
|
|
||||||
return e.client.Has(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Engine) Del(keys ...string) (int64, error) {
|
|
||||||
return e.client.Del(keys...)
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
//
|
|
||||||
// cache_contact.go
|
|
||||||
// Copyright (C) 2022 tiglog <me@tiglog.com>
|
|
||||||
//
|
|
||||||
// Distributed under terms of the MIT license.
|
|
||||||
//
|
|
||||||
|
|
||||||
package gcache
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type ICacheAdapter interface {
|
|
||||||
Get(key string, dest interface{}) error
|
|
||||||
Set(key string, val interface{}, exp time.Duration) error
|
|
||||||
Del(keys ...string) (int64, error)
|
|
||||||
Has(key string) bool
|
|
||||||
}
|
|
@ -1,70 +0,0 @@
|
|||||||
//
|
|
||||||
// cache_test.go
|
|
||||||
// Copyright (C) 2023 tiglog <me@tiglog.com>
|
|
||||||
//
|
|
||||||
// Distributed under terms of the MIT license.
|
|
||||||
//
|
|
||||||
|
|
||||||
package gcache_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"git.hexq.cn/tiglog/golib/gcache"
|
|
||||||
"git.hexq.cn/tiglog/golib/gtest"
|
|
||||||
"github.com/go-redis/redis/v8"
|
|
||||||
)
|
|
||||||
|
|
||||||
func getRedis() *redis.Client {
|
|
||||||
opt, _ := redis.ParseURL(os.Getenv("REDIS_URL"))
|
|
||||||
return redis.NewClient(opt)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRedis(t *testing.T) {
|
|
||||||
rds := getRedis()
|
|
||||||
cmd := rds.Ping(context.Background())
|
|
||||||
ret, err := cmd.Result()
|
|
||||||
gtest.Nil(t, err)
|
|
||||||
fmt.Println(ret)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCacheNew(t *testing.T) {
|
|
||||||
rds := getRedis()
|
|
||||||
cm1 := gcache.NewWithRedis(rds)
|
|
||||||
cm2 := gcache.NewWithRedis(rds)
|
|
||||||
gtest.Equal(t, cm1, cm2)
|
|
||||||
}
|
|
||||||
func TestRedisAdapter(t *testing.T) {
|
|
||||||
rds := getRedis()
|
|
||||||
cm := gcache.NewWithRedis(rds)
|
|
||||||
key := "foo"
|
|
||||||
cm.Del(key)
|
|
||||||
r1 := cm.Has(key)
|
|
||||||
gtest.False(t, r1)
|
|
||||||
|
|
||||||
var err error
|
|
||||||
val1 := "bar"
|
|
||||||
err = cm.Set(key, val1, time.Second)
|
|
||||||
gtest.Nil(t, err)
|
|
||||||
|
|
||||||
var r2 string
|
|
||||||
err = cm.Get(key, &r2)
|
|
||||||
gtest.Nil(t, err)
|
|
||||||
gtest.Equal(t, val1, r2)
|
|
||||||
|
|
||||||
val2 := 2
|
|
||||||
err = cm.Set(key, val2, time.Hour)
|
|
||||||
gtest.Nil(t, err)
|
|
||||||
var r3 int
|
|
||||||
err = cm.Get(key, &r3)
|
|
||||||
gtest.Nil(t, err)
|
|
||||||
gtest.Equal(t, val2, r3)
|
|
||||||
|
|
||||||
n, err := cm.Del(key)
|
|
||||||
gtest.Nil(t, err)
|
|
||||||
gtest.Equal(t, int64(1), n)
|
|
||||||
}
|
|
21
gcache/config.go
Normal file
21
gcache/config.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
//
|
||||||
|
// config.go
|
||||||
|
// Copyright (C) 2023 tiglog <me@tiglog.com>
|
||||||
|
//
|
||||||
|
// Distributed under terms of the MIT license.
|
||||||
|
//
|
||||||
|
|
||||||
|
package gcache
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
shards int
|
||||||
|
expiredCallback ExpiredCallback
|
||||||
|
hash IHash
|
||||||
|
clearInterval time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfig() *Config {
|
||||||
|
return &Config{shards: 1024, hash: newDefaultHash(), clearInterval: 1 * time.Second}
|
||||||
|
}
|
42
gcache/hash.go
Normal file
42
gcache/hash.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
//
|
||||||
|
// hash.go
|
||||||
|
// Copyright (C) 2023 tiglog <me@tiglog.com>
|
||||||
|
//
|
||||||
|
// Distributed under terms of the MIT license.
|
||||||
|
//
|
||||||
|
|
||||||
|
package gcache
|
||||||
|
|
||||||
|
// IHash is responsible for generating unsigned, 64-bit hash of provided string. IHash should minimize collisions
|
||||||
|
// (generating same hash for different strings) and while performance is also important fast functions are preferable (i.e.
|
||||||
|
// you can use FarmHash family).
|
||||||
|
type IHash interface {
|
||||||
|
Sum64(string) uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
// newDefaultHash returns a new 64-bit FNV-1a IHash which makes no memory allocations.
|
||||||
|
// Its Sum64 method will lay the value out in big-endian byte order.
|
||||||
|
// See https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_function
|
||||||
|
func newDefaultHash() IHash {
|
||||||
|
return fnv64a{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fnv64a struct{}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// offset64 FNVa offset basis. See https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_function#FNV-1a_hash
|
||||||
|
offset64 = 14695981039346656037
|
||||||
|
// prime64 FNVa prime value. See https://en.wikipedia.org/wiki/Fowler–Noll–Vo_hash_function#FNV-1a_hash
|
||||||
|
prime64 = 1099511628211
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sum64 gets the string and returns its uint64 hash value.
|
||||||
|
func (f fnv64a) Sum64(key string) uint64 {
|
||||||
|
var hash uint64 = offset64
|
||||||
|
for i := 0; i < len(key); i++ {
|
||||||
|
hash ^= uint64(key[i])
|
||||||
|
hash *= prime64
|
||||||
|
}
|
||||||
|
|
||||||
|
return hash
|
||||||
|
}
|
122
gcache/icache.go
Normal file
122
gcache/icache.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
//
|
||||||
|
// icache.go
|
||||||
|
// Copyright (C) 2023 tiglog <me@tiglog.com>
|
||||||
|
//
|
||||||
|
// Distributed under terms of the MIT license.
|
||||||
|
//
|
||||||
|
|
||||||
|
package gcache
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// ExpiredCallback Callback the function when the key-value pair expires
|
||||||
|
// Note that it is executed after expiration
|
||||||
|
type ExpiredCallback func(k string, v interface{}) error
|
||||||
|
|
||||||
|
type ICache interface {
|
||||||
|
//Set key to hold the string value. If key already holds a value, it is overwritten, regardless of its type.
|
||||||
|
//Any previous time to live associated with the key is discarded on successful SET operation.
|
||||||
|
//Example:
|
||||||
|
//c.Set("demo", 1)
|
||||||
|
//c.Set("demo", 1, WithEx(10*time.Second))
|
||||||
|
//c.Set("demo", 1, WithEx(10*time.Second), WithNx())
|
||||||
|
Set(k string, v interface{}, opts ...SetIOption) bool
|
||||||
|
//Get the value of key.
|
||||||
|
//If the key does not exist the special value nil,false is returned.
|
||||||
|
//Example:
|
||||||
|
//c.Get("demo") //nil, false
|
||||||
|
//c.Set("demo", "value")
|
||||||
|
//c.Get("demo") //"value", true
|
||||||
|
Get(k string) (interface{}, bool)
|
||||||
|
//GetSet Atomically sets key to value and returns the old value stored at key.
|
||||||
|
//Returns nil,false when key not exists.
|
||||||
|
//Example:
|
||||||
|
//c.GetSet("demo", 1) //nil,false
|
||||||
|
//c.GetSet("demo", 2) //1,true
|
||||||
|
GetSet(k string, v interface{}, opts ...SetIOption) (interface{}, bool)
|
||||||
|
//GetDel Get the value of key and delete the key.
|
||||||
|
//This command is similar to GET, except for the fact that it also deletes the key on success.
|
||||||
|
//Example:
|
||||||
|
//c.Set("demo", "value")
|
||||||
|
//c.GetDel("demo") //"value", true
|
||||||
|
//c.GetDel("demo") //nil, false
|
||||||
|
GetDel(k string) (interface{}, bool)
|
||||||
|
//Del Removes the specified keys. A key is ignored if it does not exist.
|
||||||
|
//Return the number of keys that were removed.
|
||||||
|
//Example:
|
||||||
|
//c.Set("demo1", "1")
|
||||||
|
//c.Set("demo2", "1")
|
||||||
|
//c.Del("demo1", "demo2", "demo3") //2
|
||||||
|
Del(keys ...string) int
|
||||||
|
//DelExpired Only delete when key expires
|
||||||
|
//Example:
|
||||||
|
//c.Set("demo1", "1")
|
||||||
|
//c.Set("demo2", "1", WithEx(1*time.Second))
|
||||||
|
//time.Sleep(1*time.Second)
|
||||||
|
//c.DelExpired("demo1", "demo2") //true
|
||||||
|
DelExpired(k string) bool
|
||||||
|
//Exists Returns if key exists.
|
||||||
|
//Return the number of exists keys.
|
||||||
|
//Example:
|
||||||
|
//c.Set("demo1", "1")
|
||||||
|
//c.Set("demo2", "1")
|
||||||
|
//c.Exists("demo1", "demo2", "demo3") //2
|
||||||
|
Exists(keys ...string) bool
|
||||||
|
//Expire Set a timeout on key.
|
||||||
|
//After the timeout has expired, the key will automatically be deleted.
|
||||||
|
//Return false if the key not exist.
|
||||||
|
//Example:
|
||||||
|
//c.Expire("demo", 1*time.Second) // false
|
||||||
|
//c.Set("demo", "1")
|
||||||
|
//c.Expire("demo", 1*time.Second) // true
|
||||||
|
Expire(k string, d time.Duration) bool
|
||||||
|
//ExpireAt has the same effect and semantic as Expire, but instead of specifying the number of seconds representing the TTL (time to live),
|
||||||
|
//it takes an absolute Unix Time (seconds since January 1, 1970). A Time in the past will delete the key immediately.
|
||||||
|
//Return false if the key not exist.
|
||||||
|
//Example:
|
||||||
|
//c.ExpireAt("demo", time.Now().Add(10*time.Second)) // false
|
||||||
|
//c.Set("demo", "1")
|
||||||
|
//c.ExpireAt("demo", time.Now().Add(10*time.Second)) // true
|
||||||
|
ExpireAt(k string, t time.Time) bool
|
||||||
|
//Persist Remove the existing timeout on key.
|
||||||
|
//Return false if the key not exist.
|
||||||
|
//Example:
|
||||||
|
//c.Persist("demo") // false
|
||||||
|
//c.Set("demo", "1")
|
||||||
|
//c.Persist("demo") // true
|
||||||
|
Persist(k string) bool
|
||||||
|
//Ttl Returns the remaining time to live of a key that has a timeout.
|
||||||
|
//Returns 0,false if the key does not exist or if the key exist but has no associated expire.
|
||||||
|
//Example:
|
||||||
|
//c.Set("demo", "1")
|
||||||
|
//c.Ttl("demo") // 0,false
|
||||||
|
//c.Set("demo", "1", WithEx(10*time.Second))
|
||||||
|
//c.Ttl("demo") // 10*time.Second,true
|
||||||
|
Ttl(k string) (time.Duration, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
type IItem interface {
|
||||||
|
Expired() bool
|
||||||
|
CanExpire() bool
|
||||||
|
SetExpireAt(t time.Time)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Item struct {
|
||||||
|
v interface{}
|
||||||
|
expire time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Item) Expired() bool {
|
||||||
|
if !i.CanExpire() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return time.Now().After(i.expire)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Item) CanExpire() bool {
|
||||||
|
return !i.expire.IsZero()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Item) SetExpireAt(t time.Time) {
|
||||||
|
i.expire = t
|
||||||
|
}
|
255
gcache/memory.go
Normal file
255
gcache/memory.go
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
//
|
||||||
|
// memory.go
|
||||||
|
// Copyright (C) 2023 tiglog <me@tiglog.com>
|
||||||
|
//
|
||||||
|
// Distributed under terms of the MIT license.
|
||||||
|
//
|
||||||
|
|
||||||
|
package gcache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewMemCache(opts ...ICacheOption) ICache {
|
||||||
|
conf := NewConfig()
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &memCache{
|
||||||
|
shards: make([]*memCacheShard, conf.shards),
|
||||||
|
closed: make(chan struct{}),
|
||||||
|
shardMask: uint64(conf.shards - 1),
|
||||||
|
config: conf,
|
||||||
|
hash: conf.hash,
|
||||||
|
}
|
||||||
|
for i := 0; i < len(c.shards); i++ {
|
||||||
|
c.shards[i] = newMemCacheShard(conf)
|
||||||
|
}
|
||||||
|
if conf.clearInterval > 0 {
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(conf.clearInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
for _, shard := range c.shards {
|
||||||
|
shard.checkExpire()
|
||||||
|
}
|
||||||
|
case <-c.closed:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
cache := &MemCache{c}
|
||||||
|
// Associated finalizer function with obj.
|
||||||
|
// When the obj is unreachable, close the obj.
|
||||||
|
runtime.SetFinalizer(cache, func(cache *MemCache) { close(cache.closed) })
|
||||||
|
return cache
|
||||||
|
}
|
||||||
|
|
||||||
|
type MemCache struct {
|
||||||
|
*memCache
|
||||||
|
}
|
||||||
|
|
||||||
|
type memCache struct {
|
||||||
|
shards []*memCacheShard
|
||||||
|
hash IHash
|
||||||
|
shardMask uint64
|
||||||
|
config *Config
|
||||||
|
closed chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *memCache) Set(k string, v interface{}, opts ...SetIOption) bool {
|
||||||
|
item := Item{v: v}
|
||||||
|
for _, opt := range opts {
|
||||||
|
if pass := opt(c, k, &item); !pass {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hashedKey := c.hash.Sum64(k)
|
||||||
|
shard := c.getShard(hashedKey)
|
||||||
|
shard.set(k, &item)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *memCache) Get(k string) (interface{}, bool) {
|
||||||
|
hashedKey := c.hash.Sum64(k)
|
||||||
|
shard := c.getShard(hashedKey)
|
||||||
|
return shard.get(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *memCache) GetSet(k string, v interface{}, opts ...SetIOption) (interface{}, bool) {
|
||||||
|
defer c.Set(k, v, opts...)
|
||||||
|
return c.Get(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *memCache) GetDel(k string) (interface{}, bool) {
|
||||||
|
defer c.Del(k)
|
||||||
|
return c.Get(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *memCache) Del(ks ...string) int {
|
||||||
|
var count int
|
||||||
|
for _, k := range ks {
|
||||||
|
hashedKey := c.hash.Sum64(k)
|
||||||
|
shard := c.getShard(hashedKey)
|
||||||
|
count += shard.del(k)
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// DelExpired Only delete when key expires
|
||||||
|
func (c *memCache) DelExpired(k string) bool {
|
||||||
|
hashedKey := c.hash.Sum64(k)
|
||||||
|
shard := c.getShard(hashedKey)
|
||||||
|
return shard.delExpired(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *memCache) Exists(ks ...string) bool {
|
||||||
|
for _, k := range ks {
|
||||||
|
if _, found := c.Get(k); !found {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *memCache) Expire(k string, d time.Duration) bool {
|
||||||
|
v, found := c.Get(k)
|
||||||
|
if !found {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return c.Set(k, v, WithEx(d))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *memCache) ExpireAt(k string, t time.Time) bool {
|
||||||
|
v, found := c.Get(k)
|
||||||
|
if !found {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return c.Set(k, v, WithExAt(t))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *memCache) Persist(k string) bool {
|
||||||
|
v, found := c.Get(k)
|
||||||
|
if !found {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return c.Set(k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *memCache) Ttl(k string) (time.Duration, bool) {
|
||||||
|
hashedKey := c.hash.Sum64(k)
|
||||||
|
shard := c.getShard(hashedKey)
|
||||||
|
return shard.ttl(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *memCache) getShard(hashedKey uint64) (shard *memCacheShard) {
|
||||||
|
return c.shards[hashedKey&c.shardMask]
|
||||||
|
}
|
||||||
|
|
||||||
|
//////////////
|
||||||
|
// shard //
|
||||||
|
//////////////
|
||||||
|
|
||||||
|
type memCacheShard struct {
|
||||||
|
hashmap map[string]Item
|
||||||
|
lock sync.RWMutex
|
||||||
|
expiredCallback ExpiredCallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMemCacheShard(conf *Config) *memCacheShard {
|
||||||
|
return &memCacheShard{expiredCallback: conf.expiredCallback, hashmap: map[string]Item{}}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *memCacheShard) set(k string, item *Item) {
|
||||||
|
c.lock.Lock()
|
||||||
|
c.hashmap[k] = *item
|
||||||
|
c.lock.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *memCacheShard) get(k string) (interface{}, bool) {
|
||||||
|
c.lock.RLock()
|
||||||
|
item, exist := c.hashmap[k]
|
||||||
|
c.lock.RUnlock()
|
||||||
|
if !exist {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
if !item.Expired() {
|
||||||
|
return item.v, true
|
||||||
|
}
|
||||||
|
if c.delExpired(k) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
return c.get(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *memCacheShard) getSet(k string, item *Item) (interface{}, bool) {
|
||||||
|
defer c.set(k, item)
|
||||||
|
return c.get(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *memCacheShard) getDel(k string) (interface{}, bool) {
|
||||||
|
defer c.del(k)
|
||||||
|
return c.get(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *memCacheShard) del(k string) int {
|
||||||
|
var count int
|
||||||
|
c.lock.Lock()
|
||||||
|
v, found := c.hashmap[k]
|
||||||
|
if found {
|
||||||
|
delete(c.hashmap, k)
|
||||||
|
if !v.Expired() {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.lock.Unlock()
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
// delExpired Only delete when key expires
|
||||||
|
func (c *memCacheShard) delExpired(k string) bool {
|
||||||
|
c.lock.Lock()
|
||||||
|
item, found := c.hashmap[k]
|
||||||
|
if !found || !item.Expired() {
|
||||||
|
c.lock.Unlock()
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
delete(c.hashmap, k)
|
||||||
|
c.lock.Unlock()
|
||||||
|
if c.expiredCallback != nil {
|
||||||
|
_ = c.expiredCallback(k, item.v)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *memCacheShard) ttl(k string) (time.Duration, bool) {
|
||||||
|
c.lock.RLock()
|
||||||
|
v, found := c.hashmap[k]
|
||||||
|
c.lock.RUnlock()
|
||||||
|
if !found || !v.CanExpire() || v.Expired() {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return v.expire.Sub(time.Now()), true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *memCacheShard) checkExpire() {
|
||||||
|
var expiredKeys []string
|
||||||
|
c.lock.RLock()
|
||||||
|
for k, item := range c.hashmap {
|
||||||
|
if item.Expired() {
|
||||||
|
expiredKeys = append(expiredKeys, k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.lock.RUnlock()
|
||||||
|
for _, k := range expiredKeys {
|
||||||
|
c.delExpired(k)
|
||||||
|
}
|
||||||
|
}
|
31
gcache/memory_test.go
Normal file
31
gcache/memory_test.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
//
|
||||||
|
// memory_test.go
|
||||||
|
// Copyright (C) 2023 tiglog <me@tiglog.com>
|
||||||
|
//
|
||||||
|
// Distributed under terms of the MIT license.
|
||||||
|
//
|
||||||
|
|
||||||
|
package gcache_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.hexq.cn/tiglog/golib/gcache"
|
||||||
|
"git.hexq.cn/tiglog/golib/gtest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUsage(t *testing.T) {
|
||||||
|
|
||||||
|
c := gcache.NewMemCache()
|
||||||
|
c.Set("a", 1)
|
||||||
|
c.Set("b", 1, gcache.WithEx(1*time.Second))
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
r1, ok := c.Get("a") // 1, true
|
||||||
|
gtest.True(t, ok)
|
||||||
|
gtest.Equal(t, 1, r1)
|
||||||
|
|
||||||
|
r2, ok := c.Get("b")
|
||||||
|
gtest.False(t, ok)
|
||||||
|
gtest.Nil(t, r2)
|
||||||
|
}
|
68
gcache/option.go
Normal file
68
gcache/option.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
//
|
||||||
|
// option.go
|
||||||
|
// Copyright (C) 2023 tiglog <me@tiglog.com>
|
||||||
|
//
|
||||||
|
// Distributed under terms of the MIT license.
|
||||||
|
//
|
||||||
|
|
||||||
|
package gcache
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// SetIOption The option used to cache set
|
||||||
|
type SetIOption func(ICache, string, IItem) bool
|
||||||
|
|
||||||
|
// WithEx Set the specified expire time, in time.Duration.
|
||||||
|
func WithEx(d time.Duration) SetIOption {
|
||||||
|
return func(c ICache, k string, v IItem) bool {
|
||||||
|
v.SetExpireAt(time.Now().Add(d))
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithExAt Set the specified expire deadline, in time.Time.
|
||||||
|
func WithExAt(t time.Time) SetIOption {
|
||||||
|
return func(c ICache, k string, v IItem) bool {
|
||||||
|
v.SetExpireAt(t)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ICacheOption The option used to create the cache object
|
||||||
|
type ICacheOption func(conf *Config)
|
||||||
|
|
||||||
|
// WithShards set custom size of sharding. Default is 1024
|
||||||
|
// The larger the size, the smaller the lock force, the higher the concurrency performance,
|
||||||
|
// and the higher the memory footprint, so try to choose a size that fits your business scenario
|
||||||
|
func WithShards(shards int) ICacheOption {
|
||||||
|
if shards <= 0 {
|
||||||
|
panic("Invalid shards")
|
||||||
|
}
|
||||||
|
return func(conf *Config) {
|
||||||
|
conf.shards = shards
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithExpiredCallback set custom expired callback function
|
||||||
|
// This callback function is called when the key-value pair expires
|
||||||
|
func WithExpiredCallback(ec ExpiredCallback) ICacheOption {
|
||||||
|
return func(conf *Config) {
|
||||||
|
conf.expiredCallback = ec
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHash set custom hash key function
|
||||||
|
func WithHash(hash IHash) ICacheOption {
|
||||||
|
return func(conf *Config) {
|
||||||
|
conf.hash = hash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithClearInterval set custom clear interval.
|
||||||
|
// Interval for clearing expired key-value pairs. The default value is 1 second
|
||||||
|
// If the d is 0, the periodic clearing function is disabled
|
||||||
|
func WithClearInterval(d time.Duration) ICacheOption {
|
||||||
|
return func(conf *Config) {
|
||||||
|
conf.clearInterval = d
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
= 缓存设计
|
= 说明
|
||||||
:author: tiglog
|
:author: tiglog
|
||||||
:experimental:
|
:experimental:
|
||||||
:toc: left
|
:toc: left
|
||||||
@ -11,51 +11,18 @@
|
|||||||
:source-highlighter: rouge
|
:source-highlighter: rouge
|
||||||
:rouge-style: github
|
:rouge-style: github
|
||||||
:source-linenums-option:
|
:source-linenums-option:
|
||||||
:revdate: 2022-11-30
|
:revdate: 2023-10-11
|
||||||
:imagesdir: ./img
|
:imagesdir: ./img
|
||||||
|
|
||||||
|
|
||||||
== 实现
|
== 快速使用
|
||||||
|
|
||||||
已于 2023-06-21 实现。
|
[source,go]
|
||||||
|
|
||||||
可以基于 `redis` 进行一些基本使用。
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
== 设计
|
|
||||||
|
|
||||||
从使用倒推设计。
|
|
||||||
|
|
||||||
=== 场景1
|
|
||||||
|
|
||||||
自己管理 `key`:
|
|
||||||
|
|
||||||
[source,golang]
|
|
||||||
----
|
----
|
||||||
ck := "key_foo"
|
c := cache.NewMemCache()
|
||||||
data := cache.get(ck)
|
c.Set("a", 1)
|
||||||
if !data { // <1>
|
c.Set("b", 1, cache.WithEx(1*time.Second))
|
||||||
data = FETCH_DATA()
|
time.sleep(1*time.Second)
|
||||||
cache.set(ck, data, 7200) // <2>
|
c.Get("a") // 1, true
|
||||||
}
|
c.Get("b") // nil, false
|
||||||
return data
|
|
||||||
----
|
----
|
||||||
|
|
||||||
<1> `get` 值为 `false` 表示没有缓存或缓存已过期
|
|
||||||
<2> 7200 为缓存有效期(单位为秒),若指定为 0 表示不过期。
|
|
||||||
|
|
||||||
=== 场景2
|
|
||||||
|
|
||||||
程序自动管理 `key`:
|
|
||||||
|
|
||||||
[source,golang]
|
|
||||||
----
|
|
||||||
cache.get(func() {
|
|
||||||
return 'foo'
|
|
||||||
}, 7200)
|
|
||||||
----
|
|
||||||
|
|
||||||
这种方式一般情况下比较方便,要是需要手动使缓存失效,则要麻烦一些。因此,这种方
|
|
||||||
式暂时不实现。
|
|
||||||
|
|
||||||
|
10
gcache/redis.go
Normal file
10
gcache/redis.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
//
|
||||||
|
// redis.go
|
||||||
|
// Copyright (C) 2023 tiglog <me@tiglog.com>
|
||||||
|
//
|
||||||
|
// Distributed under terms of the MIT license.
|
||||||
|
//
|
||||||
|
|
||||||
|
package gcache
|
||||||
|
|
||||||
|
// TODO 暂时不需要,后面再实现
|
Loading…
Reference in New Issue
Block a user