背景
线上有个页面查询接口卡死了,一直加载不出来。。
起因:我们有一个敏感词匹配的功能,用户可以通过接口查询敏感词触发记录。该接口会根据传入的敏感词、时间范围等条件,从数据库中查询匹配的文本记录,并返回分页结果。然而,随着数据量的增加,接口的性能逐渐下降,甚至在高并发或大数据量场景下会导致接口卡死,严重影响用户体验。
也就是说,Service层里面拼装数据,然后再手动分页返回。。少量数据肯定是可以应付过去,但是线上数据有几十万,一发布到线上肯定就崩了。
问题分析
- 全表扫描:每次查询都需要对聊天记录表进行全文搜索,数据量较大时性能较差。
- 实时计算:每次请求都需要实时计算匹配结果,无法利用缓存或预计算结果。
- 分页性能差:在大数据量下,分页查询的性能较差,尤其是
LIMIT
和OFFSET
的使用会导致数据库扫描大量无用数据。 - 接口响应慢:由于上述原因,接口在高并发或大数据量场景下响应时间过长,甚至卡死。
解决方案
为了解决上述问题,我们采取了以下优化措施:
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
进行分批次处理,避免内存溢出。
- 将匹配结果分批处理,每批最多 200 条记录,批量插入到
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了。