文章目录
一、拆分表关联
数据信息:
订单(order):order_id, product_id, order_code
商品(product):product_id, product_code
最初写法:
汇总页面显示的不止一张表的数据。以前的没注意性能问题时,SQL写法为:
SELECT
o.order_code,
p.product_code
FROM
order o
LEFT JOIN product p ON o.product_id = p.product_id
<where>
<if test="orderCode != null and orderCode != ''">
and o.order_code like #{orderCode}
</if>
<if test="productCode != null and productCode != ''">
and p.product_code like #{productCode}
</if>
</where>
这样写,如果两个表数据量都很大,那么这么写查询就会很慢。
进行拆分:
1、首先判断查询参数中是否有副表的字段
如果参数中由副表中的字段,那么将 includeProductFlag
设置为 Y
2、第一次查询
SELECT
o.order_code,
<if test='includeProductFlag == "Y"'>
p.product_code,
</if>
o.product_id
FROM
order o
<if test='includeProductFlag == "Y"'>
LEFT JOIN product p ON o.product_id = p.product_id
</if>
<where>
<if test="orderCode != null and orderCode != ''">
and o.order_code like #{orderCode}
</if>
<if test="productCode != null and productCode != ''">
and p.product_code like #{productCode}
</if>
</where>
3、第二次查询
判断 includeProductFlag
是否为 Y
。
如果是 Y
,那么就直接返回,因为我们第一次查询已经进行连表查询了。
如果不是 Y
,那就需要使用第一次查询得到的 prodcut_id
放进 product 使用IN进行查询。查询出结果后,进行数据组装。
获取参数代码:
List<Order> orderList = 第一次查询结果;
// 如果第一次为空,或者includeProductFlag=='Y',直接返回
if (CollectionUtils.isEmpty(orderList) || "Y".equal(includeProductFlag)) {
return orderList;
}
// 获取prodctIds
Set<Long> productIds = orderList.stream().map(Order::getProductId).collect(Collectors.toSet());
if (CollectionUtils.isEmpty(productIds)) {
return orderList;
}
SQL:
SELECT
product_id,
product_code
FROM
product
WHERE
product_id IN
<foreach collection="productIds" item="item" open="(" separator="," close=")">
#{item}
</foreach>
组装代码:
List<Product> productList = 第二次查询的结果;
if (CollectionUtils.isEmpty(productList)) {
return orderList;
}
// 先构建Map
Map<Long, Product> productIdToProduct = productList.stream()
.collect(Collectors.toMap(
Product::getProductId,
p -> p,
(x1, x2) -> x1
));
// 进行数据组装
for (Order order : orderList) {
Long productId = order.getProductId();
if (!productIdToProduct.contains(productId)) {
continue;
}
Product product = productIdToProduct.get(productId);
order.setProductCode(product.getProductCode());
}
4、注意
查出来之后在代码里进行拼接。
不要用 for 循环里面再 for 循环来找。
要使用 for 循环加 Map.get() 来找。
因为for + for 的时间复杂度是 n*m。
for + Map 的时间复杂度是 m+n
二、拆分数量(count)和内容(content)
对于大数据量的表格的汇总查询。
发现问题:
由于汇总页一般是分页查询,会发现前几页内容(content)的查询速度明显快于数量(count)的。
解决思路:
将数量(count)的查询用单独开一个线程进行查询。
这里不给内容也开一个原因是:汇总页面展示的就是内容,你数量先出来了也没用。但是只要内容出来了,你数量后出来我是可以接受的。
方案:
1、需要一个唯一标识
由前端创建,并在查询时传给后端。
当刷新页面或是后端报错后,需要刷新该UUID。
2、创建一个查询数量的线程:
UUID-count:查询数量。查出数量存入redis中,注意只对当前UUID生效,失效时间不能太长。
3、代码思路:
- 前端查询时传一个UUID进来。
- 先创建一个线程用来查询数量。
- 查询内容
- 内容查出来后,查询一下 redis 中有没有该 UUID 的数量。有就一起返回;没有就不返回数量,但是需要返回一个下一次查询时间(nextQueryTime)。
- 前端收到没有数量的数据,拿着UUID调用一个公用的数量查询接口(countUuidApi)。这个接口作用就是去redis中查询有没有这个UUID对应的数量信息,如果没有,就需要返回前端一个下一次查询时间(nextQueryTime)。
- 还需设置一个定时任务(java.util.concurrent.ScheduledExecutorService),来管理线程。判断线程是否需要进行终止。
4、注意事项
4.1、线程控制
4.1.1、何时需要进行关闭线程?
- 整个流程超时(timeout):防止哪里出了问题,一直跑
- 允许前端和网络延迟,但是在我返回数据后的 下一次查询时间(nextQueryTime)+ 允许浮动时间(floatTime)还没收到第二次查询请求,也要关闭线程。
4.1.2、如何关闭线程?
- 查到了数量后,线程自动结束。
- 使用线程池创建线程,注意要给线程添加名字(UUID)。
我们需要在一个定时任务(其实就是一个循环查询的东西:java.util.concurrent.ScheduledExecutorService)中关闭线程。
我们需要从redis(即:UUID-thread)中获取到线程的名字(UUID),然后通过线程名字关闭这个线程。
4.2、集群信息同步
4.2.1、如何保存?
我第一次查询是服务器A处理的。我第二次进来,结果是服务器B处理。我的服务器B怎么获取之前的信息呢?
通过redis,使用redis将查询的基础信息保存起来。
4.2.2、需要保存的信息:
A、线程信息:
- redis名称:
UUID-thread(UUID) - 内容:
threadStartTime:线程开始时间(这个是为了在线程处理时间过长的时候关闭线程)
lastExecuteTime:上一次执行时间(这个是为了在我们长时间没接收到前端下一次查询时关闭线程用的)
nextQueryTime:下一次查询时间(这个是为了在我们长时间没接收到前端下一次查询时关闭线程用的) - 删除缓存信息时间:
在线程结束的时候删除。
什么时候线程会结束呢?查出来数量(count),或者查询报错,或者我们关闭了线程。
B、数量(count)信息:
- redis名称:
UUID-count(UUID) - 内容:
数量(count)信息 - 删除缓存信息时间:
根据自己需要设置,如果对时效性不是很高,那就设置高一点。如果对时效性特别高,那就在查询到的时候,就把这个redis删了。