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 }