x: add Redis backend for cache

Adds a RedisCache config block and redisCache implementation for the
generic cache.Cache interface, enabling distributed HTTP response
caching across gost instances.

go-redis/redis/v8 was already a dependency (used by recorder and
internal loader) — no new imports needed.
This commit is contained in:
ginuerzh
2026-07-29 22:39:19 +08:00
parent 0ab5363f60
commit 3157436140
3 changed files with 147 additions and 5 deletions
+119
View File
@@ -0,0 +1,119 @@
package cache
import (
"context"
"time"
"github.com/go-gost/core/cache"
"github.com/go-gost/core/logger"
"github.com/go-redis/redis/v8"
)
// defaultKeyPrefix is prepended to cache keys when RedisOptions.KeyPrefix is empty.
const defaultKeyPrefix = "gost:cache:"
// RedisOptions configures a Redis cache.
type RedisOptions struct {
// Addr is the Redis server address (host:port).
Addr string
// DB is the Redis database number.
DB int
// Username for Redis authentication. Optional.
Username string
// Password for Redis authentication. Optional.
Password string
// DefaultTTL is applied to entries stored without an explicit TTL.
// A non-positive value means entries never expire by default.
DefaultTTL time.Duration
// KeyPrefix is prepended to all cache keys. Defaults to "gost:cache:".
KeyPrefix string
// Logger is used for debug logging. Optional.
Logger logger.Logger
}
// redisCache is a cache.Cache backed by a Redis server.
//
// Redis expires keys natively, so an expired entry is a true miss here — the
// interface's serve-stale contract (returning expired entries from Get) does
// not apply to this backend.
type redisCache struct {
client *redis.Client
defaultTTL time.Duration
keyPrefix string
logger logger.Logger
}
// NewRedisCache creates a Redis-backed cache. Close releases the underlying
// client connections.
func NewRedisCache(opts RedisOptions) cache.Cache {
keyPrefix := opts.KeyPrefix
if keyPrefix == "" {
keyPrefix = defaultKeyPrefix
}
return &redisCache{
client: redis.NewClient(&redis.Options{
Addr: opts.Addr,
Username: opts.Username,
Password: opts.Password,
DB: opts.DB,
}),
defaultTTL: opts.DefaultTTL,
keyPrefix: keyPrefix,
logger: opts.Logger,
}
}
// Get returns the entry for key, or cache.ErrNotFound if the key is absent
// (including keys Redis has already expired).
func (c *redisCache) Get(ctx context.Context, key string) (*cache.Entry, error) {
k := c.keyPrefix + key
pipe := c.client.Pipeline()
getCmd := pipe.Get(ctx, k)
ttlCmd := pipe.PTTL(ctx, k)
if _, err := pipe.Exec(ctx); err != nil {
if err == redis.Nil {
return nil, cache.ErrNotFound
}
return nil, err
}
entry := &cache.Entry{
Data: []byte(getCmd.Val()),
}
if ttl := ttlCmd.Val(); ttl > 0 {
entry.Expiration = time.Now().Add(ttl)
}
return entry, nil
}
// Set stores data under key. A non-positive TTL (after applying the default)
// stores the entry without expiration.
func (c *redisCache) Set(ctx context.Context, key string, data []byte, opts ...cache.SetOption) error {
var options cache.SetOptions
for _, opt := range opts {
opt(&options)
}
ttl := options.TTL
if ttl <= 0 {
ttl = c.defaultTTL
}
if ttl < 0 {
ttl = 0 // redis: 0 = no expiration
}
return c.client.Set(ctx, c.keyPrefix+key, data, ttl).Err()
}
// Delete removes key from the cache. Deleting an absent key is a no-op.
func (c *redisCache) Delete(ctx context.Context, key string) error {
return c.client.Del(ctx, c.keyPrefix+key).Err()
}
// Close releases the Redis client. Implements io.Closer so the registry
// closes the client on hot reload.
func (c *redisCache) Close() error {
return c.client.Close()
}
+13 -2
View File
@@ -324,11 +324,22 @@ type RewriterConfig struct {
Plugin *PluginConfig `yaml:",omitempty" json:"plugin,omitempty"` Plugin *PluginConfig `yaml:",omitempty" json:"plugin,omitempty"`
} }
// CacheConfig defines a named cache store. Only the Memory backend is // CacheConfig defines a named cache store. Memory and Redis backends are
// currently supported; other backends (Redis, file, plugin) are follow-ups. // supported; other backends (file, plugin) are follow-ups.
type CacheConfig struct { type CacheConfig struct {
Name string `json:"name"` Name string `json:"name"`
Memory *MemoryCache `yaml:",omitempty" json:"memory,omitempty"` Memory *MemoryCache `yaml:",omitempty" json:"memory,omitempty"`
Redis *RedisCache `yaml:",omitempty" json:"redis,omitempty"`
}
// RedisCache configures the Redis cache backend.
type RedisCache struct {
Addr string `json:"addr"`
DB int `yaml:",omitempty" json:"db,omitempty"`
Username string `yaml:",omitempty" json:"username,omitempty"`
Password string `yaml:",omitempty" json:"password,omitempty"`
TTL time.Duration `yaml:",omitempty" json:"ttl,omitempty"`
Key string `yaml:",omitempty" json:"key,omitempty"` // key prefix, default "gost:cache:"
} }
// MemoryCache configures the in-memory cache backend. // MemoryCache configures the in-memory cache backend.
+15 -3
View File
@@ -9,9 +9,9 @@ import (
"github.com/go-gost/x/config" "github.com/go-gost/x/config"
) )
// ParseCache converts a CacheConfig into a cache.Cache. Only the memory // ParseCache converts a CacheConfig into a cache.Cache. Memory and Redis
// backend is currently supported. Returns nil when cfg is nil or no backend // backends are supported. Returns nil when cfg is nil or no backend is
// is configured. // configured.
func ParseCache(cfg *config.CacheConfig) cache.Cache { func ParseCache(cfg *config.CacheConfig) cache.Cache {
if cfg == nil { if cfg == nil {
return nil return nil
@@ -28,5 +28,17 @@ func ParseCache(cfg *config.CacheConfig) cache.Cache {
}) })
} }
if cfg.Redis != nil {
return xcache.NewRedisCache(xcache.RedisOptions{
Addr: cfg.Redis.Addr,
DB: cfg.Redis.DB,
Username: cfg.Redis.Username,
Password: cfg.Redis.Password,
DefaultTTL: cfg.Redis.TTL,
KeyPrefix: cfg.Redis.Key,
Logger: logger.Default(),
})
}
return nil return nil
} }