- Removed Gorse-related configurations, handlers, and dependencies from the codebase. - Introduced HotRank feature with configuration options for ranking posts based on recent activity. - Updated application structure to support HotRank processing, including new caching mechanisms and database interactions. - Cleaned up related DTOs and repository methods to reflect the removal of Gorse and the addition of HotRank functionality.
264 lines
6.6 KiB
Go
264 lines
6.6 KiB
Go
package cache
|
||
|
||
import (
|
||
"cmp"
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"math/rand"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/redis/go-redis/v9"
|
||
)
|
||
|
||
var ErrKeyNotFound = errors.New("key not found")
|
||
|
||
// Cache 缓存接口
|
||
type Cache interface {
|
||
// Set 设置缓存值,支持TTL
|
||
Set(key string, value interface{}, ttl time.Duration)
|
||
// Get 获取缓存值
|
||
Get(key string) (interface{}, bool)
|
||
// Delete 删除缓存
|
||
Delete(key string)
|
||
// DeleteByPrefix 根据前缀删除缓存
|
||
DeleteByPrefix(prefix string)
|
||
// Clear 清空所有缓存
|
||
Clear()
|
||
// Exists 检查键是否存在
|
||
Exists(key string) bool
|
||
// Increment 增加计数器的值
|
||
Increment(key string) int64
|
||
// IncrementBy 增加指定值
|
||
IncrementBy(key string, value int64) int64
|
||
|
||
// ==================== Hash 操作 ====================
|
||
// HSet 设置 Hash 字段
|
||
HSet(ctx context.Context, key string, field string, value interface{}) error
|
||
// HMSet 批量设置 Hash 字段
|
||
HMSet(ctx context.Context, key string, values map[string]interface{}) error
|
||
// HGet 获取 Hash 字段值
|
||
HGet(ctx context.Context, key string, field string) (string, error)
|
||
// HMGet 批量获取 Hash 字段值
|
||
HMGet(ctx context.Context, key string, fields ...string) ([]interface{}, error)
|
||
// HGetAll 获取 Hash 所有字段
|
||
HGetAll(ctx context.Context, key string) (map[string]string, error)
|
||
// HDel 删除 Hash 字段
|
||
HDel(ctx context.Context, key string, fields ...string) error
|
||
|
||
// ==================== Sorted Set 操作 ====================
|
||
// ZAdd 添加 Sorted Set 成员
|
||
ZAdd(ctx context.Context, key string, score float64, member string) error
|
||
// ZRangeByScore 按分数范围获取成员(升序)
|
||
ZRangeByScore(ctx context.Context, key string, min, max string, offset, count int64) ([]string, error)
|
||
// ZRevRangeByScore 按分数范围获取成员(降序)
|
||
ZRevRangeByScore(ctx context.Context, key string, max, min string, offset, count int64) ([]string, error)
|
||
// ZRevRange 按排名倒序取成员(score 从高到低,start/stop 为排名下标,含端点)
|
||
ZRevRange(ctx context.Context, key string, start, stop int64) ([]string, error)
|
||
// ZReplaceSortedSet 原子替换整个 ZSET(先删后批量写入)
|
||
ZReplaceSortedSet(ctx context.Context, key string, members []redis.Z) error
|
||
// ZRem 删除 Sorted Set 成员
|
||
ZRem(ctx context.Context, key string, members ...interface{}) error
|
||
// ZCard 获取 Sorted Set 成员数量
|
||
ZCard(ctx context.Context, key string) (int64, error)
|
||
|
||
// ==================== 计数器操作 ====================
|
||
// Incr 原子递增(返回新值)
|
||
Incr(ctx context.Context, key string) (int64, error)
|
||
// Expire 设置过期时间
|
||
Expire(ctx context.Context, key string, ttl time.Duration) error
|
||
}
|
||
|
||
const nullMarkerValue = "__carrot_cache_null__"
|
||
|
||
var loadLocks sync.Map
|
||
|
||
type Settings struct {
|
||
Enabled bool
|
||
KeyPrefix string
|
||
DefaultTTL time.Duration
|
||
NullTTL time.Duration
|
||
JitterRatio float64
|
||
PostListTTL time.Duration
|
||
ConversationTTL time.Duration
|
||
UnreadCountTTL time.Duration
|
||
GroupMembersTTL time.Duration
|
||
DisableFlushDB bool
|
||
}
|
||
|
||
var settings = Settings{
|
||
Enabled: true,
|
||
DefaultTTL: 30 * time.Second,
|
||
NullTTL: 5 * time.Second,
|
||
JitterRatio: 0.1,
|
||
PostListTTL: 30 * time.Second,
|
||
ConversationTTL: 60 * time.Second,
|
||
UnreadCountTTL: 30 * time.Second,
|
||
GroupMembersTTL: 120 * time.Second,
|
||
DisableFlushDB: true,
|
||
}
|
||
|
||
func Configure(s Settings) {
|
||
settings.Enabled = s.Enabled
|
||
if s.KeyPrefix != "" {
|
||
settings.KeyPrefix = s.KeyPrefix
|
||
}
|
||
if s.DefaultTTL > 0 {
|
||
settings.DefaultTTL = s.DefaultTTL
|
||
}
|
||
if s.NullTTL > 0 {
|
||
settings.NullTTL = s.NullTTL
|
||
}
|
||
if s.JitterRatio > 0 {
|
||
settings.JitterRatio = s.JitterRatio
|
||
}
|
||
if s.PostListTTL > 0 {
|
||
settings.PostListTTL = s.PostListTTL
|
||
}
|
||
if s.ConversationTTL > 0 {
|
||
settings.ConversationTTL = s.ConversationTTL
|
||
}
|
||
if s.UnreadCountTTL > 0 {
|
||
settings.UnreadCountTTL = s.UnreadCountTTL
|
||
}
|
||
if s.GroupMembersTTL > 0 {
|
||
settings.GroupMembersTTL = s.GroupMembersTTL
|
||
}
|
||
settings.DisableFlushDB = s.DisableFlushDB
|
||
}
|
||
|
||
func GetSettings() Settings {
|
||
return settings
|
||
}
|
||
|
||
func normalizeKey(key string) string {
|
||
return cmp.Or(settings.KeyPrefix, "") + ":" + key
|
||
}
|
||
|
||
// ResolveKey 返回带全局前缀的实际 Redis 键(供需直连 Redis 的组件使用)
|
||
func ResolveKey(key string) string {
|
||
return normalizeKey(key)
|
||
}
|
||
|
||
func normalizePrefix(prefix string) string {
|
||
return cmp.Or(settings.KeyPrefix, "") + ":" + prefix
|
||
}
|
||
|
||
func SetWithJitter(c Cache, key string, value interface{}, ttl time.Duration, jitterRatio float64) {
|
||
if !settings.Enabled {
|
||
return
|
||
}
|
||
c.Set(key, value, ApplyTTLJitter(ttl, jitterRatio))
|
||
}
|
||
|
||
func SetNull(c Cache, key string, ttl time.Duration) {
|
||
if !settings.Enabled {
|
||
return
|
||
}
|
||
c.Set(key, nullMarkerValue, ttl)
|
||
}
|
||
|
||
func ApplyTTLJitter(ttl time.Duration, jitterRatio float64) time.Duration {
|
||
if ttl <= 0 || jitterRatio <= 0 {
|
||
return ttl
|
||
}
|
||
if jitterRatio > 1 {
|
||
jitterRatio = 1
|
||
}
|
||
maxJitter := int64(float64(ttl) * jitterRatio)
|
||
if maxJitter <= 0 {
|
||
return ttl
|
||
}
|
||
delta := rand.Int63n(maxJitter + 1)
|
||
return ttl + time.Duration(delta)
|
||
}
|
||
|
||
func GetTyped[T any](c Cache, key string) (T, bool) {
|
||
var zero T
|
||
if !settings.Enabled {
|
||
return zero, false
|
||
}
|
||
raw, ok := c.Get(key)
|
||
if !ok {
|
||
recordMiss()
|
||
return zero, false
|
||
}
|
||
if str, ok := raw.(string); ok && str == nullMarkerValue {
|
||
recordHit()
|
||
return zero, false
|
||
}
|
||
|
||
if typed, ok := raw.(T); ok {
|
||
recordHit()
|
||
return typed, true
|
||
}
|
||
|
||
var out T
|
||
switch v := raw.(type) {
|
||
case string:
|
||
if err := json.Unmarshal([]byte(v), &out); err != nil {
|
||
recordDecodeError()
|
||
return zero, false
|
||
}
|
||
recordHit()
|
||
return out, true
|
||
case []byte:
|
||
if err := json.Unmarshal(v, &out); err != nil {
|
||
recordDecodeError()
|
||
return zero, false
|
||
}
|
||
recordHit()
|
||
return out, true
|
||
default:
|
||
data, err := json.Marshal(v)
|
||
if err != nil {
|
||
recordDecodeError()
|
||
return zero, false
|
||
}
|
||
if err := json.Unmarshal(data, &out); err != nil {
|
||
recordDecodeError()
|
||
return zero, false
|
||
}
|
||
recordHit()
|
||
return out, true
|
||
}
|
||
}
|
||
|
||
func GetOrLoadTyped[T any](
|
||
c Cache,
|
||
key string,
|
||
ttl time.Duration,
|
||
jitterRatio float64,
|
||
nullTTL time.Duration,
|
||
loader func() (T, error),
|
||
) (T, error) {
|
||
if cached, ok := GetTyped[T](c, key); ok {
|
||
return cached, nil
|
||
}
|
||
|
||
lockValue, _ := loadLocks.LoadOrStore(key, &sync.Mutex{})
|
||
lock := lockValue.(*sync.Mutex)
|
||
lock.Lock()
|
||
defer lock.Unlock()
|
||
|
||
if cached, ok := GetTyped[T](c, key); ok {
|
||
return cached, nil
|
||
}
|
||
|
||
loaded, err := loader()
|
||
if err != nil {
|
||
var zero T
|
||
return zero, err
|
||
}
|
||
|
||
encoded, marshalErr := json.Marshal(loaded)
|
||
if marshalErr == nil && string(encoded) == "null" && nullTTL > 0 {
|
||
SetNull(c, key, nullTTL)
|
||
return loaded, nil
|
||
}
|
||
|
||
SetWithJitter(c, key, loaded, ttl, jitterRatio)
|
||
return loaded, nil
|
||
}
|