DataPool 技术设计文档 —— 线程安全的数据中枢
# DataPool 技术设计文档
## 1. 概述
一句话描述:DataPool 是一个线程安全的单例数据中枢,通过 Key-Value 存储和事件订阅机制解耦数据生产者与消费者。
## 2. 设计动机
- 为什么需要 DataPool?(模块解耦、数据共享、事件驱动)
- 解决了什么问题?(多设备数据汇聚、UI 实时更新、日志过滤)
## 3. 核心特性
- ✅ 线程安全(ConcurrentDictionary)
- ✅ 事件驱动(订阅/取消订阅)
- ✅ 智能日志过滤(单次变化阈值 + 累计变化阈值)
- ✅ 数据质量标记(Quality:0/1/-1)
- ✅ 时间戳记录
- ✅ 回调异常隔离
- ✅ 取消订阅重试机制
## 4. 架构图
[数据生产者] → Write → [DataPool] → 订阅通知 → [数据消费者]
↓
[日志记录]
(阈值过滤后)
## 5. 核心 API
### 5.1 写入数据
void Write(string key, object value, int quality = 1)
### 5.2 读取数据
DataPoint Read(string key)
### 5.3 订阅变化
void Subscribe(string key, EventHandler<DataPoint> callback)
### 5.4 取消订阅
void Unsubscribe(string key, EventHandler<DataPoint> callback)
### 5.5 阈值配置
double ChangeThreshold { get; set; } // 默认 1
void SetThreshold(string key, double threshold)
## 6. 智能日志过滤机制
- **单次变化触发**:数值变化 > ChangeThreshold
- **累计变化触发**:从上次记录日志后,累计变化 > ChangeThreshold
- **首次写入**:始终记录
- **质量变化**:始终记录
- **类型变化**:始终记录
using System.Collections.Concurrent; namespace TestDataPool { /// <summary> /// 数据点实体,包含值、时间戳和质量标记 /// </summary> public class DataPoint { /// <summary>数据值</summary> public object Value { get; set; } /// <summary>数据产生的时间戳</summary> public DateTime Timestamp { get; set; } = DateTime.Now; /// <summary> /// 数据质量标记 /// <para>0 = 初始值</para> /// <para>1 = 好</para> /// <para>-1 = 坏</para> /// </summary> public int Quality { get; set; } = 0; } /// <summary> /// 全局数据池(单例模式) /// <para>线程安全的数据存储中枢,支持订阅/取消订阅数据变化事件</para> /// </summary> public class DataPool { private static readonly Lazy<DataPool> _instance = new(() => new DataPool()); /// <summary>获取 DataPool 单例实例</summary> public static DataPool Instance => _instance.Value; private readonly ConcurrentDictionary<string, DataPoint> _store = new(); private readonly ConcurrentDictionary<string, EventHandler<DataPoint>> _subscriptions = new(); // ========================================== // 变化阈值配置 // ========================================== /// <summary> /// 全局变化阈值(默认 1) /// <para>当单次变化或累计变化超过此阈值时记录日志</para> /// </summary> public double ChangeThreshold { get; set; } = 1; private readonly ConcurrentDictionary<string, double> _customThresholds = new(); // 存储每个 Key 的累计变化量(带符号) private readonly ConcurrentDictionary<string, double> _cumulativeDelta = new(); private readonly LogHelper _log = LogHelper.GetLogger("DataPool"); private DataPool() { _log.Info("DataPool 单例初始化完成"); } // ========================================== // 阈值管理 // ========================================== /// <summary> /// 为指定 Key 单独设置变化阈值(优先级高于全局阈值) /// </summary> /// <param name="key">数据键名</param> /// <param name="threshold">变化阈值</param> public void SetThreshold(string key, double threshold) => _customThresholds.AddOrUpdate(key, threshold, (k, old) => threshold); /// <summary> /// 移除指定 Key 的自定义阈值(恢复使用全局阈值) /// </summary> /// <param name="key">数据键名</param> public void RemoveThreshold(string key) => _customThresholds.TryRemove(key, out _); /// <summary> /// 获取指定 Key 的有效阈值(优先返回自定义阈值,否则返回全局阈值) /// </summary> private double GetThreshold(string key) => _customThresholds.TryGetValue(key, out var t) ? t : ChangeThreshold; // ========================================== // 写入数据 // ========================================== /// <summary> /// 写入数据到数据池 /// </summary> /// <param name="key">数据键名,格式建议 "{设备ID}.{属性名}"</param> /// <param name="value">数据值</param> /// <param name="quality">数据质量:1=好,0=初始,-1=坏</param> /// <remarks> /// <para>写入后会触发订阅此 Key 的所有回调</para> /// <para>单次变化或累计变化超过阈值时记录日志</para> /// </remarks> public void Write(string key, object value, int quality = 1) { try { var oldPoint = ReadInternal(key); bool shouldLog = ShouldLogChange(key, oldPoint, value, quality); // ===== 新增:累计变化检测 ===== double delta = 0; if (oldPoint != null) { try { double oldVal = Convert.ToDouble(oldPoint.Value); double newVal = Convert.ToDouble(value); delta = newVal - oldVal; } catch { /* 忽略非数值类型 */ } } if (oldPoint != null && Math.Abs(delta) > 0.0000001) { _cumulativeDelta.AddOrUpdate(key, delta, (k, old) => old + delta); } _cumulativeDelta.TryGetValue(key, out double cumulative); double threshold = GetThreshold(key); bool cumulativeExceed = Math.Abs(cumulative) > threshold; if (cumulativeExceed) shouldLog = true; // ===== 新增结束 ===== var point = new DataPoint { Value = value, Timestamp = DateTime.Now, Quality = quality }; _store.AddOrUpdate(key, point, (k, old) => point); // 触发订阅回调,每个订阅者独立 try-catch if (_subscriptions.TryGetValue(key, out var handler) && handler != null) { foreach (EventHandler<DataPoint> singleHandler in handler.GetInvocationList()) { try { singleHandler?.Invoke(this, point); } catch (Exception ex) { _log.Error($"订阅回调异常 [{key}]: {ex.Message}"); } } } // 记录日志 if (shouldLog) { string oldVal = oldPoint?.Value?.ToString() ?? "null"; // 判断触发原因 if (cumulativeExceed && oldPoint != null) { _log.Info($"写入: {key} = {value} (累计变化: {cumulative:+0.000;-0.000;0}, 旧值: {oldVal})"); } else if (oldPoint != null && Math.Abs(delta) > threshold) { _log.Info($"写入: {key} = {value} (单次变化: {delta:+0.000;-0.000;0}, 旧值: {oldVal})"); } else { _log.Info($"写入: {key} = {value} (旧值: {oldVal})"); } // 记录后重置累计变化 _cumulativeDelta.AddOrUpdate(key, 0, (k, old) => 0); } } catch (Exception ex) { _log.Error($"写入失败 [{key}]: {ex.Message}"); } } /// <summary> /// 判断数据变化是否值得记录日志(仅单次变化判断,累计判断在 Write 中额外处理) /// </summary> private bool ShouldLogChange(string key, DataPoint oldPoint, object newValue, int newQuality) { // 首次写入 -> 记录 if (oldPoint == null) return true; // 质量变化 -> 记录 if (oldPoint.Quality != newQuality) return true; // 类型变化 -> 记录 if (oldPoint.Value?.GetType() != newValue?.GetType()) return true; try { double oldDouble = Convert.ToDouble(oldPoint.Value); double newDouble = Convert.ToDouble(newValue); double threshold = GetThreshold(key); return Math.Abs(newDouble - oldDouble) > threshold; } catch { return true; // 无法转换,保守记录 } } // ========================================== // 读取数据 // ========================================== /// <summary> /// 内部读取方法,不记录日志(用于 Write 中判断变化) /// </summary> private DataPoint ReadInternal(string key) { _store.TryGetValue(key, out var point); return point; } /// <summary> /// 读取数据点 /// </summary> /// <param name="key">数据键名</param> /// <returns> /// <para>如果 Key 存在,返回对应的 DataPoint</para> /// <para>如果 Key 不存在,返回 null</para> /// </returns> public DataPoint Read(string key) { try { _store.TryGetValue(key, out var point); if (point != null) { _log.Info($"读取数据: Key={key}, Value={point.Value}, Quality={point.Quality}"); } else { _log.Info($"读取数据: Key={key} 不存在,返回 null"); } return point; } catch (Exception ex) { _log.Error($"读取数据失败: Key={key}, 异常: {ex.Message}"); return null; } } // ========================================== // 订阅 / 取消订阅 // ========================================== /// <summary> /// 订阅指定 Key 的数据变化事件 /// </summary> /// <param name="key">要订阅的数据键名</param> /// <param name="callback">数据变化时的回调方法</param> /// <remarks> /// <para>当该 Key 被 Write 时,所有订阅者的回调会被依次执行</para> /// <para>如果 callback 为 null,会记录错误日志并忽略</para> /// </remarks> public void Subscribe(string key, EventHandler<DataPoint> callback) { if (callback == null) { _log.Error("订阅失败: callback 为 null"); return; } try { _subscriptions.AddOrUpdate(key, callback, (k, old) => old + callback); _log.Info($"订阅成功: {key}"); } catch (Exception ex) { _log.Error($"订阅失败 [{key}]: {ex.Message}"); } } /// <summary> /// 取消订阅指定 Key 的数据变化事件 /// </summary> /// <param name="key">要取消订阅的数据键名</param> /// <param name="callback">要移除的回调方法</param> /// <remarks> /// <para>必须传入与 Subscribe 时相同的委托实例才能成功移除</para> /// <para>如果 callback 为 null,会记录错误日志并忽略</para> /// <para>如果该 Key 没有其他订阅者,会自动从订阅字典中移除该 Key</para> /// <para>极端并发下会自动重试最多 3 次</para> /// </remarks> public void Unsubscribe(string key, EventHandler<DataPoint> callback) { if (callback == null) { _log.Error("取消订阅失败: callback 为 null"); return; } const int maxRetries = 3; int retryCount = 0; while (retryCount < maxRetries) { try { if (!_subscriptions.TryGetValue(key, out var existing)) { _log.Info($"取消订阅: {key} 未找到订阅,忽略"); return; } var newHandler = existing - callback; if (newHandler == null) { if (_subscriptions.TryRemove(key, out _)) { _log.Info($"取消订阅: {key},已移除所有回调"); return; } } else { if (_subscriptions.TryUpdate(key, newHandler, existing)) { _log.Info($"取消订阅: {key},移除指定回调"); return; } } retryCount++; if (retryCount < maxRetries) { _log.Info($"取消订阅并发冲突 [{key}],第 {retryCount} 次重试..."); Thread.Sleep(1); } } catch (Exception ex) { _log.Error($"取消订阅异常 [{key}]: {ex.Message}"); return; } } _log.Error($"取消订阅失败 [{key}],已达到最大重试次数 {maxRetries}"); } // ========================================== // 辅助方法 // ========================================== /// <summary> /// 获取数据池中所有已存储的 Key /// </summary> /// <returns>所有 Key 的集合</returns> public IEnumerable<string> GetAllKeys() => _store.Keys; } }
测试这个datapool的代码
class Program { static void Main(string[] args) { Console.WriteLine("======================================="); Console.WriteLine("开始 DataPool 独立单元测试"); Console.WriteLine("======================================="); RunAllTests(); Console.WriteLine("======================================="); Console.WriteLine("所有测试执行完毕,按任意键退出..."); Console.ReadKey(); } static void RunAllTests() { Test_BasicReadWrite(); Test_SubscriptionTrigger(); Test_Unsubscribe(); Test_ConcurrentStress(); } // ============================================ // 测试 1:基础读写 // ============================================ static void Test_BasicReadWrite() { var pool = DataPool.Instance; string key = "TEST.Basic"; pool.Write(key, 3.14159); var point = pool.Read(key); if (point != null && point.Value is double d && Math.Abs(d - 3.14159) < 0.0001) { Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine($"✅ 基础读写测试通过: 读取到 {d}"); Console.ResetColor(); } else { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine($"❌ 基础读写测试失败: 读取到 null 或值不正确"); Console.ResetColor(); } } // ============================================ // 测试 2:订阅通知(写数据时触发回调) // ============================================ static void Test_SubscriptionTrigger() { var pool = DataPool.Instance; string key = "TEST.Subscription"; var tcs = new TaskCompletionSource<bool>(); EventHandler<DataPoint> handler = (s, e) => { if (e.Value is int val && val == 999) { tcs.TrySetResult(true); } }; pool.Subscribe(key, handler); pool.Write(key, 999); bool triggered = tcs.Task.Wait(2000); if (triggered) { Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("✅ 订阅通知测试通过: 回调被成功触发"); Console.ResetColor(); } else { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("❌ 订阅通知测试失败: 回调未触发"); Console.ResetColor(); } pool.Unsubscribe(key, handler); } // ============================================ // 测试 3:取消订阅(取消后不再触发回调) // ============================================ static void Test_Unsubscribe() { var pool = DataPool.Instance; string key = "TEST.Unsubscribe"; bool wasCalled = false; EventHandler<DataPoint> handler = (s, e) => { wasCalled = true; Console.WriteLine($"回调被意外触发,值={e.Value}"); }; pool.Subscribe(key, handler); pool.Write(key, 1); Thread.Sleep(100); if (!wasCalled) { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("❌ 预检失败:订阅后回调未触发,测试无法继续"); Console.ResetColor(); pool.Unsubscribe(key, handler); return; } wasCalled = false; pool.Unsubscribe(key, handler); pool.Write(key, 2); Thread.Sleep(100); if (!wasCalled) { Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("✅ 取消订阅测试通过: 取消后回调未被触发"); Console.ResetColor(); } else { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("❌ 取消订阅测试失败: 取消后回调仍被触发(内存泄漏风险)"); Console.ResetColor(); } } // ============================================ // 测试 4:并发压力测试(多线程同时写同一个key) // ============================================ static void Test_ConcurrentSameKeyStress() { var pool = DataPool.Instance; string key = "TEST.Stress"; int writeCount = 5000; int completedWrites = 0; bool hasError = false; var tasks = new List<Task>(); for (int t = 0; t < 5; t++) { tasks.Add(Task.Run(() => { try { for (int i = 0; i < writeCount; i++) { pool.Write(key, i); Interlocked.Increment(ref completedWrites); } } catch (Exception ex) { hasError = true; Console.WriteLine($"并发线程抛出异常: {ex.Message}"); } })); } Task.WaitAll(tasks.ToArray()); if (!hasError && completedWrites == 5 * writeCount) { var final = pool.Read(key); Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine($"✅ 并发压力测试通过: 共写入 {completedWrites} 次,最终值={final?.Value}"); Console.ResetColor(); } else { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine($"❌ 并发压力测试失败: 写入次数={completedWrites}, 期望={5 * writeCount}, 有异常={hasError}"); Console.ResetColor(); } } /// <summary> /// 测试 5:并发压力测试(多线程同时写不同的key)这是真实的使用场景,验证 DataPool 在高并发下的稳定性和线程安全性。 /// </summary> static void Test_ConcurrentStress() { var pool = DataPool.Instance; int threadCount = 5; int writeCountPerThread = 5000; bool hasError = false; var tasks = new List<Task>(); for (int t = 0; t < threadCount; t++) { // 每个线程使用独立的 Key string key = $"TEST.Stress_Thread_{t}"; tasks.Add(Task.Run(() => { try { for (int i = 0; i < writeCountPerThread; i++) { pool.Write(key, i); } } catch (Exception ex) { hasError = true; Console.WriteLine($"线程 {key} 抛出异常: {ex.Message}"); } })); } Task.WaitAll(tasks.ToArray()); if (!hasError) { Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine($"✅ 并发压力测试通过: {threadCount} 个线程各写入 {writeCountPerThread} 次,无异常"); // 可选:验证每个 Key 的值是否正确(最后一个写入的值应为 writeCountPerThread - 1) for (int t = 0; t < threadCount; t++) { string key = $"TEST.Stress_Thread_{t}"; var point = pool.Read(key); if (point != null) { Console.WriteLine($" Key={key}, 最终值={point.Value}, 质量={point.Quality}"); } else { Console.WriteLine($" Key={key} 不存在!"); } } Console.ResetColor(); } else { Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine($"❌ 并发压力测试失败: 有线程抛出了异常"); Console.ResetColor(); } } }

浙公网安备 33010602011771号