我们在 cache 里面提供了部分缓存模式的支持,用于解决、或者说缓解缓存穿透、击穿和雪崩的问题。

这些缓存模式都是使用装饰器设计模型实现的。

# Read Through

Read Through 缓存模式本质上就是帮助你在缓存没有的情况下去数据库加载数据,并且回写缓存。

package main

import (
	"context"
	"fmt"
	"github.com/beego/beego/v2/client/cache"
	"log"
	"time"
)

func main() {
	var c cache.Cache = cache.NewMemoryCache()
	var err error
	c, err = cache.NewReadThroughCache(c,
		// expiration, same as the expiration of key
		time.Minute,
		// load func, how to load data if the key is absent.
		// in general, you should load data from database.
		func(ctx context.Context, key string) (any, error) {
			return fmt.Sprintf("hello, %s", key), nil
		})
	if err != nil {
		panic(err)
	}

	val, err := c.Get(context.Background(), "Beego")
	if err != nil {
		panic(err)
	}
	// print hello, Beego
	fmt.Print(val)
}

在 NewReadThroughCache 的初始化方法里面,接收:

  • c Cache:被装饰的缓存实现
  • expiration time.Duration:缓存过期时间
  • loadFunc func(ctx context.Context, key string) (any, error):如果缓存当中没有这个 key,那么就会调用这个方法去加载数据

在前面的例子里面,我们在 key 找不到的时候,直接返回了拼接的字符串。正常情况下,在生产环境下,是从数据库中加载数据的。

对于这个实现来说,当调用 c 上面的 Get 方法得到 nil,或者 err 不为 nil,就会尝试加载数据

# Write Through

package main

import (
	"context"
	"fmt"
	"github.com/beego/beego/v2/client/cache"
	"time"
)

func main() {
	c := cache.NewMemoryCache()
	wtc, err := cache.NewWriteThroughCache(c, func(ctx context.Context, key string, val any) error {
		fmt.Printf("write data to somewhere key %s, val %v \n", key, val)
		return nil
	})
	if err != nil {
		panic(err)
	}
	err = wtc.Set(context.Background(),
		"/biz/user/id=1", "I am user 1", time.Minute)
	if err != nil {
		panic(err)
	}
	// it will print write data to somewhere key /biz/user/id=1, val I am user 1
}

NewWriteThroughCache 接收两个参数:

  • c Cache:被装饰的缓存实现
  • fn func(ctx context.Context, key string, val any):存储数据

WriteThroughCache 会先调用 fn,而后再写缓存。

注意,WriteThroughCache 并不能解决一致性的问题,你自己使用的时候要小心。

# Random Expire

这个模式主要用于解决缓存雪崩问题,即大量的 key 在同一时间过期,那么就可以考虑在设置 key-value 的时候,给过期时间加上一个随机偏移量。

package main

import (
	"context"
	"fmt"
	"github.com/beego/beego/v2/client/cache"
	"math/rand"
	"time"
)

func main() {
	mc := cache.NewMemoryCache()
	// use the default strategy which will generate random time offset (range: [3s,8s)) expired
	c := cache.NewRandomExpireCache(mc)
	// so the expiration will be [1m3s, 1m8s)
	err := c.Put(context.Background(), "hello", "world", time.Minute)
	if err != nil {
		panic(err)
	}

	c = cache.NewRandomExpireCache(mc,
		// based on the expiration
		cache.WithRandomExpireOffsetFunc(func() time.Duration {
			val := rand.Int31n(100)
			fmt.Printf("calculate offset %d", val)
			return time.Duration(val) * time.Second
		}))

	// so the expiration will be [1m0s, 1m100s)
	err = c.Put(context.Background(), "hello", "world", time.Minute)
	if err != nil {
		panic(err)
	}
}

NewRandomExpireCache 默认情况下会给过期时间加上一个 [3s, 8s) 的偏移量。这个偏移量在数据量不多,并且过期时间在几分钟级是合适的。如果你需要更加的复杂的策略,可以使用 WithRandomExpireOffsetFunc 选项。

当然,WithRandomExpireOffsetFunc 选项是有局限性的,如果不能满足你的需求,你可以自己写一个类似的实现,例如说根据 key 将要被设置的过期时间,加上一个百分比的偏移量,例如说 1% 内的随机偏移量。

# Singleflight

在 key 不存在,或者查询缓存失败的情况下,会有多个 goroutine 尝试去加载数据,那么使用该模式可以确保,一个 key 在当前进程里面只有一个 goroutine 去加载数据。

package main

import (
	"context"
	"fmt"
	"github.com/beego/beego/v2/client/cache"
	"time"
)

func main() {
	c := cache.NewMemoryCache()
	c, err := cache.NewSingleflightCache(c, time.Minute, func(ctx context.Context, key string) (any, error) {
		return fmt.Sprintf("hello, %s", key), nil
	})
	if err != nil {
		panic(err)
	}
	val, err := c.Get(context.Background(), "Beego")
	if err != nil {
		panic(err)
	}
	// it will output hello, Beego
	fmt.Print(val)
}

NewSingleflightCache 与 NewReadThroughCache 参数是一样的含义。

但是要注意,不同的 key 之间没有影响,不同的 Cache 实例之间也没有影响。例如,如果此时有五十个 goroutine 加载五十个不同的 key,那么最终落在数据库上的查询还是会有五十个。

# Bloom Filter

该模式用于高并发环境下快速判断 key 对应的数据是否存在,比较适合解决缓存穿透问题。

package main

import (
	"context"
	"fmt"
	"github.com/beego/beego/v2/client/cache"
	"time"
)

func main() {
	c := cache.NewMemoryCache()
	c, err := cache.NewBloomFilterCache(c, func(ctx context.Context, key string) (any, error) {
		return fmt.Sprintf("hello, %s", key), nil
	}, &AlwaysExist{}, time.Minute)
	if err != nil {
		panic(err)
	}

	val, err := c.Get(context.Background(), "Beego")
	if err != nil {
		panic(err)
	}
	fmt.Println(val)
}

type AlwaysExist struct {
}

func (a *AlwaysExist) Test(data string) bool {
	return true
}

func (a *AlwaysExist) Add(data string) {

}

在这个例子里面,我们传入一个永远返回 true(表示数据存在)的布隆过滤器。正常情况下,在你的业务里面,应该是基于内存或者基于 Redis 来实现一个布隆过滤器。