package cache import ( "encoding/json" "errors" "fmt" "time" "github.com/gomodule/redigo/redis" "qoobing.com/gomod/log" ) 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.cacheSecond = time.Duration(cfg.RedisCacheLifetimeSecond) * time.Second if cfg.RedisCacheKeyPrefix == "" { panic("redis cache's key prefix must not be null") } c.redisCache.cachePrefix = fmt.Sprintf("%s-cache-id:", cfg.RedisCacheKeyPrefix) } else if cfg.UseRedisCache { panic("want to user redis cache, but redis.Conn is nil") } return c } type cacheItem[T any] struct { Data *T `json:"d"` //cache data CreateTime time.Time `json:"t"` //cache create time } type localCacher[T any] struct { cacheItems map[string]*cacheItem[T] cacheSecond time.Duration } type redisCacher[T any] 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[T]) GetFromCache(id string) (t *T, err error) { var earliestCreateTime = time.Now().Add(-c.cacheSecond) if a, ok := c.cacheItems[id]; !ok { return nil, ErrNotFound } else if earliestCreateTime.Before(a.CreateTime) { return a.Data, nil } //TODO: cocurrent delete(c.cacheItems, id) return nil, ErrNotFound } func (c *localCacher[T]) SetIntoCache(id string, t *T) (err error) { c.cacheItems[id] = &cacheItem[T]{ Data: t, CreateTime: time.Now(), } fmt.Println("cacheItems", c.cacheItems) return nil } func (c *redisCacher[T]) GetFromCache(id string) (*T, error) { var ( rds = c.rds redisKey = c.cachePrefix + id redisValue = "" err error = nil ) // Step 1. read from redis if redisValue, err = redis.String(rds.Do("GET", redisKey)); err != nil { log.Infof("get cache failed: 'redis return<%s>'", err.Error()) return nil, ErrNotFound } // Step 2. decode from string a := &cacheItem[T]{} if err = json.Unmarshal([]byte(redisValue), a); err != nil { log.Errorf("get cache failed: 'json unmarshal failed <%s>'", err.Error()) return nil, ErrNotFound } // 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", id) return nil, ErrNotFound } } return a.Data, nil } func (c *redisCacher[T]) SetIntoCache(id string, t *T) error { var ( rds = c.rds redisKey = c.cachePrefix + id redisValue = []byte{} err error = nil ) // Step 1. decode from string var a = cacheItem[T]{Data: t, CreateTime: time.Now()} if redisValue, err = json.Marshal(a); err != nil { log.Errorf("set cache failed: 'json marshal failed <%s>'", err.Error()) return errors.New("set cache failed: 'marshal failed'") } // Step 2. read from redis if _, err = rds.Do("SET", redisKey, string(redisValue)); err != nil { log.Errorf("set cache failed: 'redis failed <%s>'", err.Error()) return errors.New("set 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 { if c.localCache != nil { log.Infof("set cache(id:%s) to localCache by redisCacher") c.localCache.SetIntoCache(id, dat) } 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 all cache failed, and storager is nil, "+ "trade as not found. may be you forgot set in db ???", id) return nil, ErrNotFound } } if c.getter == nil { log.Warningf("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, ErrNotFound } // Step 4. get from [storager(database or somewhere)] dat, err = c.getter.GetById(id) if err != nil { log.Errorf("cache(id:%s) is not in database, something error: %s", id, err) return nil, ErrNotFound } 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) } else { log.Debugf("success set data into localCache, id: %s", id) } } // redisCache if c.redisCache != nil { if err = c.redisCache.SetIntoCache(id, dat); err != nil { log.Infof("set data into redisCache failed, err:%s", err) } else { log.Debugf("success set data into redisCache, id: %s", id) } } return nil }