深度翻页导出导致慢SQL,mysqlCPU飙升优化方案

慢SQL原因分析:

1.深度翻页

2.多表JOIN

  1. 大IN

  2. id倒排序

本文针对深度翻页的优化进行探讨

方案1:

将limit offset, pageSize的方式改成 id > xx limit pageSize.

这样能走Id索引,提高速度。

缺点:不能使用多线程,入参ID从上页结果。

方案2:

终极方案,基于 方案1再优化, 将limit offset, pageSize 的方式改成 id > startId and id< endId .

优点: 能用多线程并发查询。

步骤:

(1) 查询 对应表的ID范围,COUNT条数

(2) 根据count条数,和每页数量,计算页数,根据页数 和 ID范围进行ID范围切分。

(3)根据ID范围,发起多线程并发查询。

其中具体核心逻辑代码:

ID范围查询

sql 复制代码
<!-- 统计分页查询总条数 -->
	<select id="findIdRange" resultType="com.xyy.ms.export.core.erpreport.dto.ExportIdRangeDTO">
	select
		min(b.id) as minId, max(b.id) as maxId, count(1) as count
		from storage_batchnum b
		<include refid="batchNumExportWhere"></include>
	</select>

ID切分逻辑:

java 复制代码
package com.xyy.ms.export.core.erpreport.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;

/**
 * @author stivenjin
 * @version 1.0
 * @description 说明: 取表中最小和最大ID, 用ID翻页查询,避免深度翻页(批号库存翻页导出)
 * 翻页优化步骤:
 * 1:根据ID范围,进行切分组
 * 2:用每组 的边界值进行id范围翻页查询。
 * @date 2023/9/1 18:10
 */
@Getter
@Setter
@ToString
@AllArgsConstructor
public class ExportIdRangeDTO implements Serializable {
    /**
     * 最小ID
     */
    private int minId = 0;
    /**
     * 最大ID
     */
    private int maxId = 0;
    /**
     * 总条数
     */
    private long count = 0;

    public boolean isValid() {
        return minId > 0 && maxId > 0;
    }

    /**
     * 按页数分隔ID范围
     * @param pageCount
     * @return
     */
    public List<ExportIdRangeDTO> splitByPageCount(int pageCount) {
        List<ExportIdRangeDTO> splitList = new ArrayList<ExportIdRangeDTO>();
        int startId = minId;
        int endId = maxId;
        int pageSize = (int)Math.ceil((Double.valueOf(maxId) - Double.valueOf(minId)) / pageCount);
        System.out.println("pageSize:" + pageSize + ",pageCount:" + pageCount);
        int tmp = endId;
        for(int i = 1 ;i<=pageCount;i++){
            if(startId <= tmp){
                if(startId + pageSize <= tmp){
                    endId = startId + pageSize ;
                }else{
                    endId = tmp;
                }
            }else{
                break;
            }
            //System.out.println("循环调用:" + startId + " : " + endId);
            splitList.add(new ExportIdRangeDTO(startId, endId, 0));
            if(endId <= tmp){
                startId = endId +1;
            }
        }
        return splitList;
    }

    public static void main(String[] args) {
        ExportIdRangeDTO dto = new ExportIdRangeDTO(100,823540, 0);
        dto.splitByPageCount(10);
        System.out.println("切分一片原始:" + dto.getMinId() + " : " + dto.getMaxId());
    }
}
复制代码
<if test="minId != null and maxId != null">
   and b.id >= #{minId} and b.id &lt;= #{maxId}
</if>

按ID范围切分后,可用多线程并发查询导出

taskExecutor.submit

java 复制代码
// 增加顺序按起点ID导出模式,避免深度翻页慢SQL(之前是多线程并发深度翻页查MYSQL,mysql cpu飙升)
            if (batchNumExportUseId) {
                ExportIdRangeDTO idRangeRes = exportStorageBatchNumApi.findIdRange(params);
                logger.info(" taskId [{}] 开始-异步顺序导出,idRange={}",taskId, JSON.toJSONString(idRangeRes));
                if (idRangeRes != null && idRangeRes.isValid()) {
                    paramsObject.put("pageSize", StorageWebConstant.PURCHASE_CALL_PAGESIZE);

                    int pageCnt = (int)(idRangeRes.getCount()/StorageWebConstant.PURCHASE_CALL_PAGESIZE);
                    pageCnt = pageCnt + (idRangeRes.getCount()%StorageWebConstant.PURCHASE_CALL_PAGESIZE == 0 ? 0:1);
                    List<ExportIdRangeDTO> idRangeList = idRangeRes.splitByPageCount(pageCnt);

                    AtomicInteger pageNum = new AtomicInteger(0);
                    for (ExportIdRangeDTO idRange : idRangeList) {
                        int pn = pageNum.incrementAndGet();
                        Map<String, Object> exportParamMap = new HashMap<>();
                        exportParamMap.putAll(paramsObject);
                        exportParamMap.put("pageNum", pn);
                        exportParamMap.put("minId", idRange.getMinId());
                        exportParamMap.put("maxId", idRange.getMaxId());
                        logger.info("##  taskId [" + taskId + "]开始导出,第 " + pn + " 页 {}-{}", idRange.getMaxId(), idRange.getMaxId());
                        exportMap.putIfAbsent(pn, taskExecutor.submit(() -> storageReportService.listStorageBatchNumReportView(exportParamMap)));
                    }

                    for (int i = 1; i <= pageNum.get(); i++) {
                        List<StorageReportViewVo> list = exportMap.get(i).get().getList();
                        ExportExcelUtil.insertDataToExcel(work, colName, list, line, true);
                        line = line + list.size();
                    }
                }


            } 
相关推荐
o(╥﹏╥)16 分钟前
linux(ubuntu )卡死怎么强制重启
linux·数据库·ubuntu·系统安全
阿里嘎多学长30 分钟前
docker怎么部署高斯数据库
运维·数据库·docker·容器
Yuan_o_32 分钟前
Linux 基本使用和程序部署
java·linux·运维·服务器·数据库·后端
Sunyanhui137 分钟前
牛客网 SQL36查找后排序
数据库·sql·mysql
老王笔记1 小时前
MHA binlog server
数据库·mysql
lovelin+v175030409662 小时前
安全性升级:API接口在零信任架构下的安全防护策略
大数据·数据库·人工智能·爬虫·数据分析
DT辰白2 小时前
基于Redis的网关鉴权方案与性能优化
数据库·redis·缓存
2401_871213302 小时前
mysql高阶语句
数据库·mysql
Mitch3112 小时前
【漏洞复现】CVE-2021-45788 SQL Injection
sql·web安全·docker·prometheus·metersphere