403 lines
9.4 KiB
Markdown
403 lines
9.4 KiB
Markdown
# 采集数据历史快照化改造方案
|
||
|
||
## 背景
|
||
|
||
当前系统的 `collected_data` 更接近“当前结果表”:
|
||
|
||
- 同一个 `source + source_id` 会被更新覆盖
|
||
- 前端列表页默认读取这张表
|
||
- `collection_tasks` 只记录任务执行状态,不直接承载数据版本语义
|
||
|
||
这套方式适合管理后台,但不利于后续做态势感知、时间回放、趋势分析和版本对比。
|
||
如果后面需要回答下面这类问题,当前模型会比较吃力:
|
||
|
||
- 某条实体在过去 7 天如何变化
|
||
- 某次采集相比上次新增了什么、删除了什么、值变了什么
|
||
- 某个时刻地图上“当时的世界状态”是什么
|
||
- 告警是在第几次采集后触发的
|
||
|
||
因此建议把采集数据改造成“历史快照 + 当前视图”模型。
|
||
|
||
## 目标
|
||
|
||
1. 每次触发采集都保留一份独立快照,历史可追溯。
|
||
2. 管理后台默认仍然只看“当前最新状态”,不增加使用复杂度。
|
||
3. 后续支持:
|
||
- 时间线回放
|
||
- 两次采集差异对比
|
||
- 趋势分析
|
||
- 按快照回溯告警和地图状态
|
||
4. 尽量兼容现有接口,降低改造成本。
|
||
|
||
## 结论
|
||
|
||
不建议继续用以下两种单一模式:
|
||
|
||
- 直接覆盖旧数据
|
||
问题:没有历史,无法回溯。
|
||
|
||
- 软删除旧数据再全量新增
|
||
问题:语义不清,历史和“当前无效”混在一起,后续统计复杂。
|
||
|
||
推荐方案:
|
||
|
||
- 保留历史事实表
|
||
- 维护当前视图
|
||
- 每次采集对应一个明确的快照批次
|
||
|
||
## 推荐数据模型
|
||
|
||
### 方案概览
|
||
|
||
建议拆成三层:
|
||
|
||
1. `collection_tasks`
|
||
继续作为采集任务表,表示“这次采集任务”。
|
||
|
||
2. `data_snapshots`
|
||
新增快照表,表示“某个数据源在某次任务中产出的一个快照批次”。
|
||
|
||
3. `collected_data`
|
||
从“当前结果表”升级为“历史事实表”,每一行归属于一个快照。
|
||
|
||
同时再提供一个“当前视图”:
|
||
|
||
- SQL View / 物化视图 / API 查询层封装均可
|
||
- 语义是“每个 `source + source_id` 的最新有效记录”
|
||
|
||
### 新增表:`data_snapshots`
|
||
|
||
建议字段:
|
||
|
||
| 字段 | 类型 | 含义 |
|
||
|---|---|---|
|
||
| `id` | bigint PK | 快照主键 |
|
||
| `datasource_id` | int | 对应数据源 |
|
||
| `task_id` | int | 对应采集任务 |
|
||
| `source` | varchar(100) | 数据源名,如 `top500` |
|
||
| `snapshot_key` | varchar(100) | 可选,业务快照标识 |
|
||
| `reference_date` | timestamptz nullable | 这批数据的参考时间 |
|
||
| `started_at` | timestamptz | 快照开始时间 |
|
||
| `completed_at` | timestamptz | 快照完成时间 |
|
||
| `record_count` | int | 快照总记录数 |
|
||
| `status` | varchar(20) | `running/success/failed/partial` |
|
||
| `is_current` | bool | 当前是否是该数据源最新快照 |
|
||
| `parent_snapshot_id` | bigint nullable | 上一版快照,可用于 diff |
|
||
| `summary` | jsonb | 本次快照统计摘要 |
|
||
|
||
说明:
|
||
|
||
- `collection_tasks` 偏“执行过程”
|
||
- `data_snapshots` 偏“数据版本”
|
||
- 一个任务通常对应一个快照,但保留分层更清晰
|
||
|
||
### 升级表:`collected_data`
|
||
|
||
建议新增字段:
|
||
|
||
| 字段 | 类型 | 含义 |
|
||
|---|---|---|
|
||
| `snapshot_id` | bigint not null | 归属快照 |
|
||
| `task_id` | int nullable | 归属任务,便于追查 |
|
||
| `entity_key` | varchar(255) | 实体稳定键,通常可由 `source + source_id` 派生 |
|
||
| `is_current` | bool | 当前是否为该实体最新记录 |
|
||
| `previous_record_id` | bigint nullable | 上一个版本的记录 |
|
||
| `change_type` | varchar(20) | `created/updated/unchanged/deleted` |
|
||
| `change_summary` | jsonb | 字段变化摘要 |
|
||
| `deleted_at` | timestamptz nullable | 对应“本次快照中消失”的实体 |
|
||
|
||
保留现有字段:
|
||
|
||
- `source`
|
||
- `source_id`
|
||
- `data_type`
|
||
- `name`
|
||
- `title`
|
||
- `description`
|
||
- `country`
|
||
- `city`
|
||
- `latitude`
|
||
- `longitude`
|
||
- `value`
|
||
- `unit`
|
||
- `metadata`
|
||
- `collected_at`
|
||
- `reference_date`
|
||
- `is_valid`
|
||
|
||
### 当前视图
|
||
|
||
建议新增一个只读视图:
|
||
|
||
`current_collected_data`
|
||
|
||
语义:
|
||
|
||
- 对每个 `source + source_id` 只保留最新一条 `is_current = true` 且 `deleted_at is null` 的记录
|
||
|
||
这样:
|
||
|
||
- 管理后台继续像现在一样查“当前数据”
|
||
- 历史分析查 `collected_data`
|
||
|
||
## 写入策略
|
||
|
||
### 触发按钮语义
|
||
|
||
“触发”不再理解为“覆盖旧表”,而是:
|
||
|
||
- 启动一次新的采集任务
|
||
- 生成一个新的快照
|
||
- 将本次结果写入历史事实表
|
||
- 再更新当前视图标记
|
||
|
||
### 写入流程
|
||
|
||
1. 创建 `collection_tasks` 记录,状态 `running`
|
||
2. 创建 `data_snapshots` 记录,状态 `running`
|
||
3. 采集器拉取原始数据并标准化
|
||
4. 为每条记录生成 `entity_key`
|
||
- 推荐:`{source}:{source_id}`
|
||
5. 将本次记录批量写入 `collected_data`
|
||
6. 与上一个快照做比对,计算:
|
||
- 新增
|
||
- 更新
|
||
- 未变
|
||
- 删除
|
||
7. 更新本批记录的:
|
||
- `change_type`
|
||
- `previous_record_id`
|
||
- `is_current`
|
||
8. 将上一批同实体记录的 `is_current` 置为 `false`
|
||
9. 将本次快照未出现但上一版存在的实体标记为 `deleted`
|
||
10. 更新 `data_snapshots.status = success`
|
||
11. 更新 `collection_tasks.status = success`
|
||
|
||
### 删除语义
|
||
|
||
这里不建议真的删记录。
|
||
建议采用“逻辑消失”模型:
|
||
|
||
- 历史行永远保留
|
||
- 如果某实体在新快照里消失:
|
||
- 上一条历史记录补一条“删除状态记录”或标记 `change_type = deleted`
|
||
- 同时该实体不再出现在当前视图
|
||
|
||
这样最适合态势感知。
|
||
|
||
## API 改造建议
|
||
|
||
### 保持现有接口默认行为
|
||
|
||
现有接口:
|
||
|
||
- `GET /api/v1/collected`
|
||
- `GET /api/v1/collected/{id}`
|
||
- `GET /api/v1/collected/summary`
|
||
|
||
建议默认仍返回“当前视图”,避免前端全面重写。
|
||
|
||
### 新增历史查询能力
|
||
|
||
建议新增参数或新接口:
|
||
|
||
#### 1. 当前/历史切换
|
||
|
||
`GET /api/v1/collected?mode=current|history`
|
||
|
||
- `current`:默认,查当前视图
|
||
- `history`:查历史事实表
|
||
|
||
#### 2. 按快照查询
|
||
|
||
`GET /api/v1/collected?snapshot_id=123`
|
||
|
||
#### 3. 快照列表
|
||
|
||
`GET /api/v1/snapshots`
|
||
|
||
支持筛选:
|
||
|
||
- `datasource_id`
|
||
- `source`
|
||
- `status`
|
||
- `date_from/date_to`
|
||
|
||
#### 4. 快照详情
|
||
|
||
`GET /api/v1/snapshots/{id}`
|
||
|
||
返回:
|
||
|
||
- 快照基础信息
|
||
- 统计摘要
|
||
- 与上一版的 diff 摘要
|
||
|
||
#### 5. 快照 diff
|
||
|
||
`GET /api/v1/snapshots/{id}/diff?base_snapshot_id=122`
|
||
|
||
返回:
|
||
|
||
- `created`
|
||
- `updated`
|
||
- `deleted`
|
||
- `unchanged`
|
||
|
||
## 前端改造建议
|
||
|
||
### 1. 数据列表页
|
||
|
||
默认仍看当前数据,不改用户使用习惯。
|
||
|
||
建议新增:
|
||
|
||
- “视图模式”
|
||
- 当前数据
|
||
- 历史数据
|
||
- “快照时间”筛选
|
||
- “只看变化项”筛选
|
||
|
||
### 2. 数据详情页
|
||
|
||
详情页建议展示:
|
||
|
||
- 当前记录基础信息
|
||
- 元数据动态字段
|
||
- 所属快照
|
||
- 上一版本对比入口
|
||
- 历史版本时间线
|
||
|
||
### 3. 数据源管理页
|
||
|
||
“触发”按钮文案建议改成更准确的:
|
||
|
||
- `立即采集`
|
||
|
||
并在详情里补:
|
||
|
||
- 最近一次快照时间
|
||
- 最近一次快照记录数
|
||
- 最近一次变化数
|
||
|
||
## 迁移方案
|
||
|
||
### Phase 1:兼容式落地
|
||
|
||
目标:先保留当前页面可用。
|
||
|
||
改动:
|
||
|
||
1. 新增 `data_snapshots`
|
||
2. 给 `collected_data` 增加:
|
||
- `snapshot_id`
|
||
- `task_id`
|
||
- `entity_key`
|
||
- `is_current`
|
||
- `previous_record_id`
|
||
- `change_type`
|
||
- `change_summary`
|
||
- `deleted_at`
|
||
3. 现有数据全部补成一个“初始化快照”
|
||
4. 现有 `/collected` 默认改查当前视图
|
||
|
||
优点:
|
||
|
||
- 前端几乎无感
|
||
- 风险最小
|
||
|
||
### Phase 2:启用差异计算
|
||
|
||
目标:采集后可知道本次改了什么。
|
||
|
||
改动:
|
||
|
||
1. 写入时做新旧快照比对
|
||
2. 写 `change_type`
|
||
3. 生成快照摘要
|
||
|
||
### Phase 3:前端态势感知能力
|
||
|
||
目标:支持历史回放和趋势分析。
|
||
|
||
改动:
|
||
|
||
1. 快照时间线
|
||
2. 版本 diff 页面
|
||
3. 地图时间回放
|
||
4. 告警和快照关联
|
||
|
||
## 唯一性与索引建议
|
||
|
||
### 建议保留的业务唯一性
|
||
|
||
在“同一个快照内部”,建议唯一:
|
||
|
||
- `(snapshot_id, source, source_id)`
|
||
|
||
不要在整张历史表上强加:
|
||
|
||
- `(source, source_id)` 唯一
|
||
|
||
因为历史表本来就应该允许同一实体跨快照存在多条版本。
|
||
|
||
### 建议索引
|
||
|
||
- `idx_collected_data_snapshot_id`
|
||
- `idx_collected_data_source_source_id`
|
||
- `idx_collected_data_entity_key`
|
||
- `idx_collected_data_is_current`
|
||
- `idx_collected_data_reference_date`
|
||
- `idx_snapshots_source_completed_at`
|
||
|
||
## 风险点
|
||
|
||
1. 存储量会明显增加
|
||
- 需要评估保留周期
|
||
- 可以考虑冷热分层
|
||
|
||
2. 写入复杂度上升
|
||
- 需要批量 upsert / diff 逻辑
|
||
|
||
3. 当前接口语义会从“表”变成“视图”
|
||
- 文档必须同步
|
||
|
||
4. 某些采集器缺稳定 `source_id`
|
||
- 需要补齐实体稳定键策略
|
||
|
||
## 对当前项目的具体建议
|
||
|
||
结合当前代码,推荐这样落地:
|
||
|
||
### 短期
|
||
|
||
1. 先设计并落表:
|
||
- `data_snapshots`
|
||
- `collected_data` 新字段
|
||
2. 采集完成后每次新增快照
|
||
3. `/api/v1/collected` 默认查 `is_current = true`
|
||
|
||
### 中期
|
||
|
||
1. 在 `BaseCollector._save_data()` 中改成:
|
||
- 生成快照
|
||
- 批量写历史
|
||
- 标记当前
|
||
2. 将 `CollectionTask.id` 关联到 `snapshot.task_id`
|
||
|
||
### 长期
|
||
|
||
1. 地图接口支持按 `snapshot_id` 查询
|
||
2. 仪表盘支持“最近一次快照变化量”
|
||
3. 告警支持绑定到快照版本
|
||
|
||
## 最终建议
|
||
|
||
最终建议采用:
|
||
|
||
- 历史事实表:保存每次采集结果
|
||
- 当前视图:服务管理后台默认查询
|
||
- 快照表:承载版本批次和 diff 语义
|
||
|
||
这样既能保留历史,又不会把当前页面全部推翻重做,是最适合后续做态势感知的一条路径。
|