Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: add examples, fix README, fix typo #7

Merged
merged 1 commit into from
Jun 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
26 changes: 13 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# go-cache

[![GoDev](https://img.shields.io/badge/go.dev-doc-007d9c?style=flat-square&logo=read-the-docs)](https://pkg.go.dev/github.com/viney-shih/go-cache?tab=doc)
[![Go Doc](https://img.shields.io/badge/godoc-reference-blue.svg)](https://pkg.go.dev/github.com/viney-shih/go-cache?tab=doc)
[![Build Status](https://app.travis-ci.com/viney-shih/go-cache.svg?branch=master)](https://app.travis-ci.com/viney-shih/go-cache)
[![Go Report Card](https://goreportcard.com/badge/github.com/viney-shih/go-cache)](https://goreportcard.com/report/github.com/viney-shih/go-cache)
[![codecov](https://codecov.io/gh/viney-shih/go-cache/branch/master/graph/badge.svg?token=QKRiNSU5Gn)](https://codecov.io/gh/viney-shih/go-cache)
Expand All @@ -10,22 +10,22 @@

<p align="center">
<img src="assets/logo.png" title="viney-shih/go-cache" />
<span>Photo by <a href="https://github.com/ashleymcnamara">Ashley McNamara</a>, via <a href="https://github.com/ashleymcnamara/gophers">ashleymcnamara/gophers</a> (CC BY-NC-SA 4.0)</span>
<span style="font-size: 14px; font-weight: 400; color: rgba(117, 117, 117, 1); font-family: sohne, "Helvetica Neue", Helvetica, Arial, sans-serif;">Photo by <a href="https://github.com/ashleymcnamara">Ashley McNamara</a>, via <a href="https://github.com/ashleymcnamara/gophers">ashleymcnamara/gophers</a> (CC BY-NC-SA 4.0)</span>
</p>

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.
A flexible multi-layered caching library 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 **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.
- **Easy to use** : provide a friendly interface to deal with both caching mechnaism by simple configuration. Limit the size of memory on single instance (pod) as well.
- **Maintain consistency** : evict keys between distributed systems by `Pub-Sub` pattern.
- **Data compression** : provide customized marshal and unmarshal functions.
- **Fix concurrency issue** : prevent data racing happened on single instance (pod).
Expand Down Expand Up @@ -93,7 +93,7 @@ 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.
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
Expand All @@ -107,7 +107,7 @@ rds := cache.NewRedis(redis.NewRing(&redis.RingOptions{
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.
Treat it as a common **key:value** store like Redis. But more advanced, it coordinated the usage between multi-layered caching mechanism inside.

```go
type Object struct {
Expand All @@ -117,7 +117,7 @@ type Object struct {

func Example_setAndGetPattern() {
// We create a group of cache named "set-and-get".
// It uses the shared cache only with TTL of ten seconds.
// It uses the shared cache only. Each key will be expired within ten seconds.
c := cacheFactory.NewCache([]cache.Setting{
{
Prefix: "set-and-get",
Expand Down Expand Up @@ -159,7 +159,7 @@ func Example_setAndGetPattern() {

### 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.
`GetByFunc()` is the easier way to deal with the cache by implementing the **getter function** in the parameter. When the cache is missing, it is going to refill the cache automatically.

```go
func ExampleCache_GetByFunc() {
Expand All @@ -177,7 +177,7 @@ func ExampleCache_GetByFunc() {
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..
// The getter is used to generate data when cache missed, and refill 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
Expand All @@ -192,7 +192,7 @@ func ExampleCache_GetByFunc() {
}
```

`MGetter` is another approaching way to do this. Set this function durning registering the Setting.
`MGetter` is another approaching way to do this. Set this function durning registering the *Setting*.

```go
func ExampleService_Create_mGetter() {
Expand All @@ -206,7 +206,7 @@ func ExampleService_Create_mGetter() {
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..
// The MGetter is used to generate data when cache missed, and refill 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.
Expand Down
4 changes: 2 additions & 2 deletions cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ func (c *cache) GetByFunc(ctx context.Context, prefix, key string, container int
// cache missed once
c.onCacheMiss(prefix, key, 1)

// using oneTimeGetter to implement read-through pattern
// using oneTimeGetter to implement Cache-Aside pattern
intf, err := getter()
if err != nil {
return nil, err
Expand Down Expand Up @@ -143,7 +143,7 @@ func (c *cache) MGet(ctx context.Context, prefix string, keys ...string) (Result
return res, nil
}

// 2. using mGetter to implement read-through pattern
// 2. using mGetter to implement Cache-Aside pattern
intfs, err := cfg.mGetter(missKeys...)
if err != nil {
return nil, err
Expand Down
2 changes: 1 addition & 1 deletion event_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func (s *eventSuite) TestSubscribedEventsHandlerWithSet() {
s.Require().NoError(err)
s.Require().Equal([]Value{{Valid: true, Bytes: []byte("100")}}, val)

// trigger evict event without keys, nothing happend
// trigger evict event without keys, nothing happened
s.Require().NoError(s.mb.send(mockEventCTX, event{
Type: EventTypeEvict,
Body: eventBody{Keys: []string{}},
Expand Down
56 changes: 48 additions & 8 deletions example_advanced_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ type Person struct {
Age int
}

// Example_readThroughPattern will demo multiple cache layers and multiple
// prefix keys at the same time.
func Example_readThroughPattern() {
// Example_cacheAsidePattern demonstrates multi-layered caching and
// multiple prefix keys at the same time.
func Example_cacheAsidePattern() {
tinyLfu := cache.NewTinyLFU(10000)
rds := cache.NewRedis(redis.NewRing(&redis.RingOptions{
Addrs: map[string]string{
Expand All @@ -43,7 +43,7 @@ func Example_readThroughPattern() {
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..
// The MGetter is used to generate data when cache missed, and refill the cache automatically..
// You can read from DB or other microservices.
// Assume we read from MySQL according to the key "jacky" and get the value of
// Person{FirstName: "jacky", LastName: "Lin", Age: 38}
Expand All @@ -60,25 +60,65 @@ func Example_readThroughPattern() {
ctx := context.TODO()
teacher := &Person{}
if err := c.GetByFunc(ctx, "teacher", "jacky", teacher, func() (interface{}, error) {
// The getter is used to generate data when cache missed, and refill it to the cache automatically..
// The getter is used to generate data when cache missed, and refill the cache automatically..
// You can read from DB or other microservices.
// Assume we read from MySQL according to the key "jacky" and get the value of Object{Str: "value2", Num: 2}
// Assume we read from MySQL according to the key "jacky" and get the value of
// Person{FirstName: "jacky", LastName: "Wang", Age: 83} .
return Person{FirstName: "Jacky", LastName: "Wang", Age: 83}, nil
}); err != nil {
panic("not expected")
}

fmt.Println(teacher) // Output: {FirstName: "Jacky", LastName: "Wang", Age: 83}
fmt.Println(teacher) // {FirstName: "Jacky", LastName: "Wang", Age: 83}

student := &Person{}
if err := c.Get(ctx, "student", "jacky", student); err != nil {
panic("not expected")
}

fmt.Println(student) // Output: {FirstName: "Jacky", LastName: "Lin", Age: 38}
fmt.Println(student) // {FirstName: "Jacky", LastName: "Lin", Age: 38}

// Output:
// &{Jacky Wang 83}
// &{Jacky Lin 38}

}

// Example_pubsubPattern demonstrates how to leverage Pubsub pattern
// to broadcast evictions between distributed systems, and
// make in-memory cache consistency eventually ASAP.
func Example_pubsubPattern() {
tinyLfu := cache.NewTinyLFU(10000)
rds := cache.NewRedis(redis.NewRing(&redis.RingOptions{
Addrs: map[string]string{
"server1": ":6379",
},
}))

cacheF := cache.NewFactory(rds, tinyLfu, cache.WithPubSub(rds))
c := cacheF.NewCache([]cache.Setting{
{
Prefix: "user",
CacheAttributes: map[cache.Type]cache.Attribute{
cache.SharedCacheType: {TTL: time.Hour},
cache.LocalCacheType: {TTL: 10 * time.Minute},
},
},
})

ctx := context.TODO()
user := &Person{}
if err := c.GetByFunc(ctx, "user", "tony", user, func() (interface{}, error) {
// The getter is used to generate data when cache missed, and refill the cache automatically.
// Assume we read from MySQL according to the key "tony" and get the value of
// Person{FirstName: "Tony", LastName: "Stock", Age: 87} .
// At the same time, it will broadcast the eviction about the prefix "user" and the key "tony" to others.
return Person{FirstName: "Tony", LastName: "Stock", Age: 87}, nil
}); err != nil {
panic("not expected")
}

fmt.Println(user) // {FirstName: "Tony", LastName: "Stock", Age: 87}
// Output:
// &{Tony Stock 87}
}
4 changes: 2 additions & 2 deletions example_readthrough_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ func ExampleCache_GetByFunc() {
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..
// The getter is used to generate data when cache missed, and refill 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
Expand Down Expand Up @@ -68,7 +68,7 @@ func ExampleFactory_NewCache_mGetter() {
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..
// The MGetter is used to generate data when cache missed, and refill 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.
Expand Down
4 changes: 2 additions & 2 deletions example_setandget_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,11 +52,11 @@ func Example_setAndGetPattern() {
if err := c.Get(ctx, "set-and-get", "key", container); err != nil {
panic("not expected")
}
fmt.Println(container) // Output: Object{ Str: "value1", Num: 1}
fmt.Println(container) // 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")
fmt.Println(err) // errors.New("cache key is missing")
}

// Output:
Expand Down
4 changes: 2 additions & 2 deletions interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ func NewFactory(sharedCache Adapter, localCache Adapter, options ...ServiceOptio
// Cache is generated by Factory based on the need specified in the Setting slice.
// Use the following methods to create key/value store.
type Cache interface {
// GetByFunc returns a value in the cache. It also follows up the read-through pattern.
// GetByFunc returns a value in the cache. It also follows up the Cache-Aside pattern.
// When cache-miss happened, it relaods the value by the getter, and fill in the cache again.
GetByFunc(context context.Context, prefix, key string, container interface{}, getter OneTimeGetterFunc) error
// Get returns a value in the cache.
Expand All @@ -84,7 +84,7 @@ type Setting struct {
Prefix string
// CacheAttributes includes all detail attributes.
CacheAttributes map[Type]Attribute
// MGetter should be provided when using read-through pattern
// MGetter should be provided when using Cache-Aside pattern
MGetter MGetterFunc
// MarshalFunc specified the marshal function
// Needs to consider with unmarshal function at the same time.
Expand Down