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)

大家都在看

  • 手套—牛客网

    left[i] 和right[i] 是匹配的数值 0 和 1 左边0个 右边1个 因为数据中有0这种数值 说明有不能匹配的手套。所以这些手套必须加上 避免没有全拿时 拿到了不匹配的…

    Linux 2023年6月13日
    096
  • 6.22(js–>案例应用)

    (练习1)简易计算器: <html lang="en"> <head> <meta charset="UTF-8&quo…

    Linux 2023年6月7日
    0117
  • 面试题:Java序列化与反序列化

    序列化和反序列化的概念 应用场景? 序列化实现的方式 继承Serializable接口,普通序列化 继承Externalizable接口,强制自定义序列化 serialVersio…

    Linux 2023年6月6日
    0124
  • Docker部署Dotnet

    方法一:打包+镜像 部署 将要部署的项目及其依赖的项目上传至指定文件夹下 要部署的项目添加Docker支持,生成Dockerfile文件 将生成的Dockerfile文件上传至要部…

    Linux 2023年6月13日
    0116
  • linux学习相关资料整理

    Posted on2022-09-08 13:09 brad1208 阅读(20 ) 评论() 编辑 linux常用指令记录 Python3.9.9安装 supervisor安装与…

    Linux 2023年6月6日
    099
  • 剑指offer计划18( 搜索与回溯算法中等)—java

    1.1、题目1 剑指 Offer 55 – II. 平衡二叉树 1.2、解法 递归和下一面一题的结合版,abs去绝对值判断两边的差,然后递归isBalanced来遍历二…

    Linux 2023年6月11日
    064
  • 排查线上问题的9种方式

    德国科技管理专家斯坦门茨早年移居美国,他以非凡的才能成为美国企业界的佼佼者。一次,美国著名的福特公司的一组电机发生故障,在束手无策之时,公司请斯坦门茨出马解决问题。 斯坦门茨在电机…

    Linux 2023年6月14日
    098
  • 022.常见硬盘检测方式

    硬盘监测概述 硬盘异常损坏日常相对概率较高,同时不同的文件系统(xfs,reiserfs,ext3)其检测方式不同。建议使用dmesag查看有没有硬件I/O故障的日志,也可使用用f…

    Linux 2023年6月7日
    093
  • Ruby快速入门

    推荐网站:http://ruby-for-beginners.rubymonstas.org/index.html源码参考:https://gitee.com/komavideo/…

    Linux 2023年6月14日
    0101
  • 解决vscode+python不提示numpy函数的问题

    使用vscode编写numpy代码时,对于numpy.array()等方法总是无法提示。查找了很多博客后,大部分都是修改配置和安装多种vscode插件,经过尝试后方法对于我来说无效…

    Linux 2023年6月7日
    0101
  • 整理一些Windows桌面运维常用的命令,并且整合成脚本

    github地址:alittlemc/toy 名字叫toy吧,没有啥技术含量,帮不了大忙,但是可以作为在一旁递工具的小弟,还是可减少自己一定的工作量的,毕竟有一些太长的命令也不是很…

    Linux 2023年6月6日
    0107
  • 用动态端口,增强winrm,open sshd的,服务器安全

    前言 我开发了一套开源,免费,跨平台的devops脚本批量运维工具。【kaiiit家的饭店】是软件的正式名字。【卡死你3000】是第一版开发代号。 想要增强win被控机密码安全。可…

    Linux 2023年6月14日
    085
  • 06-MyBatis中ResultType和ResultMap的区别

    MyBatis中ResultType和ResultMap的区别 如果数据库结果集中的列名和要封装的属性名完全一致的话用 resultType属性 如果数据库结果集中的列名和要封装实…

    Linux 2023年6月7日
    0106
  • Mysql 5.7开启binlog日志

    Mysql 5.7开启binlog日志 前言 binlog是MySQL的二进制日志,并且是MySQL中最重要的日志。binlog记录了对MySQL数据库执行更改的所有操作,包括对数…

    Linux 2023年6月6日
    0109
  • Linux 0.11源码阅读笔记-高速缓冲

    高速缓冲 概念 高速缓冲区是内存中的一块内存,它充当块设备和内核中其他程序之间的桥梁。如果内核程序需要访问块设备中的数据,则需要通过高速缓冲区进行间接操作。 [En] The hi…

    Linux 2023年5月27日
    081
  • QT:设置子窗口显示在父窗口的位置(绝对坐标)

    故事背景:最近需要在父窗体修改按钮上弹出二次确认框,之前要么使用 QDesktopWidget,要么使用QCursor,来设置弹窗位置,但是这两种方式不是很理想,就是想弹到相对父窗…

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