- EodPriceQueryService.TryGetPrice: FR007 快照 miss 直查库命中后 Invalidate, 下次访问整表重载回 O(1),否则该键 TTL 窗口内每次读取都退化为点查 - 移除点查多余的 OrderByDescending(主键等值查询取首行即可) - Fr007FixingCache.EnsureFresh: 重载失败(DB抖动)保留旧快照不抛并重置TTL时钟 ——历史定盘不可变旧值依旧正确,同时防重试风暴;版本失效路径不受TTL掩盖 - 补两测试: 重载失败保快照窗口内不重试 / 首次装载失败miss不抛窗口内不重试
110 lines
5.5 KiB
C#
110 lines
5.5 KiB
C#
using YLErp.Modules;
|
|
|
|
namespace YLErp.Modules.DataProviderModule
|
|
{
|
|
/// <summary>
|
|
/// FR007 定盘快照缓存。定盘是"每工作日一个数、发布后不变"的小表(一年约 250 行,全历史数千行),
|
|
/// 故采用整表快照而非逐键缓存:
|
|
///
|
|
/// <para>① <b>预载</b>:首次访问一次性 SELECT 全历史进字典,此后读取 O(1)、零查询;</para>
|
|
/// <para>② <b>miss 不进快照</b>(负缓存防线):当日定盘 ~11:15 后才出现、历史可补录——
|
|
/// 字典查不到必须回退直查库,绝不能把"查不到"缓存住,否则发布后仍沿用旧利率;</para>
|
|
/// <para>③ <b>写侧版本失效</b>:经应用的 FR007 写入(SwapFlowService 新增/修改/删除)立即失效,
|
|
/// 下次访问整表重载一次;</para>
|
|
/// <para>④ <b>TTL 1 分钟兜底</b>:覆盖直改库与其他通用价格写入口(导入/复制/合成等),
|
|
/// 可见性窗口 ≤1 分钟,重启无需。</para>
|
|
///
|
|
/// <para>快照以引用替换发布(Volatile.Write),读侧要么看到旧快照要么看到新快照,无撕裂;
|
|
/// 重载加锁去抖,避免并发下重复整表查询。</para>
|
|
/// </summary>
|
|
public static class Fr007FixingCache
|
|
{
|
|
public const string UnderlyingCode = "FR007";
|
|
|
|
/// <summary>TTL:兜底"直接改库/通用写入口"的可见性窗口(FR007 专属写入口走版本失效,不受此限)。</summary>
|
|
internal static TimeSpan Ttl = TimeSpan.FromMinutes(1);
|
|
|
|
/// <summary>测试接缝:时钟(默认 UtcNow Ticks)。</summary>
|
|
internal static Func<long> NowTicks = () => DateTime.UtcNow.Ticks;
|
|
|
|
/// <summary>测试接缝:快照装载器(默认直查库)。返回 日期→ReferencePrice 全量映射。</summary>
|
|
internal static Func<Dictionary<DateTime, double>> LoadSnapshot = LoadFromDb;
|
|
|
|
private static Dictionary<DateTime, double> _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<DateTime, double> 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);
|
|
}
|
|
|
|
/// <summary>命中返回 true。miss 仅代表"快照里没有",调用方须直查库兜底(当日新发布/补录)。</summary>
|
|
public static bool TryGet(DateTime valueDate, out double price)
|
|
{
|
|
EnsureFresh();
|
|
return Volatile.Read(ref _snapshot).TryGetValue(valueDate.Date, out price);
|
|
}
|
|
|
|
/// <summary>写侧失效:FR007 行经应用新增/修改/删除后调用,下次访问整表重载。</summary>
|
|
public static void Invalidate() => Interlocked.Increment(ref _writeVersion);
|
|
|
|
/// <summary>重置为未装载状态并恢复默认接缝(仅测试用)。</summary>
|
|
internal static void ResetForTest()
|
|
{
|
|
lock (_reloadLock)
|
|
{
|
|
_snapshot = new Dictionary<DateTime, double>();
|
|
_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;
|
|
}
|
|
}
|