2026-04-22 16:01:59 +08:00
|
|
|
|
package cache
|
2026-03-13 09:38:18 +08:00
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"encoding/json"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
2026-03-24 05:18:30 +08:00
|
|
|
|
redislib "github.com/redis/go-redis/v9"
|
2026-03-17 00:47:17 +08:00
|
|
|
|
"go.uber.org/zap"
|
2026-03-13 09:38:18 +08:00
|
|
|
|
|
2026-04-22 16:01:59 +08:00
|
|
|
|
redisPkg "with_you/internal/pkg/redis"
|
2026-03-13 09:38:18 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// 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 设置缓存值
|
2026-03-30 04:49:35 +08:00
|
|
|
|
func (c *RedisCache) Set(key string, value any, ttl time.Duration) {
|
2026-03-13 09:38:18 +08:00
|
|
|
|
key = normalizeKey(key)
|
|
|
|
|
|
// 将值序列化为JSON
|
|
|
|
|
|
data, err := json.Marshal(value)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
recordSetError()
|
2026-03-17 00:47:17 +08:00
|
|
|
|
zap.L().Error("Failed to marshal value for key",
|
|
|
|
|
|
zap.String("key", key),
|
|
|
|
|
|
zap.Error(err),
|
|
|
|
|
|
)
|
2026-03-13 09:38:18 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := c.client.Set(c.ctx, key, data, ttl); err != nil {
|
|
|
|
|
|
recordSetError()
|
2026-03-17 00:47:17 +08:00
|
|
|
|
zap.L().Error("Failed to set key",
|
|
|
|
|
|
zap.String("key", key),
|
|
|
|
|
|
zap.Error(err),
|
|
|
|
|
|
)
|
2026-03-13 09:38:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Get 获取缓存值
|
2026-03-30 04:49:35 +08:00
|
|
|
|
func (c *RedisCache) Get(key string) (any, bool) {
|
2026-03-13 09:38:18 +08:00
|
|
|
|
key = normalizeKey(key)
|
|
|
|
|
|
data, err := c.client.Get(c.ctx, key)
|
|
|
|
|
|
if err != nil {
|
2026-03-24 05:18:30 +08:00
|
|
|
|
if err == redislib.Nil {
|
2026-03-13 09:38:18 +08:00
|
|
|
|
return nil, false
|
|
|
|
|
|
}
|
2026-03-17 00:47:17 +08:00
|
|
|
|
zap.L().Error("Failed to get key",
|
|
|
|
|
|
zap.String("key", key),
|
|
|
|
|
|
zap.Error(err),
|
|
|
|
|
|
)
|
2026-03-13 09:38:18 +08:00
|
|
|
|
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 {
|
2026-03-17 00:47:17 +08:00
|
|
|
|
zap.L().Error("Failed to delete key",
|
|
|
|
|
|
zap.String("key", key),
|
|
|
|
|
|
zap.Error(err),
|
|
|
|
|
|
)
|
2026-03-13 09:38:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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 {
|
2026-03-17 00:47:17 +08:00
|
|
|
|
zap.L().Error("Failed to scan keys with prefix",
|
|
|
|
|
|
zap.String("prefix", prefix),
|
|
|
|
|
|
zap.Error(err),
|
|
|
|
|
|
)
|
2026-03-13 09:38:18 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if len(keys) > 0 {
|
|
|
|
|
|
recordInvalidateMultiple(int64(len(keys)))
|
|
|
|
|
|
if err := c.client.Del(c.ctx, keys...); err != nil {
|
2026-03-17 00:47:17 +08:00
|
|
|
|
zap.L().Error("Failed to delete keys with prefix",
|
|
|
|
|
|
zap.String("prefix", prefix),
|
|
|
|
|
|
zap.Error(err),
|
|
|
|
|
|
)
|
2026-03-13 09:38:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
cursor = nextCursor
|
|
|
|
|
|
if cursor == 0 {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
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),
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-13 09:38:18 +08:00
|
|
|
|
// Clear 清空所有缓存
|
|
|
|
|
|
func (c *RedisCache) Clear() {
|
|
|
|
|
|
if settings.DisableFlushDB {
|
2026-03-17 00:47:17 +08:00
|
|
|
|
zap.L().Debug("Skip FlushDB because cache.disable_flushdb=true")
|
2026-03-13 09:38:18 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
recordInvalidate()
|
|
|
|
|
|
rdb := c.client.GetClient()
|
|
|
|
|
|
if err := rdb.FlushDB(c.ctx).Err(); err != nil {
|
2026-03-17 00:47:17 +08:00
|
|
|
|
zap.L().Error("Failed to clear cache",
|
|
|
|
|
|
zap.Error(err),
|
|
|
|
|
|
)
|
2026-03-13 09:38:18 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Exists 检查键是否存在
|
|
|
|
|
|
func (c *RedisCache) Exists(key string) bool {
|
|
|
|
|
|
key = normalizeKey(key)
|
|
|
|
|
|
n, err := c.client.Exists(c.ctx, key)
|
|
|
|
|
|
if err != nil {
|
2026-03-17 00:47:17 +08:00
|
|
|
|
zap.L().Error("Failed to check existence of key",
|
|
|
|
|
|
zap.String("key", key),
|
|
|
|
|
|
zap.Error(err),
|
|
|
|
|
|
)
|
2026-03-13 09:38:18 +08:00
|
|
|
|
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 {
|
2026-03-17 00:47:17 +08:00
|
|
|
|
zap.L().Error("Failed to increment key",
|
|
|
|
|
|
zap.String("key", key),
|
|
|
|
|
|
zap.Error(err),
|
|
|
|
|
|
)
|
2026-03-13 09:38:18 +08:00
|
|
|
|
return 0
|
|
|
|
|
|
}
|
|
|
|
|
|
return result
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ==================== RedisCache Hash 操作 ====================
|
|
|
|
|
|
|
|
|
|
|
|
// HSet 设置 Hash 字段
|
2026-03-30 04:49:35 +08:00
|
|
|
|
func (c *RedisCache) HSet(ctx context.Context, key string, field string, value any) error {
|
2026-03-13 09:38:18 +08:00
|
|
|
|
key = normalizeKey(key)
|
|
|
|
|
|
return c.client.HSet(ctx, key, field, value)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// HMSet 批量设置 Hash 字段
|
2026-03-30 04:49:35 +08:00
|
|
|
|
func (c *RedisCache) HMSet(ctx context.Context, key string, values map[string]any) error {
|
2026-03-13 09:38:18 +08:00
|
|
|
|
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 字段值
|
2026-03-30 04:49:35 +08:00
|
|
|
|
func (c *RedisCache) HMGet(ctx context.Context, key string, fields ...string) ([]any, error) {
|
2026-03-13 09:38:18 +08:00
|
|
|
|
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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-13 09:38:18 +08:00
|
|
|
|
// ==================== 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)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-24 05:18:30 +08:00
|
|
|
|
// 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...)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-13 09:38:18 +08:00
|
|
|
|
// ZRem 删除 Sorted Set 成员
|
2026-03-30 04:49:35 +08:00
|
|
|
|
func (c *RedisCache) ZRem(ctx context.Context, key string, members ...any) error {
|
2026-03-13 09:38:18 +08:00
|
|
|
|
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()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-13 09:38:18 +08:00
|
|
|
|
// 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
|
|
|
|
|
|
}
|