cacheme - Redis Caching Framework For Go
- Statically Typed - 100% statically typed using code generation.
- Scale Efficiently - thundering herd protection via pub/sub.
- Cluster Support - same API for redis & redis cluster.
- Memoize - dynamic key generation 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.
Installation
go get github.com/Yiling-J/cacheme-go/cmd
After installing cacheme-go
codegen, go to the root directory of your project, and run:
go run github.com/Yiling-J/cacheme-go/cmd init
The command above will generate cacheme
directory under root directory:
└── cacheme
├── fetcher
│ └── fetcher.go
└── schema
└── schema.go
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,
},
}
)
More details here
Store Generation
Run code generation from the root directory of the project as follows:
go run github.com/Yiling-J/cacheme-go/cmd generate
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 a client and get data, fetch function will be called if cache not exist:
import (
"your_project/cacheme"
"your_project/cacheme/fetcher"
)
func example() (string, error) {
ctx := context.TODO()
fetcher.Setup()
client := cacheme.New(
redis.NewClient(&redis.Options{
Addr: "localhost:6379",
Password: "",
DB: 0,
}),
)
store := client.SimpleCacheStore
result, err := store.Get(ctx, "foo")
if err != nil {
return "", err
}
return result, err
}
Redis Cluster:
import (
"your_project/cacheme"
"your_project/cacheme/fetcher"
)
func example() (string, error) {
ctx := context.TODO()
fetcher.Setup()
client := cacheme.NewCluster(
redis.NewClusterClient(&redis.ClusterOptions{
Addrs: []string{
":7000",
":7001",
":7002"},
}),
)
store := client.SimpleCacheStore
result, err := store.Get(ctx, "foo")
if err != nil {
return "", err
}
return result, err
}
Invalid your cache:
err := store.Invalid(ctx, "foo")
Update your cache:
err := store.Update(ctx, "foo")
Redis pipeline(using same client, skip client creation here):
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)
}
Mixed pipeline:
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)
fmt.Println(err)
// 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)
}
Invalid all cache with version:
// 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{}
- string:
- Version - version number, for schema change.
- TTL - redis ttl using go time.
- Singleflight - bool, if
true
, concurrent requests to same key on same machine will call Redis only once
Notes:
-
Duplicate name/key is not allowed.
-
If set
Singleflight
totrue
, RedisGET
command will be wrapped in a singleflight, so concurrent requests to same key will callRedis
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 should'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.
- you have some products to sell, and thousands people will view the detail at same time, so the product key
-
Full redis key has 3 parts: prefix + schema key + version. Schema Key
category:{{.categoryID}}:book:{{.bookID}}
with prefixcacheme
, version 1 will generate key:cacheme:category:1:book:3:v1
Also you will see
categoryID
andbookID
in generated code, as fetch func params.