接口性能优化思路
背景
HIS 这边有一个扫码取药的接口,涉及到整个就诊流程:预约–》签到–》开始就诊–》添加处方及药品–》确认处方–》确认账单–》创建交易及订单 --》付款 --》 结束就诊 --》 签退。
整个流程业务非常复杂,涉及接口和远程服务也非常多,因此接口响应很慢,耗时很长。
类似的涉及到整个就诊流程的操作还有很多,比如:核销,也是要走一遍上面的流程,只是在【诊中】的操作不同,可能是添加药品,也可能是添加检查,体检,诊次套餐之类。这类操作每个步骤都非常明确,常常以一个 Pipeline 的形式封装起来,使用了内部自研的一个类似于流程引擎的框架,就叫 pipeline 。总之这类的操作遇到问题都是一样的:就是涉及到流程很复杂很长,接口响应很慢,而且一旦失败还涉及到分布式事物的问题。
目前,分布式事物是依靠自研的 Pipeline 来解决的,每个 Step 相当于一个原子操作,每一步都有一个单独的 rollback 方法进行异常回滚。不涉及长事物,理论上来说分布式事物这块带来的耗时影响是可以忽略的。
那面对这样一个耗时很长的接口应该从哪些方面进行优化呢?
这里拿【扫码取药】接口来举例说明。想知道一个 接口在各个地方的耗时详情,有一个非常方便的地方就是分布式链路追踪系统,我们公司使用的是 Skywalking ,打开 Skywalking 的后台,点击 追踪 搜索该接口,切换 表格 视图,就能看到该次接口请求的所有执行过程以及消耗时间:
有了这个图我们就可以做到有的放矢,针对不同的情况做出相应的处理方式。
优化思路
1. 同步改异步
这种方法操作起来很简单,效果也很明显,但是并不是所有的耗时操作都能够进行异步处理的,常见的异步场景就是,发送 MQ 通知,发短信等等。如下面 MQ 消费耗时 1254 ms。
如果项目使用 SpringBoot 框架开发,同步改异步将非常简单,只需要加个简单的 @Async
注解即可,如下:
2. SQL 慢查询优化
在 Skywalking 面板中,我们能清晰的看到 每一步 SQL 语句的执行耗时,下面这个接口涉及到一次条用,耗时 1244 ms
查看 SQL 简化如下:
通过分析 SQL 语句,不用看解释执行计划,就可以猜到,where 查询条件用到了两个条件,但是耗时却有 1240 ms,大概率应该是没有为这两个条件建立索引导致,再去查看该表的 DDL 语句,发现果然只有一个 source_no 索引,此时为了提升查询效率我们可以为 source_no 和 source_type 建立联合索引。建立联合索引的查询耗时为: 42 ms
按照这种思路,一一找出所有的慢查询 SQL 进行优化,能极大的提升性能。
3. 避免无谓的模型转换
有这样一种情况,比如 service 层调用 XXXModuleService 层代码,返回了 XXXInfo 模型,比如 PrescriptionOrderInfo 这个处方单模型,里面可能有包含的一些药品以及供应商信息,这些信息是调用了远程服务查询拼装的一个 PrescriptionOrderInfo 模型,但是 service 层的方法可能只需要 处方状态,药品 ID 之类的,这些字段本身处方表就可以获取到,如果再经过一层模型转换显然增加了耗时。解决方案就是 增加一个返回简单模型的接口,比如数据库表对应的 处方模型 Prescription ,这样就能避免模型转换带来的时间损耗。
这也提醒我们,在调用 其它接口时,要做到按需索取,如果遇到查询返回一个非常复杂的模型,而自己只需要用到其中几个关键字段,就要思考是否有更简单的更适合的模型来实现。
4. 查询缓存
由于扫码取药涉及多个 pipeline ,流程又很复杂,所以会出现一种情况。就是有一些接口在一次扫码取药操作中会被调用十几次,尤其是那些比较耗时的远程服务,每次耗时高达一两百毫秒,十几次下来性能堪忧。
遇到这种情况,一种可行的方法就是增加缓存,根据入参缓存远程查询结果。需要注意的是,对缓存的接口有较高的要求:比如 不能有易变的状态变量,像处方状态,库存数量这些不要去缓存,一旦缓存失效不及时,会对业务造成严重的 BUG 。并且还要注意,缓存的时间要深思熟虑,一般来说,缓存时间不应大于接口的最大耗时。比如一次扫码取药的操作耗时 10s,那么缓存的时间就设置为 10s 是最适合的。
如上图,缓存key 可以是方法的入参,缓存时间就取整个接口的响应耗时。在 SpringBoot 项目中,实现上面的缓存方案非常容易。我们可以借助 @Cacheable
注解,可以作用域方法上,它的作用就是把方法入参当做 key ,把方法返回值作为缓存结果,缓存到 内存数据库如 Redis 中。这个注解有一个参数 cacheNames,就是缓存的 key,我们可以对指定的 key 设置缓存时间。
上面的代码对 findCacheableContract 方法进行了缓存,缓存的名称是 CACHE_FIND_CONTRACT
,key 的值默认是 query。tostring() 方法的结果,缓存 value 是 返回值 List 列表。
现在如果想对 缓存 CACHE_FIND_CONTRACT
设置缓存时间该怎么做呢?可以参考下面的配置:
如上代码,分别对 CACHE_FIND_CONTRACT
和 CACHE_GET_MEDICINE
两个缓存进行了 10s 和 15s 的缓存配置。
至此,经过上面的配置,我们就很容易的实现了对查询结果进行缓存,这种方法对性能的提高也有很大的帮助,尤其是使用本地内存进行缓存的时候。但对缓存的对象有较高的要求,使用时需要慎重。
5. 从产品角度出发:改变操作流程
如果以上方法做完后,接口性能还是堪忧,那就说明这个接口确实复杂,耗时不可避免,不放转变下解决思路。
拿扫码取药举例,可以从产品角度改变一下操作流程,用户点击扫码取药之后,后端直接返回,并提供一个单独的查询扫码取药结果的接口,让前端去轮询。这也不失为一个办法。
总结
以上就是在优化扫码取药接口时总结的一些优化思路,经过上面的层层优化,测试环境测试,接口性能提升三分之二左右,效果还是很明显的。
最后,个人水平有限,本文仅抛砖引玉,如果你也有好的性能优化思路,可以在评论区留言,一起探讨一下。