Files
lingniu-vehicle-ingest/docs/architecture/storage-minimal-contract.md
2026-07-03 15:52:53 +08:00

107 lines
8.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 车辆数据最小落库合约
## 目标
车辆接入系统只持久化能回答业务问题、能回放纠错、能支撑高频查询的数据。协议细节默认保存在 raw不向上层业务表扩散。
## 第一性原则
1. raw 是证据层,必须能证明收到过什么、解析成什么。
2. Kafka 是回放层,消费者失败后优先靠 Kafka lag 追平。
3. Redis 是当前态缓存,不是历史库。
4. TDengine 只承担高写入时间序列raw、位置历史。
5. MySQL 只承担低基数业务状态:身份映射、实时快照、日指标。
6. 同一个事实只落一个主表,其他表只存查询必要的投影。
7. 新字段先进入 `parsed_json`,只有稳定查询需求出现后才提升为列。
## 目标表边界
| 存储 | 表/Key | 保留内容 | 不保留内容 |
| --- | --- | --- | --- |
| TDengine | `raw_frames` | raw hex/text、完整 `parsed_json`、解析状态、协议标签、车辆标识标签 | `fields_json` 这类可从 `parsed_json`/业务表再得到的重复字段 |
| TDengine | `raw_frame_payload_chunks` | 超长 raw/parsed payload 分片 | 业务查询字段 |
| TDengine | `vehicle_locations` | 时间、VIN/协议标签、经纬度、速度、方向、SOC、总里程等位置核心字段 | 完整协议 JSON、注册鉴权信息、phone/device_id/vehicle_key 兜底标识 |
| MySQL | `vehicle_realtime_snapshot` | 每个协议+VIN 的最新事件时间、接收时间、车牌、事件 ID | phone、device、source endpoint、完整 JSON |
| MySQL | `vehicle_realtime_location` | 每个协议+VIN 的最新位置核心字段 | raw、parsed JSON、消息头内部字段 |
| MySQL | `vehicle_daily_mileage` | 日期、VIN、协议、日里程、首末总里程、样本数 | 临时 vehicle key、泛化 metric key/value、自增 id、created_at、每帧细节、位置点列表 |
| MySQL | `vehicle_identity_binding` | 人工维护或导入的 VIN、车牌、phone、oem 映射 | 注册历史、协议状态、device_id、自增 id、created_at |
| MySQL | `jt808_registration` | JT808 phone 主键下的注册、鉴权、VIN 匹配状态、来源端点 | GB32960/MQTT 注册信息、位置历史、created_at |
| Redis | `vehicle:realtime-raw:{protocol}:{vin}` | 每协议每 VIN 最新完整 parsed 状态 | 无 VIN 临时身份、历史数据、统计结果 |
## 当前应收敛的重复点
1. `vehicle_mileage_points``vehicle_locations.total_mileage_km` 重复。
- 状态Go 写入链路已停止创建和写入 `vehicle_mileage_points`
- 查询:不再暴露单独 `/api/history/mileage-points`,里程点直接从 `/api/history/locations``total_mileage_km` 获取。
- 生产ECS TDengine 历史库已在上线前重建,`vehicle_mileage_points` 不再存在。
2. `raw_frames.fields_json``raw_frames.parsed_json``vehicle_locations` 重复。
- 状态:新建 raw schema 和 raw 写入已停止使用 `fields_json`
- 兼容raw 查询只选择公共列,不依赖 `fields_json`;旧表保留该列也不影响查询。
- 生产ECS TDengine 历史库已在上线前重建,`raw_frames` 不再包含 `fields_json`
3. `vehicle_daily_mileage` 不再接收临时 `vehicle_key`
- 原因:日统计是正式业务指标,应只统计已经定位到 VIN 的车辆。
- 无 VIN 的 JT808 等数据保留在 raw/Redis/session 中用于排查和待绑定,不进入正式车辆指标。
- 状态schema 使用 `PRIMARY KEY (vin, stat_date, protocol)`,不保留自增 `id``created_at`;首末总里程和样本数保留为日里程计算状态。
- 索引:主键已覆盖按 VIN 查询,不再额外维护 `idx_vin`;只保留按日期、协议日期查询需要的索引。
- 查询:`/api/stats/daily-metrics` 默认不执行 `COUNT(*)``total` 表示本页返回条数;只有报表总页数等场景传 `includeTotal=true`
- 生产RDS 已在上线前删除泛化 `vehicle_daily_metric`,改为专用 `vehicle_daily_mileage`
4. `vehicle_locations` 不再接收临时身份兜底字段。
- 原因:位置历史是正式车辆时间序列,只按 `protocol + vin` 分表和查询。
- 无 VIN 的位置帧仍保留在 `raw_frames.parsed_json`,待 identity/binding 修复后可从 raw 重放补写。
- 查询:`/api/history/locations` 默认不执行大表 `COUNT(*)``total` 表示本页返回条数;只有需要精确总数时传 `includeTotal=true`
- 生产ECS TDengine `vehicle_locations` 已在上线前重建为 `protocol``vin` 两个 tag。
5. 历史 raw 查询默认不做精确总数。
- 原因:`raw_frames` 是最高写入量证据层,分页/排查通常只需要最新一页数据。
- 查询:`/api/history/raw-frames` 默认 `total` 为本页返回条数;只有导出前预估、后台管理需要总页数时才传 `includeTotal=true`
6. MySQL realtime 当前态表不保留代理主键和创建时间。
- 原因:`vehicle_realtime_snapshot``vehicle_realtime_location` 都是每协议每 VIN 一行的当前态投影,业务主键就是 `(protocol, vin)`
- 状态schema 使用 `PRIMARY KEY (protocol, vin)`,不保留自增 `id``created_at`,对外只暴露最新 `updated_at`
- 索引:不在 `vehicle_realtime_location` 维护经纬度组合索引;实时表只回答当前态,地理范围和轨迹类查询走 TDengine 历史位置。
- 查询:`/api/realtime/snapshots``/api/realtime/locations` 默认不执行 `COUNT(*)``total` 表示本页返回条数;只有需要精确总数时传 `includeTotal=true`
- 写入:车牌从 `vehicle_identity_binding` 按 VIN 主键直查,并使用 VIN 级短 TTL 内存缓存,避免实时帧每条都打 MySQL缓存不作为事实来源。
- 不写 MySQL KV 投影表;实时全量字段保留在 Redis KV历史全量字段走 TDengine raw fields。
- 生产:项目上线前可直接重建这两张 MySQL 表,实时数据会从 Kafka 新消息继续投影。
7. Redis realtime 只服务已定位 VIN 的正式车辆。
- 原因:在线状态和实时数据是业务当前态,不是身份排查队列。
- 无 VIN 的 808 等帧只进入 raw 证据层和注册/绑定排查链路,不写 `vehicle:latest:*``vehicle:online:*``vehicle:realtime-raw:*`
- 绑定补齐 VIN 后,后续新帧自然进入实时态;历史补偿需要从 raw/Kafka 回放。
8. identity 层只保留两张表。
- `vehicle_identity_binding`VIN 与 plate/phone/oem 的映射,供 808 等协议反查 VIN。该表只允许外部导入或人工维护服务运行时只读。
- `jt808_registration`808 注册、鉴权、最新活跃和 VIN 匹配状态。
- 状态:`vehicle_identity_binding` 使用 VIN 主键;`jt808_registration` 使用 phone 主键;两张表都不保留代理自增主键和 `created_at`
- 查询:`vehicle_identity_binding` 的 phone/plate 都是唯一键,接入侧按唯一键直查 VIN不做无意义排序。
- 写入链路gateway 对 binding 查 VIN 使用短 TTL 内存缓存,包含未命中缓存,避免无 VIN 高频位置帧每条都打 MySQL。
- 生产RDS 已在上线前删除旧 `vehicle_identity_binding_registration``vehicle_identity_bindings`gateway 启动会自动创建最小 schema。
## 字段提升规则
协议字段进入系统后分三层:
1. `parsed_json`:默认入口,保存完整结构化解析。
2. 核心列只有跨协议稳定查询需要时才提升例如经纬度、速度、SOC、总里程。
3. 指标表:只有聚合口径稳定、产品需要分页/排序/报表时才持久化。
反例:
- 不因为某个协议字段存在就增加 MySQL 列。
- 不为了方便调试在 snapshot/location 放完整 JSON。
- 不为 telemetry field 配置服务提前复制一份字段表;字段配置服务应从 raw/Kafka 回放生成自己的结果。
## 删除或迁移顺序
1. 先写测试证明旧 API 能从新主表读到同等结果。
2. 改代码停止写重复表或重复列。
3. 部署后观察 Kafka lag、writer 成功计数、API 结果。
4. 查询生产库确认旧表不再新增。
5. 做一次备份或导出。
6. 删除旧表或旧列。
任何一步无法证明安全,就停在兼容状态,不直接删生产数据。