This commit introduces several architectural improvements and optimizations across the codebase: - **Performance & Reliability**: - Implemented Redis pipelining in `ConversationCache.CacheMessage` to reduce network round-trips. - Added a circuit breaker to the JPush client to prevent cascading failures. - Introduced batch deletion and batch member addition capabilities in repositories. - Added message idempotency support using `client_msg_id` and a Redis-based cache. - Optimized WebSocket handling with connection limits (total and per-user) and improved error logging. - **Code Refactoring**: - Refactored `Router` to use a `RouterDeps` struct, simplifying the constructor and improving maintainability. - Unified model ID generation logic using new `id_helper.go` (supporting UUID and Snowflake). - Standardized JSON serialization/deserialization in models using `json_helper.go`. - Refactored DTO conversion logic, specifically for `UserResponse` (using functional options) and `Report` responses. - Removed redundant/deprecated DTOs like `PostDetailResponse` and `TradeItemDetailResponse`. - **Cache Improvements**: - Enhanced `LayeredCache` with `SetRaw` to avoid double-encoding when promoting values from Redis to local cache. - Added `DeleteBatch` support to the cache interface. - **Other Changes**: - Cleaned up `config.go` by removing redundant default values and explicit environment variable overrides. - Improved WebSocket registration flow to handle connection limits gracefully.
266 lines
6.7 KiB
Go
266 lines
6.7 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 any, ttl time.Duration)
|
||
// Get 获取缓存值
|
||
Get(key string) (any, 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
|
||
// DeleteBatch 批量删除多个 key(使用 Pipeline 减少网络往返)
|
||
DeleteBatch(keys []string)
|
||
|
||
// ==================== Hash 操作 ====================
|
||
// HSet 设置 Hash 字段
|
||
HSet(ctx context.Context, key string, field string, value any) error
|
||
// HMSet 批量设置 Hash 字段
|
||
HMSet(ctx context.Context, key string, values map[string]any) error
|
||
// HGet 获取 Hash 字段值
|
||
HGet(ctx context.Context, key string, field string) (string, error)
|
||
// HMGet 批量获取 Hash 字段值
|
||
HMGet(ctx context.Context, key string, fields ...string) ([]any, 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 ...any) 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 any, 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
|
||
}
|