8

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

 2 years 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旭阳』交流沟通


Recommend

  • 2

    编辑导语:近年来视频号发展迅猛,越来越多的品牌和商家开始重视起视频号直播,想要入局挖掘红利。然而,为什么别人的直播人数那么多,自己的却寥寥无几呢?很可能是「直播预约」没有做好,本文作者分享了5个提升直播预约量的技巧,希望能给你带来帮助。...

  • 4

    本篇文章主要讲解下Map家族中3个相对冷门的容器,分别是WeakHashMap、EnumMap、IdentityHashMap, 想必大家在平时的工作中也很少用到,或者压根不知道他们的特性以及适用场景,本篇文章就带你一探究竟。 WeakHashMap WeakHashMap称...

  • 5

    注解想必大家都用过,也叫元数据,是一种代码级别的注释,可以对类或者方法等元素做标记说明,比如Spring框架中的@Service,@Component等。那么今天我想问大家的是类被继承了,注解能否继承呢?可能会和大家想的不一样,感兴趣的可以往下...

  • 8

    大家项目中如果有生成随机数的需求,我想大多都会选择使用Random来实现,它内部使用了CAS来实现。 实际上,JDK1.7之后,提供了另外一个生成随机数的类ThreadLocalRandom,那么他们二者之间的性能是怎么样的呢? Random的使用 Rand...

  • 4

    在使用spring的过程中,我们有没有发现它的扩展能力很强呢? 由于这个优势的存在,使得spring具有很强的包容性,所以很多第三方应用或者框架可以很容易的投入到spring的怀抱中。今天我们主要来学习Spring中很常用的11个扩展点,你用过几个呢?

  • 7

    欢迎关注微信公众号「JAVA旭阳」交流和学习 IntelliJ目前已经成为市面上最受欢迎的Java开发工具,这得益于里面非常丰富的插件机制。本文我将分享在日常开发中我经常使用的5个插件,它们可以帮助您提高工作效率。...

  • 8

    丧心病狂,竟有Thread.sleep(0)这种神仙写法? 最...

  • 9

    作为一名java开发程序员,不知道大家有没有遇到过一些匪夷所思的bug。这些错误通常需要您几个小时才能解决。当你找到它们的时候,你可能会默默地骂自己是个傻瓜。是的,这些可笑的bug基本上都是你忽略了一些基础知识造成的。其实都是很低级的错误。今天,我总结一些...

  • 6

    微服务架构如今非常的流行,这个架构下可能经常会遇到“双写”的场景。双写是指您的应用程序需要在两个不同的系统中更改数据的情况,比如它需要将数据存储在数据库中并向消息队列发送事件。您需要保证这两个操作都会成功。如果两个操作之一失败,您的系统可能会变得不...

  • 4

    不知道大家的项目是否都有对接口API进行自动化测试,反正像我们这种小公司是没有的。由于最近一直被吐槽项目质量糟糕,只能研发自己看看有什么接口测试方案。那么在本文中,我将探索如何使用 Rest Assured 自动化 API 测试,Rest Assured...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK