第一章:时空数据基础概念
1.1 什么是移动对象(Moving Object)?
- 定义:随时间变化位置的实体(车辆、手机、动物)
- 数学表示:
> MO = (x_1, y_1, t_1), (x_2, y_2, t_2), ..., (x_n, y_n, t_n) > > $ > > 即一系列 **(经度, 纬度, 时间戳)** 三元组。 #### 1.2 地理围栏(Geofence) * **静态围栏**:固定多边形(如仓库边界) * **动态围栏**:随时间变化的区域(如移动基站覆盖区) * **事件类型** : > * **Enter**:对象首次进入围栏 > * **Exit**:对象离开围栏 > * **Dwell**:在围栏内停留超过阈值 *** ** * ** *** ### 第二章:混合存储架构设计 #### 2.1 为什么需要混合引擎? | 需求 | PostGIS | Redis | |----|---------|-------| > * **持久化历史轨迹** \| ✅ 强大 \| ❌ 内存易失 > * **毫秒级围栏检测** \| ❌ 批处理慢 \| ✅ 流式处理 > * **复杂时空查询** \| ✅ ST-Intersects / ST-DWithin \| ❌ 功能有限 > **架构图**: [设备/GPS App] │ ↓ (实时位置) [Redis Stream: locations] │ ├──→ [RedisGears 脚本] → 实时围栏检测 → [Webhook 告警] │ └──→ [Flask 定时任务] → 聚合写入 PostGIS(每 5 分钟) *** ** * ** *** ### 第三章:实时地理围栏 ------ RedisGears 实现 #### 3.1 Redis 数据结构设计 # 1. 实时位置流(每个设备一个 stream) XADD locations:device_123 * lat 39.9042 lon 116.4074 ts 1705650000 # 2. 围栏定义(Hash 存储 GeoJSON) HSET geofences:warehouse_1 geojson '{"type":"Polygon","coordinates":[[[...]]]}' HSET geofences:warehouse_1 active_from 1705600000 HSET geofences:warehouse_1 active_to 1705700000 # 3. 设备状态(记录上次是否在围栏内) SET device_state:device_123:warehouse_1 "inside" #### 3.2 RedisGears 脚本(Python) # gears/geofence.py import redis from shapely.geometry import Point, shape import json def check_geofence(record): # 解析位置流 device_id = record['key'].split(':')[1] lat = float(record['value']['lat']) lon = float(record['value']['lon']) ts = int(record['value']['ts']) r = redis.Redis() # 获取所有活跃围栏 for fence_key in r.scan_iter(match="geofences:*"): fence = r.hgetall(fence_key) if not (int(fence[b'active_from']) <= ts <= int(fence[b'active_to'])): continue # 解析 GeoJSON poly = shape(json.loads(fence[b'geojson'])) point = Point(lon, lat) # 注意:GeoJSON 是 [lon, lat] # 检查是否在围栏内 is_inside = poly.contains(point) fence_id = fence_key.decode().split(':')[1] state_key = f"device_state:{device_id}:{fence_id}" last_state = r.get(state_key) # 触发事件 if is_inside and last_state != b"inside": # Enter 事件 trigger_webhook("enter", device_id, fence_id, ts) r.set(state_key, "inside") elif not is_inside and last_state == b"inside": # Exit 事件 trigger_webhook("exit", device_id, fence_id, ts) r.set(state_key, "outside") # 注册为 RedisGears 流处理器 GB('StreamReader', desc='Geofence Checker')\ .map(check_geofence)\ .register('locations:*') > **部署**: redis-cli -x RG.PYEXECUTE < gears/geofence.py > **效果** :设备进入围栏 **\<100ms** 触发 HTTP 告警。 *** ** * ** *** ### 第四章:轨迹持久化与分析 ------ PostGIS #### 4.1 表结构设计 -- 移动对象轨迹表 CREATE TABLE trajectories ( id SERIAL PRIMARY KEY, device_id VARCHAR(50) NOT NULL, trip_id UUID NOT NULL, -- 同一出行的轨迹分段 traj GEOMETRY(LINESTRINGZM, 4326) -- Z=海拔, M=时间戳(微秒) ); -- 创建时空索引 CREATE INDEX idx_traj_gist ON trajectories USING GIST(traj); > **LINESTRINGZM**: > > * X = 经度 > * Y = 纬度 > * Z = 海拔(可选) > * M = 时间戳(关键!) #### 4.2 写入轨迹(Flask 聚合任务) # services/trajectory_aggregator.py from sqlalchemy import func def aggregate_to_postgis(): """每 5 分钟将 Redis 位置聚合为轨迹""" r = redis.Redis() devices = r.smembers("active_devices") for device in devices: # 获取最近 5 分钟位置 points = r.xrange(f"locations:{device}", min="-", max="+") if len(points) < 2: continue # 构建 WKT LINESTRINGZM wkt_parts = [] for _, data in points: lat = float(data[b'lat']) lon = float(data[b'lon']) ts = int(data[b'ts']) wkt_parts.append(f"{lon} {lat} 0 {ts}") # Z=0(无海拔) wkt = f"LINESTRING ZM ({', '.join(wkt_parts)})" # 插入 PostGIS db.session.execute( "INSERT INTO trajectories (device_id, trip_id, traj) VALUES (:dev, :trip, ST_GeomFromText(:wkt, 4326))", {"dev": device, "trip": str(uuid.uuid4()), "wkt": wkt} ) db.session.commit() # 清理 Redis r.delete(f"locations:{device}") #### 4.3 高级时空查询 ##### 场景1:找出所有经过某区域的车辆 SELECT device_id FROM trajectories WHERE ST_Intersects( traj, ST_GeomFromText('POLYGON((...))', 4326) ); ##### 场景2:计算两轨迹的时空交集(接触者追踪) -- 找出 device_A 与 device_B 在 10 米内共处超过 5 分钟 WITH proximity AS ( SELECT ST_Distance( ST_Force2D(a.traj), ST_Force2D(b.traj) ) AS dist, ST_M(a.traj) AS time_a, ST_M(b.traj) AS time_b FROM trajectories a, trajectories b WHERE a.device_id = 'device_A' AND b.device_id = 'device_B' AND ABS(ST_M(a.traj) - ST_M(b.traj)) < 300 -- 时间差 < 5 分钟 ) SELECT COUNT(*) FROM proximity WHERE dist < 10; -- 距离 < 10 米 *** ** * ** *** ### 第五章:前端可视化 ------ MapLibre GL JS #### 5.1 实时车辆移动 #### 5.2 轨迹回放控件 * 使用 **MapLibre 的 `setFilter`** 动态显示某时间段轨迹 * 时间轴滑块联动,支持播放/暂停 *** ** * ** *** ### 第六章:隐私保护设计 #### 6.1 差分隐私(Differential Privacy) 对上报位置添加拉普拉斯噪声: # utils/privacy.py import numpy as np def add_location_noise(lat, lon, epsilon=1.0): """epsilon 越小,隐私越强,精度越低""" sensitivity = 1.0 # 假设最大移动距离 scale = sensitivity / epsilon noise_lat = np.random.laplace(0, scale) noise_lon = np.random.laplace(0, scale) return lat + noise_lat, lon + noise_lon > **权衡**:ε=0.1(高隐私)→ 位置偏移 ~1km;ε=2.0(低隐私)→ 偏移 ~50m。 #### 6.2 自动数据过期 PostGIS 轨迹表增加 TTL 字段: ALTER TABLE trajectories ADD COLUMN expire_at TIMESTAMP; CREATE INDEX idx_expire ON trajectories(expire_at); -- 每天清理过期数据 DELETE FROM trajectories WHERE expire_at < NOW(); > **合规**:满足 GDPR "数据最小化"原则。 *** ** * ** *** ### 第七章:性能优化 #### 7.1 Redis 优化 * **分片**:按设备 ID 哈希分片到多个 Redis 实例 * **压缩**:使用 Protocol Buffers 序列化位置数据 #### 7.2 PostGIS 优化 * **分区表** :按 `trip_id` 或日期分区 * **物化视图**:预计算常用围栏进出记录 *** ** * ** *** ### 第八章:场景实战 #### 8.1 共享单车电子围栏 * **业务规则** : > * 进入围栏:开始计费 > * 离开围栏:结束计费 + 检查是否停在指定停车点 * **技术实现** : > * RedisGears 监听 `enter/exit` 事件 > * 调用计费服务 API #### 8.2 物流停留点检测(续) -- 找出速度 < 1 km/h 且持续 > 10 分钟的点 SELECT device_id, ST_StartPoint(traj) AS stop_point, ST_M(ST_EndPoint(traj)) - ST_M(ST_StartPoint(traj)) AS duration_seconds FROM trajectories WHERE -- 计算平均速度(米/秒):距离 / 时间 ST_Length(ST_Force2D(traj)) / NULLIF((ST_M(ST_EndPoint(traj)) - ST_M(ST_StartPoint(traj))), 0) < 0.28 -- ≈1 km/h AND (ST_M(ST_EndPoint(traj)) - ST_M(ST_StartPoint(traj))) > 600; -- >10 分钟 > **业务价值**:自动识别装卸货、休息、堵车等行为,优化调度。 #### 8.3 疫情接触者追踪 * **输入**:确诊者轨迹 + 全城用户匿名轨迹 * **输出**:高风险接触者列表(时空交集 ≥ 15 分钟 \& 距离 ≤ 2 米) * **隐私保障** : > * 用户轨迹 ID 匿名化(不可逆哈希) > * 结果仅通知疾控中心,不暴露他人身份 # services/contact_tracing.py def find_contacts(confirmed_traj_id: str, risk_threshold_minutes=15): sql = """ SELECT DISTINCT u.device_hash FROM trajectories u, trajectories c WHERE c.id = :confirmed_id AND u.device_hash != c.device_hash AND ST_DWithin(ST_Force2D(u.traj), ST_Force2D(c.traj), 2) -- 距离 ≤ 2 米 AND ABS(ST_M(ST_StartPoint(u.traj)) - ST_M(ST_StartPoint(c.traj))) < 900 -- 时间窗口 ±15 分钟 AND ST_M(ST_EndPoint(u.traj)) - ST_M(ST_StartPoint(u.traj)) >= 900; -- 停留 ≥15 分钟 """ return db.session.execute(sql, {"confirmed_id": confirmed_traj_id}).fetchall() > **注意**:实际部署需通过政府授权,并符合《个人信息保护法》。 *** ** * ** *** ### 第九章:系统监控与告警 #### 9.1 关键指标 | 指标 | 目标 | 工具 | |----|----|----| > * **围栏触发延迟** \| \<200ms \| Prometheus + Grafana > * **轨迹写入吞吐** \| \>10k 点/秒 \| Redis INFO > * **PostGIS 查询 P99** \| \<500ms \| pg_stat_statements #### 9.2 告警规则 * **Redis 内存 \>80%** → 扩容或清理 * **围栏事件丢失率 \>1%** → 检查 RedisGears 脚本错误日志 *** ** * ** *** ### 第十章:未来演进 #### 10.1 向量时空数据库 * **新兴技术** : > * **Apache Sedona**(原 GeoSpark):分布式时空分析 > * **TileDB-VCF**:支持时空+基因组等多维数据 * **优势**:PB 级轨迹数据秒级响应。 #### 10.2 边缘计算集成 * **设备端预处理** : > * 手机/车载终端本地判断是否接近围栏 > * 仅上报"可能进入"事件,减少带宽 * **协议** :使用 **MQTT over TLS** 低功耗传输。 *** ** * ** *** ### 总结:让时空数据驱动智能决策 **时空数据不是 GPS 坐标堆砌,而是理解物理世界运行规律的钥匙。**