github.com/binbinly/pkg@v0.0.11-0.20240321014439-f4fbf666eb0f/repo/repo.go (about) 1 package repo 2 3 import ( 4 "context" 5 "reflect" 6 "time" 7 8 "github.com/binbinly/pkg/cache" 9 "github.com/binbinly/pkg/logger" 10 "github.com/pkg/errors" 11 "github.com/redis/go-redis/v9" 12 "golang.org/x/sync/singleflight" 13 "gorm.io/gorm" 14 ) 15 16 var g singleflight.Group 17 18 // Repo struct 19 type Repo struct { 20 Cache cache.Cache 21 } 22 23 // GetCache 获取 cache 24 func (r *Repo) GetCache() cache.Cache { 25 return r.Cache 26 } 27 28 // QueryCache 查询启用缓存 29 // 缓存的更新策略使用 Cache Aside Pattern 30 // see: https://coolshell.cn/articles/17416.html 31 func (r *Repo) QueryCache(ctx context.Context, key string, data any, ttl time.Duration, query func(any) error) (err error) { 32 // 从cache获取 33 err = r.Cache.Get(ctx, key, data) 34 if errors.Is(err, cache.ErrPlaceholder) { 35 // 空数据也需要返回空的数据结构,保持与gorm返回一直的结构 see gorm.first() 36 reflectValue := reflect.ValueOf(data) 37 for reflectValue.Kind() == reflect.Ptr { 38 if reflectValue.IsNil() && reflectValue.CanAddr() { 39 reflectValue.Set(reflect.New(reflectValue.Type().Elem())) 40 } 41 42 reflectValue = reflectValue.Elem() 43 } 44 switch reflectValue.Kind() { 45 case reflect.Slice, reflect.Array: 46 if reflectValue.Len() == 0 && reflectValue.Cap() == 0 { 47 // if the slice cap is externally initialized, the externally initialized slice is directly used here 48 reflectValue.Set(reflect.MakeSlice(reflectValue.Type(), 0, 20)) 49 } 50 } 51 logger.Debugf("[repo] key %v is empty", key) 52 return nil 53 } else if err != nil && err != redis.Nil { 54 return errors.Wrapf(err, "[repo] get cache by key: %s", key) 55 } 56 57 // 检查缓存取出的数据是否为空,不为空说明已经从缓存中取到了数据,直接返回 58 if elem := reflect.ValueOf(data).Elem(); !elem.IsNil() { 59 logger.Debugf("[repo] get from obj cache, key: %v, kind:%v", key, elem.Kind()) 60 return 61 } 62 63 // use sync/singleflight mode to get data 64 // why not use redis lock? see this topic: https://redis.io/topics/distlock 65 // demo see: https://github.com/go-demo/singleflight-demo/blob/master/main.go 66 // https://juejin.cn/post/6844904084445593613 67 _, err, _ = g.Do(key, func() (any, error) { 68 // 从数据库中获取 69 err = query(data) 70 // if data is empty, set not found cache to prevent cache penetration(缓存穿透) 71 if errors.Is(err, gorm.ErrRecordNotFound) || errors.Is(err, gorm.ErrEmptySlice) { 72 if err = r.Cache.SetCacheWithNotFound(ctx, key); err != nil { 73 logger.Warnf("[repo] SetCacheWithNotFound err, key: %s", key) 74 } 75 return data, nil 76 } else if err != nil { 77 return nil, errors.Wrapf(err, "[repo] query db") 78 } 79 80 // set cache 81 if err = r.Cache.Set(ctx, key, data, ttl); err != nil { 82 return nil, errors.Wrapf(err, "[repo] set data to cache key: %s", key) 83 } 84 return data, nil 85 }) 86 if err != nil { 87 return errors.Wrapf(err, "[repo] get err via single flight do key: %s", key) 88 } 89 90 return nil 91 } 92 93 // DelCache 删除缓存 94 func (r *Repo) DelCache(ctx context.Context, key string) { 95 if err := r.Cache.Del(ctx, key); err != nil { 96 logger.Warnf("[repo] del cache key: %v", key) 97 } 98 }