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.
308 lines
7.9 KiB
Go
308 lines
7.9 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...)
|
||
}
|
||
|
||
// 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
|
||
}
|