目录
在工作过程中,程序员经常会遇到接口响应过慢,这时候,就需要对程序接口进行优化。
1.数据量比较大,批量操作数据入库
优化前:
//for循环单笔入库
for(orderDetail detail:list){
insert(detail);
}
优化后:
// 批量入库,mybatis demo实现
<insert id="insertBatch" parameterType="java.util.List">
insert into trans_detail( id,orderNo,amount,payer,payee) values
<foreach collection="list" item="item" index="index" separator=",”>
( #{item.id},#{item.orderNo}, #{item.amount},#{item.payer},#{item.payee})
</foreach>
</insert>
单位(ms)
|
for循环单笔入库
|
批量入库
|
500条
|
1427
|
1143
|
1000条
|
1866
|
1411
|
2.耗时操作考虑异步处理
比如有个用户请求接口中,需要做业务操作,给运营发送邮件,和记录操作日志。为了实现起来比较方便,通常我们会将这些逻辑放在接口中同步执行,势必会对接口性能造成一定的影响。
接口内部流程图如下:
给运营发送邮件和添加用户操作日志功能,被提交到了两个单独的线程池中。
这样接口中重点关注的是业务操作,把其他的逻辑交给线程异步执行,这样改造之后,让接口性能瞬间提升了。
但使用线程池有个小问题就是:如果服务器重启了,或者是需要被执行的功能出现异常了,无法重试,会丢数据。
那么这个问题该怎么办呢?
使用 mq 改造之后,接口逻辑如下:
对于发送邮件和用户操作日志功能,在接口中并没真正实现,它只发送了 mq 消息到 mq 服务器。然后由 mq 消费者消费消息时,才真正的执行这两个功能。
这样改造之后,接口性能同样提升了,因为发送 mq 消息速度是很快的,我们只需关注业务操作的代码即可。
3.合理恰当并行调用
在开发接口时,我们常常需要在某个接口中,调用其他服务的接口。
比如有这样的业务场景:
在用户信息查询接口中需要返回多个信息:用户名称、地址、性别、等级、积分等信息。
而用户名称、性别、等级、头像在用户服务中,用户名等在个人信息基础服务中,等级在等级服务中,积分在积分值服务中。为了汇总这些数据统一返回,需要另外提供一个对外接口服务。
于是,用户信息查询接口需要调用用户查询接口、等级查询接口和积分成长值查询接口,然后汇总数据统一返回。
调用过程如下图所示:
调用远程接口总耗时 530ms = (用户)200ms + (等级)150ms + (成长值)180ms
显然这种串行调用远程接口性能是非常不好的,调用远程接口总的耗时为所有的远程接口耗时之和。这样无疑会影响到接口相应速度。
我们可以进行如下操作
// 查询客户基础信息
CompletableFuture<ClientBase> f1 = CompletableFuture
.supplyAsync(() -> clientBaseMapper.queryByClientId(clientId),CommonThreadPool.getExecutor());
// 查询客户等级信息
CompletableFuture<ClientLevel> f2 = CompletableFuture
.supplyAsync(() -> clientLevelMapper.queryByClientId(clientId), CommonThreadPool.getExecutor());
// 查询客户成长值信息
CompletableFuture<ClientGrowthLevel> f3 = CompletableFuture
.supplyAsync(() -> clientGrowthLevel.queryByClientId(clientId), CommonThreadPool.getExecutor());
CompletableFuture<Void> total = CompletableFuture
.allOf(f1, f2, f3);
total.get();
4.合理拆分接口
在开发接口时,我们常常需要在某个接口中,调用其他服务的接口。
比如有这样的业务场景:
在用户信息查询接口中需要返回多个信息:用户基础信息,用户头像等。
有些开发者会把这作为一个接口进行开发:用户综合接口,但把该接口拆分成用户基础信息接口,和用户头像接口无疑会提高用户综合接口的相应速度。
5.合理使用缓存
在部分业务场景,恰当地使用缓存,是可以大大提高接口性能的。这里的缓存包括:Redis,JVM本地缓存,memcached,或者Map等。
在写代码中,我们通常会遇到这样一种情况,我们的枚举值配置在数据库中,而查询客户基本信息列表无疑会查询返回证件类型等字段。
如果我们查询中获取到证件类型的枚举,不要在for循环中去逐条调用查询枚举配置,而是需要在外层查询一次证件类型枚举,转换成map,然后从map中取值填充。
我们可以进行如下操作
Category parent = categoryMapper.getCategoryByCode("CertificateType");
Map map = parent.stream().collect(Collectors.toMap(Category::getCode, Category::getDesc);
for(Client client :List){
client.setCertificateTypeName(map.get(client.getCertificateType()))
}
6.优化程序逻辑、代码
避免出现死循环
有些时候,写代码一不留神,循环语句就出现死循环了。
出现这种情况往往就是 condition 条件没处理好,导致没有退出循环,从而导致死循环。
出现死循环,大概率是代码的 bug 导致的,不过这种情况很容易被测出来。
但是,可能还有一种比较隐秘的死循环代码,当用正常数据时,测不出问题,一旦出现有异常数据,才会复现死循环的问题。
避免无限递归
无限递归的场景会严重影响接口性能,最终会发生堆栈溢出。总之,在写递归代码时,建议设置递归深度(假设限定为 5),然后在递归方法中做一定判断,如果深度大于 5 时,则自动返回,这样就可以避免无限递归了。
7.SQL优化(比如:添加索引)
我们可以通过这些方式优化我们的SQL:
加索引
避免返回不必要的数据
优化sql结构
分库分表
读写分离
8.压缩传输内容
压缩传输内容,文件变得更小,因此传输会更快啦。10M带宽,传输10k的报文,一般比传输1M的会快呀;打个比喻,一匹千里马,它驮着一百斤的货跑得快,还是驮着10斤的货物跑得快呢?
解析:如果你的接口性能不好,且传输报文比较大的话,这时候是可以考虑压缩文件内容传输的,可能会有意想不到的效果。
9.考虑使用文件/MQ等其他方式暂存,异步再落地DB
如果数据太大,落地数据库实在是慢的话,可以考虑先用文件的方式保存,或者考虑MQ,先落地,再异步保存到数据库。
然后再从mq中消费数据,或者读取文件内容,在进行业务处理。
如果耗时瓶颈就在数据库插入操作这里了,那就考虑文件保存或者MQ或者其他方式暂存吧,文件保存数据,对比一下耗时,有时候会有意想不到的效果哦。
10.跟产品讨论需求最恰当,最合理的实现方式
比如有个页面需要展示客户好友列表的需求,产品说要展示所有的客户好友列表,如果一个用户的客户列表信息好大,你拉取所有客户数据回来,接口性能就降下来了。如果产品打桩分析,会发现,一般用户看客户列表,也就看前几页,或者可以做个好友列表按钮,这样就不会影响用户也买你的展示了。那个超大分页加载问题也是类似的。即limit +一个超大的数,一般会很慢的。
更多消息资讯,请访问昂焱数据。