go缓存设计 redis 发布订阅

一般缓存 有内存缓存, 没有就读redis, redis没有就读tidb;如何防止缓存穿透,这里我们用golang.org/x/sync/singleflight解决,还有缓存更新,比如多个节点如何更新,这里借用redis 发布订阅;需要更新redis 就发送一条发布消息, 订阅的所有结点就删除内存缓存

缓存代码如下:

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "github.com/patrickmn/go-cache"
    "github.com/redis/go-redis/v9"
    "golang.org/x/sync/singleflight"
    "time"
)

var globalCache *cache.Cache
var channelKey = "cache_Channel"
var rdb *redis.Client

func init() {
    globalCache = cache.New(5*time.Minute, 10*time.Minute)

    rdb = redis.NewClient(&redis.Options{
        Addr:     "localhost:6379",
        Password: "", // no password set
        DB:       0,  // use default DB
    })

    ctx := context.Background()
    if _, err := rdb.Ping(ctx).Result(); err != nil {
        panic(err)
    }

    pubSub := rdb.Subscribe(ctx, channelKey)
    go func() {
        ch := pubSub.Channel() // 获取一个 channel,用于接收消息
        for msg := range ch {  // 循环读取消息
            if key := msg.Payload; len(key) > 0 {
                rdb.Del(ctx, key).Err()
                globalCache.Delete(key)
                fmt.Println(fmt.Sprintf("redis 订阅%s", key))
            }

        }
    }()

}

func GetCacheData[T any](ctx context.Context, key string, t T, load func() (T, error)) error {
    val, ok := globalCache.Get(key)
    if ok {
        t, ok = val.(T)
        if ok {
            fmt.Println(fmt.Sprintf("内存 读取 %s", key))
            return nil
        }
    }

    client := rdb
    var sg singleflight.Group

    val, err, _ := sg.Do(key, func() (interface{}, error) {

        //读取redis
        str, err := client.Get(ctx, key).Result()
        if err == nil && len(str) > 0 {
            err = json.Unmarshal([]byte(str), &t)
            if err == nil {
                globalCache.Set(key, t, time.Minute*1)
            }
            fmt.Println(fmt.Sprintf("redis 读取 %s", key))
            return t, err
        }

        if err != nil && err != redis.Nil {
            return nil, err
        }

        t, err = load()
        if err != nil {
            return nil, err
        }

        fmt.Println(fmt.Sprintf("db 读取 %s", key))

        jsonByte, err := json.Marshal(t)
        if err != nil {
            return nil, err
        }

        //设置redis 缓存
        err = client.Set(ctx, key, string(jsonByte), time.Minute*5).Err()
        if err != nil {
            return t, err
        }
        return t, nil
    })

    if err != nil {
        return err
    }

    t = val.(T)

    globalCache.Set(key, t, time.Minute*1)

    return err
}

func RemoveCache(ctx context.Context, key string) error {
    //通知其他节点
    err := rdb.Publish(ctx, channelKey, key).Err()
    if err != nil {
        return err
    }
    fmt.Println(fmt.Sprintf("redis 发布 %s", key))
    return err
}

这里redis 部分 是否需要方法 singleflight.Group 根据实际情况, 但是tidb部分需要放到里面,使用代码

package main

import (
	"context"
	"fmt"
)

func main() {
	ctx := context.Background()
	key := "firstDemo111"
	p := new(Person)
	err := GetCacheData[*Person](ctx, key, p, f)
	//fmt.Println(fmt.Sprintf("%+v", p))

	err = GetCacheData[*Person](ctx, key, p, f)
	//	fmt.Println(fmt.Sprintf("%+v", p))
	RemoveCache(ctx, key)

	err = GetCacheData[*Person](ctx, key, p, f)
	//fmt.Println(fmt.Sprintf("%+v", p))

	err = GetCacheData[*Person](ctx, key, p, f)

	ch := make(chan int, 1)
	val := <-ch
	fmt.Println(val)
	fmt.Println(err)
}

type Person struct {
	Name string
	Age  int
}

func f() (*Person, error) {
	return &Person{Name: "demo", Age: 19}, nil
}

  运行效果 确认,多个节点 redis 订阅 发布可以通信

image

 

posted on 2025-12-08 20:18  dz45693  阅读(9)  评论(0)    收藏  举报

导航