goredis

package module
v0.1.2 Latest Latest
Warning

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

Go to latest
Published: Aug 13, 2026 License: MIT Imports: 9 Imported by: 0

README

goredis

Tag-based cache invalidation layer over go-redis.

The Problem

Cache invalidation is hard. When a piece of data changes, you need to clear every cached key that depends on it. Without a relation tracker, you either:

  • TTL everything out (stale data window)
  • Delete by fragile key patterns (SCAN with prefix)
  • Manually track every key (error-prone)

The Solution

Each cache key is registered under one or more tags (Redis Sets). When data changes, invalidate by tag — all associated keys are discovered and deleted in parallel.

SET goredis:user:42        → "goredis:tag:users"    SADD user:42
SET goredis:user:list      → "goredis:tag:users"    SADD user:list
SET goredis:stats          → "goredis:tag:dashboard" SADD stats
                             "goredis:tag:users"    SADD stats

InvalidateByTags(ctx, cache, "users")
    → deletes: goredis:user:42, goredis:user:list, goredis:stats
    → deletes: goredis:tag:users

A reverse mapping (goredis:keytags:<key>) is also stored with the same TTL, enabling periodic garbage collection of stale tag set members via CleanTag.

Installation

go get github.com/aruncs31s/goredis

Quick Start

import (
    "context"
    "time"
    "github.com/go-redis/redis/v8"
    "github.com/aruncs31s/goredis"
)

ctx := context.Background()
rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
cache := goredis.New(rdb)

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

// Cache a user under the "users" tag
user, err := goredis.CacheGetOrFetch(ctx, cache, "user:42", 5*time.Minute,
    func() (*User, error) {
        return &User{ID: 42, Name: "Alice"}, nil
    },
    "users",
)

// After a user update, clear all cached data tagged "users"
cache.InvalidateByTags(ctx, "users")

Features

  • Generic — typed CacheGetOrFetch[T], no type assertions
  • Singleflight — concurrent misses for the same key coalesce into one fetch (stampede protection)
  • Tag-based invalidation — delete all keys under a tag in one call
  • Key→tags reverse mapping — enables periodic stale-key cleanup via CleanTag
  • Pluggable codec — swap JSON for msgpack, protobuf, gob, etc.
  • Metrics hooks — plug in OpenTelemetry, Prometheus, or your own
  • Namespace support — prefix all keys to support multi-tenant or environment isolation
  • Atomic SET+SADD — uses MULTI/EXEC (TxPipeline) for atomicity
  • Batch APIsCacheGetMany, CacheSetMany, InvalidatePrefix, InvalidatePattern
  • Nil-safe — pass nil for the Redis client to silently skip caching

API

New
func New(rdb *redis.Client, opts ...Option) *Client

Creates a cache client. Options:

  • WithCodec(codec Codec) — custom serializer (default: JSON)
  • WithNamespace(ns string) — key prefix (default: "goredis")
  • WithMetrics(m Metrics) — observability hooks
CacheGetOrFetch[T]
func CacheGetOrFetch[T any](
    ctx context.Context,
    c *Client,
    key string,
    ttl time.Duration,
    fetch func() (T, error),
    tags ...string,
) (T, error)

Cache-aside pattern with singleflight stampede protection. Returns cached value if found, otherwise calls fetch, stores the result, and registers the key under the given tags.

CacheGet[T]
func CacheGet[T any](ctx context.Context, c *Client, key string) (T, bool)

Low-level get. Returns the value and true on cache hit.

CacheGetMany[T]
func CacheGetMany[T any](ctx context.Context, c *Client, keys ...string) map[string]T

Bulk retrieval via MGET. Missing keys are omitted from the result map.

CacheSet
func (c *Client) CacheSet(ctx context.Context, key string, value any, ttl time.Duration, tags ...string) error

Store a value and register it under the given tags. Uses MULTI/EXEC for atomicity across SET, SADD, and EXPIRE.

CacheSetMany
func (c *Client) CacheSetMany(ctx context.Context, items map[string]any, ttl time.Duration, tags ...string) error

Atomic bulk set. All keys are registered under the same tags.

InvalidateByTags
func (c *Client) InvalidateByTags(ctx context.Context, tags ...string) error

Delete every cache key registered under the given tags, clean up the tag index Sets and reverse mappings. Keys are deduplicated across overlapping tags. SMembers lookups run concurrently (one goroutine per tag). All deletions are batched in a pipeline.

InvalidatePrefix
func (c *Client) InvalidatePrefix(ctx context.Context, prefix string) error

Delete all cache keys whose logical key starts with prefix (uses SCAN + DEL).

InvalidatePattern
func (c *Client) InvalidatePattern(ctx context.Context, pattern string) error

Delete all cache keys matching a glob pattern (uses SCAN + DEL).

CleanTag
func (c *Client) CleanTag(ctx context.Context, tag string) error

Remove stale (expired) key references from a tag set. Useful as a periodic maintenance task — run it as a cron job to prevent tag sets from accumulating garbage.

HashRequest
func (c *Client) HashRequest(req any) (string, error)

Deterministic SHA-256 hex string from a request struct. Useful for generating cache keys.

Passing nil for rdb

All functions accept nil for the Redis client — caching is silently skipped. This makes it easy to conditionally enable Redis without sprinkling if checks at every call site.

Codec

The default codec is JSON. Swap it out:

import "github.com/vmihailenco/msgpack/v5"

cache := goredis.New(rdb, goredis.WithCodec(msgpackCodec{}))

Implement the Codec interface for any format:

type Codec interface {
    Marshal(any) ([]byte, error)
    Unmarshal([]byte, any) error
}

Metrics

Implement the Metrics interface to hook into cache events:

type Metrics interface {
    Hit(key string)
    Miss(key string)
    Set(key string)
    Invalidate(tags []string)
    FetchDuration(key string, d time.Duration)
}

Namespace

Isolate environments or tenants:

cache := goredis.New(rdb, goredis.WithNamespace("prod"))

Keys will be prefixed as prod:<key>, prod:tag:<tagname>, prod:keytags:<key>.

How It Works

  1. CacheSet marshals the value using the configured codec, then opens a MULTI/EXEC transaction to atomically SET the key, SADD it to each tag's Set, and store the reverse key→tags mapping. The tag Set gets a TTL of ttl + 1 minute to automatically clean up stale references.

  2. InvalidateByTags fans out SMembers calls across goroutines (one per tag), deduplicates all referenced cache keys, then issues a pipeline DEL for all keys, reverse mappings, and tag Sets.

  3. CacheGetOrFetch uses singleflight to coalesce concurrent misses for the same key — only one goroutine calls the fetch function while the others wait for its result.

  4. CleanTag checks each member of a tag Set against EXISTS and removes stale entries, preventing garbage accumulation over time.

Documentation

Index

Constants

View Source
const DefaultNamespace = "goredis"

Variables

View Source
var ErrCacheDisabled = errors.New("redis client is nil, caching disabled")

Functions

func CacheGet

func CacheGet[T any](ctx context.Context, c *Client, key string) (T, bool)

CacheGet retrieves a cached value.

func CacheGetMany added in v0.1.0

func CacheGetMany[T any](ctx context.Context, c *Client, keys ...string) map[string]T

CacheGetMany retrieves multiple values at once via MGET.

func CacheGetManyT added in v0.1.0

func CacheGetManyT[T any](ctx context.Context, m *MultiTenantClient, tenantID string, keys ...string) map[string]T

CacheGetMany retrieves multiple values at once for a tenant.

func CacheGetOrFetch

func CacheGetOrFetch[T any](
	ctx context.Context,
	c *Client,
	key string,
	ttl time.Duration,
	fetch func() (T, error),
	tags ...string,
) (T, error)

CacheGetOrFetch retrieves a value by key, calling fetch on miss. Concurrent requests for the same key are coalesced using singleflight to prevent cache stampedes.

func CacheGetOrFetchT added in v0.1.0

func CacheGetOrFetchT[T any](
	ctx context.Context,
	m *MultiTenantClient,
	tenantID, key string,
	ttl time.Duration,
	fetch func() (T, error),
	tags ...string,
) (T, error)

CacheGetOrFetch retrieves a value by key for a tenant, calling fetch on miss.

func CacheGetT added in v0.1.0

func CacheGetT[T any](ctx context.Context, m *MultiTenantClient, tenantID, key string) (T, bool)

CacheGet retrieves a cached value for a tenant.

func Get added in v0.1.2

func Get[T any](
	ctx context.Context,
	c *Client,
	key string,
	ttl time.Duration,
	fetch func() (T, error),
	tags ...string,
) (T, error)

func GetT added in v0.1.2

func GetT[T any](
	ctx context.Context,
	m *MultiTenantClient,
	tenantID, key string,
	ttl time.Duration,
	fetch func() (T, error),
	tags ...string,
) (T, error)

GetT retrieves a value by key for a tenant, calling fetch on miss.

func HashRequest

func HashRequest(req any) string

Types

type Client added in v0.1.0

type Client struct {
	// contains filtered or unexported fields
}

Client is a tag-based cache invalidation layer over go-redis.

func New added in v0.1.0

func New(rdb *redis.Client, opts ...Option) *Client

func (*Client) CacheSet added in v0.1.0

func (c *Client) CacheSet(
	ctx context.Context,
	key string,
	value any,
	ttl time.Duration,
	tags ...string,
) error

CacheSet stores a value and registers it under the given tags. Uses MULTI/EXEC (TxPipeline) for atomicity across SET, SADD, EXPIRE.

func (*Client) CacheSetMany added in v0.1.0

func (c *Client) CacheSetMany(
	ctx context.Context,
	items map[string]any,
	ttl time.Duration,
	tags ...string,
) error

CacheSetMany stores multiple values atomically.

func (*Client) CleanTag added in v0.1.0

func (c *Client) CleanTag(ctx context.Context, tag string) error

CleanTag removes stale key references from a tag set by checking whether each member still exists in the cache. Useful as a periodic maintenance task to prevent tag sets from accumulating garbage.

func (*Client) HashRequest added in v0.1.0

func (c *Client) HashRequest(req any) (string, error)

HashRequest generates a deterministic hex hash from a request struct.

func (*Client) InvalidateByTags added in v0.1.0

func (c *Client) InvalidateByTags(ctx context.Context, tags ...string) error

InvalidateByTags deletes all cache keys associated with the given tags. Keys are deduplicated across tags, and all deletions are batched in a pipeline.

func (*Client) InvalidatePattern added in v0.1.0

func (c *Client) InvalidatePattern(ctx context.Context, pattern string) error

InvalidatePattern deletes all cache keys matching the given glob pattern.

func (*Client) InvalidatePrefix added in v0.1.0

func (c *Client) InvalidatePrefix(ctx context.Context, prefix string) error

InvalidatePrefix deletes all cache keys with the given key prefix.

type Codec added in v0.1.0

type Codec interface {
	Marshal(any) ([]byte, error)
	Unmarshal([]byte, any) error
}

Codec serializes and deserializes cache values.

type JSONCodec added in v0.1.0

type JSONCodec struct{}

func (JSONCodec) Marshal added in v0.1.0

func (JSONCodec) Marshal(v any) ([]byte, error)

func (JSONCodec) Unmarshal added in v0.1.0

func (JSONCodec) Unmarshal(b []byte, v any) error

type Metrics added in v0.1.0

type Metrics interface {
	Hit(key string)
	Miss(key string)
	Set(key string)
	Invalidate(tags []string)
	FetchDuration(key string, d time.Duration)
}

Metrics hooks for observability.

type MultiTenantClient added in v0.1.0

type MultiTenantClient struct {
	// contains filtered or unexported fields
}

MultiTenantClient manages per-tenant cache clients with isolated namespaces. Each tenant gets its own *Client with namespace <base>:<tenantID>, ensuring key and tag isolation across tenants while sharing the underlying Redis connection pool.

func NewMultiTenant added in v0.1.0

func NewMultiTenant(rdb *redis.Client, base string, opts ...Option) *MultiTenantClient

NewMultiTenant creates a MultiTenantClient. base is the environment prefix (e.g. "prod", "staging"). opts are shared across all tenant clients (codec, metrics, etc.).

func (*MultiTenantClient) CacheSet added in v0.1.0

func (m *MultiTenantClient) CacheSet(
	ctx context.Context,
	tenantID, key string,
	value any,
	ttl time.Duration,
	tags ...string,
) error

CacheSet stores a value and registers it under tags for a tenant.

func (*MultiTenantClient) CacheSetMany added in v0.1.0

func (m *MultiTenantClient) CacheSetMany(
	ctx context.Context,
	tenantID string,
	items map[string]any,
	ttl time.Duration,
	tags ...string,
) error

CacheSetMany stores multiple values atomically for a tenant.

func (*MultiTenantClient) CleanTag added in v0.1.0

func (m *MultiTenantClient) CleanTag(ctx context.Context, tenantID, tag string) error

CleanTag removes stale key references from a tag set for a tenant.

func (*MultiTenantClient) HashRequest added in v0.1.0

func (m *MultiTenantClient) HashRequest(ctx context.Context, tenantID string, req any) (string, error)

HashRequest generates a deterministic hex hash from a request struct for a tenant.

func (*MultiTenantClient) InvalidateByTags added in v0.1.0

func (m *MultiTenantClient) InvalidateByTags(ctx context.Context, tenantID string, tags ...string) error

InvalidateByTags deletes all cache keys associated with the given tags for a tenant.

func (*MultiTenantClient) InvalidatePattern added in v0.1.0

func (m *MultiTenantClient) InvalidatePattern(ctx context.Context, tenantID, pattern string) error

InvalidatePattern deletes all cache keys matching the given glob pattern for a tenant.

func (*MultiTenantClient) InvalidatePrefix added in v0.1.0

func (m *MultiTenantClient) InvalidatePrefix(ctx context.Context, tenantID, prefix string) error

InvalidatePrefix deletes all cache keys with the given key prefix for a tenant.

func (*MultiTenantClient) Tenant added in v0.1.0

func (m *MultiTenantClient) Tenant(tenantID string) *Client

Client returns the per-tenant *Client for the given tenantID, creating it lazily.

type Option added in v0.1.0

type Option func(*Client)

func WithCodec added in v0.1.0

func WithCodec(codec Codec) Option

func WithMetrics added in v0.1.0

func WithMetrics(m Metrics) Option

func WithNamespace added in v0.1.0

func WithNamespace(ns string) Option

Directories

Path Synopsis
prometheus command

Jump to

Keyboard shortcuts

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