
接口性能优化对于从事后端开发的同学来说,肯定再熟悉不过了,因为它是一个跟开发语言无关的公共问题。
该问题说简单也简单,说复杂也复杂。
有时候,只需加个索引就能解决问题。 有时候,需要做代码重构。 有时候,需要增加缓存。 有时候,需要引入一些中间件,比如mq。 有时候,需要需要分库分表。 有时候,需要拆分服务。 等等。。。导致接口性能问题的原因千奇百怪,不同的项目不同的接口,原因可能也不一样。
本文我总结了一些行之有效的,优化接口性能的办法,给有需要的朋友一个参考。
1.索引接口性能优化大家第一个想到的可能是:优化索引。
没错,优化索引的成本是最小的。
你通过查看线上日志或者监控报告,查到某个接口用到的某条sql语句耗时比较长。
这时你可能会有下面这些疑问:
该sql语句加索引了没?
加的索引生效了没?
mysql选错索引了没?
1.1 没加索引sql语句中where条件的关键字段,或者order by后面的排序字段,忘了加索引,这个问题在项目中很常见。
项目刚开始的时候,由于表中的数据量小,加不加索引sql查询性能差别不大。
后来,随着业务的发展,表中数据量越来越多,就不得不加索引了。
可以通过命令:
show index from `order`;
能单独查看某张表的索引情况。
也可以通过命令:
show create table `order`;
查看整张表的建表语句,里面同样会显示索引情况。
通过ALTER TABLE命令可以添加索引:
ALTER TABLE `order` ADD INDEX idx_name (name);
也可以通过CREATE INDEX命令添加索引:
CREATE INDEX idx_name ON `order` (name);
不过这里有一个需要注意的地方是:想通过命令修改索引,是不行的。
目前在mysql中如果想要修改索引,只能先删除索引,再重新添加新的。
删除索引可以用DROP INDEX命令:
ALTER TABLE `order` DROP INDEX idx_name;
用DROP INDEX命令也行:
DROP INDEX idx_name ON `order`;1.2 索引没生效
通过上面的命令我们已经能够确认索引是有的,但它生效了没?此时你内心或许会冒出这样一个疑问。
那么,如何查看索引有没有生效呢?
答:可以使用explain命令,查看mysql的执行计划,它会显示索引的使用情况。
例如:
explain select * from `order` where code='002';
结果:
通过这几列可以判断索引使用情况,执行计划包含列的含义如下图所示:
如果你想进一步了解explain的详细用法,可以看看我的另一篇文章《explain | 索引优化的这把绝世好剑,你真的会用吗?》
说实话,sql语句没有走索引,排除没有建索引之外,最大的可能性是索引失效了。
下面说说索引失效的常见原因:
如果不是上面的这些原因,则需要再进一步排查一下其他原因。
1.3 选错索引此外,你有没有遇到过这样一种情况:明明是同一条sql, 唐林只有入参不同而已。有的时候走的索引a,有的时候却走的索引b?
没错,有时候mysql会选错索引。
必要时可以使用force index来强制查询sql走某个索引。
至于为什么mysql会选错索引,后面有专门的文章介绍的,这里先留点悬念。
2. sql优化如果优化了索引之后,也没啥效果。
接下来试着优化一下sql语句,因为它的改造成本相对于java代码来说也要小得多。
下面给大家列举了sql优化的15个小技巧:
由于这些技巧在我之前的文章中已经详细介绍过了,在这里我就不深入了。
更详细的内容,可以看我的另一篇文章《聊聊sql优化的15个小技巧》,相信看完你会有很多收获。
3. 远程调用很多时候,我们需要在某个接口中,调用其他服务的接口。
比如有这样的业务场景:
在用户信息查询接口中需要返回:用户名称、性别、等级、头像、积分、成长值等信息。
而用户名称、性别、等级、头像在用户服务中,积分在积分服务中,成长值在成长值服务中。为了汇总这些数据统一返回,需要另外提供一个对外接口服务。
于是,用户信息查询接口需要调用用户查询接口、积分查询接口 和 成长值查询接口,然后汇总数据统一返回。
调用过程如下图所示:
调用远程接口总耗时 530ms = 200ms + 150ms + 180ms
显然这种串行调用远程接口性能是非常不好的,调用远程接口总的耗时为所有的远程接口耗时之和。
那么如何优化远程接口性能呢?
3.1 并行调用上面说到,既然串行调用多个远程接口性能很差,为什么不改成并行呢?
如下图所示:
调用远程接口总耗时 200ms = 200ms(即耗时最长的那次远程接口调用)
在java8之前可以通过实现Callable接口,获取线程返回结果。
java8以后通过CompleteFuture类实现该功能。我们这里以CompleteFuture为例:
public UserInfo getUserInfo(Long id) throws InterruptedException,产品中心 ExecutionException { final UserInfo userInfo = new UserInfo(); CompletableFuture userFuture = CompletableFuture.supplyAsync(() -> { getRemoteUserAndFill(id, userInfo); return Boolean.TRUE; }, executor); CompletableFuture bonusFuture = CompletableFuture.supplyAsync(() -> { getRemoteBonusAndFill(id, userInfo); return Boolean.TRUE; }, executor); CompletableFuture growthFuture = CompletableFuture.supplyAsync(() -> { getRemoteGrowthAndFill(id, userInfo); return Boolean.TRUE; }, executor); CompletableFuture.allOf(userFuture, bonusFuture, growthFuture).join(); userFuture.get(); bonusFuture.get(); growthFuture.get(); return userInfo; }
温馨提醒一下,这两种方式别忘了使用线程池。示例中我用到了executor,表示自定义的线程池,为了防止高并发场景下,出现线程过多的问题。
3.2 数据异构上面说到的用户信息查询接口需要调用用户查询接口、积分查询接口 和 成长值查询接口,然后汇总数据统一返回。
那么,我们能不能把数据冗余一下,把用户信息、积分和成长值的数据统一存储到一个地方,比如:redis,存的数据结构就是用户信息查询接口所需要的内容。然后通过用户id,直接从redis中查询数据出来,不就OK了?
如果在高并发的场景下,为了提升接口性能,远程接口调用大概率会被去掉,而改成保存冗余数据的数据异构方案。
但需要注意的是,如果使用了数据异构方案,就可能会出现数据一致性问题。
用户信息、积分和成长值有更新的话,大部分情况下,会先更新到数据库,然后同步到redis。但这种跨库的操作,可能会导致两边数据不一致的情况产生。
4. 重复调用重复调用在我们的日常工作代码中可以说随处可见,但如果没有控制好,会非常影响接口的性能。
不信,我们一起看看。
4.1 循环查数据库有时候,我们需要从指定的用户集合中,查询出有哪些是在数据库中已经存在的。
实现代码可以这样写:
public List<User> queryUser(List<User> searchList) { if (CollectionUtils.isEmpty(searchList)) { return Collections.emptyList(); } List<User> result = Lists.newArrayList(); searchList.forEach(user -> result.add(userMapper.getUserById(user.getId()))); return result; }
这里如果有50个用户,则需要循环50次,去查询数据库。我们都知道,每查询一次数据库,就是一次远程调用。
如果查询50次数据库,就有50次远程调用,这是非常耗时的操作。
那么,我们如何优化呢?
具体代码如下:
public List<User> queryUser(List<User> searchList) { if (CollectionUtils.isEmpty(searchList)) { return Collections.emptyList(); } List<Long> ids = searchList.stream().map(User::getId).collect(Collectors.toList()); return userMapper.getUserByIds(ids); }
提供一个根据用户id集合批量查询用户的接口,只远程调用一次,就能查询出所有的数据。
这里有个需要注意的地方是:id集合的大小要做限制,最好一次不要请求太多的数据。要根据实际情况而定,建议控制每次请求的记录条数在500以内。
4.2 死循环有些小伙伴看到这个标题,可能会感到有点意外,死循环也算?
代码中不是应该避免死循环吗?为啥还是会产生死循环?
有时候死循环是我们自己写的,例如下面这段代码:
while(true) { if(condition) { break; } System.out.println("do samething"); }
这里使用了while(true)的循环调用,这种写法在CAS自旋锁中使用比较多。
当满足condition等于true的时候,则自动退出该循环。
如果condition条件非常复杂,一旦出现判断不正确,或者少写了一些逻辑判断,就可能在某些场景下出现死循环的问题。
出现死循环,大概率是开发人员人为的bug导致的,不过这种情况很容易被测出来。
还有一种隐藏的比较深的死循环,是由于代码写的不太严谨导致的。如果用正常数据,可能测不出问题,但一旦出现异常数据,就会立即出现死循环。
4.3 无限递归如果想要打印某个分类的所有父分类,可以用类似这样的递归方法实现:
public void printCategory(Category category) { if(category == null