网站图片一般多大,软件开发公司的优势,花钱做网站不给源代码,wordpress ajax文章接口性能优化对于从事后端开发的同学来说#xff0c;肯定再熟悉不过了#xff0c;因为它是一个跟开发语言无关的公共问题。
该问题说简单也简单#xff0c;说复杂也复杂。
有时候#xff0c;只需加个索引就能解决问题。
有时候#xff0c;需要做代码重构。
有时候肯定再熟悉不过了因为它是一个跟开发语言无关的公共问题。
该问题说简单也简单说复杂也复杂。
有时候只需加个索引就能解决问题。
有时候需要做代码重构。
有时候需要增加缓存。
有时候需要引入一些中间件比如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 code002;结果 通过这几列可以判断索引使用情况执行计划包含列的含义如下图所示 说实话sql语句没有走索引排除没有建索引之外最大的可能性是索引失效了。
下面说说索引失效的常见原因 如果不是上面的这些原因则需要再进一步排查一下其他原因。
1.3 选错索引
此外你有没有遇到过这样一种情况明明是同一条sql只有入参不同而已。有的时候走的索引a有的时候却走的索引b
没错有时候mysql会选错索引。
必要时可以使用force index来强制查询sql走某个索引。
至于为什么mysql会选错索引后面有专门的文章介绍的这里先留点悬念。
2. sql优化
如果优化了索引之后也没啥效果。
接下来试着优化一下sql语句因为它的改造成本相对于java代码来说也要小得多。
下面给大家列举了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 ListUser queryUser(ListUser searchList) {if (CollectionUtils.isEmpty(searchList)) {return Collections.emptyList();}ListUser result Lists.newArrayList();searchList.forEach(user - result.add(userMapper.getUserById(user.getId())));return result;
}这里如果有50个用户则需要循环50次去查询数据库。我们都知道每查询一次数据库就是一次远程调用。
如果查询50次数据库就有50次远程调用这是非常耗时的操作。
那么我们如何优化呢
具体代码如下
public ListUser queryUser(ListUser searchList) {if (CollectionUtils.isEmpty(searchList)) {return Collections.emptyList();}ListLong 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 || category.getParentId() null) {return;} System.out.println(父分类名称 category.getName());Category parent categoryMapper.getCategoryById(category.getParentId());printCategory(parent);
}正常情况下这段代码是没有问题的。
但如果某次有人误操作把某个分类的parentId指向了它自己这样就会出现无限递归的情况。导致接口一直不能返回数据最终会发生堆栈溢出。 建议写递归方法时设定一个递归的深度比如分类最大等级有4级则深度可以设置为4。然后在递归方法中做判断如果深度大于4时则自动返回这样就能避免无限循环的情况。 5. 异步处理
有时候我们接口性能优化需要重新梳理一下业务逻辑看看是否有设计上不太合理的地方。
比如有个用户请求接口中需要做业务操作发站内通知和记录操作日志。为了实现起来比较方便通常我们会将这些逻辑放在接口中同步执行势必会对接口性能造成一定的影响。
接口内部流程图如下 这个接口表面上看起来没有问题但如果你仔细梳理一下业务逻辑会发现只有业务操作才是核心逻辑其他的功能都是非核心逻辑。 在这里有个原则就是核心逻辑可以同步执行同步写库。非核心逻辑可以异步执行异步写库。 上面这个例子中发站内通知和用户操作日志功能对实时性要求不高即使晚点写库用户无非是晚点收到站内通知或者运营晚点看到用户操作日志对业务影响不大所以完全可以异步处理。
通常异步主要有两种多线程 和 mq。
5.1 线程池
使用线程池改造之后接口逻辑如下 发站内通知和用户操作日志功能被提交到了两个单独的线程池中。
这样接口中重点关注的是业务操作把其他的逻辑交给线程异步执行这样改造之后让接口性能瞬间提升了。
但使用线程池有个小问题就是如果服务器重启了或者是需要被执行的功能出现异常了无法重试会丢数据。
那么这个问题该怎么办呢
5.2 mq
使用mq改造之后接口逻辑如下 对于发站内通知和用户操作日志功能在接口中并没真正实现它只发送了mq消息到mq服务器。然后由mq消费者消费消息时才真正的执行这两个功能。
这样改造之后接口性能同样提升了因为发送mq消息速度是很快的我们只需关注业务操作的代码即可。
6. 避免大事务
很多小伙伴在使用spring框架开发项目时为了方便喜欢使用Transactional注解提供事务功能。
没错使用Transactional注解这种声明式事务的方式提供事务功能确实能少写很多代码提升开发效率。
但也容易造成大事务引发其他的问题。
下面用一张图看看大事务引发的问题。 从图中能够看出大事务问题可能会造成接口超时对接口的性能有直接的影响。
我们该如何优化大事务呢 少用Transactional注解 将查询(select)方法放到事务外 事务中避免远程调用 事务中避免一次性处理太多数据 有些功能可以非事务执行 有些功能可以异步处理 ——EOF——
福利
扫码回复【酒店】可免费领取酒店管理系统源码