分页查询接口,从2s优化到了0.01s

前言

最近有小伙伴问了这样一个问题:百万商品分页查询接口,如何保证接口的性能?

这就需要对该分页查询接口做优化了。

这篇文章从9个方面跟大家一起聊聊分页查询接口优化的一些小技巧,希望对你会有所帮助。

图片



1 增加默认条件

对于分页查询接口,如果没有特殊要求,我们可以在输入参数中,给一些默认值。

这样可以缩小数据范围,避免每次都count所有数据的情况。

对于商品查询,这种业务场景,我们可以默认查询当天上架状态的商品列表。

例如:

select * from product 
where edit_date>='2023-02-20' and edit_date<'2023-02-21' and status=1

如果每天有变更的商品数量不多,通过这两个默认条件,就能过滤掉绝大部分数据,让分页查询接口的性能提升不少。

温馨提醒一下:记得给时间状态字段增加一个联合索引

2 减少每页大小

分页查询接口通常情况下,需要接收两个参数:pageNo(即:页码)和pageSize(即:每页大小)。

如果分页查询接口的调用端,没有传pageNo默认值是1,如果没有传pageSize也可以给一个默认值10或者20。

不太建议pageSize传入过大的值,会直接影响接口性能。

在前端有个下拉控件,可以选择每页的大小,选择范围是:10、20、50、100。

前端默认选择的每页大小为10

不过在实际业务场景中,要根据产品需求而且,这里只是一个参考值。

3 减少join表的数量

有时候,我们的分页查询接口的查询结果,需要join多张表才能查出数据。

比如在查询商品信息时,需要根据商品名称、单位、品牌、分类等信息查询数据。

这时候写一条sql可以查出想要的数据,比如下面这样的:

select 
  p.id,
  p.product_name,
  u.unit_name,
  b.brand_name,
  c.category_name
from product p
inner join unit u on p.unit_id = u.id
inner join brand b on p.brand_id = b.id
inner join category c on p.category_id = c.id
where p.name='测试商品' 
limit 0,20;

使用product表去join了unit、brand和category这三张表。

其实product表中有unit_id、brand_id和category_id三个字段。

我们可以先查出这三个字段,获取分页的数据缩小范围,之后再通过主键id集合去查询额外的数据。

我们可以把sql改成这样:

select 
  p.id,
  p.product_id,
  u.unit_id,
  b.brand_id,
  c.category_id
from product
where name='测试商品'
limit 0,20;

这个例子中,分页查询之后,我们获取到的商品列表其实只要20条数据。

再根据20条数据中的id集合,获取其他的名称,例如:

select id,name 
from unit
where id in (1,2,3);

然后在程序中填充其他名称。

伪代码如下:

List<Product> productList = productMapper.search(searchEntity);
List<Long> unitIdList = productList.stream().map(Product::getUnitId).distinct().collect(Collectors.toList());
List<Unit> unitList = UnitMapper.queryUnitByIdList(unitIdList);
for(Product product: productList) {
   Optional<Unit> optional = unitList.stream().filter(x->x.getId().equals(product.getId())).findAny();
   if(optional.isPersent()) {
      product.setUnitName(optional.get().getName());
   } 
}

这样就能有效的减少join表的数量,可以一定的程度上优化查询接口的性能。

4 优化索引

分页查询接口性能出现了问题,最直接最快速的优化办法是:优化索引

因为优化索引不需要修改代码,只需回归测试一下就行,改动成本是最小的。

我们需要使用explain关键字,查询一下生产环境分页查询接口的执行计划

看看有没有创建索引,创建的索引是否合理,或者索引失效了没。

索引不是创建越多越好,也不是创建越少越好,我们需要根据实际情况,到生产环境测试一下sql的耗时情况,然后决定如何创建或优化索引。

建议优先创建联合索引

5 用straight_join

有时候我们的业务场景很复杂,有很多查询sql,需要创建多个索引。

在分页查询接口中根据不同的输入参数,最终的查询sql语句,MySQL根据一定的抽样算法,却选择了不同的索引。

不知道你有没有遇到过,某个查询接口,原本性能是没问题的,但一旦输入某些参数,接口响应时间就非常长。

这时候如果你此时用explain关键字,查看该查询sql执行计划,会发现现在走的索引,跟之前不一样,并且驱动表也不一样。

之前一直都是用表a驱动表b,走的索引c。

此时用的表b驱动表a,走的索引d。

为了解决Mysql选错索引的问题,最常见的手段是使用force_index关键字,在代码中指定走的索引名称。

但如果在代码中硬编码了,后面一旦索引名称修改了,或者索引被删除了,程序可能会直接报错。

这时该怎么办呢?

答:我们可以使用straight_join代替inner join

straight_join会告诉Mysql用左边的表驱动右边的表,能改表优化器对于联表查询的执行顺序。

之前的查询sql如下:

select p.id from product p
inner join warehouse w on p.id=w.product_id;
...

我们用它将之前的查询sql进行优化:

select p.id from product p
straight_join warehouse w on p.id=w.product_id;
...

6 数据归档

随着时间的推移,我们的系统用户越来越多,产生的数据也越来越多。

单表已经到达了几千万。

这时候分页查询接口性能急剧下降,我们不能不做分表处理了。

做简单的分表策略是将历史数据归档,比如:在主表中只保留最近三个月的数据,三个月前的数据,保证到历史表中。

我们的分页查询接口,默认从主表中查询数据,可以将数据范围缩小很多。

如果有特殊的需求,再从历史表中查询数据,最近三个月的数据,是用户关注度最高的数据。

7 使用count(*)

在分页查询接口中,需要在sql中使用count关键字查询总记录数

目前count有下面几种用法:

  • count(1)

  • count(id)

  • count(普通索引列)

  • count(未加索引列)

那么它们有什么区别呢?

  • count(*) :它会获取所有行的数据,不做任何处理,行数加1。

  • count(1):它会获取所有行的数据,每行固定值1,也是行数加1。

  • count(id):id代表主键,它需要从所有行的数据中解析出id字段,其中id肯定都不为NULL,行数加1。

  • count(普通索引列):它需要从所有行的数据中解析出普通索引列,然后判断是否为NULL,如果不是NULL,则行数+1。

  • count(未加索引列):它会全表扫描获取所有数据,解析中未加索引列,然后判断是否为NULL,如果不是NULL,则行数+1。

由此,最后count的性能从高到低是:

count(*) ≈ count(1) > count(id) > count(普通索引列) > count(未加索引列)

所以,其实count(*)是最快的。

我们在使用count统计总记录数时,一定要记得使用count(*)。

8 从ClickHouse查询

有些时候,join的表实在太多,没法去掉多余的join,该怎么办呢?

答:可以将数据保存到ClickHouse

ClickHouse是基于列存储的数据库,不支持事务,查询性能非常高,号称查询十几亿的数据,能够秒级返回。

为了避免对业务代码的嵌入性,可以使用Canal监听Mysqlbinlog日志。当product表有数据新增时,需要同时查询出单位、品牌和分类的数据,生成一个新的结果集,保存到ClickHouse当中。

查询数据时,从ClickHouse当中查询,这样使用count(*)的查询效率能够提升N倍。

需要特别提醒一下:使用ClickHouse时,新增数据不要太频繁,尽量批量插入数据。

其实如果查询条件非常多,使用ClickHouse也不是特别合适,这时候可以改成ElasticSearch,不过它跟Mysql一样,存在深分页问题。

9 数据库读写分离

有时候,分页查询接口性能差,是因为用户并发量上来了。

在系统的初期,还没有多少用户量,读数据请求和写数据请求,都是访问的同一个数据库,该方式实现起来简单、成本低。

刚开始分页查询接口性能没啥问题。

但随着用户量的增长,用户的读数据请求和写数据请求都明显增多。

我们都知道数据库连接有限,一般是配置的空闲连接数是100-1000之间。如果多余1000的请求,就只能等待,就可能会出现接口超时的情况。

因此,我们有必要做数据库的读写分离。写数据请求访问主库,读数据请求访问从库,从库的数据通过binlog从主库同步过来。

根据不同的用户量,可以做一主一从,一主两从,或一主多从。

数据库读写分离之后,能够提升查询接口的性能。

<think>首先,用户的问题是:“我想寻找将Java接口返回的大量点位数据渲染到地图上的实现方案请问Java大量点位数据地图渲染实现方案”。这是一个关于Java处理大量点位数据并渲染到地图上的实现方案。我需要根据系统级指令来构建回答:-所有行内数学表达式必须使用$...$格式。-独立公式必须使用$$...$$$$格式并单独成段。-LaTeX语法正确。-使用中文回答。-生成相关问题。-回答中引用的段落末尾自然地添加引用标识,例如[^1]。用户可见层的要求:回答结构清晰,帮助用户逐步解决问题,尽量保证回答真实可靠。参考站内引用:有5个引用,我需要参考这些来回答问题。这些引用涉及:-[^1]:关于将点坐标设置到网格并更新数据。-[^2]:服务器渲染优势,但浏览器适应性差。-[^3]:多源数据融合和动态呈现。-[^4]:GIS海量数据前端加载优化策略,如屏幕范围加载、层级加载、缓存。-[^5]:高德地图优化实践,工具和方法论加速方案。用户的问题是处理大量点位数据渲染到地图上,所以我应该结合这些引用中的优化策略。结构化回答:1.**引言**:简要解释问题。2.**关键挑战**:处理大量数据时的性能问题。3.**实现方案**:分步描述方案,包括:-数据获取:从Java接口获取数据。-数据处理:优化数据,如网格化、简化。-渲染方案:服务器端渲染或客户端渲染。-优化策略:基于引用[^4],如屏幕范围加载、层级加载、缓存。4.**示例代码**:提供Java代码片段展示关键部分。5.**结论**:总结最佳实践。6.**相关问题**:生成相关问题。使用中文回答。生成相关问题:至少3个,类似示例中的格式。引用标识:在回答中自然地添加引用,如提到优化策略时引用[^4]。数学表达式:用户问题中没有明显的数学内容,但如果有,比如坐标计算,确保用$...$格式。例如,计算网格坐标时,用$x=\frac{l.lng}{\text{网格经度大小}}$。现在,脑暴回答大纲:-**步骤1:数据获取**-从Java接口获取点位数据。-示例代码:使用JavaHTTP客户端或REST模板。-**步骤2:数据处理**-引用[^1]:将点坐标网格化,减少渲染点数量。-例如,$x=\lfloor\frac{\text{lng}}{\Delta\text{lng}}\rfloor$,$y=\lfloor\frac{\text{lat}}{\Delta\text{lat}}\rfloor$。-简化数据:移除重复点或使用聚类。-**步骤3:渲染方案**-选项1:服务器端渲染(引用[^2]):在服务器生成图像发送到客户端,但浏览器适应性差。-选项2:客户端渲染:使用JavaScript库如Leaflet或OpenLayers。-结合引用[^4]:优化前端加载,如只加载可见区域数据。-**步骤4:优化策略**-屏幕范围加载:只请求和渲染当前地图视图内的点。-层级加载:在不同缩放级别加载不同细节的数据。-数据缓存:避免重复请求。-**示例代码**:提供Java代码处理数据,并JavaScript渲染片段。生成相关问题:1.如何优化Java接口返回大量数据的性能?2.前端地图渲染库有哪些推荐?3.如何处理动态更新的点位数据?确保回答真实可靠:基于引用和一般知识。</think>### Java 接口返回大量点位数据的地图渲染实现方案 处理 Java 接口返回的大量点位数据(如成千上万个坐标点)并渲染到地图上时,核心挑战在于性能优化。大规模数据直接渲染可能导致浏览器卡顿或服务器过载。基于最佳实践和参考引用,我将提供一个结构化的实现方案,分为数据获取、数据处理、渲染策略和优化步骤。整个过程优先使用 Java 处理数据,并结合前端技术(如 JavaScript 地图库)进行高效渲染。方案参考了 GIS 优化策略[^4] 和服务器渲染优势[^2],确保可靠性和可扩展性。 #### 1. **数据获取:从 Java 接口获取点位数据** 首先,在 Java 端调用接口获取点位数据。使用 HTTP 客户端(如 Spring 的 `RestTemplate` 或 Java 11+ 的 `HttpClient`)异步获取数据,避免阻塞主线程。关键点包括: - **异步处理**:使用 `CompletableFuture` 或 Spring WebFlux 实现非阻塞请求,提高吞吐量。 - **数据分页**:如果接口支持,通过分页参数(如 `pageSize` 和 `pageNum`)分批获取数据,减少单次请求负载。 - 示例 Java 代码: ```java import org.springframework.web.client.RestTemplate; import java.util.List; public class PointDataService { public List<Point> fetchPoints(String apiUrl, int pageSize, int pageNum) { RestTemplate restTemplate = new RestTemplate(); String url = apiUrl + "?pageSize=" + pageSize + "&pageNum=" + pageNum; return restTemplate.getForObject(url, List.class); // 假设返回 List<Point> } } ``` 这里,`Point` 是一个自定义类,包含经度(lng)、纬度(lat)等属性。 #### 2. **数据处理:在 Java优化点位数据** 获取原始数据后,在 Java 端进行预处理,减少渲染点数量。这能显著提升性能,尤其适用于海量数据[^4]。主要方法: - **网格化聚合**:将点坐标映射到网格单元,减少渲染点数。例如,计算网格坐标 $x = \lfloor \frac{\text{lng}}{\Delta \text{lng}} \rfloor$ 和 $y = \lfloor \frac{\text{lat}}{\Delta \text{lat}} \rfloor$,其中 $\Delta \text{lng}$ 和 $\Delta \text{lat}$ 是网格大小。每个网格只渲染一个代表点(如中心点或聚合点),避免重复[^1]。 - **数据简化**:使用聚类算法(如 K-means)或简化库(如 Douglas-Peucker)移除冗余点。 - **更新数据存储**:如果数据频繁变化,在 Java 服务中批量更新网格化数据到数据库,便于后续复用[^1]。 - 示例 Java 代码(网格化处理): ```java import java.util.List; public class GridProcessor { private static final double DELTA_LNG = 0.01; // 网格经度大小,根据需求调整 private static final double DELTA_LAT = 0.01; // 网格纬度大小 public void processPoints(List<Point> points) { for (Point point : points) { int gridX = (int) Math.floor(point.getLng() / DELTA_LNG); int gridY = (int) Math.floor(point.getLat() / DELTA_LAT); point.setGrid(gridX, gridY); // 设置网格坐标 // 更新数据到数据库或缓存 // testService.update(point); // 引用[^1] 中的更新逻辑 } } } ``` #### 3. **渲染策略:选择高效的地图渲染方式** 渲染方案分为服务器端和客户端两种,各有优劣。推荐结合使用,以平衡性能和用户体验: - **服务器端渲染(SSR)**:在 Java 服务器生成静态地图图像(如使用 GeoTools 或生成 PNG),然后发送到前端。优势:处理巨型数据高效,利用多线程能力[^2]。缺点:浏览器适应性差,不易响应交互(如缩放)。适用于静态报告或后台系统。 - 实现:集成 Java 库生成瓦片地图,通过 API 提供图像 URL。 - **客户端渲染(CSR)**:将处理后的数据发送到前端,使用 JavaScript 地图库(如 Leaflet、OpenLayers 或 Mapbox)渲染。优势:交互性强,适应不同分辨率。关键优化:基于屏幕范围动态加载数据[^4]。 - 实现步骤: 1. Java 端提供 REST API 返回点位数据(如 JSON 格式)。 2. 前端监听地图视图变化(如移动或缩放),只请求当前可见区域的数据。 3. 使用 WebSocket 或 Server-Sent Events (SSE) 支持实时更新。 - **混合方案**:对超大数据(>10万点),先在服务器预处理并生成轻量级数据,再在客户端渲染。引用[^3] 提到数据融合技术(如聚合三维模型),可扩展此方案。 #### 4. **优化策略:确保高性能和可靠性** 针对海量数据,应用以下优化[^4][^5]: - **屏幕范围加载**:前端只加载地图视口内的点。计算视口边界,Java API 添加过滤参数(如 `minLng`, `maxLng`, `minLat`, `maxLat`)。 - 数学公式:视口范围定义为 $[\text{minLng}, \text{maxLng}] \times [\text{minLat}, \text{maxLat}]$,Java 端过滤数据。 - **层级加载(LOD)**:根据不同缩放级别加载不同细节的数据。例如,高缩放级别(放大)加载原始点,低缩放级别(缩小)加载聚合网格。 - **数据缓存**: - 前端缓存:使用内存或 IndexedDB 存储已请求数据,避免重复调用 API。 - 后端缓存:Java 中用 Redis 或 Caffeine 缓存网格化数据,加速响应。 - **性能监控**:集成工具(如 Prometheus)分析耗时点,引用[^5] 强调工具化优化的重要性。 - 完整示例(前端伪代码,结合 Java API): ```javascript // 前端使用 Leaflet 地图库 const map = L.map('map').setView([39.9, 116.4], 10); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map); // 监听地图移动事件,动态加载数据 map.on('moveend', function() { const bounds = map.getBounds(); const url = `/api/points?minLng=${bounds.getWest()}&maxLng=${bounds.getEast()}&minLat=${bounds.getSouth()}&maxLat=${bounds.getNorth()}`; fetch(url) .then(response => response.json()) .then(points => { points.forEach(point => { L.marker([point.lat, point.lng]).addTo(map); }); }); }); ``` #### 总结 实现 Java 大量点位数据的地图渲染,核心是“预处理 + 动态加载”。在 Java 端进行网格化聚合和过滤,减少数据量;前端结合地图库实现按需渲染。推荐混合方案:Java 处理数据优化(参考[^1][^4]),前端负责交互式渲染。对于超大数据,优先服务器端预处理[^2]。测试时,逐步增加数据量监控性能。根据引用[^3],此方案可扩展集成物联网或三维数据。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值