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  }