redisclient

package
v1.0.442 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 9, 2026 License: MIT Imports: 15 Imported by: 0

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

Constants

This section is empty.

Variables

View Source
var ErrNotFound = errors.New("not found")

ErrNotFound is returned by Get and HGet for a missing key or field.

Functions

func IsNotFoundError

func IsNotFoundError(err error) bool

IsNotFoundError reports whether err is (or wraps) ErrNotFound, or whose message contains "not found".

func Marshal

func Marshal(v any) (any, error)

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

func NewRedisClient(cfg *Config) (*redis.Client, error)

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.

func UnmarshalStringCmd

func UnmarshalStringCmd(val *redis.StringCmd, v any) error

UnmarshalStringCmd unmarshals the value from Redis storage into the provided variable. string and []byte are converted as-is, while JSON is unmarshaled into the target type.

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

type RedisClient struct {
	*redis.Client
	// contains filtered or unexported fields
}

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) Exists

func (c *RedisClient) Exists(ctx context.Context, key string) (bool, error)

Exists reports whether key exists.

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

func (c *RedisClient) Get(ctx context.Context, key string, v any) error

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) HDel

func (c *RedisClient) HDel(ctx context.Context, key string, fields ...string) error

HDel removes fields from the hash at key.

func (*RedisClient) HExists

func (c *RedisClient) HExists(ctx context.Context, key string, field string) (bool, error)

HExists reports whether the hash at key has field.

func (*RedisClient) HGet

func (c *RedisClient) HGet(ctx context.Context, key string, field string) (string, error)

HGet returns a field of the hash at key, or ErrNotFound when the field or the hash is missing.

func (*RedisClient) HGetAll

func (c *RedisClient) HGetAll(ctx context.Context, key string) (map[string]string, error)

HGetAll returns all fields of the hash at key (empty map when missing).

func (*RedisClient) HKeys

func (c *RedisClient) HKeys(ctx context.Context, key string) ([]string, error)

HKeys returns the field names of the hash at key.

func (*RedisClient) HSet

func (c *RedisClient) HSet(ctx context.Context, key string, field string, value any) error

HSet sets a single field of the hash at key.

func (*RedisClient) HSetMany

func (c *RedisClient) HSetMany(ctx context.Context, key string, values map[string]any) error

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) HVals

func (c *RedisClient) HVals(ctx context.Context, key string) ([]string, error)

HVals returns the field values of the hash at key.

func (*RedisClient) IsLocked

func (c *RedisClient) IsLocked(ctx context.Context, key string) (bool, error)

IsLocked reports whether anyone holds the lock "lock:<key>".

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

func (c *RedisClient) Keys(ctx context.Context, pattern string) ([]string, error)

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) LIndex

func (c *RedisClient) LIndex(ctx context.Context, key string, index int64) (string, error)

LIndex returns the element at index in the list at key.

func (*RedisClient) LLen

func (c *RedisClient) LLen(ctx context.Context, key string) (int64, error)

LLen returns the length of the list at key (0 when missing).

func (*RedisClient) LPop

func (c *RedisClient) LPop(ctx context.Context, key string) (string, error)

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) LPush

func (c *RedisClient) LPush(ctx context.Context, key string, values ...any) error

LPush prepends values to the list at key.

func (*RedisClient) LRange

func (c *RedisClient) LRange(ctx context.Context, key string, start, stop int64) ([]string, error)

LRange returns the elements of the list at key between the start and stop indexes (inclusive, negative counts from the end).

func (*RedisClient) LTrim

func (c *RedisClient) LTrim(ctx context.Context, key string, start, stop int64) error

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

func (c *RedisClient) RPop(ctx context.Context, key string) (string, error)

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) RPush

func (c *RedisClient) RPush(ctx context.Context, key string, values ...any) error

RPush appends values to the list at key.

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

func (c *RedisClient) ReleaseLock(ctx context.Context, key, token string) (bool, error)

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) SAdd

func (c *RedisClient) SAdd(ctx context.Context, key string, members ...any) error

SAdd adds members to the set at key.

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) SCard

func (c *RedisClient) SCard(ctx context.Context, key string) (int64, error)

SCard returns the number of members of the set at key.

func (*RedisClient) SIsMember

func (c *RedisClient) SIsMember(ctx context.Context, key string, member any) (bool, error)

SIsMember reports whether member belongs to the set at key.

func (*RedisClient) SMembers

func (c *RedisClient) SMembers(ctx context.Context, key string) ([]string, error)

SMembers returns all members of the set at key.

func (*RedisClient) SRem

func (c *RedisClient) SRem(ctx context.Context, key string, members ...any) error

SRem removes members from the set at key.

func (*RedisClient) ScanKeys

func (c *RedisClient) ScanKeys(ctx context.Context, pattern string, limit int) ([]string, error)

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

func (c *RedisClient) Set(ctx context.Context, key string, v any, expiration time.Duration) error

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) TTL

func (c *RedisClient) TTL(ctx context.Context, key string) (time.Duration, error)

TTL returns the remaining TTL of key (-1 for no expiry, -2 when missing).

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

func (c *RedisClient) ZAdd(ctx context.Context, key string, score float64, member string) error

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) ZCard

func (c *RedisClient) ZCard(ctx context.Context, key string) (int64, error)

ZCard returns the number of members of the sorted set at key.

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

func (c *RedisClient) ZRem(ctx context.Context, key string, members ...any) error

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL