找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

2445

积分

0

好友

329

主题
发表于 7 小时前 | 查看: 4| 回复: 0

从数据建模到性能优化,一站式掌握位置智能系统的核心设计

前言

在物联网和移动互联网时代,GPS轨迹数据已成为众多业务场景的核心资产:

  • 🚗 网约车/物流:车辆轨迹追踪、行程回放、偏航检测
  • 📱 运动健康:跑步/骑行路线记录、轨迹分享
  • 🛫 出行服务:航班/船舶轨迹监控
  • 🔐 安全风控:设备位置异常检测、电子围栏

然而,GPS轨迹数据具有海量、高频、时空关联的特点,给存储和查询带来巨大挑战:

┌─────────────────────────────────────────────────────────────┐
│                   GPS 轨迹数据特征                          │
├─────────────────────────────────────────────────────────────┤
│  • 数据量大:10 万车辆 × 1 分钟/点 × 24 小时 = 1.44 亿点/天    │
│  • 写入高频:每秒数万至数十万写入                            │
│  • 查询复杂:时空范围、轨迹匹配、邻近搜索、聚合统计          │
│  • 时效性强:近期数据频繁访问,历史数据逐渐冷却              │
└─────────────────────────────────────────────────────────────┘

本文将基于MongoDB的地理空间特性,带你从零构建一个生产级的GPS轨迹存储与查询系统,涵盖从数据建模、索引优化到查询模式的完整设计。

一、为什么选择 MongoDB?

1.1 传统关系型 vs MongoDB

┌─────────────────────────────────────────────────────────────┐
│              关系型数据库 (MySQL + PostGIS)                   │
├─────────────────────────────────────────────────────────────┤
│  ✅ 优势:ACID 事务、复杂 JOIN、成熟生态                      │
│  ❌ 劣势:水平扩展困难、写入瓶颈、Schema 固定                 │
│  📊 场景:1000 万点以下、查询简单、强一致性要求               │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                    MongoDB                                   │
├─────────────────────────────────────────────────────────────┤
│  ✅ 优势:                                                   │
│     • 原生 GeoJSON 支持,丰富的地理空间操作符                 │
│     • 水平扩展(分片),轻松应对海量数据                     │
│     • 灵活 Schema,适应轨迹数据的多变性                       │
│     • 高写入吞吐,适合 IoT 场景                              │
│  📊 场景:亿级数据、高并发写入、复杂空间查询                  │
└─────────────────────────────────────────────────────────────┘

1.2 MongoDB 地理空间核心能力

特性 说明 适用场景
2dsphere 索引 基于球面的地理索引 地球表面位置查询
nearSphere 邻近搜索 查找附近的点
$geoWithin 范围内查询 电子围栏、区域统计
$geoIntersects 相交查询 轨迹与区域相交检测
$geoNear 聚合管道邻近查询 带排序和距离的复杂查询
GeoJSON 标准地理数据格式 点、线、多边形存储

二、数据模型设计

2.1 核心设计原则

┌─────────────────────────────────────────────────────────────┐
│                   GPS 轨迹数据模型设计原则                    │
├─────────────────────────────────────────────────────────────┤
│  1. 查询驱动:根据查询模式设计数据结构                        │
│  2. 时间分区:按时间分片,支持高效范围查询                    │
│  3. 冷热分离:近期数据热存储,历史数据归档                    │
│  4. 适度冗余:用空间换时间,减少关联查询                      │
│  5. 索引优化:复合索引覆盖常用查询模式                        │
└─────────────────────────────────────────────────────────────┘

2.2 方案一:单点存储模式(推荐用于高频写入)

// 轨迹点集合:gps_points
{
  _id: ObjectId("..."),

  // 核心地理信息
  location: {
    type: "Point",
    coordinates: [116.407526, 39.904030] // [经度,纬度]
  },

  // 轨迹标识
  trackId: "track_20240115_001",
  deviceId: "device_12345",
  userId: "user_67890",

  // 时间信息
  timestamp: ISODate("2024-01-15T10:30:00Z"),
  date: "2024-01-15", // 用于分区查询
  hour: 10,

  // 运动状态
  speed: 45.5,        // 速度 km/h
  direction: 135.0,   // 方向 角度
  altitude: 50.5,     // 海拔 米
  accuracy: 10.0,     // 精度 米

  // 扩展信息
  address: "北京市朝阳区 xxx 路",
  eventType: "normal", // normal, start, end, alarm

  // 元数据
  createdAt: ISODate("2024-01-15T10:30:01Z")
}

索引设计

// 1. 地理空间索引(必须)
db.gps_points.createIndex({ location: "2dsphere" })

// 2. 时间范围查询
db.gps_points.createIndex({ timestamp: -1 })

// 3. 设备 + 时间复合查询(最常用)
db.gps_points.createIndex({ deviceId: 1, timestamp: -1 })

// 4. 地理 + 时间复合查询
db.gps_points.createIndex({ location: "2dsphere", timestamp: -1 })

// 5. 多条件复合查询
db.gps_points.createIndex({
  deviceId: 1,
  timestamp: -1,
  location: "2dsphere"
})

// 6. 按日期分区查询
db.gps_points.createIndex({ date: 1, deviceId: 1 })

优势

  • ✅ 写入性能极高,文档结构简单
  • ✅ 支持灵活的点级查询和聚合
  • ✅ 易于分片和水平扩展

劣势

  • ❌ 查询完整轨迹需要多次查询
  • ❌ 数据量巨大(单点存储)

2.3 方案二:轨迹聚合模式(推荐用于轨迹回放)

// 轨迹集合:tracks
{
  _id: ObjectId("..."),

  // 轨迹标识
  trackId: "track_20240115_001",
  deviceId: "device_12345",
  userId: "user_67890",

  // 时间范围
  startTime: ISODate("2024-01-15T10:00:00Z"),
  endTime: ISODate("2024-01-15T11:30:00Z"),
  date: "2024-01-15",

  // 轨迹几何(LineString)
  path: {
    type: "LineString",
    coordinates: [
      [116.407526, 39.904030],
      [116.408526, 39.905030],
      [116.409526, 39.906030],
      // ... 更多点
    ]
  },

  // 轨迹统计
  totalDistance: 15.5,    // 总距离 km
  duration: 5400,         // 持续时间 秒
  avgSpeed: 35.5,         // 平均速度 km/h
  maxSpeed: 78.0,         // 最大速度 km/h
  pointCount: 180,        // 轨迹点数

  // 起止点
  startPoint: {
    location: { type: "Point", coordinates: [116.407526, 39.904030] },
    address: "北京市朝阳区 A 地",
    time: ISODate("2024-01-15T10:00:00Z")
  },
  endPoint: {
    location: { type: "Point", coordinates: [116.418526, 39.915030] },
    address: "北京市海淀区 B 地",
    time: ISODate("2024-01-15T11:30:00Z")
  },

  // 详细轨迹点(可选,用于精确回放)
  points: [
    {
      location: { type: "Point", coordinates: [116.407526, 39.904030] },
      timestamp: ISODate("2024-01-15T10:00:00Z"),
      speed: 0,
      direction: 0
    },
    {
      location: { type: "Point", coordinates: [116.408526, 39.905030] },
      timestamp: ISODate("2024-01-15T10:00:30Z"),
      speed: 35,
      direction: 45
    }
    // ... 更多点
  ],

  // 元数据
  status: "completed", // ongoing, completed, aborted
  createdAt: ISODate("2024-01-15T10:00:00Z"),
  updatedAt: ISODate("2024-01-15T11:30:00Z")
}

索引设计

// 1. 轨迹路径的空间索引
db.tracks.createIndex({ path: "2dsphere" })

// 2. 设备 + 时间查询
db.tracks.createIndex({ deviceId: 1, startTime: -1 })

// 3. 时间范围查询
db.tracks.createIndex({ startTime: -1, endTime: -1 })

// 4. 起止点查询
db.tracks.createIndex({ "startPoint.location": "2dsphere" })
db.tracks.createIndex({ "endPoint.location": "2dsphere" })

// 5. 用户维度查询
db.tracks.createIndex({ userId: 1, startTime: -1 })

优势

  • ✅ 轨迹查询高效,一次获取完整路径
  • ✅ 支持轨迹级别的统计和分析
  • ✅ 存储空间相对节省

劣势

  • ❌ 文档大小有限制(16MB)
  • ❌ 更新轨迹需要重写文档

2.4 方案三:混合模式(生产环境推荐)

┌─────────────────────────────────────────────────────────────┐
│                    混合存储架构                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│   实时数据 (最近 7 天)          历史数据 (7 天前)             │
│   ┌─────────────────┐          ┌─────────────────┐          │
│   │   gps_points    │  ──────> │   gps_points    │          │
│   │   (热数据)       │  归档     │   (冷数据/归档)  │          │
│   └─────────────────┘          └─────────────────┘          │
│           │                                      │          │
│           ▼                                      ▼          │
│   ┌─────────────────┐          ┌─────────────────┐          │
│   │    tracks       │          │ tracks_archive  │          │
│   │   (活跃轨迹)     │          │   (历史轨迹)     │          │
│   └─────────────────┘          └─────────────────┘          │
│                                                             │
└─────────────────────────────────────────────────────────────┘

数据流转策略

// 定时任务:每天凌晨将昨天的数据归档
// 1. 聚合生成轨迹文档
db.gps_points.aggregate([
  {
    $match: {
      date: "2024-01-14", // 昨天的日期
      deviceId: "device_12345"
    }
  },
  {
    $sort: { timestamp: 1 }
  },
  {
    $group: {
      _id: "$trackId",
      deviceId: { $first: "$deviceId" },
      userId: { $first: "$userId" },
      startTime: { $first: "$timestamp" },
      endTime: { $last: "$timestamp" },
      coordinates: {
        $push: "$location.coordinates"
      },
      points: {
        $push: {
          location: "$location",
          timestamp: "$timestamp",
          speed: "$speed",
          direction: "$direction"
        }
      },
      totalDistance: { $sum: "$distance" },
      pointCount: { $sum: 1 }
    }
  },
  {
    $out: "tracks"
  }
])

// 2. 删除已归档的热点数据(可选)
db.gps_points.deleteMany({
  date: { $lt: "2024-01-08" } // 7 天前的数据
})

三、核心查询场景实战

3.1 场景一:查找指定区域内的轨迹点

// 查询北京市中心 5 公里范围内的轨迹点
db.gps_points.find({
  location: {
    $near: {
      $geometry: {
        type: "Point",
        coordinates: [116.407526, 39.904030]
      },
      $maxDistance: 5000  // 5 公里
    }
  },
  timestamp: {
    $gte: ISODate("2024-01-15T10:00:00Z"),
    $lte: ISODate("2024-01-15T12:00:00Z")
  }
})

// 按距离排序
db.gps_points.find({
  location: {
    $nearSphere: {
      $geometry: {
        type: "Point",
        coordinates: [116.407526, 39.904030]
      },
      $minDistance: 0,
      $maxDistance: 10000
    }
  }
}).sort({ timestamp: -1 }).limit(100)

3.2 场景二:电子围栏(多边形区域查询)

// 定义电子围栏(中关村软件园区域)
const zhongguancunPark = {
  type: "Polygon",
  coordinates: [[
    [116.301, 40.050],
    [116.320, 40.050],
    [116.320, 40.065],
    [116.301, 40.065],
    [116.301, 40.050]
  ]]
}

// 查询在围栏内的轨迹点
db.gps_points.find({
  location: {
    $geoWithin: {
      $geometry: zhongguancunPark
    }
  },
  deviceId: "device_12345",
  timestamp: {
    $gte: ISODate("2024-01-15T00:00:00Z"),
    $lte: ISODate("2024-01-15T23:59:59Z")
  }
})

// 查询进入过围栏的轨迹
db.tracks.find({
  path: {
    $geoIntersects: {
      $geometry: zhongguancunPark
    }
  }
})

3.3 场景三:轨迹回放(获取完整轨迹)

// 方案 1:从 gps_points 查询
db.gps_points.find({
  deviceId: "device_12345",
  trackId: "track_20240115_001"
}).sort({ timestamp: 1 })

// 方案 2:从 tracks 查询(推荐)
db.tracks.findOne({
  trackId: "track_20240115_001"
}, {
  path: 1,
  points: 1,
  startTime: 1,
  endTime: 1,
  totalDistance: 1
})

// 聚合管道:计算轨迹统计信息
db.gps_points.aggregate([
  {
    $match: {
      deviceId: "device_12345",
      trackId: "track_20240115_001"
    }
  },
  {
    $sort: { timestamp: 1 }
  },
  {
    $group: {
      _id: "$trackId",
      deviceId: { $first: "$deviceId" },
      startTime: { $first: "$timestamp" },
      endTime: { $last: "$timestamp" },
      points: {
        $push: {
          location: "$location",
          timestamp: "$timestamp",
          speed: "$speed"
        }
      },
      totalDistance: { $sum: "$distance" },
      avgSpeed: { $avg: "$speed" },
      maxSpeed: { $max: "$speed" }
    }
  },
  {
    $project: {
      duration: {
        $divide: [
          { $subtract: ["$endTime", "$startTime"] },
          1000
        ]
      },
      points: 1,
      totalDistance: 1,
      avgSpeed: { $round: ["$avgSpeed", 2] },
      maxSpeed: 1
    }
  }
])

3.4 场景四:查找附近的车辆/设备

// 查找当前位置 3 公里内的所有车辆
db.gps_points.aggregate([
  {
    $geoNear: {
      near: {
        type: "Point",
        coordinates: [116.407526, 39.904030]
      },
      distanceField: "distance",
      maxDistance: 3000,
      query: {
        timestamp: {
          $gte: ISODate("2024-01-15T10:25:00Z") // 最近 5 分钟
        }
      },
      spherical: true,
      key: "location"
    }
  },
  {
    $group: {
      _id: "$deviceId",
      latestLocation: { $first: "$location" },
      latestTime: { $first: "$timestamp" },
      distance: { $first: "$distance" },
      speed: { $first: "$speed" }
    }
  },
  {
    $project: {
      deviceId: "$_id",
      location: "$latestLocation",
      distance: { $round: [{ $divide: ["$distance", 1000] }, 2] },
      speed: 1,
      latestTime: 1
    }
  },
  { $limit: 50 }
])

3.5 场景五:轨迹匹配(判断是否偏离路线)

// 定义预定路线
const plannedRoute = {
  type: "LineString",
  coordinates: [
    [116.407526, 39.904030],
    [116.408526, 39.905030],
    [116.409526, 39.906030],
    [116.410526, 39.907030]
  ]
}

// 查找偏离路线超过 500 米的轨迹点
db.gps_points.aggregate([
  {
    $match: {
      trackId: "track_20240115_001"
    }
  },
  {
    $addFields: {
      distanceToRoute: {
        $function: {
          body: function(location, route) {
            // 计算点到线的最短距离(简化版)
            let minDistance = Infinity;
            for (let i = 0; i < route.coordinates.length - 1; i++) {
              const d = pointToSegmentDistance(
                location.coordinates,
                route.coordinates[i],
                route.coordinates[i + 1]
              );
              minDistance = Math.min(minDistance, d);
            }
            return minDistance;
          },
          args: ["$location", plannedRoute],
          lang: "js"
        }
      }
    }
  },
  {
    $match: {
      distanceToRoute: { $gt: 500 } // 偏离超过 500 米
    }
  }
])

3.6 场景六:停留点检测

// 检测在某地停留超过 10 分钟的位置
db.gps_points.aggregate([
  {
    $match: {
      deviceId: "device_12345",
      date: "2024-01-15"
    }
  },
  {
    $sort: { timestamp: 1 }
  },
  {
    $group: {
      _id: {
        deviceId: "$deviceId",
        // 将位置网格化(约 100 米精度)
        gridLat: { $floor: { $multiply: ["$location.coordinates.1", 1000] } },
        gridLng: { $floor: { $multiply: ["$location.coordinates.0", 1000] } }
      },
      firstTime: { $first: "$timestamp" },
      lastTime: { $last: "$timestamp" },
      location: { $first: "$location" },
      pointCount: { $sum: 1 }
    }
  },
  {
    $addFields: {
      durationSeconds: {
        $divide: [
          { $subtract: ["$lastTime", "$firstTime"] },
          1000
        ]
      }
    }
  },
  {
    $match: {
      durationSeconds: { $gt: 600 },  // 超过 10 分钟
      pointCount: { $gt: 10 }         // 至少 10 个点
    }
  },
  {
    $project: {
      deviceId: "$_id.deviceId",
      location: 1,
      arrivalTime: "$firstTime",
      leaveTime: "$lastTime",
      durationMinutes: { $round: [{ $divide: ["$durationSeconds", 60] }, 1] },
      pointCount: 1
    }
  },
  { $sort: { durationSeconds: -1 } }
])

3.7 场景七:里程统计

// 按天统计行驶里程
db.gps_points.aggregate([
  {
    $match: {
      deviceId: "device_12345",
      date: { $gte: "2024-01-01", $lte: "2024-01-31" },
      distance: { $exists: true }
    }
  },
  {
    $group: {
      _id: "$date",
      totalDistance: { $sum: "$distance" },
      avgSpeed: { $avg: "$speed" },
      maxSpeed: { $max: "$speed" },
      driveTime: {
        $sum: {
          $cond: [{ $gt: ["$speed", 0] }, 1, 0]
        }
      }
    }
  },
  {
    $project: {
      date: "$_id",
      totalDistance: { $round: ["$totalDistance", 2] },
      avgSpeed: { $round: ["$avgSpeed", 1] },
      maxSpeed: 1,
      driveTimeMinutes: { $round: [{ $divide: ["$driveTime", 60] }, 1] }
    }
  },
  { $sort: { date: 1 } }
])

// 按月统计
db.tracks.aggregate([
  {
    $match: {
      deviceId: "device_12345",
      startTime: {
        $gte: ISODate("2024-01-01"),
        $lte: ISODate("2024-01-31")
      }
    }
  },
  {
    $group: {
      _id: {
        year: { $year: "$startTime" },
        month: { $month: "$startTime" },
        day: { $dayOfMonth: "$startTime" }
      },
      totalDistance: { $sum: "$totalDistance" },
      tripCount: { $sum: 1 },
      avgDuration: { $avg: "$duration" }
    }
  },
  { $sort: { "_id.year": 1, "_id.month": 1, "_id.day": 1 } }
])

四、性能优化实战

4.1 索引优化策略

// 1. 使用 explain 分析查询
db.gps_points.find({
  deviceId: "device_12345",
  timestamp: { $gte: ISODate("2024-01-15T00:00:00Z") }
}).explain("executionStats")

// 输出分析:
{
  "queryPlanner": {
    "winningPlan": {
      "stage": "FETCH",
      "inputStage": {
        "stage": "IXSCAN",
        "keyPattern": { deviceId: 1, timestamp: -1 },
        "indexName": "deviceId_1_timestamp_-1"
      }
    }
  },
  "executionStats": {
    "executionTimeMillis": 15,
    "totalKeysExamined": 1000,
    "totalDocsExamined": 1000,
    "nReturned": 500
  }
}

// 2. 创建覆盖索引(避免回表)
db.gps_points.createIndex({
  deviceId: 1,
  timestamp: -1,
  location: 1,
  speed: 1
})

// 3. 使用部分索引(减少索引大小)
db.gps_points.createIndex(
  { location: "2dsphere" },
  {
    partialFilterExpression: {
      timestamp: { $gte: ISODate("2024-01-01") }
    }
  }
)

// 4. 稀疏索引(可选字段)
db.gps_points.createIndex(
  { address: 1 },
  { sparse: true }
)

4.2 分片策略

// 1. 启用分片
sh.enableSharding("gps_db")

// 2. 按设备 ID 分片(适合设备维度查询)
sh.shardCollection("gps_db.gps_points", {
  deviceId: "hashed"
})

// 3. 按时间范围分片(适合时间范围查询)
sh.shardCollection("gps_db.gps_points", {
  date: 1,
  deviceId: 1
})

// 4. 复合分片键(推荐)
sh.shardCollection("gps_db.gps_points", {
  deviceId: "hashed",
  timestamp: -1
})

// 5. 预分割数据块(避免初始热点)
sh.splitAt("gps_db.gps_points", {
  deviceId: "device_00001",
  timestamp: ISODate("2024-01-01")
})

4.3 写入优化

// 1. 批量写入(推荐)
const batchSize = 1000;
const batch = [];

for (let point of points) {
  batch.push({
    insertOne: {
      document: {
        location: point.location,
        deviceId: point.deviceId,
        timestamp: point.timestamp,
        speed: point.speed,
        date: point.date
      }
    }
  });

  if (batch.length >= batchSize) {
    db.gps_points.bulkWrite(batch);
    batch.length = 0;
  }
}
if (batch.length > 0) {
  db.gps_points.bulkWrite(batch);
}

// 2. 调整 Write Concern(牺牲一致性换性能)
db.gps_points.insertOne(doc, {
  w: 1,  // 只需主节点确认
  j: false  // 不等待 journal
})

// 3. 禁用索引(批量导入时)
db.gps_points.dropIndexes()
// ... 批量导入数据
db.gps_points.createIndex({ deviceId: 1, timestamp: -1 })

// 4. 使用无序插入
db.gps_points.insertMany(docs, { ordered: false })

4.4 查询优化

// 1. 使用投影减少返回字段
db.gps_points.find({
  deviceId: "device_12345",
  date: "2024-01-15"
}, {
  location: 1,
  timestamp: 1,
  speed: 1,
  _id: 0
})

// 2. 限制返回数量
db.gps_points.find({
  deviceId: "device_12345"
}).sort({ timestamp: -1 }).limit(100)

// 3. 使用 hint 强制使用索引
db.gps_points.find({
  deviceId: "device_12345",
  timestamp: { $gte: ISODate("2024-01-15") }
}).hint({ deviceId: 1, timestamp: -1 })

// 4. 避免全表扫描
// ❌ 不推荐:对字段进行计算
db.gps_points.find({
  $where: "this.timestamp.getDate() === '2024-01-15'"
})

// ✅ 推荐:使用范围查询
db.gps_points.find({
  timestamp: {
    $gte: ISODate("2024-01-15T00:00:00Z"),
    $lt: ISODate("2024-01-16T00:00:00Z")
  }
})

// 5. 使用 readPreference 读写分离
db.gps_points.find({
  deviceId: "device_12345"
}).readPref("secondary")

4.5 存储优化

// 1. 数据压缩
db.runCommand({
  collMod: "gps_points",
  pipeline: { $compress: { method: "zstd" } }
})

// 2. TTL 自动过期(自动删除 90 天前的数据)
db.gps_points.createIndex(
  { timestamp: 1 },
  { expireAfterSeconds: 90 * 24 * 60 * 60 }
)

// 3. 字段级优化
// 使用缩写减少字段名长度
{
  "l": { type: "Point", coordinates: [...] }, // location
  "d": "device_12345",                        // deviceId
  "t": ISODate("..."),                        // timestamp
  "s": 45.5,                                  // speed
  "dt": "2024-01-15"                          // date
}

// 4. 使用 capped collection(固定集合)
db.createCollection("gps_realtime", {
  capped: true,
  size: 10 * 1024 * 1024 * 1024, // 10GB
  max: 10000000                   // 1000 万条
})

五、完整案例:网约车轨迹系统

5.1 系统架构

┌─────────────────────────────────────────────────────────────────────────┐
│                      网约车轨迹系统架构                                  │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌──────────┐  ┌──────────┐  ┌──────────┐                              │
│  │ 司机端   │  │ 乘客端   │  │ 运营端   │                              │
│  │   App    │  │   App    │  │  Web     │                              │
│  └────┬─────┘  └────┬─────┘  └────┬─────┘                              │
│       │             │             │                                     │
│       └─────────────┴──────┬──────┘                                     │
│                            │ HTTP/WebSocket                              │
│       ┌────────────────────┴────────────────────┐                       │
│       │            API Gateway                   │                       │
│       └────────────────────┬────────────────────┘                       │
│                            │                                             │
│       ┌────────────────────┴────────────────────┐                       │
│       │           轨迹服务集群                   │                       │
│       │  ┌─────────┐  ┌─────────┐  ┌─────────┐  │                       │
│       │  │ 接收上报 │  │ 轨迹查询 │  │ 数据分析 │  │                       │
│       │  └─────────┘  └─────────┘  └─────────┘  │                       │
│       └────────────────────┬────────────────────┘                       │
│                            │                                             │
│       ┌────────────────────┴────────────────────┐                       │
│       │          MongoDB 分片集群                │                       │
│       │  ┌─────────┐  ┌─────────┐  ┌─────────┐  │                       │
│       │  │ Shard 1 │  │ Shard 2 │  │ Shard 3 │  │                       │
│       │  └─────────┘  └─────────┘  └─────────┘  │                       │
│       └─────────────────────────────────────────┘                       │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

5.2 数据上报接口

// Node.js + Express 示例
const express = require('express');
const { MongoClient } = require('mongodb');

const app = express();
app.use(express.json());

// MongoDB 连接
const client = new MongoClient('mongodb://localhost:27017');
const db = client.db('ride_hailing');
const pointsCollection = db.collection('gps_points');
const tracksCollection = db.collection('tracks');

// 批量上报轨迹点
app.post('/api/track/upload', async (req, res) => {
  const { deviceId, points } = req.body;

  // points: [{ lat, lng, speed, timestamp, direction }]

  const batch = points.map(p => ({
    insertOne: {
      document: {
        location: {
          type: "Point",
          coordinates: [p.lng, p.lat]
        },
        deviceId,
        trackId: generateTrackId(deviceId, p.timestamp),
        timestamp: new Date(p.timestamp),
        date: new Date(p.timestamp).toISOString().split('T')[0],
        hour: new Date(p.timestamp).getHours(),
        speed: p.speed,
        direction: p.direction,
        accuracy: p.accuracy || 10,
        createdAt: new Date()
      }
    }
  }));

  try {
    await pointsCollection.bulkWrite(batch, { ordered: false });
    res.json({ success: true, count: points.length });
  } catch (error) {
    console.error('Upload error:', error);
    res.status(500).json({ success: false, error: error.message });
  }
});

// 实时上报(WebSocket)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
  let deviceId;
  let batchBuffer = [];

  ws.on('message', async (data) => {
    const point = JSON.parse(data);

    if (!deviceId) {
      deviceId = point.deviceId;
    }

    batchBuffer.push({
      location: { type: "Point", coordinates: [point.lng, point.lat] },
      deviceId,
      timestamp: new Date(point.timestamp),
      speed: point.speed,
      direction: point.direction
    });

    // 每 10 个点批量写入一次
    if (batchBuffer.length >= 10) {
      await flushBatch(batchBuffer);
      batchBuffer = [];
    }
  });

  // 连接关闭时 flush 剩余数据
  ws.on('close', async () => {
    if (batchBuffer.length > 0) {
      await flushBatch(batchBuffer);
    }
  });
});

async function flushBatch(batch) {
  await pointsCollection.insertMany(batch, { ordered: false });
}

function generateTrackId(deviceId, timestamp) {
  const date = new Date(timestamp).toISOString().split('T')[0];
  return `${deviceId}_${date}`;
}

5.3 轨迹查询接口

// 查询车辆实时位置
app.get('/api/vehicle/:deviceId/location', async (req, res) => {
  const { deviceId } = req.params;

  const location = await pointsCollection.findOne(
    { deviceId },
    { sort: { timestamp: -1 } },
    { projection: { location: 1, timestamp: 1, speed: 1, _id: 0 } }
  );

  res.json({ success: true, data: location });
});

// 查询历史轨迹
app.get('/api/track/history', async (req, res) => {
  const { deviceId, startDate, endDate } = req.query;

  const tracks = await pointsCollection.find({
    deviceId,
    timestamp: {
      $gte: new Date(startDate),
      $lte: new Date(endDate)
    }
  }).sort({ timestamp: 1 }).toArray();

  // 转换为 GeoJSON LineString
  const coordinates = tracks.map(t => t.location.coordinates);

  res.json({
    success: true,
    data: {
      type: "LineString",
      coordinates,
      points: tracks.map(t => ({
        timestamp: t.timestamp,
        speed: t.speed,
        direction: t.direction
      }))
    }
  });
});

// 查询附近车辆
app.get('/api/vehicle/nearby', async (req, res) => {
  const { lng, lat, radius = 3000 } = req.query;

  const vehicles = await pointsCollection.aggregate([
    {
      $geoNear: {
        near: {
          type: "Point",
          coordinates: [parseFloat(lng), parseFloat(lat)]
        },
        distanceField: "distance",
        maxDistance: parseInt(radius),
        query: {
          timestamp: {
            $gte: new Date(Date.now() - 5 * 60 * 1000) // 5 分钟内
          }
        },
        spherical: true
      }
    },
    {
      $group: {
        _id: "$deviceId",
        location: { $first: "$location" },
        timestamp: { $first: "$timestamp" },
        distance: { $first: "$distance" },
        speed: { $first: "$speed" }
      }
    },
    { $limit: 100 }
  ]).toArray();

  res.json({ success: true, data: vehicles });
});

// 电子围栏告警
app.post('/api/fence/check', async (req, res) => {
  const { deviceId, fence } = req.body;
  // fence: { type: "Polygon", coordinates: [...] }

  const violations = await pointsCollection.find({
    deviceId,
    location: {
      $geoWithin: { $geometry: fence }
    },
    timestamp: {
      $gte: new Date(Date.now() - 24 * 60 * 60 * 1000)
    }
  }).toArray();

  res.json({
    success: true,
    data: {
      deviceId,
      violationCount: violations.length,
      violations: violations.map(v => ({
        time: v.timestamp,
        location: v.location
      }))
    }
  });
});

5.4 性能监控

// 慢查询日志
db.setProfilingLevel(1, 100);  // 记录超过 100ms 的查询

// 查询慢查询日志
db.system.profile.find({
  ns: "gps_db.gps_points",
  millis: { $gt: 100 }
}).sort({ ts: -1 }).limit(10)

// 索引使用统计
db.gps_points.aggregate([
  { $indexStats: {} }
])

// 集合统计
db.gps_points.stats()

// 分片平衡状态
sh.status()

六、最佳实践总结

6.1 设计 Checklist

□ 数据模型
  □ 根据查询模式选择存储方案(单点/聚合/混合)
  □ 设计合理的分片和分区策略
  □ 预留扩展字段

□ 索引设计
  □ 2dsphere 地理空间索引
  □ 时间范围索引
  □ 复合索引覆盖常用查询
  □ 定期分析索引使用情况

□ 写入优化
  □ 批量写入(100-1000 条/批)
  □ 合理设置 Write Concern
  □ 使用无序插入

□ 查询优化
  □ 使用投影减少返回字段
  □ 限制返回数量
  □ 避免全表扫描
  □ 使用 explain 分析查询

□ 存储优化
  □ 设置 TTL 自动过期
  □ 冷热数据分离
  □ 定期归档历史数据
  □ 启用压缩

□ 监控告警
  □ 慢查询监控
  □ 索引命中率监控
  □ 分片平衡监控
  □ 存储空间监控

6.2 性能基准参考

场景 配置 性能指标
写入 3 节点副本集 5 万点/秒
写入 5 节点分片集群 20 万点/秒
邻近查询 单点 + 半径 < 50ms (百万数据)
范围查询 多边形 + 时间 < 100ms (百万数据)
轨迹回放 1 小时轨迹 < 200ms
聚合统计 天级里程 < 500ms

总结

MongoDB 凭借其原生的地理空间支持和优秀的水平扩展能力,是存储和查询 GPS 轨迹数据的理想选择。

核心要点回顾

  1. 数据模型:根据业务场景选择单点存储、轨迹聚合或混合模式
  2. 索引设计:2dsphere 索引 + 时间索引 + 复合索引
  3. 查询优化:使用 GeoJSON 标准、聚合管道、合理投影
  4. 性能扩展:分片集群、批量写入、读写分离
  5. 数据管理:TTL 过期、冷热分离、定期归档

GPS 轨迹系统的设计没有银弹,需要根据实际业务场景(数据量、查询模式、一致性要求)进行权衡。希望本文的实战经验能帮助你在项目中构建高效、可靠的位置智能系统。如果你想了解更多关于数据库架构设计的深度内容,可以访问 云栈社区后端与架构板块,那里有更多关于系统设计和性能优化的讨论。本文涉及的 MongoDB 具体实践和技巧也在社区的技术栈板块有详细展开。

基于 MongoDB 6.0+




上一篇:深度解析Claude Code:从Agent循环到上下文治理的工程实践
下一篇:岚图汽车公有云服务招标结果出炉:阿里云1190万、腾讯云1987万、华为云2160万
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-17 21:16 , Processed in 0.892729 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表