Files
backend/internal/cache/redis_cache.go

308 lines
7.9 KiB
Go
Raw Normal View History

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
}
}
}
refactor: improve system stability, performance, and code structure 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.
2026-05-04 13:07:03 +08:00
// 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...)
}
feat(core): optimize performance and reliability through batching and redis-backed unread counts This commit introduces several significant architectural improvements to enhance system performance, scalability, and reliability: - **Performance Optimization (N+1 Problem Resolution)**: Implemented batching mechanisms across `MessageHandler`, `ChatService`, `GroupService`, `MessageService`, and `UserService`. This replaces multiple individual database/cache queries with single batch operations for fetching unread counts, last messages, participants, and user information. - **Enhanced Unread Count Management**: Migrated unread count tracking to a Redis Hash-based approach (`unread:hash:{userID}`). This allows for atomic increments/decrements and efficient retrieval of both individual conversation unread counts and total unread counts for a user. - **Improved Message Sequencing**: Integrated Redis-based sequence (`seq`) pre-allocation for messages to ensure strict ordering and reduce database contention during high-concurrency message creation. - **Push Service Reliability**: Refactored the push notification system to include persistent `PushRecord` tracking. Added a recovery mechanism (`recoverPendingPushes`) to reload pending notifications from the database upon service startup, ensuring better delivery guarantees. - **WebSocket Reliability**: Updated the WebSocket hub to move away from in-memory history replay in favor of a client-driven synchronization model (`sync_required` event and `ack` handling), reducing memory overhead and improving connection stability. - **Cache Layer Enhancements**: Added `HIncrBy` and `IncrBySeq` to the `Cache` interface and its implementations (`RedisCache`, `LayeredCache`) to support the new unread and sequence management logic.
2026-05-04 18:31:03 +08:00
// HIncrBy 原子递增 Hash 字段的值
func (c *RedisCache) HIncrBy(ctx context.Context, key string, field string, incr int64) (int64, error) {
key = normalizeKey(key)
return c.client.HIncrBy(ctx, key, field, incr)
}
// ==================== 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)
}
feat(core): optimize performance and reliability through batching and redis-backed unread counts This commit introduces several significant architectural improvements to enhance system performance, scalability, and reliability: - **Performance Optimization (N+1 Problem Resolution)**: Implemented batching mechanisms across `MessageHandler`, `ChatService`, `GroupService`, `MessageService`, and `UserService`. This replaces multiple individual database/cache queries with single batch operations for fetching unread counts, last messages, participants, and user information. - **Enhanced Unread Count Management**: Migrated unread count tracking to a Redis Hash-based approach (`unread:hash:{userID}`). This allows for atomic increments/decrements and efficient retrieval of both individual conversation unread counts and total unread counts for a user. - **Improved Message Sequencing**: Integrated Redis-based sequence (`seq`) pre-allocation for messages to ensure strict ordering and reduce database contention during high-concurrency message creation. - **Push Service Reliability**: Refactored the push notification system to include persistent `PushRecord` tracking. Added a recovery mechanism (`recoverPendingPushes`) to reload pending notifications from the database upon service startup, ensuring better delivery guarantees. - **WebSocket Reliability**: Updated the WebSocket hub to move away from in-memory history replay in favor of a client-driven synchronization model (`sync_required` event and `ack` handling), reducing memory overhead and improving connection stability. - **Cache Layer Enhancements**: Added `HIncrBy` and `IncrBySeq` to the `Cache` interface and its implementations (`RedisCache`, `LayeredCache`) to support the new unread and sequence management logic.
2026-05-04 18:31:03 +08:00
// IncrBySeq 原子递增指定值(用于 seq 对齐)
func (c *RedisCache) IncrBySeq(ctx context.Context, key string, delta int64) (int64, error) {
key = normalizeKey(key)
rdb := c.client.GetClient()
return rdb.IncrBy(ctx, key, delta).Result()
}
// 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
}