mirror of
https://github.com/truewhile/MeBox.git
synced 2026-09-29 03:26:37 +08:00
254 lines
5.9 KiB
Go
254 lines
5.9 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/redis/go-redis/v9"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/truewhile/MeBox/internal/config"
|
|
)
|
|
|
|
type RuntimeCacheService struct {
|
|
log *zap.Logger
|
|
client *redis.Client
|
|
prefix string
|
|
|
|
mu sync.RWMutex
|
|
memory map[string]runtimeCacheItem
|
|
obj map[string]runtimeObjectItem
|
|
limit int
|
|
}
|
|
|
|
type runtimeCacheItem struct {
|
|
raw []byte
|
|
expiresAt time.Time
|
|
}
|
|
|
|
// runtimeObjectItem 直存 Go 对象,跳过 JSON 编解码。热点路径(整库行、
|
|
// 分组结果)每次请求都要完整反序列化,字节缓存避免了 SQL 却没避免解码;
|
|
// 对象缓存命中时零解码零分配。存储的值视为不可变:读取方如需修改必须
|
|
// 自行浅拷贝。仅进程内生效(Redis 只支持字节),多实例部署退化为各实例
|
|
// 独立缓存,与现有内存 L1 语义一致。
|
|
type runtimeObjectItem struct {
|
|
value any
|
|
expiresAt time.Time
|
|
}
|
|
|
|
func NewRuntimeCacheService(cfg *config.Config, log *zap.Logger) *RuntimeCacheService {
|
|
c := &RuntimeCacheService{log: log, memory: map[string]runtimeCacheItem{}, obj: map[string]runtimeObjectItem{}, limit: 2048}
|
|
if cfg == nil {
|
|
return c
|
|
}
|
|
c.prefix = strings.Trim(strings.TrimSpace(cfg.Cache.RedisPrefix), ":")
|
|
if c.prefix == "" {
|
|
c.prefix = "mebox"
|
|
}
|
|
rawURL := strings.TrimSpace(cfg.Cache.RedisURL)
|
|
if rawURL == "" {
|
|
return c
|
|
}
|
|
opts, err := redis.ParseURL(rawURL)
|
|
if err != nil {
|
|
if log != nil {
|
|
log.Warn("redis cache disabled: invalid redis url", zap.Error(err))
|
|
}
|
|
return c
|
|
}
|
|
client := redis.NewClient(opts)
|
|
pingCtx, cancel := context.WithTimeout(context.Background(), 1200*time.Millisecond)
|
|
defer cancel()
|
|
if err := client.Ping(pingCtx).Err(); err != nil {
|
|
if log != nil {
|
|
log.Warn("redis cache unavailable; using in-process cache", zap.Error(err))
|
|
}
|
|
_ = client.Close()
|
|
return c
|
|
}
|
|
c.client = client
|
|
if log != nil {
|
|
log.Info("redis runtime cache enabled with in-process L1", zap.String("addr", opts.Addr), zap.String("prefix", c.prefix))
|
|
}
|
|
return c
|
|
}
|
|
|
|
func (c *RuntimeCacheService) Enabled() bool {
|
|
return c != nil
|
|
}
|
|
|
|
func (c *RuntimeCacheService) Close() error {
|
|
if c == nil || c.client == nil {
|
|
return nil
|
|
}
|
|
return c.client.Close()
|
|
}
|
|
|
|
func (c *RuntimeCacheService) GetJSON(ctx context.Context, key string, out any) bool {
|
|
if !c.Enabled() || strings.TrimSpace(key) == "" || out == nil {
|
|
return false
|
|
}
|
|
fullKey := c.key(key)
|
|
if raw, ok := c.getMemory(fullKey); ok {
|
|
return json.Unmarshal(raw, out) == nil
|
|
}
|
|
if c.client != nil {
|
|
raw, err := c.client.Get(ctx, fullKey).Bytes()
|
|
if err == nil {
|
|
if json.Unmarshal(raw, out) != nil {
|
|
return false
|
|
}
|
|
c.setMemory(fullKey, raw, 2*time.Second)
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (c *RuntimeCacheService) SetJSON(ctx context.Context, key string, value any, ttl time.Duration) {
|
|
if !c.Enabled() || strings.TrimSpace(key) == "" || value == nil || ttl <= 0 {
|
|
return
|
|
}
|
|
raw, err := json.Marshal(value)
|
|
if err != nil {
|
|
return
|
|
}
|
|
fullKey := c.key(key)
|
|
c.setMemory(fullKey, raw, ttl)
|
|
if c.client != nil {
|
|
_ = c.client.Set(ctx, fullKey, raw, ttl).Err()
|
|
}
|
|
}
|
|
|
|
// GetObject 返回缓存中的对象。返回值不可变:调用方需要修改时必须先自行拷贝。
|
|
func (c *RuntimeCacheService) GetObject(key string) (any, bool) {
|
|
if !c.Enabled() || strings.TrimSpace(key) == "" {
|
|
return nil, false
|
|
}
|
|
fullKey := c.key(key)
|
|
now := time.Now()
|
|
c.mu.RLock()
|
|
item, ok := c.obj[fullKey]
|
|
c.mu.RUnlock()
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
if now.After(item.expiresAt) {
|
|
c.mu.Lock()
|
|
delete(c.obj, fullKey)
|
|
c.mu.Unlock()
|
|
return nil, false
|
|
}
|
|
return item.value, true
|
|
}
|
|
|
|
// SetObject 存入一个此后视为不可变的对象。
|
|
func (c *RuntimeCacheService) SetObject(key string, value any, ttl time.Duration) {
|
|
if !c.Enabled() || strings.TrimSpace(key) == "" || value == nil || ttl <= 0 {
|
|
return
|
|
}
|
|
fullKey := c.key(key)
|
|
now := time.Now()
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if len(c.obj) >= c.limit {
|
|
for k, item := range c.obj {
|
|
if now.After(item.expiresAt) || len(c.obj) >= c.limit {
|
|
delete(c.obj, k)
|
|
}
|
|
if len(c.obj) < c.limit {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
c.obj[fullKey] = runtimeObjectItem{value: value, expiresAt: now.Add(ttl)}
|
|
}
|
|
|
|
func (c *RuntimeCacheService) DeletePrefix(ctx context.Context, prefix string) {
|
|
if !c.Enabled() || strings.TrimSpace(prefix) == "" {
|
|
return
|
|
}
|
|
fullPrefix := c.key(prefix)
|
|
c.deleteMemoryPrefix(fullPrefix)
|
|
if c.client != nil {
|
|
pattern := fullPrefix + "*"
|
|
var cursor uint64
|
|
for {
|
|
keys, next, err := c.client.Scan(ctx, cursor, pattern, 200).Result()
|
|
if err != nil {
|
|
return
|
|
}
|
|
if len(keys) > 0 {
|
|
_ = c.client.Del(ctx, keys...).Err()
|
|
}
|
|
cursor = next
|
|
if cursor == 0 {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func (c *RuntimeCacheService) key(key string) string {
|
|
key = strings.TrimLeft(strings.TrimSpace(key), ":")
|
|
if c.prefix == "" {
|
|
return key
|
|
}
|
|
return c.prefix + ":" + key
|
|
}
|
|
|
|
func (c *RuntimeCacheService) getMemory(key string) ([]byte, bool) {
|
|
now := time.Now()
|
|
c.mu.RLock()
|
|
item, ok := c.memory[key]
|
|
c.mu.RUnlock()
|
|
if !ok {
|
|
return nil, false
|
|
}
|
|
if now.After(item.expiresAt) {
|
|
c.mu.Lock()
|
|
delete(c.memory, key)
|
|
c.mu.Unlock()
|
|
return nil, false
|
|
}
|
|
return item.raw, true
|
|
}
|
|
|
|
func (c *RuntimeCacheService) setMemory(key string, raw []byte, ttl time.Duration) {
|
|
if ttl <= 0 || len(raw) == 0 {
|
|
return
|
|
}
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if len(c.memory) >= c.limit {
|
|
now := time.Now()
|
|
for k, item := range c.memory {
|
|
if now.After(item.expiresAt) || len(c.memory) >= c.limit {
|
|
delete(c.memory, k)
|
|
}
|
|
if len(c.memory) < c.limit {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
c.memory[key] = runtimeCacheItem{raw: append([]byte(nil), raw...), expiresAt: time.Now().Add(ttl)}
|
|
}
|
|
|
|
func (c *RuntimeCacheService) deleteMemoryPrefix(prefix string) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
for key := range c.memory {
|
|
if strings.HasPrefix(key, prefix) {
|
|
delete(c.memory, key)
|
|
}
|
|
}
|
|
for key := range c.obj {
|
|
if strings.HasPrefix(key, prefix) {
|
|
delete(c.obj, key)
|
|
}
|
|
}
|
|
}
|