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 }