性能文章>每个 Java 工程师都必须知道的五个 API 性能优化技巧>

每个 Java 工程师都必须知道的五个 API 性能优化技巧转载

1年前
409324

为什么你的 API 响应这么慢?也许你需要解决这些问题。

作为后端开发人员,我们总是在编写各种 API,无论是为前端 Web 提供数据支持的HTTP REST API ,还是提供内部使用的 RPC API。

这些 API 在服务初期可能表现不错,但随着用户数量的增长,一开始响应很快的API变得越来越慢,直到用户抱怨:“你的系统太糟糕了。我只是浏览一个网页。怎么这么慢?” 这时,您需要考虑如何优化您的 API 性能。

要提高你的API的性能,我们首先要知道什么问题会导致接口响应慢。API设计需要考虑很多方面。

开发语言级别只占很小的一部分。哪个部分设计的不好,就会成为性能瓶颈。影响API性能的因素有很多,总结如下。

  • 数据库慢查询
  • 复杂的业务逻辑
  • 低性能代码
  • 资源不足

第四点比较容易解决。如果你的系统有一定的扩展性,可以通过增加服务器来解决。对于其他需要优化的点,我们需要根据场景提出解决方案。至于低性能代码,其实我之前写过一篇Java代码性能优化的文章,感兴趣的读者可以看看。

让您的 Java 应用程序运行得更快的十个优化技巧
这些技巧可能会多次提高您的应用程序的性能。
媒体网

在这篇文章中,我总结了一些行之有效的API性能优化技巧,希望能给有需要的朋友一些帮助。

并行调用

假设我们现在有一个电子商务系统需要提交订单。该功能需要调用盘点系统进行盘点扣账,还需要获取用户地址信息。最后,调用风控系统判断本次交易没有风险。该接口的大部分设计可能会将接口设计为顺序执行接口。毕竟,我们需要获取用户地址信息并完成库存扣除,才能进行下一步。伪代码可以写成如下:

如果我们仔细分析这个函数,我们会发现几个方法调用之间没有强依赖关系。而且,这三个系统的调用都是耗时的。假设这些系统的耗时调用分布如下


	//check stock
	stockService.check();
	//invoke addressService
	addressService.getByUser();
	//risk control
	riskControlSerivce.check();
	
	return doSubmitOrder(orderInfo);
}
  • stockService.check()需要 150 毫秒。
  • addressService.getByUser()需要 200 毫秒。
  • riskControlSerivce.check()需要 300 毫秒。

如果这个API是顺序调用的,整个API的执行时间是650ms(150ms+200ms+300ms)。如果能转化为并行调用,API的执行时间为300ms,性能直接提升50%。使用并行调用,伪代码可以写成如下


	//check stock
	CompletableFuture<Void> stockFuture = CompletableFuture.supplyAsync(() -> {
        return stockService.check(); 
    }, executor);
	//invoke addressService
	CompletableFuture<Address> addressFuture = CompletableFuture.supplyAsync(() -> {
        return addressService.getByUser();
    }, executor);
	//risk control
	CompletableFuture<Void> riskFuture = CompletableFuture.supplyAsync(() -> {
        return 	riskControlSerivce.check();
    }, executor);

	CompletableFuture.allOf(stockFuture, addressFuture, riskFuture);
	stockFuture.get();
	addressFuture.get();
	riskFuture.get();
	return doSubmitOrder(orderInfo);

避免大额交易

所谓大交易,就是那些需要很长时间的交易。如果使用spring@Transaction管理事务,需要注意是否不小心启动了大事务。因为spring的事务管理原理是将多个事务合并为一个执行,如果一个API中有多个数据库读写,并且这个API的并发访问量比较高,很可能大事务也会导致数据库中锁住了很多数据,造成大量block,数据库连接池连接耗尽。修改上面的例子。

public Boolean submitOrder(orderInfo orderInfo) {

    //check stock
    stockService.check();
    //invoke addressService
    addressService.getByUser();
    //risk control
    riskControlRpcApi.check();
    
    orderService.insertOrder(orderInfo);
    orderDetailService.insertOrderDetail(orderInfo);
    
    return true;
}

相信这种代码已经出现在很多人写的业务中了。RPC 是一种非 DB 操作,它与持久层代码混合在一起。这种代码绝对是一笔大买卖。它不仅需要查询用户的地址和扣除库存,还需要插入订单数据和订单详情。这一系列操作需要合并到同一个事务中。如果 RPC 响应慢,当前线程会一直占用数据库连接,导致并发场景下数据库连接耗尽。不仅如此,如果事务需要回滚,你的 API 响应会因为回滚慢而变慢。

这时候,我们就需要考虑缩减业务。我们可以将非事务性操作和事务性操作分开,像这样

private OrderDaoService orderDaoService;

public Boolean submitOrder(OrderInfo orderInfo) {

    //invoke addressService
    addressService.getByUser();
    //risk control
    riskControlRpcApi.check();
    return orderDaoService.doSubmitOrder(orderInfo);
}

@Service
public class OrderDaoService{

    @Transactional(rollbackFor=Exception.class)
    public Boolean doSubmitOrder(OrderInfo orderInfo) {
        //check stock
        stockService.check();
        orderService.insertOrder(orderInfo);
        orderDetailService.insertOrderDetail(orderInfo);
        return true;
    }
}

或者,您可以使用 spring 的 programmatic transaction TransactionTemplate。

private TransactionTemplate transactionTemplate;

public void submitOrder(OrderInfo orderInfo) {

	//invoke addressService
	addressService.getByUser();
	//risk control
	riskControlRpcApi.check();
	return transactionTemplate.execute(()->{
		return doSubmitOrder(orderInfo);
	})
}

public Boolean doSubmitOrder(OrderInfo orderInfo) {
		//check stock
		stockService.check();
		orderService.insertOrder(orderInfo);
		orderDetailService.insertOrderDetail(orderInfo);
		return true;
	}

添加适当的索引

当我们的服务处于运营初期,系统需要存储的数据量很小。可能是数据库没有添加索引来快速存储和访问数据。但是可以发现,随着业务的增长,单表数据量不断增加,数据库的查询性能变差。这时候,我们应该给你的数据库表添加合适的索引。可以通过命令查看表的索引(这里以MySQL为例)。

显示来自 your_table_name 的索引;

ALTER TABLE通过命令添加索引。

ALTER TABLE your_table_name ADD INDEX index_name(username);

有时,即使添加了一些索引,数据查询仍然很慢。这时候可以使用explain命令查看执行计划,判断你的SQL语句是否命中了索引。例如

解释 select * from product_info where type=0;

你会得到一个分析结果:

1231.png
一般来说,缺少索引的情况有几种。

  • 不满足最左前缀原则。例如,您idx(a,b,c)为tb1. 但是你的 SQL 语句是这样写的select * from tb1 where b=‘xxx’ and c=‘xxxx’;。
  • 索引列具有算术运算。select * from tb1 where a%10=0;
  • 索引列使用函数。select * from tb1 where date_format(a,’%m-%d-%Y’)=‘2022-08-06’;
  • like使用关键字的模糊查询。select * from tb1 where a like ‘%aaa’;
  • 使用not in或not exist关键字。

返回更少的数据

如果我们查询大量符合条件的数据,我们不需要返回所有数据。我们可以以分页的形式增量地提供数据。这样,我们需要通过网络传输的数据更少,编码和解码数据的时间更少,API响应更快。

然而,传统的limit offset方法用于分页(select * from product limit 10000,20)。当页数很大时,查询会越来越慢。这是因为使用的原理limit offset是找出10000条数据,然后丢弃之前的9980条数据。我们可以通过使用延迟关联来优化这个 SQL。

使用缓存

缓存是一种以空间换时间的解决方案。一些用户经常访问的数据直接缓存在内存中。因为内存的读取速度比磁盘IO快很多,所以我们也可以使用适当的缓存来提高API的性能。简单来说,我们可以使用Java HashMap、ConcurrentHashMap,也可以使用caffeine等本地缓存,也可以使用Memcached、Redis等分布式缓存中间件。

最后

我在这里列出了五个通用的 API 性能优化技巧。这些技巧只有在系统有一定的并发压力时才有效。如果您的系统访问量不大,请慎重考虑以上建议。您应该寻找其他更有效的解决方案。最后,希望这篇文章对你有所帮助。如果你想了解更多关于Java和系统架构设计的知识,请关注我。

点赞收藏
分类:标签:
金色梦想

终身学习。

请先登录,查看2条精彩评论吧
快去登录吧,你将获得
  • 浏览更多精彩评论
  • 和开发者讨论交流,共同进步

为你推荐

一次 Rancher go 应用内存占用过高问题排查

一次 Rancher go 应用内存占用过高问题排查

实现定时任务的六种策略

实现定时任务的六种策略

4
2