8

5个接口性能提升的通用技巧 - JAVA旭阳

 1 year ago
source link: https://www.cnblogs.com/alvinscript/p/17020436.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

作为后端开发人员,我们总是在编写各种API,无论是为前端web提供数据支持的HTTP REST API ,还是提供内部使用的RPC API。这些API在服务初期可能表现不错,但随着用户数量的增长,一开始响应很快的API越来越慢,直到用户抱怨:“你的系统太糟糕了。” 我只是浏览网页。为什么这么慢?”。这时候你就需要考虑如何优化你的API性能了。

要想提高你的API的性能,我们首先要知道哪些问题会导致接口响应慢。API设计需要考虑很多方面。开发语言层面只占一小部分。哪个部分设计不好就会成为性能瓶颈。影响API性能的因素有很多,总结如下:

  • 数据库慢查询
  • 复杂的业务逻辑
  • 糟糕的代码
  • ........

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

欢迎关注个人公众号『JAVA旭阳』交流沟通

1. 并发调用

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

public Boolean submitOrder(orderInfo orderInfo) {

	//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%。使用并行调用,大致代码如下:

public Boolean submitOrder(orderInfo orderInfo) {

	//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);
}

2. 避免大事务

所谓大事务,就是历经时间很长的事务。如果使用Spring @Transaction管理事务,需要注意是否不小心启动了大事务。因为Spring的事务管理原理是将多个事务合并到一个执行中,如果一个API里面有多个数据库读写,而且这个API的并发访问量比较高,很可能大事务会导致太大大量数据锁在数据库中,造成大量阻塞,数据库连接池连接耗尽。

@Transactional(rollbackFor=Exception.class)
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;
}

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

这个时候就需要考虑减小事务了,我们可以把非事务操作和事务操作分开,像这样:

@Autowired
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 的编程事务TransactionTemplate

@Autowired
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;
	}

3. 添加合适的索引

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

show index from `your_table_name`;

ALTER TABLE通过命令添加索引。

ALTER TABLE `your_table_name` ADD INDEX index_name(username);

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

explain select * from product_info where type=0;

你会得到一个分析结果:

21248331e378453bbcf26a6b25809f4b~tplv-k3u1fbpfcp-zoom-1.image

一般来说,索引失效有几种情况:

  • 不满足最左前缀原则。例如,您创建一个组合索引idx(a,b,c)。但是你的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')='2023-01-02';
  • like使用关键字的模糊查询。select * from tb1 where a like '%aaa';
  • 使用not innot exist关键字。

4. 返回更少的数据

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

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

select * from product where id in (select id from product limit 10000,20);

5. 使用缓存

缓存是一种以空间换时间的解决方案。一些用户经常访问的数据直接缓存在内存中。因为内存的读取速度远快于磁盘IO,所以我们也可以通过适当的缓存来提高API的性能。简单的,我们可以使用Java的HashMapConcurrentHashMap,或者caffeine等本地缓存,或者MemcachedRedis等分布式缓存中间件。

我在这里列出了五个通用的 API 性能优化技巧,这些技巧只有在系统有一定的并发压力时才有效。如果本文对你有帮助的话,请留下一个赞吧。

欢迎关注个人公众号『JAVA旭阳』交流沟通


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK