package cache import ( "context" "joybuywork/internal/app" "joybuywork/internal/consts/ckey" "joybuywork/internal/plugins/jsonm" "errors" "fmt" "math/rand" "sync" "sync/atomic" "time" grds "github.com/go-redis/redis/v8" "gorm.io/gorm/schema" ) var keyPrefix = "dbcache:" var ( cacheHits atomic.Int64 cacheMisses atomic.Int64 ) func init() { machineIP := app.GetLocalIP() app.GoSafe(func() { ticker := time.NewTicker(4 * time.Hour) defer ticker.Stop() for range ticker.C { hits := cacheHits.Swap(0) misses := cacheMisses.Swap(0) total := hits + misses if total == 0 { continue } hitRate := float64(hits) / float64(total) * 100 app.FeishuNormalSend(fmt.Sprintf( "db cache hit rate in the last minute: ip=%s, rate=%.2f%% (%d/%d)", machineIP, hitRate, hits, total, )) } }) } func recordCacheResult(err error) { if err == nil { cacheHits.Add(1) return } cacheMisses.Add(1) } type Option func(cache *Cache) type Cache struct { ctx context.Context tabler schema.Tabler rdb *grds.Client } func (c *Cache) init(ctx context.Context, tabler schema.Tabler, opts ...Option) { c.ctx = ctx c.tabler = tabler c.rdb = app.Redis.Cache for _, opt := range opts { opt(c) } } func (c *Cache) getIdxInt64(key string, find func() ([]byte, error), opts ...Event) (idx int64, err error) { event := redisEvent{} for _, opt := range opts { opt(&event) } data, err := c.redis(c.ctx, key, find, event) if err != nil || len(data) == 0 { return } err = jsonm.Unmarshal(data, &idx) return } func (c *Cache) get(key string, find func() ([]byte, error), opts ...Event) (entry []byte, err error) { event := redisEvent{} for _, opt := range opts { opt(&event) } return c.redis(c.ctx, key, find, event) } func (c *Cache) getKeys(keys []string, find func(idKeys []string) (map[string][]byte, error), opts ...Event) (entrys [][]byte, err error) { event := redisEvent{} for _, opt := range opts { opt(&event) } return c.redisKeys(c.ctx, keys, find, event) } // Redis 获取数据 func (c *Cache) redis(ctx context.Context, idKey string, find func() ([]byte, error), event redisEvent) (data []byte, err error) { data, err = c.rdb.Get(ctx, idKey).Bytes() recordCacheResult(err) if err == nil || !errors.Is(err, grds.Nil) { return } //锁 idKeyLock := ckey.ForLock(idKey) lock := app.NewFollower(ctx, idKeyLock).Lock() defer lock.Release() if lock.Error() != nil { err = lock.Error() return } data, err = c.rdb.Get(ctx, idKey).Bytes() if err == nil || !errors.Is(err, grds.Nil) { return } data, err = find() if err != nil { return } // 不存数据就设置成某个数据 if data == nil && event.NilSet != nil { data, err = event.NilSet() if err != nil { return } } err = c.set(ctx, idKey, data, event) if err != nil { return nil, err } return } // Redis 获取数据 func (c *Cache) redisKeys(ctx context.Context, idKeys []string, find func(idKeys []string) (map[string][]byte, error), event redisEvent) (list [][]byte, err error) { type state struct { ok bool idKey string } wg := sync.WaitGroup{} ch := make(chan state, 1) var lcAppend = func(idKey string) (err1 error) { //锁 var one []byte idKeyLock := ckey.ForLock(idKey) lock := app.NewFollower(ctx, idKeyLock).Lock() defer lock.Release() if lock.Error() != nil { return lock.Error() } one, err1 = c.rdb.Get(ctx, idKey).Bytes() if err1 == nil { list = append(list, one) ch <- state{ ok: true, idKey: idKey, } } if !errors.Is(err1, grds.Nil) { return } err1 = nil wg.Add(1) ch <- state{ ok: false, idKey: idKey, } return } app.GoSafe(func() { var memIdKeys, aLen []string defer func() { if len(memIdKeys) == 0 { return } dm, _ := find(memIdKeys) for idKey, data := range dm { if data == nil { return } list = append(list, data) _ = c.set(ctx, idKey, data, event) } for range memIdKeys { wg.Done() } }() for { idKey, ok := <-ch if !ok { return } aLen = append(aLen, idKey.idKey) if !idKey.ok { memIdKeys = append(memIdKeys, idKey.idKey) } if len(aLen) == len(idKeys) { return } } }) for _, idKey := range idKeys { var one []byte one, err = c.rdb.Get(ctx, idKey).Bytes() recordCacheResult(err) if err == nil { list = append(list, one) ch <- state{ ok: true, idKey: idKey, } continue } if !errors.Is(err, grds.Nil) { return } err = nil key := idKey wg.Add(1) app.GoSafe(func() { defer wg.Done() _ = lcAppend(key) }) } wg.Wait() return } func (c *Cache) Prefix() string { return fmt.Sprintf("%s%s%s", ckey.ProjectPrefix, keyPrefix, c.tabler.TableName()) } func (c *Cache) TTLRand() time.Duration { return time.Duration(rand.Intn(60)+60) * time.Minute } func (c *Cache) set(ctx context.Context, key string, data []byte, event redisEvent) (err error) { if data != nil { //1小时~2小时随机 return c.rdb.Set(ctx, key, data, c.TTLRand()).Err() } // 如果是nil也存 if event.NilSetLimit > 0 { return c.rdb.Set(ctx, key, data, event.NilSetLimit).Err() } return } type LocalLogger struct { } func (l LocalLogger) Printf(format string, v ...interface{}) { app.Logger.Info(context.Background(), fmt.Sprintf(format, v...)) } const idxSplit = "||" func (c *Cache) IdxKey(values ...any) string { var suffix string var idxL = len(values) / 2 var idx int for i := 0; i < idxL; i++ { if i == 0 { suffix += fmt.Sprintf("%v_%v", values[idx], values[idx+1]) } else { suffix += fmt.Sprintf("%s%v_%v", idxSplit, values[idx], values[idx+1]) } idx += 2 } return fmt.Sprintf("%s%s%s:idx:%s", ckey.ProjectPrefix, keyPrefix, c.tabler.TableName(), suffix) }