接口性能优化思路

背景

HIS 这边有一个扫码取药的接口,涉及到整个就诊流程:预约–》签到–》开始就诊–》添加处方及药品–》确认处方–》确认账单–》创建交易及订单 --》付款 --》 结束就诊 --》 签退。

整个流程业务非常复杂,涉及接口和远程服务也非常多,因此接口响应很慢,耗时很长。

类似的涉及到整个就诊流程的操作还有很多,比如:核销,也是要走一遍上面的流程,只是在【诊中】的操作不同,可能是添加药品,也可能是添加检查,体检,诊次套餐之类。这类操作每个步骤都非常明确,常常以一个 Pipeline 的形式封装起来,使用了内部自研的一个类似于流程引擎的框架,就叫 pipeline 。总之这类的操作遇到问题都是一样的:就是涉及到流程很复杂很长,接口响应很慢,而且一旦失败还涉及到分布式事物的问题。

目前,分布式事物是依靠自研的 Pipeline 来解决的,每个 Step 相当于一个原子操作,每一步都有一个单独的 rollback 方法进行异常回滚。不涉及长事物,理论上来说分布式事物这块带来的耗时影响是可以忽略的。

那面对这样一个耗时很长的接口应该从哪些方面进行优化呢?

这里拿【扫码取药】接口来举例说明。想知道一个 接口在各个地方的耗时详情,有一个非常方便的地方就是分布式链路追踪系统,我们公司使用的是 Skywalking ,打开 Skywalking 的后台,点击 追踪 搜索该接口,切换 表格 视图,就能看到该次接口请求的所有执行过程以及消耗时间:

接口性能优化思路_缓存

有了这个图我们就可以做到有的放矢,针对不同的情况做出相应的处理方式。

优化思路

1. 同步改异步

这种方法操作起来很简单,效果也很明显,但是并不是所有的耗时操作都能够进行异步处理的,常见的异步场景就是,发送 MQ 通知,发短信等等。如下面 MQ 消费耗时 1254 ms。

接口性能优化思路_缓存_02

如果项目使用 SpringBoot 框架开发,同步改异步将非常简单,只需要加个简单的 ​​@Async​​ 注解即可,如下:

@Async
public void sendOrderMq() {
// ...
}

2. SQL 慢查询优化

在 Skywalking 面板中,我们能清晰的看到 每一步 SQL 语句的执行耗时,下面这个接口涉及到一次条用,耗时 1244 ms

接口性能优化思路_redis_03

查看 SQL 简化如下:

select * from `income` where (`source_no` in (?) and `source_type` = ?)

通过分析 SQL 语句,不用看解释执行计划,就可以猜到,where 查询条件用到了两个条件,但是耗时却有 1240 ms,大概率应该是没有为这两个条件建立索引导致,再去查看该表的 DDL 语句,发现果然只有一个 source_no 索引,此时为了提升查询效率我们可以为 source_no 和 source_type 建立联合索引。建立联合索引的查询耗时为: 42 ms

接口性能优化思路_redis_04

按照这种思路,一一找出所有的慢查询 SQL 进行优化,能极大的提升性能。

3. 避免无谓的模型转换

有这样一种情况,比如 service 层调用 XXXModuleService 层代码,返回了 XXXInfo 模型,比如 PrescriptionOrderInfo 这个处方单模型,里面可能有包含的一些药品以及供应商信息,这些信息是调用了远程服务查询拼装的一个 PrescriptionOrderInfo 模型,但是 service 层的方法可能只需要 处方状态,药品 ID 之类的,这些字段本身处方表就可以获取到,如果再经过一层模型转换显然增加了耗时。解决方案就是 增加一个返回简单模型的接口,比如数据库表对应的 处方模型 Prescription ,这样就能避免模型转换带来的时间损耗。

接口性能优化思路_redis_05

这也提醒我们,在调用 其它接口时,要做到按需索取,如果遇到查询返回一个非常复杂的模型,而自己只需要用到其中几个关键字段,就要思考是否有更简单的更适合的模型来实现。

4. 查询缓存

由于扫码取药涉及多个 pipeline ,流程又很复杂,所以会出现一种情况。就是有一些接口在一次扫码取药操作中会被调用十几次,尤其是那些比较耗时的远程服务,每次耗时高达一两百毫秒,十几次下来性能堪忧。

接口性能优化思路_缓存_06

遇到这种情况,一种可行的方法就是增加缓存,根据入参缓存远程查询结果。需要注意的是,对缓存的接口有较高的要求:比如 不能有易变的状态变量,像处方状态,库存数量这些不要去缓存,一旦缓存失效不及时,会对业务造成严重的 BUG 。并且还要注意,缓存的时间要深思熟虑,一般来说,缓存时间不应大于接口的最大耗时。比如一次扫码取药的操作耗时 10s,那么缓存的时间就设置为 10s 是最适合的。

接口性能优化思路_redis_07

如上图,缓存key 可以是方法的入参,缓存时间就取整个接口的响应耗时。在 SpringBoot 项目中,实现上面的缓存方案非常容易。我们可以借助 ​​@Cacheable​​ 注解,可以作用域方法上,它的作用就是把方法入参当做 key ,把方法返回值作为缓存结果,缓存到 内存数据库如 Redis 中。这个注解有一个参数 cacheNames,就是缓存的 key,我们可以对指定的 key 设置缓存时间。

// 缓存合同的查询结果
@Cacheable(value = CACHE_FIND_CONTRACT)
public List<ContractInfo> findCacheableContract(ContractQuery query) {
//...
}

上面的代码对 findCacheableContract 方法进行了缓存,缓存的名称是 ​​CACHE_FIND_CONTRACT​​,key 的值默认是 query。tostring() 方法的结果,缓存 value 是 返回值 List 列表。

现在如果想对 缓存 ​​CACHE_FIND_CONTRACT​​设置缓存时间该怎么做呢?可以参考下面的配置:

private Map<String, RedisCacheConfiguration> _getRedisCacheConfigurationMap(
RedisTemplate template) {
Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = Maps.newHashMap();

// 查询合同的缓存配置
// @see com.xingren.clinic.member.services.contract.ContractQueryService.findContract
redisCacheConfigurationMap.put(CACHE_FIND_CONTRACT, this.getRedisCacheConfigurationWithTtl(template, 10));

// 查询药品的缓存配置
// @see MedicineRemoteService.*
redisCacheConfigurationMap.put(CACHE_GET_MEDICINE, this.getRedisCacheConfigurationWithTtl(template, 15));
return redisCacheConfigurationMap;
}

private RedisCacheConfiguration _getRedisCacheConfigurationWithTtl(RedisTemplate template,
Integer seconds) {
return RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(template.getValueSerializer()))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer((RedisSerializer<String>) template.getKeySerializer()))
.entryTtl(Duration.ofSeconds(seconds));
}

如上代码,分别对 ​​CACHE_FIND_CONTRACT​​​ 和 ​​CACHE_GET_MEDICINE​​ 两个缓存进行了 10s 和 15s 的缓存配置。

至此,经过上面的配置,我们就很容易的实现了对查询结果进行缓存,这种方法对性能的提高也有很大的帮助,尤其是使用本地内存进行缓存的时候。但对缓存的对象有较高的要求,使用时需要慎重。

5. 从产品角度出发:改变操作流程

如果以上方法做完后,接口性能还是堪忧,那就说明这个接口确实复杂,耗时不可避免,不放转变下解决思路。

拿扫码取药举例,可以从产品角度改变一下操作流程,用户点击扫码取药之后,后端直接返回,并提供一个单独的查询扫码取药结果的接口,让前端去轮询。这也不失为一个办法。

总结

以上就是在优化扫码取药接口时总结的一些优化思路,经过上面的层层优化,测试环境测试,接口性能提升三分之二左右,效果还是很明显的。

最后,个人水平有限,本文仅抛砖引玉,如果你也有好的性能优化思路,可以在评论区留言,一起探讨一下。