Files
backend/internal/cache/redis_cache.go
lan 90c57f1a1c
All checks were successful
Build Backend / build (push) Successful in 2m7s
Build Backend / build-docker (push) Successful in 1m51s
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#️⃣{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

308 lines
7.9 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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...)
}
// 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)
}
// 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
}