一、业务场景
统计不同应用,在不同渠道上架后用户的访问量和浏览量。因为数据量很大,所以考虑分表。
数据是我们对接的系统提供,因为我们系统本身没有埋点,不统计用户量。对接的系统,分为两个维度,一个是应用维度,一个是用户维度统计,我们这边只涉及应用维度的数据统计。他们把数据存储到es表内,es也涉及分表,逻辑同数据库分表,过去我们是直接从es取数据,但这样的话两个系统就是强关联关系,需要es相同,现在改成需要es中取的数据由他们系统提供接口,我们再通过网关调用他们的接口获取。
我们自身是把对方提供的数据存到数据库中,下方代码也是展示这些数据存取,因为数据是从其他系统获取,所以由其他系统提供一个数据的接口,我们把这个系统的地址配置到nacos上,通过读取nacos获取接口的地址。
然后我们通过一个定时任务,获取数据,定时任务轮训的时间为30分钟。还预留了一个接口可以自己输入时间获取数据,避免因各种原因造成的获取数据问题
二、实际代码
protected Result doWork(String s) {
try {
String today = DateUtil.currDay();
EsDataByAppService esDataByAppService = SpringUtil.getBean(EsDataByAppService.class);
esDataByAppService.calEsDataByAppDay(today);
int hour = LocalDateTime.now().getHour();
int min = LocalDateTime.now().getMinute();
//重新统计一遍昨日数据保证数据准确性
if (SysNameConstant.NUM0 == hour && SysNameConstant.NUM30 >= min) {
today = DateUtil.befoDay(1);
esDataByAppService.calEsDataByAppDay(today);
}
} catch (Exception e) {
LOG.error("system error", e);
}
return null;
}
/**
* 计算某天的数据并入库
*
* @param time
*/
public void calEsDataByAppDay(String time) {
try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
SimpleDateFormat sdf2 = new SimpleDateFormat("yyyy-MM");
Calendar c = Calendar.getInstance();
String startTime = time + " 00:00:00";
String endTime = time + " 23:59:59";
Date sdate = sdf.parse(startTime);
Date edate = sdf.parse(endTime);
int hour = LocalDateTime.now().getHour();
int min = LocalDateTime.now().getMinute();
Boolean lasttimes = false;
LOG.debug(time + " es统计数据入库开始 时间: " + LocalDateTime.now());
//每天23点30分以后最后一次统计
if (hour == SysNameConstant.NUM0 && min == SysNameConstant.NUM0) {
lasttimes = true;
if ((time.equals(DateUtil.currDay()))) {
c.setTime(sdate);
c.add(Calendar.DAY_OF_MONTH, -1);
sdate = c.getTime();
c.setTime(edate);
c.add(Calendar.DAY_OF_MONTH, -1);
edate = c.getTime();
}
} else if (hour == StatisticsConstant.NUM23 && min > StatisticsConstant.NUM30) {
lasttimes = true;
} else {
LOG.info("********");
}
int year = DateUtil.getYear(sdate);
int month = DateUtil.getMonth(sdate);
String tableName;
if (month <= SysNameConstant.NUM6) {
tableName = year + "_1";
} else {
tableName = year + "_2";
}
addDateByApp(sdate, edate, startTime, tableName);
//最后一次更新当天数据的同时间更新当月和当年数据
if (lasttimes) {
this.calibrationStatisticsMonth(sdf2.format(sdate));
this.calibrationStatisticsYear(String.valueOf(year));
}
LOG.debug(time + " es统计数据入库结束");
} catch (Exception e) {
LOG.error("服务器错误!", e);
}
}
public void addDateByApp(Date start, Date end, String startTime, String tableName) {
String interfaceAddress = properties.getInterfaceAddress();
String url = interfaceAddress + "/interface/findapptodate";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
HttpSender httpSender = null;
List<RegionAndChannelAndTerminal> findAppAccessByConditions = new ArrayList<>(Constants.MAP_INIT_SIZE);
try{
httpSender = HttpSender.getInstancePost(url);
httpSender.addParam("starttime", sdf.format(start));
httpSender.addParam("endtime", sdf.format(end));
httpSender.execute();
String res = httpSender.getResult();
JSONObject jsonObject = JSON.parseObject(res);
JSONObject jsonObjectData = jsonObject.getJSONObject(SystemConstant.DATA);
if(jsonObjectData != null){
JSONArray jsonArray = jsonObjectData.getJSONArray("result");
if(jsonArray != null){
for (int i = 0; i < jsonArray.size(); i++) {
JSONObject jsonObject2 = jsonArray.getJSONObject(i);
RegionAndChannelAndTerminal customObject = JSON.toJavaObject(jsonObject2, RegionAndChannelAndTerminal.class);
findAppAccessByConditions.add(customObject);
}
}
}
}catch (Exception e){
LOG.error("调用获取数据接口失败" + e.getMessage(), e);
}finally {
if (httpSender != null) {
httpSender.close();
}
}
if (CollectionUtils.isNotEmpty(findAppAccessByConditions)) {
//删除旧数据
esDateByAppDao.removeDayData(startTime, tableName);
for (RegionAndChannelAndTerminal appModuleEs : findAppAccessByConditions) {
EsDateByApp esDateByApp = new EsDateByApp();
esDateByApp.setAppid(appModuleEs.getAppid());
esDateByApp.setAppname(appModuleEs.getAppName());
esDateByApp.setPv(appModuleEs.getPv());
esDateByApp.setUv(appModuleEs.getUv());
esDateByApp.setKeyvalue(appModuleEs.getKeyValue());
esDateByApp.setShowtime(startTime);
esDateByApp.setChannel(appModuleEs.getChannel());
esDateByAppDao.saveDateData(esDateByApp, tableName);
}
}
}
三、查询时分表思路
天表按年份月份分表,如2023年前6个月,table2023_1,后六个月table2023_2。
删除旧数据前先判断表是否存在,如果表存在则根据时间和表名删除数据。
存储数据前同样先判断表是否存在,不存在则先创建表,再插入数据。
年表,月表没有分表,在每天最后的一次数据统计的时候,先分组查询数据库天表中的按当年或者当月统计的数据,再删除年表,月表中当年,当月的数据,最后插入。
至于取数据,我们设计是查询时间能任意选,但结束时间-开始时间只能是一年内,因为一年就是三张表联查了,同样我会根据传入时间进行区分,得到年份月份,拼成表,然后判断表是否存在,存在的表则拼接成sql去查询,整体逻辑就是这样。