技术面试中,一定会被问到性能优化有关的问题。这一类问题大多数都是开放性的,考察求职者的知识储备和逻辑思维。我们的脑洞可以开大一点,多说一些解决方案,充分展示自己的能力。
比如这个题:一个接口耗时10秒,如何优化为1秒? 这个问题脱离实际生产情况,属于八股文。如果生产环境中出现性能低下的接口,通常怎么应对?
- 根据接口重要性确定是否优化
过早优化是万恶之源。性能优化的目标是,在最合适的性价比下达到理想的性能提升。过度优化会增加系统复杂度和维护成本,使得开发和测试周期变长。如果接口承接着非核心业务,调用方也能忍受目前的耗时,优化工作可以暂缓,比这重要的活儿多得是。
- 无法精准预测优化后的耗时
优化方案再好,也只能保证优化方向的正确。压测报告出来之前,谁都不能保证采用某方案耗时一定在1秒以内,精确预测结果是不科学的。
确定接口要优化的话,实际开展的步骤是:
- 接口调研:根据日志确定耗时高是常态,不是偶发性。结合监控系统、链路跟踪系统等工具定位性能瓶颈。
- 制定方案:结合调用方的需求和开发人员的经验确定优化方案,比如调用方希望耗时在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可以集成使用这三者。