Golang版JetCache缓存框架

2023-09-13  本文已影响0人  阿广本尊

一、引言

早在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。

二、Golang版JetCache

2.1介绍

jetcache-go是基于go-redis/cache拓展的通用缓存访问框架。 实现了类似Java版JetCache的核心功能,包括:

✅ 二级缓存自由组合:本地缓存、集中缓存、本地缓存+集中缓存

✅ Once接口采用单飞(singleflight)模式,高并发且线程安全

✅ 默认采用MsgPack来编解码Value

✅ 本地缓存默认实现了TinyLFUFreeCache

✅ 集中缓存默认实现了go-redis/v8的适配器,你也可以自定义实现

✅ 可以自定义errNotFound,通过占位符替换,缓存空结果防止缓存穿透

✅ 支持开启分布式缓存异步刷新

✅ 指标采集,默认实现了通过日志打印各级缓存的统计指标(QPM、Hit、Miss、Query、QueryFail)

✅ 集中缓存查询故障自动降级

2.2安装

使用最新版本的jetcache-go,您可以在项目中导入该库:

go get https://github.com/daoshenzzg/jetcache-go

2.3快速开始

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()

}

2.4配置选项

// 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.

}

2.5缓存指标收集和统计

您可以实现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|          -|          -|

------------+------------+------------+------------+------------+------------+------------

2.6自定义日志

import "github.com/daoshenzzg/jetcache-go/logger"

// Set your Logger

logger.SetDefaultLogger(l logger.Logger)

2.7自定义编解码

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))

上一篇下一篇

猜你喜欢

热点阅读