golang基于redis和机器内存的多级缓存

matryoshka

支持分布式环境,基于redis和机器内存(memory)的多级缓存。

  • 一级缓存使用 freecache作为本地缓存,当数据在本地缓存中不存在时,会向第二级缓存请求数据。
  • 二级缓存默认使用redis作为分布式缓存,当数据在二级缓存中不存在时,会向资源层请求数据。
  • 当资源层某条数据更新,可以将缓存中对应的数据删除,二级分布式缓存会直接删除,一级内存缓存会默认利用redis的 sub/pub 机制,将所有机器下的数据删除。

功能

  • 支持每级缓存的请求qps、命中qps、请求资源层qps的监控统计
  • 支持只使用一级或二级缓存
  • 支持自定义二级缓存和sub/pub的实现方式,只需要实现对应接口 WithDistributedCache、WithPubSubChannel
  • 防缓存击穿,单机版的互斥锁,当从资源层同时加载同一数据时,每台机器最多只有一次请求会落到资源层,其他请求会等到数据同步到缓存后,直接从缓存中获取数据。
  • 支持自定义错误处理,WithErrHandler
    项目地址:https://github.com/smokezl/matryoshka

安装

go get github.com/smokezl/matryoshka

导入

import "github.com/smokezl/matryoshka"

基本使用方式

初始化

// 1、初始化全局缓存
conf := &matryoshka.RedisConfig{
   Addr:        "127.0.0.1:6379",
   MaxRetry:    2,
   Pwd:         "",
   IdleTimeout: 10,
   ConnTimeout: 100,
   MaxIdle:     50,
   MaxActive:   500,
}
cache := matryoshka.Init(
   matryoshka.WithErrHandler(func(ctx context.Context, err error) {
       fmt.Println("test print,", err)
   }),
   matryoshka.WithDistributedCache(matryoshka.NewDefaultCache(conf)),
   matryoshka.WithPubSubChannel(matryoshka.NewDefaultPubSub("sub_key", conf)),
)

// 2、根据不同的使用场景, 创建独立场景的缓存
scene := "productInfo"
// 内存 cache 超时时间(redis超时是内存的n倍,默认20倍,可以通过 WithRedisTtlFactor 设置倍数)
expire := 20
soureLoadFn:= func(ctx context.Context, "cachePre", key string) (string, error) {
   return "product info", nil
}
batchSoureLoadFn = func(ctx context.Context, "cachePre", keys []string) (map[string]string, error) {
   valMap := make(map[string]string)
   for _, key := range keys{
       valMap[key] = "product info"
   }
   return valMap, nil
}

h := cache.NewCache(scene, expire)

// 3、获取单个数据
val, err := h.Get(ctx, "key",soureLoadFn)
if err == matryoshka.ErrNotFound {
   //deal empty
}
if err != nil {
   //deal err
}

// 4、获取批量数据
valMap, err := h.BatchGet(ctx, []string{"key1","key2"},batchSoureLoadFn)
if err != nil {
   //deal err
}
if len(valMap) == 0 {
   //deal empty
}

初始化全局缓存配置函数

1、WithCacheSize(size int)

设置全局缓存内存大小,单位 byte,默认为 1024 * 1024 * 100(100m)

2、WithErrHandler(f errHandler)

设置错误执行函数,默认为 nil

type errHandler func(ctx context.Context, err error)
3、WithDistributedCache(i DistributedCacheI)

设置从redis缓存io数据的接口,如果不设置,redis缓存将无法使用,内置 NewDefaultCache

type DistributedCacheI interface {
    IsNilErr(err error) bool
    Get(key string) (val string, err error)
    MGet(keys []string) (valMap map[string]string, err error)
    SetEx(key string, val string, ttl int) (err error)
    Del(keys []string) (err error)
}
4、WithPubSubChannel(i PubSubChannelI)

设置订阅发布的接口,如果不设置,内存缓存分布式同步功能将无法使用,内置 NewDefaultPubSub

type PubSubChannelI interface {
    GetKey() string
    Subscribe(key string, receiver func(message string, err error))
    Publish(key string, val string) (err error)
}
5、使用
conf := &matryoshka.RedisConfig{
    Addr:        "127.0.0.1:6379",
    MaxRetry:    2,
    Pwd:         "",
    IdleTimeout: 10,
    ConnTimeout: 100,
    MaxIdle:     50,
    MaxActive:   500,
    WriteTimeout:100, 
    ReadTimeout: 100,
}
type externalCache struct {
    rp *redis.Pool
}

func NewExternalCache() *externalCache {
    return &externalCache{
        rp: &redis.Pool{
            MaxIdle:     pubSubMaxConn,
            MaxActive:   pubSubMaxConn,
            IdleTimeout: time.Duration(idleTimeout) * time.Second,
            Dial: func() (conn redis.Conn, e error) {
                return redis.Dial("tcp", conf.Addr,
                    redis.DialPassword(pwd),
                    redis.DialConnectTimeout(time.Duration(connTimeout)*time.Millisecond),
                    redis.DialReadTimeout(time.Duration(c.ReadTimeout)*time.Millisecond),
                    redis.DialWriteTimeout(time.Duration(c.WriteTimeout)*time.Millisecond),
                )
            },
        },
    }
}

func (e *externalCache) IsNilErr(err error) bool {
    return err == redis.ErrNil
}

func (e *externalCache) MGet(keys []string) (valMap map[string]string, err error) {
    conn := e.rp.Get()
    defer conn.Close()

    var args []interface{}
    for _, key := range keys {
        args = append(args, key)
    }
    vals, err := redis.Strings(conn.Do("MGET", args...))
    if err != nil {
        return nil, err
    }
    lv := len(vals)
    valMap = make(map[string]string, lv)
    for i := 0; i < lv; i++ {
        valMap[keys[i]] = vals[i]
    }
    return
}

func (e *externalCache) Get(key string) (val string, err error) {
    conn := e.rp.Get()
    defer conn.Close()
    val, err = redis.String(conn.Do("GET", key))
    return
}

func (e *externalCache) SetEx(key string, val string, ttl int) (err error) {
    conn := e.rp.Get()
    defer conn.Close()
    _, err = redis.String(conn.Do("SETEX", key, ttl, val))
    return
}

func (e *externalCache) Del(key string) error {
    conn := e.rp.Get()
    defer conn.Close()
    _, err := redis.Int(conn.Do("DEL", key))
    return err
}


cache := matryoshka.Init(conf,
    matryoshka.WithCacheSize(1024*1024*1024),
    matryoshka.WithErrHandler(func(ctx context.Context, err error){
        //deal err
    }),
    //内置
    //matryoshka.WithDistributedCache(matryoshka.NewDefaultCache(conf)),

    matryoshka.WithDistributedCache(NewExternalCache(conf)),

    //内置
    matryoshka.WithPubSubChannel(matryoshka.NewDefaultPubSub("sub_key", conf)),
)

创建独立场景缓存配置函数

1、WithCacheType(ct CacheType)

设置开启的缓存,默认同时开启内存和redis缓存

CacheTypeAll = 1 //同时开启内存和redis缓存
CacheTypeMem = 2 //只开启内部缓存
CacheTypeExt = 3 //只开启redis缓存
2、WithRedisTtlFactor(factor int)

设置redis缓存超时时间,默认为20倍expire时间
redis超时时间计算公式为: redisTtl = expire * factor

2、使用
h := cache.NewCache(scene, expire,
    matryoshka.WithCacheType(matryoshka.CacheTypeAll),
    matryoshka.WithRedisTtlFactor(30)
)
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,780评论 4 369
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,424评论 1 305
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,397评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,576评论 0 218
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,997评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,945评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,107评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,850评论 0 208
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,625评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,804评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,285评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,613评论 3 261
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,291评论 3 242
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,164评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,963评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,096评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,886评论 2 278