| package cachex |
|
|
| import ( |
| "context" |
| "errors" |
| "strings" |
| "sync" |
| "time" |
|
|
| "github.com/go-redis/redis/v8" |
| "github.com/samber/hot" |
| ) |
|
|
| const ( |
| defaultRedisOpTimeout = 2 * time.Second |
| defaultRedisScanTimeout = 30 * time.Second |
| defaultRedisDelTimeout = 10 * time.Second |
| ) |
|
|
| type HybridCacheConfig[V any] struct { |
| Namespace Namespace |
|
|
| |
| Redis *redis.Client |
| RedisCodec ValueCodec[V] |
| RedisEnabled func() bool |
|
|
| |
| Memory func() *hot.HotCache[string, V] |
| } |
|
|
| |
| type HybridCache[V any] struct { |
| ns Namespace |
|
|
| redis *redis.Client |
| redisCodec ValueCodec[V] |
| redisEnabled func() bool |
|
|
| memOnce sync.Once |
| memInit func() *hot.HotCache[string, V] |
| mem *hot.HotCache[string, V] |
| } |
|
|
| func NewHybridCache[V any](cfg HybridCacheConfig[V]) *HybridCache[V] { |
| return &HybridCache[V]{ |
| ns: cfg.Namespace, |
| redis: cfg.Redis, |
| redisCodec: cfg.RedisCodec, |
| redisEnabled: cfg.RedisEnabled, |
| memInit: cfg.Memory, |
| } |
| } |
|
|
| func (c *HybridCache[V]) FullKey(key string) string { |
| return c.ns.FullKey(key) |
| } |
|
|
| func (c *HybridCache[V]) redisOn() bool { |
| if c.redis == nil || c.redisCodec == nil { |
| return false |
| } |
| if c.redisEnabled == nil { |
| return true |
| } |
| return c.redisEnabled() |
| } |
|
|
| func (c *HybridCache[V]) memCache() *hot.HotCache[string, V] { |
| c.memOnce.Do(func() { |
| if c.memInit == nil { |
| c.mem = hot.NewHotCache[string, V](hot.LRU, 1).Build() |
| return |
| } |
| c.mem = c.memInit() |
| }) |
| return c.mem |
| } |
|
|
| func (c *HybridCache[V]) Get(key string) (value V, found bool, err error) { |
| full := c.ns.FullKey(key) |
| if full == "" { |
| var zero V |
| return zero, false, nil |
| } |
|
|
| if c.redisOn() { |
| ctx, cancel := context.WithTimeout(context.Background(), defaultRedisOpTimeout) |
| defer cancel() |
|
|
| raw, e := c.redis.Get(ctx, full).Result() |
| if e == nil { |
| v, decErr := c.redisCodec.Decode(raw) |
| if decErr != nil { |
| var zero V |
| return zero, false, decErr |
| } |
| return v, true, nil |
| } |
| if errors.Is(e, redis.Nil) { |
| var zero V |
| return zero, false, nil |
| } |
| var zero V |
| return zero, false, e |
| } |
|
|
| return c.memCache().Get(full) |
| } |
|
|
| func (c *HybridCache[V]) SetWithTTL(key string, v V, ttl time.Duration) error { |
| full := c.ns.FullKey(key) |
| if full == "" { |
| return nil |
| } |
|
|
| if c.redisOn() { |
| raw, err := c.redisCodec.Encode(v) |
| if err != nil { |
| return err |
| } |
| ctx, cancel := context.WithTimeout(context.Background(), defaultRedisOpTimeout) |
| defer cancel() |
| return c.redis.Set(ctx, full, raw, ttl).Err() |
| } |
|
|
| c.memCache().SetWithTTL(full, v, ttl) |
| return nil |
| } |
|
|
| |
| func (c *HybridCache[V]) Keys() ([]string, error) { |
| if c.redisOn() { |
| return c.scanKeys(c.ns.MatchPattern()) |
| } |
| return c.memCache().Keys(), nil |
| } |
|
|
| func (c *HybridCache[V]) scanKeys(match string) ([]string, error) { |
| ctx, cancel := context.WithTimeout(context.Background(), defaultRedisScanTimeout) |
| defer cancel() |
|
|
| var cursor uint64 |
| keys := make([]string, 0, 1024) |
| for { |
| k, next, err := c.redis.Scan(ctx, cursor, match, 1000).Result() |
| if err != nil { |
| return keys, err |
| } |
| keys = append(keys, k...) |
| cursor = next |
| if cursor == 0 { |
| break |
| } |
| } |
| return keys, nil |
| } |
|
|
| func (c *HybridCache[V]) Purge() error { |
| if c.redisOn() { |
| keys, err := c.scanKeys(c.ns.MatchPattern()) |
| if err != nil { |
| return err |
| } |
| if len(keys) == 0 { |
| return nil |
| } |
| _, err = c.DeleteMany(keys) |
| return err |
| } |
|
|
| c.memCache().Purge() |
| return nil |
| } |
|
|
| func (c *HybridCache[V]) DeleteByPrefix(prefix string) (int, error) { |
| fullPrefix := c.ns.FullKey(prefix) |
| if fullPrefix == "" { |
| return 0, nil |
| } |
| if !strings.HasSuffix(fullPrefix, ":") { |
| fullPrefix += ":" |
| } |
|
|
| if c.redisOn() { |
| match := fullPrefix + "*" |
| keys, err := c.scanKeys(match) |
| if err != nil { |
| return 0, err |
| } |
| if len(keys) == 0 { |
| return 0, nil |
| } |
|
|
| res, err := c.DeleteMany(keys) |
| if err != nil { |
| return 0, err |
| } |
| deleted := 0 |
| for _, ok := range res { |
| if ok { |
| deleted++ |
| } |
| } |
| return deleted, nil |
| } |
|
|
| |
| allKeys := c.memCache().Keys() |
| keys := make([]string, 0, 128) |
| for _, k := range allKeys { |
| if strings.HasPrefix(k, fullPrefix) { |
| keys = append(keys, k) |
| } |
| } |
| if len(keys) == 0 { |
| return 0, nil |
| } |
| res, _ := c.DeleteMany(keys) |
| deleted := 0 |
| for _, ok := range res { |
| if ok { |
| deleted++ |
| } |
| } |
| return deleted, nil |
| } |
|
|
| |
| |
| func (c *HybridCache[V]) DeleteMany(keys []string) (map[string]bool, error) { |
| res := make(map[string]bool, len(keys)) |
| if len(keys) == 0 { |
| return res, nil |
| } |
|
|
| fullKeys := make([]string, 0, len(keys)) |
| for _, k := range keys { |
| k = c.ns.FullKey(k) |
| if k == "" { |
| continue |
| } |
| fullKeys = append(fullKeys, k) |
| } |
| if len(fullKeys) == 0 { |
| return res, nil |
| } |
|
|
| if c.redisOn() { |
| ctx, cancel := context.WithTimeout(context.Background(), defaultRedisDelTimeout) |
| defer cancel() |
|
|
| pipe := c.redis.Pipeline() |
| cmds := make([]*redis.IntCmd, 0, len(fullKeys)) |
| for _, k := range fullKeys { |
| |
| cmds = append(cmds, pipe.Unlink(ctx, k)) |
| } |
| _, err := pipe.Exec(ctx) |
| if err != nil && !errors.Is(err, redis.Nil) { |
| return res, err |
| } |
| for i, cmd := range cmds { |
| deleted := cmd != nil && cmd.Err() == nil && cmd.Val() > 0 |
| res[fullKeys[i]] = deleted |
| } |
| return res, nil |
| } |
|
|
| return c.memCache().DeleteMany(fullKeys), nil |
| } |
|
|
| func (c *HybridCache[V]) Capacity() (mainCacheCapacity int, missingCacheCapacity int) { |
| if c.redisOn() { |
| return 0, 0 |
| } |
| return c.memCache().Capacity() |
| } |
|
|
| func (c *HybridCache[V]) Algorithm() (mainCacheAlgorithm string, missingCacheAlgorithm string) { |
| if c.redisOn() { |
| return "redis", "" |
| } |
| return c.memCache().Algorithm() |
| } |
|
|