Documentation
¶
Overview ¶
Package redisclient wraps github.com/redis/go-redis/v9 with a key prefix, JSON marshalling of values, error wrapping and a set of higher-level helpers: bounded ("with eviction") sets and hashes, an owner-bound distributed lock and a one-execution-per-window rate limiter.
RedisClient embeds *redis.Client, so raw commands remain available, while the wrapper methods apply the prefix via Key. Provider is the interface implemented by RedisClient and is what consumers should depend on.
rc, err := redisclient.New(&redisclient.Config{
Server: "redis://localhost:6379/0",
Password: "secret",
})
if err != nil {
return err
}
defer rc.Close()
c := rc.WithPrefix("myapp") // keys become "/myapp/<key>"
if err := c.Set(ctx, "user:1", user, time.Hour); err != nil {
return err
}
var u User
if err := c.Get(ctx, "user:1", &u); redisclient.IsNotFoundError(err) {
// missing
}
token, remaining, err := c.TryLock(ctx, "job", 30*time.Second)
if err != nil {
return err
}
if token == "" {
return fmt.Errorf("job is locked for %s", remaining)
}
defer c.ReleaseLock(ctx, "job", token) // only while token holds the lock
Config YAML/JSON fields: server (redis:// or rediss:// URL), ttl, client_tls {cert, key, trusted_ca}, user, password. Password from Config overrides credentials embedded in the URL. Only the server address and database are logged; a malformed URL is reported without the URL.
Keys: a key is cleaned as a rooted path and joined with the prefix, so ".." cannot leave the prefix, and lock and rate-limit keys cannot leave their "lock:" and "ratelimit:" namespaces; Keys and ScanKeys take Redis glob patterns relative to the prefix, match the prefix literally and return relative keys.
Values: string and []byte are stored as-is, everything else is JSON encoded (see Marshal / UnmarshalStringCmd). Get and HGet return ErrNotFound for a missing key; the other commands return the wrapped go-redis error. WithPrefix returns a child that shares the connection and whose Close is a no-op; only the root client returned by New closes the connection, and after that the commands of the root and its children fail with redis.ErrClosed.
Coordination: TryLock returns a random owner token and ReleaseLock deletes the lock only while it still holds that token (a Lua compare-and-delete), so an expired owner cannot release a successor's lock. TryAcquireRateLimit starts a window with SET NX PX: one call per window succeeds, denied calls write nothing, and the window is measured by the server's clock. SAddWithEviction and HSetWithEviction update the collection and its insertion-order list in one Lua script. The server must allow Lua scripting (EVALSHA).
Index ¶
- Variables
- func IsNotFoundError(err error) bool
- func Marshal(v any) (any, error)
- func NewRedisClient(cfg *Config) (*redis.Client, error)
- func UnmarshalStringCmd(val *redis.StringCmd, v any) error
- type Config
- type DistributedLock
- type Provider
- type RateLimiter
- type RedisClient
- func (c *RedisClient) Close() error
- func (c *RedisClient) Del(ctx context.Context, key string) error
- func (c *RedisClient) Exists(ctx context.Context, key string) (bool, error)
- func (c *RedisClient) Expire(ctx context.Context, key string, expiration time.Duration) (bool, error)
- func (c *RedisClient) Get(ctx context.Context, key string, v any) error
- func (c *RedisClient) GetRateLimitRemainingTime(ctx context.Context, key string) (time.Duration, error)
- func (c *RedisClient) HDel(ctx context.Context, key string, fields ...string) error
- func (c *RedisClient) HExists(ctx context.Context, key string, field string) (bool, error)
- func (c *RedisClient) HGet(ctx context.Context, key string, field string) (string, error)
- func (c *RedisClient) HGetAll(ctx context.Context, key string) (map[string]string, error)
- func (c *RedisClient) HKeys(ctx context.Context, key string) ([]string, error)
- func (c *RedisClient) HSet(ctx context.Context, key string, field string, value any) error
- func (c *RedisClient) HSetMany(ctx context.Context, key string, values map[string]any) error
- func (c *RedisClient) HSetWithEviction(ctx context.Context, hashKey, orderListKey string, maxFields int64, ...) error
- func (c *RedisClient) HVals(ctx context.Context, key string) ([]string, error)
- func (c *RedisClient) IsLocked(ctx context.Context, key string) (bool, error)
- func (c *RedisClient) Key(key string) string
- func (c *RedisClient) Keys(ctx context.Context, pattern string) ([]string, error)
- func (c *RedisClient) LIndex(ctx context.Context, key string, index int64) (string, error)
- func (c *RedisClient) LLen(ctx context.Context, key string) (int64, error)
- func (c *RedisClient) LPop(ctx context.Context, key string) (string, error)
- func (c *RedisClient) LPush(ctx context.Context, key string, values ...any) error
- func (c *RedisClient) LRange(ctx context.Context, key string, start, stop int64) ([]string, error)
- func (c *RedisClient) LTrim(ctx context.Context, key string, start, stop int64) error
- func (c *RedisClient) Ping(ctx context.Context) error
- func (c *RedisClient) RPop(ctx context.Context, key string) (string, error)
- func (c *RedisClient) RPush(ctx context.Context, key string, values ...any) error
- func (c *RedisClient) RawClient() *redis.Client
- func (c *RedisClient) ReleaseLock(ctx context.Context, key, token string) (bool, error)
- func (c *RedisClient) SAdd(ctx context.Context, key string, members ...any) error
- func (c *RedisClient) SAddWithEviction(ctx context.Context, key string, listKey string, limit int64, member string) error
- func (c *RedisClient) SCard(ctx context.Context, key string) (int64, error)
- func (c *RedisClient) SIsMember(ctx context.Context, key string, member any) (bool, error)
- func (c *RedisClient) SMembers(ctx context.Context, key string) ([]string, error)
- func (c *RedisClient) SRem(ctx context.Context, key string, members ...any) error
- func (c *RedisClient) ScanKeys(ctx context.Context, pattern string, limit int) ([]string, error)
- func (c *RedisClient) Set(ctx context.Context, key string, v any, expiration time.Duration) error
- func (c *RedisClient) SubKey(key string) string
- func (c *RedisClient) TTL(ctx context.Context, key string) (time.Duration, error)
- func (c *RedisClient) TryAcquireRateLimit(ctx context.Context, key string, window time.Duration) (bool, time.Duration, error)
- func (c *RedisClient) TryLock(ctx context.Context, key string, timeout time.Duration) (string, time.Duration, error)
- func (c *RedisClient) WithPrefix(prefix string) *RedisClient
- func (c *RedisClient) ZAdd(ctx context.Context, key string, score float64, member string) error
- func (c *RedisClient) ZCard(ctx context.Context, key string) (int64, error)
- func (c *RedisClient) ZIncrBy(ctx context.Context, key string, increment float64, member string) error
- func (c *RedisClient) ZRem(ctx context.Context, key string, members ...any) error
- func (c *RedisClient) ZRemRangeByRank(ctx context.Context, key string, start, stop int64) (int64, error)
- func (c *RedisClient) ZRevRangeWithScores(ctx context.Context, key string, start, stop int64) ([]redis.Z, error)
Constants ¶
This section is empty.
Variables ¶
var ErrNotFound = errors.New("not found")
ErrNotFound is returned by Get and HGet for a missing key or field.
Functions ¶
func IsNotFoundError ¶
IsNotFoundError reports whether err is (or wraps) ErrNotFound, or whose message contains "not found".
func Marshal ¶
Marshal marshals the value into a format suitable for Redis storage. string and []byte are stored as-is, while other types are marshaled to JSON.
func NewRedisClient ¶
NewRedisClient builds a raw *redis.Client from cfg: the URL is parsed, TLS is configured from ClientTLS files, Password/User override the URL credentials, and maintenance notifications are disabled. Only the server address and database are logged, and a malformed URL is reported without the URL, which may embed a password. The connection is established lazily.
Types ¶
type Config ¶
type Config struct {
// Server is the redis:// or rediss:// URL (see redis.ParseURL).
Server string `json:"server,omitempty" yaml:"server,omitempty"`
// TTL is informational for callers; the client itself applies no default expiry.
TTL time.Duration `json:"ttl,omitempty" yaml:"ttl,omitempty"`
// ClientTLS describes the TLS certs used to connect to the cluster
ClientTLS *gserver.TLSInfo `json:"client_tls,omitempty" yaml:"client_tls,omitempty"`
// User is the ACL user name; only applied when Password is set.
User string `json:"user,omitempty" yaml:"user,omitempty"`
// Password overrides the credentials embedded in Server.
Password string `json:"password,omitempty" yaml:"password,omitempty"`
}
Config specifies the Redis connection.
type DistributedLock ¶
type DistributedLock interface {
// TryLock attempts to acquire the lock for key, expiring after timeout
// (at least 1ms). It returns the owner token when the lock was
// acquired; otherwise the token is empty and the remaining time is how
// long the current holder keeps the lock.
TryLock(ctx context.Context, key string, timeout time.Duration) (token string, remaining time.Duration, err error)
// ReleaseLock releases the lock for key only while it is still held
// with token, the value returned by TryLock. It returns false when the
// lock expired or is held by another owner.
ReleaseLock(ctx context.Context, key, token string) (bool, error)
// IsLocked reports whether anyone holds the lock for key.
IsLocked(ctx context.Context, key string) (bool, error)
}
DistributedLock provides an owner-bound lock that expires on its own.
type Provider ¶
type Provider interface {
io.Closer
RateLimiter
DistributedLock
// Value operations
Get(ctx context.Context, key string, value any) error
Set(ctx context.Context, key string, value any, expiration time.Duration) error
Del(ctx context.Context, key string) error
// List operations
LPush(ctx context.Context, key string, values ...any) error
RPush(ctx context.Context, key string, values ...any) error
LPop(ctx context.Context, key string) (string, error)
RPop(ctx context.Context, key string) (string, error)
LRange(ctx context.Context, key string, start, stop int64) ([]string, error)
LTrim(ctx context.Context, key string, start, stop int64) error
LLen(ctx context.Context, key string) (int64, error)
LIndex(ctx context.Context, key string, index int64) (string, error)
// Set operations
SAdd(ctx context.Context, key string, members ...any) error
SRem(ctx context.Context, key string, members ...any) error
SIsMember(ctx context.Context, key string, member any) (bool, error)
SMembers(ctx context.Context, key string) ([]string, error)
SCard(ctx context.Context, key string) (int64, error)
SAddWithEviction(ctx context.Context, key string, listKey string, limit int64, member string) error
// Sorted Set (ZSet) operations
ZAdd(ctx context.Context, key string, score float64, member string) error
ZIncrBy(ctx context.Context, key string, increment float64, member string) error
ZRem(ctx context.Context, key string, members ...any) error
ZCard(ctx context.Context, key string) (int64, error)
// ZRevRangeWithScores returns the specified range of elements in the sorted set stored at key,
// by index, with scores ordered from high to low.
// To get top N elements, use ZRevRangeWithScores(key, 0, N-1)
// To get bottom N elements, use ZRevRangeWithScores(key, -N, -1)
ZRevRangeWithScores(ctx context.Context, key string, start, stop int64) ([]redis.Z, error)
// ZRemRangeByRank removes all elements in the sorted set stored at key
// within the given indexes.
// Start and stop are 0-based indexes, with 0 being the first element.
// To remove all ranks below the top N elements, use ZRemRangeByRank(key, 0, -maxN-1)
ZRemRangeByRank(ctx context.Context, key string, start, stop int64) (int64, error)
// Hash operations
HSetMany(ctx context.Context, key string, vals map[string]any) error
HSet(ctx context.Context, key string, field string, value any) error
HGet(ctx context.Context, key string, field string) (string, error)
HGetAll(ctx context.Context, key string) (map[string]string, error)
HDel(ctx context.Context, key string, fields ...string) error
HExists(ctx context.Context, key string, field string) (bool, error)
HKeys(ctx context.Context, key string) ([]string, error)
HVals(ctx context.Context, key string) ([]string, error)
HSetWithEviction(ctx context.Context, hashKey, orderListKey string, maxFields int64, field string, value any) error
// Metadata
ScanKeys(ctx context.Context, pattern string, limit int) ([]string, error)
Exists(ctx context.Context, key string) (bool, error)
Expire(ctx context.Context, key string, expiration time.Duration) (bool, error)
TTL(ctx context.Context, key string) (time.Duration, error)
Ping(ctx context.Context) error
}
Provider is the Redis operations interface implemented by *RedisClient; depend on it rather than on the concrete type. All keys are relative to the client prefix. See the RedisClient methods for per-operation details.
type RateLimiter ¶
type RateLimiter interface {
// TryAcquireRateLimit starts a window of the given length (at least
// 1ms) for key and returns true when no window is active; otherwise it
// returns false and the time until the active window ends. A denied
// call does not extend the active window.
TryAcquireRateLimit(ctx context.Context, key string, window time.Duration) (bool, time.Duration, error)
// GetRateLimitRemainingTime returns the time until the active window
// for key ends (at least 1ms while it is active), or 0 when none is
// active.
GetRateLimitRemainingTime(ctx context.Context, key string) (time.Duration, error)
}
RateLimiter allows one execution per window for a key, across every client of the Redis server.
type RedisClient ¶
RedisClient implements Provider on top of an embedded *redis.Client. The wrapper methods prefix keys (see Key) and wrap errors; the embedded client's own methods are also reachable but bypass the prefix. Create it with New or NewWithClient, and derive namespaced children with WithPrefix.
func New ¶
func New(cfg *Config) (*RedisClient, error)
New creates a root RedisClient (no prefix) from cfg. The returned client owns the connection: its Close closes it.
func NewWithClient ¶
func NewWithClient(client *redis.Client) (*RedisClient, error)
NewWithClient wraps an existing *redis.Client without a prefix. The caller keeps ownership of the connection: Close is a no-op. The error is always nil.
func (*RedisClient) Close ¶
func (c *RedisClient) Close() error
Close closes the underlying connection when this client owns it (created by New) and returns the close error; for clients from NewWithClient or WithPrefix it is a no-op. It is idempotent. Children from WithPrefix share the connection: after the root is closed, their commands, like the root's, fail with redis.ErrClosed.
func (*RedisClient) Del ¶
func (c *RedisClient) Del(ctx context.Context, key string) error
Del removes key; a missing key is not an error.
func (*RedisClient) Expire ¶
func (c *RedisClient) Expire(ctx context.Context, key string, expiration time.Duration) (bool, error)
Expire sets the TTL of key and reports whether the key existed.
func (*RedisClient) Get ¶
Get loads the value stored under key into v, which must be a non-nil pointer (see UnmarshalStringCmd). It returns ErrNotFound for a missing key.
func (*RedisClient) GetRateLimitRemainingTime ¶
func (c *RedisClient) GetRateLimitRemainingTime(ctx context.Context, key string) (time.Duration, error)
GetRateLimitRemainingTime returns the time until the active window of "ratelimit:<key>" ends, at least 1ms while it is active, or 0 when no window is active (or the key has no expiry).
func (*RedisClient) HGet ¶
HGet returns a field of the hash at key, or ErrNotFound when the field or the hash is missing.
func (*RedisClient) HGetAll ¶
HGetAll returns all fields of the hash at key (empty map when missing).
func (*RedisClient) HSetMany ¶
HSetMany sets several fields of the hash at key in one HSET command. An empty map is a no-op.
func (*RedisClient) HSetWithEviction ¶
func (c *RedisClient) HSetWithEviction(ctx context.Context, hashKey, orderListKey string, maxFields int64, field string, value any) error
HSetWithEviction sets field in the hash at hashKey and appends it to the insertion-order list at orderListKey when the field is new (removing stale list entries for it first); updating a field keeps its position. While the list holds more than maxFields entries (maxFields >= 1), its oldest entry is removed from the list and the hash. The update runs as one Lua script, so it is atomic, and hashKey and orderListKey must differ. value is encoded as by HSet. A key of the wrong type fails the call before anything is written. Adding a new field scans the list (LREM), so its cost grows with maxFields.
func (*RedisClient) Key ¶
func (c *RedisClient) Key(key string) string
Key returns the full Redis key for key, or key itself when there is no prefix. key is cleaned as a rooted path and then joined with the prefix, so "a//b", "/a/" and "a" name the same key and ".." cannot leave the prefix: "../x" names "<prefix>x".
func (*RedisClient) Keys ¶
Keys returns the keys, relative to the prefix, that match the Redis glob pattern, which is cleaned like a key; the prefix matches literally. This method should be used mostly for testing, as in prod many keys maybe returned. It blocks and scans the entire Redis keyspace — not safe for large production datasets.
func (*RedisClient) LPop ¶
LPop removes and returns the first element of the list at key. An empty or missing list yields a wrapped redis.Nil error, not ErrNotFound.
func (*RedisClient) LRange ¶
LRange returns the elements of the list at key between the start and stop indexes (inclusive, negative counts from the end).
func (*RedisClient) LTrim ¶
LTrim keeps only the elements of the list at key between start and stop.
func (*RedisClient) Ping ¶
func (c *RedisClient) Ping(ctx context.Context) error
Ping checks connectivity to the server.
func (*RedisClient) RPop ¶
RPop removes and returns the last element of the list at key. An empty or missing list yields a wrapped redis.Nil error, not ErrNotFound.
func (*RedisClient) RawClient ¶
func (c *RedisClient) RawClient() *redis.Client
RawClient returns the raw Redis client This is useful for using the client in a context where the Provider interface is not used
func (*RedisClient) ReleaseLock ¶
ReleaseLock deletes the lock "lock:<key>" only while it holds token, in one Lua script (compare-and-delete). It returns true when the lock was released and false when it expired or another owner holds it; an empty token is an error.
func (*RedisClient) SAddWithEviction ¶
func (c *RedisClient) SAddWithEviction(ctx context.Context, key string, listKey string, limit int64, member string) error
SAddWithEviction adds member to the set at key and appends it to the insertion-order list at listKey when it was not a member yet (removing stale list entries for it first); re-adding a member keeps its position. While the list holds more than limit entries (limit >= 1), its oldest entry is removed from the list and the set. The update runs as one Lua script, so it is atomic, and key and listKey must differ. A key of the wrong type fails the call before anything is written. Adding a new member scans the list (LREM), so its cost grows with limit.
func (*RedisClient) ScanKeys ¶
ScanKeys returns keys matching pattern (relative to the prefix, see Keys) using SCAN with a COUNT hint of 100, stopping once at least limit keys were collected (limit <= 0 means 1000) or the scan completes. The last batch may take the result past limit.
func (*RedisClient) Set ¶
Set stores v under key (see Marshal for the encoding) with the given expiration; 0 means no expiry and redis.KeepTTL keeps the existing one.
func (*RedisClient) SubKey ¶
func (c *RedisClient) SubKey(key string) string
SubKey strips the client prefix from a full Redis key, inverting Key for cleaned keys; the prefix itself, Key(""), maps to "".
func (*RedisClient) TryAcquireRateLimit ¶
func (c *RedisClient) TryAcquireRateLimit(ctx context.Context, key string, window time.Duration) (bool, time.Duration, error)
TryAcquireRateLimit allows one execution per window for key: it starts a window by creating "ratelimit:<key>" with SET NX PX and returns (true, 0, nil), or returns (false, time until the active window ends, nil) when the key exists. A denied call writes nothing, so polling does not extend the window, and the check and the write are one atomic command. The window, which must be at least 1ms, is measured by the Redis server's clock, and the allowed call's window applies until it ends. Under a prefix, a key whose ".." segments would leave the "ratelimit:" namespace is an error, here and in GetRateLimitRemainingTime.
func (*RedisClient) TryLock ¶
func (c *RedisClient) TryLock(ctx context.Context, key string, timeout time.Duration) (string, time.Duration, error)
TryLock attempts to acquire the lock "lock:<key>" with SET NX PX, expiring after timeout, which must be at least 1ms. It returns (token, 0, nil) when acquired, where token is a random value that ReleaseLock requires, otherwise ("", remaining TTL, nil); the remaining TTL is 0 for a lock written without expiry. The acquisition and the TTL read run in one MULTI/EXEC transaction. Under a prefix, a key whose ".." segments would leave the "lock:" namespace is an error, here and in ReleaseLock and IsLocked.
func (*RedisClient) WithPrefix ¶
func (c *RedisClient) WithPrefix(prefix string) *RedisClient
WithPrefix returns a child client sharing the connection whose keys are namespaced under "/<prefix>/" (surrounding spaces and slashes are trimmed and the prefix is cleaned as a path, so "a//b" is "/a/b/"). The child's Close is a no-op. Prefixes do not nest: the parent prefix is ignored.
func (*RedisClient) ZAdd ¶
ZAdd adds member with score to the sorted set at key, updating the score if the member exists. The go-redis error is returned unwrapped.
func (*RedisClient) ZIncrBy ¶
func (c *RedisClient) ZIncrBy(ctx context.Context, key string, increment float64, member string) error
ZIncrBy adds increment to the score of member in the sorted set at key. The go-redis error is returned unwrapped.
func (*RedisClient) ZRem ¶
ZRem removes members from the sorted set at key. The go-redis error is returned unwrapped.
func (*RedisClient) ZRemRangeByRank ¶
func (c *RedisClient) ZRemRangeByRank(ctx context.Context, key string, start, stop int64) (int64, error)
ZRemRangeByRank removes the elements of the sorted set at key between the start and stop ranks (0-based, ascending by score) and returns the count removed. Use (0, -N-1) to keep only the top N. The go-redis error is returned unwrapped.
func (*RedisClient) ZRevRangeWithScores ¶
func (c *RedisClient) ZRevRangeWithScores(ctx context.Context, key string, start, stop int64) ([]redis.Z, error)
ZRevRangeWithScores returns the elements of the sorted set at key between the start and stop ranks ordered by score from high to low, with scores. Use (0, N-1) for the top N and (-N, -1) for the bottom N.