likes
comments
collection
share

为了解决count查询慢而写的分页查询总量缓存器

作者站长头像
站长
· 阅读数 20

项目上出现了分页查询接口慢的问题,慢的原因是表数据量大,count查询很慢。

分页查询接口有两次查询:

  • 一次查分页数据,比如15条:

    SELECT * FROM sys_user LIMIT 1,15;
    
  • 另一次查数据总量:

    SELECT COUNT(*) FROM sys_user;
    

count查询需要一行行读数据,累加起来得到总量,效率很低。

要解决count查询慢问题,一般有以下几种方案:

  • 优化count查询语句,走索引、减少JOIN表

    一行语句,没有可优化空间,pass。

  • 分页接口的两次查询用多线程同时进行,节省一点时间

    查询LIMIT分页数据,靠前的数据一般都不慢,多线程增加代码复杂性,而且目前的问题是count耗时长,所以该方案也pass。

  • 第一次查询后缓存起来,设置一个合适的过期时间,缓存期间可以快速响应

    简单可行,缺点是数据实时性不高,新增、删除的数量短期内不会更新,不适合用在高实时性的场景下。

  • 使用EXPLAIN获取的预估行数

    响应快,其原理是根据数据总大小和采样的行数据大小做计算得出的预估行数,这是一个近似值,不适合要求高精确度的场景。

  • 转移数据到其他查询快的数据库,如Elasticsearch、ClickHouse

    既然MySQL不中用,那就换其他的存储引擎,虽然解决了查询慢的问题,但是工作量激增。

本文选择的是查询缓存的方案,因为不要求高实时性。

代码编写

现在编写代码,写一个分页数量缓存工具类:PageCountCacheManager

实现内容:

提供缓存接口函数,支持设置缓存条件:查询时间超过指定值、查询数量超过指定值,当满足任一条件即可触发缓存机制。

直接贴代码:

import com.cc.utils.CacheUtil;

import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;

/**
 * 分页查询的数据总量缓存器
 *
 * @author cc
 * @date 2023-09-18 10:28
 */
public class PageCountCacheManager {

    /**
     * 查询时间超过这个值就缓存,单位(秒)
     */
    private static final long TIME = 5;

    /**
     * 查询数量超过这个值就缓存
     */
    private static final long AMOUNT = 100000;

    /**
     * 缓存过期时间,单位(秒)
     */
    private static final long EXPIRE_TIME = 60;

    /**
     * 按查询时间来缓存,超过一定时间的查询就缓存起来
     */
    public static long cacheByTime(String key, Callable<Long> task) {
        key = "PageCount_Time_" + key;
        return run(key, task, true, false, false, TIME, 0, EXPIRE_TIME);
    }

    public static long cacheByTime(String key, Callable<Long> task, long time, long expireTime) {
        key = "PageCount_Time_" + key;
        return run(key, task, true, false, false, time, 0, expireTime);
    }

    /**
     * 按查询数量来缓存,超过一定数量的查询就缓存起来
     */
    public static long cacheByAmount(String key, Callable<Long> task) {
        key = "PageCount_Amount_" + key;
        return run(key, task, false, true, false, 0, AMOUNT, EXPIRE_TIME);
    }

    public static long cacheByAmount(String key, Callable<Long> task, long amount, long expireTime) {
        key = "PageCount_Amount_" + key;
        return run(key, task, false, true, false, 0, amount, expireTime);
    }

    /**
     * 结合查询时间和查询数量来缓存,当超过一定时间或一定数量就缓存起来
     */
    public static long cacheByBoth(String key, Callable<Long> task) {
        key = "PageCount_Both_" + key;
        return run(key, task, false, false, true, TIME, AMOUNT, EXPIRE_TIME);
    }

    public static long cacheByBoth(String key, Callable<Long> task, long time, long amount, long expireTime) {
        key = "PageCount_Both_" + key;
        return run(key, task, false, false, true, time, amount, expireTime);
    }

    /*************** 私有方法 ***************/
    private static long run(String key, Callable<Long> task, boolean byTime, boolean byAmount, boolean byBoth, long time, long amount, long expireTime) {
        long start = System.currentTimeMillis();

        // 查数据
        Long result = null;
        try {
            result = task.call();
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 查询结果为空,令其为0
        if (result == null) {
            result = 0L;
        }

        if (byTime || byBoth) {
            // 判断时间
            long executionTime = System.currentTimeMillis() - start;
            if (executionTime > time) {
                // 缓存数据
                CacheUtil.set(key, result, expireTime, TimeUnit.SECONDS);
            }
        }

        if (byAmount || byBoth) {
            // 判断数量
            if (result > amount) {
                // 缓存数据
                CacheUtil.set(key, result, expireTime, TimeUnit.SECONDS);
            }
        }
        return result;
    }
}

其中CacheUtil是缓存类,可以是JVM内缓存,也可以是RedisTemplate缓存,这个依项目而定。

使用方式:

// 这是Service的查询函数
@Override
public long countByCondition(BaseCondition condition) {
    String key = this.getClass().getName();
    return PageCountCacheManager.cacheByTime(key, ()-> mapper.countByCondition(condition));
}