汇总页面分页查询优化


一、拆分表关联

数据信息:

订单(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删了。
  • 22
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值