业务场景
项目使用MongoDB进行监测设备上传数据存储,数据的频率达到了秒/分钟级别,单个设备每月分钟数据达到约60 * 24 * 30=43200,通常一个项目的设备接入量500到1000台,单月数据量2kw~4kw,如果不进行分表存储,随着项目的运行,最终单表数据量过大,业务操作到达性能瓶颈。
方案
粗细粒度数据
非细粒度数据,如小时数据、日数据,对这些数据进行单表数据量估算,在项目生命周期内数据量可以控制在亿级别内,未进行分表,采用时间字段冗余设计,如数据中会有hour,day,month,year等格式化好的时间字段,对小时数据集合按照设备id与day创建组合唯一索引,索引信息如下:
#索引信息
{
"deviceId" : 1,
"day" : 1
}
这样索引量为=小时数据集合总条数/24,大大降低MongoDB的内存开销,我们查询设备编码为device01,时间为2021-08-12 18小时数据时候,根据小时时间得出day=2021-08-12,查询条件如下:
#查询条件
{
"deviceId": "device01", "day": "2021-08-12", "hour" :"2021-08-12 18"
}
查询时候应用到索引,响应速度<10ms
细粒度数据
细粒度数据增长速度快,随着项目的生产运行,单表数据量大会过大,根据参考业务场景估算,月数据量2kw~4kw,年数据量千台设备能控制在5亿内,但是MongoDB有锁库(低版本)和锁表(中版本)的问题,即使有存储文件的分片,在建立索引、查询索引情况下开销极大,所以我们的目标还是把单表数据量控制在亿级以下,同时也可以支撑更多设备项目接入,所以按照月份进行分表存储,具体业务实现如下:
graph TD
协议服务 --> 数据处理 --> 存储服务
- 协议服务 接收设备上传数据,解码,封装,发生带数据处理服务
- 数据处理 对数据进行业务处理,添加时间字段(如分钟数据填加hour、day...)
- 存储服务 按照数据类型对数据进行存储前业务处理,数据目标表,分表实现
分表实现
分表存储
分钟数据按月分表 表名格式 minute_data_yyyyMM 示例:minute_data_202108 分钟数据为json,数据里面包含时间戳ms,代码实现:
/**
* 数据存储(伪代码 参考实现即可)
*/
public class SinkService {
@Autowired
private MongoTemplate mongoTemplate;
public void sinkMinuteData(JSONObject data) {
long ms = data.getLongValue("ms");
String month = FastDateFormat.getInstance("yyyyMM").format(ms);
String subTable = String.join("_", "minute_data", month);
mongoTemplate.getCollection(collectionName).insertOne(new Documet(data));
}
}
分表查询
分表查询,对于精确查询,参考粗粒度数据建立索引,分钟数据使用deviceId及hour建索引,查询条件的时间封装表名称即可,示例如下:
【精确查询】查询站点编码为device01 分钟时间为2021-08-12 00:01的数据
public class QueryDataService {
@Autowired
private MongoTemplate mongoTemplate;
/**
* deviceId 设备id
* ms 分钟对应的时间戳
*/
public JSONObject findOneMinute(String deviceId, long ms) {
String month = FastDateFormat.getInstance("yyyyMM").format(ms);
String subTable = String.join("_", "minute_data", month);
String hour = FastDateFormat.getInstance("yyyy-MM-dd HH").format(ms);
String minute = FastDateFormat.getInstance("yyyy-MM-dd HH:mm").format(ms);
Document condition = new Document();
condition.append("deviceId", deviceId).append("hour", hour).append("minute", minute);
Document fields = new Document().append(Constants.MONGO_ID, 0);
return mongoTemplate.findOne(new BasicQuery(condition, fields), JSONObject.class, subTable);
}
}
查询语句
db.minute_data_202108.find({"deviceId":"device01", "hour":"2021-08-12 00", "minute":"2021-08-12 00:01"},{"_id":0});
【范围查询】查询站点编码为device01,时间范围为2021-07-31 23:59到2021-08-01 00:59
业务分析:分钟数据安装月分表存储,时间范围跨月份时候如何查询最简最优
数据查询应该在业务层面避免大范围查询,或者采取分页查询
- 方案一 分页循环查(控制查询数据量,循环调用精确查询的方法)
public class RangeQueryService {
@Autowired
private QueryDataService queryDataService;
/**
* 循环调用精确查询 时间范围小的情况下可以使用
*/
public List<JSONObject> findRangeMinute(String deviceId, long startMs, long endMs) {
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(startMs);
List<JSONObject> dataList = Lists.newArrayList();
while (calendar.getTimeInMillis() <= endMs) {
JSONObject data = queryDataService.findOneMinute(deviceId, calendar.getTimeInMillis());
calendar.add(Calendar.MINUTE, 1);
if(data != null) {
dataList.add(data);
}
}
return dataList;
}
}
-
方案二 提前处理好表名称,比如这种情况就是会涉及两张表minute_data_202107 minute_data_202108 查询两次即可,具体代码就不上了(ps:如果跨两个月,数据量太大了,实际业务场景不会出现也不应该出现,任何数据库都应该避免单次大范围查询)
总结:分表查询根据自己业务场景进行特定实现即可,提前规划好查询业务,适当结合缓存技术,比如最新分钟数据放入缓存等等,避免所有压力都在MongoDB