You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

321 lines
5.7 KiB
Go

package cache
import (
"context"
"joybuywork/internal/app"
"joybuywork/internal/consts/ckey"
"joybuywork/internal/plugins/jsonm"
"errors"
"fmt"
"math/rand"
"sync"
"sync/atomic"
"time"
grds "github.com/go-redis/redis/v8"
"gorm.io/gorm/schema"
)
var keyPrefix = "dbcache:"
var (
cacheHits atomic.Int64
cacheMisses atomic.Int64
)
func init() {
machineIP := app.GetLocalIP()
app.GoSafe(func() {
ticker := time.NewTicker(4 * time.Hour)
defer ticker.Stop()
for range ticker.C {
hits := cacheHits.Swap(0)
misses := cacheMisses.Swap(0)
total := hits + misses
if total == 0 {
continue
}
hitRate := float64(hits) / float64(total) * 100
app.FeishuNormalSend(fmt.Sprintf(
"db cache hit rate in the last minute: ip=%s, rate=%.2f%% (%d/%d)",
machineIP,
hitRate,
hits,
total,
))
}
})
}
func recordCacheResult(err error) {
if err == nil {
cacheHits.Add(1)
return
}
cacheMisses.Add(1)
}
type Option func(cache *Cache)
type Cache struct {
ctx context.Context
tabler schema.Tabler
rdb *grds.Client
}
func (c *Cache) init(ctx context.Context, tabler schema.Tabler, opts ...Option) {
c.ctx = ctx
c.tabler = tabler
c.rdb = app.Redis.Cache
for _, opt := range opts {
opt(c)
}
}
func (c *Cache) getIdxInt64(key string, find func() ([]byte, error), opts ...Event) (idx int64, err error) {
event := redisEvent{}
for _, opt := range opts {
opt(&event)
}
data, err := c.redis(c.ctx, key, find, event)
if err != nil || len(data) == 0 {
return
}
err = jsonm.Unmarshal(data, &idx)
return
}
func (c *Cache) get(key string, find func() ([]byte, error), opts ...Event) (entry []byte, err error) {
event := redisEvent{}
for _, opt := range opts {
opt(&event)
}
return c.redis(c.ctx, key, find, event)
}
func (c *Cache) getKeys(keys []string, find func(idKeys []string) (map[string][]byte, error), opts ...Event) (entrys [][]byte, err error) {
event := redisEvent{}
for _, opt := range opts {
opt(&event)
}
return c.redisKeys(c.ctx, keys, find, event)
}
// Redis 获取数据
func (c *Cache) redis(ctx context.Context, idKey string, find func() ([]byte, error), event redisEvent) (data []byte, err error) {
data, err = c.rdb.Get(ctx, idKey).Bytes()
recordCacheResult(err)
if err == nil || !errors.Is(err, grds.Nil) {
return
}
//锁
idKeyLock := ckey.ForLock(idKey)
lock := app.NewFollower(ctx, idKeyLock).Lock()
defer lock.Release()
if lock.Error() != nil {
err = lock.Error()
return
}
data, err = c.rdb.Get(ctx, idKey).Bytes()
if err == nil || !errors.Is(err, grds.Nil) {
return
}
data, err = find()
if err != nil {
return
}
// 不存数据就设置成某个数据
if data == nil && event.NilSet != nil {
data, err = event.NilSet()
if err != nil {
return
}
}
err = c.set(ctx, idKey, data, event)
if err != nil {
return nil, err
}
return
}
// Redis 获取数据
func (c *Cache) redisKeys(ctx context.Context, idKeys []string, find func(idKeys []string) (map[string][]byte, error), event redisEvent) (list [][]byte, err error) {
type state struct {
ok bool
idKey string
}
wg := sync.WaitGroup{}
ch := make(chan state, 1)
var lcAppend = func(idKey string) (err1 error) {
//锁
var one []byte
idKeyLock := ckey.ForLock(idKey)
lock := app.NewFollower(ctx, idKeyLock).Lock()
defer lock.Release()
if lock.Error() != nil {
return lock.Error()
}
one, err1 = c.rdb.Get(ctx, idKey).Bytes()
if err1 == nil {
list = append(list, one)
ch <- state{
ok: true,
idKey: idKey,
}
}
if !errors.Is(err1, grds.Nil) {
return
}
err1 = nil
wg.Add(1)
ch <- state{
ok: false,
idKey: idKey,
}
return
}
app.GoSafe(func() {
var memIdKeys, aLen []string
defer func() {
if len(memIdKeys) == 0 {
return
}
dm, _ := find(memIdKeys)
for idKey, data := range dm {
if data == nil {
return
}
list = append(list, data)
_ = c.set(ctx, idKey, data, event)
}
for range memIdKeys {
wg.Done()
}
}()
for {
idKey, ok := <-ch
if !ok {
return
}
aLen = append(aLen, idKey.idKey)
if !idKey.ok {
memIdKeys = append(memIdKeys, idKey.idKey)
}
if len(aLen) == len(idKeys) {
return
}
}
})
for _, idKey := range idKeys {
var one []byte
one, err = c.rdb.Get(ctx, idKey).Bytes()
recordCacheResult(err)
if err == nil {
list = append(list, one)
ch <- state{
ok: true,
idKey: idKey,
}
continue
}
if !errors.Is(err, grds.Nil) {
return
}
err = nil
key := idKey
wg.Add(1)
app.GoSafe(func() {
defer wg.Done()
_ = lcAppend(key)
})
}
wg.Wait()
return
}
func (c *Cache) Prefix() string {
return fmt.Sprintf("%s%s%s", ckey.ProjectPrefix, keyPrefix, c.tabler.TableName())
}
func (c *Cache) TTLRand() time.Duration {
return time.Duration(rand.Intn(60)+60) * time.Minute
}
func (c *Cache) set(ctx context.Context, key string, data []byte, event redisEvent) (err error) {
if data != nil {
//1小时~2小时随机
return c.rdb.Set(ctx, key, data, c.TTLRand()).Err()
}
// 如果是nil也存
if event.NilSetLimit > 0 {
return c.rdb.Set(ctx, key, data, event.NilSetLimit).Err()
}
return
}
type LocalLogger struct {
}
func (l LocalLogger) Printf(format string, v ...interface{}) {
app.Logger.Info(context.Background(), fmt.Sprintf(format, v...))
}
const idxSplit = "||"
func (c *Cache) IdxKey(values ...any) string {
var suffix string
var idxL = len(values) / 2
var idx int
for i := 0; i < idxL; i++ {
if i == 0 {
suffix += fmt.Sprintf("%v_%v", values[idx], values[idx+1])
} else {
suffix += fmt.Sprintf("%s%v_%v", idxSplit, values[idx], values[idx+1])
}
idx += 2
}
return fmt.Sprintf("%s%s%s:idx:%s", ckey.ProjectPrefix, keyPrefix, c.tabler.TableName(), suffix)
}