github.com/lingyao2333/mo-zero@v1.4.1/core/stores/cache/cachenode.go (about)

     1  package cache
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"math"
     8  	"math/rand"
     9  	"sync"
    10  	"time"
    11  
    12  	"github.com/lingyao2333/mo-zero/core/jsonx"
    13  	"github.com/lingyao2333/mo-zero/core/logx"
    14  	"github.com/lingyao2333/mo-zero/core/mathx"
    15  	"github.com/lingyao2333/mo-zero/core/stat"
    16  	"github.com/lingyao2333/mo-zero/core/stores/redis"
    17  	"github.com/lingyao2333/mo-zero/core/syncx"
    18  )
    19  
    20  const (
    21  	notFoundPlaceholder = "*"
    22  	// make the expiry unstable to avoid lots of cached items expire at the same time
    23  	// make the unstable expiry to be [0.95, 1.05] * seconds
    24  	expiryDeviation = 0.05
    25  )
    26  
    27  // indicates there is no such value associate with the key
    28  var errPlaceholder = errors.New("placeholder")
    29  
    30  type cacheNode struct {
    31  	rds            *redis.Redis
    32  	expiry         time.Duration
    33  	notFoundExpiry time.Duration
    34  	barrier        syncx.SingleFlight
    35  	r              *rand.Rand
    36  	lock           *sync.Mutex
    37  	unstableExpiry mathx.Unstable
    38  	stat           *Stat
    39  	errNotFound    error
    40  }
    41  
    42  // NewNode returns a cacheNode.
    43  // rds is the underlying redis node or cluster.
    44  // barrier is the barrier that maybe shared with other cache nodes on cache cluster.
    45  // st is used to stat the cache.
    46  // errNotFound defines the error that returned on cache not found.
    47  // opts are the options that customize the cacheNode.
    48  func NewNode(rds *redis.Redis, barrier syncx.SingleFlight, st *Stat,
    49  	errNotFound error, opts ...Option) Cache {
    50  	o := newOptions(opts...)
    51  	return cacheNode{
    52  		rds:            rds,
    53  		expiry:         o.Expiry,
    54  		notFoundExpiry: o.NotFoundExpiry,
    55  		barrier:        barrier,
    56  		r:              rand.New(rand.NewSource(time.Now().UnixNano())),
    57  		lock:           new(sync.Mutex),
    58  		unstableExpiry: mathx.NewUnstable(expiryDeviation),
    59  		stat:           st,
    60  		errNotFound:    errNotFound,
    61  	}
    62  }
    63  
    64  // Del deletes cached values with keys.
    65  func (c cacheNode) Del(keys ...string) error {
    66  	return c.DelCtx(context.Background(), keys...)
    67  }
    68  
    69  // DelCtx deletes cached values with keys.
    70  func (c cacheNode) DelCtx(ctx context.Context, keys ...string) error {
    71  	if len(keys) == 0 {
    72  		return nil
    73  	}
    74  
    75  	logger := logx.WithContext(ctx)
    76  	if len(keys) > 1 && c.rds.Type == redis.ClusterType {
    77  		for _, key := range keys {
    78  			if _, err := c.rds.DelCtx(ctx, key); err != nil {
    79  				logger.Errorf("failed to clear cache with key: %q, error: %v", key, err)
    80  				c.asyncRetryDelCache(key)
    81  			}
    82  		}
    83  	} else if _, err := c.rds.DelCtx(ctx, keys...); err != nil {
    84  		logger.Errorf("failed to clear cache with keys: %q, error: %v", formatKeys(keys), err)
    85  		c.asyncRetryDelCache(keys...)
    86  	}
    87  
    88  	return nil
    89  }
    90  
    91  // Get gets the cache with key and fills into v.
    92  func (c cacheNode) Get(key string, val interface{}) error {
    93  	return c.GetCtx(context.Background(), key, val)
    94  }
    95  
    96  // GetCtx gets the cache with key and fills into v.
    97  func (c cacheNode) GetCtx(ctx context.Context, key string, val interface{}) error {
    98  	err := c.doGetCache(ctx, key, val)
    99  	if err == errPlaceholder {
   100  		return c.errNotFound
   101  	}
   102  
   103  	return err
   104  }
   105  
   106  // IsNotFound checks if the given error is the defined errNotFound.
   107  func (c cacheNode) IsNotFound(err error) bool {
   108  	return errors.Is(err, c.errNotFound)
   109  }
   110  
   111  // Set sets the cache with key and v, using c.expiry.
   112  func (c cacheNode) Set(key string, val interface{}) error {
   113  	return c.SetCtx(context.Background(), key, val)
   114  }
   115  
   116  // SetCtx sets the cache with key and v, using c.expiry.
   117  func (c cacheNode) SetCtx(ctx context.Context, key string, val interface{}) error {
   118  	return c.SetWithExpireCtx(ctx, key, val, c.aroundDuration(c.expiry))
   119  }
   120  
   121  // SetWithExpire sets the cache with key and v, using given expire.
   122  func (c cacheNode) SetWithExpire(key string, val interface{}, expire time.Duration) error {
   123  	return c.SetWithExpireCtx(context.Background(), key, val, expire)
   124  }
   125  
   126  // SetWithExpireCtx sets the cache with key and v, using given expire.
   127  func (c cacheNode) SetWithExpireCtx(ctx context.Context, key string, val interface{},
   128  	expire time.Duration) error {
   129  	data, err := jsonx.Marshal(val)
   130  	if err != nil {
   131  		return err
   132  	}
   133  
   134  	return c.rds.SetexCtx(ctx, key, string(data), int(math.Ceil(expire.Seconds())))
   135  }
   136  
   137  // String returns a string that represents the cacheNode.
   138  func (c cacheNode) String() string {
   139  	return c.rds.Addr
   140  }
   141  
   142  // Take takes the result from cache first, if not found,
   143  // query from DB and set cache using c.expiry, then return the result.
   144  func (c cacheNode) Take(val interface{}, key string, query func(val interface{}) error) error {
   145  	return c.TakeCtx(context.Background(), val, key, query)
   146  }
   147  
   148  // TakeCtx takes the result from cache first, if not found,
   149  // query from DB and set cache using c.expiry, then return the result.
   150  func (c cacheNode) TakeCtx(ctx context.Context, val interface{}, key string,
   151  	query func(val interface{}) error) error {
   152  	return c.doTake(ctx, val, key, query, func(v interface{}) error {
   153  		return c.SetCtx(ctx, key, v)
   154  	})
   155  }
   156  
   157  // TakeWithExpire takes the result from cache first, if not found,
   158  // query from DB and set cache using given expire, then return the result.
   159  func (c cacheNode) TakeWithExpire(val interface{}, key string, query func(val interface{},
   160  	expire time.Duration) error) error {
   161  	return c.TakeWithExpireCtx(context.Background(), val, key, query)
   162  }
   163  
   164  // TakeWithExpireCtx takes the result from cache first, if not found,
   165  // query from DB and set cache using given expire, then return the result.
   166  func (c cacheNode) TakeWithExpireCtx(ctx context.Context, val interface{}, key string,
   167  	query func(val interface{}, expire time.Duration) error) error {
   168  	expire := c.aroundDuration(c.expiry)
   169  	return c.doTake(ctx, val, key, func(v interface{}) error {
   170  		return query(v, expire)
   171  	}, func(v interface{}) error {
   172  		return c.SetWithExpireCtx(ctx, key, v, expire)
   173  	})
   174  }
   175  
   176  func (c cacheNode) aroundDuration(duration time.Duration) time.Duration {
   177  	return c.unstableExpiry.AroundDuration(duration)
   178  }
   179  
   180  func (c cacheNode) asyncRetryDelCache(keys ...string) {
   181  	AddCleanTask(func() error {
   182  		_, err := c.rds.Del(keys...)
   183  		return err
   184  	}, keys...)
   185  }
   186  
   187  func (c cacheNode) doGetCache(ctx context.Context, key string, v interface{}) error {
   188  	c.stat.IncrementTotal()
   189  	data, err := c.rds.GetCtx(ctx, key)
   190  	if err != nil {
   191  		c.stat.IncrementMiss()
   192  		return err
   193  	}
   194  
   195  	if len(data) == 0 {
   196  		c.stat.IncrementMiss()
   197  		return c.errNotFound
   198  	}
   199  
   200  	c.stat.IncrementHit()
   201  	if data == notFoundPlaceholder {
   202  		return errPlaceholder
   203  	}
   204  
   205  	return c.processCache(ctx, key, data, v)
   206  }
   207  
   208  func (c cacheNode) doTake(ctx context.Context, v interface{}, key string,
   209  	query func(v interface{}) error, cacheVal func(v interface{}) error) error {
   210  	logger := logx.WithContext(ctx)
   211  	val, fresh, err := c.barrier.DoEx(key, func() (interface{}, error) {
   212  		if err := c.doGetCache(ctx, key, v); err != nil {
   213  			if err == errPlaceholder {
   214  				return nil, c.errNotFound
   215  			} else if err != c.errNotFound {
   216  				// why we just return the error instead of query from db,
   217  				// because we don't allow the disaster pass to the dbs.
   218  				// fail fast, in case we bring down the dbs.
   219  				return nil, err
   220  			}
   221  
   222  			if err = query(v); err == c.errNotFound {
   223  				if err = c.setCacheWithNotFound(ctx, key); err != nil {
   224  					logger.Error(err)
   225  				}
   226  
   227  				return nil, c.errNotFound
   228  			} else if err != nil {
   229  				c.stat.IncrementDbFails()
   230  				return nil, err
   231  			}
   232  
   233  			if err = cacheVal(v); err != nil {
   234  				logger.Error(err)
   235  			}
   236  		}
   237  
   238  		return jsonx.Marshal(v)
   239  	})
   240  	if err != nil {
   241  		return err
   242  	}
   243  	if fresh {
   244  		return nil
   245  	}
   246  
   247  	// got the result from previous ongoing query.
   248  	// why not call IncrementTotal at the beginning of this function?
   249  	// because a shared error is returned, and we don't want to count.
   250  	// for example, if the db is down, the query will be failed, we count
   251  	// the shared errors with one db failure.
   252  	c.stat.IncrementTotal()
   253  	c.stat.IncrementHit()
   254  
   255  	return jsonx.Unmarshal(val.([]byte), v)
   256  }
   257  
   258  func (c cacheNode) processCache(ctx context.Context, key, data string, v interface{}) error {
   259  	err := jsonx.Unmarshal([]byte(data), v)
   260  	if err == nil {
   261  		return nil
   262  	}
   263  
   264  	report := fmt.Sprintf("unmarshal cache, node: %s, key: %s, value: %s, error: %v",
   265  		c.rds.Addr, key, data, err)
   266  	logger := logx.WithContext(ctx)
   267  	logger.Error(report)
   268  	stat.Report(report)
   269  	if _, e := c.rds.DelCtx(ctx, key); e != nil {
   270  		logger.Errorf("delete invalid cache, node: %s, key: %s, value: %s, error: %v",
   271  			c.rds.Addr, key, data, e)
   272  	}
   273  
   274  	// returns errNotFound to reload the value by the given queryFn
   275  	return c.errNotFound
   276  }
   277  
   278  func (c cacheNode) setCacheWithNotFound(ctx context.Context, key string) error {
   279  	seconds := int(math.Ceil(c.aroundDuration(c.notFoundExpiry).Seconds()))
   280  	return c.rds.SetexCtx(ctx, key, notFoundPlaceholder, seconds)
   281  }