ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

无人机管控系统四库架构:Java+Redis+MySQL+MongoDB实战

无人机管控系统四库架构:Java+Redis+MySQL+MongoDB实战 简介本资源是一套完整的无人机飞行管控平台实战项目源码面向Java全栈开发者及物联网系统学习者解决多终端协同、实时状态监控与异构数据统一管理等典型工业级场景问题。项目采用前后端分离架构包含Fly-UI前端VueSVG可视化、Fly后端Spring Boot及无人机客户端Java深度整合Redis缓存、MySQL关系型存储与MongoDB文档数据库配置灵活开箱即用。压缩包共701个文件以299个Java核心业务逻辑文件、109个Vue组件与页面、83个JS交互脚本、87个SVG矢量图标为主辅以SQL建表脚本、YML配置、MD说明文档等整体8.35MB结构清晰模块边界明确。已有90人学习下载提供完整项目说明、若依环境配置手册、白马湖飞行案例文档及多环境启动脚本ry.bat/mvnw.cmd/.env.development便于快速部署、二次开发与系统级调试。1. 为什么无人机飞行管控平台非得用 Java Redis MySQL MongoDB 四件套你手头刚接到一个「低空经济」类政务或园区项目要实时显示上百架无人机的位置、状态、任务进度支持飞手端上报异常、调度中心下发指令、历史轨迹回放、电子围栏越界告警——但一跑起来就卡顿轨迹延迟超3秒告警漏发后台查个7天前的飞行日志要等20秒。这不是代码写得烂而是架构选错了。这个标题里的「Java Redis MySQL MongoDB」不是堆技术名词凑简历而是针对无人机管控场景的刚性组合Java 提供高并发业务逻辑与成熟生态Spring Boot Netty 处理 WebSocket 长连接Redis 扛住每秒万级位置心跳、分布式锁控制指令幂等、布隆过滤器拦截非法起飞请求MySQL 存结构化核心数据——飞手资质、设备注册、任务工单、审批流MongoDB 存非结构化爆炸数据——每架机每秒产生的 GPS 坐标传感器原始值视频帧元数据单日轻松破亿文档。它不面向 hobbyist 玩具机而是给通航公司、城市空中交通UAM试点、大型物流园区落地用的生产级方案。如果你正被「实时性」和「海量时序数据」两座大山压得喘不过气又不想重写 C 或上 Flink/Kafka 这种重型中间件这套四件套就是目前最稳、最易维护、招聘也最方便的解法。2. 四库分工怎么划别让 MongoDB 变成第二个 MySQL2.1 数据分层设计什么进 MySQL什么进 MongoDB什么只存 Redis很多人一上来就把所有数据往 MongoDB 塞结果发现索引膨胀、聚合慢、备份耗时最后又倒腾回 MySQL——这是典型没吃透 NoSQL 的边界。我们按CRUD 频率 查询模式 数据形态三维度切分数据类型示例字段存储位置关键理由强事务型飞手账号余额、设备租赁合同、任务结算单MySQL需 ACID、外键约束、复杂 JOIN如查某飞手名下所有未结清合同高写入时序型每架无人机每秒上报的lat,lng,alt,speed,battery,voltageMongoDB文档天然适配传感器数据结构TTL 索引自动过期旧数据地理空间索引支持“5km 内所有无人机”查询高频读写缓存型实时位置快照最新坐标状态、电子围栏白名单、指令下发状态RedisGEOADD存坐标做半径检索SETNX加分布式锁防重复指令HASH存设备状态避免全量查库临时状态型WebSocket 连接会话、任务执行中间态如“正在上传视频”RedisEXPIRE自动清理断连残留PUB/SUB推送指令到指定飞手终端提示MongoDB 不是 MySQL 的替代品而是它的“减压阀”。把 GPS 轨迹点硬塞进 MySQL 的point字段单表百万行后ORDER BY time DESC LIMIT 100就开始抖动。而 MongoDB 的_id默认是时间戳前缀 ObjectId天然按时间排序查最近100条比 MySQL 快 3 倍以上。2.2 Java 层如何用 Spring Data 统一管理四库访问别写四套 DAO用 Spring Boot 的多数据源 分库注解让业务代码无感切换。核心配置如下# application.yml spring: datasource: mysql: url: jdbc:mysql://localhost:3306/uav_core?useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 mongodb: uri: mongodb://localhost:27017/uav_telemetry redis: host: localhost port: 6379 database: 0// 定义 MongoDB 实体注意 Document 和 Indexed Document(collection flight_telemetry) public class Telemetry { Id private String id; // 自动生成 ObjectId private String droneId; Indexed(background true) // 后台建索引不影响写入 private LocalDateTime timestamp; GeoSpatialIndexed // 地理空间索引支持 $near 查询 private Point location; // Spring Data MongoDB 内置类型 private double altitude; private int battery; }// MongoDB Repository自动实现 CRUD 地理查询 Repository public interface TelemetryRepository extends MongoRepositoryTelemetry, String { // 查某架机最近 N 条轨迹利用 ObjectId 时间特性 ListTelemetry findTopNByDroneIdOrderByTimestampDesc(String droneId, int n); // 查 5km 内所有无人机地理空间查询 ListTelemetry findByLocationNear(Point point, Distance distance); // TTL 索引自动删除 30 天前数据在实体类加 Document(expireAfterSeconds 2592000) }// Redis 操作封装用 Lettuce非 Jedis Component public class DroneRedisService { private final RedisTemplateString, Object redisTemplate; public DroneRedisService(RedisTemplateString, Object redisTemplate) { this.redisTemplate redisTemplate; } // 存实时位置Hash 结构避免每次全量更新 public void saveLatestPosition(String droneId, DronePosition position) { String key drone:pos: droneId; redisTemplate.opsForHash().putAll(key, Map.of(lat, position.getLat(), lng, position.getLng(), alt, position.getAlt(), ts, System.currentTimeMillis())); redisTemplate.expire(key, Duration.ofMinutes(5)); // 5分钟过期防脏数据 } // 获取位置直接从 Hash 读比 String 序列化快 public DronePosition getLatestPosition(String droneId) { String key drone:pos: droneId; MapObject, Object posMap redisTemplate.opsForHash().entries(key); if (posMap.isEmpty()) return null; return new DronePosition( Double.parseDouble(posMap.get(lat).toString()), Double.parseDouble(posMap.get(lng).toString()), Double.parseDouble(posMap.get(alt).toString()) ); } }关键参数说明GeoSpatialIndexed必须配合2dsphere索引否则$near查询无效建索引命令db.flight_telemetry.createIndex({location: 2dsphere})expireAfterSeconds 2592000MongoDB TTL 索引单位是秒30 天 2592000 秒不是毫秒填错就永不过期redisTemplate.expire(key, Duration.ofMinutes(5))Redis 过期时间用Duration别用expire(key, 300)这种老式 API易出精度问题3. Redis 分布式锁怎么防住“指令重复下发”这种致命 bug3.1 为什么普通 setnx 不够看这个翻车现场某次紧急召回指令下发调度员点了两次“立即返航”结果飞手端收到两条指令无人机执行了两次返航逻辑导致电池耗尽迫降——根本原因是第一次指令处理耗时 800ms查数据库发 MQTT还没来得及删锁第二次请求setnx成功拿到锁继续执行两个线程同时操作同一架机状态混乱普通SET key value NX PX 10000只保证“加锁原子性”不保证“锁持有者唯一性”。必须引入Redlock Lua 脚本校验。3.2 生产级锁实现Lettuce Redisson 二选一方案一用 Redisson推荐封装完善!-- pom.xml -- dependency groupIdorg.redisson/groupId artifactIdredisson-spring-boot-starter/artifactId version3.23.3/version /dependencyService public class CommandService { Resource private RedissonClient redissonClient; public boolean sendCommand(String droneId, Command command) { // 锁 key 带业务标识避免不同无人机互斥 RLock lock redissonClient.getLock(cmd:lock: droneId); try { // 等待锁最多 3 秒持有锁 30 秒必须大于最长业务耗时 if (lock.tryLock(3, 30, TimeUnit.SECONDS)) { // 校验指令幂等性查 Redis 是否已存在该指令 ID String cmdKey cmd:executed: droneId; Boolean exists redissonClient.getBucket(cmdKey).isExists(); if (Boolean.TRUE.equals(exists)) { log.warn(Command {} already executed for drone {}, command.getId(), droneId); return false; } // 执行业务逻辑发 MQTT、更新状态等 executeCommand(droneId, command); // 记录已执行TTL 24 小时防锁失效后重复 redissonClient.getBucket(cmdKey).set(command.getId(), Duration.ofHours(24)); return true; } else { log.warn(Failed to acquire lock for drone {}, droneId); return false; } } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } }方案二纯 Lettuce Lua轻量适合已用 Lettuce 的项目// Lua 脚本先检查锁是否存在且属于当前线程再删除防误删 private static final String UNLOCK_SCRIPT if redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end; public boolean safeUnlock(String lockKey, String requestId) { DefaultRedisScriptLong script new DefaultRedisScript(); script.setScriptText(UNLOCK_SCRIPT); script.setResultType(Long.class); return redisTemplate.execute(script, Collections.singletonList(lockKey), requestId) 1L; }参数陷阱tryLock(3, 30, TimeUnit.SECONDS)第一个 3 是等待时间第二个 30 是锁持有时间。持有时间必须 最长业务耗时 网络抖动余量否则锁自动释放其他线程趁虚而入cmd:executed:xxx的 TTL 设为 24 小时而非锁的 30 秒因为锁只是防并发TTL 是防极端情况如服务宕机未删锁下的重复执行4. MongoDB 地理空间查询为什么总查不准三个坑踩到就是线上事故4.1 坐标存反了经度在前纬度在后不是 lat/lngMongoDB 的Point要求[longitude, latitude]x,y而 Java 的org.springframework.data.geo.Point构造函数是new Point(lat, lng)——顺序相反// ❌ 错误把纬度当 x经度当 y查出来全是错的 new Point(39.9042, 116.4074); // 北京坐标但存成 [39.9042, 116.4074] // ✅ 正确显式用经纬度常量避免脑补 new Point(116.4074, 39.9042); // [lng, lat] // 或用 builder更安全 Point.of(116.4074, 39.9042);血泪经验上线前用db.flight_telemetry.find().limit(1)看 raw 数据确认location字段是[116.4074, 39.9042]格式不是{lat: 39.9042, lng: 116.4074}。后者无法用$near。4.2 索引没建对2dsphere 不是 2d别用错类型// ❌ 错误2d 索引只支持平面坐标不支持球面距离计算 db.flight_telemetry.createIndex({location: 2d}) // ✅ 正确2dsphere 支持地球曲率返回真实公里数 db.flight_telemetry.createIndex({location: 2dsphere})验证索引是否生效// 查看执行计划确保 stage 是 GEO_NEAR db.flight_telemetry.find({ location: { $near: { $geometry: { type: Point, coordinates: [116.4074, 39.9042] }, $maxDistance: 5000 // 米 } } }).explain(executionStats)如果executionStats.executionStages.stage是IXSCAN而非GEO_NEAR说明索引没命中。4.3 距离单位混淆$maxDistance 是米不是公里// ❌ 错误以为 5 是 5 公里实际是 5 米查不到任何数据 Query query Query.query(Criteria.where(location) .near(new Point(116.4074, 39.9042)) .maxDistance(5)); // 单位米 // ✅ 正确5 公里 5000 米 Query query Query.query(Criteria.where(location) .near(new Point(116.4074, 39.9042)) .maxDistance(5000));避坑 / 常见问题 / 排查现象findByLocationNear()返回空列表但find()能查到数据原因location字段存的是{lat: xx, lng: yy}对象不是[lng, lat]数组或索引类型是2d不是2dsphere解决用db.flight_telemetry.findOne()看原始数据格式运行db.flight_telemetry.getIndexes()确认索引类型现象地理查询响应慢explain()显示COLLSCAN原因没建2dsphere索引或索引建在错误字段如建在position.lng上而非整个location对象解决db.flight_telemetry.createIndex({location: 2dsphere})确保字段名完全匹配实体类Field(location)现象$near查询报错geoNear requires location data原因集合里混存了location: null或location: {}的脏数据解决先清理db.flight_telemetry.deleteMany({location: {$exists: false}})再建索引现象Java 端Point.of(lng, lat)传参后MongoDB 里存成{x: lng, y: lat}原因Spring Data MongoDB 版本 3.0默认用LegacyPoint需显式配置解决升级到 Spring Data MongoDB 3.6或在application.yml加spring.data.mongodb.grid-fs.database禁用 GridFS现象maxDistance设 1000但返回结果里有距离 2km 的点原因$near默认按球面距离算但若坐标系不是 WGS84EPSG:4326结果失真解决确认所有坐标输入都是 WGS84 经纬度MongoDB 3.6 默认支持无需额外配置5. MySQL 怎么扛住“飞行任务审批流”这种强关联查询5.1 表结构设计别让一张 task 表包打天下无人机任务不是简单 CRUD涉及飞手、设备、空域、气象、审批人多角色流转。常见错误是把所有字段塞进uav_task表-- ❌ 反模式宽表JOIN 多、更新锁表、扩展性差 CREATE TABLE uav_task ( id BIGINT PRIMARY KEY, drone_id VARCHAR(32), pilot_id VARCHAR(32), start_time DATETIME, end_time DATETIME, status ENUM(draft,approved,running,completed,rejected), approver_id VARCHAR(32), -- 审批人 approve_time DATETIME, -- 审批时间 reject_reason TEXT, -- 驳回原因 airspace_id VARCHAR(32), -- 空域编号 weather_condition VARCHAR(32) -- 天气 );正确分拆-- ✅ 主任务表核心状态高频更新 CREATE TABLE uav_task ( id BIGINT PRIMARY KEY AUTO_INCREMENT, drone_id VARCHAR(32) NOT NULL, pilot_id VARCHAR(32) NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status TINYINT NOT NULL DEFAULT 0, -- 0draft,1approved... created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, INDEX idx_status_time (status, updated_at) ); -- ✅ 审批记录表无限追加不锁主表 CREATE TABLE uav_task_approval ( id BIGINT PRIMARY KEY AUTO_INCREMENT, task_id BIGINT NOT NULL, approver_id VARCHAR(32) NOT NULL, status TINYINT NOT NULL, -- 1approved, -1rejected comment TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_task_id (task_id), FOREIGN KEY (task_id) REFERENCES uav_task(id) ON DELETE CASCADE ); -- ✅ 空域申请表独立生命周期 CREATE TABLE uav_airspace_request ( id BIGINT PRIMARY KEY AUTO_INCREMENT, task_id BIGINT NOT NULL, airspace_code VARCHAR(64) NOT NULL, valid_from DATETIME NOT NULL, valid_to DATETIME NOT NULL, status TINYINT DEFAULT 0, -- 0pending,1granted... INDEX idx_task_id (task_id), FOREIGN KEY (task_id) REFERENCES uav_task(id) ON DELETE CASCADE );优势更新审批状态只写uav_task_approvaluav_task表几乎不锁查“某飞手所有待审批任务”SELECT t.* FROM uav_task t JOIN uav_task_approval a ON t.ida.task_id WHERE a.approver_idxxx AND a.status0走idx_task_id索引空域数据可单独归档不影响主任务表性能5.2 索引优化覆盖索引干掉回表查“某设备最近 10 个已完成任务”原 SQLSELECT id, start_time, end_time, status FROM uav_task WHERE drone_id DJI_001 AND status 2 ORDER BY end_time DESC LIMIT 10;若只有INDEX idx_drone_status (drone_id, status)MySQL 会先用索引找到 100 行再回表取end_time排序——IO 拉满。改成覆盖索引-- ✅ 包含 ORDER BY 字段避免 filesort ALTER TABLE uav_task ADD INDEX idx_drone_status_endtime (drone_id, status, end_time);验证执行计划EXPLAIN中Extra列应为Using index而非Using index condition; Using filesort。5.3 分页优化用游标分页替代 OFFSET查第 10000 页任务LIMIT 99990, 10MySQL 要扫描前 99990 行。生产环境必须用游标// ✅ 游标分页用上一页最后一条的 end_time 作为条件 public ListTaskSummary getTasksAfterTime(String droneId, LocalDateTime cursorTime, int size) { return taskMapper.selectAfterTime(droneId, cursorTime, size); } // XML mapper select idselectAfterTime resultTypeTaskSummary SELECT id, start_time, end_time, status FROM uav_task WHERE drone_id #{droneId} AND status 2 AND end_time #{cursorTime} -- 关键用时间戳做游标 ORDER BY end_time DESC LIMIT #{size} /select注意cursorTime必须是上一页最后一条的end_time且该字段有索引。若同时间有多条加id二级排序防漏数据。6. 一套能跑通的最小验证脚本5 分钟看到轨迹在地图上动起来别等全部功能做完才验证。用最简路径跑通“无人机上报 → Redis 缓存 → MongoDB 落库 → Web 页面展示”证明架构可行。6.1 准备工作三库一键启动Docker# 启动 MySQL带初始化脚本 docker run -d \ --name uav-mysql \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD123456 \ -v $(pwd)/mysql-init:/docker-entrypoint-initdb.d \ -d mysql:8.0 # 启动 MongoDB开启地理索引 docker run -d \ --name uav-mongo \ -p 27017:27017 \ -v $(pwd)/mongo-data:/data/db \ mongo:6.0 --bind_ip_all # 启动 Redis docker run -d \ --name uav-redis \ -p 6379:6379 \ redis:7-alpinemysql-init/01-create-tables.sqlCREATE DATABASE IF NOT EXISTS uav_core CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE uav_core; CREATE TABLE uav_drone ( id VARCHAR(32) PRIMARY KEY, name VARCHAR(64), model VARCHAR(64) );6.2 模拟无人机上报Python 脚本# simulate_drone.py import time import random import requests import json # 模拟 3 架无人机循环上报 drones [ {id: DJI_001, lat: 39.9042, lng: 116.4074}, {id: DJI_002, lat: 39.9050, lng: 116.4080}, {id: DJI_003, lat: 39.9035, lng: 116.4065} ] while True: for drone in drones: # 微调坐标模拟移动 lat drone[lat] random.uniform(-0.0001, 0.0001) lng drone[lng] random.uniform(-0.0001, 0.0001) # 上报到 Java 后端假设接口 /api/v1/telemetry payload { droneId: drone[id], location: {lng: lng, lat: lat}, altitude: random.randint(50, 120), battery: random.randint(70, 100), timestamp: int(time.time() * 1000) } try: requests.post(http://localhost:8080/api/v1/telemetry, jsonpayload, timeout2) print(fSent {drone[id]} - {lat:.6f}, {lng:.6f}) except Exception as e: print(fFailed: {e}) time.sleep(1) # 每秒上报一次6.3 Java 后端接收入口Spring Boot ControllerRestController RequestMapping(/api/v1) public class TelemetryController { Autowired private TelemetryService telemetryService; PostMapping(/telemetry) public ResponseEntityString receiveTelemetry(RequestBody TelemetryRequest request) { // 1. 存 Redis实时位置 droneRedisService.saveLatestPosition(request.getDroneId(), new DronePosition(request.getLocation().getLat(), request.getLocation().getLng(), request.getAltitude())); // 2. 存 MongoDB原始轨迹 Telemetry telemetry new Telemetry(); telemetry.setDroneId(request.getDroneId()); telemetry.setTimestamp(LocalDateTime.now()); telemetry.setLocation(new Point(request.getLocation().getLng(), request.getLocation().getLat())); telemetry.setAltitude(request.getAltitude()); telemetry.setBattery(request.getBattery()); telemetryRepository.save(telemetry); return ResponseEntity.ok(OK); } }6.4 前端简易地图展示HTML Leaflet!DOCTYPE html html head link relstylesheet hrefhttps://unpkg.com/leaflet1.9.4/dist/leaflet.css / /head body div idmap styleheight: 500px;/div script srchttps://unpkg.com/leaflet1.9.4/dist/leaflet.js/script script const map L.map(map).setView([39.9042, 116.4074], 13); L.tileLayer(https://{a-d}.tile.openstreetmap.org/{z}/{x}/{y}.png).addTo(map); // 每 2 秒拉一次 Redis 实时位置 setInterval(() { fetch(/api/v1/position/latest) .then(r r.json()) .then(data { // 清空旧标记 map.eachLayer(l l instanceof L.Marker map.removeLayer(l)); // 添加新标记 data.forEach(d { L.marker([d.lat, d.lng]) .bindPopup(Drone: ${d.id}brBattery: ${d.battery}%) .addTo(map); }); }); }, 2000); /script /body /htmlJava 后端提供/api/v1/position/latest接口从 Redis 读取所有drone:pos:*的 Hash 数据组装成 JSON 返回。跑通标志执行python simulate_drone.py打开 HTML 页面看到三个小图标在地图上缓慢移动docker exec -it uav-mongo mongo进入容器use uav_telemetrydb.flight_telemetry.countDocuments({})返回持续增长的数字docker exec -it uav-redis redis-cli keys drone:pos:*看到三个 key这 5 分钟验证比写 1000 行业务逻辑更能建立信心。很多团队卡在“不知道架构对不对”其实只要这个环路通了后面加审批、告警、回放都是锦上添花。我带过的项目里80% 的延期都源于早期没做这个最小闭环验证等到联调才发现 Redis 连不上、MongoDB 地理索引没生效、Java 端坐标存反——全在测试环境暴露而不是上线后。现在我的习惯是任何新项目第一天下午必须跑通这个红绿灯脚本上报→存储→展示否则不算开工。希望帮到你。本文还有配套的精品资源点击获取
返回列表