哈喽大家好,这里是最锋利的矛。今天早上刚来公司,正在开心的努力工作(mo yu),忽然瞅到右下角头像闪烁。
心想,嚯一大早就有妹子找我。点开聊天框发现确实是新来的测试妹子,不过聊天框里赫然出现了你的接口为什么这么慢?时间一个接口顶别人好几个之类话。
作为公司人称优化侠的我,说我慢可以,但是比别人几个接口加起来都慢真是我婶婶可以忍我叔叔都不能忍。所以赶紧打开链路监控工具(这里用的是sky working)看下什么情况,争取给新来的妹子留个好印象。
接口是一个单据批量完成的场景,简化逻辑如图所示:
发现大部分耗时都花在的其中一个对外方法的调用上,看来毒瘤就是他了。
代码逻辑优化
一般遇见这种情况首先就能想到的肯定查询没走到索引,不过在去看数据库前我先来review下这个接口的代码,发现这个接口在去调用真正的执行逻辑前需要先查询好几个基础属性配置的东西。
而这个接口比较远古,经由的好几位开发的维护,抽取的方法又比较分散,导致查询基础属性这个接口调用了好几次,虽然这个是根据主键去查询耗时很短,但是苍蝇再小也是肉,我优化侠眼里是柔不得一点沙子的。
所以给他都合并成一处查询,后续用到配置的地方都通过参数传递下去。
数据库优化
接下来看耗时最长的一个业务,在sky working里点击是可以看见具体耗时的sql,这里为了展示方便就用order表替代了,原sql大致为
select * from `order` where order_type_id = 7079 or complete_time between '2024-03-01' and '2024-03-04'
两个查询条件一个订单类型ID,一个订单完成时间区间,两个字段存在一个联合索引idx_type_complete_time。
根据我对八股文的理解,一眼就看出来这个sql的问题,or让索引失效了,不过我们还是按步骤一步一步分析下。
通过explain分析上面sql可以看到type为all,虽然用到了联合索引,不过因为条件or还是进行了全表扫描。这样肯定是不行的,最好是两个字段都可以用上索引。
所以我想到了把这个联合索引进行拆分为idx_type和idx_type_complete_time,并将sql分为两个单条件的查询,然后在代码里对结果进行合并。Let's do this.
---sql1
select * from `order` where order_type_id = 7079 ;
---sql2
select * from `order` where complete_time between '2024-03-01' and '2024-03-04';
可以看到两个sql用到了最合适的索引,然后进行下一步,在代码中取并集并去重。
public OrderEntity findOrderList(Long orderTypeId, Date timeStart, Date timeEnd) {
List<OrderEntity> orderEntities = orderDao.findByOrder(orderTypeId);
List<OrderEntity> orderEntityList = orderDao.findByCompleteTime(timeStart, timeEnd);
// 合并两个数组并去重
List<OrderEntity> mergeList = mergeOrders(orderEntities, orderEntityList);
}
public static List<OrderEntity> mergeOrders(List<OrderEntity> list1, List<OrderEntity> list2) {
Set<Long> idsSet = new HashSet<>();
list1.forEach(order -> idsSet.add(order.getId()));
list2.forEach(order -> idsSet.add(order.getId()));
return Stream.concat(list1.stream(), list2.stream())
.filter(order -> idsSet.contains(order.getId()))
.collect(Collectors.toList());
}
异步回调处理
那么最后来看调用财务服务的接口,可以看到接口总耗时3.8秒,光是这一个调用就花了2.7秒多。
首先了解一下调用这个接口的目的,它是在订单完成时调用财务系统,然后根据财务系统的返回结果来更新订单的结算状态。
因为调用的外部系统服务,会存在分布式事务的问题,所以针对订单调用财务收单接口的操作除了在调用异常时会存入表中自动补偿外还会在用户页面上有手动补偿的按钮,那么在根据订单结算状态的状态机保证幂等性的前提下,完全可以异步调用财务接口后回调订单修改状态。
确认了要优化的点,我想到了两种异步回调实现的方法,一个是简单粗暴直接开一个线程丢到线程池,另一个就是使用spring event加@Async注解实现异步事件来实现。
直接开线程
实现
CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
//处理财务业务成功后回调修改订单结算状态
doFinance(orderId);
}, executor);
优点
这种方式的优点是简单直接,适用于较小的应用程序或简单的异步任务。
缺点
- 资源管理:手动创建线程可能导致过多的线程被创建,消耗大量系统资源,甚至导致性能下降。
- 生命周期管理:你需要自己管理线程的生命周期,包括启动、停止等。
- 异常处理:异常处理可能较为复杂,因为主线程可能无法直接感知到子线程中的异常。
- 同步问题:如果异步操作的结果需要同步到主线程或其他线程,可能会引入额外的同步问题。
spring event
实现
- 创建一个自定义事件类
public class FinanceEvent extends ApplicationEvent {
private final Long orderId;
public FinanceEvent(Object source, Long orderId) {
super(source);
this.orderId = orderId;
}
public Long getOrderId() {
return orderId;
}
}
- 定义异步事件监听器:创建一个监听器类来处理订单事件,并使用
@Async
注解使其异步执行。
@Component
public class FinanceEventListener implements ApplicationListener<FinanceEvent> {
@Async
@Override
public void onApplicationEvent(FinanceEvent event) {
Long orderId = event.getOrderId();
// 处理财务业务成功后回调修改订单结算状态
}
}
- 发布事件
@Service
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
public Boolean orderComplete( Long orderId) {
// 1.处理订单相关逻辑
// 2.发布财务收单事件
FinanceEvent event = new FinanceEvent(this, orderId);
eventPublisher.publishEvent(event);
return true;
}
}
优点
- 易于集成:如果你的应用程序已经使用Spring框架,那么使用Spring事件机制非常直观,不需要额外的配置来支持事件处理。
- 生命周期管理:Spring自动管理事件监听器的生命周期,包括初始化和销毁。
- 异常处理:Spring事件机制可以更容易地捕获和处理事件处理过程中的异常。
- 依赖注入:事件监听器可以利用Spring的依赖注入特性,使得代码更简洁、更易于维护。
- 事件传播:Spring事件可以在整个应用程序中传播,使得事件的触发和处理更加灵活。
缺点
- 依赖Spring:使用Spring事件机制需要依赖Spring框架,这可能不适合那些不使用Spring的应用程序。
- 配置复杂度:虽然Spring提供了强大的功能,但也意味着配置相对复杂,特别是对于初学者来说。
- 性能影响:对于高并发场景,Spring事件机制可能会因为框架本身的开销而带来一定的性能影响。
由于使用spring event更方便管理,而且项目是是用DDD架构写的且已经存在一套用于不同模块间解耦的框架,就是用spring event来实现的,所以相当选择了spring event来改造。
在改造过后,流程变成了这个样子
可以看到需要同步执行的流程变短了许多,最耗时的都被丢到异步事件消费里执行了。
总结
通过对数据库索引的优化并修改查询逻辑和异步事件回调的方式优化高耗时的外部接口调用将原本3.8秒的接口优化到不到1秒。
最重要是测试妹子也满意的点头,大赞我的接口真快,我也带着开心的buff开始了牛马的新一天,大家都有光明的未来。
记得点赞,记得点赞,记得点赞...