MongoDB分表存储查询方案

7,006 阅读2分钟

业务场景

项目使用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