苏三说技术

苏三说技术 查看完整档案

成都编辑  |  填写毕业院校某知名互联网公司  |  资深java工程师 编辑 www.susan.net.cn 编辑
编辑

公众号:「苏三说技术」 维护者目前就职于某知名互联网公司,从事开发、架构和部分管理工作。实战经验丰富,对jdk、spring、springboot、springcloud、mybatis等开源框架源码有一定研究,欢迎关注,和我一起交流。

个人动态

苏三说技术 发布了文章 · 2020-12-31

15张图带你彻底弄明白spring循环依赖,再也不用怕了

1.由同事抛的一个问题开始

最近项目组的一个同事遇到了一个问题,问我的意见,一下子引起的我的兴趣,因为这个问题我也是第一次遇到。平时自认为对spring循环依赖问题还是比较了解的,直到遇到这个和后面的几个问题后,重新刷新了我的认识。

我们先看看当时出问题的代码片段:

@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    @Async
    public void test1() {
    }
}
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

这两段代码中定义了两个Service类:TestService1和TestService2,在TestService1中注入了TestService2的实例,同时在TestService2中注入了TestService1的实例,这里构成了循环依赖。

只不过,这不是普通的循环依赖,因为TestService1的test1方法上加了一个@Async注解。

大家猜猜程序启动后运行结果会怎样?

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'testService1': Bean with name 'testService1' has been injected into other beans [testService2] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.

报错了。。。原因是出现了循环依赖。

「不科学呀,spring不是号称能解决循环依赖问题吗,怎么还会出现?」

如果把上面的代码稍微调整一下:

@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    public void test1() {
    }
}

把TestService1的test1方法上的@Async注解去掉,TestService1和TestService2都需要注入对方的实例,同样构成了循环依赖。

但是重新启动项目,发现它能够正常运行。这又是为什么?

带着这两个问题,让我们一起开始spring循环依赖的探秘之旅。

2.什么是循环依赖?

循环依赖:说白是一个或多个对象实例之间存在直接或间接的依赖关系,这种依赖关系构成了构成一个环形调用。

第一种情况:自己依赖自己的直接依赖

file
第二种情况:两个对象之间的直接依赖

file
第三种情况:多个对象之间的间接依赖
file

前面两种情况的直接循环依赖比较直观,非常好识别,但是第三种间接循环依赖的情况有时候因为业务代码调用层级很深,不容易识别出来。

3.循环依赖的N种场景

spring中出现循环依赖主要有以下场景:
file

单例的setter注入

这种注入方式应该是spring用的最多的,代码如下:

@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    public void test1() {
    }
}
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

这是一个经典的循环依赖,但是它能正常运行,得益于spring的内部机制,让我们根本无法感知它有问题,因为spring默默帮我们解决了。

spring内部有三级缓存:

  • singletonObjects 一级缓存,用于保存实例化、注入、初始化完成的bean实例
  • earlySingletonObjects 二级缓存,用于保存实例化完成的bean实例
  • singletonFactories 三级缓存,用于保存bean创建工厂,以便于后面扩展有机会创建代理对象。

下面用一张图告诉你,spring是如何解决循环依赖的:

file

                                            图1

细心的朋友可能会发现在这种场景中第二级缓存作用不大。

那么问题来了,为什么要用第二级缓存呢?

试想一下,如果出现以下这种情况,我们要如何处理?

@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;
    @Autowired
    private TestService3 testService3;

    public void test1() {
    }
}
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}
@Service
publicclass TestService3 {

    @Autowired
    private TestService1 testService1;

    public void test3() {
    }
}

TestService1依赖于TestService2和TestService3,而TestService2依赖于TestService1,同时TestService3也依赖于TestService1。

按照上图的流程可以把TestService1注入到TestService2,并且TestService1的实例是从第三级缓存中获取的。

假设不用第二级缓存,TestService1注入到TestService3的流程如图:

file

                                               图2

TestService1注入到TestService3又需要从第三级缓存中获取实例,而第三级缓存里保存的并非真正的实例对象,而是ObjectFactory对象。说白了,两次从三级缓存中获取都是ObjectFactory对象,而通过它创建的实例对象每次可能都不一样的。

这样不是有问题?

为了解决这个问题,spring引入的第二级缓存。上面图1其实TestService1对象的实例已经被添加到第二级缓存中了,而在TestService1注入到TestService3时,只用从第二级缓存中获取该对象即可。

file

                                                 图3

还有个问题,第三级缓存中为什么要添加ObjectFactory对象,直接保存实例对象不行吗?

答:不行,因为假如你想对添加到三级缓存中的实例对象进行增强,直接用实例对象是行不通的。

针对这种场景spring是怎么做的呢?

答案就在AbstractAutowireCapableBeanFactory类doCreateBean方法的这段代码中:

file
它定义了一个匿名内部类,通过getEarlyBeanReference方法获取代理对象,其实底层是通过AbstractAutoProxyCreator类的getEarlyBeanReference生成代理对象。

多例的setter注入

这种注入方法偶然会有,特别是在多线程的场景下,具体代码如下:

@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    public void test1() {
    }
}
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

很多人说这种情况spring容器启动会报错,其实是不对的,我非常负责任的告诉你程序能够正常启动。

为什么呢?

其实在AbstractApplicationContext类的refresh方法中告诉了我们答案,它会调用finishBeanFactoryInitialization方法,该方法的作用是为了spring容器启动的时候提前初始化一些bean。该方法的内部又调用了preInstantiateSingletons方法
file
标红的地方明显能够看出:非抽象、单例 并且非懒加载的类才能被提前初始bean。

而多例即SCOPE_PROTOTYPE类型的类,非单例,不会被提前初始化bean,所以程序能够正常启动。

如何让他提前初始化bean呢?

只需要再定义一个单例的类,在它里面注入TestService1

@Service
publicclass TestService3 {

    @Autowired
    private TestService1 testService1;
}

重新启动程序,执行结果:

Requested bean is currently in creation: Is there an unresolvable circular reference?

果然出现了循环依赖。

注意:这种循环依赖问题是无法解决的,因为它没有用缓存,每次都会生成一个新对象。

构造器注入

这种注入方式是spring4.x以上的版本中官方推荐的方式,具体如下代码:

@Service
publicclass TestService1 {

    public TestService1(TestService2 testService2) {
    }
}
@Service
publicclass TestService2 {

    public TestService2(TestService1 testService1) {
    }
}

运行结果:

Requested bean is currently in creation: Is there an unresolvable circular reference?

出现了循环依赖,为什么呢?

file
从图中的流程看出构造器注入只是添加了三级缓存,并没有使用缓存,所以也无法解决循环依赖问题。

单例的代理对象setter注入

这种注入方式其实也比较常用,比如平时使用:@Async注解的场景,会通过AOP自动生成代理对象。

我那位同事的问题也是这种情况。

@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    @Async
    public void test1() {
    }
}
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

从前面得知程序启动会报错,出现了循环依赖:

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'testService1': Bean with name 'testService1' has been injected into other beans [testService2] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using 'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example.

为什么会循环依赖呢?

答案就在下面这张图中:

file

说白了,bean初始化完成之后,后面还有一步去检查:第二级缓存 和 原始对象 是否相等。由于它对前面流程来说无关紧要,所以前面的流程图中省略了,但是在这里是关键点,我们重点说说:

file

那位同事的问题正好是走到这段代码,发现第二级缓存 和 原始对象不相等,所以抛出了循环依赖的异常。

如果这时候把TestService1改个名字,改成:TestService6,其他的都不变。

@Service
publicclass TestService6 {

    @Autowired
    private TestService2 testService2;

    @Async
    public void test1() {
    }
}

再重新启动一下程序,神奇般的好了。

what? 这又是为什么?

这就要从spring的bean加载顺序说起了,默认情况下,spring是按照文件完整路径递归查找的,按路径+文件名排序,排在前面的先加载。所以TestService1比TestService2先加载,而改了文件名称之后,TestService2比TestService6先加载。

为什么TestService2比TestService6先加载就没问题呢?

答案在下面这张图中:

file

这种情况testService6中其实第二级缓存是空的,不需要跟原始对象判断,所以不会抛出循环依赖。

DependsOn循环依赖

还有一种有些特殊的场景,比如我们需要在实例化Bean A之前,先实例化Bean B,这个时候就可以使用@DependsOn注解。

@DependsOn(value = "testService2")
@Service
publicclass TestService1 {

    @Autowired
    private TestService2 testService2;

    public void test1() {
    }
}
@DependsOn(value = "testService1")
@Service
publicclass TestService2 {

    @Autowired
    private TestService1 testService1;

    public void test2() {
    }
}

程序启动之后,执行结果:

Circular depends-on relationship between 'testService2' and 'testService1'

这个例子中本来如果TestService1和TestService2都没有加@DependsOn注解是没问题的,反而加了这个注解会出现循环依赖问题。

这又是为什么?

答案在AbstractBeanFactory类的doGetBean方法的这段代码中:
file

它会检查dependsOn的实例有没有循环依赖,如果有循环依赖则抛异常。

4.出现循环依赖如何解决?

项目中如果出现循环依赖问题,说明是spring默认无法解决的循环依赖,要看项目的打印日志,属于哪种循环依赖。目前包含下面几种情况:

file

生成代理对象产生的循环依赖

这类循环依赖问题解决方法很多,主要有:

  • 使用@Lazy注解,延迟加载
  • 使用@DependsOn注解,指定加载先后关系
  • 修改文件名称,改变循环依赖类的加载顺序

使用@DependsOn产生的循环依赖

这类循环依赖问题要找到@DependsOn注解循环依赖的地方,迫使它不循环依赖就可以解决问题。

多例循环依赖

这类循环依赖问题可以通过把bean改成单例的解决。

构造器循环依赖

这类循环依赖问题可以通过使用@Lazy注解解决。

当然最好的解决循环依赖问题最佳方案是从代码设计上规避,但是复杂的系统中有可能没法避免。

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多BAT大厂的前辈交流和学习。

本文由博客群发一文多发等运营工具平台 OpenWrite 发布
查看原文

赞 1 收藏 0 评论 1

苏三说技术 发布了文章 · 2020-12-23

迷茫了,我们到底该不该用lombok?

前言

最近上网查资料发现很多人对lombok褒贬不一,引起了我的兴趣,因为我们项目中也在大量使用lombok,大家不同的观点让我也困惑了几天,今天结合我实际的项目经验,说说我的个人建议。

随便搜搜就找到了这几篇文章:

这些人建议使用 lombok,觉得它是一个神器,可以大大提高编码效率,并且让代码更优雅。

在搜索的过程中,有些文章却又不推荐使用:

这些人觉得它有一些坑,容易给项目埋下隐患,我们到底该听谁的呢?

为什么建议使用lombok?

1.传统javabean

在没使用lombok之前,我们一般是这样定义javabean的:

public class User {

    private Long id;
    private String name;
    private Integer age;
    private String address;

    public User() {

    }

    public User(Long id, String name, Integer age, String address) {
        this.id = id;
        this.name = name;
        this.age = age;
        this.address = address;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Integer getAge() {
        return age;
    }

    public String getAddress() {
        return address;
    }


    public void setId(Long id) {
        this.id = id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        User user = (User) o;
        return Objects.equals(id, user.id) &&
                Objects.equals(name, user.name) &&
                Objects.equals(age, user.age) &&
                Objects.equals(address, user.address);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, name, age, address);
    }

    @Override
    public String toString() {
        return "User{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", age=" + age +
                ", address='" + address + '\'' +
                '}';
    }
}

User类中包含了:成员变量、getter/setter方法、构造方法、equals、hashCode方法。

咋一看,代码还是挺多的。而且还有个问题,如果User类中的代码修改了,比如:age字段改成字符串类型,或者name字段名称修改了,是不是需要同步修改相关的成员变量、getter/setter方法、构造方法、equals、hashCode方法全都修改一遍?

也许有些朋友会说:现在的idea非常智能,可以把修改一次性搞定。

没错,但是有更优雅的处理方法。

2.lombok的使用

第一步,引入jar包

  <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.4</version>
      <scope>provided</scope>
  </dependency>

第二步,在idea中安装插件


注意:如果不按照插件idea中就无法编译使用lombok注解的代码。

第三步,在代码中使用lombok注解

上面的User类代码可以改成这样:

@ToString
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class User {

    private Long id;
    private String name;
    private Integer age;
    private String address;
}

so good,代码可以优化到如此简单。User类的主体只用定义成员变量,其他的方法全都交给注解来完成。

如果修改了成员变量名称或者类型,怎么办呢?

@ToString
@EqualsAndHashCode
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class User {

    private Long id;
    private String userName;
    private String age;
    private String address;
}

你只用一心一意修改成员变量即可,其他的根本不用操心,简直太爽了。

更让人兴奋的是,还能进一步优化:

@NoArgsConstructor
@AllArgsConstructor
@Data
public class User {

    private Long id;
    private String userName;
    private String age;
    private String address;
}

@Data相当于@Getter、@Setter、@ToString、@EqualsAndHashCode、@RequiredArgsConstructor的集合。

lombok注解整理如下:

                  图片来源占小狼

从上面看出使用lombok给人最大的感受是代码量显著减少了,能够有效的提升开发效率,而代码看起来更优雅,确实是一个不可多得的神器。

lombok工作原理

java程序的解析分为:运行时解析编译时解析

通常我们通过反射获取类、方法、注解和成员变量就是运行时解析。但是这种方式效率其实不高,要在程序运行起来才能解析。

这时候编译时解析就体现出它的价值了。

编译时解析又分为:注解处理器(Annotation Processing Tool)
JSR 269 插入式注解处理器(Pluggable Annotation Processing API)

第一种处理器它最早是在 JDK 1.5 与注解(Annotation) 一起引入的,它是一个命令行工具,能够提供构建时基于源代码对程序结构的读取功能,能够通过运行注解处理器来生成新的中间文件,进而影响编译过程。

不过在JDK 1.8以后,第一种处理器被淘汰了,取而代之的是第二种处理器,我们一起看看它的处理流程:

Lombok的底层具体实现流程如下:

  1. javac对源代码进行分析,生成了一棵抽象语法树(AST)
  2. 编译过程中调用实现了“JSR 269 API”的Lombok程序
  3. 此时Lombok就对第一步骤得到的AST进行处理,找到@Data注解所在类对应的语法树(AST),然后修改该语法树(AST),增加getter和setter方法定义的相应树节点
  4. javac使用修改后的抽象语法树(AST)生成字节码文件,即给class增加新的节点(代码块)

为什么建议不用lombok?

即使lombok是一个神器,但是却有很多人不建议使用,这又是为什么呢?

1.强制要求队友安装idea插件

这点确实比较恶心,因为如果使用lombok注解编写代码,就要求参与开发的所有人都必须安装idea的lombok插件,否则代码编译出错。

2.代码可读性变差

使用lombok注解之后,最后生成的代码你其实是看不到的,你能看到的是代码被修改之前的样子。如果要想查看某个getter或setter方法的引用过程,是非常困难的。

3.升级JDK对功能有影响

有人把JDK从Java 8升级到Java 11时,我发现Lombok不能正常工作了。

4.有一些坑

  1. 使用@Data时会默认使用@EqualsAndHashCode(callSuper=false),这时候生成的equals()方法只会比较子类的属性,不会考虑从父类继承的属性,无论父类属性访问权限是否开放。
  2. 使用@Builder时要加上@AllArgsConstructor,否则可能会报错。

5.不便于调试

我们平时大部分人都喜欢用debug调试定位问题,但是使用lombok生成的代码不太好调试。

6.上下游系统强依赖

如果上游系统中提供的fegin client使用了lombok,那么下游系统必须也使用lombok,否则会报错,上下游系统构成了强依赖。

我们该如何选择?

lombok有利有弊,我们该如何选择呢?

个人建议要结合项目的实际情况做最合理的选择。

  1. 如果你参与的是一个新项目,上下游系统都是新的,这时候建议使用lombok,因为它可以显著提升开发效率。
  2. 如果你参与的是一个老项目,并且以前没有使用过lombok,建议你后面也不要使用,因为代码改造成本较高。如果以前使用过lombok,建议你后面也使用,因为代码改造成本较高。
  3. 其实只要引入jar包可能都有:强制要求队友安装idea插件、升级JDK对功能有影响、有一些坑 和 上下游系统强依赖 这几个问题,只要制定好规范,多总结使用经验这些问题不大。
  4. 代码的可读性变差 和 不便于调试 这两个问题,我认为也不大,因为lombok一般被使用在javabean上,该类的逻辑相对来说比较简单,很多代码一眼就能看明白,即使不调试问题原因也能猜测7、8分。

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

本文由博客群发一文多发等运营工具平台 OpenWrite 发布
查看原文

赞 1 收藏 0 评论 0

苏三说技术 发布了文章 · 2020-12-21

explain | 索引优化的这把绝世好剑,你真的会用吗?

前言

对于互联网公司来说,随着用户量和数据量的不断增加,慢查询是无法避免的问题。一般情况下如果出现慢查询,意味着接口响应慢、接口超时等问题。如果是高并发的场景,可能会出现数据库连接被占满的情况,直接导致服务不可用。

慢查询的确会导致很多问题,我们要如何优化慢查询呢?

主要解决办法有:

  • 监控sql执行情况,发邮件、短信报警,便于快速识别慢查询sql
  • 打开数据库慢查询日志功能
  • 简化业务逻辑
  • 代码重构、优化
  • 异步处理
  • sql优化
  • 索引优化

其他的办法先不说,后面有机会再单独介绍。今天我重点说说索引优化,因为它是解决慢查询sql问题最有效的手段。

如何查看某条sql的索引执行情况呢?

没错,在sql前面加上explain关键字,就能够看到它的执行计划,通过执行计划,我们可以清楚的看到表和索引执行的情况,索引有没有执行、索引执行顺序和索引的类型等。

索引优化的步骤是:

  1. 使用explain查看sql执行计划
  2. 判断哪些索引使用不当
  3. 优化sql,sql可能需要多次优化才能达到索引使用的最优值

既然索引优化的第一步是使用explain,我们先全面的了解一下它。

explain介绍

先看看mysql的官方文档是怎么描述explain的:

image.png

  • EXPLAIN可以使用于 SELECT, DELETE, INSERT, REPLACE,和 UPDATE语句。
  • 当EXPLAIN与可解释的语句一起使用时,MySQL将显示来自优化器的有关语句执行计划的信息。也就是说,MySQL解释了它将如何处理该语句,包括有关如何连接表以及以何种顺序连接表的信息。
  • 当EXPLAIN与非可解释的语句一起使用时,它将显示在命名连接中执行的语句的执行计划。
  • 对于SELECT语句, EXPLAIN可以显示的其他执行计划的警告信息。

explain详解

explain的语法:

`{EXPLAIN | DESCRIBE | DESC}`
 `tbl_name [col_name | wild]`
`{EXPLAIN | DESCRIBE | DESC}`
 `[explain_type]`
 `{explainable_stmt | FORCONNECTION connection_id}`
`explain_type: {`
 `EXTENDED`
 `| PARTITIONS`
 `| FORMAT = format_name`
`}`
`format_name: {`
 `TRADITIONAL`
 `| JSON`
`}`
`explainable_stmt: {`
 `SELECTstatement`
 `| DELETEstatement`
 `| INSERTstatement`
 `| REPLACEstatement`
 `| UPDATEstatement`
`}`

用一条简单的sql看看使用explain关键字的效果:

explain select * from test1;

执行结果:
image.png

从上图中看到执行结果中会显示12列信息,每列具体信息如下:

image.png

说白了,我们要搞懂这些列的具体含义才能正常判断索引的使用情况。

话不多说,直接开始介绍吧。

id列

该列的值是select查询中的序号,比如:1、2、3、4等,它决定了表的执行顺序。

某条sql的执行计划中一般会出现三种情况:

  1. id相同
  2. id不同
  3. id相同和不同都有

那么这三种情况表的执行顺序是怎么样的呢?

1.id相同

执行sql如下:

explain select * from test1 t1 inner join test1 t2 on t1.id=t2.id

结果:image.png
我们看到执行结果中的两条数据id都是1,是相同的。

这种情况表的执行顺序是怎么样的呢?

答案:从上到下执行,先执行表t1,再执行表t2。

执行的表要怎么看呢?

答案:看table字段,这个字段后面会详细解释。

2.id不同

执行sql如下:

explain select * from test1 t1 where t1.id = (select id from  test1 t2 where  t2.id=2);

结果:image.png
我们看到执行结果中两条数据的id不同,第一条数据是1,第二条数据是2。

这种情况表的执行顺序是怎么样的呢?

答案:序号大的先执行,这里会从下到上执行,先执行表t2,再执行表t1。

3.id相同和不同都有

执行sql如下:

explain
select t1.* from test1 t1
inner join (select max(id) mid from test1 group by id) t2
on t1.id=t2.mid

结果:

image.png

我们看到执行结果中三条数据,前面两条数据的的id相同,第三条数据的id跟前面的不同。

这种情况表的执行顺序又是怎么样的呢?

答案:先执行序号大的,先从下而上执行。遇到序号相同时,再从上而下执行。所以这个列子中表的顺序顺序是:test1、t1、

也许你会在这里心生疑问:<`derived2>` 是什么鬼?

它表示派生表,别急后面会讲的。

还有一个问题:id列的值允许为空吗?

答案在后面揭晓。

select_type列

该列表示select的类型。具体包含了如下11种类型:

image.png

但是常用的其实就是下面几个:

image.png

下面看看这些SELECT类型具体是怎么出现的:

1.SIMPLE


执行sql如下:

```
explain select * from test1;

```

结果:

image.png

它只在简单SELECT查询中出现,不包含子查询和UNION,这种类型比较直观就不多说了。

2.PRIMARY 和 SUBQUERY


执行sql如下:

```
explain select * from test1 t1 where t1.id = (select id from  test1 t2 where  t2.id=2);
```

结果:

image.png

我们看到这条嵌套查询的sql中,最外层的t1表是PRIMARY类型,而最里面的子查询t2表是SUBQUERY类型。

3.DERIVED


执行sql如下:

```

explain

select t1.* from test1 t1
inner join (select max(id) mid from test1 group by id) t2
on t1.id=t2.mid

```

结果:

image.png

最后一条记录就是衍生表,它一般是FROM列表中包含的子查询,这里是sql中的分组子查询。

4.UNION 和 UNION RESULT

执行sql如下:


```
explain
select * from test1
union
select* from test2

```

结果:
image.png

test2表是UNION关键字之后的查询,所以被标记为UNION,test1是最主要的表,被标记为PRIMARY。而<union1,2>表示id=1和id=2的表union,其结果被标记为UNION RESULT。

UNION 和 UNION RESULT一般会成对出现。

此外,回答上面的问题:id列的值允许为空吗?

如果仔细看上面那张图,会发现id列是可以允许为空的,并且是在SELECT类型为: UNION RESULT的时候。

table列

该列的值表示输出行所引用的表的名称,比如前面的:test1、test2等。

但也可以是以下值之一:

  • <unionM,N>:具有和id值的行的M并集N。
  • <derivedN>:用于与该行的派生表结果id的值N。派生表可能来自(例如)FROM子句中的子查询 。
  • <subqueryN>:子查询的结果,其id值为N

partitions列

该列的值表示查询将从中匹配记录的分区

type列

该列的值表示连接类型,是查看索引执行情况的一个重要指标。包含如下类型:
image.png

执行结果从最好到最坏的的顺序是从上到下。

我们需要重点掌握的是下面几种类型:

system > const > eq_ref > ref > range > index > ALL

在演示之前,先说明一下test2表中只有一条数据:

image.png

并且code字段上面建了一个普通索引:

image.png

下面逐一看看常见的几个连接类型是怎么出现的:

  1. system

    这种类型要求数据库表中只有一条数据,是const类型的一个特例,一般情况下是不会出现的。

  2. const

    通过一次索引就能找到数据,一般用于主键或唯一索引作为条件的查询sql中,执行sql如下:

    explain select * from test2 where id=1;
    

结果:
image.png

3.eq_ref

常用于主键或唯一索引扫描。执行sql如下:

 explain select * from test2 t1 inner join test2 t2 on t1.id=t2.id;
 

结果:

image.png

此时,有人可能感到不解,const和eq\_ref都是对主键或唯一索引的扫描,有什么区别?


   

答:const只索引一次,而eq_ref主键和主键匹配,由于表中有多条数据,一般情况下要索引多次,才能全部匹配上。

4.ref

常用于非主键和唯一索引扫描。执行sql如下:

   explain select * from test2 where code = '001';
   

结果:

image.png

5.range

常用于范围查询,比如:between ... and 或 In 等操作,执行sql如下:

explain select * from test2 where id between 1 and 2;

结果:
image.png

6.index

全索引扫描。执行sql如下:

explain select code from test2

结果:
image.png

7.ALL

全表扫描。执行sql如下:

explain select *  from test2;

结果:

image.png

 

possible_keys列

该列表示可能的索引选择。

请注意,此列完全独立于表的顺序,这就意味着possible_keys在实践中,某些键可能无法与生成的表顺序一起使用。

image.png

如果此列是NULL,则没有相关的索引。在这种情况下,您可以通过检查该WHERE 子句以检查它是否引用了某些适合索引的列,从而提高查询性能。

key列

该列表示实际用到的索引。

可能会出现possible_keys列为NULL,但是key不为NULL的情况。

演示之前,先看看test1表结构:

image.png

test1表中数据:

image.png

使用的索引:

image.png

code和name字段使用了联合索引。

执行sql如下:

`explain select code  from test1;`

结果:

image.png

这条sql预计没有使用索引,但是实际上使用了全索引扫描方式的索引。

key_len列

该列表示使用索引的长度。上面的key列可以看出有没有使用索引,key_len列则可以更进一步看出索引使用是否充分。不出意外的话,它是最重要的列。
image.png

有个关键的问题浮出水面:key\_len是如何计算的?

决定key_len值的三个因素:

  1.字符集

  2.长度

  3.是否为空 

常用的字符编码占用字节数量如下:

image.png

目前我的数据库字符编码格式用的:UTF8占3个字节。

mysql常用字段占用字节数:
image.png

此外,如果字段类型允许为空则加1个字节。

上图中的 184是怎么算的?

184 = 30 3 + 2 + 30 3 + 2

再把test1表的code字段类型改成char,并且改成允许为空:

image.png

执行sql如下:

explain select code  from test1;

结果:
image.png

怎么算的?

183 = 30 3 + 1 + 30  3 + 2

**还有一个问题:为什么这列表示索引使用是否充分呢,还有使用不充分的情况?
**

执行sql如下:

explain select code  from test1 where code='001';

结果:

image.png

上图中使用了联合索引:idx_code_name,如果索引全匹配key_len应该是183,但实际上却是92,这就说明没有使用所有的索引,索引使用不充分。

ref列

该列表示索引命中的列或者常量。

执行sql如下:

explain select *  from test1 t1 inner join test1 t2 on t1.id=t2.id where t1.code='001';

结果:

image.png

我们看到表t1命中的索引是const(常量),而t2命中的索引是列sue库的t1表的id字段。

rows列

该列表示MySQL认为执行查询必须检查的行数。

image.png

对于InnoDB表,此数字是估计值,可能并不总是准确的。

filtered列

该列表示按表条件过滤的表行的估计百分比。最大值为100,这表示未过滤行。值从100减小表示过滤量增加。

image.png

rows显示了检查的估计行数,rows× filtered显示了与下表连接的行数。例如,如果 rows为1000且 filtered为50.00(50%),则与下表连接的行数为1000×50%= 500。

Extra列

该字段包含有关MySQL如何解析查询的其他信息,这列还是挺重要的,但是里面包含的值太多,就不一一介绍了,只列举几个常见的。

1.Impossible WHERE

表示WHERE后面的条件一直都是false,

执行sql如下:

 explain select code  from test1 where 'a' = 'b';

结果:

image.png

2.Using filesort

表示按文件排序,一般是在指定的排序和索引排序不一致的情况才会出现。

执行sql如下:

 explain select code  from test1 order by name desc;

结果:

image.png

这里建立的是code和name的联合索引,顺序是code在前,name在后,这里直接按name降序,跟之前联合索引的顺序不一样。

3.Using index

表示是否用了覆盖索引,说白了它表示是否所有获取的列都走了索引。

image.png

上面那个例子中其实就用到了:Using index,因为只返回一列code,它字段走了索引。

4.Using temporary

表示是否使用了临时表,一般多见于order by 和 group by语句。

执行sql如下:

explain select name  from test1 group by name;

结果:

image.png

  1. Using where

    表示使用了where条件过滤。

  2. Using join buffer

    表示是否使用连接缓冲。来自较早联接的表被部分读取到联接缓冲区中,然后从缓冲区中使用它们的行来与当前表执行联接。

索引优化的过程

   1.先用慢查询日志定位具体需要优化的sql

   2.使用explain执行计划查看索引使用情况

   3.重点关注:

       key(查看有没有使用索引)

       key_len(查看索引使用是否充分)

       type(查看索引类型)

       Extra(查看附加信息:排序、临时表、where条件为false等)

   一般情况下根据这4列就能找到索引问题。

   4.根据上1步找出的索引问题优化sql

   5.再回到第2步

最后说一句(求关注,别白嫖我)


如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多BAT大厂的前辈交流和学习。image.png

推荐阅读:

让人头痛的大事务问题到底要如何解决?

mybatis日志功能是如何设计的?

zuul如果两个filter的order一样,是如何排序的?

mysql的这几个坑你踩过没?真是防不胜防

线程池最佳线程数量到底要如何配置?

这8种保证线程安全的技术你都知道吗?

springboot面试杀手锏-自动配置原理

查看原文

赞 2 收藏 0 评论 0

苏三说技术 赞了文章 · 2020-12-11

面试官本拿求素数搞我,但被我优雅的“回击“了(素数筛)

原创公众号(希望能支持一下):bigsai 转载请联系bigsai
文章收录在github

前言

现在的面试官,是无数开发者的梦魇,能够吊打面试官的属实不多,因为大部分面试官真的有那么那几下子。但在面试中,我们这些小生存者不能全盘否定只能单点突破—从某个问题上让面试官眼前一亮。这不,今天就来分享来了。

这年头,算法岗内卷不说,开发岗也有点内卷,对开发者要求越来越高了,而面试官也是处心积虑的 "刁难" 面试者,凡是都喜欢由浅入深,凡是都喜欢问个:你知道为什么?你知道原理吗?之类。并且,以前只是大厂面试官喜欢问算法,大厂员工底子好,很多甚至有ACM经验或者系统刷题经验,这很容易理解,但现在一些小公司面试官也是张口闭口 xx算法、xx数据结构你说说看,这不,真的被问到了。

求一个质数

在这么一次的过程,面试官问我算法题我不吃惊,我实现早把十大排序原理、复杂度分析、代码手写实现出来了,也把链表、树的各种操作温习的滚瓜烂熟,不过突然就是很诧异的面试官来了一道求素数问题,我把场景还原一下:

面试官:你知道怎么求素数吗?

我:求素数?

面试官:是的,就是求素数。

我:这很简单啊,判断一个数为素数,那么肯定就没有两个数(除了自身和1)相乘等于它,只需要枚举看看有没有能够被它整除的数就可以了,如果有那么就不是素数,如果没有,那么就是素数。

面试官露出一种失望的表情,说我说的对,但没答到点子上,让我具体说一下。

下面开始开始我的表演:

首先,最笨的方法,判断n是否为素数,就是枚举[2,n-1]之间有没有直接能够被n整除的,如果有,那么返回false这个就不是素数,否则就是素数,代码如下:

boolean isprime(int value){
  for(int i=2;i<value;i++)
  {
       if(value%i==0)
       {return false;}
  }
    return true;
}

这种判断一个素数的时间复杂度为O(n).

但是其实这种太浪费时间了,完全没必要这样,可以优化一下 。如果一个数不是质数,那么必定是两个数的乘积,而这两个数通常一个大一个小,并且小的小于等于根号n,大的大于等于根号n,我们只需要枚举小的可能范围,看看是否能够被整除,就可以判断这个数是否为素数啦。例如100=2*50=4*25=5*20=10*10 只需要找2—10这个区间即可。右侧的一定有个对应的不需要管它。

boolean isprime(int value)
{
  for(int i=2;i*i<value+1;i++)
    {
       if(value%i==0)
       {return false;}
    }
    return true;
}

这里之所以要小于value+1,就是要包含根号的情况,例如 3*3=9.要包含3.这种时间复杂度求单个数是O(sqrt(n))。面试官我给你画张图让你看看其中区别:

image-20201208105627132

说到这里面试官露出欣慰的笑容。

面试官:不错不错,基本点掌握了
我:老哥,其实求素数精髓不在这,这个太低效在很多时候,比如求小于n的所有素数,你看看怎么搞?

面试官:用个数组用第二种方法求O(n*sqrt(n))还行啊。

求多个素数

求多个素数的时候(小于n的素数),上面的方法就很繁琐了,因为有大量重复计算,因为 计算某个数的倍数 是否为素数的时候出现大量的重复计算,如果这个数比较大那么对空间浪费比较多。

这样,素数筛的概念就被发明和使用。筛的原理是从前往后进行一种递推、过滤排序以来统计素数。

埃拉托斯特尼(Eratosthenes)筛法

我们看一个数如果不是为素数,那么这个数没有数的乘积能为它,那么这样我们可以根据这个思想进行操作啊:

直接从前往后枚举,这个数位置没被标记的肯定就是素数,如果这个数是素数那么将这个数的倍数标记一下(下次遍历到就不需要在计算)。如果不是素数那么就进行下一步。这样数值越大后面计算次数越少,在进行具体操作时候可借助数组进行判断。所以埃氏筛的核心思想就是将素数的倍数确定为合数

假设刚开始全是素数,2为素数,那么2的倍数均不是素数;然后遍历到3,3的倍数标记一下;下个是5(因为4已经被标记过);一直到n-1为止。具体流程可以看图:

image-20201208112829007

具体代码为:

boolean isprime[];
long prime[];
void getprime()
{
        prime=new long[100001];//记录第几个prime
      int index=0;//标记prime当前下标
        isprime=new boolean [1000001];//判断是否被标记过
        for(int i=2;i<1000001;i++)
        {
            if(!isprime[i])
            {
                prime[index++]=i;
            }
            for(int j=i+i;j<1000000;j=j+i)//他的所有倍数都over
            {
                isprime[j]=true;                    
            }
        }
}

这种筛的算法复杂度为O(nloglogn);别小瞧多的这个logn,数据量大一个log可能少不少个0,那时间也是十倍百倍甚至更多的差距。

欧拉筛

面试官已经开始点头赞同了,哦哦的叫了起来,可其实还没完。还有个线性筛—欧拉筛。观察上述的埃氏筛,有很多重复的计算,尤其是前面的素数,比如2和3的最小公倍数为6,每3次2的计算就也会遇到是3的倍数,而欧拉筛在埃氏筛的基础上改进,有效的避免了这个重复计算。

具体是何种思路呢?就是埃氏筛是遇到一个质数将它的倍数计算到底,而欧拉筛则是只用它乘以已知晓的素数的乘积进行标记,如果素数能够被整除那就停止往后标记。

在实现上同样也是用两个数组,一个存储真实有效的素数,一个用来作为标记使用。

  • 在遍历到一个数的时候,如果这个数没被标记,那么这个数存在素数的数组中,对应下标加1.
  • 不管这个数是不是素数,遍历已知素数将它和该素数的乘积值标记,如果这个素数能够被当前值i整除,那么停止操作进行下一轮。

具体实现的代码为:

boolean isprime[];
int prime[];
void getprimeoula()// 欧拉筛
{
        prime = new int[100001];// 记录第几个prime
        int index = 0;
        isprime = new boolean[1000001];
        for (int i = 2; i < 1000001; i++) {
            if (!isprime[i]) {
                prime[index++] = i;
            }
            for (int j = 0; j < index && i * prime[j] <= 100000; j++){//已知素数范围内枚举
                isprime[i * prime[j]] = true;// 标记乘积
                if (i % prime[j] == 0)
                    break;
            }
        }
}

你可能会问为啥if (i % prime[j] == 0)就要break。

如果i%prime[j]==0,那么就说明i=prime[j]*k. k为一个整数。
那么如果进行下一轮的话
i*prime[j+1]=(prime[j]*k)*prime[j+1]=prime[j]*(k*prime[j+1]) i=k*prime[j+1]两个位置就产生冲突重复计算啦,所以一旦遇到能够被整除的就停止。

image-20201208121324157

你可以看到这个过程,6只标记12而不标记18,18被9*2标记。详细理解还需要多看看代码想想。过程图就不画啦!欧拉的思路就是离我较近的我给它标记。欧拉筛的时间复杂度为O(n),因为每个数只标记一次。

面试官露出一脸欣赏的表情,说了句不错,下面就是聊聊家常,让我等待下一次面试!

image-20201208121913781
原创不易,bigsai我请你帮两件事帮忙一下:

  1. 点赞支持一下, 您的肯定是我在思否创作的源源动力。
  2. 微信搜索「bigsai」,关注我的公众号(新人求支持),不仅免费送你电子书,我还会第一时间在公众号分享知识技术。加我还可拉你进力扣打卡群一起打卡LeetCode。

记得关注、咱们下次再见!

查看原文

赞 14 收藏 7 评论 4

苏三说技术 发布了文章 · 2020-12-06

揭露 | mybatis日志不为人知的秘密

引言

我们在使用mybatis时,如果出现sql问题,一般会把mybatis配置文件中的logging.level参数改成debug,这样就能在日志中看到某个mapper最终执行sql、入参和影响数据行数。我们拿到sql和入参,手动拼接成完整的sql,然后将该sql在数据库中执行一下,就基本能定位到问题原因。

mybatis的日志功能使用起来还是非常方便的,大家有没有想过它是如何设计的呢?

从logging目录开始

我们先看一下mybatis的logging目录,该目录的功能决定了mybatis使用什么日志工具打印日志。

logging目录结构如下:

file

它里面除了jdbc目录,还包含了7个子目录,每一个子目录代表一种日志打印工具,目前支持6种日志打印工具和1种非日志打印工具。我们用一张图来总结一下
file

除了上面的7种日志工具之外,它还抽象出一个Log接口,所有的日志打印工具必须实现该接口,后面可以面向接口编程。

定义了LogException异常,该异常是日志功能的专属异常,如果你有看过mybatis其他源码的话,不难发现,其他功能也定义专属异常,比如:DataSourceException等,这是mybatis的惯用手法,主要是为了将异常细粒度的划分,以便更快定位问题。

此外,它还定义了LogFactory日志工厂,以便于屏蔽日志工具实例的创建细节,让用户使用起来更简单。

如果是你该如何设计这个功能?

我们按照上面目录结构的介绍其实已经有一些思路:

  1. 定义一个Log接口,以便于统一抽象日志功能,这7种日志功能都实现Log接口,并且重写日志打印方法。
  2. 定义一个LogFactory日志工厂,它会根据我们项目中引入的某个日志打印工具jar包,创建一个具体的日志打印工具实例。

看起来,不错。但是,再仔细想想,LogFactory中如何判断项目中引入了某个日志打印工具jar包才创建相应的实例呢?我们第一个想到的可能是用if...else判断不就行了,再想想感觉用if...else不好,7种条件判断太多了,并非优雅的编程。这时候,你会想一些避免太长if...else判断的方法,当然如果你看过我之前写的文章《实战|如何消除又臭又长的if...else判断更优雅的编程?》,可能已经学到了几招,但是mybatis却用了一个新的办法。

mybatis是如何设计这个功能的?

  1. 从Log接口开始

file
它里面抽象了日志打印的5种方法和2种判断方法。

  1. 再分析LogFactory的代码

file
它里面定义了一个静态的构造器logConstructor,没有用if...else判断,在static代码块中调用了6个tryImplementation方法,该方法会启动一个执行任务去调用了useXXXLogging方法,创建日志打印工具实例。

file
当然tryImplementation方法在执行前会判断构造器logConstructor为空才允许执行任务中的run方法。下一步看看useXXXLogging方法:
file
看到这里,聪明的你可能会有这样的疑问,从上图可以看出mybatis定义了8种useXXXLogging方法,但是在前面的static静态代码块中却只调用了6种,这是为什么?

对比后发现:useCustomLogging 和 useStdOutLogging 前面是没调用的。useStdOutLogging它里面使用了StdOutImpl类
file
该类其实就是通过JDK自带的System类的方法打印日志的,无需引入额外的jar包,所以不参与static代码块中的判断。

而useCustomLogging方法需要传入一个实现了Log接口的类,如果mybatis默认提供的6种日志打印工具不满足要求,以便于用户自己扩展。

而这个方法是在Configuration类中调用的,如果用户有自定义logImpl参数的话。

file

file
具体是在XMLConfigBuilder类的settingsElement方法中调用
file
再回到前面LogFactory的setImplementation方法

file
它会先找到实现了Log接口的类的构造器,返回将该构造器赋值给全局的logConstructor。

这样一来,就可以通过getLog方法获取到Log实例。
file

然后在业务代码中通过下面这种方式获取Log对象,调用它的方法打印日志了。
file

梳理一下LogFactory的流程:

  • 在static代码块中根据逐个引入日志打印工具jar包中的日志类,先判断如果全局变量logConstructor为空,则加载并获取相应的构造器,如果可以获取到则赋值给全局变量logConstructor。
  • 如果全局变量logConstructor不为空,则不继续获取构造器。
  • 根据getLog方法获取Log实例
  • 通过Log实例的具体日志方法打印日志

在这里还分享一个知识点,如果某个工具类里面都是静态方法,那么要把该工具类的构造方法定义成private的,防止被疑问调用,LogFactory就是这么做的。
file

适配器模式

日志模块除了使用工厂模式之外,还是有了适配器模式。

适配器模式会将所需要适配的类转换成调用者能够使用的目标接口

涉及以下几个角色:

  • 目标接口( Target )
  • 需要适配的类( Adaptee )
  • 适配器( Adapter)

file
mybatis是怎么用适配器模式的?
file
上图中标红的类对应的是Adapter角色,Log是Target角色。
file
而LogFactory就是Adaptee,它里面的getLog方法里面包含是需要适配的对象。

sql执行日志打印原理

从上面已经能够确定使用哪种日志打印工具,但在sql执行的过程中是如何打印日志的呢?这就需要进一步分析logging目录下的jdbc目录了。

file
看看这几个类的关系图:
file
ConnectionLogger、PreparedStatementLogger、ResultSetLogger和StatementLogger都继承了BaseJdbcLogger类,并且实现了InvocationHandler接口。从类名非常直观的看出,这4种类对应的数据库jdbc功能。

file
它们实现了InvocationHandler接口意味着它用到了动态代理,真正起作用的是invoke方法,我们以ConnectionLogger为例:
file

如果调用了prepareStatement方法,则会打印debug日志。
file

上图中传入的original参数里面包含了\n\t等分隔符,需要将分隔符替换成空格,拼接成一行sql。

最终会在日志中打印sql、入参和影响行数:
file
上图中的sql语句是在ConnectionLogger类中打印的

那么入参和影响行数呢?

入参在PreparedStatementLogger类中打印的
file
影响行数在ResultSetLogger类中打印的
file

大家需要注意的一个地方是:sql、入参和影响行数只打印了debug级别的日志,其他级别并没打印。所以需要在mybatis配置文件中的logging.level参数配置成debug,才能打印日志。

彩蛋

不知道大家有没有发现这样一个问题:

在LogFactory的代码中定义了很多匿名的任务执行器

file
但是在实际调用时,却没有在线程中执行,而是直接调用的,这是为什么?

答案是为了保证顺序执行,如果所有的日志工具jar包都有,加载优先级是:slf4j 》commonsLog 》log4j2 》log4j 》jdkLog 》NoLog

还有个问题,顺序执行就可以了,为什么要把匿名内部类定义成Runnable的呢?

这里非常有迷惑性,因为它没创建Thread类,并不会多线程执行。我个人认为,这里是mybatis的开发者的一种偷懒,不然需要定义一个新类代替这种执行任务的含义,还不如就用已有的。

最后说一句(求关注,别白嫖我)

求关注,如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,坚持原创不易,您的支持是我坚持下来最大的动力。
file

求一键三连:点赞、转发、在看。在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多BAT大厂的前辈交流和学习。

本文由博客群发一文多发等运营工具平台 OpenWrite 发布
查看原文

赞 1 收藏 0 评论 1

苏三说技术 发布了文章 · 2020-12-05

拜托,别再让我优化大事务了,我的头都要裂开了

前言

最近有个网友问了我一个问题:系统中大事务问题要如何处理?

正好前段时间我在公司处理过这个问题,我们当时由于项目初期时间比较紧张,为了快速完成业务功能,忽略了系统部分性能问题。项目顺利上线后,专门抽了一个迭代的时间去解决大事务问题,目前已经优化完成,并且顺利上线。现给大家总结了一下,我们当时使用的一些解决办法,以便大家被相同问题困扰时,可以参考一下。

大事务引发的问题

在分享解决办法之前,先看看系统中如果出现大事务可能会引发哪些问题
file

从上图可以看出如果系统中出现大事务时,问题还不小,所以我们在实际项目开发中应该尽量避免大事务的情况。如果我们已有系统中存在大事务问题,该如何解决呢?

解决办法

少用@Transactional注解

大家在实际项目开发中,我们在业务方法加上@Transactional注解开启事务功能,这是非常普遍的做法,它被称为声明式事务。
部分代码如下:

@Transactional(rollbackFor=Exception.class)
   public void save(User user) {
         doSameThing...
   }

然而,我要说的第一条是:少用@Transactional注解。
为什么?

我们知道@Transactional注解是通过spring的aop起作用的,但是如果使用不当,事务功能可能会失效。如果恰巧你经验不足,这种问题不太好排查。至于事务哪些情况下会失效,可以参考我之前写的《spring事务的这10种坑,你稍不注意可能就会踩中!!!》这篇文章。
@Transactional注解一般加在某个业务方法上,会导致整个业务方法都在同一个事务中,粒度太粗,不好控制事务范围,是出现大事务问题的最常见的原因。

那我们该怎么办呢?

可以使用编程式事务,在spring项目中使用TransactionTemplate类的对象,手动执行事务。
部分代码如下:

   @Autowired
   private TransactionTemplate transactionTemplate;
   
   ...
   
   public void save(final User user) {
         transactionTemplate.execute((status) => {
            doSameThing...
            return Boolean.TRUE;
         })
   }

从上面的代码中可以看出,使用TransactionTemplate的编程式事务功能自己灵活控制事务的范围,是避免大事务问题的首选办法。

当然,我说少使用@Transactional注解开启事务,并不是说一定不能用它,如果项目中有些业务逻辑比较简单,而且不经常变动,使用@Transactional注解开启事务开启事务也无妨,因为它更简单,开发效率更高,但是千万要小心事务失效的问题。

将查询(select)方法放到事务外

如果出现大事务,可以将查询(select)方法放到事务外,也是比较常用的做法,因为一般情况下这类方法是不需要事务的。

比如出现如下代码:

@Transactional(rollbackFor=Exception.class)
   public void save(User user) {
         queryData1();
         queryData2();
         addData1();
         updateData2();
   }

可以将queryData1和queryData2两个查询方法放在事务外执行,将真正需要事务执行的代码才放到事务中,比如:addData1和updateData2方法,这样就能有效的减少事务的粒度。
如果使用TransactionTemplate的编程式事务这里就非常好修改。

   @Autowired
   private TransactionTemplate transactionTemplate;
   
   ...
   
   public void save(final User user) {
         queryData1();
         queryData2();
         transactionTemplate.execute((status) => {
            addData1();
            updateData2();
            return Boolean.TRUE;
         })
   }

但是如果你实在还是想用@Transactional注解,该怎么拆分呢?

public void save(User user) {
         queryData1();
         queryData2();
         doSave();
    }
   
    @Transactional(rollbackFor=Exception.class)
    public void doSave(User user) {
       addData1();
       updateData2();
    }

这个例子是非常经典的错误,这种直接方法调用的做法事务不会生效,给正在坑中的朋友提个醒。因为@Transactional注解的声明式事务是通过spring aop起作用的,而spring aop需要生成代理对象,直接方法调用使用的还是原始对象,所以事务不会生效。

有没有办法解决这个问题呢?

1.新加一个Service方法

这个方法非常简单,只需要新加一个Service方法,把@Transactional注解加到新Service方法上,把需要事务执行的代码移到新方法中。具体代码如下:

@Servcie
  publicclass ServiceA {
     @Autowired
     prvate ServiceB serviceB;
  
     public void save(User user) {
           queryData1();
           queryData2();
           serviceB.doSave(user);
     }
   }
   
   @Servcie
   publicclass ServiceB {
   
      @Transactional(rollbackFor=Exception.class)
      public void doSave(User user) {
         addData1();
         updateData2();
      }
   
   }

2.在该Service类中注入自己

如果不想再新加一个Service类,在该Service类中注入自己也是一种选择。具体代码如下:

@Servcie
  publicclass ServiceA {
     @Autowired
     prvate ServiceA serviceA;
  
     public void save(User user) {
           queryData1();
           queryData2();
           serviceA.doSave(user);
     }
     
     @Transactional(rollbackFor=Exception.class)
     public void doSave(User user) {
         addData1();
         updateData2();
      }
   }

可能有些人可能会有这样的疑问:这种做法会不会出现循环依赖问题?

其实spring ioc内部的三级缓存保证了它,不会出现循环依赖问题。如果你想进一步了解循环依赖问题,可以看看我之前文章《spring解决循环依赖为什么要用三级缓存?》。

3.在该Service类中使用AopContext.currentProxy()获取代理对象

上面的方法2确实可以解决问题,但是代码看起来并不直观,还可以通过在该Service类中使用AOPProxy获取代理对象,实现相同的功能。具体代码如下:

@Servcie
  publicclass ServiceA {
  
     public void save(User user) {
           queryData1();
           queryData2();
           ((ServiceA)AopContext.currentProxy()).doSave(user);
     }
     
     @Transactional(rollbackFor=Exception.class)
     public void doSave(User user) {
         addData1();
         updateData2();
      }
   }

事务中避免远程调用

我们在接口中调用其他系统的接口是不能避免的,由于网络不稳定,这种远程调的响应时间可能比较长,如果远程调用的代码放在某个事物中,这个事物就可能是大事务。当然,远程调用不仅仅是指调用接口,还有包括:发MQ消息,或者连接redis、mongodb保存数据等。

@Transactional(rollbackFor=Exception.class)
   public void save(User user) {
         callRemoteApi();
         addData1();
   }

远程调用的代码可能耗时较长,切记一定要放在事务之外。

   @Autowired
   private TransactionTemplate transactionTemplate;
   
   ...
   
   public void save(final User user) {
         callRemoteApi();
         transactionTemplate.execute((status) => {
            addData1();
            return Boolean.TRUE;
         })
   }

有些朋友可能会问,远程调用的代码不放在事务中如何保证数据一致性呢?这就需要建立:重试+补偿机制,达到数据最终一致性了。

事务中避免一次性处理太多数据

如果一个事务中需要处理的数据太多,也会造成大事务问题。比如为了操作方便,你可能会一次批量更新1000条数据,这样会导致大量数据锁等待,特别在高并发的系统中问题尤为明显。

解决办法是分页处理,1000条数据,分50页,一次只处理20条数据,这样可以大大减少大事务的出现。

非事务执行

在使用事务之前,我们都应该思考一下,是不是所有的数据库操作都需要在事务中执行?

   @Autowired
   private TransactionTemplate transactionTemplate;
   
   ...
   
   public void save(final User user) {
         transactionTemplate.execute((status) => {
            addData();
            addLog();
            updateCount();
            return Boolean.TRUE;
         })
   }

上面的例子中,其实addLog增加操作日志方法 和 updateCount更新统计数量方法,是可以不在事务中执行的,因为操作日志和统计数量这种业务允许少量数据不一致的情况。

   @Autowired
   private TransactionTemplate transactionTemplate;
   
   ...
   
   public void save(final User user) {
         transactionTemplate.execute((status) => {
            addData();           
            return Boolean.TRUE;
         })
         addLog();
         updateCount();
   }

当然大事务中要鉴别出哪些方法可以非事务执行,其实没那么容易,需要对整个业务梳理一遍,才能找出最合理的答案。

异步处理

还有一点也非常重要,是不是事务中的所有方法都需要同步执行?我们都知道,方法同步执行需要等待方法返回,如果一个事务中同步执行的方法太多了,势必会造成等待时间过长,出现大事务问题。

看看下面这个列子:

   @Autowired
   private TransactionTemplate transactionTemplate;
   
   ...
   
   public void save(final User user) {
         transactionTemplate.execute((status) => {
            order();
            delivery();
            return Boolean.TRUE;
         })
   }

order方法用于下单,delivery方法用于发货,是不是下单后就一定要马上发货呢?

答案是否定的。

这里发货功能其实可以走mq异步处理逻辑。

   @Autowired
   private TransactionTemplate transactionTemplate;
   
   ...
   
   public void save(final User user) {
         transactionTemplate.execute((status) => {
            order();
            return Boolean.TRUE;
         })
         sendMq();
   }

总结

本人从网友的一个问题出发,结合自己实际的工作经验分享了处理大事务的6种办法:

  • 少用@Transactional注解
  • 将查询(select)方法放到事务外
  • 事务中避免远程调用
  • 事务中避免一次性处理太多数据
  • 非事务执行
  • 异步处理

最后说一句(求关注,不要白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,或者点赞、转发、在看。在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多大厂的前辈交流和学习。

file

本文由博客群发一文多发等运营工具平台 OpenWrite 发布
查看原文

赞 1 收藏 0 评论 1

苏三说技术 发布了文章 · 2020-12-04

求你别再用swagger了,给你推荐几个在线文档生成神器

前言

最近公司打算做一个openapi开放平台,让我找一款好用的在线文档生成工具,具体要求如下:

  1. 必须是开源的
  2. 能够实时生成在线文档
  3. 支持全文搜索
  4. 支持在线调试功能
  5. 界面优美

说实话,这个需求看起来简单,但是实际上一点的都不简单。

我花了几天时间到处百度,谷歌,技术博客 和 论坛查资料,先后调研了如下文档生成工具:

gitbook

github地址:https://github.com/GitbookIO/gitbook

开源协议:Apache-2.0 License

Star: 22.9k

开发语言:javascript

用户:50万+

推荐指数:★★★

示例地址:https://www.servicemesher.com/envoy/intro/arch_overview/dynamic_configuration.html

image.png

gitBook是一款文档编辑工具。它的功能类似金山WPS中的word或者微软office中的word的文档编辑工具。它可以用来写文档、建表格、插图片、生成pdf。当然,以上的功能WPS、office可能做得更好,但是,gitBook还有更最强大的功能:它可以用文档建立一个网站,让更多人了解你写的书,另外,最最核心的是,他支持Git,也就意味着,它是一个分布式的文档编辑工具。你可以随时随地来编写你的文档,也可以多人共同编写文档,哪怕多人编写同一页文档,它也能记录每个人的内容,然后告诉你他们之间的区别,也能记录你的每一次改动,你可以查看每一次的书写记录和变化,哪怕你将文档都删除了,它也能找回来!这就是它继承git后的厉害之处!

优点:使用起来非常简单,支持全文搜索,可以跟git完美集成,对代码无任何嵌入性,支持markdown格式的文档编写。

缺点:需要单独维护一个文档项目,如果接口修改了,需要手动去修改这个文档项目,不然可能会出现接口和文档不一致的情况。并且,不支持在线调试功能。

个人建议:如果对外的接口比较少,或者编写之后不会经常变动可以用这个。

smartdoc

gitee地址: https://gitee.com/smart-doc-team/smart-doc

开源协议:Apache-2.0 License

Star: 758

开发语言:html、javascript

用户:小米、科大讯飞、1加

推荐指数:★★★★

示例地址:https://gitee.com/smart-doc-team/smart-doc/wikis/文档效果图?sort_id=1652819

image.png

smart-doc是一个java restful api文档生成工具,smart-doc颠覆了传统类似swagger这种大量采用注解侵入来生成文档的实现方法。smart-doc完全基于接口源码分析来生成接口文档,完全做到零注解侵入,只需要按照java标准注释的写就能得到一个标准的markdown接口文档。

优点:基于接口源码分析生成接口文档,零注解侵入,支持html、pdf、markdown格式的文件导出。

缺点:需要引入额外的jar包,不支持在线调试

个人建议:如果实时生成文档,但是又不想打一些额外的注解,比如:使用swagger时需要打上@Api、@ApiModel等注解,就可以使用这个。

redoc

github地址:https://github.com/Redocly/redoc

开源协议:MIT License

Star: 10.7K

开发语言:typescript、javascript

用户:docker、redocly

推荐指数:★★★☆

示例地址:https://docs.docker.com/engine/api/v1.40/

image.png

redoc自己号称是一个最好的在线文档工具。它支持swagger接口数据,提供了多种生成文档的方式,非常容易部署。使用redoc-cli能够将您的文档捆绑到零依赖的 HTML文件中,响应式三面板设计,具有菜单/滚动同步。

优点:非常方便生成文档,三面板设计

缺点:不支持中文搜索,分为:普通版本 和 付费版本,普通版本不支持在线调试。另外UI交互个人感觉不适合国内大多数程序员的操作习惯。

个人建议:如果想快速搭建一个基于swagger的文档,并且不要求在线调试功能,可以使用这个。

knife4j

gitee地址:https://gitee.com/xiaoym/knife4j

开源协议:Apache-2.0 License

Star: 3k

开发语言:java、javascript

用户:未知

推荐指数:★★★★

示例地址:http://swagger-bootstrap-ui.xiaominfo.com/doc.html

image.png

knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案,前身是swagger-bootstrap-ui,取名kni4j是希望她能像一把匕首一样小巧,轻量,并且功能强悍。

优点:基于swagger生成实时在线文档,支持在线调试,全局参数、国际化、访问权限控制等,功能非常强大。

缺点:界面有一点点丑,需要依赖额外的jar包

个人建议:如果公司对ui要求不太高,可以使用这个文档生成工具,比较功能还是比较强大的。

yapi

github地址:https://github.com/YMFE/yapi

开源协议:Apache-2.0 License

Star: 17.8k

开发语言:javascript

用户:腾讯、阿里、百度、京东等大厂

推荐指数:★★★★★

示例地址:https://www.taojibao.cn/

image.png

yapi是去哪儿前端团队自主研发并开源的,主要支持以下功能:

  • 可视化接口管理
  • 数据mock
  • 自动化接口测试
  • 数据导入(包括swagger、har、postman、json、命令行)
  • 权限管理
  • 支持本地化部署
  • 支持插件
  • 支持二次开发

优点:功能非常强大,支持权限管理、在线调试、接口自动化测试、插件开发等,BAT等大厂等在使用,说明功能很好。

缺点:在线调试功能需要安装插件,用户体检稍微有点不好,主要是为了解决跨域问题,可能有安全性问题。不过要解决这个问题,可以自己实现一个插件,应该不难。

个人建议:如果不考虑插件安全的安全性问题,这个在线文档工具还是非常好用的,可以说是一个神器,笔者在这里强烈推荐一下。

apidoc

github地址:https://github.com/apidoc/apidoc

开源协议:MIT License

Star: 8.7k

开发语言:javascript

用户:未知

推荐指数:★★★★☆

示例地址:https://apidocjs.com/example/#api-User
image.png

apidoc 是一个简单的 RESTful API 文档生成工具,它从代码注释中提取特定格式的内容生成文档。支持诸如 Go、Java、C++、Rust 等大部分开发语言,具体可使用 apidoc lang 命令行查看所有的支持列表。

apidoc 拥有以下特点:

  1. 跨平台,linux、windows、macOS 等都支持;
  2. 支持语言广泛,即使是不支持,也很方便扩展;
  3. 支持多个不同语言的多个项目生成一份文档;
  4. 输出模板可自定义;
  5. 根据文档生成 mock 数据;

优点:基于代码注释生成在线文档,对代码的嵌入性比较小,支持多种语言,跨平台,也可自定义模板。支持搜索和在线调试功能。

缺点:需要在注释中增加指定注解,如果代码参数或类型有修改,需要同步修改注解相关内容,有一定的维护工作量。

个人建议:这种在线文档生成工具提供了另外一种思路,swagger是在代码中加注解,而apidoc是在注解中加数据,代码嵌入性更小,推荐使用。

showdoc

github地址:https://github.com/star7th/showdoc

开源协议:Apache Licence

Star: 8.1k

开发语言:javascript、php

用户:超过10000+互联网团队正在使用

推荐指数:★★★★☆

示例地址:https://www.showdoc.com.cn/demo?page_id=9

ShowDoc就是一个非常适合IT团队的在线文档分享工具,它可以加快团队之间沟通的效率。

它都有些什么功能:

  1. 响应式网页设计,可将项目文档分享到电脑或移动设备查看。同时也可以将项目导出成word文件,以便离线浏览。
  2. 权限管理,ShowDoc上的项目有公开项目和私密项目两种。公开项目可供任何登录与非登录的用户访问,而私密项目则需要输入密码验证访问。密码由项目创建者设置。
  3. ShowDoc采用markdown编辑器,点击编辑器上方的按钮可方便地插入API接口模板和数据字典模板。
  4. ShowDoc为页面提供历史版本功能,你可以方便地把页面恢复到之前的版本。
  5. 支持文件导入,文件可以是postman的json文件、swagger的json文件、showdoc的markdown压缩包,系统会自动识别文件类型。

优点:支持项目权限管理,多种格式文件导入,全文搜索等功能,使用起来还是非常方便的。并且既支持部署自己的服务器,也支持在线托管两种方式。

缺点:不支持在线调试功能

个人建议:如果不要求在线调试功能,这个在线文档工具值得使用。

最后说一句(求关注,不要白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,或者点赞、转发、在看。在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多大厂的前辈交流和学习。

本文由博客群发一文多发等运营工具平台 OpenWrite 发布
查看原文

赞 3 收藏 1 评论 0

苏三说技术 发布了文章 · 2020-11-25

zuul如果两个filter的order一样,是如何排序的?

最近有个网友问了一个问题,zuul中如果两个filter的order一样,是如何排序的?引起了我的兴趣,特地去阅读了它的源码。

如果你有使用过springcloud应该听说过zuul,它的定位是分布式微服务中的API网关服务,当然后面可能要被gateway替代了。zuul是一个L7应用程序网关,提供了动态路由,监视,弹性,安全性等功能。zuul的大部分功能是通过filter实现的。

image.png

zuul定义了四种不同生命周期的filter

image.png

为了方便操作,zuul内置了一些filter,这些filter主要通过@EnableZuulServer@EnableZuulProxy注解开启相关功能。@EnableZuulServer注解开启的filter功能如下:

image.png

@EnableZuulProxy注解除了开启上面这些filter功能之外,还开启了如下的功能:

image.png

只需继承ZuulFilter类,实现它的filterTypefilterOrdershouldFilterrun方法即可,具体实现可参考如下代码:

public class LogFilter extends ZuulFilter {

    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 1;
    }

    @Override
    public boolean shouldFilter() {
        return RequestContext.getCurrentContext().sendZuulResponse();
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext currentContext = RequestContext.getCurrentContext();
        HttpServletRequest request = currentContext.getRequest();
        log.info("zuul pre filter-->" + request.getRequestURL() + "-->" + request.getMethod());
        return null;
    }
}

上面的四个方法有哪些作用呢?

image.png

需要注意的是,要想使zuul的功能生效,切记要在springboot启动类上定义@EnableZuulServer@EnableZuulProxy注解,表示开启zuul的功能。

先看看所有的zuulFilter在哪里执行的,谜底就在FilterProcessor类的runFilters方法中。

image.png

该方法很简单,先获取所有zuulFilter,然后遍历所有zuulFilter,调用processZuulFilter方法执行具体的zuulFilter,然后将执行结果返回。

我们重点看看这个方法

FilterLoader.getInstance().getFiltersByType(sType);

该方法的具体逻辑
image.png

  1. 根据filterType从缓存中获取filter集合,如果缓存中有直接返回
  2. 如果缓存中没有,则创建filter集合,将所有filter中跟filterType的filter添加到filter集合中。
  3. 排序filter集合
  4. 将新创建的filter集合放入缓存。

从上面可以看出filter的排序是通过如下方法执行的:

Collections.sort(list);

该方法底层其实是通过listsort方法实现的image.png

看看ArrayListsort方法,传入的Comparator为null

image.png
它的底层又是通过Arrays类的静态方法sort实现的

image.png
由于上一步Comparator为null,则会执行sort方法。

image.png
该方法是通过ComparableTimSort类的sort方法实现的,这个方法是最核心的方法了
image.png

我们可以看到该方法其实是通过binarySort二分查找排序的。

image.png
通过compareTo方法比较大小。

我们回头再看看ZuulFilter

image.png
它实现了Comparable接口,重写了compareTo方法

image.png

所以,看到这里我们可以得出结论:ZuulFilter是通过Integercompare方法比较filterOrder参数值大小来排序的。

我们看看Integercompare方法具体的逻辑!
image.png

如果x==y,则返回0,x<y,则返回 -1,否则返回1 前面在二分查找中,只有x<y时,才会交换位置。
image.png

看到这里,我们得出这样的结论,如果filterOrder一样,则Collections.sort(list);排序时不交换位置,这按照ZuulFilter默认加载顺序。那么,ZuulFilter的默认加载顺序是怎么样的?
image.png

它是通过getAllFilters方法获取ZuulFilter集合,该方法其实返回的是名称为filtersConcurrentHashMapvalues,即返回Set集合,是无序的。

  • 重要的事情说三遍:如果filterOrder一样,ZuulFilter是无序的。
  • 重要的事情说三遍:如果filterOrder一样,ZuulFilter是无序的。
  • 重要的事情说三遍:如果filterOrder一样,ZuulFilter是无序的。

所以,filterOrder切记不要定义相同的,不然可能会出现无法预知的执行结果。

自定义排序其实有两种方法

  • 实现Comparable接口,重写compareTo方法,
  • 实现Comparator接口,重写compare方法

    如果要使用Collections.sort(list);排序,它默认用的是第一种方法,上面的filterOrder之所以可以排序,是因为Integer实现了Comparable接口,重写了compareTo方法

image.png

如果想自己定义排序规则可以通过实现Comparator接口,重写compare方法。

Collections.sort(list,new Comparator<Integer>(){
    @Override
    public int compare(Integer o1, Integer o2) {
        return o2 - o1;
    }
});

它的底层也是通过二分查找实现的
image.png

那么这两种方法有什么区别呢?

  • Comparable接口位于java.lang包下,而Comparator接口位于java.util包下。
  • Comparable接口是内部比较器,一个类如果想要使用Collections.sort(list) 方法进行排序,则需要实现该接口
  • Comparator接口是外部比较器用于对那些没有实现Comparable接口或者对已经实现的Comparable中的排序规则不满意进行排序.无需改变类的结构,更加灵活。

zuul中是通过filterOrder参数的大小排序的,而在spring中是通过@Order注解排序的。

image.png
默认情况下,如果不指定value值,则value是Integer的最大值。由于排序规则是value越小,则排在越靠前,所以如果不指定value值,则它排在最后。
image.png
spring是通过OrderComparator类排序的,它实现了Comparator接口,它的doCompare方法实现的排序。

image.png
最终也是调用Integer类的compare方法,该方法前面已经介绍过了。

如果这篇文章对您有所帮助,或者有所启发的话,帮忙扫描下发二维码关注一下,或者点赞、转发、在看。在公众号中回复:面试、代码神器、开发手册、时间管理有超赞的粉丝福利,另外回复:加群,可以跟很多大厂的前辈交流和学习。
image.png

查看原文

赞 1 收藏 0 评论 0

苏三说技术 发布了文章 · 2020-11-03

线程池最佳线程数量到底要如何配置?

一、前言

对应从事后端开发的同学来说,线程是必须要使用了,因为使用它可以提升系统的性能。但是,创建线程和销毁线程都是比较耗时的操作,频繁的创建和销毁线程会浪费很多CPU的资源。此外,如果每个任务都创建一个线程去处理,这样线程会越来越多。我们知道每个线程默认情况下占1M的内存空间,如果线程非常多,内存资源将会被耗尽。这时,我们需要线程池去管理线程,不会出现内存资源被耗尽的情况,也不会出现频繁创建和销毁线程的情况,因为它内部是可以复用线程的。

二、从实战开始

在介绍线程池之前,让我们先看个例子。

这个类的功能就是使用Executors类的newSingleThreadExecutor方法创建了的一个单线程池,他里面会执行Callable线程任务。

三、创建线程池的方法

我们仔细看看Executors类,会发现它里面给我们封装了不少创建线程池的静态方法,如下图所示:

其实,我们总结一下其实只有6种:

1.newCachedThreadPool可缓冲线程池

      它的核心线程数是0,最大线程数是integer的最大值,每隔60秒回收一次空闲线程,使用SynchronousQueue队列。SynchronousQueue队列比较特殊,内部只包含一个元素,插入元素到队列的线程被阻塞,直到另一个线程从队列中获取了队列中存储的元素。同样,如果线程尝试获取元素并且当前不存在任何元素,则该线程将被阻塞,直到线程将元素插入队列。

2.newFixedThreadPool固定大小线程池

      它的核心线程数 和 最大线程数是一样,都是nThreads变量的值,该变量由用户自己决定,所以说是固定大小线程池。此外,它的每隔0毫秒回收一次线程,换句话说就是不回收线程,因为它的核心线程数 和 最大线程数是一样,回收了没有任何意义。此外,使用了LinkedBlockingQueue队列,该队列其实是有界队列,很多人误解了,只是它的初始大小比较大是integer的最大值。

3.newScheduledThreadPool定时任务线程池

      它的核心线程数是corePoolSize变量,需要用户自己决定,最大线程数是integer的最大值,同样,它的每隔0毫秒回收一次线程,换句话说就是不回收线程。使用了DelayedWorkQueue队列,该队列具有延时的功能。

4.newSingleThreadExecutor单个线程池

       其实,跟上面的newFixedThreadPool是一样的,稍微有一点区别是核心线程数 和 最大线程数 都是1,这就是为什么说它是单线程池的原因。

5.newSingleThreadScheduledExecutor单线程定时任务线程池

      该线程池是对上面介绍过的ScheduledThreadPoolExecutor定时任务线程池的简单封装,核心线程数固定是1,其他的功能一模一样。

6.newWorkStealingPool窃取线程池

        它是JDK1.8增加的新线程池,跟其他的实现方式都不一样,它底层是通过ForkJoinPool类来实现的。会创建一个含有足够多线程的线程池,来维持相应的并行级别,它会通过工作窃取的方式,使得多核的 CPU 不会闲置,总会有活着的线程让 CPU 去运行。

讲了这么多,具体要怎么用呢?

     其实newFixedThreadPool、newCachedThreadPool、newSingleThreadExecutor 和 newWorkStealingPool方法创建和使用线程池的方法是一样的。这四个方法创建线程池返回值是ExecutorService,通过它的execute方法执行线程。

       newScheduledThreadPool 和 newSingleThreadScheduledExecutor 方法创建和使用线程池的方法也是一样的

      以上两个方法创建的线程池返回值是ScheduledExecutorService,通过它的schedule提交线程,并且可以配置延迟执行的时间。

四、自定义线程池

       Executors类有这么多方法可以创建线程池,但是阿里巴巴开发规范中却明确规定不要使用Executors类创建线程池,这是为什么呢?

      newCachedThreadPool可缓冲线程池,它的最大线程数是integer的最大值,意味着使用它创建的线程池,可以创建非常多的线程,我们都知道一个线程默认情况下占用内存1M,如果创建的线程太多,占用内存太大,最后肯定会出现内存溢出的问题。

        newFixedThreadPool和newSingleThreadExecutor在这里都称为固定大小线程池,它的队列使用的LinkedBlockingQueue,我们都知道这个队列默认大小是integer的最大值,意味着可以往该队列中加非常多的任务,每个任务也是要内存空间的,如果任务太多,最后肯定也会出现内存溢出的问题。

        阿里建议使用ThreadPoolExecutor类创建线程池,其实从刚刚看到的Executors类创建线程池的newFixedThreadPool等方法可以看出,它也是使用ThreadPoolExecutor类创建线程池的。

       从上图可以看出ThreadPoolExecutor类的构造方法有4个,里面包含了很多参数,让我们先一起认识一下:

我们根据上面的内容自定义一个线程池:

       从上面可以看到,我们使用ThreadPoolExecutor类自定义了一个线程池,它的核心线程数是8,最大线程数是 10,空闲线程回收时间是30,单位是秒,存放任务的队列用的ArrayBlockingQueue,而队列满的处理策略用的AbortPolicy。使用这个队列,基本可以保持线程在系统的可控范围之内,不会出现内存溢出的问题。但是也不是绝对的,只是出现内存溢出的概率比较小。

     当然,阿里巴巴开发规范建议不使用Executors类创建线程池,并不表示它完全没用,在一些低并发的业务场景照样可以使用。

五、最佳线程数

在使用线程池时,很多同学都有这样的疑问,不知道如何配置线程数量,今天我们一起探讨一下这个问题。

1.经验值

配置线程数量之前,首先要看任务的类型是 IO密集型,还是CPU密集型?

什么是IO密集型?

比如:频繁读取磁盘上的数据,或者需要通过网络远程调用接口。

什么是CPU密集型?

比如:非常复杂的调用,循环次数很多,或者递归调用层次很深等。

IO密集型配置线程数经验值是:2N,其中N代表CPU核数。

CPU密集型配置线程数经验值是:N + 1,其中N代表CPU核数。

如果获取N的值?

      那么问题来了,混合型(既包含IO密集型,又包含CPU密集型)的如何配置线程数?

混合型如果IO密集型,和CPU密集型的执行时间相差不太大,可以拆分开,以便于更好配置。如果执行时间相差太大,优化的意义不大,比如IO密集型耗时60s,CPU密集型耗时1s。

2.最佳线程数目算法

除了上面介绍是经验值之外,其实还提供了计算公式:

        很显然线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。

        虽说最佳线程数目算法更准确,但是线程等待时间和线程CPU时间不好测量,实际情况使用得比较少,一般用经验值就差不多了。再配合系统压测,基本可以确定最适合的线程数。

查看原文

赞 1 收藏 0 评论 0

苏三说技术 赞了文章 · 2020-10-17

spring-boot-route(十七)使用aop记录操作日志

在上一章内容中——使用logback管理日志,我们详细讲述了如何将日志生成文件进行存储。但是在实际开发中,使用文件存储日志用来快速查询问题并不是最方便的,一个优秀系统除了日志文件还需要将操作日志进行持久化,来监控平台的操作记录。今天我们一起来学习一下如何通过apo来记录日志。

为了让记录日志更加灵活,我们将使用自定义的注解来实现重要操作的日志记录功能。

一 日志记录表

日志记录表主要包含几个字段,业务模块,操作类型,接口地址,处理状态,错误信息以及操作时间。数据库设计如下:

CREATE TABLE `sys_oper_log` (
   `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日志主键',
   `title` varchar(50) CHARACTER SET utf8 DEFAULT '' COMMENT '模块标题',
   `business_type` int(2) DEFAULT '0' COMMENT '业务类型(0其它 1新增 2修改 3删除)',
   `method` varchar(255) CHARACTER SET utf8 DEFAULT '' COMMENT '方法名称',
   `status` int(1) DEFAULT '0' COMMENT '操作状态(0正常 1异常)',
   `error_msg` varchar(2000) CHARACTER SET utf8 DEFAULT '' COMMENT '错误消息',
   `oper_time` datetime DEFAULT NULL COMMENT '操作时间',
   PRIMARY KEY (`id`)
 ) ENGINE=InnoDB CHARSET=utf8mb4 CHECKSUM=1 COMMENT='操作日志记录'

对应的实体类如下:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class SysOperLog implements Serializable {
    private static final long serialVersionUID = 1L;

    /** 日志主键 */
    private Long id;

    /** 操作模块 */
    private String title;

    /** 业务类型(0其它 1新增 2修改 3删除) */
    private Integer businessType;

    /** 请求方法 */
    private String method;

    /** 错误消息 */
    private String errorMsg;

    private Integer status;

    /** 操作时间 */
    private Date operTime;
}

二 自定义注解及处理

自定义注解包含两个属性,一个是业务模块title,另一个是操作类型businessType

@Target({ ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Log {
    /**
     * 模块
     */
    String title() default "";

    /**
     * 功能
     */
    BusinessType businessType() default BusinessType.OTHER;
}

使用aop对自定义的注解进行处理

@Aspect
@Component
@Slf4j
public class LogAspect {

    @Autowired
    private AsyncLogService asyncLogService;

    // 配置织入点
    @Pointcut("@annotation(com.javatrip.aop.annotation.Log)")
    public void logPointCut() {}

    /**
     * 处理完请求后执行
     *
     * @param joinPoint 切点
     */
    @AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
    public void doAfterReturning(JoinPoint joinPoint, Object jsonResult) {
        handleLog(joinPoint, null, jsonResult);
    }

    /**
     * 拦截异常操作
     * 
     * @param joinPoint 切点
     * @param e 异常
     */
    @AfterThrowing(value = "logPointCut()", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
        handleLog(joinPoint, e, null);
    }

    protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult) {
        try {
            // 获得注解
            Log controllerLog = getAnnotationLog(joinPoint);
            if (controllerLog == null) {
                return;
            }

            SysOperLog operLog = new SysOperLog();
            operLog.setStatus(0);
            if (e != null) {
                operLog.setStatus(1);
                operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
            }
            // 设置方法名称
            String className = joinPoint.getTarget().getClass().getName();
            String methodName = joinPoint.getSignature().getName();
            operLog.setMethod(className + "." + methodName + "()");
            // 处理设置注解上的参数
            getControllerMethodDescription(joinPoint, controllerLog, operLog);
            // 保存数据库
            asyncLogService.saveSysLog(operLog);
        } catch (Exception exp) {
            log.error("==前置通知异常==");
            log.error("日志异常信息 {}", exp);
        }
    }

    /**
     * 获取注解中对方法的描述信息 用于Controller层注解
     * 
     * @param log 日志
     * @param operLog 操作日志
     * @throws Exception
     */
    public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog) {
        // 设置action动作
        operLog.setBusinessType(log.businessType().ordinal());
        // 设置标题
        operLog.setTitle(log.title());
    }

    /**
     * 是否存在注解,如果存在就获取
     */
    private Log getAnnotationLog(JoinPoint joinPoint) {
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        if (method != null) {
            return method.getAnnotation(Log.class);
        }
        return null;
    }
}

操作类型的枚举类:

public enum BusinessType {
    /**
     * 其它
     */
    OTHER,

    /**
     * 新增
     */
    INSERT,

    /**
     * 修改
     */
    UPDATE,

    /**
     * 删除
     */
    DELETE,
}

使用异步方法将操作日志存库,为了方便我直接使用jdbcTemplate在service中进行存库操作。

@Service
public class AsyncLogService {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * 保存系统日志记录
     */
    @Async
    public void saveSysLog(SysOperLog log) {

        String sql = "INSERT INTO sys_oper_log(title,business_type,method,STATUS,error_msg,oper_time) VALUES(?,?,?,?,?,?)";
        jdbcTemplate.update(sql,new Object[]{log.getTitle(),log.getBusinessType(),log.getMethod(),log.getStatus(),log.getErrorMsg(),new Date()});
    }
}

三 编写接口测试

将自定义注解写在业务方法上,测试效果

@RestController
@RequestMapping("person")
public class PersonController {
    @GetMapping("/{name}")
    @Log(title = "system",businessType = BusinessType.OTHER)
    public Person getPerson(@PathVariable("name") String name, @RequestParam int age){
        return new Person(name,age);
    }

    @PostMapping("add")
    @Log(title = "system",businessType = BusinessType.INSERT)
    public int addPerson(@RequestBody Person person){
        if(StringUtils.isEmpty(person)){
            return -1;
        }
        return 1;
    }

    @PutMapping("update")
    @Log(title = "system",businessType = BusinessType.UPDATE)
    public int updatePerson(@RequestBody Person person){
        if(StringUtils.isEmpty(person)){
            return -1;
        }
        return 1;
    }

    @DeleteMapping("/{name}")
    @Log(title = "system",businessType = BusinessType.DELETE)
    public int deletePerson(@PathVariable(name = "name") String name){
        if(StringUtils.isEmpty(name)){
            return -1;
        }
        return 1;
    }
}

当然,还可以在数据库中将请求参数和响应结果也进行存储,这样就能看出具体接口的操作记录了。


本文示例代码已上传至github,点个star支持一下!

Spring Boot系列教程目录

spring-boot-route(一)Controller接收参数的几种方式

spring-boot-route(二)读取配置文件的几种方式

spring-boot-route(三)实现多文件上传

spring-boot-route(四)全局异常处理

spring-boot-route(五)整合swagger生成接口文档

spring-boot-route(六)整合JApiDocs生成接口文档

spring-boot-route(七)整合jdbcTemplate操作数据库

spring-boot-route(八)整合mybatis操作数据库

spring-boot-route(九)整合JPA操作数据库

spring-boot-route(十)多数据源切换

spring-boot-route(十一)数据库配置信息加密

spring-boot-route(十二)整合redis做为缓存

spring-boot-route(十三)整合RabbitMQ

spring-boot-route(十四)整合Kafka

spring-boot-route(十五)整合RocketMQ

spring-boot-route(十六)使用logback生产日志文件

spring-boot-route(十七)使用aop记录操作日志

spring-boot-route(十八)spring-boot-adtuator监控应用

spring-boot-route(十九)spring-boot-admin监控服务

spring-boot-route(二十)Spring Task实现简单定时任务

spring-boot-route(二十一)quartz实现动态定时任务

spring-boot-route(二十二)实现邮件发送功能

spring-boot-route(二十三)开发微信公众号

这个系列的文章都是工作中频繁用到的知识,学完这个系列,应付日常开发绰绰有余。如果还想了解其他内容,扫面下方二维码告诉我,我会进一步完善这个系列的文章!

查看原文

赞 2 收藏 1 评论 2

认证与成就

  • 获得 39 次点赞
  • 获得 5 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 4 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2020-09-01
个人主页被 1.5k 人浏览