30s到0.8s,记录一次接口优化成功案例!

场景

在高并发的数据处理场景中,接口响应时间的优化显得尤为重要。本文将分享一个真实案例,其中一个数据量达到200万+的接口的响应时间从30秒降低到了0.8秒内。

这个案例不仅展示了问题诊断的过程,也提供了一系列有效的优化措施。

交易系统中,系统需要针对每一笔交易进行拦截(每一笔支付或转账就是一笔交易),拦截时需要根据定义好的规则拦截,这次需要优化的接口是一个统计规则拦截率的接口。

问题诊断

最初,接口的延迟非常高,大约需要30秒才能完成。为了定位问题,我们首先排除了网络和服务器设备因素,并打印了关键代码的执行时间。经过分析,发现问题出在SQL执行上。

发现Sql执行时间太久,查询200万条数据的执行时间竟然达到了30s,下面是是最耗时的部分相关代码逻辑:

查询代码(其实就是使用Mybatis查询,看起来正常的很)

List<Map<String, Object>> list = transhandleFlowMapper.selectDataTransHandleFlowAdd(selectSql);

统计当天的Id号(programhandleidlist字段)

SELECT programhandleidlist FROM anti_transhandle WHERE create_time BETWEEN '2024-01-08 00:00:00.0' AND '2024-01-09 00:00:00.0';

表结构(Postgresql)

我以为是Sql写的有问题,先拿着sql执行了一边,发现只执行sql的执行时间是大约800毫秒,和30秒差距巨大。

Sql层面分析

使用EXPLAIN ANALYZE函数分析sql。

EXPLAIN ANALYZE

SELECT programhandleidlist FROM anti_transhandle WHERE create_time BETWEEN '2024-01-08 00:00:00.0' AND '2024-01-09 00:00:00.0';

分析结果

看来是代码的部分有问题。

代码层面分析

List<Map<String, Object>> list = transhandleFlowMapper.selectDataTransHandleFlowAdd(selectSql);

Map的Key是programhandleIdList,Map的value是每一行的值。

在Java层面,每条数据都创建了一个Map对象,对于200万+的数据量来说,这显然是非常耗时的操作,速度是被创建了大量的Map集合给拖垮的。。

为了解决这个问题,我们尝试了将200万行数据转换为单行返回,使用PostgreSQL的array_agg和unnest函数来优化查询。

第一次遇到Mybatis查询返回导致接口速度慢的问题。

优化措施

1. SQL优化

我的思路是将200万行转为一行返回。

要将 PostgreSQL 中查询出的programhandleidlist字段(假设这是一个数组类型)的所有元素拼接为一行,您可以使用数组聚合函数array_agg结合 unnest 函数。

这样做可以先将数组展开为多行,然后将这些行再次聚合为一个单一的数组。如果您希望最终结果是一个字符串,而不是数组,您还可以使用 string_agg 函数。

以下是相应的 SQL 语句:

SELECT array_agg(elem) AS concatenated_array

FROM (

    SELECT unnest(programhandleidlist) AS elem

    FROM anti_transhandle

    WHERE create_time BETWEEN '2024-01-08 00:00:00.0' AND '2024-01-09 00:00:00.0'

) sub;

在这个查询中:

unnest(programhandleidlist)将programhandleidlist数组展开成多行。

string_agg(elem)将这些行聚合成一个以逗号分隔的字符串。

这将返回一个包含所有元素的单一数组。

查询结果由多行,拼接为了一行。

再测试,现在是正常速度了,但是查询时间依旧很高。Sql查询时间0.8秒,代码中平均1秒8左右,还有优化的空间。

将一列数据转换为了数组类型,查看一下内存占用,这一段占用了54比特,虽然占用不大,但是不知道为什么会mybatis处理时间这么久。

因为mybatis不知道数组的大小,先给数组设定一个初始大小,如果超出了数组长度,因为数组不能扩容,增加长度只能再复制一份到另一块内存中,复制的次数多了也就增加了计算时间。

数据需要在两个设备之间传输,磁盘和网络都需要时间。

2. 部分业务逻辑转到数据库中计算

再次优化sql,将一部分的逻辑放到Sql中处理,减少数据量。

业务上我需要统计programhandleidlist字段中id出现的次数,所以我直接在sql中做统计。

要统计每个数组中元素出现的次数,您需要首先使用 unnest 函数将数组展开为单独的行,然后使用 GROUP BY 和聚合函数(如 count)来计算每个元素的出现次数。这里是修改后的 SQL 语句:

SELECT elem, COUNT(*) AS count

FROM (

    SELECT unnest(programhandleidlist) AS elem

    FROM anti_transhandle

    WHERE create_time BETWEEN '2024-01-08 00:00:00.0' AND '2024-01-09 00:00:00.0'

) sub

GROUP BY elem;

在这个查询中:

unnest(programhandleidlist)将每个programhandleidlist数组展开成多个行。

GROUP BY elem对每个独立的元素进行分组。

COUNT(*)计算每个分组(即每个元素)的出现次数。

这个查询将返回两列:一列是元素(elem),另一列是该元素在所有数组中出现的次数(count)。

这条sql在代码中执行时间是0.7秒,还是时间太长,毕竟数据库的数据量太大,搜了很多方法,已经是我能做到的最快查询了。

关系型数据库 不适合做海量数据计算查询。

这个业务场景牵扯到了海量数据的统计,并不适合使用关系型数据库,如果想要真正的做到毫秒级的查询,需要从设计上改变数据的存储结果。比如使用cilckhouse、hive等存储计算。

3. 引入缓存机制

减少查询数据库的次数,决定引入本地缓存机制。选择了Caffeine作为缓存框架,易于与Spring集成。

分析业务后,当天的统计数据必须查询数据库,但是查询历史日期的采用缓存的方式。如果业务中对时效性不敏感,也可以缓存当天的数据,每隔一段时间更新一次。我这里采用缓存历史日期的数据。

1.引入Caffeine依赖

<dependency>

    <groupId>com.github.ben-manes.caffeine</groupId>

    <artifactId>caffeine</artifactId>

    <version>3.1.8</version>

</dependency>

2.配置Caffeine缓存

创建一个专门的Caffeine缓存配置。使用本地缓存选择淘汰策略很重要,由于我的业务场景使根据实现来查询,所以Caffeine将按照最近最少使用(LRU)的策略来淘汰旧数据成符合业务。

import com.github.benmanes.caffeine.cache.Caffeine;

import org.springframework.cache.CacheManager;

import org.springframework.cache.annotation.EnableCaching;

import org.springframework.cache.caffeine.CaffeineCacheManager;

import org.springframework.context.annotation.Bean;

import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

@Configuration

@EnableCaching

public class CacheConfig {

    @Bean

    public CacheManager cacheManager() {

        CaffeineCacheManager cacheManager = new CaffeineCacheManager();

        cacheManager.setCaffeine(Caffeine.newBuilder()

                .maximumSize(500)

                .expireAfterWrite(60, TimeUnit.MINUTES));

        return cacheManager;

    }

}

3.修改ruleHitRate方法来使用Caffeine缓存

在计算昨天命中率的逻辑前加入缓存检查和更新的逻辑。

使用Caffeine缓存:

@Autowired

private CacheManager cacheManager; // 注入Spring的CacheManager

private static final String YESTERDAY_HIT_RATE_CACHE = "hitRateCache"; // 缓存名称

@Override

public RuleHitRateResponse ruleHitRate(LocalDate currentDate) {

    // ... 其他代码 ...

    // 使用缓存获取昨天的命中率

    double hitRate = cacheManager.getCache(YESTERDAY_HIT_RATE_CACHE).get(currentDate.minusDays(1), () -> {

    // 查询数据库

        Map<String, String> hitRateList = dataTunnelClient.selectTransHandleFlowByTime(currentDate.minusDays(1));


  // ... 其他代码 ...

  // 返回计算后的结果

        return hitRate;

    });

    // ... 其他代码 ...

}

总结

最后,测试接口,成功将接口从30秒降低到了0.8秒以内。

这次优化让我重新真正审视了关系型数据库的劣势。选择哪种类型的数据库,取决于具体的应用场景和需求。

关系型数据库(Mysql、Oracle等)适合事务性强、数据一致性和完整性要求高的应用。

列式数据库(HBase、ClickHouse等)则适合大数据量的分析和统计,特别是在读取性能方面有显著优势。

此次的业务场景显然更适合使用列式数据库,所以导致使用关系型数据库无论如何也不能够达到足够高的性能。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容