using YLErp.Modules; namespace YLErp.Modules.DataProviderModule { /// /// FR007 定盘快照缓存。定盘是"每工作日一个数、发布后不变"的小表(一年约 250 行,全历史数千行), /// 故采用整表快照而非逐键缓存: /// /// 预载:首次访问一次性 SELECT 全历史进字典,此后读取 O(1)、零查询; /// miss 不进快照(负缓存防线):当日定盘 ~11:15 后才出现、历史可补录—— /// 字典查不到必须回退直查库,绝不能把"查不到"缓存住,否则发布后仍沿用旧利率; /// 写侧版本失效:经应用的 FR007 写入(SwapFlowService 新增/修改/删除)立即失效, /// 下次访问整表重载一次; /// TTL 1 分钟兜底:覆盖直改库与其他通用价格写入口(导入/复制/合成等), /// 可见性窗口 ≤1 分钟,重启无需。 /// /// 快照以引用替换发布(Volatile.Write),读侧要么看到旧快照要么看到新快照,无撕裂; /// 重载加锁去抖,避免并发下重复整表查询。 /// public static class Fr007FixingCache { public const string UnderlyingCode = "FR007"; /// TTL:兜底"直接改库/通用写入口"的可见性窗口(FR007 专属写入口走版本失效,不受此限)。 internal static TimeSpan Ttl = TimeSpan.FromMinutes(1); /// 测试接缝:时钟(默认 UtcNow Ticks)。 internal static Func NowTicks = () => DateTime.UtcNow.Ticks; /// 测试接缝:快照装载器(默认直查库)。返回 日期→ReferencePrice 全量映射。 internal static Func> LoadSnapshot = LoadFromDb; private static Dictionary _snapshot = new(); private static bool _loaded; // 是否已完成首次装载 private static long _loadedAt; // 快照装载时刻(NowTicks 口径) private static long _writeVersion; // 写侧版本(Invalidate 递增) private static long _snapVersion; // 装载时的写侧版本 private static readonly object _reloadLock = new(); private static Dictionary LoadFromDb() { using var db = DbContextFactory.GetYLDbContext(); return db.eod_commodity_future_price .Where(x => x.UnderlyingCode == UnderlyingCode && x.ReferencePrice != null) .Select(x => new { x.ValueDate, Price = x.ReferencePrice!.Value }) .AsEnumerable() .GroupBy(x => x.ValueDate.Date) .ToDictionary(g => g.Key, g => g.First().Price); } /// 命中返回 true。miss 仅代表"快照里没有",调用方须直查库兜底(当日新发布/补录)。 public static bool TryGet(DateTime valueDate, out double price) { EnsureFresh(); return Volatile.Read(ref _snapshot).TryGetValue(valueDate.Date, out price); } /// 写侧失效:FR007 行经应用新增/修改/删除后调用,下次访问整表重载。 public static void Invalidate() => Interlocked.Increment(ref _writeVersion); /// 重置为未装载状态并恢复默认接缝(仅测试用)。 internal static void ResetForTest() { lock (_reloadLock) { _snapshot = new Dictionary(); _loaded = false; _loadedAt = 0; _writeVersion = 0; _snapVersion = 0; Ttl = TimeSpan.FromMinutes(1); NowTicks = () => DateTime.UtcNow.Ticks; LoadSnapshot = LoadFromDb; } } private static void EnsureFresh() { if (IsFresh()) return; lock (_reloadLock) { if (IsFresh()) return; try { var snap = LoadSnapshot(); Volatile.Write(ref _snapshot, snap); Volatile.Write(ref _loaded, true); Volatile.Write(ref _loadedAt, NowTicks()); Volatile.Write(ref _snapVersion, Volatile.Read(ref _writeVersion)); } catch (Exception ex) { // 重载失败(如DB抖动):保留旧快照并重置TTL时钟——历史定盘不可变,旧值依旧正确; // 同时防止每次读取都重试全表SELECT(重试风暴)。不抛:命中继续走旧快照, // miss照旧回退直查库,新数据取不到仍会在直查处响亮报错。 // 注:版本失效(Invalidate)后的重载失败不会被TTL掩盖——版本不相等会持续重试直到成功。 Volatile.Write(ref _loaded, true); Volatile.Write(ref _loadedAt, NowTicks()); LogFactory.GetLogger("Fr007FixingCache").Error("FR007定盘缓存重载失败,沿用旧快照", ex); } } } private static bool IsFresh() => Volatile.Read(ref _loaded) && Volatile.Read(ref _snapVersion) == Volatile.Read(ref _writeVersion) && NowTicks() - Volatile.Read(ref _loadedAt) <= Ttl.Ticks; } }