Spring Boot中异步调用的正确使用姿势(详解)【转】

介绍:
异步请求的处理。除了异步请求,一般上我们用的比较多的应该是异步调用。通常在开发过程中,会遇到一个方法是和实际业务无关的,没有紧密性的。比如记录日志信息等业务。这个时候正常就是启一个新线程去做一些业务处理,让主线程异步的执行其他业务。

使用方式:

在 Spring Framework 的 Spring Task 模块,提供了 @Async 注解,可以添加在方法上,自动实现该方法的异步调用

需要在启动类或配置类加上@EnableAsync使异步调用@Async注解生效

在需要异步执行的方法上加入此注解即可@Async(“threadPool”),threadPool为自定义线程池。(@Async默认使用SimpleAsyncTaskExecutor线程池。也可以根据Bean Name指定特定线程池)

注意事项:

在默认情况下,未设置TaskExecutor时,默认是使用SimpleAsyncTaskExecutor这个线程池,但此线程不是真正意义上的线程池,因为线程不重用,每次调用都会创建一个新的线程。可通过控制台日志输出可以看出,每次输出线程名都是递增的。所以最好我们来自定义一个线程池。

调用的异步方法,不能为同一个类的方法(包括同一个类的内部类),简单来说,因为Spring在启动扫描时会为其创建一个代理类,而同类调用时,还是调用本身的代理类的,所以和平常调用是一样的。

其他的注解如@Cache等也是一样的道理,说白了,就是Spring的代理机制造成的。所以在开发中,最好把异步服务单独抽出一个类来管理。

一些异步使用场景
文章阅读的业务逻辑 = 查询文章详情 + 更新文章阅读量后再响应客户端, 其实也无需等到阅读量更新后才响应文章详情给客户端, 用户查看文章是主要逻辑, 而文章阅读量更新是次要逻辑, 况且阅读量就算更新失败一点数据偏差也不会影响用户阅读因此这两个数据库操作之间的一致性是较弱的,这类都能用异步事件去优化。

1.2 @Async失效场景
异步方法使用static修饰
异步类没有使用@Component、@Service等注解,导致spring无法扫描到异步类
调用的异步方法,不能为同一个类的方法(包括同一个类的内部类)。PS:因为Spring在启动扫描时会为其创建一个代理类,而同类调用时,还是调用本身的代理类的,所以和平常调用是一样的
类中需要使用@Autowired或@Resource等注解自动注入,不能自己手动new对象
如果使用SpringBoot框架必须在启动类中增加@EnableAsync注解
在Async 方法上标注@Transactional是没用的。 在Async 方法调用的方法上标注@Transactional 有效。

同步service方法


/**
 * @Author LiangYiXiang
 * @Description 定义同步任务service
 * @Date 2021/11/13
 **/
@Component
public class Task {

    public static Random random =new Random();

    public void doTaskOne() throws Exception {
        System.out.println("开始做任务一");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        System.out.println("完成任务一,耗时:" + (end - start) + "毫秒");
    }

    public void doTaskTwo() throws Exception {
        System.out.println("开始做任务二");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        System.out.println("完成任务二,耗时:" + (end - start) + "毫秒");
    }

    public void doTaskThree() throws Exception {
        System.out.println("开始做任务三");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        System.out.println("完成任务三,耗时:" + (end - start) + "毫秒");
    }

}

异步service方法


@Component
public class AsyncTask {

    public static Random random =new Random();

    @Async
    public Future<string> doTaskOne() throws Exception {
        System.out.println("&#x5F00;&#x59CB;&#x505A;&#x4EFB;&#x52A1;&#x4E00;");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        System.out.println("&#x5B8C;&#x6210;&#x4EFB;&#x52A1;&#x4E00;&#xFF0C;&#x8017;&#x65F6;&#xFF1A;" + (end - start) + "&#x6BEB;&#x79D2;");
        return new AsyncResult<>("&#x4EFB;&#x52A1;&#x4E00;&#x5B8C;&#x6210;");
    }

    @Async
    public Future<string> doTaskTwo() throws Exception {
        System.out.println("&#x5F00;&#x59CB;&#x505A;&#x4EFB;&#x52A1;&#x4E8C;");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        System.out.println("&#x5B8C;&#x6210;&#x4EFB;&#x52A1;&#x4E8C;&#xFF0C;&#x8017;&#x65F6;&#xFF1A;" + (end - start) + "&#x6BEB;&#x79D2;");
        return new AsyncResult<>("&#x4EFB;&#x52A1;&#x4E8C;&#x5B8C;&#x6210;");
    }

    @Async
    public Future<string> doTaskThree() throws Exception {
        System.out.println("&#x5F00;&#x59CB;&#x505A;&#x4EFB;&#x52A1;&#x4E09;");
        long start = System.currentTimeMillis();
        Thread.sleep(random.nextInt(10000));
        long end = System.currentTimeMillis();
        System.out.println("&#x5B8C;&#x6210;&#x4EFB;&#x52A1;&#x4E09;&#xFF0C;&#x8017;&#x65F6;&#xFF1A;" + (end - start) + "&#x6BEB;&#x79D2;");
        return new AsyncResult<>("&#x4EFB;&#x52A1;&#x4E09;&#x5B8C;&#x6210;");
    }

}
</string></string></string>

测试类

PS:记得在启动类或配置类加上@EnableAsync,使异步调用@Async注解生效


@SpringBootTest
public class TaskTest {

    @Autowired
    private Task task;

    @Autowired
    private AsyncTask asyncTask;

    /**
     * &#x4E09;&#x4E2A;task&#x540C;&#x6B65;&#x8C03;&#x7528;
     *
     * @author Liangyixiang
     * @date 2021/11/13
     **/
    @Test
    public void taskTest() throws Exception {
        long start = System.currentTimeMillis();
        task.doTaskOne();
        task.doTaskTwo();
        task.doTaskThree();
        long end = System.currentTimeMillis();

        System.out.println("&#x540C;&#x6B65;&#x8C03;&#x7528;&#x5168;&#x90E8;&#x5B8C;&#x6210;&#xFF0C;&#x603B;&#x8017;&#x65F6;&#xFF1A;" + (end - start) + "&#x6BEB;&#x79D2;");
    }

    /**
     * &#x4E09;&#x4E2A;task&#x5F02;&#x6B65;&#x8C03;&#x7528;
     * &#x4E0E;&#x540C;&#x6B65;&#x533A;&#x522B;&#xFF1A;
     * 1. &#x5728;&#x6D4B;&#x8BD5;&#x7528;&#x4F8B;&#x4E00;&#x5F00;&#x59CB;&#x8BB0;&#x5F55;&#x5F00;&#x59CB;&#x65F6;&#x95F4;
     *
     * 2. &#x5728;&#x8C03;&#x7528;&#x4E09;&#x4E2A;&#x5F02;&#x6B65;&#x51FD;&#x6570;&#x7684;&#x65F6;&#x5019;&#xFF0C;&#x8FD4;&#x56DE;Future&#x7C7B;&#x578B;&#x7684;&#x7ED3;&#x679C;&#x5BF9;&#x8C61;
     *
     * 3. &#x5728;&#x8C03;&#x7528;&#x5B8C;&#x4E09;&#x4E2A;&#x5F02;&#x6B65;&#x51FD;&#x6570;&#x4E4B;&#x540E;&#xFF0C;&#x5F00;&#x542F;&#x4E00;&#x4E2A;&#x5FAA;&#x73AF;&#xFF0C;&#x6839;&#x636E;&#x8FD4;&#x56DE;&#x7684;Future&#x5BF9;&#x8C61;&#x6765;&#x5224;&#x65AD;&#x4E09;&#x4E2A;&#x5F02;&#x6B65;&#x51FD;&#x6570;&#x662F;&#x5426;&#x90FD;&#x7ED3;&#x675F;&#x4E86;&#x3002;&#x82E5;&#x90FD;&#x7ED3;&#x675F;&#xFF0C;&#x5C31;&#x7ED3;&#x675F;&#x5FAA;&#x73AF;&#xFF1B;&#x82E5;&#x6CA1;&#x6709;&#x90FD;&#x7ED3;&#x675F;&#xFF0C;&#x5C31;&#x7B49;1&#x79D2;&#x540E;&#x518D;&#x5224;&#x65AD;&#x3002;
     *
     * @author Liangyixiang
     * @date 2021/11/13
     **/
    @Test
    public void asyncTest() throws Exception {

        long start = System.currentTimeMillis();

        Future<string> task1 = asyncTask.doTaskOne();
        Future<string> task2 = asyncTask.doTaskTwo();
        Future<string> task3 = asyncTask.doTaskThree();

        while(true) {
            if(task1.isDone() && task2.isDone() && task3.isDone()) {
                // &#x4E09;&#x4E2A;&#x4EFB;&#x52A1;&#x90FD;&#x8C03;&#x7528;&#x5B8C;&#x6210;&#xFF0C;&#x9000;&#x51FA;&#x5FAA;&#x73AF;&#x7B49;&#x5F85;
                break;
            }
            Thread.sleep(1000);
        }

        long end = System.currentTimeMillis();

        System.out.println("&#x5F02;&#x6B65;&#x8C03;&#x7528;&#x5168;&#x90E8;&#x5B8C;&#x6210;&#xFF0C;&#x603B;&#x8017;&#x65F6;&#xFF1A;" + (end - start) + "&#x6BEB;&#x79D2;");

    }
}
</string></string></string>

结果:

异步处理的耗时还是比同步快很多(这里没有控制变量,但是看结果懂的都懂)

image-20211114105807939

image-20211114105823328
3. @Async异步调用使用详解及优化
3.1 当前使用分析

Spring Task 提供的 @Async 注解,声明式异步 。在实现原理上,也是基于 Spring AOP 拦截,调用者将在调用时立即返回,方法的实际执行将提交给Spring TaskExecutor的任务中,由指定的线程池中的线程执行,达到异步调用的目的。

Spring应用默认的线程池,指在@Async注解在使用时,不指定线程池的名称。查看源码,@Async的默认线程池为SimpleAsyncTaskExecutor。

默认线程池的弊端

在线程池应用中,参考阿里巴巴java开发规范:线程池不允许使用Executors去创建,不允许使用系统默认的线程池,推荐通过ThreadPoolExecutor的方式,这样的处理方式让开发的工程师更加明确线程池的运行规则,规避资源耗尽的风险。

Executors各个方法的弊端:
newFixedThreadPool和newSingleThreadExecutor:主要问题是堆积的请求处理队列可能会耗费非常大的内存,甚至OOM。
newCachedThreadPool和newScheduledThreadPool:要问题是线程数最大数是Integer.MAX_VALUE,可能会创建数量非常多的线程,甚至OOM。

@Async默认异步配置使用的是SimpleAsyncTaskExecutor,该线程池默认来一个任务创建一个线程,若系统中不断的创建线程,最终会导致系统占用内存过高,引发OutOfMemoryError错误。针对线程创建问题,SimpleAsyncTaskExecutor提供了限流机制,通过concurrencyLimit属性来控制开关,当concurrencyLimit>=0时开启限流机制,默认关闭限流机制即concurrencyLimit=-1,当关闭情况下,会不断创建新的线程来处理任务。基于默认配置,SimpleAsyncTaskExecutor并不是严格意义的线程池,达不到线程复用的功能。
Spring 已经实现的线程池
SimpleAsyncTaskExecutor:不是真的线程池,这个类不重用线程,默认每次调用都会创建一个新的线程。
SyncTaskExecutor:这个类没有实现异步调用,只是一个同步操作。只适用于不需要多线程的地方。
ConcurrentTaskExecutor:Executor的适配类,不推荐使用。如果ThreadPoolTaskExecutor不满足要求时,才用考虑使用这个类。
SimpleThreadPoolTaskExecutor:是Quartz的SimpleThreadPool的类。线程池同时被quartz和非quartz使用,才需要使用此类。
ThreadPoolTaskExecutor :最常使用,推荐。其实质是对java.util.concurrent.ThreadPoolExecutor的包装。
异步的方法有
最简单的异步调用,返回值为void
带参数的异步调用,异步方法可以传入参数
存在返回值,常调用返回Future

3.2 自定义线程池执行异步方法

自定义线程池,可对系统中线程池更加细粒度的控制,方便调整线程池大小配置,线程执行异常控制和处理。在设置系统自定义线程池代替默认线程池时,虽可通过多种模式设置,但替换默认线程池最终产生的线程池有且只能设置一个(不能设置多个类继承AsyncConfigurer)自定义线程池有如下模式:
重新实现接口AsyncConfigurer
继承AsyncConfigurerSupport
配置由自定义的TaskExecutor替代内置的任务执行器

通过查看Spring源码关于@Async的默认调用规则,会优先查询源码中实现AsyncConfigurer这个接口的类,实现这个接口的类为AsyncConfigurerSupport。但默认配置的线程池和异步处理方法均为空,所以,无论是继承或者重新实现接口,都需指定一个线程池。且重新实现public Executor getAsyncExecutor()方法

最简单的实现方式:

/**
 * @Author LiangYiXiang
 * @Description &#x914D;&#x7F6E;&#x81EA;&#x5B9A;&#x4E49;&#x7EBF;&#x7A0B;&#x6C60; bean name&#x65B9;&#x5F0F;&#x8C03;&#x7528;
 * @Date 2021/11/14
 **/
@Configuration
public class AsyncConfig {

    @Bean("customExecutor")
    public ThreadPoolTaskExecutor asyncOperationExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // &#x8BBE;&#x7F6E;&#x6838;&#x5FC3;&#x7EBF;&#x7A0B;&#x6570;
        executor.setCorePoolSize(8);
        // &#x8BBE;&#x7F6E;&#x6700;&#x5927;&#x7EBF;&#x7A0B;&#x6570;
        executor.setMaxPoolSize(20);
        // &#x8BBE;&#x7F6E;&#x961F;&#x5217;&#x5927;&#x5C0F;
        executor.setQueueCapacity(Integer.MAX_VALUE);
        // &#x8BBE;&#x7F6E;&#x7EBF;&#x7A0B;&#x6D3B;&#x8DC3;&#x65F6;&#x95F4;(&#x79D2;)
        executor.setKeepAliveSeconds(60);
        // &#x8BBE;&#x7F6E;&#x7EBF;&#x7A0B;&#x540D;&#x524D;&#x7F00;+&#x5206;&#x7EC4;&#x540D;&#x79F0;
        executor.setThreadNamePrefix("AsyncOperationThread-");
        executor.setThreadGroupName("AsyncOperationGroup");
        // &#x6240;&#x6709;&#x4EFB;&#x52A1;&#x7ED3;&#x675F;&#x540E;&#x5173;&#x95ED;&#x7EBF;&#x7A0B;&#x6C60;
        executor.setWaitForTasksToCompleteOnShutdown(true);
        // &#x521D;&#x59CB;&#x5316;
        executor.initialize();
        return executor;
    }
}

使用:

/**
 * &#x4F7F;&#x7528;&#x81EA;&#x5B9A;&#x4E49;&#x7EBF;&#x7A0B;&#x6C60;&#x5F02;&#x6B65;&#x65B9;&#x6CD5;
 *
 * @return void
 * @author Liangyixiang
 * @date 2021/11/14
 **/
@Async("customExecutor")
public void testAsyncTask3() throws InterruptedException {
    System.out.println("&#x5185;&#x90E8;&#x7EBF;&#x7A0B;&#xFF1A;" + Thread.currentThread().getName());
    System.out.println("&#x5F00;&#x59CB;&#x6267;&#x884C;&#x4EFB;&#x52A1;2");
    long start = System.currentTimeMillis();
    Thread.sleep(2000);
    long end = System.currentTimeMillis();
    System.out.println("&#x5B8C;&#x6210;&#x4EFB;&#x52A1;&#x4E8C;&#xFF0C;&#x8017;&#x65F6;&#xFF1A;" + (end - start) + "&#x6BEB;&#x79D2;");
}

PS:比较推荐的配置方式如下3.3
3.3 全局处理异步方法中的异常

实现AsyncConfigurer接口的getAsyncExecutor方法和getAsyncUncaughtExceptionHandler方法改造配置类

全局异步异常处理:

/**
 * @Author LiangYiXiang
 * @Description &#x5168;&#x5C40;&#x5F02;&#x6B65;&#x8C03;&#x7528;&#x9519;&#x8BEF;&#x5904;&#x7406;
 * @Date 2021/11/14
 **/
public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

    @Override
    public void handleUncaughtException(Throwable throwable, Method method, Object... obj) {
        System.out.println("&#x5F02;&#x5E38;&#x6355;&#x83B7;---------------------------------");
        System.out.println("Exception message - " + throwable.getMessage());
        System.out.println("Method name - " + method.getName());
        for (Object param : obj) {
            System.out.println("Parameter value - " + param);
        }
        System.out.println("&#x5F02;&#x5E38;&#x6355;&#x83B7;---------------------------------");
    }

}

配置类:

/**
 * @Author LiangYiXiang
 * @Description &#x5B9E;&#x73B0;AsyncConfigurer&#x63A5;&#x53E3;&#xFF0C;&#x914D;&#x7F6E;&#x81EA;&#x5B9A;&#x4E49;&#x7EBF;&#x7A0B;&#x6C60;
 * @Date 2021/11/14
 **/
@Configuration
public class AsyncConfig implements AsyncConfigurer {
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // &#x8BBE;&#x7F6E;&#x6838;&#x5FC3;&#x7EBF;&#x7A0B;&#x6570;
        executor.setCorePoolSize(8);
        // &#x8BBE;&#x7F6E;&#x6700;&#x5927;&#x7EBF;&#x7A0B;&#x6570;
        executor.setMaxPoolSize(20);
        // &#x8BBE;&#x7F6E;&#x961F;&#x5217;&#x5927;&#x5C0F;
        executor.setQueueCapacity(Integer.MAX_VALUE);
        // &#x8BBE;&#x7F6E;&#x7EBF;&#x7A0B;&#x6D3B;&#x8DC3;&#x65F6;&#x95F4;(&#x79D2;)
        executor.setKeepAliveSeconds(60);
        // &#x8BBE;&#x7F6E;&#x7EBF;&#x7A0B;&#x540D;&#x524D;&#x7F00;+&#x5206;&#x7EC4;&#x540D;&#x79F0;
        executor.setThreadNamePrefix("MyThread-");
        executor.setThreadGroupName("MyAsyncGroup");
        // &#x6240;&#x6709;&#x4EFB;&#x52A1;&#x7ED3;&#x675F;&#x540E;&#x5173;&#x95ED;&#x7EBF;&#x7A0B;&#x6C60;
        executor.setWaitForTasksToCompleteOnShutdown(true);
        // &#x521D;&#x59CB;&#x5316;
        executor.initialize();
        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new CustomAsyncExceptionHandler();
    }
}

异步请求是会一直等待response相应的,需要返回结果给客户端的;而异步调用我们往往会马上返回给客户端响应,完成这次整个的请求,至于异步调用的任务后台自己慢慢跑就行,客户端不会关心。
5. 最后一些思考
进程内 的队列或者线程池,相对不可靠 的原因是,队列和线程池中的任务仅仅存储在内存中,如果 JVM 进程被异常关闭,将会导致丢失,未被执行。例如:异步方法执行失败后对Controller前半部分的非异步操作无影响, 异步方法在整个业务逻辑中不是100%可靠的,对于强一致性的业务来说不适用。
而分布式消息队列,异步调用会以一个消息的形式,存储在消息队列的服务器上,所以即使 JVM 进程被异常关闭,消息依然在消息队列的服务器上。所以,使用进程内 的队列或者线程池来实现异步调用的话,一定要尽可能的保证 JVM 进程的优雅关闭,保证它们在关闭前被执行完成。
考虑到异步调用的可靠性 ,我们一般会考虑引入分布式消息队列,例如说 RabbitMQ、RocketMQ、Kafka 等等。但是在一些时候,我们并不需要这么高的可靠性,可以使用进程内 的队列或者线程池。
————————————————
版权声明:本文为CSDN博主「Gangbb」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_37132495/article/details/121322146

Original: https://www.cnblogs.com/fb010001/p/16711450.html
Author: 方斌
Title: Spring Boot中异步调用的正确使用姿势(详解)【转】

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/586162/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • Python代码模板

    #!/usr/bin/env python -*- encoding: utf-8 -*- class ClassName: def __init__(self, arg1, ar…

    Linux 2023年6月14日
    080
  • ssh远程连接服务(二)三台虚拟机之间的免密登录

    创建三台虚拟机主机名分别为node01、node02、node03 在node01虚拟机上生成密钥对 然后将生成的公钥分别复制到node02、node03的虚拟机上(前提三台虚拟机…

    Linux 2023年6月7日
    094
  • dbus的奇妙世界

    故事背景 在linux开发中我们经常会用到dbus来进行进程间通信,但是如何理解dbus服务端和客户端呢?很多小伙伴可能都会遇到类似的问题,而且都是含含糊糊的,接下来我们直接上硬菜…

    Linux 2023年5月27日
    081
  • 软件工程 统一过程软件(RUP) 第5篇随笔

    1.RUP简介 本质: 是”一般的过程框架” 为软件开发,进行不同抽象层之间”映射”,安排其开发活动的次序,指定任务和需要开发的志平…

    Linux 2023年6月7日
    0106
  • 高等代数:3 线性方程组的解集的结构

    3 线性方程组的解集的结构 1、定义1:数域K上所有n元有序数组组成的集合(K^{n}),连同定义在它上面的加法运算和数量乘法运算,以及满足的8条运算法则一起,称为数域K上的一个 …

    Linux 2023年6月8日
    085
  • 如何写出健壮可靠的shell脚本

    1 脚本失败时即退出 ; set -e 例子: 可以在脚本的开头设置如下set -e 2 打印脚本执行过程 sh -x test.sh #整个过程执行了哪些命令或者在开头加上set…

    Linux 2023年5月28日
    079
  • idea 运行 tyarn 命令提示系统禁止运行脚本

    无法加载文件D:……….(报错信息。。。),因为在此系统上禁止运行脚本,有关详细信息,请参阅 https:/go.microsoft.com/f…

    Linux 2023年6月13日
    079
  • 玩转redis-简单消息队列

    使用 go语言基于 redis写了一个简单的消息队列源码地址使用demo redis的 list 非常的灵活,可以从左边或者右边添加元素,当然也以从任意一头读取数据 添加数据和获取…

    Linux 2023年5月28日
    093
  • 操作系统实现-boot.asm实现

    博客网址:www.shicoder.top微信:18223081347欢迎加群聊天 :452380935 这一次我们进入操作系统实现的真实编码, 这一次主要是完善对boot.asm…

    Linux 2023年6月13日
    0122
  • 操作系统虚拟内存发展史

    404. 抱歉,您访问的资源不存在。 可能是URL不正确,或者对应的内容已经被删除,或者处于隐私状态。 [En] It may be that the URL is incorre…

    Linux 2023年5月27日
    0103
  • 【socket】基于socket通信-线程上报温度

    线程是一条执行路径,是程序执行时的最小单位,它是进程的一个执行流,是CPU调度和分派的基本单位,一个进程可以由很多个线程组成,线程间共享进程的所有资源,每个线程有自己的堆栈和局部变…

    Linux 2023年6月13日
    095
  • Java基础系列–07_Object类的学习及源码分析

    Java中Object根类及其底层的学习 Object: 超类(1)Object是类层次结构的顶层类,是所有类的 根类,超类。所有的类都直接或者间接的继承自Object类。所有对象…

    Linux 2023年6月7日
    080
  • PHP8.1.10手动安装教程及报错解决梳理

    安装php版本8.1.10:https://www.php.net/distributions/php-8.1.10.tar.gz 易错步骤梳理: 1、安装的版本是php8,因此教…

    Linux 2023年6月6日
    085
  • POJ1861(Network)-Kruskal

    题目在这 Sample Input 4 6 1 2 1 1 3 1 1 4 2 2 3 1 3 4 1 2 4 1 Sample Output 1 4 1 2 1 3 2 3 3 …

    Linux 2023年6月7日
    075
  • arch安装桌面环境

    arch可以安装图形用户界面需要的软件包有:xorg-server,xorg-xinit,xfce4 xorg是linux桌面环境下的服务程序,xorg-init是启动xorg的客…

    Linux 2023年6月13日
    081
  • [编程一生]历史文章分类汇总

    2021年过去了,总结一下我的239篇原创。方便大家利用自带的搜索功能当智能机器人来用。 面试类 方法论 架构类 网络通信与 操作系统原理 稳定性建设 Java 中间件 程序人生 …

    Linux 2023年6月13日
    085
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球