refactor: 重构 gcache,支持 memory、redis 和 filesystem 三种 store

This commit is contained in:
tiglog 2023-10-16 20:49:23 +08:00
parent b6b0a7c0ca
commit 8b29ccee17
14 changed files with 704 additions and 549 deletions

18
gcache/cache.go Normal file
View File

@ -0,0 +1,18 @@
//
// cache.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
import "time"
type ICache interface {
Set(key string, data []byte, expires time.Duration) error
Get(key string) ([]byte, error)
Delete(key string) error
Flush() error
Has(key string) bool
}

51
gcache/cache_test.go Normal file
View File

@ -0,0 +1,51 @@
//
// cache_test.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache_test
import (
"os"
"testing"
"time"
"git.hexq.cn/tiglog/golib/gcache"
"git.hexq.cn/tiglog/golib/gtest"
"github.com/redis/go-redis/v9"
)
var sample_name = "golib gcache"
func TestRedisCache(t *testing.T) {
REDIS_URL := os.Getenv("REDIS_URL")
// fmt.Println(REDIS_URL)
redisOpt, err := redis.ParseURL(REDIS_URL)
gtest.Nil(t, err)
var store gcache.ICache = gcache.NewRedisStore(gcache.RCClientOptions(redisOpt))
var sampleData = []byte(sample_name)
err = store.Set("name", sampleData, time.Minute*1)
gtest.Nil(t, err)
r1, err := store.Get("name")
gtest.Nil(t, err)
gtest.Equal(t, string(r1), sample_name)
}
func TestMemoryCache(t *testing.T) {
store := gcache.NewMemoryStore()
var err error
var sampleData = []byte(sample_name)
err = store.Set("name", sampleData, time.Minute*1)
gtest.Nil(t, err)
r1, err := store.Get("name")
gtest.Nil(t, err)
gtest.Equal(t, string(r1), sample_name)
}

View File

@ -1,21 +0,0 @@
//
// 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}
}

214
gcache/fs_adapter.go Normal file
View File

@ -0,0 +1,214 @@
//
// fs_adapter.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
import (
"bytes"
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"syscall"
"time"
)
// Option is an optional type
type FCOption func(fs *FSStore)
func BaseDirectory(base string) FCOption {
return func(fs *FSStore) {
fs.baseDir = base
}
}
func FCSerializer(serializer Serializer) FCOption {
return func(fs *FSStore) {
fs.b = serializer
}
}
func FCCacheKeyGenerator(fn KeyFunc) FCOption {
return func(fs *FSStore) {
fs.keyFn = fn
}
}
const (
defaultFilePerm os.FileMode = 0644
defaultDirectoryFilePerm = 0755
)
func FilePathKeyFunc(s string) string {
hashSum := md5.Sum([]byte(s))
hashSumAsString := hex.EncodeToString(hashSum[:])
return filepath.Join(string(hashSumAsString[0:2]),
string(hashSumAsString[2:4]),
string(hashSumAsString[4:6]), hashSumAsString)
}
func createDirectory(dir string) error {
return os.MkdirAll(dir, defaultDirectoryFilePerm)
}
type FSStore struct {
baseDir string
b Serializer
keyFn KeyFunc
}
func NewFSStore(baseDir string, opts ...FCOption) (*FSStore, error) {
_, err := os.Stat(baseDir)
if err != nil { //Directory does not exist..Let's create it
if err := createDirectory(baseDir); err != nil {
panic(fmt.Errorf("Base directory could not be created : %s", err))
}
}
opts = append(opts, BaseDirectory(baseDir))
store := &FSStore{}
for _, opt := range opts {
opt(store)
}
if store.b == nil {
store.b = NewCacheSerializer()
}
if len(strings.TrimSpace(store.baseDir)) == 0 {
return nil, errors.New("onecache : base directory not provided")
}
if store.keyFn == nil {
store.keyFn = FilePathKeyFunc
}
return store, nil
}
func (fs *FSStore) Set(key string, data []byte, expiresAt time.Duration) error {
path := fs.filePathFor(key)
if err := createDirectory(filepath.Dir(path)); err != nil {
return err
}
i := &Item{ExpiresAt: time.Now().Add(expiresAt), Data: data}
b, err := fs.b.Serialize(i)
if err != nil {
return err
}
return writeFile(path, b)
}
func (fs *FSStore) Get(key string) ([]byte, error) {
var b = new(bytes.Buffer)
f, err := os.OpenFile(fs.filePathFor(key), os.O_RDONLY, 0644)
if err != nil {
pe, ok := err.(*os.PathError)
if !ok {
return nil, err
}
if pe.Err == syscall.ENOENT && pe.Op == "open" {
return nil, ErrCacheMiss
}
return nil, err
}
if _, err := io.Copy(b, f); err != nil {
f.Close()
return nil, err
}
f.Close()
i := new(Item)
if err := fs.b.DeSerialize(b.Bytes(), i); err != nil {
return nil, err
}
if i.IsExpired() {
fs.Delete(key)
return nil, ErrCacheMiss
}
return i.Data, nil
}
func (fs *FSStore) Delete(key string) error {
return os.RemoveAll(fs.filePathFor(key))
}
func (fs *FSStore) Flush() error {
return os.RemoveAll(fs.baseDir)
}
func (fs *FSStore) GC() {
filepath.Walk(
fs.baseDir,
func(path string, finfo os.FileInfo, err error) error {
if err != nil {
return err
}
if finfo.IsDir() {
return nil
}
currentItem := new(Item)
byt, err := os.ReadFile(path)
if err != nil {
return err
}
if err = fs.b.DeSerialize(byt, currentItem); err != nil {
return err
}
if currentItem.IsExpired() {
if err := os.Remove(path); !os.IsExist(err) {
return err
}
}
return nil
})
}
func (fs *FSStore) Has(key string) bool {
_, err := os.Stat(fs.filePathFor(key))
return !os.IsNotExist(err)
}
func (fs *FSStore) filePathFor(key string) string {
return filepath.Join(fs.baseDir, fs.keyFn(key))
}
func writeFile(path string, b []byte) error {
return os.WriteFile(path, b, defaultFilePerm)
}

View File

@ -1,42 +0,0 @@
//
// 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/FowlerNollVo_hash_function
func newDefaultHash() IHash {
return fnv64a{}
}
type fnv64a struct{}
const (
// offset64 FNVa offset basis. See https://en.wikipedia.org/wiki/FowlerNollVo_hash_function#FNV-1a_hash
offset64 = 14695981039346656037
// prime64 FNVa prime value. See https://en.wikipedia.org/wiki/FowlerNollVo_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
}

View File

@ -1,122 +0,0 @@
//
// 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
}

View File

@ -1,255 +0,0 @@
//
// 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)
}
}

155
gcache/memory_adapter.go Normal file
View File

@ -0,0 +1,155 @@
//
// memory_adapter.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
import (
"sync"
"time"
)
// Option defines options for creating a memory store
type MCOption func(i *InMemoryStore)
// BufferSize configures the store to allow a maximum of n
func BufferSize(n int) MCOption {
return func(i *InMemoryStore) {
i.bufferSize = n
}
}
// KeyFunc allows for dynamic generation of cache keys
func MCCacheKeyGenerator(fn KeyFunc) MCOption {
return func(i *InMemoryStore) {
i.keyfn = fn
}
}
// New returns a configured in memory store
func NewMemoryStore(opts ...MCOption) *InMemoryStore {
i := &InMemoryStore{}
for _, opt := range opts {
opt(i)
}
if i.keyfn == nil {
i.keyfn = DefaultKeyFunc
}
if i.data == nil {
if i.bufferSize == 0 {
i.bufferSize = 100
}
i.data = make(map[string]*Item, i.bufferSize)
}
return i
}
// Represents an in-memory store
type InMemoryStore struct {
lock sync.RWMutex
data map[string]*Item
bufferSize int
keyfn KeyFunc
}
func (i *InMemoryStore) Set(key string, data []byte, expires time.Duration) error {
i.lock.Lock()
i.data[i.keyfn(key)] = &Item{
ExpiresAt: time.Now().Add(expires),
Data: copyData(data),
}
i.lock.Unlock()
return nil
}
func (i *InMemoryStore) Get(key string) ([]byte, error) {
i.lock.RLock()
item := i.data[i.keyfn(key)]
if item == nil {
i.lock.RUnlock()
return nil, ErrCacheMiss
}
if item.IsExpired() {
i.lock.RUnlock()
i.Delete(key)
return nil, ErrCacheMiss
}
i.lock.RUnlock()
return copyData(item.Data), nil
}
func (i *InMemoryStore) Delete(key string) error {
i.lock.RLock()
_, ok := i.data[i.keyfn(key)]
if !ok {
i.lock.RUnlock()
return ErrCacheMiss
}
i.lock.RUnlock()
i.lock.Lock()
delete(i.data, i.keyfn(key))
i.lock.Unlock()
return nil
}
func (i *InMemoryStore) Flush() error {
i.lock.Lock()
i.data = make(map[string]*Item, i.bufferSize)
i.lock.Unlock()
return nil
}
func (i *InMemoryStore) Has(key string) bool {
i.lock.RLock()
_, ok := i.data[i.keyfn(key)]
i.lock.RUnlock()
return ok
}
func (i *InMemoryStore) GC() {
i.lock.Lock()
for k, item := range i.data {
if item.IsExpired() {
//No need to spawn a new goroutine since we
//still have the lock here
delete(i.data, k)
}
}
i.lock.Unlock()
}
func (i *InMemoryStore) count() int {
i.lock.Lock()
n := len(i.data)
i.lock.Unlock()
return n
}
func copyData(data []byte) []byte {
result := make([]byte, len(data))
copy(result, data)
return result
}

View File

@ -1,31 +0,0 @@
//
// 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)
}

View File

@ -1,68 +0,0 @@
//
// 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
}
}

View File

@ -1,10 +0,0 @@
//
// redis.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
// TODO 暂时不需要,后面再实现

87
gcache/redis_adapter.go Normal file
View File

@ -0,0 +1,87 @@
//
// redis_adapter.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
import (
"context"
"time"
"github.com/redis/go-redis/v9"
)
// Option is a redis option type
type RCOption func(r *RedisStore)
// ClientOptions is an Option type that allows configuring a redis client
func RCClientOptions(opts *redis.Options) RCOption {
return func(r *RedisStore) {
r.client = redis.NewClient(opts)
}
}
// CacheKeyGenerator allows configuring the cache key generation process
func RCCacheKeyGenerator(fn KeyFunc) RCOption {
return func(r *RedisStore) {
r.keyFn = fn
}
}
type RedisStore struct {
client *redis.Client
keyFn KeyFunc
}
// New returns a new RedisStore by applying all options passed into it
// It also sets sensible defaults too
func NewRedisStore(opts ...RCOption) *RedisStore {
r := &RedisStore{}
for _, opt := range opts {
opt(r)
}
if r.client == nil {
panic("需要指定 redis 的连接参数")
}
if r.keyFn == nil {
r.keyFn = DefaultKeyFunc
}
return r
}
func (r *RedisStore) Set(k string, data []byte, expires time.Duration) error {
cmd := r.client.Set(context.Background(), r.key(k), data, expires)
return cmd.Err()
}
func (r *RedisStore) Get(key string) ([]byte, error) {
return r.client.Get(context.Background(), r.key(key)).Bytes()
}
func (r *RedisStore) Delete(key string) error {
return r.client.Del(context.Background(), r.key(key)).Err()
}
func (r *RedisStore) Flush() error {
return r.client.FlushDB(context.Background()).Err()
}
func (r *RedisStore) Has(key string) bool {
if _, err := r.Get(key); err != nil {
return false
}
return true
}
func (r *RedisStore) key(k string) string {
return r.keyFn(k)
}

59
gcache/types.go Normal file
View File

@ -0,0 +1,59 @@
//
// tyeps.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
import (
"errors"
"time"
)
const (
EXPIRES_DEFAULT = time.Duration(0)
EXPIRES_FOREVER = time.Duration(-1)
)
var (
ErrCacheMiss = errors.New("Key not found")
ErrCacheNotStored = errors.New("Data not stored")
ErrCacheNotSupported = errors.New("Operation not supported")
ErrCacheDataCannotBeIncreasedOrDecreased = errors.New(`
Data isn't an integer/string type. Hence, it cannot be increased or decreased`)
)
// DefaultKeyFunc is the default implementation of cache keys
// All it does is to preprend "onecache:" to the key sent in by client code
func DefaultKeyFunc(s string) string {
return "gcache:" + s
}
// Item identifes a cached piece of data
type Item struct {
ExpiresAt time.Time
Data []byte
}
// Interface for all onecache store implementations
type Store interface {
Set(key string, data []byte, expires time.Duration) error
Get(key string) ([]byte, error)
Delete(key string) error
Flush() error
Has(key string) bool
}
// Some stores like redis and memcache automatically clear out the cache
// But for the filesystem and in memory, this cannot be said.
// Stores that have to manually clear out the cached data should implement this method.
// It's implementation should re run this function everytime the interval is reached
// Say every 5 minutes.
type GarbageCollector interface {
GC()
}
// KeyFunc defines a transformer for cache keys
type KeyFunc func(s string) string

120
gcache/utils.go Normal file
View File

@ -0,0 +1,120 @@
//
// utils.go
// Copyright (C) 2023 tiglog <me@tiglog.com>
//
// Distributed under terms of the MIT license.
//
package gcache
import (
"bytes"
"encoding/gob"
"strconv"
"time"
)
// Helper method to check if an item is expired.
// Current usecase for this is for garbage collection
func (i *Item) IsExpired() bool {
return time.Now().After(i.ExpiresAt)
}
type Serializer interface {
Serialize(i interface{}) ([]byte, error)
DeSerialize(data []byte, i interface{}) error
}
func NewCacheSerializer() *CacheSerializer {
return &CacheSerializer{}
}
// Helper to serialize and deserialize types
type CacheSerializer struct {
}
// Convert a given type into a byte array
// Caveat -> Types you create might have to be registered with the encoding/gob package
func (b *CacheSerializer) Serialize(i interface{}) ([]byte, error) {
if b, ok := i.([]byte); ok {
return b, nil
}
var buf bytes.Buffer
enc := gob.NewEncoder(&buf)
if err := enc.Encode(i); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// Writes a byte array into a type.
func (b *CacheSerializer) DeSerialize(data []byte, i interface{}) error {
if b, ok := i.(*[]byte); ok {
*b = data
return nil
}
return gob.NewDecoder(bytes.NewBuffer(data)).Decode(i)
}
// Increment increases the value of an item by the specified number of steps
func Increment(val interface{}, steps int) (interface{}, error) {
var ret interface{}
switch val.(type) {
case int:
ret = val.(int) + steps
case int32:
ret = val.(int32) + int32(steps)
case int64:
ret = val.(int64) + int64(steps)
case uint:
ret = val.(uint) + uint(steps)
case uint8:
ret = val.(uint8) + uint8(steps)
case uint16:
ret = val.(uint16) + uint16(steps)
case uint32:
ret = val.(uint32) + uint32(steps)
case uint64:
ret = val.(uint64) + uint64(steps)
case string:
num, err := strconv.Atoi(val.(string))
if err != nil {
return -0, err
}
num += steps
ret = strconv.Itoa(num)
default:
return -0, ErrCacheDataCannotBeIncreasedOrDecreased
}
return ret, nil
}
// Decrement decreases the value of an item by the specified number of steps
func Decrement(val interface{}, steps int) (interface{}, error) {
return Increment(val, steps*-1)
}