接口耗时10秒如何优化为1秒

接口耗时10秒如何优化为1秒

技术面试中,一定会被问到性能优化有关的问题。这一类问题大多数都是开放性的,考察求职者的知识储备和逻辑思维。我们的脑洞可以开大一点,多说一些解决方案,充分展示自己的能力。

比如这个题:一个接口耗时10秒,如何优化为1秒? 这个问题脱离实际生产情况,属于八股文。如果生产环境中出现性能低下的接口,通常怎么应对?

  • 根据接口重要性确定是否优化

过早优化是万恶之源。性能优化的目标是,在最合适的性价比下达到理想的性能提升。过度优化会增加系统复杂度和维护成本,使得开发和测试周期变长。如果接口承接着非核心业务,调用方也能忍受目前的耗时,优化工作可以暂缓,比这重要的活儿多得是。

  • 无法精准预测优化后的耗时

优化方案再好,也只能保证优化方向的正确。压测报告出来之前,谁都不能保证采用某方案耗时一定在1秒以内,精确预测结果是不科学的。

确定接口要优化的话,实际开展的步骤是:

  1. 接口调研:根据日志确定耗时高是常态,不是偶发性。结合监控系统、链路跟踪系统等工具定位性能瓶颈。
  2. 制定方案:结合调用方的需求和开发人员的经验确定优化方案,比如调用方希望耗时在3秒内或者接受异步通知。
  3. 实施方案:通过代码重构、引入中间件等方式改造接口,最好包含灰度方案或者动态切换新老逻辑的开关。

回到面试题本身,如何回答这个问题,能让面试官满意呢?这个题目的重点是“优化”和“1秒”,必须说出几种可能的性能瓶颈以及对应优化方案,方案为什么可以降低耗时。

常规业务系统譬如电商、物流都属于数据密集型系统,即数据是系统成败的决定性因素,包括数据的规模、数据的复杂度、数据产生与变化的速率。这类系统对IO的操作频率远远高于CPU,减少IO操作、提高CPU利用率是性能优化的大方向。排除掉网络质量问题,导致接口性能问题的原因很多,下面聊聊性能瓶颈与优化方案。

1.业务逻辑复杂

随着业务的发展,接口逻辑变复杂是难免的,重点是做好复杂度的管理。代码层面上,要充分的模块化,理清核心逻辑与辅助逻辑。如果在接口上叠加新需求,要全盘考虑合理性和扩展性。

  • 接口异步调用

接口异步调用是最直接的方案,耗时都在网络请求和RPC连接上,耗时肯定在1秒内。由于无法同步返回数据,调用方要异步处理结果,增加了系统复杂性。

以常用的RPC框架Dubbo为例,在消费端引用服务时增加async配置即可实现异步调用。如下所示,name为需要异步调用的方法名,async=true表示是否启用:

<dubbo:reference id="asyncOrderService" check="false" interface="com.alibaba.dubbo.demo.AsyncOrderService">
    <dubbo:method name="createOrder" async="true" />
</dubbo:reference>

配置了异步调用,直接调用将返回null:

Order order = asyncOrderService.createOrder(createOrderDto);

通过RpcContext获取Future对象,调用get方法时阻塞获取返回结果:

asyncOrderService.createOrder(createOrderDto);
Future<String> future = RpcContext.getContext().getFuture();
Order order = future.get();
  • 业务异步处理

如果不能接受整个接口异步调用,考虑将部分非核心流程异步执行。比如下单接口包含查库存、生成订单、发送短信三个步骤,发送短信不是核心流程,可以改为发送MQ消息触发短信,能省下一点耗时。

    /**
     * 创建订单
     * @return
     */
    public Order createOrder()
    {
        //查询库存
        ProductStore productStore = productStoreService.queryProductStore(productSkuDto);
        //TODO 判断库存
        //生成订单
        Order order = orderService.createUserOrder(createOrderDto);
        if(order == null) {
            throw new Exception("创建订单失败");
        }
        //TODO 组装短信消息
        //发送订单成功短信MQ
        mqProducer.sendMessage(orderSuccessMsg);
        retrun order;
    }
  • 并行处理

在满足业务逻辑的前提下,将没有关联的步骤由串行改为并行执行。比如有A和B两个步骤,分别耗时200ms和100ms,并行执行后最大耗时就是A的200ms。以下代码演示了Java语言的并行处理,将“查询满100减10信息”和“查询可用优惠券信息”的结果汇总返回给接口。

/**
* 查询购物车优惠活动标签
*/
public void getDiscountActivityTag(CartItemDTO cartItemDTO) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        List<Callable<String>> tasks = Lists.newArrayList();
        tasks.add(new Callable<String>() {
            @Override
            public String call() throws Exception {
                //查询满100减10信息
                 return null;
            }
        });
        tasks.add(new Callable<String>() {
            @Override
            public String call() throws Exception {
                //查询可用优惠券信息
                return null;
            }
        });
        try {
            List<Future<String>> futureList = executorService.invokeAll(tasks, 3000, TimeUnit.MILLISECONDS);
            if(CollectionUtils.isEmpty(futureList) {
                return STR_BLANK;
            }
            //组装优惠活动标签
            return StringUtils.join(futureList,STR_SPLIT);
        } catch (InterruptedException e) {
            logger.info("查询购物车优惠活动标签发生错误",e);
        }
        executorService.shutdown();

        return STR_BLANK;
    }
  • 调整业务流程

从10秒优化到1秒是个不小的挑战,如果没有好的技术方案能够实现,可以尝试调整业务流程。将一个接口做完的事情,拆成两三个接口来做,每个接口的耗时自然就减少了。通过校验业务数据,保证拆分后的接口的顺序调用。

2.数据库读写性能差

系统发展到一定的阶段,单实例的数据库一定无法支撑高并发的读写,优先考虑的应该是索引优化和冷数据归档,分库分表是最后的大招。

  • 冷数据归档:将用户不太关注的历史数据从单表中迁移走,前端提示用户只提供最近N个月数据。保证每个表的数据在一千万左右,查询耗时在0.5秒以内。

  • 索引优化:索引可以大大提高数据的查询速度。索引优化需要重点关注索引失效的原因。如果单表的数据量过大,优化索引也无法改善性能。

  • 数据缓存:读多写少、弱实时性的场景,尝试缓存数据。缓存的介质常常是内存,查询速度远高于数据库的磁盘,提升性能的效果非常明显。常用分布式缓存组件是Redis,二级缓存组件有Guava Cache、Caffeine、Encache,Spring Cache可以集成使用这三者。

参考

https://zhuanlan.zhihu.com/p/464671514
https://blog.csdn.net/itguangit/article/details/113399096

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注