2024年2月

前言

随着
DotNetGuide技术社区交流群
的不断壮大(目前4个群都已经满500人,已开5群现已有180多个小伙伴),越来越多应届生和其他领域的小伙伴加入了我们这个大家庭。在此期间我在个人微信、公众号和技术交流群里看到比较多的留言和提问就是:
C#/.NET该如何自学入门?有什么好的学习资料和路线推荐的吗?

自学入门建议

  • 确认学习目标:自学C#/.NET首先你需要大概了解该门语言和框架的发展、前景和基本特点,从自身实际情况和方向出发确认学习的必要性。
  • 制定学习计划:制定一个详细的学习计划(比如每天学习一个C#/.NET知识点、小技能),这将有助于你更好地组织学习过程。
  • 微软官方文档是最好的起点:对于C#/.NET的学习,微软官方文档是一个非常好的学习资料。这些文档提供了详细的说明和示例代码,涵盖了C#/.NET的各个方面。按照文档的目录逐步学习,可以让你循序渐进地掌握语言和框架的各个语法和概念。
  • 多动手多实践:熟能生巧,学习的过程中除了阅读文档和示例代码以外,动手实践也是非常重要的。通过动手去实践所学的知识点,从而加深你对代码的理解和提高编程能力。
  • 寻找其他资源:除了微软官方文档,还有许多其他优质的学习资源可供选择,如在线文档教程、视频课程和编程社区。通过结合不同的学习资源,你可以获得更全面的学习体验。

温馨提示:自学是一个枯燥无味的过程,通过制定有效的学习计划鞭策自己每天打卡学习,从而避免三天打鱼两天晒网的情况,合理的利用微软官方技术文档和其他学习资源、并动手进行实践、你将会逐步掌握C#/.NET编程技能。

合理利用微软官方技术文档

对于C#/.NET的学习,微软官方文档是一个非常好的学习资料。这些文档提供了详细的说明和示例代码,涵盖了C#/.NET的各个方面。按照文档的目录逐步学习,可以让你循序渐进地掌握语言和框架的各个语法和概念。

微软.NET 文档

微软 C# 文档

全面的C#/.NET/.NET Core学习、工作、面试指南



记录、收集和总结C#/.NET/.NET Core基础知识、学习路线、开发实战、学习视频、文章、书籍、项目框架、社区组织、开发必备工具、常见面试题、面试须知、简历模板、以及自己在学习和工作中的一些微薄见解。希望能和大家一起学习,共同进步

image

前言

近日心血来潮想做一个开源项目,目标是做一款可以适配多端、功能完备的模板工程,包含后台管理系统和前台系统,开发者基于此项目进行裁剪和扩展来完成自己的功能开发。本项目为前后端分离开发,后端基于
Java21

SpringBoot3
开发,后端使用
Spring Security

JWT

Spring Data JPA
等技术栈,前端提供了
vue

angular

react

uniapp

微信小程序
等多种脚手架工程。

项目地址:
https://gitee.com/breezefaith/fast-alden

在前后端分离的项目开发过程中,我们通常会对数据返回格式进行统一的处理,这样可以方便前端人员取数据,后端发生异常时同样会使用此格式将异常信息返回给前端。本文将介绍在SpringBoot项目中如何实现统一异常处理。

实现步骤

定义统一响应对象类

/**
 * 响应结果类
 *
 * @param <T> 任意类型
 */
@Data
public class ResponseResult<T> {
    /**
     * 响应状态码,200是正常,非200表示异常
     */
    private int status;
    /**
     * 异常编号
     */
    private String errorCode;
    /**
     * 异常信息
     */
    private String message;
    /**
     * 响应数据
     */
    private T data;

    public static <T> ResponseResult<T> success() {
        return success(HttpServletResponse.SC_OK, null, null);
    }

    public static <T> ResponseResult<T> success(T data) {
        return success(HttpServletResponse.SC_OK, null, data);
    }

    public static <T> ResponseResult<T> fail(String message) {
        return fail(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null, message, null);
    }

    public static <T> ResponseResult<T> fail(String errorCode, String message) {
        return fail(HttpServletResponse.SC_INTERNAL_SERVER_ERROR, errorCode, message, null);
    }

    public static <T> ResponseResult<T> success(int status, String message, T data) {
        ResponseResult<T> r = new ResponseResult<>();
        r.setStatus(status);
        r.setMessage(message);
        r.setData(data);

        return r;
    }

    public static <T> ResponseResult<T> fail(int status, String errorCode, String message) {
        return fail(status, errorCode, message, null);
    }

    public static <T> ResponseResult<T> fail(int status, String errorCode, String message, T data) {
        ResponseResult<T> r = new ResponseResult<>();
        r.setStatus(status);
        r.setErrorCode(errorCode);
        r.setMessage(message);
        r.setData(data);
        return r;
    }

}

定义业务异常枚举接口和实现

通常一个系统中的自定义业务异常是可穷举的,可以考虑通过定义枚举的方式来列举所有的业务异常。

首先我们来定义一个异常信息枚举的基类接口。

public interface IBizExceptionEnum {
    String getCode();

    String getMessage();
}

再给出一个常用的异常信息的枚举类,如果有其他业务模块的异常信息,同样可以通过实现
IBizExceptionEnum
接口来进行定义。

@Getter
public enum BizExceptionEnum implements IBizExceptionEnum {
    ENTITY_IS_NULL("Base_Entity_Exception_0001", "实体为空"),
    ENTITY_ID_IS_NULL("Base_Entity_Exception_0002", "实体id字段为空"),
    ENTITY_ID_IS_DUPLCATED("Base_Entity_Exception_0003", "实体id字段%s重复");

    private final String code;
    private final String message;

    BizExceptionEnum(String code, String message) {
        this.code = code;
        this.message = message;
    }
}

定义业务异常基类

业务异常基类
BizException
继承自
RuntimeException
,代码中主动抛出的异常建议都包装为该类的实例。

/**
 * 业务异常基类,支持参数化的异常信息
 */
@Getter
@Setter
public class BizException extends RuntimeException {
    private String code;
    private Object[] args;

    public BizException() {
        super();
    }

    public BizException(String message) {
        super(message);
    }

    public BizException(Throwable cause) {
        super(cause);
    }

    public BizException(String message, Throwable cause) {
        super(message, cause);
    }

    public BizException(Throwable cause, String code, String message, Object... args) {
        super(message, cause);
        this.code = code;
        this.args = args;
    }

    public BizException(String code, String message, Object... args) {
        super(message);
        this.code = code;
        this.args = args;
    }

    public BizException(IBizExceptionEnum exceptionEnum, Object... args) {
        this(exceptionEnum.getCode(), exceptionEnum.getMessage(), args);
    }

    public BizException(Throwable cause, IBizExceptionEnum exceptionEnum, Object... args) {
        this(cause, exceptionEnum.getCode(), exceptionEnum.getMessage(), args);
    }

    @Override
    public String getMessage() {
        if (code != null) {
            if (args != null && args.length > 0) {
                return String.format(super.getMessage(), args);
            }
        }
        return super.getMessage();
    }
}

定义全局异常处理切面

本步骤需要使用
@RestControllerAdvice
注解,它是一个组合注解,由
@ControllerAdvice

@ResponseBody
组成,而
@ControllerAdvice
继承了
@Component
,因此
@RestControllerAdvice
本质上是个
Component
,用于定义
@ExceptionHandler

@InitBinder

@ModelAttribute
方法,适用于所有使用
@RequestMapping
方法。

还要用到
@ExceptionHandler
注解,可以认为它是一个异常拦截器,它采用“就近原则”,存在多个满足条件的异常处理器时会选择最接近的一个来使用。它本质上就是使用Spring AOP定义的一个切面,在系统抛出异常后执行。

具体实现代码如下:

/**
 * 全局异常处理切面
 */
@RestControllerAdvice
public class GlobalExceptionHandlerAdvice {
    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandlerAdvice.class);

    @ExceptionHandler({BizException.class})
    public ResponseResult<Object> handleBizException(BizException e, HttpServletRequest request, HttpServletResponse response) {
        log.error(e.getCode() + ": " + e.getMessage(), e);
        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        return ResponseResult.fail(e.getCode(), e.getMessage());
    }

    @ExceptionHandler({RuntimeException.class, Exception.class})
    public ResponseResult<Object> handleRuntimeException(Exception e, HttpServletRequest request, HttpServletResponse response) {
        log.error(e.getMessage(), e);
        response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
        return ResponseResult.fail(e.getMessage());
    }
}

上述代码会对系统中抛出的
BizException

RuntimeException

Exception
对象进行处理,把异常包装为
ResponseResult
对象后将异常编号和异常信息返回给前端。

测试和验证

下面我们就可以定义一个Controller类来进行简单的测试。

@RestController
@RequestMapping("/demo")
public class DemoController {
    @GetMapping("/method1")
    public ResponseResult<Integer> method1() {
        throw new BizException(BizExceptionEnum.ENTITY_IS_NULL);
    }

    @GetMapping("/method2")
    public void method2() {
        throw new BizException(BizExceptionEnum.ENTITY_ID_IS_NULL);
    }

    @GetMapping(value = "/method3")
    public String method3() {
        throw new BizException(BizExceptionEnum.ENTITY_ID_IS_DUPLCATED, "1");
    }

    @GetMapping(value = "/method4")
    public String method4() {
        // 抛出ArithmeticException异常
        return String.valueOf(1 / 0);
    }
}

总结

本文介绍了如何在SpringBoot项目中实现统一异常处理,如有错误,还望批评指正。

在后续实践中我也是及时更新自己的学习心得和经验总结,希望与诸位看官一起进步。

大数据是涉及以TB或PB为单位的大型数据集的大量数据。根据一项调查,今天大约90%的数据是在过去两年中产生的。大数据帮助公司对其提供的产品和服务产生有价值的见解。近年来,每家公司都使用大数据技术来完善其营销活动和技术。对于那些对准备跨国公司大数据面试感兴趣的人来说,本文是一个极好的指南。

如何为大数据面试做准备

准备大数据面试需要技术和解决问题的技能。修改Hadoop、Spark和数据处理框架等概念。确保理解分布式计算原理和算法-实践工具,如ApacheHave和ApachePig。此外,准备好讨论实际应用程序和案例研究,突出您从大数据集中提取有价值见解的能力。

热门大数据面试问题

以下是一些最常被问到的大数据面试问题:

1. 什么是大数据?这个问题为什么重要呢?

大数据是正常软件无法管理的大数据集合。它包括音频、文本、视频、网站和多媒体内容。大数据很重要,因为它有助于做出明智的决策,提高运营效率,甚至在风险和故障出现之前就对其进行预测。

2. 你能解释一下大数据的5v吗?

大数据的五个V是:

  • Volume: 存储在数据仓库中的数据量。
  • Velocity: 这是数据实时生成的速度。
  • Variety: 大数据由各种数据集组成,如结构化、半结构化和非结构化数据。
  • Veracity: 数据的可靠性或质量。
  • Value: 原始数据对任何组织都毫无用处,但一旦将其转化为有价值的见解,它对任何组织的价值都会增加。

3. 大数据和传统数据处理系统有什么区别?

传统的数据处理系统是为结构化数据设计的,并在定义的限制内运行。相比之下,大数据系统处理大量结构化和非结构化数据,利用分布式计算和存储实现可伸缩性。

4. 大数据如何推动现代企业的决策?

大数据通过从大数据集中提供可操作的见解,帮助制定决策。它支持数据驱动的战略和预测性分析,并增强了对客户行为、市场趋势和运营效率的了解。

5. 大数据分析面临哪些常见挑战?

挑战包括管理数据量、速度和多样性、确保数据质量、解决安全问题、处理实时处理以及处理分布式计算环境的复杂性。

6. 大数据和数据分析有何不同?

大数据处理大数据集,而数据分析专注于从数据中提取洞察力。大数据包括存储和处理,而数据分析侧重于统计分析。

7. 你能说出各种大数据技术和平台的名字吗?

一些大数据技术包括:

  • Hadoop
  • Apache Spark
  • Apache Flink
  • NoSQL databases (e.g., MongoDB)

受欢迎的平台是 Apache HBase 和 Apache Kafka.

8. 大数据中的数据隐私是如何管理的?

数据隐私通过加密、访问控制、匿名化技术和遵守GDPR等法规进行管理。还采用了差异隐私等隐私保护方法。

9. 大数据在AI和ML中扮演着什么角色?

大数据提供了训练机器学习模型所需的大量数据集。它通过使深度学习算法能够分析大量数据来增强AI功能。

10. 大数据对云计算有何影响?

大数据通过提供存储和处理能力影响云计算。AWS、Azure和Google Cloud等云平台提供大数据服务。

11. 什么是数据可视化?为什么它在大数据中很重要?

数据可视化让复杂的信息变得更简单,让决策者更轻松。它有助于识别大型数据集中的模式和趋势,有助于为决策提供信息。

12. 你能解释一下数据湖的概念吗?

数据湖是以原始格式保存大量原始数据的存储存储器。它们允许组织存储结构化和非结构化数据,从而实现灵活的分析和探索。

13. 大数据分析如何帮助风险管理?

大数据分析通过提供对潜在风险的实时洞察来增强风险管理。它支持预测性建模、欺诈检测和识别可能指示风险的模式。

14. 大数据的伦理考量是什么?

大数据伦理,也被称为数据伦理,对有关数据,特别是个人数据的错误和正确行为的概念进行系统化、辩护和推荐。

15. 大数据如何改变医疗、金融或零售行业?

在医疗保健领域,大数据改善了患者护理和药物发现。在金融领域,它有助于发现欺诈和进行风险评估。在零售业,它通过个性化推荐和库存管理来增强客户体验。

初级大数据面试问题

大数据访谈的基本问题和答案如下:

1. 定义Hadoop及其组件。

Hadoop是一个开源框架。它是基于Java的。它为应用程序管理大量数据的存储和处理。Hadoop的元素包括:

  • HDFS
  • MapReduce
  • YARN
  • Hadoop Common

2. 什么是MapReduce?

MapReduce是一种跨分布式系统处理和创建大数据的模型。

3. 什么是HDFS?它怎麽工作?

HDFS是Hadoop的存储组件,通过分发文件来处理大文件。

4. 你能描述一下大数据中的数据序列化吗?

数据序列化是将对象转换为字节流的过程。它有助于更轻松地保存或传输。

5. 什么是分布式文件系统?

分布式文件系统或DFS是一种服务,它允许组织服务器保存分布在多个文件服务器或位置上的文件。它增强了可访问性、容错性和可扩展性,而不是依赖单个集中式文件服务器。

6. Apache Pig的基本操作是什么?

Apache Pig是一个分析和处理大型数据集的高级平台。它的主要操作是加载、过滤、转换和存储数据。

7. 在大数据环境中解释NoSQL数据库。

NoSQL是一款适合大数据繁重需求的数据库基础设施。

8. 什么是数据仓库?

数据仓库是存储和管理结构化数据的存储库。该企业系统有助于分析和报告来自各种来源的结构化和半结构化数据。

9. 列式数据库是如何工作的?

列式数据库按列而不是按行组织数据,在存储效率和查询性能方面具有优势。

10. 什么是Apache Hive?它是如何使用的?

Apache Hive是一个数据仓库基础设施。它提供了一种类似SQL的语言(HiveQL),用于查询和管理大型数据集。

11. 解释数据工程师在大数据中的角色。

数据工程师设计、开发和维护用于处理和分析大型数据集的基础设施。它们确保数据的可用性和数据的质量。

12. 什么是数据挖掘?

数据挖掘涉及使用统计方法、ML和人工智能从大数据集中提取知识。

13. 描述大数据中的批处理

批处理是以预定的时间间隔处理大量数据的过程,为不需要实时结果的任务提供效率。

14. 实时数据处理是如何工作的?

实时数据处理在数据创建时对其进行处理。它有助于即时分析,因此有助于做出更合理的决策。

15. 大数据分析的不同类型有哪些?

大数据分析包括:

  • 描述性分析
  • 诊断分析
  • 预测分析
  • 规范性分析

16. 你能解释一下数据处理的概念吗?

数据整理是将原始数据清理成适当格式以供分析的过程。

17. Apache Spark是什么?它与Hadoop有什么不同?

Apache Spark是一个快速引擎,而Hadoop是一个处理框架。

18. 解释Kafka在大数据中的作用。

Apache Kafka 是一个分布式消息平台。它有助于构建实时数据管道和消息应用。

19. 什么是数据管道?

数据管道是一组过程,其中数据以其原始形式从各种数据源中摄取。然后将其移植到数据存储/数据湖数据仓库。它将数据从源转换到目标。

20. 您如何确保大数据项目中的数据质量?

大数据项目中的数据质量包括验证、清理和丰富数据,以确保准确性和可靠性。技术包括数据分析、验证规则和监控数据质量指标。

中级大数据面试问题

当晋升到更高的职位时,准备回答以下问题:

1. 解释数据库中的分片

分片是对多个服务器上的数据进行水平分区,以提高性能。

2. 实时处理大数据面临哪些挑战?

实时处理挑战包括处理大量数据和维护数据一致性。

3. 如何处理数据集中丢失或损坏的数据?

策略包括数据补偿,使用统计方法填充缺失值,以及在预处理过程中识别和处理损坏的数据。

4. 你能解释一下CAP定理吗?

根据CAP定理,分布式系统不能同时提供一致性、可用性和分区容差。系统设计人员必须在这些属性之间进行选择。

5. 分布式缓存是如何工作的?

分布式缓存将频繁访问的数据跨多个节点存储在内存中,提高了数据访问速度,降低了数据库负载。

6. 讨论大数据中的lambda架构

Lambda架构结合了大数据应用的批处理和实时处理,允许处理历史和实时数据。

7. Hadoop中的边缘节点是什么?

Hadoop中的边缘节点是Hadoop和外部网络之间的机器,帮助执行数据处理任务。

8. 解释Zookeeper在大数据环境中的角色

ZooKeeper用于大数据环境中的分布式协调和同步,确保一致性和可靠性。

9. 您如何优化大数据解决方案?

优化涉及到提高大数据系统的性能和效率的步骤。

10. 什么是大数据背景下的机器学习?

大数据中的机器学习使用算法来学习模式并进行预测。

11. 讨论数据流的概念

数据流涉及实时处理和分析连续数据流,从而实现即时洞察和行动。

12. 图形处理与传统数据处理有何不同?

图形处理专注于分析数据中的关系和联系,使其适用于社交网络分析和推荐系统。

13. 解释ETL(提取、转换、加载)在大数据中的作用。

ETL涉及从源中提取数据。然后将其转换为可用的格式并加载到目标目的地进行分析。

14. 什么是Data Lake House?

数据湖屋是将数据湖和数据仓库相结合的架构,为存储和分析提供统一的平台。

15. 讨论数据治理在大数据中的重要性

数据治理可确保整个组织的数据质量、安全性和合规性,并指导其正确使用和管理。

16. 如何在大数据中实施安全措施?

安全措施包括身份验证和监控,以保护大量数据系统免受未经授权的访问。

17. 结构化数据和非结构化数据有什么区别?

结构化数据遵循固定的结构,而非结构化数据没有固定的结构。

18. 讨论大数据在预测分析中的使用

预测性分析使用实时数据来预测未来趋势,帮助制定决策。

19. 您如何管理数据可伸缩性挑战?

解决可伸缩性挑战涉及水平扩展、优化数据存储和利用云计算资源。

20. 大数据中的数据备份和恢复的最佳实践是什么?

最佳做法是定期备份和测试备份和恢复流程,以确保数据完整性。

高级大数据面试问题

如果您的目标是担任高级职位,请查看以下高级大数据面试问题:

1. 解释大数据中的数据偏度概念。

数据偏斜是指数据在分区间分布不均,影响处理效率。缓解策略涉及分区和负载平衡。

2. 您如何为大量数据系统进行容量规划?

容量规划涉及估计未来的资源需求,以确保广泛的数据系统能够处理不断增长的数据量和处理需求。

3. 讨论大型数据集的数据可视化方面的高级技术。

高级技术包括交互式仪表板、多维可视化以及针对复杂数据集的新兴可视化工具。

4. 大数据集成项目涉及哪些复杂性?

整合大数据涉及解决数据格式差异,确保数据质量,以及协调不同的数据源。

5. 您如何确保大量数据系统中的高可用性和灾难恢复?

确保高可用性涉及冗余、故障转移机制和灾难恢复计划,以最大限度地减少停机时间和数据丢失。

6. 讨论AI和ML算法在大数据中的实现

在大数据中实施AI和ML包括选择适当的算法、功能工程、模型训练和部署预测分析的模型。

7. 大数据分析的最新趋势是什么?

最新的趋势包括边缘计算、人工智能的融合和大数据。

8. 您如何处理数据沿袭和元数据管理?

数据沿袭有助于跟踪数据从源到目的地的流动,而元数据管理则涉及对元数据进行编目和组织,以实现有效的数据治理。

9. 解释大数据中的复杂事件处理

复杂事件处理(CEP)涉及对数据流的实时分析,以确定模式、相关性和可操作的洞察力。

10. 讨论大数据中的分布式计算挑战

挑战包括跨分布式系统维护数据一致性、处理通信开销和解决网络延迟。

11. 您如何在大数据应用程序中进行性能调整?

性能调优涉及优化算法、并行处理和资源利用率,以提升大数据应用的速度和效率。

12. 解释数据联合的概念

数据联合将来自多个来源的数据组合到一个虚拟视图中,为查询和分析提供了统一的界面。

13. 讨论区块链在大数据中的作用。

区块链通过提供一种分散的方法来记录大数据中的交易,从而增强了数据的安全性和完整性。

14. 您如何在分布式环境中实施实时分析?

实时分析包括在数据到达时对其进行处理和分析,以实现对不断变化的条件做出反应的即时洞察和行动。

15. 量子计算对大数据有什么影响?

量子计算可能会以比经典计算机快得多的速度解决复杂问题,从而彻底改变大数据处理。

16. 讨论物联网与大数据的融合。

物联网(IoT)与大数据的集成涉及从互联设备收集和分析数据,从而为决策和自动化提供洞察力。

17. 在大数据的背景下,你如何看待伦理人工智能?

大数据和人工智能中的伦理考虑涉及确保算法决策的公平性、透明度和问责制,解决偏见,并尊重隐私。

18. 大数据系统中的多租户面临哪些挑战?

多租户挑战包括资源争用、数据隔离,以及确保共享同一基础设施的多个用户或组织的安全性和性能。

19. 讨论大数据的高级数据建模技术。

先进的技术包括预测建模、机器学习驱动的建模,以及结合特定领域的知识以更准确地表示复杂数据集。

20 大数据如何促进增强分析?

大数据通过将机器学习和NLP相结合来增强数据分析和决策能力,从而促进了增强分析。

结论

大数据涵盖了一系列技术、平台和概念,这些技术、平台和概念支持决策、推动创新并塑造各个行业的未来。

SpringBoot以其简洁高效的开发方式和强大的内嵌容器特性,为开发者提供了构建高性能后端服务的便利。然而,当面临高并发场景时,理解并合理配置Spring Boot应用以达到最佳的并发处理能力至关重要。在Spring Boot中,应用程序对HTTP请求的并发处理主要依赖于内嵌的Servlet容器(如Tomcat)。接下来我们以Tomcat为例谈谈这个问题。

本文以SpringBoot2.7.0为例。

在接下来讲解Tomcat工作线程池之前,你可以先了解一下Java线程池的工作原理,请参考这篇文章:
Java线程池最全详解

请求处理

我们的程序实际上是运行在Tomcat里的。所以SpringBoot可以处理多少请求,其实也就是看Tomcat可以处理多少请求。Tomcat的工作线程池负责接收并处理这些请求。在SpringBoot中关于Tomcat工作项城池的参数可以在
spring-boot-autoconfigure
的依赖包中的
spring-configuration-metadata.json
中有如下四个参数控制Tomcat的工作线程池:

{
	{  
	  "name": "server.tomcat.threads.max",  
	  "type": "java.lang.Integer",  
	  "description": "Maximum amount of worker threads.",  
	  "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties$Tomcat$Threads",  
	  "defaultValue": 200  
	},  
	{  
	  "name": "server.tomcat.threads.min-spare",  
	  "type": "java.lang.Integer",  
	  "description": "Minimum amount of worker threads.",  
	  "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties$Tomcat$Threads",  
	  "defaultValue": 10  
	},
	{  
	  "name": "server.tomcat.max-connections",  
	  "type": "java.lang.Integer",  
	  "description": "Maximum number of connections that the server accepts and processes at any given time. Once the limit has been reached, the operating system may still accept connections based on the \"acceptCount\" property.",  
	  "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties$Tomcat",  
	  "defaultValue": 8192  
	},
	{  
	  "name": "server.tomcat.accept-count",  
	  "type": "java.lang.Integer",  
	  "description": "Maximum queue length for incoming connection requests when all possible request processing threads are in use.",  
	  "sourceType": "org.springframework.boot.autoconfigure.web.ServerProperties$Tomcat",  
	  "defaultValue": 100  
	}
}

我们来看一下这四个参数的含义:

  • server.tomcat.threads.max:用于配置
    Tomcat
    内嵌服务器的工作线程池的最大工作线程数。工作线程用于处理接收到的HTTP请求,处理完毕后将返回到线程池中等待下一个请求。当并发请求数量小于等于
    threads.max
    时,
    Tomcat
    尽可能使用现有的工作线程来处理请求,如果并发请求数量超过了
    threads.max

    Tomcat
    不再创建新的工作线程,而是将请求放入队列等待。该值可以看做是一个临时工,如果并发请求的数量在
    threads.min-spare

    threads.max
    之间,就会使用这些临时工线程进行处理。默认值为200。主要关注的是工作线程池,用于处理请求的线程数。适用于调整并发请求的处理能力。

  • server.tomcat.threads.min-spare:确定了工作线程池的最小空闲线程数量,即使没有请求需要处理,也会保持这些线程处于等待状态,以便快速响应新的请求。该值可以看做是一个长期工,如果并发请求数小于
    min-spare
    ,就会使用这些线程去处理请求。默认值是10,即使没有活跃的请求,
    Tomcat
    也会保持至少 10 个空闲线程。

  • server.tomcat.max-connections:用于配置
    Tomcat
    内嵌服务器的连接池的最大连接数。即允许同时存在的最大连接数。这个连接数是针对整个连接池的,不仅仅是工作线程的数量。当并发请求数量导致服务器需要创建新的连接时,Tomcat 会检查连接池中的连接数是否已经达到了
    max-connections
    配置的最大值。如果已经达到最大值,新的连接请求将被放入等待队列中,直到有连接被释放。默认值8192。

  • server.tomcat.accept-count:用于配置
    Tomcat
    连接器所允许的最大等待接受连接的请求数(即等待队列)。当所有线程都在处理其他请求时,新的连接请求将会排队等待。如果队列已满,额外的连接将被拒绝。默认值是100。

我们以饭馆的示例去说明这几个参数之间的关系:
我们把
Tomcat
比作一家热门的餐厅,每个顾客都是一个请求。餐厅里一直有一部分厨师一直待命,无论有没有顾客,确保随时能够处理点单。这部分厨师就是
min-spare(长期工)
,当餐厅遇到一些大的宴会时,餐厅老板就会从外面找过来一批厨师帮忙,这部分厨师就是临时工,
临时工+长期工就是(max)
。餐厅的座位就表示同时最大可以容纳啊就餐的顾客人数,即
max-connections
。假如某时餐厅生意火爆,餐厅的座位都坐满以后,此时餐厅就会想办法在餐厅门口放上一排座椅,可以让后来的顾客在这里等待,假如里面有顾客吃完,这时外面等待的顾客就可以进去用餐。如果餐厅门口的座椅也做满,再来的顾客就会离开。

假如上述就是那家餐厅,2个绿色框代表长期工厨师即min-spare,3个红色框代表临时工厨师,共有5名厨师,即max数量,5个棕色的队列即餐厅椅子数量,即5个max-connections,餐厅最多可以容忍5名顾客吃饭,2个橘色队列代表门口等待区椅子数量,即2个accept-count。假如此时来了4个客人,那就可以区餐厅吃饭,如果来了7个客人,那么有5个客人进去吃饭,2个客人在门口等待,如果来了8个客人,那么有5个客人进去吃饭,2个客人在门口等待,剩下1个客人就会直接离开。

示例

我们创建一个SpringBoot项目,我们将上述四个参数分别修改为如下:

server.tomcat.accept-count=2  
server.tomcat.max-connections=5  
server.tomcat.threads.max=5  
server.tomcat.threads.min-spare=2

然后我们在定义一个接口,用于模拟客户端请求:

@RestController  
@RequestMapping("test")  
public class TestMaxRequestController {  
  
    @GetMapping("maxRequest")  
    @ResponseBody  
    public String testMaxRequest() throws InterruptedException {  
        System.out.println("当前请求线程: "+Thread.currentThread().getName());  
        Thread.sleep(2000);  
        return "Hello World";  
    }  
}

我们启动项目,使用
JVisualVM
监测一下此时项目中的线程数为2个:

image.png

此时我们使用
Apifox
发起一个请求,观察线程数没有增长。我们在使用
Apifox
中模拟3个线程同时请求,发现此时线程数变成5个:
image.png

我们继续增加线程数到7个,我们发现线程数还是5个,因最大线程数等于5个:

image.png

后台请求全部成功:
image.png

我们继续增加线程数到8个,此时线城数依然是5个,设置请求超时时间为1秒,但是有一个请求失败:

image.png

image.png
image.png

服务端只处理了7个请求:
image.png

结论

SpringBoot可以同时处理多少个请求,需要看其内嵌的Sevlet处理线程的能力,以Tomcat为例,即Tomcat可以处理多少请求,这需要看配置文件中设置的
server.tomcat.accept-count
以及
server.tomcat.max-connections
的大小。Tomcat同时可以处理的请求为
server.tomcat.accept-count
加上
server.tomcat.max-connections
。实际生产应用中我们需要根据服务器,硬件的配置等综合因素考虑后,调节这两个值,确保应用程序能够处理预期的并发量。

本文已收录于我的个人博客:
码农Academy的博客,专注分享Java技术干货,包括Java基础、Spring Boot、Spring Cloud、Mysql、Redis、Elasticsearch、中间件、架构设计、面试题、程序员攻略等

线程池是 Java 多线程编程中的一个重要概念,它可以有效地管理和复用线程资源,提高系统的性能和稳定性。但是线程池的使用也有一些注意事项和常见的错误,如果不小心,就可能会导致一些严重的问题,比如内存泄漏、死锁、性能下降等。

本文将介绍线程池使用不当的五个坑,以及如何避免和解决它们,大纲如下,

image

坑一:线程池中异常消失

线程池执行方法时要添加异常处理,这是一个老生常谈的问题,可是直到最近我都有同事还在犯这个错误,所以我还是要讲一下,不过我还提到了一种优雅的线程池全局异常处理的方法,大家可以往下看。

问题原因

@Test
public void test() throws Exception {
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
        5, 
        10, 
        60,
        TimeUnit.SECONDS, 
        new ArrayBlockingQueue<>(100000));
    Future<Integer> submit = threadPoolExecutor.execute(() -> {
        int i = 1 / 0; // 发生异常
        return i;
    });
}

如上代码,在线程池执行任务时,没有添加异常处理。导致任务内部发生异常时,内部错误无法被记录下来。

解决方法

在线程池执行任务方法内添加 try/catch 处理,代码如下,

@Test
public void test() throws Exception {
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
        5, 
        10, 
        60,
        TimeUnit.SECONDS, 
        new ArrayBlockingQueue<>(100000));
    Future<Integer> submit = threadPoolExecutor.execute(() -> {
        try {
            int i = 1 / 0;
            return i;
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            return null;
        }
    });
}

优雅的进行线程池异常处理

当线程池调用任务方法很多时,那么每个线程池任务执行的方法内都要添加 try/catch 处理,这就不优雅了,其实 ThreadPoolExecutor 线程池类支持传入 ThreadFactory 参数用于自定义线程工厂,这样我们在创建线程时,就可以指定 setUncaughtExceptionHandler 异常处理方法。

这样就可以做到全局处理异常了,代码如下,

ThreadFactory threadFactory = r -> {
    Thread thread = new Thread(r);
    thread.setUncaughtExceptionHandler((t, e) -> {
        // 记录线程异常
        log.error(e.getMessage(), e);
    });
    return thread;
};
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
    5, 
    10, 
    60,
    TimeUnit.SECONDS, 
    new ArrayBlockingQueue<>(100000));
threadPoolExecutor.execute(() -> {
    log.info("---------------------");
    int i = 1 / 0;
});

不过要注意的是上面 setUncaughtExceptionHandler 方法只能针对线程池的 execute 方法来全局处理异常。对于线程池的 submit 方法是无法处理的。

坑二:拒绝策略设置错误导致接口超时

在 Java 中,线程池拒绝策略可以说一个常见八股文问题。大家虽然都记住了线程池有四种决绝策略,可是实际代码编写中,我发现大多数人都只会用 CallerRunsPolicy 策略(由调用线程处理任务)。我吃过这个亏,因此也拿出来讲讲。

问题原因

曾经有一个线上业务接口使用了线程池进行第三方接口调用,线程池配置里的拒绝策略采用的是 CallerRunsPolicy。示例代码如下,

// 某个线上线程池配置如下
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
        50, // 最小核心线程数
        50, // 最大线程数,当队列满时,能创建的最大线程数
        60L, TimeUnit.SECONDS, // 空闲线程超过核心线程时,回收该线程的最大等待时间
        new LinkedBlockingQueue<>(5000), // 阻塞队列大小,当核心线程使用满时,新的线程会放进队列
        new CustomizableThreadFactory("task"), // 自定义线程名
        new ThreadPoolExecutor.CallerRunsPolicy() // 线程执行的拒绝策略
);

threadPoolExecutor.execute(() -> {
    // 调用第三方接口
    ...
});

在第三方接口异常的情况下,线程池任务调用第三方接口一直超时,导致核心线程数、最大线程数堆积被占满、阻塞队列也被占满的情况下,也就会执行拒绝策略,但是由于使用的是 CallerRunsPolicy 策略,导致线程任务直接由我们的业务线程来执行。

因为第三方接口异常,所以业务线程执行也会继继续超时,线上服务采用的 Tomcat 容器,最终也就导致 Tomcat 的最大线程数也被占满,进而无法继续向外提供服务。

解决方法

首先我们要考虑业务接口的可用性,就算线程池任务被丢弃,也不应该影响业务接口。

在业务接口稳定性得到保证的情况下,在考虑到线程池任务的重要性,不是很重要的话,可以使用 DiscardPolicy 策略直接丢弃,要是很重要,可以考虑使用消息队列来替换线程池。

坑三:重复创建线程池导致内存溢出


不知道大家有没有犯过这个问题,不过我确实犯过,归根结底还是写代码前,没有思考好业务逻辑,直接动手,写一步算一步