大批量数据查询接口优化方案之定时任务

384 阅读4分钟

背景

线上有个页面查询接口卡死了,一直加载不出来。。

起因:我们有一个敏感词匹配的功能,用户可以通过接口查询敏感词触发记录。该接口会根据传入的敏感词、时间范围等条件,从数据库中查询匹配的文本记录,并返回分页结果。然而,随着数据量的增加,接口的性能逐渐下降,甚至在高并发或大数据量场景下会导致接口卡死,严重影响用户体验。

也就是说,Service层里面拼装数据,然后再手动分页返回。。少量数据肯定是可以应付过去,但是线上数据有几十万,一发布到线上肯定就崩了。

问题分析

  1. 全表扫描:每次查询都需要对聊天记录表进行全文搜索,数据量较大时性能较差。
  2. 实时计算:每次请求都需要实时计算匹配结果,无法利用缓存或预计算结果。
  3. 分页性能差:在大数据量下,分页查询的性能较差,尤其是 LIMIT 和 OFFSET 的使用会导致数据库扫描大量无用数据。
  4. 接口响应慢:由于上述原因,接口在高并发或大数据量场景下响应时间过长,甚至卡死。

解决方案

为了解决上述问题,我们采取了以下优化措施:

1. 引入定时任务预计算匹配结果

  • 背景:敏感词匹配的结果在一定时间内是相对静态的,可以通过定时任务提前计算并存储匹配结果。

  • 实现

    • 新增一张表 message_sensitive_match,用于存储敏感词匹配记录。
    • 编写定时任务,定期从 message_text 表中查询匹配记录,并将结果批量插入到 message_sensitive_match 表中。
    • 定时任务每次运行时,只查询自上次运行以来的新增数据,避免重复计算。

2. 优化分页查询

  • 背景:直接对 message_text 表进行分页查询性能较差,尤其是 LIMIT 和 OFFSET 的使用会导致数据库扫描大量无用数据。

  • 实现

    • 改为从 message_sensitive_match 表中查询匹配记录,利用预计算结果提高查询性能。
    • 使用基于主键的分页方式(如 WHERE id > last_id LIMIT page_size),避免 OFFSET 的性能问题。

3. 批量插入与分批次处理

  • 背景:定时任务需要处理大量数据,直接单条插入会导致性能问题。

  • 实现

    • 将匹配结果分批处理,每批最多 200 条记录,批量插入到 message_sensitive_match 表中。
    • 使用 List.subList 进行分批次处理,避免内存溢出。

4. 接口优化

  • 背景:原接口每次请求都需要实时计算匹配结果,性能较差。

  • 实现

    • 改为从 message_sensitive_match 表中查询匹配记录,利用预计算结果提高查询性能。
    • 支持按时间范围、敏感词等条件过滤,直接返回分页结果。

优化后的代码示例

1. 定时任务代码

@Scheduled(fixedDelay = 500000)
public void getMessageSensitiveMatchList() {
    log.info("----------------开始执行敏感词匹配定时任务------------------");

    // 1. 查询 message_sensitive_match 表,获取每个 message_sensitive 组最新的匹配记录时间
    List<LatestMatchRecord> latestMatchRecords = messageSensitiveMapper.findLatestMatchTimes();

    // 获取分割后的敏感词列表
    List<MessageSensitive> sensitiveWordList = sensitiveWordService.getSensitiveWordList(null, null);

    final int BATCH_SIZE = 200; // 批量插入的最大数据量

    // 遍历每个敏感词,查询匹配记录并处理
    for (MessageSensitive sensitive : sensitiveWordList) {
        Long sendStartTimestamp = null;
        Long sendEndTimestamp = null;

        // 2. 获取当前敏感词的最新匹配时间
        Optional<LatestMatchRecord> latestMatch = latestMatchRecords.stream()
                .filter(record -> record.getSensitiveId().equals(sensitive.getSensitiveId()))
                .findFirst();

        if (latestMatch.isPresent()) {
            sendStartTimestamp = latestMatch.get().getLatestTime();
        }

        // 3. 查询当前敏感词的匹配消息
        List<MessageText> messagesBySingleWord = messageTextMapper.findMessagesBySingleWord(
                sensitive.getSensitiveWord(), sendStartTimestamp, sendEndTimestamp
        );

        // 4. 处理匹配到的消息,业务代码
        if (messagesBySingleWord != null && !messagesBySingleWord.isEmpty()) {
            List<SensitiveMatchVo> matchVoList = new ArrayList<>();
            // 业务代码处理,matchVoList是最终结果
            
            ...
            
            for (MessageText message : messagesBySingleWord) {

                    // **当 matchVoList 达到 BATCH_SIZE 时,先执行批量插入**
                    if (matchVoList.size() >= BATCH_SIZE) {
                        processAndInsertBatch(matchVoList);
                    }
                }
            }

            // **如果还有剩余数据没有插入,执行最后一次插入**
            if (!matchVoList.isEmpty()) {
                processAndInsertBatch(matchVoList);
            }
        }
    }
}

/**
 * 处理数据并进行批量插入 (严格 200 条一批)
 */
private void processAndInsertBatch(List<SensitiveMatchVo> matchVoList) {
    if (matchVoList.isEmpty()) {
        return;
    }

    // 7. 批量插入数据库
    int totalRecords = matchVoList.size();
    int batchSize = 200;  // 设定批量插入的大小
    int startIndex = 0;

    while (startIndex < totalRecords) {
        int endIndex = Math.min(startIndex + batchSize, totalRecords);
        List<SensitiveMatchVo> subBatch = matchVoList.subList(startIndex, endIndex);

        long inserted = messageSensitiveMapper.insertSensitiveMatches(subBatch);
        log.info("本次插入敏感词匹配记录 {} 条", inserted);

        startIndex = endIndex; // 继续下一批
    }

    // 8. 清空列表,释放内存
    matchVoList.clear();
}

2. 接口查询代码

现在原来的接口直接就能分页查询新建的表,速度肯定不一样了。

@Override
public Page<SensitiveMatchVo> getMessageSensitiveMatchList(QeryDTO queryDTO) {

    Page<SensitiveMatchVo> matchList = messageSensitiveMapper.findSensitiveMatches(
            new PageCust<>(queryDTO));

    return matchList;
}

小问题

  • 重新建立一张表,设置定时任务将数据跑进去,感觉只是权宜之计。如果我需要实时搜索到对应敏感词记录怎么办?

那直接上ElasticSearch吧。。这是真好用

个人觉得还是要根据业务的需求,以及数据的大小来选择。像这种敏感词匹配,当初选择用pgsql存储,是因为觉得他数据量也是只有几十万,没想过引入ElasticSearch,直接用了PgSql+全文索引。如果千万级别就可用ElasticSearch了。