作者: Bartosz Jedrzejewski
译者: helloworldtang
概览
在使用Spring Boot构建服务时,我们必须处理并发。有这样一种误解,认为由于使用了Servlet并为每个请求都分配了新线程,所以就不需要考虑并发了。在本文中,我将给出一些关于Spring Boot中处理多线程以及如何避免多线程可能引发的问题的实用建议。
Spring Boot并发基础知识
在Spring Boot应用程序中考虑并发时,以下关键领域需要特别关注:
- 最大线程数——这是为处理服务器请求可以分配的最大线程数
- 共享的外部资源——调用共享的外部资源,如数据库
- 异步方法调用——这些方法调用会在等待响应时将线程释放回线程池
- 共享的内部资源——调用共享的内部资源——比如缓存和潜在共享的应用程序状态
我们将依次介绍上面列出的关键领域,看看它们如何影响我们使用Spring Boot编写应用程序的方式。
Spring Boot应用程序的最大线程数
首先要注意的是,您正在处理的线程是有限数量的。
如果使用Tomcat作为嵌入式服务器(默认的),那么可以使用属性 server.tomcat.max-threads
来控制最多允许的线程数。默认设置为 0
,这意味着会使用Tomcat的默认值 200
。
了解这一点很重要,因为您可能需要修改这个最大线程数,以便高效地使用服务器提供的资源。并且在处理外部资源时,它也会成为瓶颈…
共享外部资源引发的问题
操作数据库或调用其他REST端点可能需要很长时间。
由于处理任务的线程总数是有限的,这意味着您确实希望避免出现长时间运行的、缓慢的同步请求。如果您正在等待一些缓慢的并霸占线程的任务完成,那么您可能没有充分利用您的服务器。
如果您有许多长时间运行的线程在等待响应,那么您可能最终会遇到这样一种情况:快速、简单的请求等待很长时间,“永远等待”直到请求超时或终止。
如何改善这一状况呢?
使用异步方法调用来救火
异步方法调用通常用于同时执行多个操作。理想情况下,如果需要调用三种服务:服务A、服务B和服务C,您肯定不想这样做:
- 调用服务A
- 等待服务A的响应
- 调用服务B
- 等待服务B的响应
- 调用服务C
- 等待服务C的响应
- 根据从服务A、服务B和服务C返回的数据完成业务逻辑,然后结束
如果每个服务需要3秒的响应时间,那么整个过程将需要9秒。如果按下面的流程操作会不会更好:
- 调用服务A
- 调用服务B
- 调用服务C
- 然后等待从服务A、B和C的响应
- 根据从服务A、服务B和服务C返回的数据完成业务逻辑,然后结束
在这种情况下,您可以直接执行对这三个方法的调用而不需要再依次等待每个请求完成。如果假设服务A、B和C彼此不依赖于对方,则整个过程只需要3秒。
异步和响应式微服务的概念本身就很有趣。我建议查看以下资料:
- 这个博客的响应式编程部分,特别是使用Spring Boot 2.0和Reactor开始响应式编程
- Reactive宣言
- Spring Boot 2和WebFlux
- Project Reactor by Pivotal
- Eclipse Vert.X – reactive microservices
- ReactiveX (RxJava)
这些都很吸引人,但本文中我们将只专注于Spring Boot ......
Spring Boot中进行异步调用
如何在Spring Boot中启用异步方法调用?您需要在使用了@SpringBootApplication
注解的Application类上再添加 @EnableAsync
注解。
启用了这个功能后,您就可以在服务中使用可以返回 CompletableFuture<>
类型数据的 @Async
注解了。因为您使用了 @EnableAsync
,所以使用了 @Async
的方法在执行时将在后台线程池中运行。
如果您很好地使用了异步,那么将避免许多因高并发高吞吐量而引发的不必要的性能下降。
对于Spring Boot中实现此功能的细节,我强烈建议查看 Spring官网的示例。
内部的资源共享
虽然前一节讨论的是我们通常无法控制的外部资源,但我们完全控制着系统内部的资源。
我们知道,应用程序内的资源是可控的,规避因共享而引发的问题的最佳建议,是:不共享它们!
默认情况下,Spring 服务和控制器是单例的。重要的是要意识到这一点,并且要非常小心。一旦在您的服务中有一个可变的状态时,您需要像在其它标准应用程序中一样处理它(不要认为在Spring Boot中就不需要处理了:smile)。
其他潜在的共享状态的资源是缓存和自定义的、服务器范围的组件(通常是监控、安全等)。
如果你必须要共享一些状态时,我的建议如下:
- 处理不可变对象。如果对象是不可变的,则可以避免许多与并发相关的问题。如果你需要修改对象的属性——在复制过来的新对象上改就好了。
- 了解你用到的集合类。并非所有的集合都是线程安全的。一个常见的陷阱是使用
HashMap
,并假设它是线程安全的(事实并非如此。如果您需要并发访问,请使用ConcurrentHashMap
、HashTable
或其它线程安全的解决方案)。 - 不要假设第三方库是线程安全的。大多数代码都不是,因此必须控制对共享状态的访问。
- 如果您要依赖它——学习正确的并发。强烈推荐《Java并发编程实战》,虽然写于2006年,但在2018年仍然很重要。
总结
在Spring中,并发和多线程是一个很大并且很重要的主题。在本文中,我想强调的是在编写Spring Boot应用程序时需要注意的关键领域。如果您想成功地构建高性能、高质量的服务,就需要围绕这一主题做出有意识的决策和权衡。我希望通过这篇文章你知道如何开始。