线程池深度解析:核心参数 + 拒绝策略 + 动态调整实战

线程池深度解析:核心参数 + 拒绝策略 + 动态调整实战

作为一名拥有八年经验的 Java 后端高级开发,我见过太多因线程池使用不当导致的线上问题:高峰期任务堆积 OOM、线程数过多导致 CPU 上下文切换飙升、拒绝策略配置不合理丢失核心业务请求...... 线程池看似简单,实则是高并发场景下的核心利器,也是面试中的必考点。

本文将从核心参数原理拒绝策略选型动态调整实战 三个维度,结合生产环境经验,带你彻底吃透线程池,文末还会附上我在项目中封装的通用线程池工具类,可直接 CV 使用!

一、线程池核心原理:ThreadPoolExecutor 核心参数深度解析

Java 中的线程池核心实现是 ThreadPoolExecutor,其构造方法如下:

arduino 复制代码
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) {
    // 源码实现
}

这 7 个核心参数决定了线程池的行为,下面逐一拆解,结合实际场景讲解每个参数的作用和选型技巧。

1. 核心线程数(corePoolSize):线程池的常驻线程数

  • 定义 :线程池维护的最小线程数,即使线程处于空闲状态,也不会被销毁(除非设置了 allowCoreThreadTimeOut)。

  • 实战选型

    • CPU 密集型任务(如计算、排序):corePoolSize = CPU核心数 + 1,减少上下文切换。
    • IO 密集型任务(如数据库操作、网络请求):corePoolSize = CPU核心数 * 2CPU核心数 / (1 - 阻塞系数),充分利用 CPU 资源。
    • 经验值:对于大多数后端服务,corePoolSize 通常设置为 8~32,具体需结合压测结果调整。

2. 最大线程数(maximumPoolSize):线程池的扩容上限

  • 定义:线程池允许创建的最大线程数,当核心线程数已满且任务队列已满时,线程池会创建新线程,直到达到该上限。
  • 核心注意点maximumPoolSize 只有在任务队列满了 之后才会生效!如果使用无界队列(如 LinkedBlockingQueue),该参数将永远不会被触发。
  • 实战选型maximumPoolSize 应大于 corePoolSize,通常设置为 corePoolSize * 2 或根据业务峰值流量调整,避免过度扩容导致系统资源耗尽。

3. 空闲线程存活时间(keepAliveTime + unit):线程池的收缩机制

  • 定义 :当线程池中的线程数超过 corePoolSize 时,空闲线程的存活时间,超过该时间后,空闲线程会被销毁,直到线程数等于 corePoolSize
  • 实战选型:IO 密集型任务可设置较长的存活时间(如 30 秒),CPU 密集型任务可设置较短的存活时间(如 10 秒),避免空闲线程占用资源。

4. 任务队列(workQueue):核心线程池的 "缓冲池"

  • 定义:用于存储等待执行的任务的阻塞队列,当核心线程数已满时,新任务会被加入队列。

  • 常用队列类型及选型

    队列类型 特点 适用场景
    ArrayBlockingQueue 有界队列,初始化时指定容量 生产环境首选,可避免任务无限堆积导致 OOM
    LinkedBlockingQueue 无界队列(默认容量为 Integer.MAX_VALUE 不推荐在高并发场景使用,容易导致 OOM
    SynchronousQueue 同步队列,不存储任务,直接传递给线程 适合任务执行时间极短的场景,需配合 maximumPoolSize = Integer.MAX_VALUE 使用
    PriorityBlockingQueue 优先级队列,按任务优先级执行 适合需要优先执行核心任务的场景
  • 高级开发经验生产环境必须使用有界队列!并合理设置队列容量,结合拒绝策略一起使用,避免任务堆积导致系统崩溃。

5. 线程工厂(threadFactory):线程的 "创建器"

  • 定义:用于创建线程的工厂,可自定义线程名称、优先级、是否为守护线程等。
  • 核心作用 :自定义线程名称,方便在日志和监控中排查问题。例如,将线程名称设置为 pool-name-thread-1,可以快速定位到某个线程池的线程。
  • 实战示例 :使用 guavaThreadFactoryBuilder 或自定义 ThreadFactory
scss 复制代码
ThreadFactory threadFactory = new ThreadFactoryBuilder()
    .setNameFormat("order-pool-%d")
    .setDaemon(false)
    .build();

6. 拒绝策略(handler):任务队列满了之后的 "兜底方案"

  • 定义 :当线程池达到 maximumPoolSize 且任务队列已满时,新任务的处理策略。
  • 核心重要性:拒绝策略的选择直接影响业务的可用性,不合理的拒绝策略会导致核心任务丢失。
  • 这部分内容非常重要,我们单独开一个章节深度解析。

二、拒绝策略深度解析:4 种默认策略 + 自定义策略实战

线程池的拒绝策略是 RejectedExecutionHandler 接口的实现,JDK 提供了 4 种默认策略,同时我们也可以自定义拒绝策略。

1. JDK 4 种默认拒绝策略

策略名称 特点 适用场景
AbortPolicy(默认) 直接抛出 RejectedExecutionException 异常 适合核心业务,需要快速感知任务提交失败的场景
CallerRunsPolicy 由提交任务的线程执行该任务 适合非核心业务,允许任务在调用线程中执行的场景,可避免任务丢失
DiscardPolicy 直接丢弃新任务,不抛出异常 适合非核心业务,允许任务丢失的场景,如日志收集、数据统计
DiscardOldestPolicy 丢弃队列中最旧的任务,然后尝试提交新任务 适合任务执行时间较短,旧任务的优先级低于新任务的场景

2. 自定义拒绝策略:生产环境必备

在实际项目中,默认的拒绝策略往往无法满足需求,例如,我们需要在任务被拒绝时,记录日志、发送告警、持久化任务到数据库等。此时,我们可以自定义拒绝策略。

实战需求:当任务被拒绝时,记录任务详情到日志,并发送告警邮件,同时尝试将任务持久化到 Redis,待系统恢复后重试。

自定义拒绝策略实现

typescript 复制代码
public class CustomRejectedExecutionHandler implements RejectedExecutionHandler {

    private static final Logger logger = LoggerFactory.getLogger(CustomRejectedExecutionHandler.class);

    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 1. 记录任务详情
        String taskInfo = r.toString();
        logger.error("线程池任务被拒绝,任务信息:{},线程池状态:{}", taskInfo, getThreadPoolStatus(executor));

        // 2. 发送告警邮件(此处省略邮件发送逻辑)
        // AlertUtil.sendAlert("线程池任务被拒绝", "任务信息:" + taskInfo);

        // 3. 持久化任务到Redis(此处省略Redis持久化逻辑)
        try {
            RedisUtil.lPush("thread_pool_rejected_tasks", taskInfo);
        } catch (Exception e) {
            logger.error("持久化被拒绝任务到Redis失败", e);
        }

        // 4. 可选:抛出异常,根据业务需求决定
        throw new RejectedExecutionException("Task " + r + " rejected from " + executor);
    }

    /**
     * 获取线程池状态
     */
    private String getThreadPoolStatus(ThreadPoolExecutor executor) {
        return String.format("核心线程数:%d,最大线程数:%d,当前线程数:%d,活跃线程数:%d,任务队列大小:%d,已完成任务数:%d",
                executor.getCorePoolSize(),
                executor.getMaximumPoolSize(),
                executor.getPoolSize(),
                executor.getActiveCount(),
                executor.getQueue().size(),
                executor.getCompletedTaskCount());
    }
}

3. 拒绝策略选型最佳实践

  • 核心业务 :使用 AbortPolicy + 自定义异常处理,快速感知任务提交失败,同时记录日志和发送告警。
  • 非核心业务 :使用 CallerRunsPolicy 或自定义拒绝策略,避免任务丢失。
  • 日志、统计等低优先级业务 :使用 DiscardPolicyDiscardOldestPolicy
  • 高级经验 :拒绝策略必须和有界队列配合使用,否则拒绝策略永远不会生效!

三、动态调整线程池参数:从理论到实战,应对流量波动

在实际项目中,业务流量往往是动态变化的,例如,电商平台的秒杀活动、双十一的流量峰值,以及日常的低流量时段。如果线程池参数固定不变,可能会导致在峰值时系统资源不足,在低峰时资源浪费。

因此,动态调整线程池参数 是高级 Java 开发必须掌握的技能。ThreadPoolExecutor 提供了一系列 set 方法,允许我们在运行时调整核心参数:

  • setCorePoolSize(int corePoolSize):调整核心线程数
  • setMaximumPoolSize(int maximumPoolSize):调整最大线程数
  • setKeepAliveTime(long time, TimeUnit unit):调整空闲线程存活时间
  • setRejectedExecutionHandler(RejectedExecutionHandler handler):调整拒绝策略

1. 动态调整的核心思路

  1. 监控线程池状态 :通过线程池的 get 方法获取当前状态,如活跃线程数、队列大小、已完成任务数等。
  2. 根据业务指标调整参数:根据 QPS、响应时间、CPU 使用率等业务指标,动态调整核心线程数和最大线程数。
  3. 结合配置中心:使用 Nacos、Apollo 等配置中心,实现线程池参数的动态配置,无需重启应用。

2. 实战:结合 Nacos 实现线程池参数动态调整

下面我们以 Nacos 为例,实现线程池参数的动态调整。核心步骤是:监听 Nacos 配置变化,当配置发生变化时,调用线程池的 set 方法调整参数。

步骤 1:添加 Nacos 依赖
xml 复制代码
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
步骤 2:配置 Nacos 监听
java 复制代码
@Configuration
@RefreshScope
public class ThreadPoolConfig {

    private static final Logger logger = LoggerFactory.getLogger(ThreadPoolConfig.class);

    @Value("${thread.pool.corePoolSize:8}")
    private int corePoolSize;

    @Value("${thread.pool.maximumPoolSize:16}")
    private int maximumPoolSize;

    @Value("${thread.pool.keepAliveTime:30}")
    private long keepAliveTime;

    @Value("${thread.pool.queueCapacity:1000}")
    private int queueCapacity;

    /**
     * 创建线程池
     */
    @Bean(name = "orderThreadPool")
    public ThreadPoolExecutor orderThreadPool() {
        ThreadFactory threadFactory = new ThreadFactoryBuilder()
                .setNameFormat("order-pool-%d")
                .build();

        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(queueCapacity);

        return new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                TimeUnit.SECONDS,
                workQueue,
                threadFactory,
                new CustomRejectedExecutionHandler()
        );
    }

    /**
     * 监听Nacos配置变化,动态调整线程池参数
     */
    @EventListener(RefreshEvent.class)
    public void refreshThreadPool(RefreshEvent event) {
        ThreadPoolExecutor executor = SpringContextUtil.getBean("orderThreadPool", ThreadPoolExecutor.class);
        if (executor == null) {
            logger.error("获取线程池失败");
            return;
        }

        // 调整核心线程数
        if (corePoolSize != executor.getCorePoolSize()) {
            executor.setCorePoolSize(corePoolSize);
            logger.info("动态调整核心线程数:{} -> {}", executor.getCorePoolSize(), corePoolSize);
        }

        // 调整最大线程数
        if (maximumPoolSize != executor.getMaximumPoolSize()) {
            executor.setMaximumPoolSize(maximumPoolSize);
            logger.info("动态调整最大线程数:{} -> {}", executor.getMaximumPoolSize(), maximumPoolSize);
        }

        // 调整空闲线程存活时间
        if (keepAliveTime != executor.getKeepAliveTime(TimeUnit.SECONDS)) {
            executor.setKeepAliveTime(keepAliveTime, TimeUnit.SECONDS);
            logger.info("动态调整空闲线程存活时间:{} -> {} 秒", executor.getKeepAliveTime(TimeUnit.SECONDS), keepAliveTime);
        }

        logger.info("线程池参数动态调整完成,当前状态:{}", getThreadPoolStatus(executor));
    }

    private String getThreadPoolStatus(ThreadPoolExecutor executor) {
        return String.format("核心线程数:%d,最大线程数:%d,当前线程数:%d,活跃线程数:%d,任务队列大小:%d,已完成任务数:%d",
                executor.getCorePoolSize(),
                executor.getMaximumPoolSize(),
                executor.getPoolSize(),
                executor.getActiveCount(),
                executor.getQueue().size(),
                executor.getCompletedTaskCount());
    }
}
步骤 3:Nacos 配置文件

在 Nacos 中添加配置:

ini 复制代码
# 线程池配置
thread.pool.corePoolSize=8
thread.pool.maximumPoolSize=16
thread.pool.keepAliveTime=30
thread.pool.queueCapacity=1000

当我们在 Nacos 中修改这些配置时,Spring Cloud 会自动触发 RefreshEvent 事件,我们的监听方法会被调用,从而动态调整线程池参数。

3. 动态调整的注意事项

  1. 核心线程数调整 :当核心线程数调大时,线程池会立即创建新线程;当核心线程数调小时,空闲的核心线程不会被立即销毁,需要等待 keepAliveTime 后才会被销毁(如果设置了 allowCoreThreadTimeOut)。
  2. 最大线程数调整 :最大线程数只能调大,不能调小?不是的,最大线程数可以调小,但已经创建的超过新最大线程数的线程,会在空闲时被销毁。
  3. 队列容量调整 :ThreadPoolExecutor 没有提供 setQueueCapacity 方法,因此队列容量无法动态调整。如果需要动态调整队列容量,可以自定义阻塞队列。
  4. 监控告警:在动态调整线程池参数时,必须监控线程池的状态,当参数调整异常时,及时发送告警。

四、高级开发必备:通用线程池工具类封装

作为一名高级 Java 开发,我在多个项目中封装过线程池工具类,下面是我总结的通用工具类,包含了线程池的创建、动态调整、监控等功能,可直接在生产环境中使用。

1. 线程池枚举类:管理所有线程池

arduino 复制代码
public enum ThreadPoolEnum {

    ORDER_POOL("orderPool", "订单处理线程池", 8, 16, 30, 1000),
    PAY_POOL("payPool", "支付处理线程池", 4, 8, 30, 500),
    LOG_POOL("logPool", "日志处理线程池", 2, 4, 60, 1000);

    /**
     * 线程池名称
     */
    private final String poolName;

    /**
     * 线程池描述
     */
    private final String desc;

    /**
     * 核心线程数
     */
    private final int corePoolSize;

    /**
     * 最大线程数
     */
    private final int maximumPoolSize;

    /**
     * 空闲线程存活时间(秒)
     */
    private final long keepAliveTime;

    /**
     * 队列容量
     */
    private final int queueCapacity;

    ThreadPoolEnum(String poolName, String desc, int corePoolSize, int maximumPoolSize, long keepAliveTime, int queueCapacity) {
        this.poolName = poolName;
        this.desc = desc;
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.keepAliveTime = keepAliveTime;
        this.queueCapacity = queueCapacity;
    }

    // getter 方法
    public String getPoolName() {
        return poolName;
    }

    public String getDesc() {
        return desc;
    }

    public int getCorePoolSize() {
        return corePoolSize;
    }

    public int getMaximumPoolSize() {
        return maximumPoolSize;
    }

    public long getKeepAliveTime() {
        return keepAliveTime;
    }

    public int getQueueCapacity() {
        return queueCapacity;
    }
}

2. 通用线程池工具类

scss 复制代码
public class ThreadPoolUtil {

    private static final Logger logger = LoggerFactory.getLogger(ThreadPoolUtil.class);

    /**
     * 线程池缓存
     */
    private static final Map<String, ThreadPoolExecutor> THREAD_POOL_MAP = new ConcurrentHashMap<>();

    static {
        // 初始化所有线程池
        for (ThreadPoolEnum threadPoolEnum : ThreadPoolEnum.values()) {
            ThreadPoolExecutor executor = createThreadPool(threadPoolEnum);
            THREAD_POOL_MAP.put(threadPoolEnum.getPoolName(), executor);
            logger.info("初始化线程池:{},描述:{}", threadPoolEnum.getPoolName(), threadPoolEnum.getDesc());
        }

        // 注册JVM关闭钩子,优雅关闭线程池
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            logger.info("JVM关闭,开始优雅关闭所有线程池");
            for (Map.Entry<String, ThreadPoolExecutor> entry : THREAD_POOL_MAP.entrySet()) {
                shutdownThreadPool(entry.getKey(), entry.getValue());
            }
        }));
    }

    /**
     * 创建线程池
     */
    private static ThreadPoolExecutor createThreadPool(ThreadPoolEnum threadPoolEnum) {
        ThreadFactory threadFactory = new ThreadFactoryBuilder()
                .setNameFormat(threadPoolEnum.getPoolName() + "-%d")
                .build();

        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(threadPoolEnum.getQueueCapacity());

        return new ThreadPoolExecutor(
                threadPoolEnum.getCorePoolSize(),
                threadPoolEnum.getMaximumPoolSize(),
                threadPoolEnum.getKeepAliveTime(),
                TimeUnit.SECONDS,
                workQueue,
                threadFactory,
                new CustomRejectedExecutionHandler()
        );
    }

    /**
     * 获取线程池
     */
    public static ThreadPoolExecutor getThreadPool(String poolName) {
        ThreadPoolExecutor executor = THREAD_POOL_MAP.get(poolName);
        if (executor == null) {
            throw new IllegalArgumentException("线程池不存在:" + poolName);
        }
        return executor;
    }

    /**
     * 动态调整线程池参数
     */
    public static void adjustThreadPool(String poolName, int corePoolSize, int maximumPoolSize, long keepAliveTime) {
        ThreadPoolExecutor executor = getThreadPool(poolName);
        if (executor == null) {
            return;
        }

        // 调整核心线程数
        if (corePoolSize > 0 && corePoolSize != executor.getCorePoolSize()) {
            executor.setCorePoolSize(corePoolSize);
            logger.info("动态调整线程池【{}】核心线程数:{} -> {}", poolName, executor.getCorePoolSize(), corePoolSize);
        }

        // 调整最大线程数
        if (maximumPoolSize > 0 && maximumPoolSize != executor.getMaximumPoolSize()) {
            executor.setMaximumPoolSize(maximumPoolSize);
            logger.info("动态调整线程池【{}】最大线程数:{} -> {}", poolName, executor.getMaximumPoolSize(), maximumPoolSize);
        }

        // 调整空闲线程存活时间
        if (keepAliveTime > 0 && keepAliveTime != executor.getKeepAliveTime(TimeUnit.SECONDS)) {
            executor.setKeepAliveTime(keepAliveTime, TimeUnit.SECONDS);
            logger.info("动态调整线程池【{}】空闲线程存活时间:{} -> {} 秒", poolName, executor.getKeepAliveTime(TimeUnit.SECONDS), keepAliveTime);
        }

        logger.info("线程池【{}】参数调整完成,当前状态:{}", poolName, getThreadPoolStatus(executor));
    }

    /**
     * 优雅关闭线程池
     */
    private static void shutdownThreadPool(String poolName, ThreadPoolExecutor executor) {
        if (executor == null || executor.isShutdown()) {
            return;
        }

        logger.info("开始关闭线程池:{},当前状态:{}", poolName, getThreadPoolStatus(executor));

        // 停止接收新任务
        executor.shutdown();

        try {
            // 等待60秒,让已提交的任务执行完成
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                // 超时后,强制关闭线程池
                logger.warn("线程池【{}】关闭超时,强制关闭", poolName);
                List<Runnable> droppedTasks = executor.shutdownNow();
                logger.warn("线程池【{}】强制关闭,丢弃任务数:{}", poolName, droppedTasks.size());
            }
        } catch (InterruptedException e) {
            logger.error("线程池【{}】关闭被中断", poolName, e);
            executor.shutdownNow();
        }

        logger.info("线程池【{}】关闭完成", poolName);
    }

    /**
     * 获取线程池状态
     */
    public static String getThreadPoolStatus(ThreadPoolExecutor executor) {
        return String.format("核心线程数:%d,最大线程数:%d,当前线程数:%d,活跃线程数:%d,任务队列大小:%d,已完成任务数:%d,是否关闭:%s",
                executor.getCorePoolSize(),
                executor.getMaximumPoolSize(),
                executor.getPoolSize(),
                executor.getActiveCount(),
                executor.getQueue().size(),
                executor.getCompletedTaskCount(),
                executor.isShutdown());
    }

    /**
     * 提交任务
     */
    public static void submitTask(String poolName, Runnable task) {
        ThreadPoolExecutor executor = getThreadPool(poolName);
        executor.submit(task);
    }

    /**
     * 提交任务,返回Future
     */
    public static <T> Future<T> submitTask(String poolName, Callable<T> task) {
        ThreadPoolExecutor executor = getThreadPool(poolName);
        return executor.submit(task);
    }
}

3. Spring 上下文工具类

typescript 复制代码
@Component
public class SpringContextUtil implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextUtil.applicationContext = applicationContext;
    }

    /**
     * 获取Bean
     */
    public static <T> T getBean(String beanName, Class<T> requiredType) {
        return applicationContext.getBean(beanName, requiredType);
    }

    /**
     * 获取Bean
     */
    public static <T> T getBean(Class<T> requiredType) {
        return applicationContext.getBean(requiredType);
    }
}

五、线程池最佳实践与避坑指南

作为一名八年经验的高级 Java 开发,我总结了以下线程池使用的最佳实践和避坑指南,希望能帮助你在项目中避免踩坑。

1. 最佳实践

  1. 使用有界队列:生产环境必须使用有界队列,避免任务无限堆积导致 OOM。
  2. 自定义线程工厂:设置有意义的线程名称,方便排查问题。
  3. 使用自定义拒绝策略:根据业务需求,自定义拒绝策略,记录日志、发送告警、持久化任务等。
  4. 动态调整线程池参数:结合配置中心,实现线程池参数的动态调整,应对流量波动。
  5. 优雅关闭线程池:注册 JVM 关闭钩子,在应用关闭时,优雅关闭线程池,避免任务丢失。
  6. 监控线程池状态:通过 Spring Boot Actuator 或自定义监控,监控线程池的状态,及时发现问题。
  7. 避免使用 Executors 创建线程池Executors 提供的 newFixedThreadPoolnewCachedThreadPool 等方法,默认使用无界队列,容易导致 OOM。

2. 避坑指南

  1. 坑 1:使用无界队列,导致任务堆积,最终 OOM。

    • 解决方案:使用有界队列,结合拒绝策略一起使用。
  2. 坑 2:核心线程数和最大线程数设置过大,导致 CPU 上下文切换飙升,系统性能下降。

    • 解决方案:根据业务类型(CPU 密集型 / IO 密集型)和压测结果,合理设置核心线程数和最大线程数。
  3. 坑 3:拒绝策略配置不合理,导致核心任务丢失。

    • 解决方案 :核心业务使用 AbortPolicy + 自定义异常处理,非核心业务使用 CallerRunsPolicy 或自定义拒绝策略。
  4. 坑 4:线程池没有优雅关闭,导致应用关闭时任务丢失。

    • 解决方案 :注册 JVM 关闭钩子,在应用关闭时,调用 shutdown() 方法,等待任务执行完成。
  5. 坑 5:动态调整线程池参数时,没有监控线程池状态,导致参数调整异常。

    • 解决方案:在动态调整线程池参数时,记录线程池的状态,并发送告警。

六、总结

线程池是 Java 后端开发中不可或缺的工具,掌握线程池的核心参数、拒绝策略和动态调整技巧,是高级 Java 开发的必备技能。本文从高级 Java 开发的视角,深度解析了线程池的核心参数,详细讲解了 4 种默认拒绝策略和自定义拒绝策略的实战,结合 Nacos 实现了线程池参数的动态调整,并封装了可直接在生产环境中使用的通用线程池工具类。

相关推荐
Chenyiax9 小时前
从 Chat 到 Responses:OpenAI API 抽象为什么变了?
后端
MariaH9 小时前
Koa和Express的区别
后端
MariaH9 小时前
Koa框架的使用
后端
luckdewei11 小时前
那个用 passlib 做认证的新同事,上线第一天就把用户密码写进了日志
后端
ping某12 小时前
为什么 Nginx 明明监听了 80,转发后端时却用了 4xxxx 端口?
后端·nginx
JustHappy12 小时前
我汇总了身边朋友的经历才发现,其实第一份实习是最难找的......
前端·后端·面试
uhakadotcom12 小时前
在python 的 工程化架构中 ,什么是 薄包装器层?
后端·面试·github
唐青枫16 小时前
Java JDBC 实战指南:从 Connection 到事务和连接池
java
用户14748530797416 小时前
CodeX使用Skill生成游戏美术和音乐资源,一分钟入门
后端
Melody12316 小时前
用 abort 中断 AI 流式请求,我之前做错了
后端