记录一次生产环境接口超时的优化过程
问题描述
有个场景是,订单需要最多支持两百条批量设置出库相关信息(开发阶段没有这个需求,业务使用过程中体现的需要,所以没有测试这种大批量的场景),实际使用时批量选择超过100条就会出现超时(客户端超时时间设置的10s),经测试发现选择200条时实际耗时18s左右。
优化过程记录
- 初步想到的是优化索引
- 有一定效果,但是效果不明显,还是会超时
- 优化代码,使用线程池异步处理
- 因为是异步处理,所以接口能立马详情结果(70ms),但是实际批量设置过程还没有处理完,前端会在批量设置的请求响应后执行列表查询
- 这个时候是查不出来最新设置的全部结果的,需要多次查询才能更新完成
- 在使用线程池的基础上,继续找原因优化代码
- 业务场景比较复杂,不是简单的设置发货信息,然后更新数据,实际是在更新前有大量的其他业务数据需要查询。也就是在原来批量修改的场景里,forEach循环嵌套有大量的查询,这个比较耗时
- 切换到当前多线程场景里,每个线程任务里也一样执行大量的查询任务。所以想到在任务处理前,把所有需要的业务数据批量查询好,然后转换成Map<key,value>形式的业务数据,再多线程执行修改
- 除了查询外,设置前还有个配对的服务需要调用,该服务里也有大量的查询和修改。这个也改到最外层统一处理
- 从结果来看,执行完后列表查询还是不能立马展示全部的最新信息,也要分多次
- 从线程池自身角度再优化
- 200条数据更新任务通过线程池处理,sit环境CPU核心数是4,设置的核心线程数是3,从结果查询的效果来看,实际耗时还有优化空间
- 考虑到服务器核心数较少,200条任务全部丢到线程池中,在IO密集型的场景下,多线程上下文切换的耗时还有优化的空间,所以想到将任务分批处理
- offset设置为70,这样200条就是3个任务,100条就是2个任务,能充分利用CUP核心
- 分批后再放到线程池中处理,减轻线程池压力和多线程上下文切换的耗时
- 结果立竿见影,平均耗时直接感到了3s左右
- 考虑到3s左右直接响应结果,与再去点击按钮刷新页面相比,体验更好
- 改成CompletableFuture.supplyAsync来执行异步任务,等全部执行完成后再响应请求
- 页面效果是,批量设置后,页面不可操作,等待2-3s,页面自动请求展示所有最新数据
- 线程池优化完了,从功能整体上看,订单预处理可以拆分处理
- 批量设置中取消配对服务调用,改成从订单创建时触发(通过spring event),另外通过定时器处理全部
- 从结果来看不明显,但是实际解耦了这两个功能,默认批量设置前订单信息是完善的
总结
- 总整体上看,大的功能解耦,只做好功能模块自己的事,默认上一步的数据是完备的。至于上一步数据完整的实现交由上一模块自己去实现
- 循环内不要嵌套查询
- 循环内嵌套查询,通常是在功能升级改造时出现的,也就是单个改成批量。这个没法避免,也不建议做单个功能时都默认做成批量的,没必要吧,毕竟不是每个功能都会升级为批量操作。能做到的,就是尽量结构化代码,让功能重构的时候尽量容易一些
- 使用线程池有很多讲究,这次的优化也充分体现了将大问题拆分成若干个小问题的重要性,类似分治思想。200个直接扔到线程池里的效率,是远远赶不上分成小批次再多线程处理的效率的。纯粹递归处理问题效率也远远低于分治法与动态规划
- 通常数据库是最后的性能瓶颈,好的业务模型划分、表结构设计和合理的索引也很重要