章
目
录
最近研究项目性能优化的时候,发现接口性能优化这块技巧挺多。正好看到京东云大佬分享的12种接口优化方案,感觉特别实用,今天就来给大伙唠唠。要是你也在为接口性能发愁,那可得仔细看看,建议先收藏起来,方便以后查阅!
一、为啥要做接口优化
之前在搞一个老项目的时候,为了降本增效做了不少工作,其中发现接口耗时过长的问题特别突出。于是就专门针对这个问题进行了一波接口性能优化。下面就给大家分享一下这些通用的接口优化方案,都是干货,拿走不谢!
二、接口优化实用方案
(一)批处理:减少数据库IO开销
在操作数据库的时候,批量操作能大大提高效率。比如说在循环插入数据的场景下,如果一条一条地插入数据库,会产生多次IO操作,这就比较耗时。咱们可以把数据攒起来,等一批数据都准备好了,再一次性插入或更新数据库。
// 这种for循环单笔入库的方式不可取,每次循环都执行一次插入操作,频繁与数据库交互
list.stream().forEach(msg -> {
insert();
});
// 改成批量入库就好多了,只需要一次数据库操作,大大减少了IO开销
batchInsert();
(二)异步处理:让接口“轻装上阵”
有些逻辑虽然耗时比较长,但对接口返回结果来说并不是必须马上得到的。这种情况下,我们可以把这些逻辑放到异步去执行,这样能有效降低接口的响应时间。
打个比方,在一个理财申购接口里,入账和写入申购文件这两个操作是同步执行的。但因为是T + 1交易,其实我们并不需要实时关注这两个操作的结果,所以就可以把它们改成异步处理。实现异步的方式有好几种,可以用线程池,也可以借助消息队列,或者使用调度任务框架。
(三)空间换时间:巧用缓存提升效率
合理使用缓存是典型的空间换时间策略。对于那些经常会用到,而且数据变更不太频繁的数据,我们可以提前把它们缓存起来。这样,当需要这些数据的时候,直接从缓存里取,就不用频繁地查询数据库或者重复计算了。
比如说,有个股票工具需要查询策略轮动的调仓信息,这些信息每周才更新一次。之前每次调接口都去查库,查到信息后还要经过复杂计算才能得到回测收益和跑赢沪深指数这些结果。要是把查库操作和计算结果都放到缓存里,就能节省不少执行时间。
不过要注意,缓存虽然好用,但也会带来数据一致性的问题,所以在使用的时候得综合考虑实际场景。缓存的选择也很多,像R2M、本地缓存、memcached,甚至是普通的Map都可以用来做缓存。
(四)预处理:提前准备,快速响应
预处理,简单来说就是预取思想。我们可以提前把查询需要的数据计算好,然后放到缓存里,或者存到表中的某个字段里。这样,每次接口调用的时候,直接取这些预处理好的数据就行,能大幅提升接口性能。
拿理财产品举例,在展示年化收益率的时候,通常需要根据净值去套用公式计算。如果采用预处理,提前把计算好的年化收益率存起来,每次接口调用时直接读取对应字段,就不用每次都重新计算了。
(五)池化思想:避免重复创建带来的损耗
大家肯定都用过数据库连接池、线程池这些东西,这就是池化思想的体现。池化思想的核心就是预分配和循环使用,避免重复创建对象或者连接。毕竟创建和销毁这些资源也是要耗费时间的。
其实,池化思想的应用不止局限于数据库连接池和线程池。在做业务开发的时候,我们也可以借鉴这种思想,比如创建对象池来复用对象,减少资源开销。
(六)串行改并行:提高执行效率的利器
串行执行的时候,当前逻辑必须等上一个逻辑执行完才能开始;而并行执行则是多个逻辑可以同时进行,互不干扰。在没有结果参数依赖的情况下,并行执行能节省不少时间。
就像理财持仓信息展示接口,既要查询用户的账户信息,又要查询商品信息和banner位信息来渲染持仓页。如果是串行查询,接口耗时基本就是各个查询操作耗时的累加;但如果改成并行查询,接口耗时就会大大降低。
(七)索引:数据查询的“加速器”
加索引能显著提高数据查询效率,这在接口设计的时候一般都会考虑到。不过,随着需求不断迭代,有些情况下索引可能会失效。比如说,出现隐式类型转换、对索引进行列运算(像加、减、乘、除操作)、不满足最左匹配原则、使用or关键字、not in和not exists、order by和搜索列不匹配、使用了<、>、!= 以及like以通配符开头(比如%abc’)这些情况,索引都可能不起作用。后面有时间的话,我再单独整理这些索引不生效的具体场景,这里就先不展开细说了。
(八)避免大事务:防止数据库连接“拥堵”
大事务指的是运行时间比较长的事务。由于事务长时间不提交,数据库连接会一直被占用,这样就会影响其他请求访问数据库,进而影响别的接口性能。
看下面这段代码:
@Transactional(value = "taskTransactionManager", propagation = Propagation.REQUIRED, isolation = Isolation.READ_COMMITTED, rollbackFor = {RuntimeException.class, Exception.class})
public BasicResult purchaseRequest(PurchaseRecord record) {
BasicResult result = new BasicResult();
// 插入账户任务
taskMapper.insert(ManagerParamUtil.buildTask(record, TaskEnum.Task_type.pension_account.type(), TaskEnum.Account_bizType.purchase_request.type()));
// 插入同步任务
taskMapper.insert(ManagerParamUtil.buildTask(record, TaskEnum.Task_type.pension_sync.type(), TaskEnum.Sync_bizType.purchase.type()));
// 插入影像件上传任务
taskMapper.insert(ManagerParamUtil.buildTask(record, TaskEnum.Task_type.pension_sync.type(), TaskEnum.Sync_bizType.cert.type()));
result.setInfo(ResultInfoEnum.SUCCESS);
return result;
}
这段代码是在申购申请完成后执行一系列后续操作。要是新增申购完成后发送push通知用户的需求,很可能有人会直接在后面追加代码。如果事务中嵌套了RPC调用(也就是非DB操作),而且这些非DB操作耗时比较大的话,就可能出现大事务问题。大事务可能引发死锁、接口超时、主从延迟等一系列问题。
为了避免大事务问题,可以采取下面这些方法:
- 别把RPC调用放到事务里面。
- 尽量把查询操作放在事务之外。
- 事务里不要处理太多数据。
(九)优化程序结构:清理代码“冗余”
在多次需求迭代之后,程序结构可能会出现问题。多人维护一个项目的时候,很容易出现代码叠加的情况,导致重复查询、多次创建对象等耗时问题。
解决这个问题其实也不难,我们可以对接口整体进行重构。仔细评估每个代码块的作用和用途,调整它们的执行顺序,把那些不必要的操作去掉,让代码更加简洁高效。
(十)深分页问题:解决分页查询慢的难题
分页查询中,深分页问题很常见。一般我们最先想到的分页方式就是用limit,但有时候它的性能并不好。比如说这条SQL:
select * from purchase_record where productCode = 'PA9044' and status = 4 order by orderTime desc limit 100000,200
limit 100000,200
意味着数据库要扫描100200行数据,然后只返回200行,还要丢弃掉前面的100000行,这样执行速度肯定慢。
通常可以采用标签记录法来优化,像这样:
select * from purchase_record where productCode = 'PA9044' and status = 4 and id > 100000 limit 200
这种优化方式的好处是能命中主键索引,不管查询多少页,性能都还不错。不过它也有局限性,需要有一个连续自增的字段才能用。
(十一)SQL优化:提升查询性能的关键
SQL优化对提升接口查询性能非常重要。因为这篇文章主要讲接口优化方案,关于SQL优化的具体内容就不一一列举了。大家在优化SQL的时候,可以结合索引、分页这些方面来考虑优化策略。
(十二)锁粒度避免过粗:精准加锁,提升性能
在高并发场景下,我们通常会用锁来保护共享资源。但是,如果锁的粒度太粗,会严重影响接口性能。
锁粒度指的就是加锁的范围。不管是用synchronized
还是redis分布式锁,只需要在临界资源处加锁就行。不涉及共享资源的地方,就没必要加锁。这就好比上卫生间,把卫生间门锁上就够了,没必要把客厅门也锁上。
看下面这两段代码,错误的加锁方式:
// 非共享资源
private void notShare() {}
// 共享资源
private void share() {}
private int wrong() {
synchronized (this) {
share();
notShare();
}
}
正确的加锁方式:
// 非共享资源
private void notShare() {}
// 共享资源
private void share() {}
private int right() {
notShare();
synchronized (this) {
share();
}
}
很明显,正确的加锁方式只对共享资源加锁,避免了不必要的锁竞争,能提高接口性能。
三、写在最后
说实话,很多接口的效率问题不是一下子就出现的。在需求迭代的过程中,为了尽快上线功能,有时候大家会直接在原有代码上累加,这样就很容易造成接口性能问题。
咱们以后开发需求的时候,不妨换个思路,站在接口设计者的角度去思考问题,这样能避免很多性能隐患,也是实现降本增效的有效方法。希望今天分享的这些接口优化技巧能对大家有所帮助,要是在实际应用中有啥问题,欢迎一起讨论!