ARTICLE DETAIL

资讯详情

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

无人机飞行管控平台架构:Java+Redis+MySQL+MongoDB四件套实战

无人机飞行管控平台架构:Java+Redis+MySQL+MongoDB四件套实战 简介基于 Java、Redis、MySQL 与 MongoDB 构建的无人机飞行管控平台提供完整项目源码与说明文档适合需要快速搭建无人机监管后台、学习多数据库整合的中高级 Java 开发人员。项目分为前端 fly-ui、后端 Fly 和无人机客户端 client 三个模块覆盖 Web 管理、业务服务与设备端交互并涉及三套数据库的联动读写部署时仅需按配置说明修改数据库地址与密码。包体约 8.35MB共 701 个文件以 299 个 Java 源码、109 个 Vue 页面、83 个 JS 脚本、87 个 SVG 图标及 SQL、YML、Properties 配置文件为主目录层次清楚便于按模块定位与二次开发。还附带若依环境使用手册、项目说明等文档便于理解环境搭建与核心配置。目前已有 90 人浏览学习可作为无人机管控项目二次开发的参考。1. 无人机飞行管控平台为什么需要 Java、Redis、MySQL、MongoDB 四件套深夜值班一条无人机偏离计划航线 800 米的告警推到大屏上。2 秒一条的遥测数据在这几分钟里涌进来位置、高度、电量、姿态、围栏穿越记录。如果这些全塞进 MySQLInnoDB 的行锁和磁盘 IO 会最先撑不住如果全塞进 Redis内存账单先教你做人。基于 javaRedismysqlMongoDB 实现的无人机飞行管控平台本质就是按数据的“性格”分层存储MySQL 管飞行计划、人员、设备这类强事务数据MongoDB 管轨迹、遥测这类高频写入的文档数据Redis 管在线状态、最新位置、分布式锁这类低延迟场景。这套组合在共享无人机、低空巡检、飞行审批系统里越来越常见也是我见过改动最小、查问题最顺的存储方案。它解决的核心矛盾是同一份业务数据有的要强一致有的要极高的写入吞吐有的要毫秒级读取单一数据库只能在三者里选一个。下面按我落地过的架构从建表、写代码、排障一路讲到底。2. 数据模型先分层MySQL 管审批、MongoDB 管轨迹、Redis 管状态的字段级拆解2.1 MySQL 里只放强事务数据三张核心表怎么建一张业务表该不该放 MySQL判断标准只有一个这条数据要不要事务、要不要 JOIN、要不要按关系型约束来审批流。飞行计划、设备注册、告警事件这三类必须放 MySQL。无人机遥测则不要放哪怕你看到某些老项目把它做成drone_latest这种单行表上线半年后也会被热点行锁拖死。先看设备表和飞行计划表。设备表负责无人机注册信息sn做全局唯一键业务上所有日志和轨迹都用sn关联而不是自增id因为id在对接外部系统、做数据迁移时不稳定。CREATE TABLE drone_device ( id BIGINT AUTO_INCREMENT PRIMARY KEY, sn VARCHAR(32) NOT NULL COMMENT 机身序列号对接外部系统的主键, model VARCHAR(64) NOT NULL COMMENT 机型型号, owner_id BIGINT NOT NULL COMMENT 归属用户ID, reg_status TINYINT NOT NULL DEFAULT 0 COMMENT 0未审核 1正常 2停用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_sn (sn) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT无人机设备表;飞行计划表是审批流的核心状态机字段status和乐观锁version一起用。审批人在页面上点“通过”后台执行的是UPDATE flight_plan SET status1, versionversion1 WHERE plan_no? AND version?影响行数为 0 就说明计划被并发改过提示用户刷新重试而不是直接覆盖。CREATE TABLE flight_plan ( id BIGINT AUTO_INCREMENT PRIMARY KEY, plan_no VARCHAR(32) NOT NULL COMMENT 计划编号业务幂等键, drone_sn VARCHAR(32) NOT NULL COMMENT 无人机SN, drone_id BIGINT NOT NULL COMMENT 设备表主键, pilot_id BIGINT NOT NULL COMMENT 飞手用户ID, fence_id BIGINT NOT NULL COMMENT 执飞绑定的电子围栏, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待审批 1已通过 2已驳回 3执行中 4已完成 5异常终止, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, KEY idx_drone_time (drone_sn, start_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT飞行计划表;告警事件表是 MySQL 里唯一的“高吞吐”表但它比遥测低两个数量级。只有越界、低电量、限高、通信丢失这类关键事件才落这里便于事后审计和统计报表。普通遥测不落 MySQL这个原则是后面所有设计的地基。2.2 MongoDB 轨迹文档为什么一条遥测一条文档而不是嵌套 listMongoDB 里我见过最典型的错误设计是把一次飞行任务的所有轨迹点塞进一个数组字段比如waypoints: [{ts, lng, lat}, ...]。这种做法看起来很“聚合”但一次 2 小时的任务会产生几千个点文档不断变大一次更新就要重写整个 BSON逼近 16MB 单文档上限后写入直接失败。热搜索词里有人问“MongoDB 怎么查 list 嵌套 list”通常就是踩了这个坑之后来翻文档的。我的做法是一条遥测一条独立文档字段扁平绝不嵌套数组。轨迹回放时按droneId ts排序查询性能远好于切数组。{ droneId: DJI-M300-001, ts: 1735785600000, loc: { type: Point, coordinates: [113.9421, 22.5403] }, alt: 120.5, speed: 8.3, battery: 76, attitude: { yaw: 12.3, pitch: -1.2, roll: 0.5 }, eventType: NORMAL }loc必须存 GeoJSON 格式不能用[lng, lat]裸数组否则后面 2dsphere 地理索引建不上。如果历史数据里已经是嵌套 list查询某个下标位置可以用waypoints.2.alt点号路径条件用$gt但这类代码可读性极差新项目不要走回头路。MongoDB 的 Document 天然无 schema 约束遥测字段将来要加signalLevel、windSpeed不用改表结构这是它承接时序数据最大的优势。2.3 Redis key 设计与数据类型选型别把 Redis 当成纯缓存很多团队的 Redis 只用来做 KV 缓存这在管控平台里是不够的。我按 Redis 数据类型把状态数据拆成五类每个 key 的 TTL 都经过实测调过key 模式数据类型TTL存什么选型理由drone:latest:{sn}Hash30 秒最新位置、高度、速度、电量字段固定Hash 比 String 省内存单 key 更新只改字段drone:onlineZSet永不过期按 score 清理所有在线无人机 snscore最后心跳时间天然支持“在线列表、按时间倒序”清理用 removeRangeByScoredrone:lock:{sn}String3~10 秒设备级分布式锁标记SETNX 原子加锁value 存 UUID 防误删alert:dedup:{sn}Set15 分钟已处理告警 eventId消费端幂等去重SETNX 通过才往下游推drone:geoGEO4 小时所有执行中无人机坐标大屏地图按半径搜附近设备底层也是 ZSetdrone:latest:{sn}的 TTL 定为 30 秒不是随便拍的。无人机心跳间隔一般是 2 秒30 秒意味着连续 15 条遥测没上来就可以判定通信丢失同时 key 能自动过期不需要额外写定时任务清理。如果 TTL 设太长上一轮任务里掉线的无人机坐标会一直残留在大屏上运营那边就会报“幽灵机”问题——这是玄学问题里最好查的一种Redis 里看一眼 key 的剩余生存时间就知道原因。3. Java Spring Boot 接线三套存储配置、上报接口与审批状态机的核心代码3.1 多数据源配置三个客户端各管各的池Java 工程里同时接 MySQL、Redis、MongoDB最忌讳的是在一个 Service 里又写 JdbcTemplate 又写 MongoTemplate连接参数散落各处。我习惯用 Spring Boot 3 的标准配置三套客户端各自独立连接池互不抢占。MySQL 用 HikariCPRedis 用 Lettuce 连接池MongoDB 走官方驱动。spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/drone_platform?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghairewriteBatchedStatementstrue username: drone_app password: change_me hikari: maximum-pool-size: 25 minimum-idle: 5 connection-timeout: 3000 max-lifetime: 1800000 data: redis: host: 127.0.0.1 port: 6379 password: change_me timeout: 2000ms lettuce: pool: max-active: 50 max-idle: 20 min-idle: 5 data: mongodb: uri: mongodb://drone_app:change_me127.0.0.1:27017/drone_platform auto-index-creation: truerewriteBatchedStatementstrue这个参数新手最容易漏。MyBatis 批量插入时没开它会一条一条发 SQL开了之后 MySQL 驱动才会把多条 INSERT 重写成批量语句遥测批量落库性能差好几倍。HikariCP 的maximum-pool-size不是越大越好按“核心业务线程数 × 单线程最多占用连接数”估算25 够用connection-timeout设短一点数据库卡住时应用层能快速失败而不是线程全部挂在获取连接上。MongoDB 的auto-index-creation只在开发环境开生产环境关掉避免启动时扫描全部集合自动建索引把启动时间拖到几分钟。3.2 用一个上报接口演示“一进三出”Redis 更新缓存、MongoDB 追加轨迹、MySQL 只记事件无人机遥测上报是所有业务的入口这个接口的写法决定了平台能抗住多大并发。我的实现是一条遥测进来先更新 Redis 最新状态再插 MongoDB 轨迹最后按需写 MySQL 告警事件。三步不是并列关系而是按“越快越先、越关键越后”排序。RestController RequestMapping(/api/v1) public class TelemetryController { private final StringRedisTemplate redisTemplate; private final MongoTemplate mongoTemplate; private final AlertEventMapper alertEventMapper; public TelemetryController(StringRedisTemplate redisTemplate, MongoTemplate mongoTemplate, AlertEventMapper alertEventMapper) { this.redisTemplate redisTemplate; this.mongoTemplate mongoTemplate; this.alertEventMapper alertEventMapper; } PostMapping(/telemetry) public ResultVoid accept(RequestBody DroneTelemetry telemetry) { // 一进三出先写 Redis再落 Mongo最后过滤写 MySQL refreshLatestCache(telemetry); mongoTemplate.insert(telemetry, telemetry); if (telemetry.hasAlert()) { alertEventMapper.insert(AlertEvent.from(telemetry)); } return Result.ok(); } private void refreshLatestCache(DroneTelemetry telemetry) { String key drone:latest: telemetry.getDroneId(); MapString, String fields new HashMap(); fields.put(alt, String.valueOf(telemetry.getAlt())); fields.put(speed, String.valueOf(telemetry.getSpeed())); fields.put(battery, String.valueOf(telemetry.getBattery())); fields.put(ts, String.valueOf(telemetry.getTs())); redisTemplate.opsForHash().putAll(key, fields); redisTemplate.expire(key, Duration.ofSeconds(30)); } }mongoTemplate.insert是单文档同步写上报频率在每秒几千条以内没问题如果超过这个量接口里不能同步插要改成先扔内存队列或 Redis Stream由消费者攒批后走 MongoTemplate 的 bulkWrite。hasAlert()的判断逻辑放在业务对象里只有越界、低电量这类事件才触发 MySQL 写入这样 MySQL 的写入量大约只有遥测总量的千分之一。3.3 审批通过才解锁飞行MySQL 乐观锁 Redis 分布式锁的双保险飞行计划审批是最能体现“为什么同时要 MySQL 和 Redis”的场景。MySQL 负责状态一致性Redis 负责并发互斥缺一个都会出问题。Transactional(rollbackFor Exception.class) public ResultVoid approve(String planNo, Long approverId) { // 1. Redis 分布式锁防重避免同一计划被两次审批 String lockKey plan:lock: planNo; String lockValue UUID.randomUUID().toString(); Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, lockValue, Duration.ofSeconds(10)); if (!Boolean.TRUE.equals(locked)) { return Result.fail(该计划正在审批中); } try { // 2. MySQL 乐观锁更新状态 int affected flightPlanMapper.conditionalUpdateStatus(planNo, 1); if (affected 0) { return Result.fail(计划状态已变化请刷新后重试); } // 3. 审批通过后再初始化 Mongo 任务索引避免状态先行 mongoTemplate.upsert(new Query(Criteria.where(droneId).is(planNo)), new Update().set(status, APPROVED), FlightTask.class); return Result.ok(); } finally { // 4. 释放锁先比较 value 再删除防止误删其他线程的锁 if (lockValue.equals(redisTemplate.opsForValue().get(lockKey))) { redisTemplate.delete(lockKey); } } }setIfAbsent(lockKey, lockValue, Duration.ofSeconds(10))是原子操作Redis 2.6.12 之后支持在 SETNX 时直接带过期时间不用再调 expire。锁的 value 存 UUID释放前先get比较再delete防止 A 线程超时后 B 线程拿到锁A 的 finally 把 B 的锁删掉。这段 get 加 delete 不是原子的所以我生产代码里会用 Lua 脚本封装但原理就是上面三步。这个接口的Transactional只能保护 MySQL 这一步MongoDB 里的 upsert 不在事务保护范围内挂了怎么办第 5 章专门讲补偿方案。4. 实时监控链路Redis 分布式锁、MongoDB 地理围栏与聚合查询的配合4.1 同一架无人机并发上报会乱序setIfAbsent 实现设备级互斥无人机上行链路不稳定时4G 和数传会同时传一份遥测后台经常在几百毫秒内收到两条相同droneId的报文。如果都写进 MongoDB轨迹回放时会出现时间倒跳大屏上的飞机位置像鬼畜一样闪烁。解决思路是设备级分布式锁同一台无人机的遥测串行处理旧时间戳的包直接丢弃。public void acceptWithOrder(DroneTelemetry telemetry) { String lockKey drone:lock: telemetry.getDroneId(); String lockVal UUID.randomUUID().toString(); Boolean lockOk redisTemplate.opsForValue() .setIfAbsent(lockKey, lockVal, Duration.ofSeconds(3)); if (!Boolean.TRUE.equals(lockOk)) { // 锁被占用说明该设备有包在处理当前请求直接丢弃 return; } try { String lastTs (String) redisTemplate.opsForHash().get(drone:seq: telemetry.getDroneId(), lastTs); if (lastTs ! null telemetry.getTs() Long.parseLong(lastTs)) { return; // 乱序旧包直接丢弃 } redisTemplate.opsForHash().put(drone:seq: telemetry.getDroneId(), lastTs, String.valueOf(telemetry.getTs())); redisTemplate.expire(drone:seq: telemetry.getDroneId(), Duration.ofSeconds(10)); // 继续走 Mongo 写入和 MySQL 告警判断 } finally { if (lockVal.equals(redisTemplate.opsForValue().get(lockKey))) { redisTemplate.delete(lockKey); } } }锁的过期时间设 3 秒因为一次遥测处理链路撑死几十毫秒3 秒足够真出现锁过期也不会丢数据——最坏情况是两条包同时通过时间戳比较还能再兜一层。旧的lastTs存在单独的 Hash 里而不是直接存drone:latest就是为了避免和展示字段混在一起清理逻辑各管各的。丢了那包不可惜无人机 2 秒后还会发下一条状态自然刷新。4.2 禁飞区判断MongoDB 2dsphere 与 $geoWithin 的半径换算地理围栏是管控平台最核心的功能MongoDB 的 2dsphere 索引可以直接做“点在圆内”判断不用把围栏多边形拉到 Java 内存里算。前提是早期就把loc字段存成 GeoJSON 点并且建好索引db.telemetry.createIndex({ loc: 2dsphere }) db.telemetry.createIndex({ droneId: 1, ts: -1 })Java 侧用 Spring Data MongoDB 查询圆内数据半径单位是弧度这是最容易踩的坑。new Circle(point, radius)的第二个参数不是米不是公里是弧度。公里转弧度的公式是公里数 / 6371。public boolean isInsideFence(String droneId, double lng, double lat, int radiusMeter) { Query query new Query(); query.addCriteria(Criteria.where(droneId).is(droneId)); query.addCriteria(Criteria.where(loc).within(new Circle( new org.springframework.data.geo.Point(lng, lat), radiusMeter / 6371000.0 ))); return mongoTemplate.exists(query, telemetry); }实际项目中我一般会在业务半径上额外加 50 米余量。2dsphere 的球面计算在边界上有几米误差如果禁飞区半径刚好压着航线边缘不加余量会出现“明明进去了但没告警”的漏报。漏报比误报严重得多误报可以人工解除漏报意味着监管风险。多边形禁飞区用Criteria.where(loc).within(Polygon.of(...))坐标系要统一用 WGS84高德或百度坐标必须做偏移转换后再入库否则围栏整体偏几百米。4.3 告警通道用 Redis Stream消费端崩溃后能恢复一开始图省事用 Redis Pub/Sub 推告警消费者一崩消息就丢了。后来换成 Redis Stream消费者组配合 pending list节点挂了重启后能接着消费没 ack 的消息。// 生产者告警事件进 Stream而不是直接调 WebSocket redisTemplate.opsForStream().add( StreamRecords.newRecord() .ofObject(new AlertEvent(telemetry.getDroneId(), telemetry.getEventType())) .withStreamKey(stream:alert) ); // 消费者消费者组循环读取 ListMapRecordString, Object, Object records redisTemplate.opsForStream() .read(Consumer.from(alert-group, alert-node-1), StreamReadOptions.empty().count(100).block(Duration.ofSeconds(5)), StreamOffset.create(stream:alert, ReadOffset.lastConsumed()));消费成功后要调用ack消息才会从 pending list 移除处理失败不 ack重启后会重新投递。这个机制跟 Kafka 的 consumer group 类似但不用额外引一套 MQ 组件符合这个项目的存储边界。同一时刻可能有几十条告警要按小时统计聚合直接在 MongoDB 端跑别把原始数据拉到 Java 内存里算Aggregation agg Aggregation.newAggregation( Aggregation.match(Criteria.where(droneId).is(droneId) .and(ts).gte(startTs).lte(endTs)), Aggregation.project(ts), Aggregation.project().and(DateOperators.DateOperatorFactory.dateHour(ts)).as(hour), Aggregation.group(hour).count().as(cnt) ); AggregationResultsDocument results mongoTemplate.aggregate(agg, telemetry, Document.class);$project里把时间戳转成小时字段后面$group按小时计数这个管道在 MongoDB 服务端跑完返回的只有十几个文档网络传输量极小。注意聚合的match阶段一定要带上droneId和ts范围否则全表扫描会把集合扫描到怀疑人生这也是 MongoDB“越查越慢”最常见的开头。5. 无人机飞行管控平台避坑清单5 个实测翻车点与解决记录5.1 现象遥测直落 MySQL夜间任务把主库打崩第一次做管控平台时我把“最新位置”设计成 MySQL 单行表每架无人机一条记录遥测一到就 UPDATE。200 架无人机、2 秒一条QPS 才 100MySQL 的 p99 延迟直接飙到 5 秒报警群从晚上 10 点响到凌晨 2 点。原因所有无人机都在 UPDATE 同一批行的最新位置InnoDB 行锁和插入缓冲被热点行锁放大频繁的 UPDATE 还会让聚簇索引页分裂。解决MySQL 只留关键事件实时位置全部走 Redis Hash轨迹写入 MongoDB 异步批量落库。改成这个结构后同样的并发量MySQL 的 QPS 掉到个位数数据库 CPU 从 80% 降到 5%。血泪经验是遥测这种纯追加的时序数据关系型数据库从底层就不适合别硬扛。5.2 现象Redis 内存被离线设备塞满某次线上检查发现 Redisused_memory从 4G 一路涨到 6GDCS 告警一直没停。查 key 发现drone:online这个 ZSet 里有大量几天前的无人机 snscore 还是当时的最后心跳时间。原因ZSet 没有 TTL离线设备的成员没人清理score 永远留在那里。drone:latest因为有 30 秒 TTL 反而没事drone:online是“永不过期”的设计必须靠代码清理。解决score 存最后心跳时间戳每隔 5 分钟执行一次redisTemplate.opsForZSet().removeRangeByScore(drone:online, 0, now - 10s)把 10 秒前还在线的成员全部清掉。这个清理任务要用scan遍历而不是keys数据量大时keys会阻塞 Redis 单线程。5.3 现象MongoDB 轨迹回放越来越慢项目上线三个月后回放一条 2 小时飞行轨迹前端等 4 秒才出第一帧翻页卡顿。查explain()发现查询走的是COLLSCAN全集合扫描。原因telemetry集合只建了loc的 2dsphere 索引没建droneId ts的复合索引。MongoDB 按单个条件过滤时能命中索引但排序字段ts没有索引参与只能在内存里排序。解决执行db.telemetry.createIndex({droneId:1, ts:-1})复合索引回放查询固定带droneId等值条件和ts范围条件。同时给集合加 TTL 索引ts超过 30 天的轨迹自动清理控制集合总大小。这类时序数据只会越积越多不清理的话再好的索引也扛不住磁盘 IO。5.4 现象重复告警刷屏一次越界事件在 5 分钟内被推送了 8 次大屏告警窗口疯狂弹值班人员直接把音量关了。原因两个环节重复触发。一是无人机断线重传同一个eventId的报文传了两遍二是 Redis Stream 消费者处理完业务但 ack 失败消息重新投递下游 WebSocket 又推了一遍。解决加两道幂等。Redis 侧用alert:dedup:{droneId}这个 SetSETNX eventId成功才允许推 StreamTTL 设为 15 分钟MongoDB 侧在eventId上建唯一索引即使 Stream 重复投递数据库第二道拦截保证只落一条记录。幂等键一定不能用时间戳要用设备生成的全局唯一 ID。5.5 现象审批通过但 Mongo 任务状态没同步起飞前检查被卡审批人点了通过飞行计划在 MySQL 里已经是APPROVED但 MongoDB 的 FlightTask 状态还是INIT飞手起飞前巡检查不到任务计划被卡在“已通过”和“未同步”之间。原因Transactional只保护 MySQLMongoDB 的 upsert 异常后不会回滚 MySQL 的 UPDATE两个库状态割裂。解决不要试图做跨库事务用补偿任务兜底。我加了一张plan_task_outbox本地消息表审批通过时先写 outbox 再更新计划状态定时任务扫描 outbox 里未确认的记录重放 Mongo upsert成功后标记已确认。这个方案牺牲了一点实时性但能保证最终一致。跨库事务是分布式系统的伪需求能用状态机加补偿就别上分布式事务中间件。6. 端到端验证用一条异常飞行事件把三套存储对上账6.1 模拟 17 条遥测制造一次越界验证整个链路最直接的办法是脚本模拟一架无人机从正常巡航到进入禁飞区的过程。第 1~10 条在正常位置第 11~17 条坐标偏离到围栏外eventType从 NORMAL 变为 FENCE_BREACH。#!/bin/bash DRONE_IDDJI-M300-001 BASE_TIME$(date %s%3N) for i in $(seq 1 17); do TS$((BASE_TIME i * 2000)) if [ $i -ge 11 ]; then LNG113.9480; LAT22.5370; EVENTFENCE_BREACH else LNG113.9421; LAT22.5403; EVENTNORMAL fi curl -s -X POST http://127.0.0.1:8080/api/v1/telemetry \ -H Content-Type: application/json \ -d {\droneId\:\$DRONE_ID\,\ts\:$TS,\lng\:$LNG,\lat\:$LAT,\alt\:120,\speed\:8,\battery\:80,\eventType\:\$EVENT\} \ /dev/null sleep 1 done脚本跑完后按三套存储分别对账。Redis 查最新位置字段MongoDB 查轨迹条数和最后一条时间戳MySQL 查告警事件表是不是刚好 7 条、且时间戳连续。对账是验证这套架构最值钱的一步三条链路的数据对上了才能说明没有丢数据、没有乱序、没有重复。redis-cli HGETALL drone:latest:DJI-M300-001 mongosh --quiet --eval db.telemetry.countDocuments({droneId:DJI-M300-001}) mysql -h127.0.0.1 -udrone_app -p drone_platform \ -e SELECT * FROM alert_event WHERE drone_snDJI-M300-001 ORDER BY event_time DESC LIMIT 7;6.2 回放验证与最终对账最后一步是时间线回放。按ts升序查出 17 条轨迹在页面上逐帧播放第 11 帧开始位置跳变同时告警面板出现一条 FENCE_BREACH播放到第 17 帧后轨迹停在异常区域。这套验证做完平台的核心链路才算真正跑通。我在每一次改动后都会跑一遍这个流程尤其是改过索引、调过连接池参数之后。最近一次调大 HikariCP 连接池跑完发现 Redis 的lastTs偶尔落后于 Mongo 里的最大ts查下来是连接池超时导致部分请求走了重试时间戳覆盖顺序乱了。这种问题只有端到端对账才能暴露单测覆盖不到。希望这套从建模到验证的思路能帮到你少走我当年那些弯路。本文还有配套的精品资源点击获取
返回列表