更新redis的使用和数据汇总性能优化
This commit is contained in:
+11
@@ -9,6 +9,8 @@ import com.weather.modules.weather.dailydata.vo.WeatherExcelVO;
|
||||
import com.weather.modules.security.user.SecurityUser;
|
||||
import com.weather.modules.security.user.UserContextHolder;
|
||||
import com.weather.modules.security.user.UserDetail;
|
||||
import com.weather.common.redis.RedisKeys;
|
||||
import com.weather.common.redis.RedisUtils;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -27,6 +29,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
public class WeatherDataImportManager {
|
||||
|
||||
private final WeatherDailyDataService weatherDailyDataService;
|
||||
private final RedisUtils redisUtils;
|
||||
private final Map<String, ImportProgress> tasks = new ConcurrentHashMap<>();
|
||||
|
||||
public String submitImport(MultipartFile file) throws IOException {
|
||||
@@ -82,6 +85,14 @@ public class WeatherDataImportManager {
|
||||
|
||||
progress.setStatus("COMPLETED");
|
||||
log.info("导入任务 {} 完成,共导入 {} 行", taskId, progress.getProcessedRows().get());
|
||||
|
||||
// 导入完成后清除天气汇总缓存,等待下次定时任务刷新
|
||||
try {
|
||||
redisUtils.deleteByPattern(RedisKeys.getWeatherSummarizePattern());
|
||||
log.info("已清除天气汇总缓存");
|
||||
} catch (Exception e) {
|
||||
log.warn("清除天气汇总缓存失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
public ImportProgress getProgress(String taskId) {
|
||||
|
||||
+16
-1
@@ -1,5 +1,6 @@
|
||||
package com.weather.modules.weather.dailydata.dao;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Constants;
|
||||
import com.weather.common.dao.BaseDao;
|
||||
@@ -18,7 +19,21 @@ import java.util.List;
|
||||
@Mapper
|
||||
public interface WeatherDailyDataDao extends BaseDao<WeatherDailyDataEntity> {
|
||||
|
||||
List<WeatherDailyDataEntity> selectSummarizeList(@Param(Constants.WRAPPER) QueryWrapper<WeatherDailyDataEntity> wrapper);
|
||||
List<WeatherDailyDataEntity> selectSummarizeList(@Param(Constants.WRAPPER) LambdaQueryWrapper<WeatherDailyDataEntity> wrapper);
|
||||
|
||||
/**
|
||||
* 按站点 + 月-日 + 年份范围查询汇总数据
|
||||
* @param stationIds 站点 ID 列表
|
||||
* @param month 月份 1-12
|
||||
* @param day 日 1-31
|
||||
* @param startYear 起始年份
|
||||
* @param endYear 结束年份
|
||||
*/
|
||||
List<WeatherDailyDataEntity> selectSummarizeByMonthDay(@Param("stationIds") List<Long> stationIds,
|
||||
@Param("month") int month,
|
||||
@Param("day") int day,
|
||||
@Param("startYear") int startYear,
|
||||
@Param("endYear") int endYear);
|
||||
|
||||
int insertBatchMultiRow(@Param("list") List<WeatherDailyDataEntity> list);
|
||||
}
|
||||
+89
-9
@@ -3,11 +3,15 @@ package com.weather.modules.weather.dailydata.service.impl;
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.weather.common.constant.Constant;
|
||||
import com.weather.common.page.PageData;
|
||||
import com.weather.common.redis.RedisKeys;
|
||||
import com.weather.common.redis.RedisUtils;
|
||||
import com.weather.common.service.impl.CrudServiceImpl;
|
||||
import com.weather.common.utils.ConvertUtils;
|
||||
import com.weather.common.utils.TimeUtils;
|
||||
@@ -23,14 +27,18 @@ import com.weather.modules.weather.dailydata.service.WeatherDailyDataService;
|
||||
import com.weather.modules.weather.station.dao.WeatherStationDao;
|
||||
import com.weather.modules.weather.station.entity.WeatherStationEntity;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.time.ZoneId;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@AllArgsConstructor
|
||||
public class WeatherDailyDataServiceImpl extends CrudServiceImpl<WeatherDailyDataDao, WeatherDailyDataEntity, WeatherDailyDataDto> implements WeatherDailyDataService {
|
||||
@@ -38,6 +46,7 @@ public class WeatherDailyDataServiceImpl extends CrudServiceImpl<WeatherDailyDat
|
||||
private final SysDeptService sysDeptService;
|
||||
private final WeatherStationDao weatherStationDao;
|
||||
private final WeatherDailyDataDao weatherDailyDataDao;
|
||||
private final RedisUtils redisUtils;
|
||||
|
||||
@Override
|
||||
public PageData<WeatherDailyDataDto> page(Map<String, Object> params) {
|
||||
@@ -61,7 +70,7 @@ public class WeatherDailyDataServiceImpl extends CrudServiceImpl<WeatherDailyDat
|
||||
|
||||
private List<Long> getPrioritizedStationIds() {
|
||||
UserDetail user = SecurityUser.getUser();
|
||||
if (user == null || user.getDeptId() == null) return Collections.emptyList();
|
||||
if (ObjectUtil.isEmpty(user) || user.getDeptId() == null) return Collections.emptyList();
|
||||
List<Long> deptIds = sysDeptService.getSubDeptIdList(user.getDeptId());
|
||||
if (deptIds.isEmpty()) return Collections.emptyList();
|
||||
List<WeatherStationEntity> stations = weatherStationDao.selectList(
|
||||
@@ -70,8 +79,11 @@ public class WeatherDailyDataServiceImpl extends CrudServiceImpl<WeatherDailyDat
|
||||
.in("dept_id", deptIds));
|
||||
return stations.stream()
|
||||
.map(s -> {
|
||||
try { return Long.valueOf(s.getStationCode()); }
|
||||
catch (NumberFormatException e) { return null; }
|
||||
try {
|
||||
return Long.valueOf(s.getStationCode());
|
||||
} catch (NumberFormatException e) {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter(Objects::nonNull)
|
||||
.collect(Collectors.toList());
|
||||
@@ -110,7 +122,7 @@ public class WeatherDailyDataServiceImpl extends CrudServiceImpl<WeatherDailyDat
|
||||
.map(WeatherDailyDataEntity::getStationId)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
.toList();
|
||||
Map<Long, Long> stationDeptMap = new HashMap<>();
|
||||
if (!stationIds.isEmpty()) {
|
||||
List<String> stationCodeList = stationIds.stream().map(String::valueOf).collect(Collectors.toList());
|
||||
@@ -134,7 +146,8 @@ public class WeatherDailyDataServiceImpl extends CrudServiceImpl<WeatherDailyDat
|
||||
entity.setUpdateDate(now);
|
||||
}
|
||||
|
||||
weatherDailyDataDao.insertBatchMultiRow(list);
|
||||
int successCount = weatherDailyDataDao.insertBatchMultiRow(list);
|
||||
log.info("成功插入{}条数据",successCount);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -169,11 +182,78 @@ public class WeatherDailyDataServiceImpl extends CrudServiceImpl<WeatherDailyDat
|
||||
endTime = TimeUtils.convertToLocalDateTime(queryDto.getEndtDate());
|
||||
}
|
||||
|
||||
QueryWrapper<WeatherDailyDataEntity> wrapper = new QueryWrapper<>();
|
||||
wrapper.in("station_id", stationIds)
|
||||
.between("observe_date", startTime, endTime);
|
||||
// 尝试从缓存获取(仅"历年同月同日"查询可命中缓存)
|
||||
List<DailyWeatherSummarizeDto> cached = getCachedSummarize(stationIds, startTime, endTime);
|
||||
if (cached != null) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
List<WeatherDailyDataEntity> list = weatherDailyDataDao.selectSummarizeList(wrapper);
|
||||
// 缓存未命中,查询数据库
|
||||
return querySummarizeFromDb(stationIds, startTime, endTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试从 Redis 缓存获取汇总数据
|
||||
*
|
||||
* @return 缓存命中时返回数据,未命中返回 null
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<DailyWeatherSummarizeDto> getCachedSummarize(
|
||||
List<Long> stationIds, LocalDate start, LocalDate end) {
|
||||
|
||||
// 仅当月-日相同的跨年查询可命中缓存
|
||||
if (start.getMonth() != end.getMonth() || start.getDayOfMonth() != end.getDayOfMonth()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String key = RedisKeys.getWeatherSummarizeKey(end.getMonthValue(), end.getDayOfMonth());
|
||||
Object cached = redisUtils.get(key);
|
||||
if (cached == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, List<DailyWeatherSummarizeDto>> grouped;
|
||||
try {
|
||||
grouped = (Map<String, List<DailyWeatherSummarizeDto>>) cached;
|
||||
} catch (ClassCastException e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 验证所有请求站点都在缓存中,任一缺失则降级到 DB
|
||||
for (Long sid : stationIds) {
|
||||
if (!grouped.containsKey(String.valueOf(sid))) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 从缓存中按站点过滤 + 限定年份范围(Key 为 String 避免 JSON 反序列化时 Long/Integer 类型丢失)
|
||||
return stationIds.stream()
|
||||
.flatMap(sid -> {
|
||||
List<DailyWeatherSummarizeDto> stationData = grouped.get(String.valueOf(sid));
|
||||
return stationData != null ? stationData.stream() : Stream.empty();
|
||||
})
|
||||
.filter(d -> {
|
||||
LocalDate obsDate = d.getObserveDate().toInstant()
|
||||
.atZone(ZoneId.systemDefault())
|
||||
.toLocalDate();
|
||||
return !obsDate.isBefore(start) && !obsDate.isAfter(end);
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 直接查询数据库获取汇总数据
|
||||
* <p>用 MONTH/DAY 定位具体日期 + YEAR BETWEEN 限定年份范围,避免日期 BETWEEN 全量问题</p>
|
||||
*/
|
||||
private List<DailyWeatherSummarizeDto> querySummarizeFromDb(
|
||||
List<Long> stationIds, LocalDate startTime, LocalDate endTime) {
|
||||
|
||||
List<WeatherDailyDataEntity> list = weatherDailyDataDao.selectSummarizeByMonthDay(
|
||||
stationIds,
|
||||
startTime.getMonthValue(),
|
||||
startTime.getDayOfMonth(),
|
||||
startTime.getYear(),
|
||||
endTime.getYear());
|
||||
|
||||
return BeanUtil.copyToList(list, DailyWeatherSummarizeDto.class);
|
||||
}
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package com.weather.modules.weather.dailydata.task;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.weather.common.redis.RedisKeys;
|
||||
import com.weather.common.redis.RedisUtils;
|
||||
import com.weather.modules.job.task.ITask;
|
||||
import com.weather.modules.weather.dailydata.dao.WeatherDailyDataDao;
|
||||
import com.weather.modules.weather.dailydata.dto.DailyWeatherSummarizeDto;
|
||||
import com.weather.modules.weather.dailydata.entity.WeatherDailyDataEntity;
|
||||
import jakarta.annotation.Resource;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 天气汇总缓存预热任务
|
||||
* <p>
|
||||
* 每日凌晨 1:00 执行,将历年当月-当日的气象数据
|
||||
* 按站点分组写入 Redis,供 export-sum 接口查询。
|
||||
*
|
||||
* @author 2333 123
|
||||
* @since 1.0.0 2026-06-24
|
||||
*/
|
||||
@Slf4j
|
||||
@Component("weatherSummarizeCacheTask")
|
||||
public class WeatherSummarizeCacheTask implements ITask {
|
||||
|
||||
@Resource
|
||||
private WeatherDailyDataDao weatherDailyDataDao;
|
||||
|
||||
@Resource
|
||||
private RedisUtils redisUtils;
|
||||
|
||||
@Override
|
||||
public void run(String params) {
|
||||
LocalDate today = LocalDate.now();
|
||||
int month = today.getMonthValue();
|
||||
int day = today.getDayOfMonth();
|
||||
|
||||
log.info("开始刷新天气汇总缓存,日期: {}-{}", month, day);
|
||||
|
||||
try {
|
||||
LambdaQueryWrapper<WeatherDailyDataEntity> wrapper = new LambdaQueryWrapper<>();
|
||||
wrapper.apply("MONTH(observe_date) = {0} AND DAY(observe_date) = {1}", month, day);
|
||||
|
||||
List<WeatherDailyDataEntity> list = weatherDailyDataDao.selectSummarizeList(wrapper);
|
||||
|
||||
Map<String, List<DailyWeatherSummarizeDto>> grouped = list.stream()
|
||||
.map(e -> BeanUtil.copyProperties(e, DailyWeatherSummarizeDto.class))
|
||||
.collect(Collectors.groupingBy(dto -> String.valueOf(dto.getStationId())));
|
||||
|
||||
String key = RedisKeys.getWeatherSummarizeKey(month, day);
|
||||
redisUtils.set(key, grouped, RedisUtils.NOT_EXPIRE);
|
||||
|
||||
log.info("天气汇总缓存刷新完成,共 {} 条记录,{} 个站点", list.size(), grouped.size());
|
||||
} catch (Exception e) {
|
||||
log.error("天气汇总缓存刷新失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
-4
@@ -7,7 +7,9 @@ import com.weather.modules.sys.entity.SysDeptEntity;
|
||||
import com.weather.modules.sys.service.SysParamsService;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.CommandLineRunner;
|
||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -15,18 +17,33 @@ import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@AllArgsConstructor
|
||||
public class FileScanStartupRunner implements CommandLineRunner {
|
||||
public class FileScanStartupRunner {
|
||||
|
||||
/**
|
||||
* 启动后延迟扫描时间(秒),给应用留出充分的初始化时间
|
||||
*/
|
||||
private static final long STARTUP_DELAY_SECONDS = 30;
|
||||
|
||||
private final SysParamsService sysParamsService;
|
||||
private final SysDeptDao sysDeptDao;
|
||||
private final FileWatchServiceManager fileWatchServiceManager;
|
||||
|
||||
@Override
|
||||
public void run(String... args) {
|
||||
@Async
|
||||
@EventListener(ApplicationReadyEvent.class)
|
||||
public void onApplicationReady() {
|
||||
log.info("应用已启动,{} 秒后开始文件扫描目录初始化...", STARTUP_DELAY_SECONDS);
|
||||
try {
|
||||
TimeUnit.SECONDS.sleep(STARTUP_DELAY_SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
log.warn("文件扫描延迟等待被中断,直接开始初始化");
|
||||
}
|
||||
|
||||
String rootPath = sysParamsService.getValue(Constant.FILE_SCAN_ROOT_PATH);
|
||||
if (StrUtil.isBlank(rootPath)) {
|
||||
log.warn("FILE_SCAN_ROOT_PATH 未配置,跳过文件扫描目录初始化");
|
||||
|
||||
Reference in New Issue
Block a user