-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
b4738e2
commit 70c6236
Showing
23 changed files
with
3,715 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,11 @@ | ||
# Mac only | ||
.DS_Store | ||
|
||
# for IDE | ||
.idea | ||
.vscode | ||
|
||
|
||
# Binaries for programs and plugins | ||
*.exe | ||
*.exe~ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
repos: | ||
# ========================================================================== | ||
# Golang Pre-Commit Hooks | https://github.com/tekwizely/pre-commit-golang | ||
# | ||
# Visit the project home page to learn more about the available Hooks, | ||
# including useful arguments you might want to pass into them. | ||
# | ||
# File-Based Hooks: | ||
# Run against matching staged files individually. | ||
# | ||
# Module-Based Hooks: | ||
# Run against module root folders containing matching staged files. | ||
# | ||
# Package-Based Hooks: | ||
# Run against folders containing one or more staged files. | ||
# | ||
# Repo-Based Hooks: | ||
# Run against the entire repo. | ||
# The hooks only run once (if any matching files are staged), | ||
# and are NOT provided the list of staged files, | ||
# | ||
# My-Cmd-* Hooks | ||
# Allow you to invoke custom tools in varous contexts. | ||
# Can be useful if your favorite tool(s) are not built-in (yet) | ||
# | ||
# Hook Suffixes | ||
# Hooks have suffixes in their name that indicate their targets: | ||
# | ||
# +-----------+--------------+ | ||
# | Suffix | Target | | ||
# |-----------+--------------+ | ||
# | <none> | Files | | ||
# | -mod | Module | | ||
# | -pkg | Package | | ||
# | -repo | Repo Root | | ||
# | -repo-mod | All Modules | | ||
# | -repo-pkg | All Packages | | ||
# +-----------+--------------+ | ||
# | ||
# ! Multiple Hook Invocations | ||
# ! Due to OS command-line-length limits, Pre-Commit can invoke a hook | ||
# ! multiple times if a large number of files are staged. | ||
# ! For file and repo-based hooks, this isn't an issue, but for module | ||
# ! and package-based hooks, there is a potential for the hook to run | ||
# ! against the same module or package multiple times, duplicating any | ||
# ! errors or warnings. | ||
# | ||
# Useful Hook Parameters: | ||
# - id: hook-id | ||
# args: [arg1, arg2, ..., '--'] # Pass options ('--' is optional) | ||
# always_run: true # Run even if no matching files staged | ||
# alias: hook-alias # Create an alias | ||
# | ||
# Passing Options To Hooks: | ||
# If your options contain a reference to an existing file, then you will | ||
# need to use a trailing '--' argument to separate the hook options from | ||
# the modified-file list that Pre-Commit passes into the hook. | ||
# NOTE: For repo-based hooks, '--' is not needed. | ||
# | ||
# Always Run: | ||
# By default, hooks ONLY run when matching file types are staged. | ||
# When configured to "always_run", a hook is executed as if EVERY matching | ||
# file were staged. | ||
# | ||
# Aliases: | ||
# Consider adding aliases to longer-named hooks for easier CLI usage. | ||
# ========================================================================== | ||
- repo: https://github.com/tekwizely/pre-commit-golang | ||
rev: master | ||
hooks: | ||
# | ||
# Go Build | ||
# | ||
- id: go-build-mod | ||
# - id: go-build-pkg | ||
- id: go-build-repo-mod | ||
# - id: go-build-repo-pkg | ||
# | ||
# Go Mod Tidy | ||
# | ||
- id: go-mod-tidy | ||
- id: go-mod-tidy-repo | ||
# | ||
# Go Test | ||
# | ||
- id: go-test-mod | ||
# - id: go-test-pkg | ||
- id: go-test-repo-mod | ||
# - id: go-test-repo-pkg | ||
# | ||
# Go Vet | ||
# | ||
# - id: go-vet | ||
- id: go-vet-mod | ||
# - id: go-vet-pkg | ||
- id: go-vet-repo-mod | ||
# - id: go-vet-repo-pkg | ||
# | ||
# Revive | ||
# | ||
- id: go-revive | ||
- id: go-revive-mod | ||
- id: go-revive-repo-mod | ||
# | ||
# StaticCheck | ||
# | ||
- id: go-staticcheck-mod | ||
# - id: go-staticcheck-pkg | ||
- id: go-staticcheck-repo-mod | ||
# - id: go-staticcheck-repo-pkg | ||
# | ||
# Formatters | ||
# | ||
- id: go-fmt | ||
args: [-w] | ||
- id: go-fmt-repo | ||
args: [-w] | ||
- id: go-imports # replaces go-fmt | ||
args: [-w] | ||
- id: go-imports-repo # replaces go-fmt-repo | ||
args: [-w] | ||
# | ||
# Style Checkers | ||
# | ||
- id: go-lint | ||
# |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,174 @@ | ||
# go-cache | ||
A library of mixed version of key:value store between in-memory cache and shared cache (i.e. Redis) for Go | ||
A library of mixed version of **key:value** store interacts with private (in-memory) cache and shared cache (i.e. Redis) in Go. It provides `Cache-Aside` strategy when dealing with both, and maintains the consistency of private cache between distributed systems by `Pub-Sub` pattern. | ||
|
||
Caching is a common technique that aims to improve the performance and scalability of a system. It does this by temporarily copying frequently accessed data to fast storage close to the application. Distributed applications typically implement either or both of the following strategies when caching data: | ||
- Using a `private cache`, where data is held locally on the computer that's running an instance of an application or service. | ||
- Using a `shared cache`, serving as a common source that can be accessed by multiple processes and machines. | ||
|
||
![Using a local private cache with a shared cache](./doc/img/caching.png) | ||
Ref: [https://docs.microsoft.com/en-us/azure/architecture/best-practices/images/caching/caching3.png](https://docs.microsoft.com/en-us/azure/architecture/best-practices/images/caching/caching3.png "Using a local private cache with a shared cache") | ||
|
||
Considering the flexibility, efficiency and consistency, we starts to build up our own framework. | ||
|
||
## Features | ||
- **Easy to use** : provide a friendly interface to deal with both caching mechnaism by simple configuration. Limit the resource on single instance (pod) as well. | ||
- **Data compression** : provide a customized marshal and unmarshal funciton. | ||
- **Fix concurrency issue** : prevent data racing happened on single instance (pod). | ||
- **Metric** : provide callback functions to measure the performance. (i.e. hit rate, private cache usage, ...) | ||
|
||
## Data flow | ||
### Load the cache with `Cache-Aside` strategy | ||
![load](./doc/img/get.png) | ||
|
||
### Evict the cache | ||
![evict](./doc/img/del.png) | ||
|
||
## Installation | ||
```sh | ||
go get github.com/viney-shih/go-cache | ||
``` | ||
|
||
## Get Started | ||
### Basic usage: Set-And-Get | ||
|
||
By adopting `singleton` pattern, initialize the Factory in main.go at the beginning, and deliver it to each package or business logic. | ||
|
||
```go | ||
// Initialize the Factory in main.go | ||
tinyLfu := cache.NewTinyLFU(10000) | ||
rds := cache.NewRedis(redis.NewRing(&redis.RingOptions{ | ||
Addrs: map[string]string{ | ||
"server1": ":6379", | ||
}, | ||
})) | ||
|
||
cacheFactory := cache.NewFactory(rds, tinyLfu) | ||
``` | ||
|
||
Treat it as a common **key:value** store like using Redis. But more advanced, it coordinated the usage between multi-level caching mechanism inside. | ||
|
||
```go | ||
type Object struct { | ||
Str string | ||
Num int | ||
} | ||
|
||
func Example_setAndGetPattern() { | ||
// We create a group of cache named "set-and-get". | ||
// It uses the shared cache only with TTL of ten seconds. | ||
c := cacheFactory.NewCache([]cache.Setting{ | ||
{ | ||
Prefix: "set-and-get", | ||
CacheAttributes: map[cache.Type]cache.Attribute{ | ||
cache.SharedCacheType: {TTL: 10 * time.Second}, | ||
}, | ||
}, | ||
}) | ||
|
||
ctx := context.TODO() | ||
|
||
// set the cache | ||
obj := &Object{ | ||
Str: "value1", | ||
Num: 1, | ||
} | ||
if err := c.Set(ctx, "set-and-get", "key", obj); err != nil { | ||
panic("not expected") | ||
} | ||
|
||
// read the cache | ||
container := &Object{} | ||
if err := c.Get(ctx, "set-and-get", "key", container); err != nil { | ||
panic("not expected") | ||
} | ||
fmt.Println(container) // Output: Object{ Str: "value1", Num: 1} | ||
|
||
// read the cache but failed | ||
if err := c.Get(ctx, "set-and-get", "no-such-key", container); err != nil { | ||
fmt.Println(err) // Output: errors.New("cache key is missing") | ||
} | ||
|
||
// Output: | ||
// &{value1 1} | ||
// cache key is missing | ||
} | ||
|
||
``` | ||
|
||
### Advanced usage: `Cache-Aside` strategy | ||
|
||
`GetByFunc()` is the easier way to deal with the cache by implementing the getter function in the parameter. When the cache is missing, it will read the data with the getter function and refill it in cache automatically. | ||
|
||
```go | ||
func ExampleCache_GetByFunc() { | ||
// We create a group of cache named "get-by-func". | ||
// It uses the local cache only with TTL of ten minutes. | ||
c := cacheFactory.NewCache([]cache.Setting{ | ||
{ | ||
Prefix: "get-by-func", | ||
CacheAttributes: map[cache.Type]cache.Attribute{ | ||
cache.LocalCacheType: {TTL: 10 * time.Minute}, | ||
}, | ||
}, | ||
}) | ||
|
||
ctx := context.TODO() | ||
container2 := &Object{} | ||
if err := c.GetByFunc(ctx, "get-by-func", "key2", container2, func() (interface{}, error) { | ||
// The getter is used to generate data when cache missed, and refill it to the cache automatically.. | ||
// You can read from DB or other microservices. | ||
// Assume we read from MySQL according to the key "key2" and get the value of Object{Str: "value2", Num: 2} | ||
return Object{Str: "value2", Num: 2}, nil | ||
}); err != nil { | ||
panic("not expected") | ||
} | ||
|
||
fmt.Println(container2) // Object{ Str: "value2", Num: 2} | ||
|
||
// Output: | ||
// &{value2 2} | ||
} | ||
``` | ||
|
||
`MGetter` is another approaching way to do this. Set this function durning registering the Setting. | ||
|
||
```go | ||
func ExampleService_Create_mGetter() { | ||
// We create a group of cache named "mgetter". | ||
// It uses both shared and local caches with separated TTL of one hour and ten minutes. | ||
c := cacheFactory.NewCache([]cache.Setting{ | ||
{ | ||
Prefix: "mgetter", | ||
CacheAttributes: map[cache.Type]cache.Attribute{ | ||
cache.SharedCacheType: {TTL: time.Hour}, | ||
cache.LocalCacheType: {TTL: 10 * time.Minute}, | ||
}, | ||
MGetter: func(keys ...string) (interface{}, error) { | ||
// The MGetter is used to generate data when cache missed, and refill it to the cache automatically.. | ||
// You can read from DB or other microservices. | ||
// Assume we read from MySQL according to the key "key3" and get the value of Object{Str: "value3", Num: 3} | ||
// HINT: remember to return as a slice, and the item order needs to consist with the keys in the parameters. | ||
return []Object{{Str: "value3", Num: 3}}, nil | ||
}, | ||
}, | ||
}) | ||
|
||
ctx := context.TODO() | ||
container3 := &Object{} | ||
if err := c.Get(ctx, "mgetter", "key3", container3); err != nil { | ||
panic("not expected") | ||
} | ||
|
||
fmt.Println(container3) // Object{ Str: "value3", Num: 3} | ||
|
||
// Output: | ||
// &{value3 3} | ||
} | ||
``` | ||
|
||
[More examples](./example_advanced_test.go) | ||
|
||
## References | ||
- https://docs.microsoft.com/en-us/azure/architecture/best-practices/caching | ||
- https://github.com/vmihailenco/go-cache-benchmark | ||
- https://github.com/go-redis/cache |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
package cache | ||
|
||
import ( | ||
"context" | ||
"time" | ||
) | ||
|
||
// Adapter is the interface when communicating with shared/local caches. | ||
type Adapter interface { | ||
MGet(context context.Context, keys []string) ([]Value, error) | ||
MSet(context context.Context, keyVals map[string][]byte, ttl time.Duration, options ...MSetOptions) error | ||
Del(context context.Context, keys ...string) error | ||
} | ||
|
||
// MSetOptions is an alias for functional argument. | ||
type MSetOptions func(opts *msetOptions) | ||
|
||
type msetOptions struct { | ||
onCostAdd func(key string, cost int) | ||
onCostEvict func(key string, cost int) | ||
} | ||
|
||
// WithOnCostAddFunc sets up the callback when adding the cache with key and cost. | ||
func WithOnCostAddFunc(f func(key string, cost int)) MSetOptions { | ||
return func(opts *msetOptions) { | ||
opts.onCostAdd = f | ||
} | ||
} | ||
|
||
// WithOnCostEvictFunc sets up the callback when evicting the cache with key and cost. | ||
func WithOnCostEvictFunc(f func(key string, cost int)) MSetOptions { | ||
return func(opts *msetOptions) { | ||
opts.onCostEvict = f | ||
} | ||
} | ||
|
||
func loadMSetOptions(options ...MSetOptions) *msetOptions { | ||
opts := &msetOptions{} | ||
for _, option := range options { | ||
option(opts) | ||
} | ||
|
||
return opts | ||
} | ||
|
||
// Value is returned by MGet() | ||
type Value struct { | ||
// Valid stands for existing in cache or not. | ||
Valid bool | ||
// Bytes stands for the return value in byte format. | ||
Bytes []byte | ||
} |
Oops, something went wrong.