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 }