package redis import ( "context" "errors" "fmt" "strconv" "strings" "time" "git.imall.cloud/openim/open-im-server-deploy/pkg/common/storage/cache/cachekey" "git.imall.cloud/openim/protocol/constant" "github.com/openimsdk/tools/errs" "github.com/redis/go-redis/v9" ) const onlineUserCountHistorySeparator = ":" // OnlineUserCountSample 在线人数历史采样点 type OnlineUserCountSample struct { // Timestamp 采样时间(毫秒时间戳) Timestamp int64 // Count 采样在线人数 Count int64 } // GetOnlineUserCount 读取在线人数缓存 func GetOnlineUserCount(ctx context.Context, rdb redis.UniversalClient) (int64, error) { if rdb == nil { return 0, errs.ErrInternalServer.WrapMsg("redis client is nil") } val, err := rdb.Get(ctx, cachekey.OnlineUserCountKey).Result() if err != nil { if errors.Is(err, redis.Nil) { return 0, err } return 0, errs.Wrap(err) } count, err := strconv.ParseInt(val, 10, 64) if err != nil { return 0, errs.WrapMsg(err, "parse online user count failed") } return count, nil } // RefreshOnlineUserCount 刷新在线人数缓存 func RefreshOnlineUserCount(ctx context.Context, rdb redis.UniversalClient) (int64, error) { if rdb == nil { return 0, errs.ErrInternalServer.WrapMsg("redis client is nil") } var ( cursor uint64 total int64 ) now := strconv.FormatInt(time.Now().Unix(), 10) for { keys, nextCursor, err := rdb.Scan(ctx, cursor, fmt.Sprintf("%s*", cachekey.OnlineKey), constant.ParamMaxLength).Result() if err != nil { return 0, errs.Wrap(err) } for _, key := range keys { count, err := rdb.ZCount(ctx, key, now, "+inf").Result() if err != nil { return 0, errs.Wrap(err) } if count > 0 { total++ } } cursor = nextCursor if cursor == 0 { break } } if err := rdb.Set(ctx, cachekey.OnlineUserCountKey, total, 0).Err(); err != nil { return 0, errs.Wrap(err) } return total, nil } // AppendOnlineUserCountHistory 写入在线人数历史采样 func AppendOnlineUserCountHistory(ctx context.Context, rdb redis.UniversalClient, timestamp int64, count int64) error { if rdb == nil { return errs.ErrInternalServer.WrapMsg("redis client is nil") } if timestamp <= 0 { return errs.ErrArgs.WrapMsg("invalid timestamp") } member := fmt.Sprintf("%d%s%d", timestamp, onlineUserCountHistorySeparator, count) if err := rdb.ZAdd(ctx, cachekey.OnlineUserCountHistoryKey, redis.Z{ Score: float64(timestamp), Member: member, }).Err(); err != nil { return errs.Wrap(err) } // 清理历史数据,避免无界增长 retentionMs := int64(cachekey.OnlineUserCountHistoryRetention / time.Millisecond) cutoff := timestamp - retentionMs if cutoff > 0 { if err := rdb.ZRemRangeByScore(ctx, cachekey.OnlineUserCountHistoryKey, "0", strconv.FormatInt(cutoff, 10)).Err(); err != nil { return errs.Wrap(err) } } return nil } // GetOnlineUserCountHistory 读取在线人数历史采样 func GetOnlineUserCountHistory(ctx context.Context, rdb redis.UniversalClient, startTime int64, endTime int64) ([]OnlineUserCountSample, error) { if rdb == nil { return nil, errs.ErrInternalServer.WrapMsg("redis client is nil") } if startTime <= 0 || endTime <= 0 || endTime <= startTime { return nil, nil } // 包含endTime的数据,使用endTime作为最大值 values, err := rdb.ZRangeByScore(ctx, cachekey.OnlineUserCountHistoryKey, &redis.ZRangeBy{ Min: strconv.FormatInt(startTime, 10), Max: strconv.FormatInt(endTime, 10), }).Result() if err != nil { if errors.Is(err, redis.Nil) { return nil, nil } return nil, errs.Wrap(err) } if len(values) == 0 { return nil, nil } samples := make([]OnlineUserCountSample, 0, len(values)) for _, val := range values { parts := strings.SplitN(val, onlineUserCountHistorySeparator, 2) if len(parts) != 2 { continue } ts, err := strconv.ParseInt(parts[0], 10, 64) if err != nil { continue } cnt, err := strconv.ParseInt(parts[1], 10, 64) if err != nil { continue } samples = append(samples, OnlineUserCountSample{ Timestamp: ts, Count: cnt, }) } return samples, nil }