接口性能优化报告

HEMS 慢接口性能优化报告

本报告仅针对附件中提供的 5 个慢接口本身进行优化分析,不涉及架构大改造或大范围重构。


一、接口耗时总览

接口 耗时 接口路径 主要入口方法
查询站点分页 5515 ms /hems/v1/container/page HomeServiceImpl.searchContainerPage
查询站点统计 5548 ms /hems/v1/container/getStatistics HomeServiceImpl.getContainerStatistics
查询设备实时数据 1255 ms /hems/v1/device/searchDeviceRealData SiteDeviceMonitorServiceImpl.getDeviceRealData
查询时区 1064 ms /hems/v1/container/getTimeZone DateUtils.getContainerTimeZone
查询设备监测树 未记录 /hems/v1/actionType/tree ActionTypeServiceImpl.getActionTypeTree

二、共性瓶颈定位

这 5 个接口的慢,核心原因集中在 3 个点

  1. 循环/遍历中调用 Feign/外部服务 ------ 单次请求本来不慢,但 155 个容器或 N 个设备串行调用就慢了。
  2. 全量拉取数据后内存处理 ------ 分页接口实际先把所有数据拉回来,在 Java 内存里做分页、过滤、统计。
  3. 时区/地址等静态信息没有有效缓存 ------ 每个容器/每次请求都重新查一次。

下面逐个接口拆解。


三、逐个接口分析与优化方案

3.1 /hems/v1/container/page --- 站点分页(5515ms)

当前实现路径

ContainerController.searchContainerPageHomeServiceImpl.searchContainerPagesearchEngineerHomePage/searchUserHomePage

耗时瓶颈
  1. 全量查询后内存分页

    • 虽然入参 pageSize=20,但代码中传给 dmContainerService.searchUserKeyContainerList(search)pageSize 就是 homeSearch.getPageSize()(即 20)。
    • 但后续 buildHomeInfoList 会对返回的容器列表逐个 调用 buildHomeInfo,涉及大量 Feign 调用,整体处理时间与返回条数成正比。
    • 如果工程师角色没有容器权限过滤,会一次性查询大量容器。
  2. buildHomeInfo 中循环调用外部服务

    • 每个容器调用 DateUtils.formatByContainer(container.getCreateDate(), ...)
    • 该方法内部调用 DateUtils.getContainerTimeZone(homeId)bizContainerService.getOne(containerId)每个容器一次 Feign 调用
    • 20 个容器就 20 次 Feign 调用,这是分页慢的主要原因之一。
  3. fillAddressInfo 中每个容器多次调用 geo 服务

    • geoCountryService.getCitiesPO()
    • geoCountryService.getCountryName()
    • geoCountryService.getStateName()
    • geoCountryService.getCityName()
    • 每个容器 4 次 Feign/服务调用。
  4. buildHomeInfoList 一次性查询所有通信棒设备

    • bizDeviceService.listForTypes(null, singletonList(COMMUNICATION_ROD)) 查询全量通信棒设备,再按容器分组。
    • 如果通信棒设备很多,这一步也会慢。
优化方案(只改接口本身)

方案 A:给 formatByContainer 添加本地缓存,避免每个容器重复查时区

DateUtils.getContainerTimeZone 已经有 ThreadLocal 缓存,但只缓存当前线程最近一次查询。分页接口处理多个容器时,每个容器都是一次新的 Feign 调用。建议改为方法级本地缓存(Caffeine)或 Redis 缓存

修改位置:DateUtils.getContainerTimeZone

java 复制代码
// 增加 Caffeine 本地缓存(同一 JVM 内共享)
private static final Cache<String, String> CONTAINER_TIMEZONE_CACHE = Caffeine.newBuilder()
        .maximumSize(10000)
        .expireAfterWrite(1, TimeUnit.HOURS)
        .build();

public static String getContainerTimeZone(String containerId) {
    if (StrUtil.isBlank(containerId)) {
        return getDefaultTimeZoneId();
    }
    String cached = CONTAINER_TIMEZONE_CACHE.getIfPresent(containerId);
    if (cached != null) {
        return cached;
    }
    // ... 原有逻辑
    String timeZoneId = resolveContainerTimeZone(containerId, bizContainerService, citiesMapper);
    CONTAINER_TIMEZONE_CACHE.put(containerId, timeZoneId);
    return timeZoneId;
}

时区是极稳定的静态数据,缓存 1 小时甚至更长都没问题。此改动收益最大,预计单接口可提速 30%~50%。

方案 B:fillAddressInfo 中批量缓存 geo 查询结果

目前 fillAddressInfo 每个容器分别查国家、省、市。可以在 buildHomeInfoList 开始前,先收集所有容器涉及的 cityCode一次性批量查询 city/state/country 名称,再在内存中映射。

修改位置:HomeServiceImpl.buildHomeInfoListfillAddressInfo

java 复制代码
// buildHomeInfoList 中新增
Set<Integer> cityCodes = containerList.stream()
    .map(this::extractCityCode)
    .filter(Objects::nonNull)
    .collect(Collectors.toSet());
Map<Integer, String> cityNameMap = batchGetCityNames(cityCodes, lang);
Map<Integer, String> stateNameMap = batchGetStateNames(cityCodes, lang);
Map<Integer, String> countryNameMap = batchGetCountryNames(cityCodes, lang);

// fillAddressInfo 改为从 map 中取,不再逐个调用 Feign

如果 geo 服务本身支持批量查询最佳;如果不支持,可先加本地 Caffeine 缓存,key 为 cityCode:lang

方案 C:通信棒设备查询加缓存或按需查询

buildHomeInfoList 当前查询所有通信棒设备。可以改为:

  • 先查出返回的容器 ID 列表;
  • containerId IN (...) 批量查询这些容器下的通信棒设备;
  • 或者对通信棒设备按容器维度做 Redis 缓存。

修改位置:HomeServiceImpl.buildHomeInfoList

java 复制代码
// 改为根据返回的容器ID批量查询通信棒设备
List<String> containerIds = containerList.stream()
    .map(DmContainerDTO::getContainerId)
    .collect(Collectors.toList());
Map<String, List<DmDeviceDTO>> communicationRodMap = bizDeviceService
    .listForTypesByContainerIds(containerIds, Collections.singletonList(DeviceTypeEnum.COMMUNICATION_ROD.getCode()));

需要 bizDeviceService 或底层 Feign 新增按容器批量查询的能力。如果无法改底层,可先在 HomeServiceImpl 层对结果做本地缓存。

方案 D:分页接口返回字段瘦身

如果前端列表页不需要 addrlocationcreateDate 等字段,可以在 HomeSearch 中增加一个 simple 标志,分页列表只返回必要字段,详情接口再查完整信息。


3.2 /hems/v1/container/getStatistics --- 站点统计(5548ms)

当前实现路径

ContainerController.getStatisticsHomeServiceImpl.getContainerStatistics

耗时瓶颈
java 复制代码
public List<HomeStatusStatistics> getContainerStatistics(String authType) {
    HomeSearch homeSearch = new HomeSearch();
    homeSearch.setPageNum(1);
    homeSearch.setPageSize(10000);
    List<HomeInfoDTO> homeList = searchContainerPage(homeSearch, authType).getList();
    // ... 统计
}

这个方法直接复用了 searchContainerPage ,但统计只需要每个容器的状态,却走了完整构建 HomeInfoDTO 的流程(时区、地址、通信棒、联系人等)。这是统计接口比分页还慢的核心原因。

优化方案(只改接口本身)

方案 A:为统计接口单独写轻量查询

统计只需要:容器 ID + 状态。不需要地址、时区、联系人、容量等信息。

新增方法:

java 复制代码
public List<HomeStatusStatistics> getContainerStatistics(String authType) {
    // 1. 只查容器ID和状态,不走完整 buildHomeInfo
    List<DmContainerDTO> containerList = queryContainerListForStatistics(authType);
    if (CollectionUtil.isEmpty(containerList)) {
        return buildEmptyStatistics(authType);
    }
    // 2. 批量查通信棒状态(可缓存)
    Map<String, Integer> containerStatusMap = computeContainerStatusBatch(containerList);
    // 3. 统计
    Map<Integer, Long> statusCountMap = containerStatusMap.values().stream()
            .collect(Collectors.groupingBy(status -> status, Collectors.counting()));
    return buildStatistics(statusCountMap, authType);
}

这是最有效的优化,预计统计接口可从 5.5s 降到 500ms 以内。

方案 B:如果必须复用 searchContainerPage,则减少 pageSize 并加缓存

如果逻辑复杂必须复用,至少:

  • pageSize 从 10000 改为合理的最大值;
  • searchContainerPage 加短时间的本地缓存(如 10 秒),统计接口直接命中缓存。

3.3 /hems/v1/container/getTimeZone --- 查询时区(1064ms)

当前实现路径

ContainerController.getTimeZoneDateUtils.getContainerTimeZone(homeId)

耗时瓶颈

一个单值查询接口耗时 1 秒以上,原因是:

  • DateUtils.getContainerTimeZone 调用 SpringUtil.getBean(BizContainerService.class)
  • 然后 bizContainerService.getOne(containerId) 走一次 Feign 调用
  • 再解析容器扩展属性找 timezone

整个过程就为了取一个几乎不变的字符串。

优化方案(只改接口本身)

方案 A:接口级 Redis 缓存(推荐)

ContainerController.getTimeZoneDateUtils.getContainerTimeZone 增加 Redis 缓存。

java 复制代码
@GetMapping("/getTimeZone")
public ResponseBean<String> getTimeZone(@RequestParam("homeId") String homeId) {
    String cacheKey = "hems:container:timezone:" + homeId;
    String cached = redisUtil.get(cacheKey);
    if (StrUtil.isNotBlank(cached)) {
        return ResponseBean.success(cached);
    }
    String timeZone = DateUtils.getContainerTimeZone(homeId);
    redisUtil.set(cacheKey, timeZone, 3600); // 缓存1小时
    return ResponseBean.success(timeZone);
}

时区变更频率极低,缓存 1 小时非常安全。预计该接口可从 1s 降到 10ms 以内。

方案 B:在容器信息变更时同步刷新缓存

如果担心缓存不一致,可以在容器扩展属性更新接口中主动删除或更新该缓存 key。


3.4 /hems/v1/device/searchDeviceRealData --- 设备实时数据(1255ms)

当前实现路径

DeviceMonitorController.searchDeviceRealDataSiteDeviceMonitorServiceImpl.getDeviceRealDatabuildSingleDeviceRealData

耗时瓶颈
java 复制代码
public List<DeviceRealDataResultDTO> getDeviceRealData(DeviceRealDataSearch search) {
    for (DeviceRealDataItemSearch deviceItem : deviceItemList) {
        resultList.add(buildSingleDeviceRealData(homeId, deviceTypeId, deviceItem));
    }
}

设备是串行处理的 ,每个设备调用 buildSingleDeviceRealData,其中:

  1. 如果是断路器/插座,调用 bizDeviceConfigService.monitorConfigsWithMerge() 查开关状态;
  2. 调用 actionTypeService.getActionTypeTree(queryDTO) 构建整棵动作类型树并填充实时值。

对于附件中 devices 只有 1 个设备的情况,主要耗时在 getActionTypeTree 内部的:

  • bizDeviceService.getOne() 查设备
  • fillTreeActionItemValues 中的 bizDeviceConfigService.monitorDevicesConfigsWithMerge() 批量查测点
  • queryAllTypeEleStatistics 中查分类电量统计,涉及历史曲线查询 dmMeasurePointService.searchYcPointHis()

如果有多个设备,串行会成倍增加耗时。

优化方案(只改接口本身)

方案 A:多设备并行处理(推荐)

for 循环改为并行流或 CompletableFuture,同一 homeId 下多个设备的配置查询可以并行。

修改位置:SiteDeviceMonitorServiceImpl.getDeviceRealData

java 复制代码
public List<DeviceRealDataResultDTO> getDeviceRealData(DeviceRealDataSearch search) {
    // ... 参数校验
    List<DeviceRealDataItemSearch> deviceItemList = search.getDevices();
    return deviceItemList.parallelStream()
            .map(deviceItem -> buildSingleDeviceRealData(search.getHomeId(), search.getDeviceTypeId(), deviceItem))
            .collect(Collectors.toList());
}

注意:parallelStream 使用公共 ForkJoinPool,如果下游有 Feign 调用会阻塞公共线程池。建议注入自定义线程池:

java 复制代码
@Resource(name = "applicationTaskExecutor")
private Executor asyncTaskExecutor;

public List<DeviceRealDataResultDTO> getDeviceRealData(DeviceRealDataSearch search) {
    List<CompletableFuture<DeviceRealDataResultDTO>> futures = deviceItemList.stream()
        .map(item -> CompletableFuture.supplyAsync(
            () -> buildSingleDeviceRealData(search.getHomeId(), search.getDeviceTypeId(), item),
            asyncTaskExecutor))
        .collect(Collectors.toList());
    return futures.stream().map(CompletableFuture::join).collect(Collectors.toList());
}

方案 B:批量查询动作类型树

已有 ActionTypeServiceImpl.getBatchActionTypeTree(BatchActionTypeQueryDTO) 方法,支持一次性查询多个设备的动作类型树。getSiteDeviceMonitor 已经在用,但 getDeviceRealData 没有复用。

修改 getDeviceRealData

java 复制代码
public List<DeviceRealDataResultDTO> getDeviceRealData(DeviceRealDataSearch search) {
    // 1. 批量查询动作类型树
    BatchActionTypeQueryDTO batchQueryDTO = new BatchActionTypeQueryDTO();
    batchQueryDTO.setContainerId(search.getHomeId());
    batchQueryDTO.setActionType(DEVICE_LIST);
    batchQueryDTO.setDeviceIds(deviceIdList);
    batchQueryDTO.setLevel(LEVEL);
    batchQueryDTO.setNeedFill(true);
    Map<String, ActionTypeTreeVO> treeMap = actionTypeService.getBatchActionTypeTree(batchQueryDTO);

    // 2. 批量查询开关状态(断路器/插座)
    Map<String, Map<String, MonitorPointDTO>> switchStatusMap = batchQuerySwitchStatus(search);

    // 3. 组装结果
    return deviceItemList.stream()
        .map(item -> buildResult(item, treeMap.get(item.getDeviceId()), switchStatusMap))
        .collect(Collectors.toList());
}

这样动作类型配置和测点值都只查一次,不再每个设备重复查询。这是最有效的优化。

方案 C:给 queryAllTypeEleStatistics 加缓存

该方法内部查询当天起始电量需要调历史曲线接口。如果前端不需要实时统计电量(如只需要展示原始测点值),可以在 getActionTypeTree 时传入 NeedFill=false 跳过该步骤。


3.5 /hems/v1/actionType/tree --- 设备监测树(未记录耗时)

当前实现路径

ActionTypeController.getActionTypeTreeActionTypeServiceImpl.getActionTypeTree

耗时瓶颈

searchDeviceRealData 共用同一套逻辑。主要慢在:

  1. fillTreeActionItemValuesqueryAllTypeEleStatistics → 历史曲线查询
  2. fillParamSettingsValuedeviceDfService.getDeviceParam() 查设备参数
  3. 特殊设备类型(PCS/BATTERY/METER/COMMUNICATIONROD)的额外查询
优化方案(只改接口本身)

方案 A:按需填充

ActionTypeQueryDTO 中已经有 needFill 字段。如果 /tree 接口某些场景只需要树结构不需要实时值,前端可以在入参中控制 needFill=false

后端也可以根据 level 判断:如果 level=1 只展示一级菜单,很多实时值其实不需要填充。

方案 B:分类电量统计加缓存

queryAllTypeEleStatistics 查询的是当天累计电量,这类数据可以按 containerId 维度缓存 1~5 分钟。

java 复制代码
private HashMap<String, TypeEleStatisticsDTO> queryAllTypeEleStatistics(String containerId, String traceId) {
    String cacheKey = "hems:type-ele-statistics:" + containerId;
    String cached = redisUtil.get(cacheKey);
    if (StrUtil.isNotBlank(cached)) {
        return JSON.parseObject(cached, new TypeReference<HashMap<String, TypeEleStatisticsDTO>>(){});
    }
    // ... 原有查询逻辑
    redisUtil.set(cacheKey, JSON.toJSONString(result), 60); // 缓存60秒
    return result;
}

方案 C:设备参数查询加缓存

fillParamSettingsValue 调用 deviceDfService.getDeviceParam() 查设备参数。设备参数变化不频繁,可以按 deviceId 缓存 1 分钟。


四、优化优先级与预期收益

优先级 优化项 影响接口 预期收益
P0 getContainerTimeZone 加缓存 page、getTimeZone、多个日期格式化场景 时区接口从 1s → 10ms;分页接口降 30%~50%
P0 getContainerStatistics 单独写轻量查询 getStatistics 从 5.5s → 500ms 以内
P1 searchDeviceRealData 改批量/并行 searchDeviceRealData 从 1.2s → 200~400ms
P1 fillAddressInfo 批量查 geo 或加缓存 page 分页接口再降 20%~30%
P2 queryAllTypeEleStatistics 加缓存 actionType/tree、searchDeviceRealData 树接口明显提速
P2 通信棒设备查询按需批量 page、getStatistics 减少全表扫描

五、最小改动集(推荐先实施这 4 项)

如果只改最少的代码,建议优先做这 4 项:

  1. DateUtils.getContainerTimeZone 加 Caffeine 本地缓存

    • 影响面小,收益巨大,所有涉及时区的接口都受益。
  2. ContainerController.getTimeZone 加 Redis 缓存

    • 单独接口,加缓存即可,立竿见影。
  3. HomeServiceImpl.getContainerStatistics 独立实现

    • 不再复用 searchContainerPage,只查状态必要字段。
  4. SiteDeviceMonitorServiceImpl.getDeviceRealData 改为并行或批量

    • 复用已有的 getBatchActionTypeTree,减少重复查询。

六、涉及的关键文件

文件 说明
hemsUser/src/main/java/com/dsa/hems/common/utils/DateUtils.java 时区查询加缓存
hemsUser/src/main/java/com/dsa/hems/home/controller/ContainerController.java 时区接口加缓存
hemsUser/src/main/java/com/dsa/hems/home/service/HomeServiceImpl.java 分页、统计接口优化
hemsUser/src/main/java/com/dsa/hems/device/service/impl/SiteDeviceMonitorServiceImpl.java 设备实时数据并行/批量优化
hemsUser/src/main/java/com/dsa/hems/device/service/impl/ActionTypeServiceImpl.java 分类电量统计、设备参数加缓存

七、验证建议

  1. 在修改后使用相同请求参数重放,记录各接口耗时。
  2. 重点观察 container/pagecontainer/getStatistics 的 Feign 调用次数是否显著减少。
  3. 监控 Redis/本地缓存命中率,确保缓存生效。
  4. 并行改造后观察线程池是否被占满,必要时调大 applicationTaskExecutor 线程数。

如需我直接修改其中某个文件,告诉我优先改哪个,我可以直接动手。

相关推荐
qq_401700411 小时前
Qt容器性能优化:QVector、QHash、QMap到底应该怎么选?
开发语言·qt·性能优化
treesforest1 小时前
你以为只是一串数字?你的IP地址,正在悄悄暴露这些信息
网络·网络协议·tcp/ip·网络安全·ip归属地查询·ip风控
爱喝水的鱼丶2 小时前
SAP-ABAP:一次关于“交货日期”的增强需求:从复杂增强到标准功能的回归之旅
运维·开发语言·性能优化·sap·abap·经验交流
kiros_wang3 小时前
鸿蒙性能优化全维度实战(启动速度 + 内存治理 + 帧率稳定 + 包体积瘦身)
华为·性能优化·harmonyos
拾光Ծ3 小时前
【Linux网络】网络通信实战:UDP & TCP Socket编程实现Echo Server
linux·网络·网络协议·tcp/ip·udp·线程池
派葛穆3 小时前
s7-200smart-tcp/ip通讯
网络协议·tcp/ip
灯澜忆梦5 小时前
GO_网络编程---网络协议
网络·网络协议·golang
CodexDave14 小时前
MySQL事务隔离级别与MVCC机制解析
前端·数据库·mysql·nginx·性能优化·负载均衡
凌云C16 小时前
5G 协议体系与交互流程
网络协议