cache

package module
v1.5.1 Latest Latest
Warning

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

Go to latest
Published: Mar 17, 2024 License: MIT Imports: 13 Imported by: 0

README

Redis Cache

Redis Cache is a library for caching any data structure in Redis. Redis Cache is meant to be used with the official Redis Go client and works by unmarshalling and marshaling data structures from/to bytes automatically. By default, Redis Cache will use msgpack to marshal/unmarshal data, but you can customize the behavior by providing your own Marshaller and Unmarshaller using the Serialization option with the NewCache function.

Features

  • Save/Load any data structure that can be represented as bytes/string
  • Marshalling/Unmarshalling
  • Compression
  • Instrumentation/Metrics for Prometheus or OpenTelemetry

Requirements

  • Go 1.19+
  • Redis 6+

Getting Redis Cache

go get github.com/jkratz55/redis-cache

Usage

Under the hood Redis Cache was designed to be used with go-redis. However, it can work with any type that implements the RedisClient interface.

type RedisClient interface {
    Get(ctx context.Context, key string) *redis.StringCmd
    GetEx(ctx context.Context, key string, expiration time.Duration) *redis.StringCmd
    MGet(ctx context.Context, keys ...string) *redis.SliceCmd
    Set(ctx context.Context, key string, val any, ttl time.Duration) *redis.StatusCmd
    SetNX(ctx context.Context, key string, value any, expiration time.Duration) *redis.BoolCmd
    SetXX(ctx context.Context, key string, value any, expiration time.Duration) *redis.BoolCmd
    Del(ctx context.Context, keys ...string) *redis.IntCmd
    Watch(ctx context.Context, fn func(*redis.Tx) error, keys ...string) error
    Scan(ctx context.Context, cursor uint64, match string, count int64) *redis.ScanCmd
    FlushDB(ctx context.Context) *redis.StatusCmd
    FlushDBAsync(ctx context.Context) *redis.StatusCmd
    Ping(ctx context.Context) *redis.StatusCmd
    TTL(ctx context.Context, key string) *redis.DurationCmd
    Expire(ctx context.Context, key string, expiration time.Duration) *redis.BoolCmd
}

This means that the Cache type can work with the following types in the go-redis client library.

  • redis.Client
  • redis.ClusterClient
  • redis.Ring

The following example shows the basic usage of the Redis Cache library.

package main

import (
	"context"
	"fmt"

	"github.com/redis/go-redis/v9"

	rcache "github.com/jkratz55/redis-cache"
)

type Person struct {
	FirstName string
	LastName  string
	Age       int
}

func main() {
	client := redis.NewClient(&redis.Options{
		Addr: "localhost:6379",
	})

	cache := rcache.NewCache(client)

	if err := cache.Set(context.Background(), "person", Person{
		FirstName: "Biily",
		LastName:  "Bob",
		Age:       45,
	}); err != nil {
		panic("ohhhhh snap!")
	}

	var p Person
	if err := cache.Get(context.Background(), "person", &p); err != nil {
		panic("ohhhhh snap")
	}
	fmt.Printf("%v\n", p)

	if err := cache.Delete(context.Background(), "person"); err != nil {
		panic("ohhh snap!")
	}

	if err := cache.Get(context.Background(), "person", &p); err != rcache.ErrKeyNotFound {
		panic("ohhhhh snap, this key should be gone!")
	}
}

If you wanted to use json instead of msgpack you could have customized the Cache like the example below.

marshaller := func(v any) ([]byte, error) {
    return json.Marshal(v)
}
unmarshaller := func(data []byte, v any) error {
    return json.Unmarshal(data, v)
}
rdb := rcache.NewCache(client, rcache.Serialization(marshaller, unmarshaller))

Because of limitations in GO's implementation of generics MGet is a function instead of a method on the Cache type. The MGet function accepts the Cache type as an argument to leverage the same marshaller and unmarshaller.

This library also supports atomic updates of existing keys by using the Upsert and UpsertTTL functions. If the key was modified while the upsert is in progress it will return RetryableError signaling the operation can be retried and the UpsertCallback can decide how to handle merging the changes.

Compression

In some cases compressing values stored in Redis can have tremendous benefits, particularly when storing large volumes of data, large values per key, or both. Compression reduces the size of the cache, significantly decreases bandwidth and latency but at the cost of additional CPU consumption.

This library can be configured to automatically compress and decompress values using the Compression option when calling NewCache. It accepts a Codec and out of the box gzip, flate, and lz4 are supported. However, you are free to compress data any way you please by implementing the Codec interface.

package main

import (
	"context"
	"fmt"

	"github.com/redis/go-redis/v9"

	rcache "github.com/jkratz55/redis-cache"
)

type Person struct {
	FirstName string
	LastName  string
	Age       int
}

func main() {
	client := redis.NewClient(&redis.Options{
		Addr: "localhost:6379",
	})

	c := rcache.NewCache(client, rcache.GZip())

	if err := c.Set(context.Background(), "person", Person{
		FirstName: "Biily",
		LastName:  "Bob",
		Age:       45,
	}); err != nil {
		panic("ohhhhh snap!")
	}

	var p Person
	if err := c.Get(context.Background(), "person", &p); err != nil {
		panic("ohhhhh snap")
	}
	fmt.Printf("%v\n", p)

	if err := c.Delete(context.Background(), "person"); err != nil {
		panic("ohhh snap!")
	}

	if err := c.Get(context.Background(), "person", &p); err != rcache.ErrKeyNotFound {
		panic("ohhhhh snap, this key should be gone!")
	}
}

Instrumentation

This library provides out of the box instrumentation for either Prometheus or OpenTelemetry. Instrumentation is provided for both the Redis Client and the Cache with minimal code.

Example for Prometheus:

func main() {

	redisClient := redis.NewClient(&redis.Options{
		Addr:         "localhost:6379",
		MinIdleConns: 10,
		MaxIdleConns: 100,
		PoolSize:     1000,
	})

	if err := redisClient.Ping(context.Background()).Err(); err != nil {
		fmt.Println("Opps ping to Redis failed!", err)
	}

	// Enable Redis client metrics
	if err := prometheus.InstrumentClientMetrics(redisClient); err != nil {
		panic(err)
	}

	rdb := cache.NewCache(redisClient)

	// Enable Cache metrics
	if err := prometheus.InstrumentMetrics(rdb); err != nil {
		panic(err)
	}

	// write some useful code here ...
}

The InstrumentClientMetrics and InstrumentMetrics functions accept Options to customize the metrics configuration if needed.

Documentation

Overview

Package cache implements a cache backed by Redis and the official Redis Go client.

Redis Cache provides an implementation of a Cache backed by Redis that handles serialization, compression, and metrics with little to no effort from the user.

Index

Constants

View Source
const (
	// InfiniteTTL indicates a key will never expire.
	//
	// Depending on Redis configuration keys may still be evicted if Redis is
	// under memory pressure in accordance to the eviction policy configured.
	InfiniteTTL time.Duration = -3

	// KeepTTL indicates to keep the existing TTL on the key on SET commands.
	KeepTTL = redis.KeepTTL
)

Variables

View Source
var (
	// ErrKeyNotFound is an error value that signals the key requested does not
	// exist in the cache.
	ErrKeyNotFound = errors.New("key not found")
)

Functions

func IsRetryable added in v0.5.0

func IsRetryable(err error) bool

IsRetryable accepts an error and returns a boolean indicating if the operation that generated the error is retryable.

func Upsert added in v0.5.0

func Upsert[T any](ctx context.Context, c *Cache, key string, val T, cb UpsertCallback[T]) error

Upsert retrieves the existing value for a given key and invokes the UpsertCallback. The UpsertCallback function is responsible for determining the value to be stored. The value returned from the UpsertCallback is what is set in Redis.

Upsert allows for atomic updates of existing records, or simply inserting new entries when the key doesn't exist.

Redis uses an optimistic locking model. If the key changes during the transaction Redis will fail the transaction and return an error. However, these errors are retryable. To determine if the error is retryable use the IsRetryable function with the returned error.

cb := rcache.UpsertCallback[Person](func(found bool, oldValue Person, newValue Person) Person {
	fmt.Println(found)
	fmt.Println(oldValue)
	fmt.Println(newValue)
	return newValue
})
retries := 3
for i := 0; i < retries; i++ {
	err := rcache.Upsert[Person](context.Background(), c, "BillyBob", p, cb)
	if rcache.IsRetryable(err) {
		continue
	}
	// do something useful ...
	break
}

func UpsertTTL added in v0.5.0

func UpsertTTL[T any](ctx context.Context, c *Cache, key string, val T, cb UpsertCallback[T], ttl time.Duration) error

UpsertTTL retrieves the existing value for a given key and invokes the UpsertCallback. The UpsertCallback function is responsible for determining the value to be stored. The value returned from the UpsertCallback is what is set in Redis.

Upsert allows for atomic updates of existing records, or simply inserting new entries when the key doesn't exist.

Redis uses an optimistic locking model. If the key changes during the transaction Redis will fail the transaction and return an error. However, these errors are retryable. To determine if the error is retryable use the IsRetryable function with the returned error.

cb := rcache.UpsertCallback[Person](func(found bool, oldValue Person, newValue Person) Person {
	fmt.Println(found)
	fmt.Println(oldValue)
	fmt.Println(newValue)
	return newValue
})
retries := 3
for i := 0; i < retries; i++ {
	err := rcache.UpsertTTL[Person](context.Background(), c, "BillyBob", p, cb, time.Minute * 1)
	if rcache.IsRetryable(err) {
		continue
	}
	// do something useful ...
	break
}

func Version added in v1.1.0

func Version() string

Version returns the current version of redis-cache.

Types

type Cache

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

Cache is a simple type that provides basic caching functionality: store, retrieve, and delete. It is backed by Redis and supports storing entries with a TTL.

The zero-value is not usable, and this type should be instantiated using the New function.

func New added in v1.1.0

func New(client RedisClient, opts ...Option) *Cache

New creates and initializes a new Cache instance.

By default, msgpack is used for marshalling and unmarshalling the entry values. The behavior of Cache can be configured by passing Options.

func NewCache

func NewCache(client RedisClient, opts ...Option) *Cache

NewCache creates and initializes a new Cache instance.

By default, msgpack is used for marshalling and unmarshalling the entry values. The behavior of Cache can be configured by passing Options.

DEPRECATED: NewCache has been deprecated in favor of New and subject to being removed in future versions.

func (*Cache) AddHook added in v1.1.0

func (hs *Cache) AddHook(hook Hook)

AddHook adds a Hook to the processing chain.

func (*Cache) Delete

func (c *Cache) Delete(ctx context.Context, keys ...string) error

Delete removes entries from the cache for a given set of keys.

func (*Cache) Expire added in v1.1.0

func (c *Cache) Expire(ctx context.Context, key string, ttl time.Duration) error

Expire sets a TTL on the given key.

If the key doesn't exist ErrKeyNotFound will be returned for the error value. If the key already has a TTL it will be overridden with ttl value provided.

Calling Expire with a non-positive ttl will result in the key being deleted.

func (*Cache) ExtendTTL added in v1.1.0

func (c *Cache) ExtendTTL(ctx context.Context, key string, dur time.Duration) error

ExtendTTL extends the TTL for the key by the given duration.

ExtendTTL retrieves the TTL remaining for the key, adds the duration, and then executes the EXPIRE command to set a new TTL.

If the key doesn't exist ErrKeyNotFound will be returned for the error value.

func (*Cache) Flush added in v0.6.0

func (c *Cache) Flush(ctx context.Context) error

Flush flushes the cache deleting all keys/entries.

func (*Cache) FlushAsync added in v0.6.0

func (c *Cache) FlushAsync(ctx context.Context) error

FlushAsync flushes the cache deleting all keys/entries asynchronously. Only keys that were present when FLUSH ASYNC command was received by Redis will be deleted. Any keys created during asynchronous flush will be unaffected.

func (*Cache) Get

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

Get retrieves an entry from the Cache for the given key, and if found will unmarshall the value into v.

If the key does not exist ErrKeyNotFound will be returned as the error value. A non-nil error value will be returned if the operation on the backing Redis fails, or if the value cannot be unmarshalled into the target type.

func (*Cache) GetAndExpire added in v1.1.0

func (c *Cache) GetAndExpire(ctx context.Context, key string, v any, ttl time.Duration) error

GetAndExpire retrieves a value from the Cache for the given key, decompresses it if applicable, unmarshalls the value to v, and sets the TTL for the key.

If the key does not exist ErrKeyNotFound will be returned as the error value. A non-nil error value will be returned if the operation on the backing Redis fails, or if the value cannot be unmarshalled into the target type.

Passing a negative ttl value has no effect on the existing TTL for the key. Passing a ttl value of 0 or InfiniteTTL removes the TTL and persist the key until it is either explicitly deleted or evicted according to Redis eviction policy.

func (*Cache) Healthy added in v1.1.0

func (c *Cache) Healthy(ctx context.Context) bool

Healthy pings Redis to ensure it is reachable and responding. Healthy returns true if Redis successfully responds to the ping, otherwise false.

func (*Cache) Keys added in v0.6.0

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

Keys retrieves all the keys in Redis/Cache

func (*Cache) MSet added in v1.2.0

func (c *Cache) MSet(ctx context.Context, keyvalues map[string]any) error

MSet performs multiple SET operations. Entries are added to the cache or overridden if they already exists.

MSet is atomic, either all keyvalues are set or none are set. Since MSet operates using a single atomic command it is the fastest way to bulk write entries to the Cache. It greatly reduces network overhead and latency when compared to calling SET sequentially.

func (*Cache) MSetWithTTL added in v1.3.0

func (c *Cache) MSetWithTTL(ctx context.Context, keyvalues map[string]any, ttl time.Duration) error

MSetWithTTL adds or overwrites multiple entries to the cache with a TTL value.

MSetWithTTL performs multiple SET operations using a Pipeline. Unlike MSet, MSetWithTTL is not atomic. It is possible for some entries to succeed while others fail. When the pipeline itself executes successfully but commands fail MSetWithTTL returns CommandErrors which can be inspected to understand which keys failed in the event they need to be retried or logged. If the pipeline execution fails, or marshal and compression fails, a standard error value is returned.

The command errors can be inspected following this example:

err := cache.MSetWithTTL(context.Background(), data, time.Minute*30)
if err != nil {
	var cmdErrs CommandErrors
	if errors.As(err, &cmdErrs) {
		for _, e := range cmdErrs {
			fmt.Println(e)
		}
		// todo: do something actually useful here
	} else {
		fmt.Println(err) // just a normal error value
	}
}

func (*Cache) ScanKeys added in v1.4.0

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

ScanKeys allows for scanning keys in Redis using a pattern.

func (*Cache) Set

func (c *Cache) Set(ctx context.Context, key string, v any) error

Set adds an entry into the cache, or overwrites an entry if the key already existed. The entry is set without an expiration.

func (*Cache) SetIfAbsent added in v0.3.0

func (c *Cache) SetIfAbsent(ctx context.Context, key string, v any, ttl time.Duration) (bool, error)

SetIfAbsent adds an entry into the cache only if the key doesn't already exist. The entry is set with the provided TTL and automatically removed from the cache once the TTL is expired.

func (*Cache) SetIfPresent added in v0.3.0

func (c *Cache) SetIfPresent(ctx context.Context, key string, v any, ttl time.Duration) (bool, error)

SetIfPresent updates an entry into the cache if they key already exists in the cache. The entry is set with the provided TTL and automatically removed from the cache once the TTL is expired.

func (*Cache) SetWithTTL

func (c *Cache) SetWithTTL(ctx context.Context, key string, v any, ttl time.Duration) error

SetWithTTL adds an entry into the cache, or overwrites an entry if the key already existed. The entry is set with the provided TTL and automatically removed from the cache once the TTL is expired.

func (*Cache) TTL added in v1.1.0

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

TTL returns the time to live for a particular key.

If the key doesn't exist ErrKeyNotFound will be returned for the error value. If the key doesn't have a TTL InfiniteTTL will be returned.

type Codec added in v1.0.0

type Codec interface {
	Flate(data []byte) ([]byte, error)
	Deflate(data []byte) ([]byte, error)
}

Codec is an interface type that defines the behavior for compressing and decompressing data.

type CommandError added in v1.3.0

type CommandError struct {
	Command string
	Key     string
	Err     error
}

func (CommandError) Error added in v1.3.0

func (r CommandError) Error() string

type CommandErrors added in v1.3.0

type CommandErrors []CommandError

func (CommandErrors) Error added in v1.3.0

func (r CommandErrors) Error() string

func (CommandErrors) Keys added in v1.3.0

func (r CommandErrors) Keys() []string

type CompressionHook added in v1.1.0

type CompressionHook func(data []byte) ([]byte, error)

CompressionHook is a function type that is invoked prior to compressing or decompressing data.

type Hook added in v1.1.0

type Hook interface {
	MarshalHook(next Marshaller) Marshaller
	UnmarshallHook(next Unmarshaller) Unmarshaller
	CompressHook(next CompressionHook) CompressionHook
	DecompressHook(next CompressionHook) CompressionHook
}

Hook is an interface type defining the operations that can be intercepted and potentially allow for their behavior to be modified.

The primary intention of Hook is to allow for observability: instrumentation, logging, tracing, etc.

It is important implementations of Hook call next or the execution pipeline will terminate.

type Marshaller

type Marshaller func(v any) ([]byte, error)

Marshaller is a function type that marshals the value of a cache entry for storage.

func DefaultMarshaller

func DefaultMarshaller() Marshaller

DefaultMarshaller returns a Marshaller using msgpack to marshall values.

type MultiResult added in v0.2.0

type MultiResult[T any] map[string]T

MultiResult is a type representing returning multiple entries from the Cache.

func MGet added in v0.2.0

func MGet[R any](ctx context.Context, c *Cache, keys ...string) (MultiResult[R], error)

MGet uses the provided Cache to retrieve multiple keys from Redis and returns a MultiResult.

func Scan added in v1.5.0

func Scan[T any](ctx context.Context, c *Cache, pattern string) (MultiResult[T], error)

Scan retrieves all the keys and values from Redis matching the given pattern.

Scan works similar to MGet, but allows a pattern to be specified rather than providing keys.

func (MultiResult[T]) Get added in v0.2.0

func (mr MultiResult[T]) Get(key string) (T, bool)

Get returns the value and a boolean indicating if the key exists. If the key doesn't exist the value will be the default zero value.

func (MultiResult[T]) IsEmpty added in v0.2.0

func (mr MultiResult[T]) IsEmpty() bool

IsEmpty returns a boolean indicating if the results are empty.

func (MultiResult[T]) Keys added in v0.2.0

func (mr MultiResult[T]) Keys() []string

Keys returns all the keys found.

func (MultiResult[T]) Values added in v0.2.0

func (mr MultiResult[T]) Values() []T

Values returns all the values found.

type Option

type Option func(c *Cache)

Option allows for the Cache behavior/configuration to be customized.

func Brotli added in v1.1.0

func Brotli() Option

Brotli configures the Cache to use Brotli for compressing and decompressing values stored in Redis. The default Brotli configuration uses a balanced approach between speed and compression level.

func Compression added in v1.0.0

func Compression(codec Codec) Option

Compression allows for the values to be flated and deflated to conserve bandwidth and memory at the cost of higher CPU time. Compression accepts a Codec to handle compressing and decompressing the data to/from Redis.

func Flate added in v1.0.0

func Flate() Option

Flate configures the Cache to use Flate Codec for compressing and decompressing values stored in Redis. Flate uses a default configuration favoring compression over speed.

func GZip added in v1.0.0

func GZip() Option

GZip configures the Cache to use gzip for compressing and decompressing values stored in Redis. GZip uses a default configuration favoring compression size over speed

func JSON added in v1.1.0

func JSON() Option

JSON is a convenient Option for configuring Cache to use JSON for serializing data stored in the cache.

JSON is the equivalent of using Serialization passing it a Marshaller and Unmarshaller using json.

func LZ4 added in v1.0.0

func LZ4() Option

LZ4 configures the Cache to use lz4 for compressing and decompressing values stored in Redis.

func Serialization

func Serialization(mar Marshaller, unmar Unmarshaller) Option

Serialization allows for the marshalling and unmarshalling behavior to be customized for the Cache.

A valid Marshaller and Unmarshaller must be provided. Providing nil for either will immediately panic.

type RedisClient added in v0.3.0

type RedisClient interface {
	Get(ctx context.Context, key string) *redis.StringCmd
	GetEx(ctx context.Context, key string, expiration time.Duration) *redis.StringCmd
	MGet(ctx context.Context, keys ...string) *redis.SliceCmd
	Set(ctx context.Context, key string, val any, ttl time.Duration) *redis.StatusCmd
	SetNX(ctx context.Context, key string, value any, expiration time.Duration) *redis.BoolCmd
	SetXX(ctx context.Context, key string, value any, expiration time.Duration) *redis.BoolCmd
	MSet(ctx context.Context, values ...any) *redis.StatusCmd
	Del(ctx context.Context, keys ...string) *redis.IntCmd
	Watch(ctx context.Context, fn func(*redis.Tx) error, keys ...string) error
	Scan(ctx context.Context, cursor uint64, match string, count int64) *redis.ScanCmd
	FlushDB(ctx context.Context) *redis.StatusCmd
	FlushDBAsync(ctx context.Context) *redis.StatusCmd
	Ping(ctx context.Context) *redis.StatusCmd
	TTL(ctx context.Context, key string) *redis.DurationCmd
	Expire(ctx context.Context, key string, expiration time.Duration) *redis.BoolCmd
	Pipeline() redis.Pipeliner
}

RedisClient is an interface type that defines the Redis functionality this package requires to use Redis as a cache.

type RetryableError added in v0.5.0

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

func (RetryableError) Error added in v0.5.0

func (e RetryableError) Error() string

func (RetryableError) IsRetryable added in v0.5.0

func (e RetryableError) IsRetryable() bool

type Unmarshaller

type Unmarshaller func(b []byte, v any) error

Unmarshaller is a function type that unmarshalls the value retrieved from the cache into the target type.

func DefaultUnmarshaller

func DefaultUnmarshaller() Unmarshaller

DefaultUnmarshaller returns an Unmarshaller using msgpack to unmarshall values.

type UpsertCallback added in v0.5.0

type UpsertCallback[T any] func(found bool, oldValue T, newValue T) T

UpsertCallback is a callback function that is invoked by Upsert. An UpsertCallback is passed if a key was found, the old value (or zero-value if the key wasn't found) and the new value. An UpsertCallback is responsible for determining what value should be set for a given key in the cache. The value returned from UpsertCallback is the value set.

Directories

Path Synopsis
compression
lz4
examples

Jump to

Keyboard shortcuts

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