github.com/dtm-labs/rockscache@v0.1.1/client.go (about)

     1  package rockscache
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"math"
     7  	"math/rand"
     8  	"time"
     9  
    10  	"github.com/lithammer/shortuuid"
    11  	"github.com/redis/go-redis/v9"
    12  	"golang.org/x/sync/singleflight"
    13  )
    14  
    15  const locked = "LOCKED"
    16  
    17  // Options represents the options for rockscache client
    18  type Options struct {
    19  	// Delay is the delay delete time for keys that are tag deleted. default is 10s
    20  	Delay time.Duration
    21  	// EmptyExpire is the expire time for empty result. default is 60s
    22  	EmptyExpire time.Duration
    23  	// LockExpire is the expire time for the lock which is allocated when updating cache. default is 3s
    24  	// should be set to the max of the underling data calculating time.
    25  	LockExpire time.Duration
    26  	// LockSleep is the sleep interval time if try lock failed. default is 100ms
    27  	LockSleep time.Duration
    28  	// WaitReplicas is the number of replicas to wait for. default is 0
    29  	// if WaitReplicas is > 0, it will use redis WAIT command to wait for TagAsDeleted synchronized.
    30  	WaitReplicas int
    31  	// WaitReplicasTimeout is the number of replicas to wait for. default is 3000ms
    32  	// if WaitReplicas is > 0, WaitReplicasTimeout is the timeout for WAIT command.
    33  	WaitReplicasTimeout time.Duration
    34  	// RandomExpireAdjustment is the random adjustment for the expire time. default 0.1
    35  	// if the expire time is set to 600s, and this value is set to 0.1, then the actual expire time will be 540s - 600s
    36  	// solve the problem of cache avalanche.
    37  	RandomExpireAdjustment float64
    38  	// CacheReadDisabled is the flag to disable read cache. default is false
    39  	// when redis is down, set this flat to downgrade.
    40  	DisableCacheRead bool
    41  	// CacheDeleteDisabled is the flag to disable delete cache. default is false
    42  	// when redis is down, set this flat to downgrade.
    43  	DisableCacheDelete bool
    44  	// StrongConsistency is the flag to enable strong consistency. default is false
    45  	// if enabled, the Fetch result will be consistent with the db result, but performance is bad.
    46  	StrongConsistency bool
    47  	// Context for redis command
    48  	Context context.Context
    49  }
    50  
    51  // NewDefaultOptions return default options
    52  func NewDefaultOptions() Options {
    53  	return Options{
    54  		Delay:                  10 * time.Second,
    55  		EmptyExpire:            60 * time.Second,
    56  		LockExpire:             3 * time.Second,
    57  		LockSleep:              100 * time.Millisecond,
    58  		RandomExpireAdjustment: 0.1,
    59  		WaitReplicasTimeout:    3000 * time.Millisecond,
    60  		Context:                context.Background(),
    61  	}
    62  }
    63  
    64  // Client delay client
    65  type Client struct {
    66  	rdb     redis.UniversalClient
    67  	Options Options
    68  	group   singleflight.Group
    69  }
    70  
    71  // NewClient return a new rockscache client
    72  // for each key, rockscache client store a hash set,
    73  // the hash set contains the following fields:
    74  // value: the value of the key
    75  // lockUntil: the time when the lock is released.
    76  // lockOwner: the owner of the lock.
    77  // if a thread query the cache for data, and no cache exists, it will lock the key before querying data in DB
    78  func NewClient(rdb redis.UniversalClient, options Options) *Client {
    79  	if options.Delay == 0 || options.LockExpire == 0 {
    80  		panic("cache options error: Delay and LockExpire should not be 0, you should call NewDefaultOptions() to get default options")
    81  	}
    82  	return &Client{rdb: rdb, Options: options}
    83  }
    84  
    85  // TagAsDeleted a key, the key will expire after delay time.
    86  func (c *Client) TagAsDeleted(key string) error {
    87  	return c.TagAsDeleted2(c.Options.Context, key)
    88  }
    89  
    90  // TagAsDeleted2 a key, the key will expire after delay time.
    91  func (c *Client) TagAsDeleted2(ctx context.Context, key string) error {
    92  	if c.Options.DisableCacheDelete {
    93  		return nil
    94  	}
    95  	debugf("deleting: key=%s", key)
    96  	luaFn := func(con redisConn) error {
    97  		_, err := callLua(ctx, con, ` --  delete
    98  		redis.call('HSET', KEYS[1], 'lockUntil', 0)
    99  		redis.call('HDEL', KEYS[1], 'lockOwner')
   100  		redis.call('EXPIRE', KEYS[1], ARGV[1])
   101  			`, []string{key}, []interface{}{int64(c.Options.Delay / time.Second)})
   102  		return err
   103  	}
   104  	if c.Options.WaitReplicas > 0 {
   105  		err := luaFn(c.rdb)
   106  		cmd := redis.NewCmd(ctx, "WAIT", c.Options.WaitReplicas, c.Options.WaitReplicasTimeout)
   107  		if err == nil {
   108  			err = c.rdb.Process(ctx, cmd)
   109  		}
   110  		var replicas int
   111  		if err == nil {
   112  			replicas, err = cmd.Int()
   113  		}
   114  		if err == nil && replicas < c.Options.WaitReplicas {
   115  			err = fmt.Errorf("wait replicas %d failed. result replicas: %d", c.Options.WaitReplicas, replicas)
   116  		}
   117  		return err
   118  	}
   119  	return luaFn(c.rdb)
   120  }
   121  
   122  // Fetch returns the value store in cache indexed by the key.
   123  // If the key doest not exists, call fn to get result, store it in cache, then return.
   124  func (c *Client) Fetch(key string, expire time.Duration, fn func() (string, error)) (string, error) {
   125  	return c.Fetch2(c.Options.Context, key, expire, fn)
   126  }
   127  
   128  // Fetch2 returns the value store in cache indexed by the key.
   129  // If the key doest not exists, call fn to get result, store it in cache, then return.
   130  func (c *Client) Fetch2(ctx context.Context, key string, expire time.Duration, fn func() (string, error)) (string, error) {
   131  	ex := expire - c.Options.Delay - time.Duration(rand.Float64()*c.Options.RandomExpireAdjustment*float64(expire))
   132  	v, err, _ := c.group.Do(key, func() (interface{}, error) {
   133  		if c.Options.DisableCacheRead {
   134  			return fn()
   135  		} else if c.Options.StrongConsistency {
   136  			return c.strongFetch(ctx, key, ex, fn)
   137  		}
   138  		return c.weakFetch(ctx, key, ex, fn)
   139  	})
   140  	return v.(string), err
   141  }
   142  
   143  func (c *Client) luaGet(ctx context.Context, key string, owner string) ([]interface{}, error) {
   144  	res, err := callLua(ctx, c.rdb, ` -- luaGet
   145  	local v = redis.call('HGET', KEYS[1], 'value')
   146  	local lu = redis.call('HGET', KEYS[1], 'lockUntil')
   147  	if lu ~= false and tonumber(lu) < tonumber(ARGV[1]) or lu == false and v == false then
   148  		redis.call('HSET', KEYS[1], 'lockUntil', ARGV[2])
   149  		redis.call('HSET', KEYS[1], 'lockOwner', ARGV[3])
   150  		return { v, 'LOCKED' }
   151  	end
   152  	return {v, lu}
   153  	`, []string{key}, []interface{}{now(), now() + int64(c.Options.LockExpire/time.Second), owner})
   154  	debugf("luaGet return: %v, %v", res, err)
   155  	if err != nil {
   156  		return nil, err
   157  	}
   158  	return res.([]interface{}), nil
   159  }
   160  
   161  func (c *Client) luaSet(ctx context.Context, key string, value string, expire int, owner string) error {
   162  	_, err := callLua(ctx, c.rdb, `-- luaSet
   163  	local o = redis.call('HGET', KEYS[1], 'lockOwner')
   164  	if o ~= ARGV[2] then
   165  			return
   166  	end
   167  	redis.call('HSET', KEYS[1], 'value', ARGV[1])
   168  	redis.call('HDEL', KEYS[1], 'lockUntil')
   169  	redis.call('HDEL', KEYS[1], 'lockOwner')
   170  	redis.call('EXPIRE', KEYS[1], ARGV[3])
   171  	`, []string{key}, []interface{}{value, owner, expire})
   172  	return err
   173  }
   174  
   175  func (c *Client) fetchNew(ctx context.Context, key string, expire time.Duration, owner string, fn func() (string, error)) (string, error) {
   176  	result, err := fn()
   177  	if err != nil {
   178  		_ = c.UnlockForUpdate(ctx, key, owner)
   179  		return "", err
   180  	}
   181  	if result == "" {
   182  		if c.Options.EmptyExpire == 0 { // if empty expire is 0, then delete the key
   183  			err = c.rdb.Del(ctx, key).Err()
   184  			return "", err
   185  		}
   186  		expire = c.Options.EmptyExpire
   187  	}
   188  	err = c.luaSet(ctx, key, result, int(expire/time.Second), owner)
   189  	return result, err
   190  }
   191  
   192  func (c *Client) weakFetch(ctx context.Context, key string, expire time.Duration, fn func() (string, error)) (string, error) {
   193  	debugf("weakFetch: key=%s", key)
   194  	owner := shortuuid.New()
   195  	r, err := c.luaGet(ctx, key, owner)
   196  	for err == nil && r[0] == nil && r[1].(string) != locked {
   197  		debugf("empty result for %s locked by other, so sleep %s", key, c.Options.LockSleep.String())
   198  		time.Sleep(c.Options.LockSleep)
   199  		r, err = c.luaGet(ctx, key, owner)
   200  	}
   201  	if err != nil {
   202  		return "", err
   203  	}
   204  	if r[1] != locked {
   205  		return r[0].(string), nil
   206  	}
   207  	if r[0] == nil {
   208  		return c.fetchNew(ctx, key, expire, owner, fn)
   209  	}
   210  	go withRecover(func() {
   211  		_, _ = c.fetchNew(ctx, key, expire, owner, fn)
   212  	})
   213  	return r[0].(string), nil
   214  }
   215  
   216  func (c *Client) strongFetch(ctx context.Context, key string, expire time.Duration, fn func() (string, error)) (string, error) {
   217  	debugf("strongFetch: key=%s", key)
   218  	owner := shortuuid.New()
   219  	r, err := c.luaGet(ctx, key, owner)
   220  	for err == nil && r[1] != nil && r[1] != locked { // locked by other
   221  		debugf("locked by other, so sleep %s", c.Options.LockSleep)
   222  		time.Sleep(c.Options.LockSleep)
   223  		r, err = c.luaGet(ctx, key, owner)
   224  	}
   225  	if err != nil {
   226  		return "", err
   227  	}
   228  	if r[1] != locked { // normal value
   229  		return r[0].(string), nil
   230  	}
   231  	return c.fetchNew(ctx, key, expire, owner, fn)
   232  }
   233  
   234  // RawGet returns the value store in cache indexed by the key, no matter if the key locked or not
   235  func (c *Client) RawGet(ctx context.Context, key string) (string, error) {
   236  	return c.rdb.HGet(ctx, key, "value").Result()
   237  }
   238  
   239  // RawSet sets the value store in cache indexed by the key, no matter if the key locked or not
   240  func (c *Client) RawSet(ctx context.Context, key string, value string, expire time.Duration) error {
   241  	err := c.rdb.HSet(ctx, key, "value", value).Err()
   242  	if err == nil {
   243  		err = c.rdb.Expire(ctx, key, expire).Err()
   244  	}
   245  	return err
   246  }
   247  
   248  // LockForUpdate locks the key, used in very strict strong consistency mode
   249  func (c *Client) LockForUpdate(ctx context.Context, key string, owner string) error {
   250  	lockUntil := math.Pow10(10)
   251  	res, err := callLua(ctx, c.rdb, ` -- luaLock
   252  	local lu = redis.call('HGET', KEYS[1], 'lockUntil')
   253  	local lo = redis.call('HGET', KEYS[1], 'lockOwner')
   254  	if lu == false or tonumber(lu) < tonumber(ARGV[2]) or lo == ARGV[1] then
   255  		redis.call('HSET', KEYS[1], 'lockUntil', ARGV[2])
   256  		redis.call('HSET', KEYS[1], 'lockOwner', ARGV[1])
   257  		return 'LOCKED'
   258  	end
   259  	return lo
   260  	`, []string{key}, []interface{}{owner, lockUntil})
   261  	if err == nil && res != "LOCKED" {
   262  		return fmt.Errorf("%s has been locked by %s", key, res)
   263  	}
   264  	return err
   265  }
   266  
   267  // UnlockForUpdate unlocks the key, used in very strict strong consistency mode
   268  func (c *Client) UnlockForUpdate(ctx context.Context, key string, owner string) error {
   269  	_, err := callLua(ctx, c.rdb, ` -- luaUnlock
   270  	local lo = redis.call('HGET', KEYS[1], 'lockOwner')
   271  	if lo == ARGV[1] then
   272  		redis.call('HSET', KEYS[1], 'lockUntil', 0)
   273  		redis.call('HDEL', KEYS[1], 'lockOwner')
   274  		redis.call('EXPIRE', KEYS[1], ARGV[2])
   275  	end
   276  	`, []string{key}, []interface{}{owner, c.Options.LockExpire / time.Second})
   277  	return err
   278  }