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