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.
295 lines
7.5 KiB
Go
295 lines
7.5 KiB
Go
package cache
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"time"
|
||
|
||
redislib "github.com/redis/go-redis/v9"
|
||
"go.uber.org/zap"
|
||
|
||
redisPkg "with_you/internal/pkg/redis"
|
||
)
|
||
|
||
// RedisCache Redis缓存实现
|
||
type RedisCache struct {
|
||
client *redisPkg.Client
|
||
ctx context.Context
|
||
}
|
||
|
||
// NewRedisCache 创建Redis缓存
|
||
func NewRedisCache(client *redisPkg.Client) *RedisCache {
|
||
return &RedisCache{
|
||
client: client,
|
||
ctx: context.Background(),
|
||
}
|
||
}
|
||
|
||
// Set 设置缓存值
|
||
func (c *RedisCache) Set(key string, value any, ttl time.Duration) {
|
||
key = normalizeKey(key)
|
||
// 将值序列化为JSON
|
||
data, err := json.Marshal(value)
|
||
if err != nil {
|
||
recordSetError()
|
||
zap.L().Error("Failed to marshal value for key",
|
||
zap.String("key", key),
|
||
zap.Error(err),
|
||
)
|
||
return
|
||
}
|
||
|
||
if err := c.client.Set(c.ctx, key, data, ttl); err != nil {
|
||
recordSetError()
|
||
zap.L().Error("Failed to set key",
|
||
zap.String("key", key),
|
||
zap.Error(err),
|
||
)
|
||
}
|
||
}
|
||
|
||
// Get 获取缓存值
|
||
func (c *RedisCache) Get(key string) (any, bool) {
|
||
key = normalizeKey(key)
|
||
data, err := c.client.Get(c.ctx, key)
|
||
if err != nil {
|
||
if err == redislib.Nil {
|
||
return nil, false
|
||
}
|
||
zap.L().Error("Failed to get key",
|
||
zap.String("key", key),
|
||
zap.Error(err),
|
||
)
|
||
return nil, false
|
||
}
|
||
|
||
// 返回原始字符串,由调用侧决定如何解码为目标类型
|
||
return data, true
|
||
}
|
||
|
||
// Delete 删除缓存
|
||
func (c *RedisCache) Delete(key string) {
|
||
key = normalizeKey(key)
|
||
recordInvalidate()
|
||
if err := c.client.Del(c.ctx, key); err != nil {
|
||
zap.L().Error("Failed to delete key",
|
||
zap.String("key", key),
|
||
zap.Error(err),
|
||
)
|
||
}
|
||
}
|
||
|
||
// DeleteByPrefix 根据前缀删除缓存
|
||
func (c *RedisCache) DeleteByPrefix(prefix string) {
|
||
prefix = normalizePrefix(prefix)
|
||
rdb := c.client.GetClient()
|
||
var cursor uint64
|
||
for {
|
||
keys, nextCursor, err := rdb.Scan(c.ctx, cursor, prefix+"*", 100).Result()
|
||
if err != nil {
|
||
zap.L().Error("Failed to scan keys with prefix",
|
||
zap.String("prefix", prefix),
|
||
zap.Error(err),
|
||
)
|
||
return
|
||
}
|
||
|
||
if len(keys) > 0 {
|
||
recordInvalidateMultiple(int64(len(keys)))
|
||
if err := c.client.Del(c.ctx, keys...); err != nil {
|
||
zap.L().Error("Failed to delete keys with prefix",
|
||
zap.String("prefix", prefix),
|
||
zap.Error(err),
|
||
)
|
||
}
|
||
}
|
||
|
||
cursor = nextCursor
|
||
if cursor == 0 {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
// DeleteBatch 批量删除多个 key(使用 Redis Pipeline 减少网络往返)
|
||
func (c *RedisCache) DeleteBatch(keys []string) {
|
||
if len(keys) == 0 {
|
||
return
|
||
}
|
||
normalizedKeys := make([]string, len(keys))
|
||
for i, k := range keys {
|
||
normalizedKeys[i] = normalizeKey(k)
|
||
}
|
||
|
||
recordInvalidateMultiple(int64(len(normalizedKeys)))
|
||
// 使用 pipeline 批量删除
|
||
pipe := c.client.GetClient().Pipeline()
|
||
for _, k := range normalizedKeys {
|
||
pipe.Del(c.ctx, k)
|
||
}
|
||
if _, err := pipe.Exec(c.ctx); err != nil {
|
||
zap.L().Error("Failed to batch delete keys",
|
||
zap.Int("count", len(normalizedKeys)),
|
||
zap.Error(err),
|
||
)
|
||
}
|
||
}
|
||
|
||
// Clear 清空所有缓存
|
||
func (c *RedisCache) Clear() {
|
||
if settings.DisableFlushDB {
|
||
zap.L().Debug("Skip FlushDB because cache.disable_flushdb=true")
|
||
return
|
||
}
|
||
recordInvalidate()
|
||
rdb := c.client.GetClient()
|
||
if err := rdb.FlushDB(c.ctx).Err(); err != nil {
|
||
zap.L().Error("Failed to clear cache",
|
||
zap.Error(err),
|
||
)
|
||
}
|
||
}
|
||
|
||
// Exists 检查键是否存在
|
||
func (c *RedisCache) Exists(key string) bool {
|
||
key = normalizeKey(key)
|
||
n, err := c.client.Exists(c.ctx, key)
|
||
if err != nil {
|
||
zap.L().Error("Failed to check existence of key",
|
||
zap.String("key", key),
|
||
zap.Error(err),
|
||
)
|
||
return false
|
||
}
|
||
return n > 0
|
||
}
|
||
|
||
// Increment 增加计数器的值
|
||
func (c *RedisCache) Increment(key string) int64 {
|
||
return c.IncrementBy(key, 1)
|
||
}
|
||
|
||
// IncrementBy 增加指定值
|
||
func (c *RedisCache) IncrementBy(key string, value int64) int64 {
|
||
key = normalizeKey(key)
|
||
rdb := c.client.GetClient()
|
||
result, err := rdb.IncrBy(c.ctx, key, value).Result()
|
||
if err != nil {
|
||
zap.L().Error("Failed to increment key",
|
||
zap.String("key", key),
|
||
zap.Error(err),
|
||
)
|
||
return 0
|
||
}
|
||
return result
|
||
}
|
||
|
||
// ==================== RedisCache Hash 操作 ====================
|
||
|
||
// HSet 设置 Hash 字段
|
||
func (c *RedisCache) HSet(ctx context.Context, key string, field string, value any) error {
|
||
key = normalizeKey(key)
|
||
return c.client.HSet(ctx, key, field, value)
|
||
}
|
||
|
||
// HMSet 批量设置 Hash 字段
|
||
func (c *RedisCache) HMSet(ctx context.Context, key string, values map[string]any) error {
|
||
key = normalizeKey(key)
|
||
return c.client.HMSet(ctx, key, values)
|
||
}
|
||
|
||
// HGet 获取 Hash 字段值
|
||
func (c *RedisCache) HGet(ctx context.Context, key string, field string) (string, error) {
|
||
key = normalizeKey(key)
|
||
return c.client.HGet(ctx, key, field)
|
||
}
|
||
|
||
// HMGet 批量获取 Hash 字段值
|
||
func (c *RedisCache) HMGet(ctx context.Context, key string, fields ...string) ([]any, error) {
|
||
key = normalizeKey(key)
|
||
return c.client.HMGet(ctx, key, fields...)
|
||
}
|
||
|
||
// HGetAll 获取 Hash 所有字段
|
||
func (c *RedisCache) HGetAll(ctx context.Context, key string) (map[string]string, error) {
|
||
key = normalizeKey(key)
|
||
return c.client.HGetAll(ctx, key)
|
||
}
|
||
|
||
// HDel 删除 Hash 字段
|
||
func (c *RedisCache) HDel(ctx context.Context, key string, fields ...string) error {
|
||
key = normalizeKey(key)
|
||
return c.client.HDel(ctx, key, fields...)
|
||
}
|
||
|
||
// ==================== RedisCache Sorted Set 操作 ====================
|
||
|
||
// ZAdd 添加 Sorted Set 成员
|
||
func (c *RedisCache) ZAdd(ctx context.Context, key string, score float64, member string) error {
|
||
key = normalizeKey(key)
|
||
return c.client.ZAdd(ctx, key, score, member)
|
||
}
|
||
|
||
// ZRangeByScore 按分数范围获取成员(升序)
|
||
func (c *RedisCache) ZRangeByScore(ctx context.Context, key string, min, max string, offset, count int64) ([]string, error) {
|
||
key = normalizeKey(key)
|
||
return c.client.ZRangeByScore(ctx, key, min, max, offset, count)
|
||
}
|
||
|
||
// ZRevRangeByScore 按分数范围获取成员(降序)
|
||
func (c *RedisCache) ZRevRangeByScore(ctx context.Context, key string, max, min string, offset, count int64) ([]string, error) {
|
||
key = normalizeKey(key)
|
||
return c.client.ZRevRangeByScore(ctx, key, max, min, offset, count)
|
||
}
|
||
|
||
// ZRevRange 按排名倒序获取成员
|
||
func (c *RedisCache) ZRevRange(ctx context.Context, key string, start, stop int64) ([]string, error) {
|
||
key = normalizeKey(key)
|
||
return c.client.ZRevRange(ctx, key, start, stop)
|
||
}
|
||
|
||
// ZReplaceSortedSet 删除后整表重写 ZSET
|
||
func (c *RedisCache) ZReplaceSortedSet(ctx context.Context, key string, members []redislib.Z) error {
|
||
key = normalizeKey(key)
|
||
rdb := c.client.GetClient()
|
||
if err := rdb.Del(ctx, key).Err(); err != nil {
|
||
return err
|
||
}
|
||
if len(members) == 0 {
|
||
return nil
|
||
}
|
||
return c.client.ZAddArgs(ctx, key, members...)
|
||
}
|
||
|
||
// ZRem 删除 Sorted Set 成员
|
||
func (c *RedisCache) ZRem(ctx context.Context, key string, members ...any) error {
|
||
key = normalizeKey(key)
|
||
return c.client.ZRem(ctx, key, members...)
|
||
}
|
||
|
||
// ZCard 获取 Sorted Set 成员数量
|
||
func (c *RedisCache) ZCard(ctx context.Context, key string) (int64, error) {
|
||
key = normalizeKey(key)
|
||
return c.client.ZCard(ctx, key)
|
||
}
|
||
|
||
// ==================== RedisCache 计数器操作 ====================
|
||
|
||
// Incr 原子递增(返回新值)
|
||
func (c *RedisCache) Incr(ctx context.Context, key string) (int64, error) {
|
||
key = normalizeKey(key)
|
||
return c.client.Incr(ctx, key)
|
||
}
|
||
|
||
// Expire 设置过期时间
|
||
func (c *RedisCache) Expire(ctx context.Context, key string, ttl time.Duration) error {
|
||
key = normalizeKey(key)
|
||
_, err := c.client.Expire(ctx, key, ttl)
|
||
return err
|
||
}
|
||
|
||
// GetRedisClient 获取Redis客户端
|
||
func (c *RedisCache) GetRedisClient() *redisPkg.Client {
|
||
return c.client
|
||
}
|