Golang版JetCache缓存框架
一、引言
早在2019年我就接触到了阿里巴巴开源的JetCache缓存框架,非常优雅的实现了多种缓存模式。通过注解方式就能实现缓存的配置和使用。特别的,一些业务场景简单的通过给方法加上注解@Cached、@CacheRefresh、@CachePenetrationProtect就能让接口插上性能的翅膀,极大的减少了编码量,提升了代码的可维护性。
随着互联网的高速发展,Go语言越来越流行,很多在线业务,尤其是新业务不断的从Java转战到Golang。但是,Golang的世界里截止目前似乎还没有一个类似JetCache的缓存框架出现。JetCache作者的GitHub - areyouok/jetcache-go也迟迟没有发布。找来找去,只有一个小众的GitHub - go-redis/cache: Cache library with Redis backend for Golang有点类似,该缓存框架是鼎鼎大名的GitHub - redis/go-redis: Redis Go client的作者开源的。实现了多级缓存自由组合、singleflight模式(类似@CachePenetrationProtect)等核心功能。但它不具备缓存异步刷新、缓存空结果防穿透(至少不优雅)、拓展其他Redis客户端。另外,指标统计也比较单一。
因此,我们基于GitHub - go-redis/cache: Cache library with Redis backend for Golang并参考了其他优秀开源框架的解决方案,拓展了很多新特性,实现了Golang版本的JetCache。
jetcache-go是基于go-redis/cache拓展的通用缓存访问框架。 实现了类似Java版JetCache的核心功能,包括:
✅ 二级缓存自由组合:本地缓存、集中缓存、本地缓存+集中缓存
✅ Once接口采用单飞(singleflight)模式,高并发且线程安全
✅ 默认采用MsgPack来编解码Value
✅ 集中缓存默认实现了go-redis/v8的适配器,你也可以自定义实现
✅ 可以自定义errNotFound,通过占位符替换,缓存空结果防止缓存穿透
✅ 支持开启分布式缓存异步刷新
✅ 指标采集,默认实现了通过日志打印各级缓存的统计指标(QPM、Hit、Miss、Query、QueryFail)
✅ 集中缓存查询故障自动降级
使用最新版本的jetcache-go,您可以在项目中导入该库:
go get https://github.com/daoshenzzg/jetcache-go
package cache_test
import (
"context"
"errors"
"fmt"
"time"
"github.com/go-redis/redis/v8"
"github.com/daoshenzzg/jetcache-go"
"github.com/daoshenzzg/jetcache-go/local"
"github.com/daoshenzzg/jetcache-go/logger"
"github.com/daoshenzzg/jetcache-go/remote"
"github.com/daoshenzzg/jetcache-go/util"
)
var errRecordNotFound = errors.New("mock gorm.errRecordNotFound")
type object struct {
Str string
Num int
}
func mockDBGetObject(id int) (*object, error) {
if id > 100 {
return nil, errRecordNotFound
}
return &object{Str: "mystring", Num: 42}, nil
}
func Example_basicUsage() {
ring := redis.NewRing(&redis.RingOptions{
Addrs: map[string]string{
"server1": ":6379",
"server2": ":6380",
},
})
mycache := cache.New(cache.WithName("any"),
cache.WithRemote(remote.NewGoRedisV8Adaptor(ring)),
cache.WithLocal(local.NewFreeCache(256*local.MB, time.Minute)),
cache.WithErrNotFound(errRecordNotFound))
ctx := context.TODO()
key := util.JoinAny(":", "mykey", 1)
obj, _ := mockDBGetObject(1)
if err := mycache.Set(&cache.Item{
Ctx: ctx,
Key: key,
Value: obj,
TTL: time.Hour,
}); err != nil {
panic(err)
}
var wanted object
if err := mycache.Get(ctx, key, &wanted); err == nil {
fmt.Println(wanted)
}
// Output: {mystring 42}
mycache.Close()
}
func Example_advancedUsage() {
logger.SetLevel(logger.LevelInfo)
ring := redis.NewRing(&redis.RingOptions{
Addrs: map[string]string{
"server1": ":6379",
"server2": ":6380",
},
})
mycache := cache.New(cache.WithName("any"),
cache.WithRemote(remote.NewGoRedisV8Adaptor(ring)),
cache.WithLocal(local.NewFreeCache(256*local.MB, time.Minute)),
cache.WithErrNotFound(errRecordNotFound),
cache.WithRefreshDuration(time.Minute))
obj := new(object)
err := mycache.Once(&cache.Item{
Key: util.JoinAny(":", "mykey", 1),
Value: obj, // destination
Do: func(*cache.Item) (interface{}, error) {
return mockDBGetObject(1)
},
Refresh: true, // auto refreshment
})
if err != nil {
panic(err)
}
fmt.Println(obj)
//Output: &{mystring 42}
mycache.Close()
}
// Options are used to store cache options.
type Options struct {
name string // Cache name, used for log identification and metric reporting
remote remote.Remote // Remote cache.
local local.Local // Local cache.
codec string // Value encoding and decoding method. Default is "json.Name" or "msgpack.Name". You can also customize it.
errNotFound error // Error to return for cache miss. Used to prevent cache penetration.
notFoundExpiry time.Duration // Duration for placeholder cache when there is a cache miss. Default is 1 minute.
refreshDuration time.Duration // Interval for asynchronous cache refresh. Default is 0 (refresh is disabled).
stopRefreshAfterLastAccess time.Duration // Duration for cache to stop refreshing after no access. Default is refreshDuration + 1 second.
refreshConcurrency int // Maximum number of concurrent cache refreshes. Default is 4.
statsDisabled bool // Flag to disable cache statistics.
statsHandler stats.Handler // Metrics statsHandler collector.
}
您可以实现stats.Handler接口并注册到Cache组件来自定义收集指标,例如使用Prometheus 采集指标。我们默认实现了通过日志打印统计指标,如下所示:
2023/09/11 16:42:30.695294 statslogger.go:178: [INFO] jetcache-go stats last 1m0s.
cache | qpm| hit_ratio| hit| miss| query| query_fail
------------+------------+------------+------------+------------+------------+------------
bench | 216440123| 100.00%| 216439867| 256| 256| 0|
bench_local | 216440123| 100.00%| 216434970| 5153| -| -|
bench_remote| 5153| 95.03%| 4897| 256| -| -|
------------+------------+------------+------------+------------+------------+------------
import "github.com/daoshenzzg/jetcache-go/logger"
// Set your Logger
logger.SetDefaultLogger(l logger.Logger)
import (
"github.com/daoshenzzg/jetcache-go"
"github.com/daoshenzzg/jetcache-go/encoding"
)
// Register your codec
encoding.RegisterCodec(codec Codec)
// Set your codec name
mycache := cache.New("any",
cache.WithRemote(...),
cache.WithCodec(yourCodecName string))