
MQTT+时序数据库:海量农业传感数据存储、趋势报表实现
大家好,我是「黒漂技术佬」。前两期聊了消息幂等和设备监控,这期聊一个"量变引起质变"的话题------当传感器数据多到MySQL扛不住的时候,怎么办?
一个中型智慧农场,50个大棚,每个大棚5种传感器(温度、湿度、土壤湿度、光照、CO2),每10秒上报一次。来,算笔账:
ini
单条数据 ≈ 100字节
每秒写入 = 50 × 5 = 250条
每天写入 = 250 × 86400 = 2160万条 ≈ 2GB
每年写入 ≈ 80亿条 ≈ 700GB
MySQL单表过亿行之后,连COUNT(*)都能让你怀疑人生。 这不是优化能解决的,是选型问题。你需要时序数据库。
一、为什么是MQTT + InfluxDB?
这不是拍脑袋的组合,是天然匹配:
| 层 | 技术 | 职责 |
|---|---|---|
| 数据接入 | MQTT (EMQX) | 海量设备并发连接、消息路由、Topic过滤 |
| 数据处理 | SpringBoot消费者 | 数据清洗、格式转换、幂等去重、业务逻辑 |
| 数据存储 | InfluxDB | 高性能时序写入、时间窗口聚合、降采样 |
| 数据可视化 | Grafana | 仪表盘、趋势图、告警面板 |
MQTT负责"接得住",InfluxDB负责"存得下",Grafana负责"看得清"------一条龙。
二、完整数据流架构
scss
┌──────────┐ MQTT ┌──────────┐ 消费写入 ┌──────────┐ 查询展示 ┌──────────┐
│ 传感器设备 │ ───────→ │ EMQX集群 │ ───────→ │ SpringBoot│ ───────→ │ InfluxDB │ ←───── │ Grafana │
│ (50大棚) │ QoS 1 │ (Broker) │ Shared │ (消费者) │ 批量写入 │ (时序库) │ Flux │ (可视化) │
└──────────┘ └──────────┘ Sub └──────────┘ └──────────┘ └──────────┘
│
▼
┌──────────┐
│ Redis │ (去重缓存)
└──────────┘
核心链路:传感器 → MQTT Broker → SpringBoot消费 → 批量写入InfluxDB → Grafana出图。MySQL只存设备台账、用户配置等关系型数据,传感器数据归时序库管。
三、InfluxDB存储方案设计
3.1 核心概念快速入门
InfluxDB和MySQL的组织方式完全不同,你得换个脑子:
| MySQL概念 | InfluxDB概念 | 说明 |
|---|---|---|
| Database | Bucket | 数据桶,相当于一个数据库 |
| Table | Measurement | 测量,相当于一张表 |
| Column | Field | 字段,存具体数值(不带索引) |
| Index Column | Tag | 标签,存元数据(带索引,用于WHERE过滤) |
| Row | Point | 数据点,一行记录 |
| - | Timestamp | 时间戳,每一条数据必须有 |
3.2 Measurement设计
推荐方案:统一表结构。所有传感器数据存在同一个Measurement,用tag区分:
java
// 数据点结构
Point point = Point.measurement("sensor_data")
.addTag("farm_id", "farm_01") // 农场
.addTag("greenhouse_id", "gh_03") // 大棚
.addTag("device_id", "dev_001") // 设备
.addTag("sensor_type", "temperature") // 传感器类型
.addField("value", 28.5) // 数值
.addField("unit", "celsius") // 单位(可选)
.time(Instant.now(), WritePrecision.MS);
为什么不按传感器类型建多张表? 统一表方案的好处:
- 管理简单,一个Bucket一张Measurement搞定
- 跨传感器查询方便(比如"同一大棚的温度和湿度")
- Grafana配置统一,变更tag值即可切换传感器类型
分开表的优势是查询时自然隔离,但带来的管理成本远大于收益。
3.3 Tag vs Field------避坑指南
新手最容易犯的错误是把高基数字段放到Tag里:
java
// ❌ 错误:value是连续变化的,放到Tag会导致索引爆炸
.addTag("value", "28.5")
// ✅ 正确:value放到Field,只有枚举/低基数字段放Tag
.addTag("sensor_type", "temperature")
.addField("value", 28.5)
Tag的黄金法则:Tag用于WHERE过滤和GROUP BY分组,Field用于存储实际测量值。Tag基数越低越好(几十到几百种),Field可以是任意变化值。如果把每个不同的温度值都存成Tag,InfluxDB的索引能把你内存吃干抹净。
四、写入优化
4.1 批量积攒写入
传感器数据不要来一条写一条------网络开销和系统调用比你想象的贵:
java
@Service
public class SensorDataWriter {
@Autowired
private WriteApiBlocking writeApi;
private final BlockingQueue<Point> buffer = new LinkedBlockingQueue<>(10000);
@PostConstruct
public void init() {
// 后台线程批量刷盘
ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
scheduler.scheduleAtFixedRate(this::flushBuffer, 10, 10, TimeUnit.SECONDS);
}
/**
* 接收传感器数据,先放缓冲区
*/
public void append(Point point) {
buffer.offer(point);
}
/**
* 积攒1000条或满10秒后批量写入
*/
private void flushBuffer() {
List<Point> batch = new ArrayList<>(1000);
buffer.drainTo(batch, 1000);
if (!batch.isEmpty()) {
try {
writeApi.writePoints(batch);
log.debug("批量写入InfluxDB成功, 数量: {}", batch.size());
} catch (Exception e) {
log.error("批量写入InfluxDB失败", e);
// 失败重试或写死信队列
}
}
}
}
4.2 异步非阻塞写入
生产环境推荐异步写入API------不阻塞MQTT消费线程:
java
// 初始化异步WriteApi
WriteApi asyncWriteApi = influxDBClient.makeWriteApi(
WriteOptions.builder()
.batchSize(5000) // 批量大小
.flushInterval(5000) // 刷新间隔(毫秒)
.retryInterval(1000) // 失败重试间隔
.maxRetries(3) // 最大重试次数
.bufferLimit(100000) // 缓冲区上限
.build()
);
InfluxDB单节点写入性能轻松达到10万点/秒以上,应对中大型农场绰绰有余。
五、数据降采样和保留策略
原始数据全量永久保存既不现实也没必要。温度十秒一条存三年------谁来查?真需要查的时候多半也是看趋势。
5.1 分层存储策略
arduino
原始数据 (10s间隔) → 保留30天 → 聚合到1分钟
1分钟聚合 (avg/max/min) → 保留1年 → 聚合到1小时
1小时聚合 → 保留3年 → 聚合到1天
1天聚合 → 永久保留
5.2 InfluxDB Task自动降采样
javascript
// Flux脚本:每10分钟执行一次,将原始数据聚合为1分钟粒度
option task = {
name: "sensor_downsample_1m",
every: 10m,
}
from(bucket: "agriculture_sensor")
|> range(start: -20m) // 处理最近20分钟的数据
|> filter(fn: (r) => r._measurement == "sensor_data")
|> aggregateWindow(every: 1m, fn: mean) // 按1分钟窗口取均值
|> set(key: "_measurement", value: "sensor_data_1m") // 写入降采样桶
|> to(bucket: "agriculture_sensor_1m")
同理配置1小时和1天的降采样Task。一个完整的保留策略建好后,存储成本大幅下降,查询速度反而更快。
六、趋势报表实现
6.1 日报:过去24小时温湿度变化
javascript
// Flux查询:大棚A3的24小时温度趋势
from(bucket: "agriculture_sensor")
|> range(start: -24h)
|> filter(fn: (r) => r._measurement == "sensor_data")
|> filter(fn: (r) => r.greenhouse_id == "gh_03")
|> filter(fn: (r) => r.sensor_type == "temperature")
|> filter(fn: (r) => r._field == "value")
|> aggregateWindow(every: 10m, fn: mean)
|> yield(name: "temperature_24h")
6.2 周报:每日最高/最低/平均温
javascript
from(bucket: "agriculture_sensor")
|> range(start: -7d)
|> filter(fn: (r) => r._measurement == "sensor_data")
|> filter(fn: (r) => r.sensor_type == "temperature" and r._field == "value")
|> aggregateWindow(every: 1d, fn: max, createEmpty: false)
|> yield(name: "daily_max")
// 同理把max换成min和mean各跑一次,或者一次query多次yield
6.3 月度极值分析
javascript
from(bucket: "agriculture_sensor_1h") // 直接用小时级聚合数据,超快
|> range(start: -30d)
|> filter(fn: (r) => r.sensor_type == "temperature" and r._field == "value")
|> max() // 全月最高温
|> yield(name: "monthly_max")
// 配合 group() 还可以按大棚分组:
// |> group(columns: ["greenhouse_id"])
// |> max()
6.4 SpringBoot端报表接口
java
@RestController
@RequestMapping("/api/report")
public class SensorReportController {
@Autowired
private InfluxDBClient influxDBClient;
@GetMapping("/daily")
public List<DataPoint> dailyReport(
@RequestParam String greenhouseId,
@RequestParam String sensorType,
@RequestParam(defaultValue = "24") int hours) {
String flux = String.format(
"from(bucket: \"agriculture_sensor\") " +
"|> range(start: -%dh) " +
"|> filter(fn: (r) => r.greenhouse_id == \"%s\" and r.sensor_type == \"%s\" and r._field == \"value\") " +
"|> aggregateWindow(every: 10m, fn: mean)",
hours, greenhouseId, sensorType);
QueryApi queryApi = influxDBClient.getQueryApi();
List<DataPoint> result = new ArrayList<>();
queryApi.query(flux, (record) -> {
DataPoint dp = new DataPoint();
dp.setTime(record.getTime());
dp.setValue((Double) record.getValue());
result.add(dp);
});
return result;
}
}
七、大数据量查询优化
7.1 时间范围分区查询
不要一次查一整年的数据作折线图------Grafana前端撑不住,后端也慢:
javascript
// ❌ 查一年10秒粒度的原始数据------找死
range(start: -365d) + aggregateWindow(every: 10s)
// ✅ 根据查询范围自适应聚合粒度
// < 6小时 → 10s粒度
// 6-24小时 → 1m粒度
// 1-7天 → 10m粒度
// > 7天 → 1h粒度
7.2 预聚合是王道
日报周报不要实时从原始数据算------建降采样Task提前算好,报表接口直接查聚合结果,查询延迟从秒级降到毫秒级。
智慧农业的数据量会随着规模扩大而暴涨,但InfluxDB的设计哲学就是"让你存得下、查得快"。MQTT负责数据接入,InfluxDB负责数据归宿,Grafana负责可视化------三者配合,你的农业物联网平台就有了坚实的数据底座。
------ 黒漂技术佬