commit d91b8044113449b2a50da5dbbb2c04ad4438c614 Author: bryanqiu Date: Thu Apr 6 16:57:14 2023 +0800 first upload diff --git a/cache.go b/cache.go new file mode 100644 index 0000000..01c2194 --- /dev/null +++ b/cache.go @@ -0,0 +1,228 @@ +package cache + +import ( + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/gomodule/redigo/redis" +) + +var ( + ErrNotFound = errors.New("not found") +) + +type Cacher[T any] interface { + GetFromCache(id string) (dat *T, err error) + SetIntoCache(id string, dat *T) (err error) +} + +type Getter[T any] interface { + GetById(id string) (dat *T, err error) +} + +type Config struct { + UseLocalCache bool + LocalCacheLifetimeSecond int64 + UseRedisCache bool + RedisCacheConn redis.Conn + RedisCacheKeyPrefix string + RedisCacheLifetimeSecond int64 +} + +func NewCache[T any](getter Getter[T], cfg Config) Cacher[T] { + var c = new(cacher[T]) + // Getter + c.getter = getter + + // Local cache + if cfg.UseLocalCache { + c.localCache = new(localCacher[T]) + c.localCache.cacheItems = map[string]*cacheItem[T]{} + c.localCache.cacheSecond = time.Duration(cfg.LocalCacheLifetimeSecond) * time.Second + if c.localCache.cacheSecond.Nanoseconds() <= 0 { + c.localCache.cacheSecond = 60 * time.Second + } + } + + // Redis cache + if cfg.UseRedisCache && cfg.RedisCacheConn != nil { + c.redisCache = new(redisCacher[T]) + c.redisCache.rds = cfg.RedisCacheConn + c.redisCache.cachePrefix = cfg.RedisCacheKeyPrefix + c.redisCache.cacheSecond = time.Duration(cfg.RedisCacheLifetimeSecond) * time.Second + } else if cfg.UseRedisCache { + panic("want to user redis cache, but redis.Conn is nil") + } + + return c +} + +type cacheItem[T any] struct { + Data T //cache data + CreateTime time.Time //cache create time +} + +type localCacher[T any] struct { + cacheItems map[string]*cacheItem[T] + cacheSecond time.Duration +} + +type redisCacher struct { + rds redis.Conn + cachePrefix string + cacheSecond time.Duration +} + +type cacher[T any] struct { + getter Getter[T] + localCache *localCacher[T] + redisCache *redisCacher[T] +} + +func (c *localCacher) GetFromCache(id string) (t *T, err error) { + var earliestCreateTime = time.Now().Add(-c.cacheSecond) + if a, ok := c.cacheItems[appid]; !ok { + return nil, ErrAppNotExist + } else if a.CreateTime.Before(earliestCreateTime) { + return a, nil + } else if ok { + delete(c.apps, appid) + return nil, ErrAppNotExist + } +} + +func (c *localCacher) SetIntoCache(id string, t *T) (err error) { + c.apps[id] = t + return nil +} + +func (c *redisCacher) GetFromCache(id string) (*T, error) { + var ( + rds = c.rds + redisKey = fmt.Sprintf("appconfig-by-appid:%s", appid) + redisValue = "" + err error = nil + ) + + // Step 1. read from redis + if redisValue, err = redis.String(rds.Do("GET", redisKey)); err != nil { + log.Infof("get appconfig cache failed: 'redis return<%s>'", err.Error()) + return nil, ErrAppNotExist + } + + // Step 2. decode from string + a := &App{} + if err = json.Unmarshal([]byte(redisValue), a); err != nil { + log.Errorf("get appconfig cache failed: 'json unmarshal failed <%s>'", err.Error()) + return nil, ErrAppNotExist + } + + // Step 3. check expire time + var d = c.cacheSecond + if d.Nanoseconds() > 0 { + var earliestCreateTime = time.Now().Add(-c.cacheSecond) + if a.CreateTime.Before(earliestCreateTime) { + log.Infof("app(%s) is in redis cache but expired", appid) + return nil, ErrAppNotExist + } + } + + return a, nil +} + +func (c *redisCacher) SetIntoCache(appid string, a *App) error { + var ( + rds = c.rds + redisKey = fmt.Sprintf("appconfig-by-appid:%s", appid) + redisValue = []byte{} + err error = nil + ) + + // Step 1. decode from string + if redisValue, err = json.Marshal(a); err != nil { + log.Errorf("set appconfig cache failed: 'json marshal failed <%s>'", err.Error()) + return errors.New("set appconfig cache failed: 'marshal failed'") + } + + // Step 2. read from redis + if _, err = rds.Do("SET", redisKey, string(redisValue)); err != nil { + log.Errorf("set appconfig cache failed: 'redis failed <%s>'", err.Error()) + return errors.New("set appconfig cache failed: 'redis failed'") + } + + return nil +} + +func (c *cacher[T]) GetFromCache(id string) (dat *T, err error) { + // Step 1. check id + if len(id) < 8 { + return nil, ErrNotFound + } + + // Step 2. get from [local] + if c.localCache != nil { + if dat, err := c.localCache.GetFromCache(id); err == nil { + log.Infof("get cache(id:%s) from localCacher success", id) + return dat, nil + } else { + log.Infof("get cache(id:%s) from localCacher failed, try next", id) + } + } + + // Step 3. get from [redis] + if c.redisCache != nil { + if dat, err := c.redisCache.GetFromCache(id); err == nil { + log.Infof("get cache(id:%s) from redisCacher success", id) + return dat, nil + } else if c.getter == nil { + log.Warningf("get cache(id:%s) from local failed, and storager is nil, "+ + "trade as not found. may be you forgot set in db ???", id) + return nil, ErrAppNotExist + } + } + if c.getter == nil { + log.Warninf("get cache(id:%s) from all cache failed, and storager is nil, "+ + "trade as not found. may be you forgot set in db ???", id) + return nil, ErrAppNotExist + } + + // Step 4. get from [storager(database or somewhere)] + dat, err := c.getter.GetAppId(id) + if err != nil { + log.Errorf("cache(id:%s) is not in database, something error: %s", id, err) + return nil, ErrAppNotExist + } + dat.CreateTime = time.Now() + log.Infof("get cache(id:%s) from exportStorager(maybe database) success", id) + + // Step 5. set to cacha + if c.localCache != nil { + c.localCache.SetIntoCache(id, dat) + } + if c.redisCache != nil { + c.redisCache.SetIntoCache(id, dat) + } + + // Step 6. return + log.PrintPretty("success cached/return app:", dat) + return dat, nil +} + +func (c *cacher[T]) SetIntoCache(id string, dat *T) (err error) { + // localCache + if c.localCache != nil { + if err = c.localCache.SetIntoCache(id, dat); err != nil { + log.Infof("set data into localCache failed, err:%s", err) + } + } + + // redisCache + if c.redisCache != nil { + if err = c.redisCache.SetIntoCache(id, dat); err != nil { + log.Infof("set data into redisCache failed, err:%s", err) + } + } + return nil +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..29cc0e4 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module qoobing.com/gomod/cache + +go 1.19