| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107 |
- using Application.Abstractions.Data;
- using Application.Abstractions.Stocks;
- using Microsoft.EntityFrameworkCore;
- using StackExchange.Redis;
- namespace Infrastructure.Stocks;
- /// <summary>
- /// Redis Sorted Set 기반 종목 트렌딩 캐시 (d2 §④·§⑥) — RedisChatLeaderboard/PaperLeaderboardCache 패턴 복제.
- /// 점수 = 최근 24h PostStockTag 언급량 × RecentWeight + StockBoardStats.Posts(총 글 수, tie-breaker).
- /// TTL 10분 — 만료 시 다음 호출이 DB 집계 1회 후 재시드. StockMentionAggregator 배치가 없을 때도 lazy 로 동작.
- /// </summary>
- internal sealed class RedisStockBoardTrendingCache(IConnectionMultiplexer redis) : IStockBoardTrendingCache
- {
- private static readonly TimeSpan CacheTtl = TimeSpan.FromMinutes(10);
- // 최근성 가중 — 24h 언급 1건이 총 글 10건과 동일 가중.
- private const int RecentWeight = 10;
- private const string Key = "stock-trending";
- public async Task<IReadOnlyList<StockTrendingEntry>> GetTopAsync(int count, IAppDbContext db, CancellationToken ct = default)
- {
- if (count <= 0)
- {
- return [];
- }
- var r = redis.GetDatabase();
- await EnsureSeededAsync(r, db, ct);
- var entries = await r.SortedSetRangeByRankWithScoresAsync(Key, 0, count - 1, Order.Descending);
- var rows = new List<StockTrendingEntry>(entries.Length);
- foreach (var entry in entries)
- {
- var code = (string?)entry.Element;
- if (!string.IsNullOrEmpty(code))
- {
- rows.Add(new StockTrendingEntry(code, (int)entry.Score));
- }
- }
- return rows;
- }
- public async Task InvalidateAsync(CancellationToken ct = default)
- {
- var r = redis.GetDatabase();
- await r.KeyDeleteAsync(Key);
- }
- /// <summary>캐시 미스 → DB 집계 후 ZADD 재시드. TTL 동안 후속 호출은 Redis only.</summary>
- private static async Task EnsureSeededAsync(IDatabase r, IAppDbContext db, CancellationToken ct)
- {
- if (await r.KeyExistsAsync(Key))
- {
- return;
- }
- var since = DateTime.UtcNow.AddHours(-24);
- // 최근 24h 언급량 (코드별 count)
- var recentMentions = await db.PostStockTag.AsNoTracking()
- .Where(c => c.CreatedAt >= since)
- .GroupBy(c => c.StockCode)
- .Select(g => new { Code = g.Key, Count = g.Count() })
- .ToListAsync(ct);
- // 총 글 수 (StockBoardStats 파티션)
- var totals = await db.StockBoardStats.AsNoTracking()
- .Select(c => new { c.StockCode, c.Posts })
- .ToListAsync(ct);
- var scores = new Dictionary<string, int>(StringComparer.Ordinal);
- foreach (var t in totals)
- {
- scores[t.StockCode] = t.Posts;
- }
- foreach (var m in recentMentions)
- {
- scores.TryGetValue(m.Code, out var baseScore);
- scores[m.Code] = baseScore + (m.Count * RecentWeight);
- }
- if (scores.Count == 0)
- {
- return;
- }
- var entries = scores
- .Where(kv => kv.Value > 0)
- .Select(kv => new SortedSetEntry(kv.Key, kv.Value))
- .ToArray();
- if (entries.Length == 0)
- {
- return;
- }
- await r.SortedSetAddAsync(Key, entries);
- await r.KeyExpireAsync(Key, CacheTtl);
- }
- }
|