1、背景
针对老项目,去年做了许多降本增效的事情,其中发现最多的就是接口耗时过长的问题,就集中搞了一次接口性能优化。本文将给小伙伴们分享一下接口优化的通用方案。
二、接口优化方案总结
1、批处理
批量思想:批量操作数据库,这个很好理解,我们在循环插入场景的接口中,可以在批处理执行完成后一次性插入或更新数据库,避免多次IO。 基于 Guava 对集合进行拆分防止 Mybatis 数据量过多造成的插入失败
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
</dependency>
public static <T> List<List<T>> partition(List<T> list, int size) {
Preconditions.checkNotNull(list);
Preconditions.checkArgument(size > 0);
return (List)(list instanceof RandomAccess ? new Lists.RandomAccessPartition(list, size) : new Lists.Partition(list, size));
}
batchInsert(List<T> partiion);
2、异步
异步思想:针对耗时比较长且不是结果必须的逻辑,我们可以考虑放到异步执行,这样能降低接口耗时。
例如一个理财的申购接口,入账和写入申购文件是同步执行的,因为是T+1交易,后面这两个逻辑其实不是结果必须的,我们并不需要关注它的实时结果,所以我们考虑把入账和写入申购文件改为异步处理。如图所示:
于异步的实现方式,可以用线程池,也可以用消息队列,还可以用一些调度任务框架。
3、空间换时间
一个很好理解的空间换时间的例子是合理使用缓存,针对一些频繁使用且不频繁变更的数据,可以提前缓存起来,需要时直接查缓存,避免频繁地查询数据库或者重复计算。
需要注意的事,这里用了合理二字,因为空间换时间也是一把双刃剑,需要综合考虑你的使用场景,毕竟缓存带来的数据一致性问题也挺令人头疼。
这里的缓存可以是R2M,也可以是本地缓存、memcached,或者Map。 举一个股票工具的查询例子:
因为策略轮动的调仓信息,每周只更新一次,所以原来的调接口就去查库的逻辑并不合理,而且拿到调仓信息后,需要经过复杂计算,最终得出回测收益和跑赢沪深指数这些我们想要的结果。如果我们把查库操作和计算结果放入缓存,可以节省很多的执行时间。如图:
4、预处理
也就是预取思想,就是提前要把查询的数据,提前计算好,放入缓存或者表中的某个字段,用的时候会大幅提高接口性能。跟上面那个例子很像,但是关注点不同。
举个简单的例子:理财产品,会有根据净值计算年化收益率的数据展示需求,利用净值去套用年化收益率计算公式计算的逻辑我们可以采用预处理,这样每一次接口调用直接取对应字段就可以了。
5、预处理
我们都用过数据库连接池,线程池等,这就是池思想的体现,它们解决的问题就是避免重复创建对象或创建连接,可以重复利用,避免不必要的损耗,毕竟创建销毁也会占用时间。
池化思想包含但并不局限于以上两种,总的来说池化思想的本质是预分配与循环使用,明白这个原理后,我们即使是在做一些业务场景的需求时,也可以利用起来。
比如:对象池
6、串行改并行
串行就是,当前执行逻辑必须等上一个执行逻辑结束之后才执行,并行就是两个执行逻辑互不干扰,所以并行相对来说就比较节省时间,当然是建立在没有结果参数依赖的前提下。
比如,理财的持仓信息展示接口,我们既需要查询用户的账户信息,也需要查询商品信息和banner位信息等等来渲染持仓页,如果是串行,基本上接口耗时就是累加的。如果是并行,接口耗时将大大降低。
如图:
7、索引
加索引能大大提高数据查询效率,这个在接口设计之处也会考虑到,这里不再多赘述,随着需求的迭代,我们重点整理一下索引不生效的一些场景,希望对小伙伴们有所帮助。 具体不生效场景不再一一举例,后面有时间的话,单独整理一下。
8、避免大事物
所谓大事务问题,就是运行时间较长的事务,由于事务一致不提交,会导致数据库连接被占用,影响到别的请求访问数据库,影响别的接口性能。
@Transactional(value = "taskTransactionManager", propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, rollbackFor = {RuntimeException.class, Exception.class})
public BasicResult purchaseRequest(PurchaseRecord record) {
BasicResult result = new BasicResult();
...
pushRpc.doPush(record);
result.setInfo(ResultInfoEnum.SUCCESS);
return result;
}
提示: 如果涉及到与外部中间件调用或者涉及到过多的数据表调用请使用编程式事务 所以为避免大事务问题,我们可以通过以下方案规避: 1,RPC调用不放到事务里面 2,查询操作尽量放到事务之外 3,事务中避免处理太多数据
9、优化程序结构
程序结构问题一般出现在多次需求迭代后,代码叠加形成。会造成一些重复查询、多次创建对象等耗时问题。在多人维护一个项目时比较多见。解决起来也比较简单,我们需要针对接口整体做重构,评估每个代码块的作用和用途,调整执行顺序。
10、深分页问题
深分页问题比较常见,分页我们一般最先想到的就是 limit ,为什么会慢,我们可以看下这个SQL:
select id,name,balance from account where update_time> '2020-09-19' limit 100000,10;
子查询优化
select id,name,balance FROM account where id >= (select a.id from account a where a.update_time >= '2020-09-19' limit 100000, 1) LIMIT 10;
延迟关联查询
SELECT acct1.id,acct1.name,acct1.balance FROM account acct1 INNER JOIN (SELECT a.id FROM account a WHERE a.update_time >= '2020-09-19' ORDER BY a.update_time LIMIT 100000, 10) AS acct2 on acct1.id= acct2.id;
11、SQL 优化
SQL 优化能大幅提高接口的查询性能,由于本文重点讲述接口优化的方案。
12、锁粒度避免过粗
//非共享资源
private void notShare(){
}
//共享资源
private void share(){
}
private int right(){
notShare();
synchronized (this) {
share();
}
}
最后
接口性能问题形成的原因思考 我相信很多接口的效率问题不是一朝一夕形成的,在需求迭代的过程中,为了需求快速上线,采取直接累加代码的方式去实现功能,这样会造成以上这些接口性能问题。
变换思路,更高一级思考问题,站在接口设计者的角度去开发需求,会避免很多这样的问题,也是降本增效的一种行之有效的方式。