cacheme

package module
v0.1.4 Latest Latest
Warning

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

Go to latest
Published: Nov 30, 2021 License: Apache-2.0 Imports: 19 Imported by: 0

README

cacheme - Redis Caching Framework For Go

example workflow Go Report Card Mentioned in Awesome Go

English | 中文

  • Statically Typed - 100% statically typed using code generation. Drop-in replacement, no reflect/type-assertion.
  • Scale Efficiently - thundering herd protection via pub/sub.
  • Cluster Support - same API for redis & redis cluster.
  • Memoize - dynamic key params based on code generation.
  • Versioning - cache versioning for better management.
  • Pipeline - reduce io cost by redis pipeline.

🌀 Read this first: Caches, Promises and Locks. This is how caching part works in cacheme.

🌀 Real world example with Echo and Ent: https://github.com/Yiling-J/echo-ent-cacheme-example

// old
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
comment, err := ent.Comment.Get(context.Background(), int(id))

// new
comment, err := cacheme.CommentCacheStore.Get(c.Request().Context(), c.Param("id"))

Installation

go get github.com/Yiling-J/cacheme-go/cmd

After installing cacheme-go codegen, go to the root directory(or the directory you think cacheme should stay) of your project, and run:

go run github.com/Yiling-J/cacheme-go/cmd init

The command above will generate cacheme directory under current directory:

└── cacheme
    ├── fetcher
    │   └── fetcher.go
    └── schema
        └── schema.go

It's up to you where the cacheme directory should be, just remember to use the right directory in Store Generation step.

Add Schema

Edit schema.go and add some schemas:

package schema

import (
	"time"
	cacheme "github.com/Yiling-J/cacheme-go"
)

var (
	// default prefix for redis keys
	Prefix = "cacheme"
	// store schemas
	Stores = []*cacheme.StoreSchema{
		{
			Name:         "Simple",
			Key:          "simple:{{.ID}}",
			To:           "",
			Version:      1,
			TTL:          5 * time.Minute,
			Singleflight: false,
		},
	}
)

More details here

Store Generation

Run code generation from the root directory of the project as follows:

# this will use default schema path ./cacheme/schema
go run github.com/Yiling-J/cacheme-go/cmd generate

Or you can use custom schema path:

go run github.com/Yiling-J/cacheme-go/cmd generate ./yours/cacheme/schema

This produces the following files:

└── cacheme
    ├── fetcher
    │   └── fetcher.go
    ├── schema
    │   └── schema.go
    └── store.go

store.go is generated based on schemas in schema.go. Adding more schemas and run generate again.

Add Fetcher

Each cache store should provide a fetch function in fetcher.go:

func Setup() {
	cacheme.SimpleCacheStore.Fetch = func(ctx context.Context, ID string) (string, error) {
		return ID, nil
	}
}

Use Your Stores

Create client and setup fetcher
import (
	"your_project/cacheme"
	"your_project/cacheme/fetcher"
)

func main() {
	// setup fetcher
	fetcher.Setup()
	// create client
	client := cacheme.New(
		redis.NewClient(&redis.Options{
			Addr:     "localhost:6379",
			Password: "",
			DB:       0,
		}),
	)
	// or cluster client
	client := cacheme.NewCluster(
		redis.NewClusterClient(&redis.ClusterOptions{
			Addrs: []string{
				":7000",
				":7001",
				":7002"},
		}),
	)
}
Store API
Get single result: Get

Get cached result. If not in cache, call fetch function and store data to Redis.

// "foo" is the {{.ID}} part of the schema
result, err := client.SimpleCacheStore.Get(ctx, "foo")
Get pipeline results: GetP

Get multiple keys from multiple stores using pipeline. For each key, if not in cache, call fetch function and store data to Redis.

  • single store
import cachemego "github.com/Yiling-J/cacheme-go"

pipeline := cachemego.NewPipeline(client.Redis())
ids := []string{"1", "2", "3", "4"}
var ps []*cacheme.SimplePromise
for _, i := range ids {
	promise, err := client.SimpleCacheStore.GetP(ctx, pipeline, i)
	ps = append(ps, promise)
}
err = pipeline.Execute(ctx)
fmt.Println(err)

for _, promise := range ps {
	r, err := promise.Result()
	fmt.Println(r, err)
}

Consider using GetM API for single store, see GetM example below.

  • multiple stores
import cachemego "github.com/Yiling-J/cacheme-go"

// same pipeline for different stores
pipeline := cachemego.NewPipeline(client.Redis())

ids := []string{"1", "2", "3", "4"}
var ps []*cacheme.SimplePromise // cache string
var psf []*cacheme.FooPromise // cache model.Foo struct
for _, i := range ids {
	promise, err := client.SimpleCacheStore.GetP(ctx, pipeline, i)
	ps = append(ps, promise)
}
for _, i := range ids {
	promise, err := client.FooCacheStore.GetP(ctx, pipeline, i)
	psf = append(psf, promise)
}
// execute only once
err = pipeline.Execute(ctx)
// simple store results
for _, promise := range ps {
	r, err := promise.Result()
	fmt.Println(r, err)
}
// foo store results
for _, promise := range psf {
	r, err := promise.Result()
	fmt.Println(r, err)
}

Get multiple results from single store: GetM

Get multiple keys from same store, also using Redis pipeline. For each key, if not in cache, call fetch function and store data to Redis.

qs, err := client.SimpleCacheStore.GetM("foo").GetM("bar").GetM("xyz").Do(ctx)
// qs is a queryset struct, support two methods: GetSlice and Get
// GetSlice return ordered results slice
r, err := qs.GetSlice() // r: {foo_result, bar_result, xyz_result}
// Get return result of given param
r, err := qs.Get("foo") // r: foo_result
r, err := qs.Get("bar") // r: bar_result
r, err := qs.Get("fake") // error, because "fake" not in queryset

You can also initialize a getter using MGetter

getter := client.SimpleCacheStore.MGetter()
for _, id := range ids {
	getter.GetM(id)
}
qs, err := getter.Do(c.Request().Context())
Invalid single cache: Invalid
err := client.SimpleCacheStore.Invalid(ctx, "foo")
Update single cache: Update
err := client.SimpleCacheStore.Update(ctx, "foo")
Invalid all keys: InvalidAll

Warning: This method is implemented using Redis HyperLogLog + List for memory efficiency, but inaccurate(according to Redis, standard error of 0.81%). Intend use of this method is: you update store version, then calling this method to clean legacy cache.

// invalid all version 1 simple cache
client.SimpleCacheStore.InvalidAll(ctx, "1")

Schema Definition

Each schema has 5 fields:

  • Name - store name, will be struct name in generated code, capital first.
  • Key - key with variable using go template syntax, Variable name will be used in code generation.
  • To - cached value, type of value will be used in code generation. Examples:
    • string: ""
    • int: 1
    • struct: model.Foo{}
    • struct pointer: &model.Foo{}
    • slice: []model.Foo{}
    • map: map[model.Foo]model.Bar{}
  • Version - version interface, can be string, int, or callable func() string.
  • TTL - redis ttl using go time.
  • Singleflight - bool, if true, concurrent requests to same key on same executable will call Redis only once
Notes:
  • Duplicate name/key is not allowed.
  • Everytime you update schema, run code generation again.
  • Not all store API support Singleflight option:
    • Get: support.
    • GetM: support. singleflight key will be the combination of all keys, order by alphabetical.
    // these two will use same singleflight group key
    store.GetM("foo").GetM("bar").GetM("xyz").Do(ctx)
    Store.GetM("bar").GetM("foo").GetM("xyz").Do(ctx)
    
    • GetP: not support.
  • Version callable can help you managing version better. Example:
    // models.go
    const FooCacheVersion = "1"
    type Foo struct {}
    const BarCacheVersion = "1"
    type Bar struct {Foo: Foo}
    
    // schema.go
    // version has 3 parts: foo version & bar version & global version number
    // if you change struct, update FooCacheVersion or BarCacheVersion
    // if you change fetcher function or ttl or something else, change global version number
    {
    	Name:    "Bar",
    	Key:     "bar:{{.ID}}:info",
    	To:      model.Bar{},
    	Version: func() string {return model.FooCacheVersion + model.BarCacheVersion + "1"},
    	TTL:     5 * time.Minute,
    },
    
  • If set Singleflight to true, Cacheme Get command will be wrapped in a singleflight, so concurrent requests to same key will call Redis only once. Let's use some example to explain this:
    • you have some products to sell, and thousands people will view the detail at same time, so the product key product:1:info may be hit 100000 times per second. Now you should turn on singleflight, and the actually redis hit may reduce to 5000.
    • you have cache for user shopping cart user:123:cart, only the user himself can see that. Now no need to use singleflight, becauese there shouldn't be concurrent requests to that key.
    • you are using serverless platform, AWS Lambda or similar. So each request runs in isolated environment, can't talk to each other through channels. Then singleflight make no sense.
  • Full redis key has 3 parts: prefix + schema key + version. Schema Keycategory:{{.categoryID}}:book:{{.bookID}} with prefix cacheme, version 1 will generate key:
    cacheme:category:1:book:3:v1
    
    Also you will see categoryID and bookID in generated code, as fetch func params.

Logger

You can use custom logger with cacheme, your logger should implement cacheme logger interface:

type Logger interface {
	Log(store string, key string, op string)
}

Here store is the store tag, key is cache key without prefix, op is operation type. Default logger is NOPLogger, just return and do nothing.

Set client logger:
logger := &YourCustomLogger{}
client.SetLogger(logger)
Operation Types:
  • HIT: cache hit to redis, if you enable singleflight, grouped requests only log once.
  • MISS: cache miss
  • FETCH: fetch data from fetcher

Documentation

Overview

https://github.com/kristoff-it/redis-memolock https://redis.com/blog/caches-promises-locks/

Index

Constants

This section is empty.

Variables

View Source
var ErrClosing = errors.New("operation canceled by close()")

ErrClosing happens when calling Close(), all pending requests will be failed with this error

View Source
var ErrLockRenew = errors.New("unable to renew the lock")

ErrLockRenew happens when trying to renew a lock that expired already

View Source
var ErrTimeOut = errors.New("operation timed out")

ErrTimeOut happens when the given timeout expires

Functions

func Check

func Check(stores []CacheStore)

func InvalidAll

func InvalidAll(ctx context.Context, group string, client RedisClient) error

func InvalidAllCluster

func InvalidAllCluster(ctx context.Context, group string, client RedisClient) error

func Marshal

func Marshal(v interface{}) ([]byte, error)

func SchemaToStore

func SchemaToStore(pkg string, path string, prefix string, stores []*StoreSchema, save bool) error

func Unmarshal

func Unmarshal(data []byte, v interface{}) error

func UpdateMemoLockAll

func UpdateMemoLockAll(stores []CacheStore) error

Types

type CachePipeline

type CachePipeline struct {
	Pipeline redis.Pipeliner
	Wg       *sync.WaitGroup
	Executed chan bool
}

func NewPipeline

func NewPipeline(client RedisClient) *CachePipeline

func (*CachePipeline) Execute

func (p *CachePipeline) Execute(ctx context.Context) error

type CacheStore

type CacheStore interface {
	Initialized() bool
	Tag() string
	AddMemoLock() error
}

type ExternalFetchFunc

type ExternalFetchFunc = func() error

ExternalFetchFunc has the same purpose as FetchFunc but works on the assumption that the value will be set in Redis and notificed on Pub/Sub by an external program

type FetchFunc

type FetchFunc = func() (string, time.Duration, error)

FetchFunc is the function that the caller should provide to compute the value if not present in Redis already. time.Duration defines for how long the value should be cached in Redis.

type LockRenewFunc

type LockRenewFunc = func(time.Duration) error

LockRenewFunc is the function that RenewableFetchFunc will get as input and that must be called to extend a locks' life

type Logger added in v0.1.1

type Logger interface {
	Log(store string, key string, op string)
}

type NOPLogger added in v0.1.1

type NOPLogger struct{}

func (*NOPLogger) Log added in v0.1.1

func (l *NOPLogger) Log(store string, key string, op string)

type RedisClient

type RedisClient interface {
	PSubscribe(ctx context.Context, channels ...string) *redis.PubSub
	redis.Cmdable
}

type RedisMemoLock

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

RedisMemoLock implements the "promise" mechanism

func NewRedisMemoLock

func NewRedisMemoLock(ctx context.Context, prefix string, client RedisClient, resourceTag string, lockTimeout time.Duration) (*RedisMemoLock, error)

NewRedisMemoLock Creates a new RedisMemoLock instance

func (*RedisMemoLock) AddGroup

func (r *RedisMemoLock) AddGroup(ctx context.Context, group string, key string) error

func (*RedisMemoLock) Close

func (r *RedisMemoLock) Close()

Close stops listening to Pub/Sub and resolves all pending subscriptions with ErrClosing.

func (*RedisMemoLock) DeleteCache

func (r *RedisMemoLock) DeleteCache(ctx context.Context, key string) error

func (*RedisMemoLock) GetCached

func (r *RedisMemoLock) GetCached(ctx context.Context, key string) ([]byte, error)

func (*RedisMemoLock) GetCachedP

func (r *RedisMemoLock) GetCachedP(ctx context.Context, pipe redis.Pipeliner, key string) *redis.StringCmd

func (*RedisMemoLock) Lock

func (r *RedisMemoLock) Lock(ctx context.Context, key string) (bool, error)

func (*RedisMemoLock) SetCache

func (r *RedisMemoLock) SetCache(ctx context.Context, key string, value []byte, ttl time.Duration) error

func (*RedisMemoLock) SingleGroup added in v0.1.2

func (r *RedisMemoLock) SingleGroup() *singleflight.Group

func (*RedisMemoLock) Wait

func (r *RedisMemoLock) Wait(ctx context.Context, key string) ([]byte, error)

func (*RedisMemoLock) WaitSingle added in v0.1.2

func (r *RedisMemoLock) WaitSingle(ctx context.Context, key string) ([]byte, error)

type RenewableFetchFunc

type RenewableFetchFunc = func(LockRenewFunc) (string, time.Duration, error)

RenewableFetchFunc has the same purpose as FetchFunc but, when called, it is offered a function that allows to extend the lock

type StoreSchema

type StoreSchema struct {
	Name         string
	Key          string
	To           interface{}
	Version      interface{}
	Vars         []string
	TTL          time.Duration
	Singleflight bool
}

func (*StoreSchema) SetVars

func (s *StoreSchema) SetVars(a []string)

Directories

Path Synopsis
cacheme
nolint
nolint

Jump to

Keyboard shortcuts

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