2022年春季社招面试

前言#

本人一年半工作经验,开始面试郑州的相关公司岗位。下面是记录了些面试的问题。

1、二叉树的高度#

高度可以采用递归,算出左子树和右子树的最大长度,最后+1;

public class TreeNode{
    Date value;
    TreeNode left;
    TreeNode righ;
    //省略全参构造方法
}
public int getDeep(TreeNode root){
    if(root  == null){
		return 0;
    } else {
        int left = getDeep(root.left);
        int right = getDeep(root.right);
        return Math.max(left,right) + 1;
    }
}

通过使用后序遍历的方式计算二叉树的高度。可以先计算左子树的高度h1,后计算右子树的高度h2,树的高度h3。h3 = max (h1, h2) + 1 ;这段话和上面说的一致。

二叉树的高度,也就是从根结点出发一直到叶结点的路径的长度。

2、多线程的使用#

尤其是Java线程池相关内容

线程池的基本作用#

首先使用线程池的优势是用来提高线程的复用性以及固定线程的数据量

能够做到:

​ 1、降低资源消耗,通过重复利用已经创建的线程降低线程的创建和销毁造成的消耗。

​ 2、提高响应速度,当任务到达时,任务不需要等待线程创建可以直接执行。

​ 3、提高线程的可管理性质,如果无限制的创建线程,不仅会消耗系统资源,还会降低系统稳定性,使用线程池统一管理。

创建线程池#

首先阿里巴巴在Java开发手册规定了禁止使用Executors创建线程,那为什么要这样做呢?

原因就是 newFixedThreadPool()newSingleThreadExecutor()两个方法允许请求的最大队列长度是 Integer.MAX_VALUE ,可能会出现任务堆积,出现OOM。newCachedThreadPool()允许创建的线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,导致发生OOM。

它建议使用ThreadPoolExecutor方式去创建线程池,通过上面的分析我们也知道了其实Executors 三种创建线程池的方式最终就是通过ThreadPoolExecutor来创建的,只不过有些参数我们无法控制,如果通过ThreadPoolExecutor的构造器去创建,我们就可以根据实际需求控制线程池需要的任何参数,避免发生OOM异常。

推荐通过使用ThreadPoolExecutor来创建一个线程池

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

创建ThreadPoolExecutor一共有7个参数

  • corePoolSize(核心线程数量)核心线程数,核心线程会一直存活,即使没有任务需要处理。当线程数小于核心线程数时,即使现有的线程空闲,线程池也会优先创建新线程来处理任务,而不是直接交给现有的线程处理。核心线程在allowCoreThreadTimeout被设置为true时会超时退出,默认情况下不会退出。
  • maximumPoolSize(最大线程数量)当线程数大于或等于核心线程,且任务队列已满时,线程池会创建新的线程,直到线程数量达到maxPoolSize。如果线程数已等于maxPoolSize,且任务队列已满,则已超出线程池的处理能力,线程池会拒绝处理任务而抛出异常。
  • keepAliveTime(线程空余时间)当线程空闲时间达到keepAliveTime,该线程会退出,直到线程数量等于corePoolSize。如果allowCoreThreadTimeout设置为true,则所有线程均会退出直到线程数量为0。
  • unit(线程空余时间的单位)
  • workQueue(阻塞队列)
  • threadFactory(线程生成定义)
  • handler(任务拒绝策略)

这几个参数应该很好理解,任务提交的流程步骤,分别对应着几个参数的作用:

  1. 首先会判断运行线程数是否小于corePoolSize,如果小于,则直接创建新的线程执行任务;
  2. 如果大于corePoolSize,判断workQueue阻塞队列是否已满,如果还没满,则将任务放到阻塞队列中;
  3. 如果workQueue阻塞队列已经满了,则判断当前线程数是否大于maximumPoolSize,如果没大于则创建新的线程执行任务;
  4. 如果大于maximumPoolSize,则执行任务拒绝策略(具体就是根据实现的Handler)

这里有个点需要注意下,就是workQueue阻塞队列满了,但当前线程数小于maximumPoolSize,这时候会创建新的线程执行任务。不过一般都会将corePoolSize和maximumPoolSize设置相同数量。

keepAliveTime指的是,当前运行的线程数大于核心线程数了,只要空闲时间达到了该值,就会对该线程进行回收。

创建线程池如何考量指定的线程数量?

主要根据自己的业务来决定,根据业务是CPU密集型,还是IO密集型,假设CPU的核心数是N,那么CPU密集型可以先给到N+1,IO密集型的可以给到2N去试一试,究竟要要开多少线程,需要压测来准确地定下来。

并不是说线程越大越好,如果线程过多,线程会有大量的上下文切换,带来系统的开销,影响性能。

问workQueue(阻塞队列)有几种?

有八种啊!!!,先留个学习地址吧:https://zhuanlan.zhihu.com/p/313675509

https://www.cnblogs.com/WangHaiMing/p/8798709.html

队列 有界性 数据结构
ArrayBlockingQueue bounded(有界)
LinkedBlockingQueue optionally-bounded
PriorityBlockingQueue unbounded
DelayQueue unbounded
SynchronousQueue bounded
LinkedTransferQueue unbounded
LinkedBlockingQueue unbounded
DelayWorkQueue unbounded

img

3、JUC包相关集合的底层实现#

如何实现线程安全?

JUC集合包中list和set实现类包括:

  • CopyOnWriteArrayList
  • CopyOnWriteArraySet
  • ConcurrentSkipListSet

image-20220213234654625

CopyOnWriteArrayList

是通过实现List接口,因此它是一个队列。

包含成员有ReentrantLock,和volatile修饰的Object[]数组。这也是“线程安全”的机制

Map实现包括

image-20220213234739310

ConcurrentHashMap在JDK1.7时候是通过ReentrantLock+Segement(分段锁)+HashEntry来实现的。

ConcurrentHashMap到了JDK1.8的时候,Synchronized+CAS+HashEntry+红黑树。

  • JDK1.8的实现降低锁的粒度,JDK1.7的锁粒度是基于Segement,包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(链表或者红黑树的首节点)
  • JDK1.8的数据结构变得更简单,更接近于HashMap
  • 原来链表查询时间O(n),现在遍历红黑树O(logN)

补充知识点CAS#

CAS全程Compare and Swap ,即比较并交换

CAS有三个操作数:当前值A、内存值V、要修改的新值B

图片

假设当前值A与内存值V相等,那就将内存值V改成值B;

假设当前值A与内存值B不相等,那要么就重试一次,要么就放弃更新。

将当前值与内存值进行对比,判断是否被修改过,这就是CAS的核心。

CAS对比synchronized的优势是,CAS没有加锁,多个线程可以直接操作共享资源,在实际去修改的时候才去判断能否修改成功。很多情况下会比Synchronized锁高效很多。

CAS的缺点是什么?

CAS会带来ABA的问题,从CAS更新的时候,我们可以发现它只对比当前值和内存值是否相等,这会带来问题是,假设线程A读到当前值是10,可能线程B把值修改为100,然后线程C又把值修改为10;等到线程A拿到执行权时,由于当前值和内存值是一致的,线程A是可以修改的!所谓站在线程A的角度,这个值是从未被修改的,但实际上已经被线程B和线程C修改过。这就是所谓的ABA问题。

图片

要解决ABA的问题,Java也提供了AtomicStampedReference类供我们用,说白了就是加了个版本,比对的就是内存值+版本是否一致

4、Redis集群和哨兵模式有什么区别#

集群分片,比如5主5从,数据过来后会均匀分配到这5台服务器上面,5台服务器上面的数据是不同的,但是每台服务器都有一个从服务器,从服务器的数据是和主服务器一致的。

对于这5对服务器来说,是集群分片模式;而5对服务器的每一对,都是主从模式。

主从模式:为了给Redis高可用的特性,给Redis主服务器做备份操作,多启动一台Redis服务器来形成主从架构;

主从服务器数据是一致的,如果主服务器挂了,可以手动把从服务器升级为主服务器,缩短系统不可以的时间。

哨兵模式:为了解决手动把从服务升级为主服务的操作,利用哨兵来做这一工作。哨兵主要的工作就是:监控主服务器状态、当主服务挂了,在从服务器中选出一个作为主服务器、通知故障消息给管理员、作为配置中心提供当前主服务器的信息。

可以把哨兵当做运行在特殊模式下的Redis服务器,也是集群中的一部分。

分片集群:分片集群就是往每个Redis服务器上存储一部分数据,所有的Redis服务器数据加起来,才能组成完整的数据。所以技术难点在于对不同的Key进行路由(分片),一般现有两种方案:客户端路由(SDK)和服务端路由(Proxy),客户端路由代表技术:Redis Cluster,服务端路由代表技术:Codis。

目前Redis集群分片中主流采用Codis技术。Codis是客户端直连Proxy层,由Proxy进行请求分发到不同的Redis实例上

img

所以根据自己的理解,一般Redis服务架构演变,根据业务量的增加,由 单节点服务 -》主从结构 -》哨兵模式 -》集群分片模式

5、消息队列的了解#

消息队列面试内容https://juejin.cn/post/7067322260511522823?utm_source=gold_browser_extension

了解过哪些队列

Kafuka如何保证接受消息的措施,保证消息的不丢失?

img

对于生产者来说,

生产者使用Send方法发送消息,是异步的,所以可以通过get方法或回调函数拿到调用的结果,如果失败了,可以重试,重试次数可以稍微大些,比如5次,间隔可以稍微长些。

--基础概念:当消息被追加到分区(partition)时,会为其分配一个偏移量(offset)。这个偏移量可以记录consumer消费到这个patition的哪个位置。

对于消费者来说

首先要了解,Kafka是通过偏移量(offset)来保证消息在某个分区的顺序的

解决消费端丢失数据,关闭自动提交offset,等消费者处理完后再手动提交offset

kafka的partition有多副本机制,其中一个为leader,其他称之为follower。消息会先存在leader,follower再拉取leader同步,这样生产者和消费者只和leader交互,follower副本是leader副本的备份,保证数据存储的安全性。

除此之外如果leader所在的broker挂了,会在follower之间选一个leader。

6、MySQL的隔离级别#

MySQL事务传播策略

事务的四大要素(ACID)
  1. 原子性(Atomicity)事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态。事务是一个不可分割的整体,要么成功,要么失败。
  2. 一致性(Consistency)事务开始前和结束后,数据库的完整性约束没有被破坏。
  3. 隔离性(Lsolation)同一时间,只允许一个事务请求同一数据,不同的事物之间彼此没有任何干扰。比如A事务在操作某行数据,事务B则不能操作该行数据。
  4. 持久性(Durability)事务完成后,事务对数据库所有更新将被保存到数据库,不能回滚。
事务的并发问题
  1. 脏读:事务A读了事务B更新的数据,然后B回滚了,那么A读取到的数据是脏数据;
  2. 不可重复读:事务A多次读取同一数据,事务B在事务A多次读取的过程中,对数据做了更新并提交,导致事务A多次读取同一数据,但读取结果不一致。
  3. 幻读:事务A对表内所有数据做了统一修改为0,在修改过程中事务B对表内插入了数据为1的值,当事务A修改数据后,发现有一条数据还是1,就好像发生了幻觉一样,这就是幻读。

不可重复读和幻读容易混淆,不可重复读侧重于修改,而幻读侧重于新增或删除。解决不可重复读的问题只需锁住要修改的行,而解决幻读需要锁表。

事务隔离级别

下面四种隔离级别分别对应是否可以引发并发问题的可能。

事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted)
读已提交(read-committed)
可重复读(repeatable-read)
串行化(serializable)

MySQL默认隔离级别是可重复读

  1. 读未提交,最低的事务隔离级别,一个事务还没提交时,它做的变更就能被别的事务看到。任何情况都无法保证。
  2. 读已提交,保证一个事务提交后才能被另一个事务读取,另外一个事务不能读取该事务未提交的数据。大多数数据库的默认级别就是这个,比如SQL Server,Oracle
  3. 可重复读,多次读取同一范围的数据会返回第一次查询的快照,即使其他事务对该数据做了更新修改。事务在执行期间看到的数据前后必须一致的。但如果这个事务在读取某个范围的记录时,其他事务又在改范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行,这就是幻读。所以可以避免脏读、不可重复读,但可能出现幻读。
  4. 串行化,“写”会加“写锁”,“读”会加“读锁”。当出现读写冲突时,后访问的事务必须等待前一个事务执行完成才能继续执行,事务100%隔离,避免三大问题,但并发性能下降。

一些补充

  1. 事务隔离级别为读提交时,写数据只会锁住相应的行。
  2. 事务隔离级别为可重复读时,如果检索条件有索引(包括主键索引)的时候,默认加锁方式是next-key 锁;如果检索条件没有索引,更新数据时会锁住整张表。一个间隙被事务加了锁,其他事务是不能在这个间隙插入记录的,这样可以防止幻读。
  3. 事务隔离级别为串行化时,读写数据都会锁住整张表。
  4. 隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。

一般互联网公司都选择读已提交作为主要的隔离级别,而可重复读可能因为间隙锁导致的死锁问题。

7、为什么要选择Nacos作为技术选型#

初步结论为:使用Nacos代替Eureka和apollo,主要理由为:
相比与Eureka:
(1)Nacos具备服务优雅上下线和流量管理(API+后台管理页面),而Eureka的后台页面仅供展示,需要使用api操作上下线且不具备流量管理功能。
(2)从部署来看,Nacos整合了注册中心、配置中心功能,把原来两套集群整合成一套,简化了部署维护
(3)从长远来看,Eureka开源工作已停止,后续不再有更新和维护,而Nacos在以后的版本会支持SpringCLoud+Kubernetes的组合,填补 2 者的鸿沟,在两套体系下可以采用同一套服务发现和配置管理的解决方案,这将大大的简化使用和维护的成本。同时来说,Nacos 计划实现 Service Mesh,是未来微服务的趋势
(4)从伸缩性和扩展性来看Nacos支持跨注册中心同步,而Eureka不支持,且在伸缩扩容方面,Nacos比Eureka更优(nacos支持大数量级的集群)。
(5)Nacos具有分组隔离功能,一套Nacos集群可以支撑多项目、多环境。

相比于apollo
(1) Nacos部署简化,Nacos整合了注册中心、配置中心功能,且部署相比apollo简单,方便管理和监控。
(2) apollo容器化较困难,Nacos有官网的镜像可以直接部署,总体来说,Nacos比apollo更符合KISS原则
(3)性能方面,Nacos读写tps比apollo稍强一些

结论:使用Nacos代替Eureka和apollo

8、重新学习HashMap#

常用的Map

9、Java里面==和equals区别#

如何重新equals方法,什么场景下进行equals方法的重写。

==如果比较的是基本数据类型,只要两个数据值相同,则返回true;如果比较的是引用数据类型的话,比较的引用所指向的对象的物理地址是否相同,如果地址相同,则返回true。

equals是Object类的方法,在Object方法内equal()实际上返回的就是用==进行比较的结果。但是继承Object的类一般都会重写equal方法,比如String中的equal方法就是对比两个String的值是否相等。

下面附上String的equals()方法:

public boolean equals(Object anObject) {
        if (this == anObject) {
            return true;
        }
        if (anObject instanceof String) {
            String anotherString = (String)anObject;
            int n = value.length;
            if (n == anotherString.value.length) {
                char v1[] = value;
                char v2[] = anotherString.value;
                int i = 0;
                while (n-- != 0) {
                    if (v1[i] != v2[i])
                        return false;
                    i++;
                }
                return true;
            }
        }
        return false;
    }

10、Nacos动态配置#

Nacos上如果改了数据库连接配置,是否需要重启该服务?

是的,需要重启服务,Nacos可以做到修改配置后,立即更新线上服务的配置文件,但Data Source并不会被重新创建,所以需要重启服务来改变数据库连接配置。

如何使用Nacos实现数据库连接的自动切换?

思路是借用MyBatis-Plus的DynamicRoutingDataSource动态数据源,将两个数据库连接预先配置好,通过修改配置中心的固定Key/Value值来实现动态切换数据库连接。

主要步骤:定义DynamicRoutingAndSwitchingDataSource,实现数据源动态路由,数据库配置自动切换。采用装饰模式扩展mybatis plus的DynamicRoutingDataSource的功能,实现EnvironmentChangeEvent事件的监听器ApplicationListener,在nacos配置修改的时候,监听配置的修改。Nacos配置在触发事件时,只会传输更新值的key/value值,所以,需要根据更新属性的key值,判断是否修改的数据库配置。

11、Fegin的如何使用#

Fegin这块有连接池的配置呢?

12、Mybatis是怎么在用的?#

如果让你去做这个查询所有子部门的逻辑,你会怎么做?

如何根据数据库建立一个树结构的

SQL如何写递归?

13、DDD的设计模式#

https://www.cnblogs.com/skevin/p/15874719.html学习网址

用DDD的领域驱动设计来解释当前的云改人力项目

比如api和server层放什么代码。

接口是否有鉴权的限制

分布式下,登录服务是多个节点情况下,业务代码如何获取登录用的信息。

问题:在基于充血模型的 DDD 开发模式中,将业务逻辑移动到Domain 中,Service 类变得很薄,但在我们的代码设计与实现中,并没有完全将Service 类去掉,这是为什么?或者说,Service 类在这种情况下担当的职责是什么?哪些功能逻辑会放到 Service 类中?

区别于 Domain 的职责,Service 类主要有下面这样几个职责。
(1)Service 类负责与 Repository 交流。

  • VirtualWalletService类负责与 Repository 层打交道,调用 Respository 类的方法,获取数据库中的数据,转化成领域模型 VirtualWallet,然后由领域模型 VirtualWallet 来完成业务逻辑,最后调用Repository 类的方法,将数据存回数据库。
  • 之所以让 VirtualWalletService 类与 Repository 打交道,而不是让领域模型 VirtualWallet 与 Repository 打交道,那是因为我们想保持领域模型的独立性,不与任何其他层的代码(Repository 层的代码)或开发框架(比如 Spring、MyBatis)耦合在一起,将流程性的代码逻辑(比如从 DB 中取数据、映射数据)与领域模型的业务逻辑解耦,让领域模型更加可复用。

(2)Service 类负责跨领域模型的业务聚合功能。

VirtualWalletService 类中的 transfer() 转账函数会涉及两个钱包的操作,因此这部分业务逻辑无法放到 VirtualWallet 类中,所以,我们暂且把转账业务放到 VirtualWalletService 类中了。

当然,随着功能演进,使得转账业务变得复杂起来之后,我们也可以将转账业务抽取出来,设计成一个独立的领域模型

(3)Service 类负责一些非功能性及与三方系统交互的工作。比如幂等、事务、发邮件、发消息、记录日志、调用其他系统的 RPC 接口等,都可以放到 Service 类中。

在基于充血模型的 DDD 开发模式中,尽管 Service 层被改造成了充血模型,但是 Controller 层和 Repository 层还是贫血模型,是否有必要也进行充血领域建模呢?

答案是没有必要。Controller 层主要负责接口的暴露,Repository 层主要负责与数据库打交道,这两层包含的业务逻辑并不多,如果业务逻辑比较简单,就没必要做充血建模,即便设计成充血模型,类也非常单薄,看起来也很奇怪。

尽管这样的设计是一种面向过程的编程风格,但我们只要控制好面向过程编程风格的副作用,照样可以开发出优秀的软件。那这里的副作用怎么控制呢?

就拿Repository的Entity来说,即便它被设计为贫血模型,违反面向对象编程的封装特性,有被任意代码修改数据的风险,但Entity的声明周期是有限的,一般来说,我们把它传递到Service层后,就会转化成BO或者Domain来继续后面的业务逻辑。Entity的生命周期就结束了,所以也不会任意修改

我们再来说说 Controller 层的 VO。实际上 VO 是一种 DTO(Data Transfer Object,数据传输对象)。它主要是作为接口的数据传输载体,将数据发送给其他系统。从功能上来说,它理应不包括业务逻辑、只包含数据。所以,我们将它设计成贫血模型也是比较合理的。

总结
  • 基于充血模型的DDD开发模式跟基于贫血模型的传统开发模式相比,主要区别在于Service层。在基于充血模型的开发模式下,我们将部分原来放在Service类中的业务逻辑移动到了一个充血的Domain领域模型中,让Service类的实现依赖这个Domain

在基于充血模型的 DDD 开发模式下,Service 类并不会完全移除,而是负责一些不适合放在 Domain 类中的功能。比如,负责与 Repository 层打交道、跨领域模型的业务聚合功
能、幂等事务等非功能性的工作。

基于充血模型的 DDD 开发模式跟基于贫血模型的传统开发模式相比,Controller 层和Repository 层的代码基本上相同。这是因为,Repository 层的 Entity 生命周期有限,Controller 层的 VO 只是单纯作为一种 DTO。两部分的业务逻辑都不会太复杂。业务逻辑
主要集中在 Service 层。所以,Repository 层和 Controller 层继续沿用贫血模型的设计思路是没有问题的。

来源:https://blog.csdn.net/zhizhengguan/article/details/122203737

14、MySQL优化#

如果表有一定的数据量,就应该创建对应的索引。

1、是否能够使用覆盖索引,减少回表所消耗的时间。意味着我们在select的时候,一定要指明对应的列,而不是select *;

2、考虑是否组件联合索引,如果组建联合索引,尽量将区分度最高的放在最左边,并且需要考虑最左匹配原则

3、对索引进行函数操作或者表达式计算会导致索引失效;

4、利用子查询优化超多分页场景。比如limit offset,n在MySQL是获取offset+n的记录,再返回n条。而利用子查询出n条,通过ID检索对应的记录出来,提高查询效率。

5、通过explain命令来查看SQL的执行计划,看看自己写的SQL是否走了索引,走了什么索引。通过show profile来查看SQL对系统资源的损耗情况(这点一般很少用到)

6、开启事务后,在事务内尽可能只操作数据库,并有意识地减少锁的持有时间(比如在事务内需要插入&&修改数据,那可以先插入后修改。因为修改时,更新操作会加行锁。如果先更新,那并发下可能会导致多个事务的请求等待行锁释放)

如果发生走对了索引,线上的查询为什么还是慢?

这种情况,一般是表的数据量太大了,可以考虑把比较不常用的或者旧数据给取出到其他表或者Hive中,这种情况的解决方案就是尽可能地减少该表的数据量。

还有一种解决方案是:能不能再查询数据库之前走一层缓存,利用缓存来达到常用数据的快速查询。

如果是因为字符串检索导致查询效率低的话,可以把表数据导入Elasticsearch类的搜索引擎中,后续线上查询走ES即可,也得保障MySQL到ES的数据同步程序。

如果还不行就要根据查询条件的维度,做相应的聚合表,线上的请求就查询聚合表的数据,不走原表。

利用空间换取时间的思路,相同数据换到别的地方储存提高查询效率。

15、Redis缓存穿透#

首先要了解三个情况:缓存穿透、缓存击穿、缓存雪崩。

  • 缓存穿透: Key对应的数据源并不存在,每次针对此Key的请求从缓存中获取不到,请求都回到数据源,从而可能压垮数据源。利用缓存里没有Key值,可以直接导致巨量的请求到数据库,导致数据库承载不住,从而宕机。
  • 缓存击穿:是指一个热点Key,不停的扛着大并发量,当这个Key失效的瞬间,持续的大量并发穿破缓存,直接请求数据库,导致压垮数据库。(可以设置热点Key永不过期)
  • 缓存雪崩:当缓存服务器重启或大量缓存集中在某一个时间段失效,这样在失效的时候,大量请求打到数据库上,导致数据库崩溃。

首先是缓存穿透的解决方案:

1、规范Key过滤

规范Key的命名,并且统一缓存查询的入口,在入口处对Key的命名格式进行检测,过滤不规范的Key的访问,用来过滤大部分恶意的攻击。比如可以约定项目Redis缓存Key的前缀都是以“公司名_项目名_REDIS_”开头,不符合这个约定的Key在一开始就过滤掉。

2、缓存空值

比较简单粗暴的方法,如果数据库查询为空,就把空值放到Redis缓存中,只是它的过期时间设置很短,另外为了避免不必要的内存消耗,可以定期清理空值的key。

3、加锁

根据Key从缓存中获取到的value为空时,先上锁,再去查数据库,将数据加载到缓存,若其他线程获取锁失败,则等待一段时间后重试,从而避免大量请求直接打到数据库上。单机可以使用Synchronized或ReentrantLock加锁,分布式环境需要加分布式锁,如Redis分布式锁。

4、布隆过滤器

首先需要了解哈希函数,哈希函数有一个特征:

同一个哈希函数得到的哈希值不同,那么两个哈希值的原始值肯定不同;

同一个哈希函数得到的哈希值相同,两个哈希值的原始值可能相同,也可能不相同。

布隆过滤器就是由很长的二进制向量和一系列的哈希函数组成的。

假设布隆过滤器的底层存储结构是一个长度为16的位数组,初始状态时,它的所有位置都设置为0。

img

当有变量添加到布隆过滤器中,通过K个映射函数将变量映射到位数组的K个点,并把这K个点的值设置为1(假设有三个映射函数)。

img

查询某个变量是否存在的时候,我们只需要通过同样的K个映射函数,找到对应的K个点,判断K个点上的值是否全都是1,如果全都是1则表示很可能存在,如果K个点上有任何一个是0则表示一定不存在

布隆过滤器拥有两个特性:存在一定误判的可能;不能删除布隆过滤器里面的元素;

优缺点

优点:

在空间和时间方面拥有很大优势,存入的并不是完整数据而是一个二进制向量,能节约大量内存空间;时间复杂度方面,根据映射的函数擦查询,如果有K个映射函数,那么时间复杂度就是O(K)。

因为存的不是元素本身,所以在保密性要求严格的场景下有一定优势。

缺点:

存在一定的误判。存入布隆过滤器里的元素越多,误判率越高。

不能删除布隆过滤器里面的元素,随着使用时间的越来越长,因为不能删除,存进里面的元素越来越多,占用内存越来越大,误判率越来越高,最后不得不重置。

应用场景:

应用于缓解缓存穿透!因为存在一定误判的可能,所以不能做到完全解决缓存穿透问题。

预先把数据库内的数据加入到布隆过滤器中,因为布隆过滤器的底层数据接口是一个二进制向量,所以占用的空间不是很大。在查询Redis之前,先通过布隆过滤器判断是否存在,如果不存在就直接返回,如果存在就按照原来的流程查询Redis,Redis不存在就查询数据库。

主要利用布隆过滤器的不存在的话一定不存在的特性,但存在的不一定真的存在,存在误判,所以并不能完全解决缓存穿透。

img

布隆过滤器,Redis提供了布隆过滤器插件:https://github.com/RedisBloom/RedisBloom.git

那么缓存击穿的解决方案:

1、设置热点Key永不过期;

2、加锁,根据热点Key从缓存中获取得到的Value为空时,先锁上,再去查数据库将数据加载到缓存,如果其他线程获取失败,则等待一段时间后重试,从而避免大量请求打到数据库上。单机可以使用Synchronized或ReentrantLock,分布式需要加分布式锁,如Redis分布式锁。(为了不阻塞对其他key的请求,此处可以用热点key加锁)

预防和解决缓存雪崩:

1、保证缓存服务高可用性,如使用Redis Sentinel 和 Redis Cluster,双机房部署,保证Redis服务高可用。

2、通过设置不同的过期时间,来错开缓存你过期,从而避免缓存集中失效。

16、线程#

线程的生命周期包括哪几个阶段?

线程的生命周期包含5个阶段,包括:新建、就绪、运行、阻塞、销毁。

  • 新建:就是刚使用new方法,new出来的线程;
  • 就绪:就是调用的线程的start()方法后,这时候线程处于等待CPU分配资源阶段,谁先抢的CPU资源,谁开始执行;
  • 运行:当就绪的线程被调度并获得CPU资源时,便进入运行状态,run方法定义了线程的操作和功能;
  • 阻塞:在运行状态的时候,可能因为某些原因导致运行状态的线程变成了阻塞状态,比如sleep()、wait()之后线程就处于了阻塞状态,这个时候需要其他机制将处于阻塞状态的线程唤醒,比如调用notify或者notifyAll()方法。唤醒的线程不会立刻执行run方法,它们要再次等待CPU分配资源进入运行状态;
  • 销毁:如果线程正常执行完毕后或线程被提前强制性的终止或出现异常导致结束,那么线程就要被销毁,释放资源;

img****

img

17、SpringBoot的Bean生命周期#

前言#

Spring生命周期有四个阶段:

  • Bean的定义
  • Bean的初始化
  • Bean的生存期
  • Bean的销毁

Bean的定义过程:

1、第一步,资源定位,就是Spring根据定义的注解(@Component)找到相应的类。

2、找到资源就开始解析,并将定义的信息保存起来,此时,还没有初始化Bean

3、然后将Bean的定义发布到SpringIOC容器中,此时,SpringIOC的容器中还是没有Bean的生成,只是定义的信息。

Bean的初始化:

经过Bean的定义、初始化,Spring会继续完成Bean的实例和依赖注入,这样从IOC容器中就可以得到一个完成依赖注入的Bean。

Bean的生命周期:

img

https://segmentfault.com/a/1190000041266775

Bean的创建就是调用Bena的构造方法创建出来的。

18、AOP编程#

首先AOP也是使用动态代理实现的

img

AOP 领域中的特性术语:

  • 通知(Advice): AOP 框架中的增强处理。通知描述了切面何时执行以及如何执行增强处理。
  • 连接点(join point): 连接点表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出。在 Spring AOP 中,连接点总是方法的调用。
  • 切点(PointCut): 可以插入增强处理的连接点。
  • 切面(Aspect): 切面是通知和切点的结合。
  • 引入(Introduction):引入允许我们向现有的类添加新的方法或者属性。
  • 织入(Weaving): 将增强处理添加到目标对象中,并创建一个被增强的对象,这个过程就是织入。

概念看起来总是有点懵,并且上述术语,不同的参考书籍上翻译还不一样,所以需要慢慢在应用中理解。

看下面一个切面类:

package com.sharpcj.aopdemo.test1;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class BuyAspectJ {
    @Before("execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))")
    public void haha(){
        System.out.println("男孩女孩都买自己喜欢的东西");
    }
}

使用了注解@Component表明它将作为一个Spring Bean被装配,使用注解@Aspect表示他是一个切面类,类中只有一个haha1方法,被@Before注解,表示该方法将在目标方法执行之前运行。参数("execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))")则声明了切点,表明在该切面的切点 com.sharpcj.aopdemo.test1.IBuy这个接口的buy方法。

同时还要在配置文件中启用AOP切面的功能。

package com.sharpcj.aopdemo;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan(basePackageClasses = {com.sharpcj.aopdemo.test1.IBuy.class})
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AppConfig {
}

在配置文件中加了@EnableAspectJAutoProxy 注解,启用了AOP功能,参数proxyTargetClass 的值设置为了true。默认false,两者的却别后续再说。

Spring AOP中有五种通知类型。分别如下:

img

19、动态代理#

20、Java中锁的分类#

21、多个登陆服务的节点,如何保证登陆的唯一?#

22、数据库的数据与缓存保持一致的做法?#

面试中,面试官非要建议我代码里,先删除缓存,再修改数据库,再添加缓存的操作!

有详解:https://blog.csdn.net/zhizhengguan/article/details/122972503

23、快排的逻辑和Java中Sort()的实现?#

24、抽象类和接口的区别?#

1、接口只能定义抽象方法不能实现方法,抽象类既可以定义抽象方法,也可以实现方法。
2、单继承,多实现。接口可以实现多个,只能继承一个抽象类。
3、接口强调的是功能,抽象类强调的是所属关系。
4、接口中的所有成员变量 为public static final, 静态不可修改,当然必须初始化。接口中的所有方法都是public abstract 公开抽象的。而且不能有构造方法。

抽象类就比较自由了,和普通的类差不多,可以有抽象方法也可以没有,可以有正常的方法,也可以没有。

25、如何使用和创建索引?索引有什么使用注意事项?#

哪些情况需要创建索引#

  1. 主键自动建立唯一索引
  2. 频繁作为查询条件的字段
  3. 查询中与其他表关联的字段,外键关系建立索引
  4. 单键/组合索引的选择问题,who? 高并发下倾向创建组合索引
  5. 查询中排序的字段,排序字段通过索引访问大幅提高排序速度
  6. 查询中统计或分组字段

哪些情况不要创建索引#

  1. 表记录太少
  2. 经常增删改的表
  3. 数据重复且分布均匀的表字段,只应该为最经常查询和最经常排序的数据列建立索引(如果某个数据类包含太多的重复数据,建立索引没有太大意义)
  4. 频繁更新的字段不适合创建索引(会加重IO负担)
  5. where 条件里用不到的字段不创建索引

导致 SQL 执行慢的原因#

  1. 硬件问题。如网络速度慢,内存不足,I/O 吞吐量小,磁盘空间满了等
  2. 没有索引或者索引失效
  3. 数据过多(分库分表)
  4. 服务器调优及各个参数设置(调整my.cnf)

索引优化#

  1. 全值匹配我最爱
  2. 最佳左前缀法则,比如建立了一个联合索引(a,b,c),那么其实我们可利用的索引就有(a) (a,b)(a,c)(a,b,c)
  3. 不在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描
  4. 存储引擎不能使用索引中范围条件右边的列
  5. 尽量使用覆盖索引(只访问索引的查询(索引列和查询列一致)),减少 select *
  6. is null ,is not null 也无法使用索引
  7. like "xxxx%" 是可以用到索引的,like "%xxxx" 则不行(like "%xxx%" 同理)。like 以通配符开头('%abc...')索引失效会变成全表扫描的操作,
  8. 字符串不加单引号索引失效
  9. 少用or,用它来连接时会索引失效(这个其实不是绝对的,or 走索引与否,还和优化器的预估有关,5.0 之后出现的 index merge 技术就是优化这个的)
  10. <,<=,=,>,>=,BETWEEN,IN 可用到索引,<>,not in ,!= 则不行,会导致全表扫描

建索引的几大原则#

  1. 最左前缀匹配原则,非常重要的原则,MySQL 会一直向右匹配直到遇到范围查询(>、<、between、like)就停止匹配,比如 a = 1 and b = 2 and c > 3 and d = 4 如果建立(a,b,c,d)顺序的索引,d 是用不到索引的,如果建立(a,b,d,c)的索引则都可以用到,a,b,d 的顺序可以任意调整。
  2. = 和 in 可以乱序,比如 a = 1 and b = 2 and c = 3 建立(a,b,c)索引可以任意顺序,MySQL 的查询优化器会帮你优化成索引可以识别的形式。
  3. 尽量选择区分度高的列作为索引,区分度的公式是 count(distinct col)/count(*),表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是 1,而一些状态、性别字段可能在大数据面前区分度就是 0,那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要 join 的字段我们都要求是 0.1 以上,即平均 1 条扫描 10 条记录。
  4. 索引列不能参与计算,保持列“干净”,比如 from_unixtime(create_time) = ’2014-05-29’ 就不能使用到索引,原因很简单,b+ 树中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用函数才能比较,显然成本太大。所以语句应该写成 create_time = unix_timestamp(’2014-05-29’)
  5. 尽量的扩展索引,不要新建索引。比如表中已经有 a 的索引,现在要加(a,b)的索引,那么只需要修改原来的索引即可。

26、单点登录的方案#

27、线程本地变量ThreadLocal#

https://www.cnblogs.com/moonandstar08/p/4912673.html

28、分布式锁用的哪个?#

直接上结论:

分布式锁一般有三种实现方式:1. 数据库乐观锁;2. 基于 Redis 的分布式锁;3. 基于 ZooKeeper 的分布式锁。

但为了追求更好的性能,我们通常会选择使用 Redis 或 Zookeeper 来做。

想必也有喜欢问为什么的同学,那数据库客观锁怎么就性能不好了?

使用数据库乐观锁,包括主键防重,版本号控制。但是这两种方法各有利弊。

  • 使用主键冲突的策略进行防重,在并发量非常高的情况下对数据库性能会有影响,尤其是应用数据表和主键冲突表在一个库的时候,表现更加明显。还有就是在 MySQL 数据库中采用主键冲突防重,在大并发情况下有可能会造成锁表现象,比较好的办法是在程序中生产主键进行防重。

  • 使用版本号策略

这个策略源于 MySQL 的 MVCC 机制,使用这个策略其实本身没有什么问题,唯一的问题就是对数据表侵入较大,我们要为每个表设计一个版本号字段,然后写一条判断 SQL 每次进行判断。

基于 Redis 的分布式锁#

其实 Redis 官网已经给出了实现:https://redis.io/topics/distlock,说各种书籍和博客用了各种手段去用 Redis 实现分布式锁,建议用 Redlock 实现,这样更规范、更安全。我们循序渐进来看

我们默认指定大家用的是 Redis 2.6.12 及更高的版本,就不再去讲 setnxexpire 这种了,直接 set 命令加锁

set key value[expiration EX seconds|PX milliseconds] [NX|XX]

eg:

SET resource_name my_random_value NX PX 30000

SET 命令的行为可以通过一系列参数来修改

  • EX second :设置键的过期时间为 second 秒。SET key value EX second 效果等同于 SETEX key second value
  • PX millisecond :设置键的过期时间为 millisecond 毫秒。SET key value PX millisecond 效果等同于 PSETEX key millisecond value
  • NX :只在键不存在时,才对键进行设置操作。SET key value NX 效果等同于 SETNX key value
  • XX :只在键已经存在时,才对键进行设置操作。

这条指令的意思:当 key——resource_name 不存在时创建这样的key,设值为 my_random_value,并设置过期时间 30000 毫秒。

别看这干了两件事,因为 Redis 是单线程的,这一条指令不会被打断,所以是原子性的操作。

Redis 实现分布式锁的主要步骤:

  1. 指定一个 key 作为锁标记,存入 Redis 中,指定一个 唯一的标识 作为 value。
  2. 当 key 不存在时才能设置值,确保同一时间只有一个客户端进程获得锁,满足 互斥性 特性。
  3. 设置一个过期时间,防止因系统异常导致没能删除这个 key,满足 防死锁 特性。
  4. 当处理完业务之后需要清除这个 key 来释放锁,清除 key 时需要校验 value 值,需要满足 解铃还须系铃人

设置一个随机值的意思是在解锁时候判断 key 的值和我们存储的随机数是不是一样,一样的话,才是自己的锁,直接 del 解锁就行。

当然这个两个操作要保证原子性,所以 Redis 给出了一段 lua 脚本(Redis 服务器会单线程原子性执行 lua 脚本,保证 lua 脚本在处理的过程中不会被任意其它请求打断。):

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

问题:#

我们先抛出两个问题思考:

  1. 获取锁时,过期时间要设置多少合适呢?

    预估一个合适的时间,其实没那么容易,比如操作资源的时间最慢可能要 10 s,而我们只设置了 5 s 就过期,那就存在锁提前过期的风险。这个问题先记下,我们先看下 Javaer 要怎么在代码中用 Redis 锁。

  2. 容错性如何保证呢?

    Redis 挂了怎么办,你可能会说上主从、上集群,但也会出现这样的极端情况,当我们上锁后,主节点就挂了,这个时候还没来的急同步到从节点,主从切换后锁还是丢了

带着这两个问题,我们接着看https://mp.weixin.qq.com/s/1OGnQV6wmx3ZwXhq8Kmf6g

29、Spring的源码有了解吗?#

30、Spring定时任务用过吗?#

31、单点登录有做吗#

32、Mybatis过滤器,SQL怎么做优化和监控?#

Mybatis拦截器实现SQL性能监控

  Mybatis拦截器只能拦截四类对象,分别为:Executor、ParameterHandler、StatementHandler、ResultSetHandler,而SQL数据库的操作都是从Executor开始,因此要记录Mybatis数据库操作的耗时,需要拦截Executor类,代码实现如下:

[复制代码](javascript:void(0)😉

/**
 * 数据库操作性能拦截器,记录耗时
 * @Intercepts定义Signature数组,因此可以拦截多个,但是只能拦截类型为:
 *         Executor
 *         ParameterHandler
 *         StatementHandler
 *         ResultSetHandler
 * */
@Intercepts(value = { 
        @Signature (type=Executor.class,
                method="update",
                args={MappedStatement.class,Object.class}),
        @Signature(type=Executor.class,
        method="query",
        args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class,
                CacheKey.class,BoundSql.class}),
        @Signature(type=Executor.class,
        method="query",
        args={MappedStatement.class,Object.class,RowBounds.class,ResultHandler.class})})
public class TimerInterceptor implements Interceptor {

    private static final Logger logger = Logger.getLogger(TimerInterceptor.class);
    
    /**
     * 实现拦截的地方
     * */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        Object target = invocation.getTarget();
        Object result = null;
        if (target instanceof Executor) {
            long start = System.currentTimeMillis();
            Method method = invocation.getMethod();
            /**执行方法*/
            result = invocation.proceed();
            long end = System.currentTimeMillis();
            logger.info("[TimerInterceptor] execute [" + method.getName() + "] cost [" + (end - start) + "] ms");
        }
        return result;
    }

    /**
     * Plugin.wrap生成拦截代理对象
     * */
    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, this);
    }

    @Override
    public void setProperties(Properties properties) {

    }

}

[复制代码](javascript:void(0)😉

  完成上面的拦截后,需要将该类在Mybatis配置文件中声明,如下:

<plugins>
     <!-- SQL性能拦截器 -->
     <plugin interceptor="com.quar.interceptor.TimerInterceptor" />
</plugins>

33、Fegin怎么把接口转成RestTemplent请求的?#

34、Fegin怎么做负载均衡的?#

35、InnoDB了解多少?#

36、用过哪些索引?#

37、Spring事务的传播机制?#

说到Spring的事务传播机制,就避免不了谈一下Spring事务的管理,通过我们常用的@Transactional来开启Spring事务管理。所以有必要先学习一下@Transactional的用法。

首先看下注解的几个参数:

属性名 说明
value 默认为"",当在配置文件中有多个 TransactionManager , 可以用该属性指定选择哪个事务管理器。
transactionManager 同上,用来指定transactionManager类型。
propagation 事务的传播行为,默认值为 Propagation.REQUIRED。 这个值意味着就是如果有事务, 那么加入事务, 没有的话新建一个。还有其他六种传播行为,下文解释。
isolation 事务的隔离度,默认值采用 Isolation.DEFAULT。DEFAULT是使用后端数据库默认的隔离级别。还以选择其他四种隔离级别,下文解释。
timeout 事务的超时时间(单位:秒),默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。而-1则代表永不超时。
timeoutString 同上,格式为String
readOnly 指定事务是否为只读事务,默认值为 false;为了忽略那些不需要事务的方法,比如读取数据,可以设置 readOnly 为 true。
rollbackFor 例如:Exception.class,用于指定能够触发事务回滚的异常类型,类似于黑名单。如果有多个异常类型需要指定,各类型之间可以通过逗号分隔。
rollbackForClassName 例如:"ClassNotFoundException",同rollbackFor,只不过传入的异常的类名字符串,要注意不要写错异常的类名。
noRollbackFor 例如:IOException.class,抛出指定的异常类型时,则不回滚事务,类似于白名单。
noRollbackForClassName 例如:"NullPointerException",同noRollbackFor,只不过也是传入异常的类名。
lable String数组,功能暂未了解。

自己去@Transactional的属性看了下,发现有这么多参数可以使用。所以在上面的表格总结了一下,本次主要是学习propagation和isolation的。

    @Transactional(
            propagation = Propagation.REQUIRED,
            readOnly = true,
            rollbackFor = Exception.class,
            rollbackForClassName = "ClassNotFoundException",
            isolation = Isolation.DEFAULT,
            timeout = 2,
            noRollbackFor = IOException.class,
            noRollbackForClassName = "NullPointerException",
            value = ""
    )
    public void printLog(){
        System.out.println("使用了事务");
    }

注意点: @Transactional 只能被应用到public方法上, 对于其它非public的方法,如果标记了@Transactional也不会报错,但方法没有事务功能。

介绍一下参数 isolation 可选值:

  • Isolation.DEFAULT 取后端数据库的隔离级别

  • Isolation.READ_UNCOMMITTED 读未提交(会出现脏读,不可重复读,幻读)基本不用

  • Isolation.READ_COMMITTED 读已提交(会出现不可重复读,幻读)

  • Isolation.REPEATABLE_READ 可重复读(会出现幻读)

  • Isolation.SERIALIZABLE 串行化

重点来了,传播机制在同一个service类中两个方法调用,传播机制不生效的。

所以传播机制解决的问题是不同service内的方法,有的方法带事务注解,有的方法不带事务注解,那么这些方法之间调用时出现的各种情况又该如何控制?有事务的方法调用无事物的方法会发生什么?那上级事务抛出异常,是否会让下级(子)事务进行回滚?带着这些问题,下面的传播机制类型就是 解决复杂环境下的事务互相影响 的问题。

参数propagation 可选值:

  • Propagation.REQUIRED 如果有事务,那么加入事务,没有的话新建一个;
  • Propagation.SUPPORTS 当前存在事务,则加入当前事务,如果没有事务,就以非事务方法执行;(这个和不写没区别)
  • Propagation.MANDATORY 必须在一个已有的事务中执行,否则抛出异常;
  • Propagation.REQUIRED_NEW 不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕,继续执行老的事务。(独立提交事务,不受上级方法的异常影响。)
  • Propagation.NOT_SUPPORTED 始终以非事务方式执行,如果当前存在事务,则挂起事务;
  • Propagation.NEVER 必须在一个没有的事务中执行,否则抛出异常(与Propagation.MANDATORY 正好相反)
  • Propagation.NESTED 如果当前存在事务,它将会成为上级事务的一个子事务,方法结束后并没有提交,只有等上级事务结束才提交;如果当前没有事务,则新建事务;如果它异常,上级可以捕获它的异常而不进行回滚,正常提交;但如果上级异常,它必然回滚,这就是和 REQUIRES_NEW 的区别;

网上找找了很多解释,这个感觉靠谱:https://www.pianshen.com/article/73741109131/

一般用得比较多的是 Propagation.REQUIRED , REQUIRES_NEW;

就这七种情况,想要仔细的摸清楚原理太难了,等有空了写个测试类,挨个测试一下,毕竟业务中经常出现事务嵌套的情况。

38、SpringSecurity的过滤器了解几个?#

39、怎么做多表联查?#

40、Spring启动时候,要增加一些操作,该怎么实现?#

五种方法都可以在Spring启动过程进行调用方法。

1、CommanLineRunner#

@Slf4j
@Component
@Order(2)
public class MyCommandLineRunner implements CommandLineRunner {
    
    @Override
    public void run(String... args) throws Exception {
        log.info("MyCommandLineRunner order is 2");
        if (args.length > 0){
            for (int i = 0; i < args.length; i++) {
                //根据下面的命令行参数,这里会循环两次,分别输出--foo=bar和--name=rgyb
                log.info("MyCommandLineRunner current parameter is: {}", args[i]);
            }
        }
    }
}

测试命令行参数,在将项目打包成jar后,进行启动项目时,用到参数:

java -jar springboot-application-startup-0.0.1-SNAPSHOT.jar --foo=bar --name=rgyb

其中 --foo=bar --name=rgyb 就是参数。

当Spring Boot在应用上下文中,找到CommendLineRunner bean,SpingBoot会在应用成功启动之后调用run()方法,并传递用于启动应用程序的命令行参数。

注意点:

​ 1、命令行传入的参数并没有被解析,而只是显示出传入的字符串内容,我们通过ApplicationRunner解析。

​ 2、在重写的run()方法上有throw Exception 标记,Spring Boot会将CommandLineRunner作为应用启动的一部分,如果运行run()方法时抛出Exception,应用将会终止启动。

​ 3、我们在类上添加了@Order(2)注解,当有多个CommandLineRuner时,将会按照@Order注解中的数字从小到大排序(数字可以用复数)

不要使用@Order太多,如果觉得Order非常方便的将启动逻辑按照指定顺序执行,但这么写的话,说明多个代码片段是由相互依赖关系,为了让代码更好维护,减少使用这种依赖。

总结:如果只是想要获取以空格分隔的命令行参数,用CommandLineRunner就足够了。

2、ApplicationRunner#

@Component
@Slf4j
@Order(1)
public class MyApplicationRunner implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.info("MyApplicationRunner order is 1");
        //这里的foo,可以获取到命令行参数foo的值,应该是[bar,rgyb]
        log.info("MyApplicationRunner Current parameter is {}:", args.getOptionValues("foo"));
    }
}

PS:这里就不再展示CommandLineRunner进行命令行参数的操作对比了,有兴趣可以手动试一下

重新打jar包,运行下面命令:

java -jar springboot-application-startup-0.0.1-SNAPSHOT.jar --foo=bar,rgyb

注意点:

​ 1、同使用CommandLineRunner相似,但ApplicationRunner可以通过run方法的ApplicationArguments对象解析出命令行参数,并且每个参数可以有多个值在里面,因为getOptionValues方法返回List数组;

​ 2、在重写的run()方法上有throws Exception标记,SpringBoot会将ApplicationRunner作为启动的一部分,如果运行run()方法时抛出Exception,应用将会终止启动。

​ 3、ApplicationRunner 也可以使用 @Order 注解进行排序,从启动结果来看,它与 CommandLineRunner共享 order 的顺序,稍后我们通过源码来验证这个结论

3、ApplicationListener#

@Slf4j
@Component
@Order(0)
public class MyApplicationListener implements ApplicationListener<ApplicationReadyEvent> {

    @Override
    public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
        log.info("MyApplicationListener is started up");
    }
}

注意点:

​ 1、ApplicationReadyEvent当且仅当在应用程序就绪后才被触发,甚至说上面的Listener要在这五个方案都执行了才被触发。

​ 2、代码中也可以用@Order(0) 来标记,但ApplicationListener 也是可以用在排序的,但只可以用在同类型的ApplicationListener之间排序,与之前ApplicationRunner和CommandLineRunner的排序并不共享。

总结:如果我们不需要获取命令行参数,我们可以通过 ApplicationListener创建一些全局的启动逻辑,我们还可以通过它获取 Spring Boot 支持的 configuration properties 环境变量参数

4、@PostConstruct#

创建启动逻辑的另一种简单解决方案是提供一种在bean创建期间,由Spring调用的初始化方法,我们要做的就是将@PostConstruct注解添加到方法中

@Component
@Slf4j
@DependsOn("myApplicationListener")
public class MyPostConstructBean {

    @PostConstruct
    public void testPostConstruct(){
        log.info("MyPostConstructBean");
    }
}

@PostConstruct该注解被用来修饰一个非静态的void() 方法。

被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器执行一次。

@PostConstruct在构造函数之后执行,init()方法之前执行。整个Bean初始化中的执行顺序:

Constructor(构造方法) -> @Autowired(依赖注入) -> @PostConstruct(注释的方法)

被@PostConstruct注解的方法 将在依赖于它的所有 bean 被初始化之后被调用,如果要添加人为的依赖关系并由此创建一个排序,则可以使用 @DependsOn 注解(虽然可以排序,但是不建议使用,理由和 @Order 一样)

@PostConstruct 方法固有地绑定到现有的 Spring bean,因此应仅将其用于此单个 bean 的初始化逻辑;

5、InitializingBean#

@Component
@Slf4j
public class MyInitializingBean implements InitializingBean {


    @Override
    public void afterPropertiesSet() throws Exception {
        log.info("MyInitializingBean.afterPropertiesSet()");
    }
}

InitializingBean和 @PostConstruct 一样的效果,但二者还是有差别的

​ 1、InitializingBean的afterPropertsSet,顾名思义 在属性设置之后 ,调用该方法时,该Bean的所有属性已经被Spring填充。如果我们在某些属性上使用@Autowired(常规操应该使用构造函数注入),那么Spring将在调用afterPropertiesSet 之前将bean注入这些属性。但@PostConstruct 并没有这些属性的填充。

​ 2、所以 Initializing.afterPropertsSet 解决方案比使用 @PostConstruct 更安全,因为如果我们依赖未自动注入的@Autowire字段,@PostConstruct 方法可能会遇到 NullPointerExceptions

总结:如果使用构造函数注入,而非使用@Autowire,则InitializingBean和@PostConstruct的解决方案时等效的。这其中可以去了解为什么Spring不推荐使用@Autowire注入Bean,而是推荐使用构造函数注入Bean。

下面打开SpringApplication.java类,里面有个callRunners方法,可以解释CommandLineRunner和ApplicationRunner 是在何时被调用的。

private void callRunners(ApplicationContext context, ApplicationArguments args) {
    List<Object> runners = new ArrayList<>();
    //从上下文获取 ApplicationRunner 类型的 bean
    runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());

    //从上下文获取 CommandLineRunner 类型的 bean
    runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());

    //对二者进行排序,这也就是为什么二者的 order 是可以共享的了
    AnnotationAwareOrderComparator.sort(runners);

    //遍历对其进行调用
    for (Object runner : new LinkedHashSet<>(runners)) {
        if (runner instanceof ApplicationRunner) {
            callRunner((ApplicationRunner) runner, args);
        }
        if (runner instanceof CommandLineRunner) {
            callRunner((CommandLineRunner) runner, args);
        }
    }
}

41、Liunx查看日志的命令有哪些?#

tail:

​ -n 是显示行号;相当于nl命令;例子如下:

tail -100f test.log 实时监控100行日志

​ tail -n 10 test.log 查询日志尾部最后10行的日志;

​ tail -n +10 test.log 查询10行之后的所有日志;

head:

​ 跟tail是相反的,tail是看后多少行日志;例子如下:

​ head -n 10 test.log 查询日志文件中的头10行日志;

​ head -n -10 test.log 查询日志文件除了最后10行的其他所有日志;

cat:

​ tac是倒序查看,是cat单词反写;例子如下:

​ 使用通道来进行搜索关键词

cat -n test.log |grep "debug" 查询关键字的日志

还可以用vim或者vi,进入文件内

​ 进入vim编辑模式;

​ 输入“/关键字”,注:正向查找,按enter键查找,按n键,移动到下一个符合条件的;

​ 输入 “?关键字”,注:反向查找,按shift+n 键,把光标移动到下一个符合条件的;

还有直接使用grep的

grep -n -C10 'R0619' caps-biz.txt

查看caps-biz.txt内R0619关键词的上下10行。

42、Liunx启动Jar包命令都有哪些?#

基础命令:

java -jar springProject.jar

43、nohup命令是啥?命令的后面参数还能有哪些?#

nohup 英文全称 no hang up(不挂起),用于在系统后台不挂断地运行命令,退出终端不会影响程序的运行。

nohup 命令,在默认情况下(非重定向时),会输出一个名叫 nohup.out 的文件到当前目录下,如果当前目录的 nohup.out 文件不可写,输出重定向到 $HOME/nohup.out 文件中。

nohup Command [ Arg … ] [ & ]

Command:要执行的命令。

Arg:一些参数,可 以指定输出文件。

&:让命令在后台执行,终端退出后命令仍旧执行。

44、MySQL的慢查询?#

学习别人的面试题目#

posted @   心动的感觉  阅读(219)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示
主题色彩