【精】JAVA基础进阶知识汇总-HELLO XF

创建时间: 2022/6/26 14:34
更新时间: 2023/3/21 19:27
作者: HelloXF
标签: 知识库, 重要文件

Java

基础

JAVA SE

$关键字

Java 语言目前定义了 51 个关键字,这些关键字不能作为变量名、类名和方法名来使用。以下对这些关键字进行了分类。

  1. 数据类型:boolean、int、long、short、byte、float、double、char、class、interface。

  2. 流程控制:if、else、do、while、for、switch、case、default、break、continue、return、try、catch、finally。

  3. 修饰符:public、protected、private、final、void、static、strict、abstract、transient、synchronized、volatile、native。

  4. 动作:package、import、throw、throws、extends、implements、this、supper、instanceof、new。

  5. 保留字:true、false、null、goto、const。

标识符

Java 中标识符是为方法、变量或其他用户定义项所定义的名称。标识符可以有一个或多个字符。在 Java 语言中,标识符的构成规则如下。

  • 标识符由数字(09)和字母(AZ 和 a~z)、美元符号($)、下划线(_)以及 Unicode 字符集中符号大于 0xC0 的所有符号组合构成(各符号之间没有空格)。

  • 标识符的第一个符号为字母、下划线和美元符号,后面可以是任何字母、数字、美元符号或下划线。

static和 final

final定义的变量可以看做一个常量,不能被改变; final定义的方法不能被覆盖; final定义的类不能被继承。 final static
就是再加上static的特性就可以了 static 和final是没有直接关系的 static
是在内存中分配一块区域,供整个类通用,所有的类的对象都享有它的共同的值

数据类型

byte:8位,最大存储数据量是255,存放的数据范围是-128~127之间。

short:16位,最大数据存储量是65536,数据范围是-32768~32767之间。

int:32位,最大数据存储容量是2的32次方减1,数据范围是负的2的31次方到正的2的31次方减1。

long:64位,最大数据存储容量是2的64次方减1,数据范围为负的2的63次方到正的2的63次方减1。

float:32位,数据范围在3.4e-45~1.4e38,直接赋值时必须在数字后加上f或F。

double:64位,数据范围在4.9e-324~1.8e308,赋值时可以加d或D也可以不加。

boolean:只有true和false两个取值。

char:16位,存储Unicode码,用单引号赋值。

$java6大原则

  • 单一职责原则 :简单地说就是一个类只做一件事。如果你遵守了这个原则,那么你的类就会划分的很细,每个类都有比较单一的职责,这不就是高内聚、低耦合么!单一职责原则并不是一个类只能有一个函数,而是说这个类中的函数所做的工作是高度相关的,也就是高内聚。

  • 依赖反转原则:设计和实现要依赖于抽象而非具体。

  • 里氏替换原则 :继承会给程序带来侵入性,程序的可移植性降低,增加了对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能会产生故障。里氏替换原则通俗的来讲就是: 子类可以扩展父类的功能,但不能改变父类原有的功能。 它包含以下4层含义:1、子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。2、子类中可以增加自己特有的方法。3、当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。4、当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

  • 接口隔离原则:接口隔离原则(ISP)拆分非常庞大臃肿的接口成为更小的和更具体的接口,这样客户将会只需要知道他们感兴趣的方法。很多人会觉的接口隔离原则跟之前的单一职责原则很相似,其实不然。其一,单一职责原则原注重的是职责;而接口隔离原则注重对接口依赖的隔离。其二,单一职责原则主要是约束类,其次才是接口和方法,它针对的是程序中的实现和细节;而接口隔离原则主要约束接口接口,主要针对抽象,针对程序整体框架的构。 采用接口隔离原则对接口进行约束时,要注意以下几点:1、接口尽量小,但是要有限度。对接口进行细化可以提高程序设计灵活性是不挣的事实,但是如果过小,则会造成接口数量过多,使设计复杂化。所以一定要适度。2、为依赖接口的类定制服务,只暴露给调用的类它需要的方法,它不需要的方法则隐藏起来。只有专注地为一个模块提供定制服务,才能建立最小的依赖关系。3、提高内聚,减少对外交互。使接口用最少的方法去完成最多的事情。4、运用接口隔离原则,一定要适度,接口设计的过大或过小都不好。设计接口的时候,只有多花些时间去思考和筹划,才能准确地实践这一原则。

  • 迪米特原则 :一个类应该对自己需要耦合或者调用的类知道得最少,这有点类似于接口隔离原则中的最小接口的概念。被依赖者的内部如何实现、如何复杂都与调用者或者依赖者没有关系,调用者或者依赖者只需要知道它需要它需要的方法即可,其他的一概不关心。

  • 开闭原则 :即在需要对软件进行升级、变化时应该通过扩展的形式来实现,而非修改原有代码。

$ConcurrentHashMap

我们熟知的缓存技术(比如redis、memcached)的核心其实就是在内存中维护一张巨大的哈希表,还有大家熟知的HashMap、CurrentHashMap等的应用。

我们知道HashMap是线程不安全的,在多线程环境下,使用Hashmap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。

其实可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,
从JDK1.7版本的(两次hash定位值)ReentrantLock(可重入锁)+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树。

  1. 数据结构 :取消了 Segment分段锁 的数据结构,取而代之的是数组+链表+红黑树的结构。 2. 保证线程安全机制 :JDK1.7采用segment的分段锁机制实现线程安全,其中 segment 继承自ReentrantLock。JDK1.8采用CAS+Synchronized保证线程安全。

其中value用volatile修饰,key和next用final修饰 3. 锁的粒度
:原来是对需要进行数据操作的Segment加锁,现调整为对每个数组元素加锁(Node)。 4. 链表转化为红黑树
:定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。 5. 查询时间复杂度
:从原来的遍历链表O(n),变成遍历红黑树O(logN)。

红黑树的规则:是一种自平衡二叉查找树,是在计算机科学中用到的一种数据结构,典型的用途是实现关联数组,
它虽然是复杂的,但它的最坏情况运行时间也是非常良好的,并且在实践中是高效的: 它可以在O(log n)时间内做查找,插入和删除,这里的n
是树中元素的数目。

  • 规则1: 每个节点不是黑色就是红色

  • 规则2: 根节点为黑色

  • 规则3:红色节点的父节点和子节点不能为红色

  • 规则4:所有的叶子节点都是黑色(空节点视为叶子节点NIL)

  • 规则5:每个节点到叶子节点的每个路径黑色节点的个数都相等。

$集合

java集合框架主要有collection和map两类。

Collection 接口又有 3 种子类型,List、Set 和 Queue,set是无序的并且不可重复。

再下面是一些抽象类,最后是具体实现类,常用的有
ArrayList、LinkedList、HashSet、LinkedHashSet、HashMap、LinkedHashMap 等等。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EpU1zUix-1681014581778)(【精】各大厂问题汇总_files/Image.png)]

来源于Java.util包,是非常实用常用的数据结构 字面意思就是容器。

在Java的util包中有两个所有集合的父接口Collection和Map,

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vEpDdUdu-1681014581779)(【精】各大厂问题汇总_files/Image [1].png)]

Collection和Collections

1、java.util.Collection 是一个集合接口。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java
类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式。

List,Set,Queue接口都继承Collection。
直接实现该接口的类只有AbstractCollection类,该类也只是一个抽象类,提供了对集合类操作的一些基本实现。List和Set的具体实现类基本上都直接或间接的继承了该类。

2、java.util.Collections
是一个包装类。它包含有各种有关集合操作的静态方法(对集合的搜索、排序、线程安全化等),大多数方法都是用来处理线性表的。此类不能实例化,就像一个工具类,服务于Java的Collection框架。

$多线程

evernote:///view/13973867/s64/d000038d-c37c-4a2a-90d0-0bc09d80c590/d000038d-c37c-4a2a-90d0-0bc09d80c590/

Java线程的底层实现

我们知道,线程是 CPU 独立调度的单位,通过引入线程,实现时分复用,利用并发思想使得我们的程序运行的更加迅速。

主流的操作系统都提供了线程的实现,注意这句话,谁实现的线程?是操作系统,尽管本文侧重于介绍 Java
线程的实现原理,但是请大家清楚一点,实际上实现线程的老大哥,是运行在内核态的操作系统。

Java 语言提供了不同硬件和操作系统平台下对线程操作的统一处理,每个已经执行 start() 且还未结束的 java.lang.Thread
类的实例就代表了一个线程。但是正如我们刚刚所强调的那样,线程可是由操作系统来实现的啊,那么 Java
是如何面向开发者提供的线程统一操作呢?我们来简单的看一下 Thread 类的几个关键方法都有native修饰。

因为在 Java 的 API 中,一个 native 方法往往意味着这个方法无法使用平台无关的手段来实现。所以,还是那句话,实际上线程的实现与 Java
无关,由平台所决定,Java 所做的是将 Thread
对象映射到操作系统所提供的线程上面去,对外提供统一的操作接口,向程序员隐藏了底层的细节,使程序员感觉在哪个平台上编写的有关于线程的代码都是一样的。这也是
Java 这门语言诞生之初的核心思想,一处编译,到处运行,只面向虚拟机,实现所谓的平台无关性,而这个平台无关性就是由虚拟机为我们提供的。

操作系统实现线程主要有 3 种方式

  • 用户级线程

  • 内核级线程

  • 用户级线程 + 内核级线程,混合实现

Java 线程的实现

Java 线程在 JDK1.2之前,是基于称为“绿色线程”的用户线程实现的,而在 JDK 1.2
中,线程模型替换为基于操作系统原生线程模型来实现,因此,在目前的 JDK 版本中,操作系统支持怎样的线程模型,在很大程度上决定了 Java
虚拟机的线程是怎样映射的,这点在不同平台上没有办法达成一致,虚拟机规范中也并未限定 Java 线程需要使用哪种线程模型来实现。

举个例子,对于 Sun JDK 来说,它的 Windows 版与 Linux 版都是使用一对一的线程模型实现的,一条 Java
线程就是映射到一条轻量级进程之中,因为 Windows 和 Linux 系统提供的线程模型就是一对一的。

原文链接:https://blog.csdn.net/u013568373/article/details/93474642

线程池种类

Executors工具类提供静态工厂方法

newcachedThreadPool可缓存

newFixThreadPool定长,可控制最大并发数,超出进入队列

newScheduledThreadpool 定时周期执行

newSingleThreadExecutors 单线程化

线程状态

runnable sleep dead wait notifial blocked join running

$CAS

cas是compareandswap的简称,从字面上理解就是比较并更新,简单来说:从某一内存上取值V,和预期值A进行比较,如果内存值V和预期值A的结果相等,那么我们就把新值B更新到内存,如果不相等,那么就重复上述操作直到成功为止。java中的
Atomic 系列就是使用cas实现的

volatile只用来保证变量可见性,但不保证原子性

Atomic:性能高,轻量,存在ABA问题可以通过添加版本号解决,线程安全

** VOLATILE**
是JAVA中一个极其重要关键字,它保证的内存的可见性,但是并不能够保证原子性。而CAS是采用一种无锁的方式,解决VOLATILE所不能带来的原子性等这类问题。接下来,就讲讲VOLATILE与CAS吧!

2.Atomic 原子操作*

关于atomic*原子操作,这里以AtomicInteger类为例

使用场景:

AtomicInteger,一个提供原子操作的Integer的类。在Java语言中,++i和i++操作并不是线程安全的,在使用的时候,

不可避免的会用到synchronized关键字。而AtomicInteger则通过一种线程安全的加减操作接口。

ABA

A-> B ->A 过程有改变但是 compareandset不知道

$反射原理和作用

JAVA语言编译之后会生成一个.claSs 文件,反射就是通过字节码文件找到某一个类、类中

的方法以及属性等。反射的实现主要借助以下四个类:Class:类的对象,Constructor:类的构

造方法,Field:类中的属性对象,Method:类中的方法对象。

作用:反射机制指的是程序在运行时能够获取自身的信息。在JAVA中,只要给定类的名字,那么就可以通过反射机制来获取类的所有信息。

$JVM内存分配

(1)堆内存分配

JVM初始分配的内存由-Xms 指定,默认是物理内存的1/64;JVM 最大分配的内存由-Xmx 指

定,默认是物理内存的1/4。默认空余堆内存小于 40%时,JVM就会增大堆直到-Xmx 的最大限制;

空余堆内存大于 70%时,JVM会减少堆直到-Xms 的最小限制。因此服务器一般设置-XmS、-Xmx

相等以避免在每次GC后调整堆的大小。

(2)非堆内存分配

JVM使用-XX:PermSize 设置非堆内存初始值,默认是物理内存的 1/64;由 XX:MaxPermSize

设置最大非堆内存的大小,默认是物理内存的1/4。

$GC算法

①GC(GarbageCollection 垃圾收集),GC 的对象是堆空间和永久区

②GC 算法包含:引用计数法,标记清除,标记压缩,复制算法。

J2EE

过滤器(Filter)的工作原理

一、Filter简介

Filter也称之为过滤器,它是Servlet技术中最激动人心的技术之一,WEB开发人员通过Filter技术,对web服务器管理的所有web资源:例如Jsp,

Servlet, 静态图片文件或静态html文件等进行拦截,从而实现一些特殊的功能。例如实现URL级别的权限访问控制、过滤敏感词汇、压缩响应信息等

一些高级功能。

Servlet
API中提供了一个Filter接口,开发web应用时,如果编写的Java类实现了这个接口,则把这个java类称之为过滤器Filter。通过Filter

技术,开发人员可以实现用户在访问某个目标资源之前,对访问的请求和响应进行拦截,Filter接口源代码:

public abstract interface Filter{

public abstract void init(FilterConfig paramFilterConfig) throws
ServletException;

public abstract void doFilter(ServletRequest paramServletRequest,
ServletResponse paramServletResponse, FilterChain

paramFilterChain) throws IOException, ServletException;

public abstract void destroy();

}

二、Filter是如何实现拦截的?(Filter的工作原理)

Filter接口中有一个doFilter方法,当我们编写好Filter,并配置对哪个web资源进行拦截后,WEB服务器每次在调用web资源的service方法之前,

都会先调用一下filter的doFilter方法,因此,在该方法内编写代码可达到如下目的:

调用目标资源之前,让一段代码执行。

是否调用目标资源(即是否让用户访问web资源)。

调用目标资源之后,让一段代码执行。

web服务器在调用doFilter方法时,会传递一个filterChain对象进来,filterChain对象是filter接口中最重要的一个对象,它也提供了一个

doFilter方法,开发人员可以根据需求决定是否调用此方法,调用该方法,则web服务器就会调用web资源的service方法,即web资源就会被访问,

否则web资源不会被访问。

3.2、 Filter链

在一个web应用中,可以开发编写多个Filter,这些Filter组合起来称之为一个Filter链。web服务器根据Filter在web.xml文件中的注册顺序,

决定先调用哪个Filter,当第一个Filter的doFilter方法被调用时,web服务器会创建一个代表Filter链的FilterChain对象传递给该方法。在doFilter

方法中,开发人员如果调用了FilterChain对象的doFilter方法,则web服务器会检查FilterChain对象中是否还有filter,如果有,则调用第2个filter,

如果没有,则调用目标资源。

四,S pring框架下,过滤器的配置

如果项目中使用了Spring框架,那么,很多过滤器都不用自己来写了,Spring为我们写好了一些常用的过滤器。下面我们就以字符编码的

过滤器CharacterEncodingFilter为例,只需要配置加上该过滤器就能使用。

如果我们不使用Spring的CharacterEncodingFilter类,可以自己来写。

GenericFilterBean类:

public abstract class GenericFilterBean implements Filter, BeanNameAware,
ServletContextAware, InitializingBean, DisposableBean

还没有过瘾,那就再看一个项目中使用过的一个过滤器:InvilidCharacterFilter(防止脚本攻击的过滤器)

五、Filter的生命周期

5.1、Filter的创建

Filter的创建和销毁由web服务器负责。 web应用程序启动时,web服务器将创建Filter的实例对象,并调用其init方法,完成对象的初始化

功能,从而为后续的用户请求作好拦截的准备工作,filter对象只会创建一次,init方法也只会执行一次。通过init方法的参数,可获得代表当前

filter配置信息的FilterConfig对象。

5.2、Filter的销毁

web容器调用destroy方法销毁Filter。destroy方法在Filter的生命周期中仅执行一次。在destroy方法中,可以释放过滤器使用的资源。

5.3、FilterConfig接口

用户在配置filter时,可以使用为filter配置一些初始化参数,当web容器实例化Filter对象,调用其init方法时,会把封装了

filter初始化参数的filterConfig对象传递进来。因此开发人员在编写filter时,通过filterConfig对象的方法,就可获得:

String getFilterName():得到filter的名称。

String getInitParameter(String name): 返回在部署描述中指定名称的初始化参数的值。如果不存在返回null.

Enumeration getInitParameterNames():返回过滤器的所有初始化参数的名字的枚举集合。

public ServletContext getServletContext():返回Servlet上下文对象的引用。

六、Filter的部署时的一些参数的含义

Filter的部署分为两个步骤:

1、注册Filter

2、映射Filter

6.1、注册Filter

开发好Filter之后,需要在web.xml文件中进行注册,这样才能够被web服务器调用。在web.xml文件中注册Filter范例:

用于添加描述信息,该元素的内容可为空,可以不配置。

用于为过滤器指定一个名字,该元素的内容不能为空。

元素用于指定过滤器的完整的限定类名。

元素用于为过滤器指定初始化参数,它的子元素指定参数的名字,指定参数的值。在过滤器中,

可以使用FilterConfig接口对象来访问初始化参数。如果过滤器不需要指定初始化参数,那么元素可以不配置。

6.2、映射Filter

在web.xml文件中注册了Filter之后,还要在web.xml文件中映射Filter

FilterTest

/*

元素用于设置一个 Filter 所负责拦截的资源。一个Filter拦截的资源可通过两种方式来指定:Servlet
名称和资源访问的请求路径

子元素用于设置filter的注册名称。该值必须是在元素中声明过的过滤器的名字

设置 filter 所拦截的请求路径(过滤器关联的URL样式)

指定过滤器所拦截的Servlet名称。

指定过滤器所拦截的资源被 Servlet
容器调用的方式,可以是REQUEST,INCLUDE,FORWARD和ERROR之一,默认REQUEST。用户可以设置多个
子元素用来指定 Filter 对资源的多种调用方式进行拦截。如下:

testFilter

/index.jsp

REQUEST

FORWARD

子元素可以设置的值及其意义:

REQUEST:当用户直接访问页面时,Web容器将会调用过滤器。如果目标资源是通过RequestDispatcher的include()或forward()方法访问

时,那么该过滤器就不会被调用。

INCLUDE:如果目标资源是通过RequestDispatcher的include()方法访问时,那么该过滤器将被调用。除此之外,该过滤器不会被调用。

FORWARD:如果目标资源是通过RequestDispatcher的forward()方法访问时,那么该过滤器将被调用,除此之外,该过滤器不会被调用。

ERROR:如果目标资源是通过声明式异常处理机制调用时,那么该过滤器将被调用。除此之外,过滤器不会被调用。

Spring

$依赖注入 IOC方式

set 构造器 接口

IOC(控制反转)就是 依赖倒置原则 的一种代码设计思路。 就是把原先在代码里面需要实现的对象创建、对象之间的依赖,反转给容器来帮忙实现
Spring IOC容器通过xml,注解等其它方式配置类及类之间的依赖关系,完成了对象的创建和依赖的管理注入。实现IOC的主要设计模式是 工厂模式

使用IOC的好处

  • 集中管理,实现类的可配置和易管理。

  • 降低了类与类之间的耦合度。

a setter

原理 :
在目标对象中,定义需要注入的依赖对象对应的属性和setter方法;“让ioc容器调用该setter方法”,将ioc容器实例化的依赖对象通过setter注入给目标对象,封装在目标对象的属性中。

b 构造器

原理 :
为目标对象提供一个构造方法,在构造方法中添加一个依赖对象对应的参数。ioc容器解析时,实例化目标对象时会自动调用构造方法,ioc只需要为构造器中的参数进行赋值;将ioc实例化的依赖对象作为构造器的参数传入。

【接口注入

原理 : 为依赖对象提供一个接口实现,将接口注入给目标对象,实现将接口的实现类注入的效果。比如HttpServletRequest
HttpServletResponse接口

注意:这是ioc提供的方式,spring中的ioc技术并没有实现该种注入方式】

c 方法注入

原理 :
在目标对象中定义一个普通的方法,将方法的返回值设置为需要注入的依赖对象类型。通过ioc容器调用该方法,将其创建的依赖对象作为方法的返回值返回给目标对象。

$$spring core与context理解

Spring core是核心层,拥有这BeanFactory这个强大的工厂,是所有bean的管理器;

而spring context是上下文运行环境,基于spring core之上的一个架构, 它之上是spring
web,这下明白了吧,主要应用就是web的一个初始化上下文环境;

Spring框架由7个定义良好的模块(组件)组成,各个模块可以独立存在,也可以联合使用。

(1)Spring
Core:核心容器提供了Spring的基本功能。核心容器的核心功能是用Ioc容器来管理类的依赖关系.Spring采用的模式是调用者不理会被调用者的实例的创建,由Spring容器负责被调用者实例的创建和维护,需要时注入给调用者。这是目前最优秀的解耦模式。

(2)Spring AOP:Spring的AOP模块提供了面向切面编程的支持。SpringAOP采用的是纯Java实现。Spring
AOP采用基于代理的AOP实现方案,AOP代理由Ioc容器负责生成、管理,依赖关系也一并由Ioc容器管理,尽管如此,Spring
Ioc容器并不依赖于AOP,这样我们可以自由选择是否使用AOP。

AOP(面向切面)是一种编程范式,提供从另一个角度来考虑程序结构以完善面向对象编程(OOP)。
AOP为开发者提供了一种描述横切关注点的机制,并能够自动将横切关注点织入到面向对象的软件系统中,从而实现了横切关注点的模块化。
AOP能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任,例如事务处理、日志管理、权限控制等,封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。

使用AOP的好处

  • 降低模块的耦合度

  • 使系统容易扩展

  • 提高代码复用性

(3)Spring ORM:提供了与多个第三方持久层框架的良好整合。

(4)Spring DAO:
Spring进一步简化DAO开发步骤,能以一致的方式使用数据库访问技术,用统一的方式调用事务管理,避免具体的实现侵入业务逻辑层的代码中。

(5)Spring
Context:它是一个配置文件,为Spring提供上下文信息,提供了框架式的对象访问方法。Context为Spring提供了一些服务支持,如对国际化(i18n)、电子邮件、校验和调度功能。

(6)Spring Web:提供了基础的针对Web开发的集成特性,例如多方文件上传,利用Servlet
listeners进行IoC容器初始化和针对Web的applicationContext.

(7)Spring
MVC:提供了Web应用的MVC实现。Spring的MVC框架并不是仅仅提供一种传统的实现,它提供了一种清晰的分离模型,在领域模型代码和web
form之间。并且,还可以借助Spring框架的其他特性

$Spring MVC原理

Dispatcher调度员

①客户端的所有请求都交给前端控制器DispatcherServlet 来处理,它会负责调用系统的其他模

块来真正处理用户的请求。

②DispatcherServlet 收到请求后,将根据请求的信息(包括URL、HTTP协议方法、请求头、

请求参数、Cookie 等)以及HandlerMapping 的配置找到处理该请求的 Handler(任何一个对象

都可以作为请求的Handler)。

③在这个地方 Spring 会通过HandlerAdapter 对该处理器进行封装。

④HandlerAdapter 是一个适配器,它用统一的接口对各种Handler 中的方法进行调用。

5Handler完成对用户请求的处理后,会返回一个ModelAndView对象给 DispatcherServlet,

ModelAndView顾名思义,包含了数据模型以及相应的视图的信息。

③ModelAndView 的视图是逻辑视图,DispatcherServlet 还要借助ViewResolver 完成从逻辑视

图到真实视图对象的解析工作。

当得到真正的视图对象后,DispatcherServlet会利用视图对象对模型数据进行染。

③客户端得到响应,可能是一个普通的 HTML 页面,也可以是 XML或 JSON 字符串,还可以是一

张图片或者一个PDF文件。

EJB

既然说了EJB 是为了"服务集群"和"企业级开发",那么,总得说说什么是所谓的"服务

集群"和"企业级开发"吧!

J2EE 对于这个问题的处理方法是将业务逻辑从客户端软件中抽取出来,封装在一个组

件中。这个组件运行在一个独立的服务器上,客户端软件通过网络调用组件提供的服务以实

现业务逻辑,而客户端软件的功能单纯到只负责发送调用请求和显示处理结果。在J2EE 中,

这个运行在一个独立的服务器上,并封装了业务逻辑的组件就是EJB(Enterprise Java

Bean)组件。

JSP内置对象及方法

JSP一共有9个内置对象:request、response、session、application、out、pagecontext、config、page、exception。

  1. request对象。

resquest对象是javax.servlet.http.HttpServletRequest类的一个实例。客户端的请求信息封装在resquest中发送给服务器端。request的作用域是一次请求。

请求方式:request.getMethod()

请求的资源:request.getRequestURI()

请求用的协议:request.getProtocol()

请求的文件名:request.getServletPath()

请求的服务器的IP:request.getServerName()

请求服务器的端口:request.getServerPort()

客户端IP地址:request.getRemoteAddr()

客户端主机名:request.getRemoteHost()

  1. response对象。

response对象是javax.servlet.http.HttpServletResponse的一个实例。服务端
的相应信息封装在response中返回。

重定向客户端请求 response.sendRedirect(index.jsp)

  1. session对象。

session对象是javax.servlet.http.HttpSession的一个实例。在第一个JSP页面被装载时自动创建,完成会话期管理。

session对象内部使用Map类来保存数据,因此保存数据的格式为 “Key/value”。

获取Session对象编号 session.getId()

添加obj到Session对象 session.setAttribute(String key,Object obj)

获取Session值 session.getAttribute(String key)

  1. application对象。

application对象是javax.servlet.ServletContext的一个实例。

实现了用户间数据的共享,可存放全局变量。它开始于服务器的启动,直到服务器的关闭,在此期间,此对象将一直存在。

添加obj到Application对象 application.setAttribute(String key,Object obj)

获取Application对象中的值 application.getAttribute(String key)

  1. out 对象。

out对象是javax.servlet.jsp.jspWriter的一个实例。用于浏览器输出数据。

输出各种类型数据 out.print()

输出一个换行符 out.newLine()

关闭流 out.close()

  1. pageContext 对象。

pageContext 对象是javax.servlet.jsp.PageContext的一个对象。作用是取得任何范围的参数,通过它可以获取
JSP页面的out、request、reponse、session、application 等对象。

  1. config 对象。

config 对象是javax.servlet.ServletConfig的一个对象。主要作用是取得服务器的配置信息。通过 pageConext对象的
getServletConfig() 方法可以获取一个config对象。

  1. cookie 对象。

cookie 对象是Web服务器保存在用户硬盘上的一段文本。唯一的记录了用户的访问信息。

将Cookie对象传送到客户端 Cookie c = new Cookie(username",john");

读取保存到客户端的Cookie response.addCookie©

  1. exception 对象。

exception 对象的作用是显示异常信息,只有在包含 isErrorPage=“true” 的页面中才可以被使用。

forword与redirect的区别

1.从[地址栏](https://www.baidu.com/s?wd=%E5%9C%B0%E5%9D%80%E6%A0%8F&tn=44039180_cpr&fenlei=mv6quAkxTZn0IZRqIHckPjm4nH00T1d9Ph7bPyDvuADzmHNBPv7-0ZwV5Hcvrjm3rH6sPfKWUMw85HfYnjn4nH6sgvPsT6KdThsqpZwYTjCEQLGCpyw9Uz4Bmy-
bIi4WUvYETgN-TLwGUv3En1R1nW6snj6z)显示来说
forward是服务器请求资源,服务器直接访问目标地址的[URL](https://www.baidu.com/s?wd=URL&tn=44039180_cpr&fenlei=mv6quAkxTZn0IZRqIHckPjm4nH00T1d9Ph7bPyDvuADzmHNBPv7-0ZwV5Hcvrjm3rH6sPfKWUMw85HfYnjn4nH6sgvPsT6KdThsqpZwYTjCEQLGCpyw9Uz4Bmy-
bIi4WUvYETgN-
TLwGUv3En1R1nW6snj6z),把那个[URL](https://www.baidu.com/s?wd=URL&tn=44039180_cpr&fenlei=mv6quAkxTZn0IZRqIHckPjm4nH00T1d9Ph7bPyDvuADzmHNBPv7-0ZwV5Hcvrjm3rH6sPfKWUMw85HfYnjn4nH6sgvPsT6KdThsqpZwYTjCEQLGCpyw9Uz4Bmy-
bIi4WUvYETgN-
TLwGUv3En1R1nW6snj6z)的响应内容读取过来,然后把这些内容再发给[浏览器](https://www.baidu.com/s?wd=%E6%B5%8F%E8%A7%88%E5%99%A8&tn=44039180_cpr&fenlei=mv6quAkxTZn0IZRqIHckPjm4nH00T1d9Ph7bPyDvuADzmHNBPv7-0ZwV5Hcvrjm3rH6sPfKWUMw85HfYnjn4nH6sgvPsT6KdThsqpZwYTjCEQLGCpyw9Uz4Bmy-
bIi4WUvYETgN-
TLwGUv3En1R1nW6snj6z).[浏览器](https://www.baidu.com/s?wd=%E6%B5%8F%E8%A7%88%E5%99%A8&tn=44039180_cpr&fenlei=mv6quAkxTZn0IZRqIHckPjm4nH00T1d9Ph7bPyDvuADzmHNBPv7-0ZwV5Hcvrjm3rH6sPfKWUMw85HfYnjn4nH6sgvPsT6KdThsqpZwYTjCEQLGCpyw9Uz4Bmy-
bIi4WUvYETgN-
TLwGUv3En1R1nW6snj6z)根本不知道服务器发送的内容从哪里来的,所以它的[地址栏](https://www.baidu.com/s?wd=%E5%9C%B0%E5%9D%80%E6%A0%8F&tn=44039180_cpr&fenlei=mv6quAkxTZn0IZRqIHckPjm4nH00T1d9Ph7bPyDvuADzmHNBPv7-0ZwV5Hcvrjm3rH6sPfKWUMw85HfYnjn4nH6sgvPsT6KdThsqpZwYTjCEQLGCpyw9Uz4Bmy-
bIi4WUvYETgN-TLwGUv3En1R1nW6snj6z)还是原来的地址.
redirect是[服务端](https://www.baidu.com/s?wd=%E6%9C%8D%E5%8A%A1%E7%AB%AF&tn=44039180_cpr&fenlei=mv6quAkxTZn0IZRqIHckPjm4nH00T1d9Ph7bPyDvuADzmHNBPv7-0ZwV5Hcvrjm3rH6sPfKWUMw85HfYnjn4nH6sgvPsT6KdThsqpZwYTjCEQLGCpyw9Uz4Bmy-
bIi4WUvYETgN-
TLwGUv3En1R1nW6snj6z)根据逻辑,发送一个状态码,告诉[浏览器](https://www.baidu.com/s?wd=%E6%B5%8F%E8%A7%88%E5%99%A8&tn=44039180_cpr&fenlei=mv6quAkxTZn0IZRqIHckPjm4nH00T1d9Ph7bPyDvuADzmHNBPv7-0ZwV5Hcvrjm3rH6sPfKWUMw85HfYnjn4nH6sgvPsT6KdThsqpZwYTjCEQLGCpyw9Uz4Bmy-
bIi4WUvYETgN-
TLwGUv3En1R1nW6snj6z)重新去请求那个地址.所以[地址栏](https://www.baidu.com/s?wd=%E5%9C%B0%E5%9D%80%E6%A0%8F&tn=44039180_cpr&fenlei=mv6quAkxTZn0IZRqIHckPjm4nH00T1d9Ph7bPyDvuADzmHNBPv7-0ZwV5Hcvrjm3rH6sPfKWUMw85HfYnjn4nH6sgvPsT6KdThsqpZwYTjCEQLGCpyw9Uz4Bmy-
bIi4WUvYETgN-
TLwGUv3En1R1nW6snj6z)显示的是新的[URL](https://www.baidu.com/s?wd=URL&tn=44039180_cpr&fenlei=mv6quAkxTZn0IZRqIHckPjm4nH00T1d9Ph7bPyDvuADzmHNBPv7-0ZwV5Hcvrjm3rH6sPfKWUMw85HfYnjn4nH6sgvPsT6KdThsqpZwYTjCEQLGCpyw9Uz4Bmy-
bIi4WUvYETgN-TLwGUv3En1R1nW6snj6z).

2.从[数据共享](https://www.baidu.com/s?wd=%E6%95%B0%E6%8D%AE%E5%85%B1%E4%BA%AB&tn=44039180_cpr&fenlei=mv6quAkxTZn0IZRqIHckPjm4nH00T1d9Ph7bPyDvuADzmHNBPv7-0ZwV5Hcvrjm3rH6sPfKWUMw85HfYnjn4nH6sgvPsT6KdThsqpZwYTjCEQLGCpyw9Uz4Bmy-
bIi4WUvYETgN-TLwGUv3En1R1nW6snj6z)来说 forward:转发页面和转发到的页面可以共享request里面的数据.
redirect:不能共享数据.

3.从运用地方来说 forward:一般用于用户登陆的时候,根据角色转发到相应的模块.
redirect:一般用于用户注销登陆时返回主页面和跳转到其它的网站等. 4.从效率来说 forward:高. redirect:低.

redis

快:利用内存 缓存。 单线程多路复用IO CPU核心数不是瓶颈

$redis的list实现消息队列

Redis 中list 的数据结构实现是双向链表,所以可以非常便捷的应用于消息队列生产者/

消费者模型)。消息的生产者只需要通过lpush将消息放入list,消费者便可以通过 rpop取出

该消息,并且可以保证消息的有序性。如果需要实现带有优先级的消息队列也可以选择 sorted

set。而 pub/sub功能也可以用作发布者 /订阅者模型的消息。

redis的AOF和RDB区别

RDB 的优点:

这种文件非常适合用于进行备份: 比如说,你可以在最近的 24 小时内,每小时备份一次 RDB 文件,并且在每个月的每一天,也备份一个 RDB 文件。
这样的话,即使遇上问题,也可以随时将数据集还原到不同的版本。 RDB 非常适用于灾难恢复 (disaster recovery)。

RDB 的缺点:

如果你需要尽量避免在服务器故障时丢失数据那么 RDB 不适合你 。 虽然 Redis 允许你设置不同的保存点(save
point)来控制保存 RDB 文件的频率, 但是, 因为RDB 文件需要保存整个数据集的状态, 所以它并不是一个轻松的操作。 因此你可能会至少 5
分钟才保存一次 RDB 文件。 在这种情况下, 一旦发生故障停机, 你就可能会丢失好几分钟的数据

AOF 的优点

使用 AOF 持久化会让 Redis 变得非常耐久(much more durable): 你可以设置不同的 fsync 策略 ,比如无 fsync
,每秒钟一次 fsync ,或者每次执行写入命令时 fsync 。 AOF 的默认策略为每秒钟 fsync 一次 ,在这种配置下,Redis
仍然可以保持良好的性能,并且就算发生故障停机,也 最多只会丢失一秒钟的数据 ( fsync
会在后台线程执行,所以主线程可以继续努力地处理命令请求)。

AOF 的缺点

对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。 根据所使用的 fsync 策略,AOF 的速度可能会慢于 RDB
在一般情况下, 每秒 fsync 的性能依然非常高 , 而关闭 fsync 可以让 AOF 的速度和 RDB 一样快, 即使在高负荷之下也是如此。
不过在处理巨大的写入载入时,RDB 可以提供更有保证的最大延迟时间(latency)。

二者的区别

RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。

AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。

RDB 和 AOF ,我应该用哪一个?

  • 如果你非常关心你的数据,但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久。

  • AOF 将 Redis 执行的每一条命令追加到磁盘中,处理巨大的写入会降低 Redis 的性能,不知道你是否可以接受。

数据库备份和灾难恢复 :定时生成 RDB 快照(snapshot)非常便于进行数据库备份, 并且 RDB 恢复数据集的速度也要比 AOF
恢复的速度要快。

Redis 支持同时开启 RDB 和 AOF,系统重启后,Redis 会优先使用 AOF 来恢复数据,这样丢失的数据会最少。

JSTL

JSTL 是JSP的标准标签库

JSP的标签集合 ,按照类别包括

核心标签,格式化标签,JSTL函数,SQL标签和XML标签

,其中前三个用的概率较高。要想使用JSTL标签库我们首先要做的就是引入对应的Jar包【standard.jar和jstl.jar】。有时候我们在jsp页面上面要嵌套大量的Java代码,但是又要在页面上进行源码的编写,复杂且难以维护,所以我们就可以利用我们的JSTL标签库进行解决这个问题。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lvUaxwzc-1681014581780)(【精】各大厂问题汇总_files/Image [2].png)]![](【精】各大厂问题汇总_files/Image
[3].png)[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mkFf8d7h-1681014581780)(【精】各大厂问题汇总_files/Image [4].png)]

EL

EL是JSP的表达式语言

,EL表达式使我们在访问JavaBean中的数据非常简单,EL
表达式语法为【${expr}】,在jsp页面中,常用于获取后台传递的数据。通常情况下,我们将JSTL标签库与EL表达式进行结合使用,能很方便的进行数据的展示。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CRHgVoiD-1681014581781)(【精】各大厂问题汇总_files/Image [5].png)]

对NuLL的判断

Empty 对于 null 和”” 都会返回true

== null 则是对null 返回true 而对”” 则是返回false

Not empty 不等于空,包括不等于null 和不等于””

null }”>

// 判断对象是否为空对象

$GET和POST

①get请求用来从服务器上获得资源,而post 是用来向服务器提交数据;

②get将表单中数据按照 name=value 的形式,添加到 action 所指向的 URL 后面,并且两者使

用"?“连接,而各个变量之间使用”&"连接;post 是将表单中的数据放在 HTTP 协议的请求头或消

息体中,传递到action所指向URL:

③get 传输的数据要受到URL长度限制(1024字节);而 post 可以传输大量的数据,上传文件

通常要使用 post方式;

④使用 get 时参数会显示在地址栏上,如果这些数据不是敏感数据,那么可以使用 get;对于敏

感数据还是应用使用post;

③get 使用 MIME类型 application/x-www-form-urlencoded 的 URL编码(也叫百分号编码)文

本的格式传递参数,保证被传送的参数由遵循规范的文本组成,例如一个空格的编码是"%20”。

Cookies和session

l、cookie 数据存放在客户的浏览器上,session数据放在服务器上。

2、cookie 不是很安全,别人可以分析存放在本地的 COOKIE 并进行 COOKIE 欺骗

考虑到安全应当使用 session。

3、session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,

考虑到减轻服务器性能方面,应当使用COOKIE。

4、单个 cookie 保存的数据不能超过 4K,很多浏览器都限制一个站点最多保存 20 个 cookie。

$大型网站在架构上应该考虑问题

一分层 :分层是处理任何复杂系统最常见的手段之一,将系统横尚切分成若千个层面,每个

层面只承担单一的职责,然后通过下层为上层提供的基础设施和服务以及上层对下层的调用来形

成一个完整的复杂的系统。计算机网络的开放系统互联参考模型(OSI/RM)和Internet 的TCP/IF

模型都是分层结构,大型网站的软件系统也可以使用分层的理念将其分为持久层(提供数据存储

和访问服务)、业务层(处理业务逻辑,系统中最核心的部分)和表示层(系统交互、视图展示)。

需要指出的是:(1)分层是逻辑上的划分,在物理上可以位于同一设备上也可以在不同的设备

上部署不同的功能模块,这样可以使用更多的计算资源来应对用户的并发访问;(2)层与层之

间应当有清晰的边界,这样分层才有意义,才更利于软件的开发和维护。

一分割 :分割是对软件的纵尚切分。我们可以将天型网站的不同功能和服务分割开,形成高内聚

低耦合的功能模块(单元)。在设计初期可以做一个粗粒度的分割,将网站分割为若干个功能模

块,后期还可以进一步对每个模块进行细粒度的分割,这样一方面有助于软件的开发和维护,另

一方面有助于分布式的部署,提供网站的并发处理能力和功能的扩展。

分布式 :除了上面提到的内容,网站的静态资源(JavaScript、CsS、图片等)也可以采用独

立分布式部署并采用独立的域名,这样可以减轻应用服务器的负载压力,也使得浏览器对资源的

加载更快。数据的存取也应该是分布式的,传统的商业级关系型数据库产品基本上都支持分布式

部署,而新生的NoSQL产品儿乎都是分布式的。当然,网站后台的业务处理也要使用分布式技术,

例如查询索引的构建、数据分析等,这些业务计算规模庞大,可以使用Hadoop以及MapReduce

分布式计算架来处理。

一集群 :集群使得有更多的服务器提供相同的服务,可以更好的提供对并发的支持。

一缓存 :所谓缓存就是用空间换取时间的技术,将数据尽可能放在距离计算最近的位置。使用缓

存是网站优化的第一定律。我们通常说的CDN、反向代理、热点数据都是对缓存技术的使用。

一异步 :异步是实现软件实体之间解耦合的义一重要手段。异步架构是典型的生产者消费者模式

二者之间没有直接的调用关系,只要保持数据结构不变,彼此功能实现可以随意变化而不互相影

响,这对网站的扩展非常有利。使用异步处理还可以提高系统可用性,加快网站的响应速度(用

Ajax加载数据就是一种异步技术),同时还可以起到削峰作用(应对瞬时高并发)。";能

推迟处理的者都要推迟处理”是网站优化的第二定律,而异步是践行网站优化第二定律的重要手段。

一亢余 :各种服务器都要提供相应的亢余服务器以便在某台或某些服务器岩机时还能保证网站可

以正常工作,同时也提供了灾难恢复的可能性。亢余是网站高可用性的重要保证。

$J2EE中常用的名词解释

  1. web容器 :给处于其中的应用程序组件(JSP,SERVLET)提供一个环境,使JSP,SERVLET直接和容器中的环境变量接接口互,不必关注其它系统问题。主要有WEB服务器来实现。例如:TOMCAT,WEBLOGIC,WEBSPHERE等。该容器提供的接口严格遵守J2EE规范中的WEB APPLICATION 标准。我们把遵守以上标准的WEB服务器就叫做J2EE中的WEB容器。

  2. Web container :实现J2EE体系结构中Web组件协议的容器。这个协议规定了一个Web组件运行时的环境,包括安全,一致性,生命周期管理,事务,配置和其它的服务。一个提供和JSP和J2EE平台APIs界面相同服务的容器。一个Web container 由Web服务器或者J2EE服务器提供。

  3. EJB容器 :Enterprise java bean 容器。更具有行业领域特色。他提供给运行在其中的组件EJB各种管理功能。只要满足J2EE规范的EJB放入该容器,马上就会被容器进行高效率的管理。并且可以通过现成的接口来获得系统级别的服务。例如邮件服务、事务管理。一个实现了J2EE体系结构中EJB组件规范的容器。 这个规范指定了一个Enterprise bean的运行时环境,包括安全,一致性,生命周期,事务, 配置,和其他的服务。

  4. JNDI :(Java Naming & Directory Interface)JAVA命名目录服务。主要提供的功能是:提供一个目录系统,让其它各地的应用程序在其上面留下自己的索引,从而满足快速查找和定位分布式应用程序的功能。

  5. JMS :(Java Message Service)JAVA消息服务。主要实现各个应用程序之间的通讯。包括点对点和广播。

  6. JTA :(Java Transaction API)JAVA事务服务。提供各种分布式事务服务。应用程序只需调用其提供的接口即可。

JPA(java persistence API)

JPA 通过JDK5.0的注解或XML来描述 对象-关系表的映射关系,并 将运行期的实体对象持久化存储到数据库中

  1. JAF :(Java Action FrameWork)JAVA安全认证框架。提供一些安全控制方面的框架。让开发者通过各种部署和自定义实现自己的个性安全控制策略。

  2. RMI/IIOP :(Remote Method Invocation /internet对象请求中介协议)他们主要用于通过远程调用服务。例如,远程有一台计算机上运行一个程序,它提供股票分析服务,我们可以在本地计算机上实现对其直接调用。当然这是要通过一定的规范才能在异构的系统之间进行通信。RMI是JAVA特有的。RMI-IIOP出现以前,只有RMI和CORBA两种选择来进行分布式程序设计。RMI-IIOP综合了RMI和CORBA的优点,克服了他们的缺点,使得程序员能更方便的编写分布式程序设计,实现分布式计算。首先,RMI-IIOP综合了RMI的简单性和CORBA的多语言性(兼容性),其次RMI-IIOP克服了RMI只能用于Java的缺点和CORBA的复杂性(可以不用掌握IDL)。

数据库

Java数据库连接池实现原理

一般来说,Java应用程序访问数据库的过程是: ①装载数据库驱动程序; ②通过jdbc建立数据库连接; ③访问数据库,执行sql语句; ④断开数据库连接。

程序开发过程中,存在很多问题:首先,每一次web请求都要建立一次数据库连接。建立连接是一个费时的活动,每次都得花费0.05s~1s的时间,而且系统还要分配内存资源。这个时间对于一次或几次数据库操作,或许感觉不出系统有多大的开销。

可是对于现在的web应用,尤其是大型电子商务网站,同时有几百人甚至几千人在线是很正常的事。在这种情况下,频繁的进行数据库连接操作势必占用很多的系统资源,网站的响应速度必定下降,严重的甚至会造成服务器的崩溃。不是危言耸听,这就是制约某些电子商务网站发展的技术瓶颈问题。其次,对于每一次数据库连接,使用完后都得断开。否则,如果程序出现异常而未能关闭,将会导致数据库系统中的内存泄漏,最终将不得不重启数据库

通过上面的分析,我们可以看出来,“数据库连接”是一种稀缺的资源,为了保障网站的正常使用,应该对其进行妥善管理。其实我们查询完数据库后,如果不关闭连接,而是暂时存放起来,当别人使用时,把这个连接给他们使用。就避免了一次建立数据库连接和断开的操作时间消耗。

数据库连接池的基本思想:就是为数据库连接建立一个“缓冲池”。预先在缓冲池中放入一定数量的连接,当需要建立数据库连接时,只需从“缓冲池”中取出一个,使用完毕之后再放回去。我们可以通过设定连接池最大连接数来防止系统无尽的与数据库连接

创建数据库连接池大概有3个步骤:

① 创建ConnectionPool实例,并初始化创建10个连接,保存在Vector中(线程安全)

② 实现getConnection()从连接库中获取一个可用的连接

③ returnConnection(conn) 提供将连接放回连接池中方法

Innodb中的行锁与表锁

InnoDB,是MySQL的数据库引擎之一,现为MySQL的默认存储引擎

前面提到过,在Innodb引擎中既支持行锁也支持表锁,那么什么时候会锁住整张表,什么时候或只锁住一行呢?

InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点MySQL与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁!

在实际应用中,要特别注意InnoDB行锁的这一特性,不然的话,可能导致大量的锁冲突,从而影响并发性能。

行级锁都是基于索引的,如果一条SQL语句用不到索引是不会使用行级锁的,会使用表级锁。行级锁的缺点是:由于需要请求大量的锁资源,所以速度慢,内存消耗大。

$数据库索引底层实现,什么时候失效

B+树

没有遵循最左匹配原则

一些关键字会导致索引失效,例如or,

! = , not in, is null ,is not unll

like 查询是以%开头

隐式转换会导致索引失效。

对索引应用内部函数,索引字段进行了运算。

$数据库设计三范式

1NF:字段不可分;

2NF:有主键,非主键字段依赖主键;

3NF:非主键字段不能相互依赖;

解释:

1NF: 原子性 字段不可再分,否则就不是关系数据库;

2NF: 唯一性 一个表只说明一个事物;

3NF:每列都与主键有 直接关系 ,不存在传递依赖;

数据库事务ACID特性

ACID,是指在可靠数据库管理系统(DBMS)中,事务(transaction)所应该具有的四个特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)

原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。

一致性指事务前后数据的完整性必须保持一致。

隔离性指多个用户并发访问数据库时,一个用户的事务不能被其他用户的事务所干扰,多个并发事务之间数据要相互隔离。

持久性是指一人事务一旦提交,它对数据库中数据的改变就是永久性的,即使数据库发生故障也不应该对其有任何影响。

MYSQL数据库的主从复制

0、为什么需要主从复制?

1、在业务复杂的系统中,有这么一个情景,有一句sql语句需要锁表,导致暂时不能使用读的服务,那么就很影响运行中的业务,使用主从复制,让主库负责写,从库负责读,这样,即使主库出现了锁表的情景,通过读从库也可以保证业务的正常运作。

2、做数据的热备

3、架构的扩展。业务量越来越大,I/O访问频率过高,单机无法满足,此时做多库的存储,降低磁盘I/O访问的频率,提高单个机器的I/O性能。

1、什么是mysql的主从复制?

MySQL 主从复制是指数据可以从一个MySQL数据库服务器主节点复制到一个或多个从节点。MySQL
默认采用异步复制方式,这样从节点不用一直访问主服务器来更新自己的数据,数据的更新可以在远程连接上进行,从节点可以复制主数据库中的所有数据库或者特定的数据库,或者特定的表。

2、mysql复制原理

原理:

(1)master服务器将数据的改变记录二进制binlog日志,当master上的数据发生改变时,则将其改变写入二进制日志中;

(2)slave服务器会在一定时间间隔内对master二进制日志进行探测其是否发生改变,如果发生改变,则开始一个I/OThread请求master二进制事件

(3)同时主节点为每个I/O线程启动一个dump线程,用于向其发送二进制事件,并保存至从节点本地的中继日志中,从节点将启动SQL线程从中继日志中读取二进制日志,在本地重放,使得其数据和主节点的保持一致,最后I/OThread和SQLThread将进入睡眠状态,等待下一次被唤醒。

也就是说:

  • 从库会生成两个线程,一个I/O线程,一个SQL线程;

  • I/O线程会去请求主库的binlog,并将得到的binlog写到本地的relay-log(中继日志)文件中;

  • 主库会生成一个log dump线程,用来给从库I/O线程传binlog;

  • SQL线程,会读取relay log文件中的日志,并解析成sql语句逐一执行;

注意:

1–
master将操作语句记录到binlog日志中,然后授予slave远程连接的权限(master一定要开启binlog二进制日志功能;通常为了数据安全考虑,slave也开启binlog功能)。
2–slave开启两个线程:IO线程和SQL线程。其中:IO线程负责读取master的binlog内容到中继日志relay
log里;SQL线程负责从relay
log日志里读出binlog内容,并更新到slave的数据库里,这样就能保证slave数据和master数据保持一致了。 3–
Mysql复制至少需要两个Mysql的服务,当然Mysql服务可以分布在不同的服务器上,也可以在一台服务器上启动多个服务。 4–
Mysql复制最好确保master和slave服务器上的Mysql版本相同(如果不能满足版本一致,那么要保证master主节点的版本低于slave从节点的版本)
5–master和slave两节点间时间需同步

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Nba3OiNV-1681014581782)(【精】各大厂问题汇总_files/Image.jpg)]

具体步骤:

1、从库通过手工执行change master to 语句连接主库,提供了连接的用户一切条件(user
、password、port、ip),并且让从库知道,二进制日志的起点位置(file名 position 号); start slave

2、从库的IO线程和主库的dump线程建立连接。

3、从库根据change master to 语句提供的file名和position号,IO线程向主库发起binlog的请求。

4、主库dump线程根据从库的请求,将本地binlog以events的方式发给从库IO线程。

5、从库IO线程接收binlog events,并存放到本地relay-
log中,传送过来的信息,会记录到http://master.info中

6、从库SQL线程应用relay-log,并且把应用过的记录到[http://relay-
log.info](https://link.zhihu.com/?target=http%3A//relay-
log.info)中,默认情况下,已经应用过的relay 会自动被清理purge

3、mysql主从形式

(一)一主一从

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VpLP5ZxP-1681014581783)(【精】各大厂问题汇总_files/Image [1].jpg)]

(二)主主复制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MZuNuczc-1681014581783)(【精】各大厂问题汇总_files/Image [2].jpg)]

(三)一主多从

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-R2jbLive-1681014581783)(【精】各大厂问题汇总_files/Image [3].jpg)]

(四)多主一从

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Czg6hTAK-1681014581784)(【精】各大厂问题汇总_files/Image [4].jpg)]

(五)联级复制

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZhbDF92U-1681014581784)(【精】各大厂问题汇总_files/Image [5].jpg)]

4、mysql主从同步延时分析

mysql的主从复制都是单线程的操作,主库对所有DDL和DML产生的日志写进binlog,由于binlog是顺序写,所以效率很高,slave的sql
thread线程将主库的DDL和DML操作事件在slave中重放。DML和DDL的IO操作是随机的,不是顺序,所以成本要高很多,另一方面,由于sql
thread也是单线程的,当主库的并发较高时,产生的DML数量超过slave的SQL
thread所能处理的速度,或者当slave中有大型query语句产生了锁等待,那么延时就产生了。

解决方案:

1.业务的持久化层的实现采用分库架构,mysql服务可平行扩展,分散压力。

2.单个库读写分离,一主多从,主写从读,分散压力。这样从库压力比主库高,保护主库。

3.服务的基础架构在业务和mysql之间加入memcache或者redis的cache层。降低mysql的读压力。

4.不同业务的mysql物理上放在不同机器,分散压力。

5.使用比主库更好的硬件设备作为slave,mysql压力小,延迟自然会变小。

6.使用更加强劲的硬件设备

mysql优化方法

1、选取最适用的字段属性(非null,字段长度合适,)

2、使用连接(JOIN)来代替子查询(Sub-Queries)

3、使用联合(UNION)来代替手动创建的临时表

4、事务

事物以BEGIN关键字开始,COMMIT关键字结束。在这之间的一条SQL操作失败,那么,ROLLBACK命令就可以把数据库恢复到BEGIN开始之前的状态。

BEGIN;

INSERT INTO salesinfo SET CustomerID=14;

UPDAT Einventory SET Quantity=11 WHERE item=‘book’;

COMMIT;

事务的另一个重要作用是当多个用户同时使用相同的数据源时,它可以利用锁定数据库的方法来为用户提供一种安全的访问方式,这样可以保证用户的操作不被其它的用户所干扰。

5、锁定表

包含有WRITE关键字的LOCKTABLE语句可以保证在UNLOCKTABLES命令被执行之前,不会有其它的访问来对表进行插入、更新或者删除的操作。

6、使用外键

锁定表的方法可以维护数据的完整性,但是它却不能保证数据的关联性。这个时候我们就可以使用外键。

例如,外键可以保证每一条销售记录都指向某一个存在的客户。在这里,外键可以把customerinfo表中的CustomerID映射到salesinfo表中CustomerID,任何一条没有合法CustomerID的记录都不会被更新或插入到salesinfo中。

7、使用索引

索引是提高数据库性能的常用方法,它可以令数据库服务器以比没有索引快得多的速度检索特定的行,尤其是在查询语句当中包含有MAX(),MIN()和ORDERBY这些命令的时候,性能提高更为明显。

8、优化的查询语句

本文通过8个方法优化Mysql数据库:创建索引、复合索引、索引不会包含有NULL值的列、使用短索引、排序的索引问题、like语句操作、不要在列上进行运算、不使用NOT
IN和<>操作

数据库连接池的工作原理

J2EE服务器启动时会建立一定数量的池连接,并一直维持不少于此数目的池连接。客户端程序需要连接时,池驱动程序会返回一个未使用的池连接并将其表记为忙。如果当前没有空闲连接,池驱动程序就新建一定数量的连接,新建连接的数量由配置参数决定。当使用的池连接调用完成后,池驱动程序将此连接表记为空闲,其他调用就可以使用这个连接。

SOAP和REST

wsdl(网络服务描述语言)是Web
Service的描述语言,也就是说wsdl文件是soap的使用说明书。在学习soap之前,认识WSDL是非常有必要的,只有能看懂WSDL文件,我们才可以去调用soap类型的Web服务,下面是一个非常简单的wsdl文件。

-JAX-RS(JSR 311 & JSR 339 & JSR 37O: 是Java 针对 REST (Representation State Transfer)架构风格制定的一套 Web Service 规范。REST是一种软件架构模式,是一种风格,它不像 SOAP那样本身承载着一种消息协议,(两种风格的 Web Service 均采用了HTTP 做传输协议,因为HTTP协议能穿越防火墙,Java的远程方法调用(RMI)等是重量级协议,通常不能穿越防火墙),因此可以将 REST视为基于HTTP协议的软件架构。REST 中最重要的两个概念是资源定位和资源操作,而HTTP协议恰好完整的提供了这两人点。HTTP 协议中的 URI 可以完成资源定位,而 GET、POST、OPTION、DELETE方法可以完成资源操作。因此REST 完全依赖HTTP协议就可以完成 WeService,而不像 SOAP协议那样只利用了 HTTP 的传输特性,定位和操作都是由 SOAP 协议自身完成的,也正是由于SOAP消息的存在使得基于SOAP的web Service显得笨重而逐渐被淘汰。

网络

TCP应用(协议),可靠性

当对网络通讯质量有要求的时候,比如:整个数据要准确无误的传递给对方,这往往用于些要求可靠的应用,比如 HTTP、HTTPS、FTP
等传输文件的协议,POP、SMTP 等邮件传输的协议三次握手超时重传,滑动窗口(避免接收双方网速不一致导致数据丢失,一种拥塞控制的协议),拥塞控制

windows和linux常用命令

win: 切换目录 cd, 列出所有任务及进程号 tasklist ,杀进程 taskkill netstat
查看网络连接状态

ping telnet ipconfig

shell常用

mkdir 和 rmdir

cp:复制命令

mv:移动命令

echo命令将输入的字符串送往标准输出,输出的字符串间以空白字符隔开, 并在最后加上换行符。

grep 命令用于从文件面搜索包含指定模式的行并打印出来,它是一种强大的文本搜索工具,支持使用正则表达式搜索文本。

rm删除命令

pwd:用于显示用户当前工作目录

cd: 用于切换用户当前工作目录

文件操作:

1、编辑文件 vi 文件名 (或者说是新建文件并用vi编辑)

2、复制文件 cp a文件 b文件 (将a文件复制一份,b就是复制文件(副本)。(两个文件都在当前路径,可以分别指定路径)

3、复制文件目录 cp a目录 b目录 -r 将a目录(包含里面的全部文件)内容 复制到b目录下,(-r 递归复制)

4、新建文件 touch 文件名 (文件不存在就新建,存在就更新新建的最新修改时间)

5、移动文件 mv a文件 b目录 (将a文件移动到b目录下)

6、重命名文件 mv a文件 b文件 (将a文件命名为b文件,注:都是在当前路径下)

7、删除文件 rm a文件 (删除a文件)

8、删除文件目录 rm a目录 -r (删除a目录,包括里面的文件)

服务器CPU和内存占用高

一、在排查问题的过程中针对CPU的问题,使用以下命令组合来排查问题

1、查看问题进程,得到进程PID:

top -c

2、查看进程里的线程明细,并手动记下CPU异常的线程PID:

top -p PID -H

3、使用jdk提供jstack命令打印出项目堆栈:

jstack pid > xxx.log

线程PID转成16进制,与堆栈中的nid对应,定位问题代码位置。

二、针对内存问题,使用以下命令组合来排查问题:

1、查看内存中的存活对象统计,找出业务相关的类名:

jmap -histo:live PID > xxx.log

2、通过简单的统计还是没法定位问题的话,就输出内存明细来分析。这个命令会将内存里的所有信息都输出,输出的文件大小和内存大小基本一致。而且会导致应用暂时挂起,所以谨慎使用。

jmap -dump:live,format=b,file=xxx.hprof PID

3、 最后对dump出来的文件进行分析。文件大小不是很大的话,使用jdk自带的jhat命令即可:

jhat -J-mx2G -port 7170

4、dump文件太大的话,可以使用jprofiler工具来分析。jprofiler工具的使用,这里不做详细介绍,有兴趣可以搜索一下。

三、需要分析GC情况,可以使用以下命令:

jstat -gc PID

这里简单介绍一下java8里面这个命令得出的列表各个列的含义:

S0C:第一个幸存区的大小 S1C:第二个幸存区的大小 S0U:第一个幸存区的使用大小 S1U:第二个幸存区的使用大小 EC:伊甸园区的大小
EU:伊甸园区的使用大小 OC:老年代大小 OU:老年代使用大小 MC:方法区大小 MU:方法区使用大小 CCSC:压缩类空间大小
CCSU:压缩类空间使用大小 YGC:年轻代垃圾回收次数 YGCT:年轻代垃圾回收消耗时间 FGC:老年代垃圾回收次数 FGCT:老年代垃圾回收消耗时间
GCT:垃圾回收消耗总时间

一般会比较关注YGC和FGC的次数。

内容补充

1、jstack输出的堆栈文件可以上传到下面这个网站,这个网站可以对堆栈内容进行统计汇总,方便我们做分析:

http://fastthread.io/index.jsp

2、排查过程小节中的第5步,jmap命令执行完后没有输出业务类,而第7步在却有。这个是因为第5步操作的时候只有1G多的内存,代码还没执行到业务对象的封装,内存就不够了,后续的代码无法被执行到。第7步操作的时候内存调整到2G,所以有部分业务对象已经被创建了。

$堆(heap)与栈(stack),静态区

堆:java对是运行是的数据区,类的对象从中分配空间,由GC负责

栈:数据项的插入和删除都只能在栈顶的一端完成,后进先出,存放一些基本的数据类型变量和对象句柄

通常我们定义一个基本数据类型的变量,一个对象的引用,还有就是函数调用的现场保存都使用内存中的栈空间;而通过
new关键字和构造器创建的对象放在堆空间;程序中的字面量(1iteral)如直接书写的100、"hello"和常量都是放在静态区中。栈空间操作起来最快但是栈很小,通常大量的对象都是放在堆空间,理论上整个内存没有被其他进程使用的空间甚至硬盘上的虚拟内存者都可以被当成堆空间来使用。

String str = new String(“hello”);

上面的语句中变量 str放在栈上,用 new创建出来的字符串对象放在堆上,而"hello"这个字面

量放在静态区。

JVM

$java内存模型分为了几块区域?元空间里有些啥?

JVM
内存共分为虚拟机栈、堆、方法区、寄存器、本地方法栈五个部分,JDK1.8有一个元数据区替代方法区了,存储一些常量池,类信息,还有class的static变量。

方法区:常量池、变量等存储地方;(持久区)

堆:实例对象存储地方;GC重点关照位置;(新生代和老年代)

程序计数器:记录程序下一步指令;

Java方法栈:方法程序运行地方;Java栈总是与线程关联在一起的,每当创建一个线程,JVM就会为该线程创建对应的Java栈;

栈(stack)是存储任何基本数据值、对对象的引用和方法的位置

本地方法栈:java方法与本地相关联

$JVM的回收机制

  • 那些内存需要回收?(对象是否可以被回收的两种经典算法: 引用计数法 和 可达性分析算法- 判断对象的引用链是否可达 )

    1. 引用计数法

给每个对象添加一个引用计数器,每当有地方引用它时,计数器 +1;引用失效时,计数器 -1。当计数器为0时对象就不再被引用。

但主流Java虚拟机没有采用这种算法,主要原因是:它难以解决对象之间循环引用的问题

  1. 可达性分析算法

通过一系列称为“GC Roots”的对象作为起始点,从这些节点向下搜索,搜索的路径称为引用链。当一个对象到 GC Roots 没有任何引用链相连(即从 GC
Roots 到该对象不可达),则此对象是不可用的,会判断为可回收对象。

Java中,可作为 GC Roots 的对象包括:

  1. 栈(栈帧中的本地变量表)中引用的对象

  2. 方法区中类 static 静态属性引用的对象

  3. 方法区中 final 常量引用的对象

  4. 本地方法栈中 JNI 引用的对象 

讲讲jvm的内存管理机制(对比着c和java讲了下,主要详细说了下对象的创建过程,说了下如何判别对象该被回收了(可达性分析算法)、垃圾回收算法、垃圾收集器)

在虚拟机遇到 new 指令时:

  1. 类加载:确保常量池中存放的是已解释的类,且对象所属类型已经初始化过,如果没有,则先执行类加载

  2. 为新生对象分配内存:对象所需内存大小在类加载时可以确定,将确定大小的内存从Java堆中划分出来

  • 分配空闲内存方法:

    • 指针碰撞:假如堆是规整的,用过的内存和空闲的内存各一边,中间使用指针作为分界点,分配内存时则将指针移动对象大小的距离

    • 空闲列表:假如堆是不规整的,虚拟机需要维护哪些内存块是可用的列表,分配时候从列表中找出足够大的空闲内存划分,并更新列表记录

  • 对象创建在并发情况下保证线程安全:例如,正在给对象A分配内存,指针还没修改,对象B同时使用了原来的指针来分配内存

    • CAS配上失败重试

    • 本地线程分配缓冲TLAB(ThreadLocal Allocation Buffer):将内存分配动作按线程划分到不同空间中进行,即每个线程在Java堆中预先分配一小块内存

将分配的内存空间初始化为零值:保证对象的实例在Java代码中可以不赋值就可直接使用,能访问到这些字段的数据类型对应的零值(例如,int类型参数默认为0)

  1. 设置对象头:设置对象的类的元数据信息、哈希码、GC分代年龄等

  2. 执行方法初始化:将对象按照程序员的意愿初始化

  • 垃圾回收算法:

  • 标记-清除算法、

    • 效率问题;遍历了两次内存空间(第一次标记,第二次清除)。

    • 空间问题:容易产生大量内存碎片,当再需要一块比较大的内存时,无法找到一块满足要求的,因而不得不再次出发GC。

  • 复制算法(现在的商业虚拟机都采用复制算法来回收新生代)、

    • 优点

    • 相对于标记–清理算法解决了内存的碎片化问题。

    • 效率更高(清理内存时,记住首尾地址,一次性抹掉)。

    • 缺点

    • 内存率不高,每次只能使用一半内存。

  • 标记-整理算法、

    • 因为前面的复制算法当对象的存活率比较高时,这样一直复制过来,复制过去,没啥意义,且浪费时间。所以针对老年代提出了“标记整理”算法。

执行步骤:

* 标记:对需要回收的进行标记

* 整理:让存活的对象,向内存的一端移动,然后直接清理掉没有用的内存。
  • 分代收集算法

    • 当前大多商用虚拟机都采用这种分代收集算法,这个算法并没有新的内容,只是根据对象的存活的时间的长短,将内存分为了新生代和老年代,这样就可以针对不同的区域,采取对应的算法。如

    • 新生代,每次都有大量对象死亡,有老年代作为内存担保,采取复制算法。

    • 老年代,对象存活时间长,采用标记整理,或者标记清理算法都可。

  • 垃圾收集器:其中用于回收新生代的收集器包括Serial、PraNew、Parallel Scavenge,回收老年代的收集器包括Serial Old、Parallel Old、CMS,还有用于回收整个Java堆的G1收集器。不同收集器之间的连线表示它们可以搭配使用。

JMM模型(Java内存模型即Java Memory Model),内存可见性介绍下

内存模型

内存泄漏是什么,怎么检测

1.什么是内存泄漏(Memory Leak)?

简单地说就是申请了一块内存空间,使用完毕后没有释放掉。它的一般表现方式是程序运行时间越长,占用内存越多,最终用尽全部内存,整个系统崩溃。由程序申请的一块内存,且没有任何一个指针指向它,那么这块内存就泄露了。

2、如何检测内存泄露

第一:良好的编码习惯,尽量在涉及内存的程序段,检测出内存泄露。当程式稳定之后,在来检测内存泄露时,无疑增加了排除的困难和复杂度。使用了内存分配的函数,一旦使用完毕,要记得要使用其相应的函数释放掉。

第二:将分配的内存的指针以链表的形式自行管理,使用完毕之后从链表中删除,程序结束时可检查改链表。

第三:Boost 中的smart pointer。

第四:一些常见的工具插件,如ccmalloc、Dmalloc、Leaky等等。

介绍2个设计模式

设计模式

面向对象原则

  1. 开闭原则——面向对象设计原则

  2. 里氏替换原则——面向对象设计原则

  3. 依赖倒置原则——面向对象设计原则

  4. 单一职责原则——面向对象设计原则

  5. 接口隔离原则——面向对象设计原则

  6. 迪米特法则——面向对象设计原则

  7. 合成复用原则——面向对象设计原则

重载和重写。为什么要有重载,我随便命名一个别的函数名不行吗?谈谈你是怎么理解的。

1.重写(Override)

从字面上看,重写就是 重新写一遍的意思。其实就是 在子类中把父类本身有的方法
重新写一遍。子类继承了父类原有的方法,但有时子类并不想原封不动的继承父类中的某个方法,所以
在方法名,参数列表,返回类型(除过子类中方法的返回值是父类中方法返回值的子类时)都相同的情况下, 对方法体进行修改或重写,这就是重写。但要注意
子类函数的访问修饰权限不能少于父类的。

重写 总结:

1.发生在父类与子类之间

2.方法名,参数列表,返回类型(除过子类中方法的返回类型是父类中返回类型的子类)必须相同

3.访问修饰符的限制一定要大于被重写方法的访问修饰符(public>protected>default>private)

4.重写方法一定不能抛出新的检查异常或者比被重写方法申明更加宽泛的检查型异常

2.重载(Overload)

在一个类中,同名的方法如果有不同的参数列表( 参数类型不同、参数个数不同甚至是参数顺序不同
)则视为重载。同时,重载对返回类型没有要求,可以相同也可以不同,但 不能通过返回类型是否相同来判断重载

重载 总结:

1.重载Overload是一个类中多态性的一种表现

2.重载要求同名方法的参数列表不同(参数类型,参数个数甚至是参数顺序)

3.重载的时候,返回值类型可以相同也可以不相同。无法以返回型别作为重载函数的区分标准

答:方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。重载发生在一个类中,同名的方法如果有不同的参数列表(参数类型不同、参数个数不同或者二者都不同)则视为重载;重写发生在子类与父类之间,重写要求子类被重写方法与父类被重写方法有相同的参数列表,有兼容的返回类型,比父类被重写方法更好访问,不能比父类被重写方法声明更多的异常(里氏代换原则)。重载对返回类型没有特殊的要求,不能根据返回类型进行区分。

抽象类和接口的区别,什么时候用抽象类什么时候用接口

  • 抽象方法必须用abstract关键字进行修饰。如果一个类含有抽象方法,则称这个类为抽象类,抽象类必须在类前用abstract关键字修饰。因为抽象类中含有无具体实现的方法,所以不能用抽象类创建对象。包含抽象方法的类称为抽象类,但并不意味着抽象类中只能有抽象方法,它和普通类一样,同样可以拥有成员变量和普通的成员方法。注意,抽象类和普通类的主要有三点区别:

    • 抽象方法必须为public或者protected(因为如果为private,则不能被子类继承,子类便无法实现该方法),缺省情况下默认为public。

    • 抽象类不能用来创建对象;

    • 如果一个类继承于一个抽象类,则子类必须实现父类的抽象方法。如果子类没有实现父类的抽象方法,则必须将子类也定义为为abstract类。

在其他方面,抽象类和普通的类并没有区别。

  • 接口中可以含有 变量和方法。但是要注意,接口中的变量会被隐式地指定为public static final变量(并且只能是public static final变量,用private修饰会报编译错误),而方法会被隐式地指定为public abstract方法且只能是public abstract方法(用其他关键字,比如private、protected、static、 final等修饰会报编译错误),并且接口中所有的方法不能有具体的实现,也就是说,接口中的方法必须都是抽象方法。从这里可以隐约看出接口和抽象类的区别,接口是一种极度抽象的类型,它比抽象类更加“抽象”,并且一般情况下不在接口中定义变量。

  • 区别

    • 一个类只能继承一个抽象类,而一个类却可以实现多个接口。

    • 抽象类是对一种事物的抽象,即对类抽象,而接口是对行为的抽象

    • 设计层面不同,抽象类作为很多子类的父类,它是一种模板式设计。而接口是一种行为规范,它是一种辐射式设计。

[详细讲一下JUC(java.util.concurrent

)包有哪些组件,用过哪些?](https://app.yinxiang.com/shard/s64/nl/13973867/30d36207-f6d3-455d-b47d-c81383c9d2e1)

  1. JUC 简介
  • 在 Java 5.0 提供了

java.util.concurrent

(简称JUC)包,在此包中增加了在并发编程中很常用的工具类, 用于定义类似于线程的自定义子系统,包括线程池,异步 IO
和轻量级任务框架;还提供了设计用于多线程上下文中 的 Collection 实现等;

atom,retranLock,线程池,blokingqueue

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GNFsvsNs-1681014581785)(【精】各大厂问题汇总_files/Y3MZZf.png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NnpwB6zg-1681014581785)(【精】各大厂问题汇总_files/RrUjee.png)]

TCP三次握手和四次挥手,挥手时各个时刻的状态是什么?

三次握手

(1)第一次握手:Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。

(2)第二次握手:Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置为1,ack (number
)=J+1,随机产生一个值seq=K,并将该数据包发送给Client以确认连接请求,Server进入SYN_RCVD状态。

(3)第三次握手:Client收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server,Server检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。

四次握手

(1)第一次挥手:Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。

(2)第二次挥手:Server收到FIN后,发送一个ACK给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。

(3)第三次挥手:Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。

(4)第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态,接着发送一个ACK给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。

Java有哪些引用类型,分别是什么特点

强引用,弱引用,软引用 ,虚引用

虚引用有哪些应用场景

虚引用

也称为幻影引用:一个对象是都有虚引用的存在都不会对生存时间都构成影响,也无法通过虚引用来获取对一个对象的真实引用。唯一的用处:能在对象被GC时收到系统通知,JAVA中用PhantomReference来实现虚引用。

适用场景

使用虚引用的目的就是为了得知对象被GC的时机,所以可以利用虚引用来进行销毁前的一些操作,比如说资源释放等。这个虚引用对于对象而言完全是无感知的,有没有完全一样,但是对于虚引用的使用者而言,就像是待观察的对象的把脉线,可以通过它来观察对象是否已经被回收,从而进行相应的处理。

事实上,虚引用有一个很重要的用途就是用来做堆外内存的释放,DirectByteBuffer就是通过虚引用来实现堆外内存的释放的。

小结

  • 虚引用是最弱的引用

  • 虚引用对对象而言是无感知的,对象有虚引用跟没有是完全一样的

  • 虚引用不会影响对象的生命周期

  • 虚引用可以用来做为对象是否存活的监控

$static final修饰的一个int 进行修改后是否需要进行重新编译

需要。一句话总结,在静态类里定义的静态属性,坚决不用引用类型,而需要用对象类型。

结论:

解决这种问题总共有三种方法:

1、将全部的CLASS文件删除掉再编译

2、将其变成GET、SET方法

3、设置成对象引用的方式处理!

Spring

知道spring AOP是如何实现的么,动态代理和CGlib分别是如何实现的

aop的简介:AOP(Aspect-OrientedProgramming,面向方面编程)(“横切”),可以说是OOP(Object-Oriented
Programing,面向对象编程)的补充和完善(为分散的对象引入公共行为)。将多个类的公共行为封装到一个可重用模块(“Aspect”-方面)减少系统的重复代码,降低模块间的耦合度。

使用场景:权限、缓存、内容传递、错误处理、懒加载、调试

概念:方面(Aspect)、连接点(Joinpoint)、通知(Advice)、切入点(Pointcut)、引入(Introduction)、目标对象、AOP代理、织入(Weaving)

主流程可以简述为:获取可以应用到此方法上的通知链拦截器链(Interceptor Chain),如果有,则应用通知,并执行joinpoint;
如果没有,则直接反射执行joinpoint。

Spring AOP的底层实现有两种方式:一种是JDK动态代理,另一种是CGLib的方式。

JDK动态代理主要涉及java.lang.reflect包下边的两个类:Proxy和InvocationHandler。其中,InvocationHandler是一个接口,可以通过实现该接口定义横切逻辑,并通过反射机制调用目标类的代码,动态地将横切逻辑和业务逻辑贬值在一起。

  1. JDK动态代理的话,他有一个限制,就是它只能为接口创建代理实例,而对于没有通过接口定义业务方法的类,如何创建动态代理实例哪?答案就是CGLib。

  2. CGLib采用底层的字节码技术,全称是:Code Generation Library,CGLib可以为一个类创建一个子类,在子类中采用方法拦截的技术拦截所有父类方法的调用并顺势织入横切逻辑。

三、JDK 和 CGLib动态代理区别

1、JDK动态代理具体实现原理:

1. 通过实现InvocationHandler接口创建自己的调用处理器;

2. 通过为Proxy类指定ClassLoader对象和一组interface来创建动态代理;

3. 通过反射机制获取动态代理类的构造函数,其唯一参数类型就是调用处理器接口类型;

4. 通过构造函数创建动态代理类实例,构造时调用处理器对象作为参数参入;

5. JDK动态代理是面向接口的代理模式,如果被代理目标没有接口那么Spring也无能为力,Spring通过Java的反射机制生产被代理接口的新的匿名实现类,重写了其中AOP的增强方法。

2、CGLib动态代理:

CGLib是一个强大、高性能的Code生产类库,可以实现运行期动态扩展java类,Spring在运行期间通过
CGlib继承要被动态代理的类,重写父类的方法,实现AOP面向切面编程呢。

3、两者对比:

JDK动态代理是面向接口的。

CGLib动态代理是通过字节码底层继承要代理类来实现 (如果被代理类被final关键字所修饰,那么抱歉会失败)。

4、使用注意:

如果要被代理的对象是个实现类,那么Spring会使用JDK动态代理来完成操作(Spirng默认采用JDK动态代理实现机制);

如果要被代理的对象不是个实现类那么,Spring会强制使用CGLib来实现动态代理

说一说Spring中Bean的加载过程,BeanFactory和FactoryBean有什么区别?

3、区别

BeanFactory是个Factory,也就是IOC容器或对象工厂,FactoryBean是个Bean。在Spring中,所有的Bean都是由BeanFactory(也就是IOC容器)来进行管理的。但对FactoryBean而言,这个Bean不是简单的Bean,而是一个能生产或者修饰对象生成的工厂Bean,它的实现与设计模式中的工厂模式和修饰器模式类似。

BeanFactory定义了IOC容器的最基本形式,并提供了IOC容器应遵守的的最基本的接口,也就是Spring
IOC所遵守的最底层和最基本的编程规范。在Spring代码中,BeanFactory只是个接口,并不是IOC容器的具体实现,但是Spring容器给出了很多种实现,如
DefaultListableBeanFactory、XmlBeanFactory、ApplicationContext等,都是附加了某种功能的实现。

Spring为此提供了一个org.springframework.bean.factory.FactoryBean的工厂类接口,用户可以通过实现该接口定制实例化Bean的逻辑。

一个 Bean 加载会经历这么几个阶段(用绿色标记):

  • 获取 BeanName ,对传入的 name 进行解析,转化为可以从 Map 中获取到 BeanDefinition 的 bean name。

  • 合并 Bean 定义 ,对父类的定义进行合并和覆盖,如果父类还有父类,会进行递归合并,以获取完整的 Bean 定义信息。

  • 实例化 ,使用构造或者工厂方法创建 Bean 实例。

  • 属性填充 ,寻找并且注入依赖,依赖的 Bean 还会递归调用 getBean 方法获取。

  • 初始化 ,调用自定义的初始化方法。

  • 获取最终的 Bean ,如果是 FactoryBean 需要调用 getObject 方法,如果需要类型转换调用 TypeConverter 进行转化。

消息

$ActiveMQ(Message Queue, 消息队列)是由哪些东西组成的?

JMS(java message service)组成的四大元素

* JMS provider : 实现JMS接口和规范的消息中间件,也就是我们的MQ服务器

* JMS producer :消息生产者,创建和发送JMS消息的客户端应用

* JMS consumer :消息消费者,接收和处理JMS消息的客户端应用

* JMS message :传送的消息。
  • activeMQ 是什么? 是 Apache 公司旗下的一个消息总线 ActiveMQ 是一个开源兼容 Java Message Service (JMS) 1.1 面向消息的中件间. 来自 Apache Software Foundation. ActiveMQ 提供松耦合的应用程序架构.

  • activeMQ 能干什么? 用来在服务与服务之间进行异步通信的

  • activeMQ 优势 1. 流量削锋 2. 任务异步处理

  • 特点:可以解耦合

  • 通信模式:

  1. 点对点 (queue)

》一个消息只能被一个服务接收》消息一旦被消费,就会消失》如果没有被消费,就会一直等待,直到被消费》多个服务监听同一个消费空间,先到先得

  1. 发布 / 订阅模式 (topic)

》一个消息可以被多个服务接收 》订阅一个主题的消费者,只能消费自它订阅之后发布的消息。
》消费端如果在生产端发送消息之后启动,是接收不到消息的,除非生产端对消息进行了持久化 (例如广播,只有当时听到的人能听到信息)

消费者生产者,写写伪代码

生产者-
消费者问题,也称有界缓冲问题,属于操作系统进程同步的经典问题,在多线程并发编程中也是经典案例,值得去学习。有一个生产者线程和一个消费者线程,生产者生产资源到资源缓冲区,消费者从资源缓冲区消费资源。资源缓冲区有界,空则不能取,满则不能产。

实现方式:1 Object.wait() / notify()方法实现;2 Lock和Condition实现

JDK 1.5 以后新增的 java.util.concurrent包新增了 BlockingQueue 接口。并提供了如下几种阻塞队列实现:

/** * 生产者消费者模式:使用{@link java.util.concurrent.BlockingQueue}实现 */ public class
ProducerConsumerByBQ{ private static final int CAPACITY = 5; public static
void main(String args[]){ LinkedBlockingDeque blockingQueue = new
LinkedBlockingDeque(CAPACITY); Thread producer1 = new Producer(“P-1”,
blockingQueue, CAPACITY); Thread producer2 = new Producer(“P-2”,
blockingQueue, CAPACITY); Thread consumer1 = new Consumer(“C1”, blockingQueue,
CAPACITY); Thread consumer2 = new Consumer(“C2”, blockingQueue, CAPACITY);
Thread consumer3 = new Consumer(“C3”, blockingQueue, CAPACITY);
producer1.start(); producer2.start(); consumer1.start(); consumer2.start();
consumer3.start(); }

IO和NIO

$NIO知道么? nio底层调用了啥?啥是非阻塞IO?

完整的IO读请求操作包括两个阶段:1)查看数据是否就绪;2)进行数据拷贝(内核将数据拷贝到用户线程)。

阻塞(blocking IO)和非阻塞(non-blocking
IO)的区别就在于第一个阶段,如果数据没有就绪,再查看数据是否就绪的过程中是一直等待,还是直接返回一个标志信息。Java中传统的IO都是阻塞IO,比如通过socket来读数据,调用read()方法之后,如果数据没有就绪,当前线程就会一直阻塞在read方法调用那里,直到有数据才返回;而如果是非阻塞IO的话,当数据没有就绪,read()方法应该返回一个标志信息,告知当前线程数据没有就绪,而不是一直在那里等待。

用户程序进行IO的读写,基本上会用到read&write两大系统调用。
read系统调用,是把数据从内核缓冲区复制到进程缓冲区;而write系统调用,是把数据从进程缓冲区复制到内核缓冲区。

  • 四种主要的IO模型:同步阻塞IO(Blocking IO),同步非阻塞IO(Non-blocking IO),IO多路复用(IO Multiplexing),异步IO(Asynchronous IO)四种IO模型,理论上越往后,阻塞越少,效率也是最优。

发起一个non-blocking socket的read读操作系统调用,流程是这个样子:

(1)在内核数据没有准备好的阶段,用户线程发起IO请求时,立即返回。用户线程需要不断地发起IO系统调用。

(2)内核数据到达后,用户线程发起系统调用,用户线程阻塞。内核开始复制数据。它就会将数据从kernel内核缓冲区,拷贝到用户缓冲区(用户内存),然后kernel返回结果。

(3)用户线程才解除block的状态,重新运行起来。经过多次的尝试,用户线程终于真正读取到数据,继续执行。

  • NIO的特点:应用程序的线程需要不断的进行 I/O 系统调用,轮询数据是否已经准备好,如果没有准备好,继续轮询,直到完成系统调用为止。

  • NIO的优点:每次发起的 IO 系统调用,在内核的等待数据过程中可以立即返回。用户线程不会阻塞,实时性较好。

  • NIO的缺点:需要不断的重复发起IO系统调用,这种不断的轮询,将会不断地询问内核,这将占用大量的 CPU 时间,系统资源利用率较低。

总之, NIO模型在高并发场景下,也是不可用的 。一般 Web 服务器不使用这种 IO
模型。一般很少直接使用这种模型,而是在其他IO模型中使用非阻塞IO这一特性。java的实际开发中,也不会涉及这种IO模型。

再次说明 ,Java NIO(New IO) 不是IO模型中的NIO模型,而是另外的一种模型,叫做IO多路复用模型( IO multiplexing

  • IO多路复用模型 的基本原理就是 select/epoll 系统调用,单个线程不断的轮询select/epoll系统调用所负责的成百上千的socket连接,当某个或者某些socket网络连接有数据到达了,就返回这些可以读写的连接。因此,好处也就显而易见了——通过一次select/epoll系统调用,就查询到到可以读写的一个甚至是成百上千的网络连接。

(1)进行select/epoll系统调用,查询可以读的连接。kernel会查询所有select的可查询socket列表,当任何一个socket中的数据准备好了,select就会返回。

当用户进程调用了select,那么整个线程会被block(阻塞掉)。

(2)用户线程获得了目标连接后,发起read系统调用,用户线程阻塞。内核开始复制数据。它就会将数据从kernel内核缓冲区,拷贝到用户缓冲区(用户内存),然后kernel返回结果。

(3)用户线程才解除block的状态,用户线程终于真正读取到数据,继续执行。

  • 多路复用IO的特点:

IO多路复用模型,建立在操作系统kernel内核能够提供的多路分离系统调用select/epoll基础之上的。多路复用IO需要用到两个系统调用(system
call), 一个select/epoll查询调用,一个是IO的读取调用。

和NIO模型相似,多路复用IO需要轮询。负责select/epoll查询调用的线程,需要不断的进行select/epoll轮询,查找出可以进行IO操作的连接。

另外,多路复用IO模型与前面的NIO模型,是有关系的。对于每一个可以查询的socket,一般都设置成为non-
blocking模型。只是这一点,对于用户程序是透明的(不感知)。

  • 多路复用IO的优点:用select/epoll的优势在于,它可以同时处理成千上万个连接(connection)。与一条线程维护一个连接相比,I/O多路复用技术的最大优势是:系统不必创建线程,也不必维护这些线程,从而大大减小了系统的开销。

Java的NIO(new IO)技术,使用的就是IO多路复用模型。在linux系统上,使用的是epoll系统调用。

  • 多路复用IO的缺点:本质上,select/epoll系统调用,属于同步IO,也是阻塞IO。都需要在读写事件就绪后,自己负责进行读写,也就是说这个读写过程是阻塞的。

如何充分的解除线程的阻塞呢?那就是异步IO模型。

  • AIO的基本流程是:用户线程通过系统调用,告知kernel内核启动某个IO操作,用户线程返回。kernel内核在整个IO操作(包括数据准备、数据复制)完成后,通知用户程序,用户执行后续的业务操作。

(1)当用户线程调用了read系统调用,立刻就可以开始去做其它的事,用户线程不阻塞。

(2)内核(kernel)就开始了IO的第一个阶段:准备数据。当kernel一直等到数据准备好了,它就会将数据从kernel内核缓冲区,拷贝到用户缓冲区(用户内存)。

(3)kernel会给用户线程发送一个信号(signal),或者回调用户线程注册的回调接口,告诉用户线程read操作完成了。

(4)用户线程读取用户缓冲区的数据,完成后续的业务操作。

  • 异步IO模型的特点:

在内核kernel的等待数据和复制数据的两个阶段,用户线程都不是block(阻塞)的。用户线程需要接受kernel的IO操作完成的事件,或者说注册IO操作完成的回调函数,到操作系统的内核。所以说,异步IO有的时候,也叫做信号驱动
IO 。

  • 异步IO模型缺点:

需要完成事件的注册与传递,这里边需要底层操作系统提供大量的支持,去做大量的工作。

目前来说, Windows 系统下通过 IOCP 实现了真正的异步 I/O。但是,就目前的业界形式来说,Windows
系统,很少作为百万级以上或者说高并发应用的服务器操作系统来使用。

而在 Linux 系统下,异步IO模型在2.6版本才引入,目前并不完善。 所以,这也是在 Linux 下,实现高并发网络编程时都是以 IO
复用模型模式为主。

关于 epoll 和 select 的区别,哪些说法是正确的?(多选)

问题:关于 epoll 和 select 的区别,哪些说法是正确的?(多选)

A. epoll 和 select 都是 I/O 多路复用的技术,都可以实现同时监听多个 I/O 事件的状态。

B. epoll 相比 select 效率更高,主要是基于其操作系统支持的I/O事件通知机制,而 select 是基于轮询机制。

C. epoll 支持水平触发和边沿触发两种模式。

D. select 能并行支持 I/O 比较小,且无法修改。 参考答案:A,B,C
【延伸】那在高并发的访问下,epoll使用那一种触发方式要高效些?当使用边缘触发的时候要注意些什么东西?

  • 消息传递方式:

select:内核需要将消息传递到用户空间,需要内核的拷贝动作;

poll:同上;

epoll:通过内核和用户空间共享一块内存来实现,性能较高;

  • 文件句柄剧增后带来的IO效率问题:

select:因为每次调用都会对连接进行线性遍历,所以随着FD剧增后会造成遍历速度的“线性下降”的性能问题;

poll:同上;

epoll:由于epoll是根据每个FD上的callable函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll不会对性能产生线性下降的问题,如果所有socket都很活跃的情况下,可能会有性能问题;

支持一个进程所能打开的最大连接数:

select:单个进程所能打开的最大连接数,是由FD_SETSIZE宏定义的,其大小是32个整数大小(在32位的机器上,大小是32
32,64位机器上FD_SETSIZE=32 64),我们可以对其进行修改,然后重新编译内核,但是性能无法保证,需要做进一步测试;

poll:本质上与select没什么区别,但是他没有最大连接数限制,他是基于链表来存储的;

epoll:虽然连接数有上线,但是很大,1G内存的机器上可以打开10W左右的连接;

epoll是一种I/O事件通知机制,是linux
内核实现IO多路复用的一个实现。IO多路复用是指,在一个操作里同时监听多个输入输出源,在其中一个或多个输入输出源可用的时候返回,然后对其的进行读写操作。

epoll监控多个文件描述符的I/O事件。epoll支持边缘触发(edge trigger,ET)或水平触发(level
trigger,LT),通过epoll_wait等待I/O事件,如果当前没有可用的事件则阻塞调用线程。

select和poll只支持LT工作模式,epoll的默认的工作模式是LT模式。

  • 水平触发:0为无数据,1为有数据。缓冲区有数据则一直为1,则一直触发。

  • 边缘触发发:0为无数据,1为有数据,只要在0变到1的上升沿才触发。 所以效率更高

JDK并没有实现边缘触发, Netty重新实现了epoll机制 ,采用边缘触发方式;另外像Nginx也采用边缘触发。

虽然epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。

  • epoll更高效的原因
  1. select和poll的动作基本一致,只是poll采用链表来进行文件描述符的存储,而select采用fd标注位来存放,所以select会受到最大连接数的限制,而poll不会。

  2. select、poll、epoll虽然都会返回就绪的文件描述符数量。但是select和poll并不会明确指出是哪些文件描述符就绪,而epoll会。造成的区别就是,系统调用返回后,调用select和poll的程序需要遍历监听的整个文件描述符找到是谁处于就绪,而epoll则直接处理即可。

  3. select、poll都需要将有关文件描述符的数据结构拷贝进内核,最后再拷贝出来。而epoll创建的有关文件描述符的数据结构本身就存于内核态中。

  4. select、poll采用轮询的方式来检查文件描述符是否处于就绪态,而epoll采用回调机制。造成的结果就是,随着fd的增加,select和poll的效率会线性降低,而epoll不会受到太大影响,除非活跃的socket很多。

  5. epoll的边缘触发模式效率高,系统不会充斥大量不关心的就绪文件描述符

多线程

为什么要用线程池

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

  2. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

  3. 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控

$$多线程,AtomicInteger底层用的啥?cas的原理,AtomicInteger用了Voliate么(是)?voliate的原理,变量加Voliate被修改了其他线程能立刻知道么(是
可见性)?

AtomicIntger 是对 int 类型的一个封装,提供原子性的访问和更新操作,其原子性操作的实现是基于 CAS (compare-and-
swap) 比较并替换 技术。它依赖于 Unsafe 提供的一些底层能力,进行底层操作;以 volatile 的 value
字段,Unsafe 会利用 value 字段的内存地址偏移,直接完成操作。CAS 是 Java 并发中所谓 lock-free 机制的基础。

voliate是java虚拟机提供的轻量级的同步机制.他保证了可见性,不保证原子性,禁止指令重排

Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排他锁单独获得这个变量。

volatile三个特点

  1. 如果一个字段被申明为volatile,那么Java内存模型则 可以保证多个线程所看到的值是一致的

  2. 禁止指定重排。

  3. volatile只能保证可见性, 不能保证原子性。

可见性的特点是: 获取主内存的那一瞬间,主内存的值新的

注意:voliate 只保证可见性,重点侧重于: 每个可以拿到其他线程修改之后的最新值(但并不是意味,只要有新值就可以 实时 获取最新值)

  • 总结
  1. 更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

  2. Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。

  3. CAS只能保证一个共享变量的原子操作

$sleep()和wait()的区别,调用这两个函数后,线程状态分别作何改变

sleep和wait的区别: 1、sleep是Thread的静态方法,wait是Object的方法,任何对象实例都能调用。
2、sleep不会释放锁,它也不需要占用锁。wait会释放锁,但调用它的前提是当前线程占有锁(即代码要在synchronized中)。
3、它们都可以被interrupted方法中断。

具体来说:

Thread.Sleep(1000)
意思是在未来的1000毫秒内本线程不参与CPU竞争,1000毫秒过去之后,这时候也许另外一个线程正在使用CPU,那么这时候操作系统是不会重新分配CPU的,直到那个线程挂起或结束,即使这个时候恰巧轮到操作系统进行CPU
分配,那么当前线程也不一定就是总优先级最高的那个,CPU还是可能被其他线程抢占去。另外值得一提的是Thread.Sleep(0)的作用,就是触发操作系统立刻重新进行一次CPU竞争,竞争的结果也许是当前线程仍然获得CPU控制权,也许会换成别的线程获得CPU控制权。

wait(1000)表示将锁释放1000毫秒,到时间后如果锁没有被其他线程占用,则再次得到锁,然后wait方法结束,执行后面的代码,如果锁被其他线程占用,则等待其他线程释放锁。注意,设置了超时时间的wait方法一旦过了超时时间,并不需要其他线程执行notify也能自动解除阻塞,但是如果没设置超时时间的wait方法必须等待其他线程执行notify。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KxazDt2s-1681014581786)(【精】各大厂问题汇总_files/Image [6].png)]

讲一下Java中线程的各种状态

java线程状态在Thread中定义,源码中能看到有个枚举State,总共定义了六种状态:

NEW: 新建状态,线程对象已经创建,但尚未启动

RUNNABLE:就绪状态,可运行状态,调用了线程的start方法,已经在java虚拟机中执行,等待获取操作系统资源如CPU,操作系统调度运行。

BLOCKED:堵塞状态。线程等待锁的状态,等待获取锁进入同步块/方法或调用wait后重新进入需要竞争锁

WAITING:等待状态。等待另一个线程以执行特定的操作。调用以下方法进入等待状态。 Object.wait(),
Thread.join(),LockSupport.park

TIMED_WAITING: 线程等待一段时间。调用带参数的Thread.sleep,
objct.wait,Thread.join,LockSupport.parkNanos,LockSupport.parkUntil

TERMINATED:进程结束状态。

状态之间的转换状态图,总结了下,如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TNRCxld3-1681014581786)(【精】各大厂问题汇总_files/Image [6].jpg)]

其中,Thread.sleep(long)使线程暂停一段时间,进入TIMED_WAITING时间,并不会释放锁,在设定时间到或被interrupt后抛出InterruptedException后进入RUNNABLE状态;
Thread.join是等待调用join方法的线程执行一段时间(join(long))或结束后再往后执行,被interrupt后也会抛出异常,join内部也是wait方式实现的。

wait方法是object的方法,线程释放锁,进入WAITING或TIMED_WAITING状态。等待时间到了或被notify/notifyall唤醒后,回去竞争锁,如果获得锁,进入RUNNABLE,否则进步BLOCKED状态等待获取锁。

下面是一个小例子,主线程中调用多线程,等待超时后如果子线程还未结束,则中断子线程(interrupt)。

$用过线程池吗?讲一下为什么用线程池,怎么用线程池

线程池做的工作主要是控制运行的线程的数量,处理过程中,将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出的数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。

他的主要特点:线程复用、控制最大并发数、管理线程。

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

提高响应速度。当任务到达时,任务可以不需要的等待线程创建就能立即执行。

提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控

1、什么是线程池: java.util.concurrent.Executors提供了一个
java.util.concurrent.Executor接口的实现用于创建线程池

多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力。假设一个服务器完成一项任务所需时间为:T1
创建线程时间,T2 在线程中执行任务的时间,T3 销毁线程时间。 如果:T1 + T3 远大于 T2,则可以采用线程池,以提高服务器性能。

一个线程池包括以下四个基本组成部分:

1、线程池管理器(ThreadPool):用于创建并管理线程池,包括 创建线程池,销毁线程池,添加新任务;

2、工作线程(PoolWorker):线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;

3、任务接口(Task):每个任务必须实现的接口,以供工作线程调度任务的执行,它主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;

4、任务队列(taskQueue):用于存放没有处理的任务。提供一种缓冲机制。

线程池技术正是关注如何缩短或调整T1,T3时间的技术,从而提高服务器程序性能的。

java提供的线程池更加强大,相信理解线程池的工作原理,看类库中的线程池就不会感到陌生了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7LoqYDQM-1681014581787)(【精】各大厂问题汇总_files/Image [7].png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AVXlDxSF-1681014581787)(【精】各大厂问题汇总_files/Image [8].png)]

知道多线程和多进程的区别吗?有什么优点呢

进程优点:每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系;

缺点:需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算 多进程调度开销比较大。

线程优点:无需跨进程边界;

缺点:每个线程与主程序共用地址空间,受限于2GB地址空间;

区别:

1、操作系统资源管理方式区别

进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。

2、所处环境区别

在操作系统中能同时运行多个进程(程序);而在同一个进程(程序)中有多个线程同时执行(通过CPU调度,在每个时间片中只有一个线程执行)

3、内存分配方面区别

系统在运行的时候会为每个进程分配不同的内存空间;而对线程而言,除了CPU外,系统不会为线程分配内存(线程所使用的资源来自其所属进程的资源),线程组之间只能共享资源。

区别

多进程是立体交通系统,虽然造价高,上坡下坡多耗点油,但是不堵车。

多线程是平面交通系统,造价低,但红绿灯太多,老堵车。

我们现在都开跑车,油(主频)有的是,不怕上坡下坡,就怕堵车。

多进程优点:

1、每个进程互相独立,不影响主程序的稳定性,子进程崩溃没关系;

2、通过增加CPU,就可以容易扩充性能;

3、可以尽量减少线程加锁/解锁的影响,极大提高性能,就算是线程运行的模块算法效率低也没关系;

4、每个子进程都有2GB地址空间和相关资源,总体能够达到的性能上限非常大。

多进程缺点:

1、逻辑控制复杂,需要和主程序交互;

2、需要跨进程边界,如果有大数据量传送,就不太好,适合小数据量传送、密集运算 多进程调度开销比较大;

3、最好是多进程和多线程结合,即根据实际的需要,每个CPU开启一个子进程,这个子进程开启多线程可以为若干同类型的数据进行处理。当然你也可以利用多线程+多CPU+轮询方式来解决问题……

4、方法和手段是多样的,关键是自己看起来实现方便有能够满足要求,代价也合适。

多线程的优点:

1、无需跨进程边界;

2、程序逻辑和控制方式简单;

3、所有线程可以直接共享内存和变量等;

4、线程方式消耗的总资源比进程方式好。

多线程缺点:

1、每个线程与主程序共用地址空间,受限于2GB地址空间;

2、线程之间的同步和加锁控制比较麻烦;

3、一个线程的崩溃可能影响到整个程序的稳定性;

4、到达一定的线程数程度后,即使再增加CPU也无法提高性能,例如Windows Server
2003,大约是1500个左右的线程数就快到极限了(线程堆栈设定为1M),如果设定线程堆栈为2M,还达不到1500个线程总数;

5、线程能够提高的总性能有限,而且线程多了之后,线程本身的调度也是一个麻烦事儿,需要消耗较多的CPU。

悲观锁和乐观锁

  • 悲观锁、乐观锁使用场景是针对数据库操作来说的,是一种锁机制。

  • 悲观锁(Pessimistic Lock):顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

  • 乐观锁(Optimistic Lock):顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制,即对数据做版本控制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。

$Synchronized的底层原理,字节码层面如何实现加锁的?

synchronized 是一个Java的关键字,很多的书中都将它称为内置锁,也叫做监视器锁。它的实现完全是有JVM来实现的。

三种方式

  • synchronized代码块。

当线程请求一个未被持有的锁(执行monitorenter),JVM将记下锁的持有者(获得Monitor对象),并且将获取锁的计数器置为1.如果同一个线程再次获取锁,计数器的值将加1(重入的实现);当执行monitorexit
(释放)释放锁计数器减1,当计数器为零,退出代码块

  • 在实例方法上使用表示是对当前实例的加锁:

出现了一个ACC _SYNCHRONIZED JVM使用它来区分方法是否是同步方法, JVM在执行方法时会检查方法是否有ACC_SYNCHRONIZED
,如果有执行线程将会持有方法所在的对象的Monitor对象,在方法执行期间任何线程无法再获取这个Monitor对象,当线程执行完该方法无论是否有异常,都会在方法结束时候释放掉这个Monitor对象。

  • 在类方法上表示对当前类的Class对象加锁:

跟非静态的区别是标示多了一个ACC_STATIC 还有就是args_size = 0 上面的args_size = 1
那么这是为什么呢。因为无论修饰代码块还是修饰方法,方法都是属于该实例的,JVM会在参数中隐式的传入this关键字。当有ACC_STATIC
的时候方法是归Class对象所有,所以参数是零。

synchronized和volatile区别

  • 首先需要理解线程安全的两个方面: 执行控制内存可见

执行控制 的目的是控制代码执行(顺序)及是否可以并发执行。

内存可见 控制的是线程执行结果在内存中对其它线程的可见性。根据
Java内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存。

  • synchronized关键字解决的是执行控制的问题,它会阻止其它线程获取当前对象的监控锁,这样就使得当前对象中被synchronized关键字保护的代码块无法被其它线程访问,也就无法并发执行。更重要的是,synchronized还会创建一个 内存屏障 ,内存屏障指令保证了所有CPU操作结果都会直接刷到主存中,从而保证了操作的内存可见性,同时也使得先获得这个锁的线程的所有操作,都 happens-before 于随后获得这个锁的线程的操作。

  • volatile关键字解决的是内存可见性的问题,会使得所有对volatile变量的读写都会直接刷到主存,即保证了变量的可见性。这样就能满足一些对变量可见性有要求而对读取顺序没有要求的需求。

使用volatile关键字仅能实现对原始变量(如boolen、 short 、int 、long等)操作的原子性,但需要特别注意,
volatile不能保证复合操作的原子性,即使只是i++,实际上也是由多个原子操作组成:read i; inc; write
i,假如多个线程同时执行i++,volatile只能保证他们操作的i是同一块内存,但依然可能出现写入脏数据的情况。

  • 在Java 5提供了原子数据类型atomic wrapper classes,对它们的increase之类的操作都是原子操作,不需要使用sychronized关键字。

对于volatile关键字,当且仅当满足以下所有条件时可使用:

  1. 对变量的写入操作不依赖变量的当前值,或者你能确保只有单个线程更新变量的值。2. 该变量没有包含在具有其他变量的不变式中。

    • volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

    • volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的

    • volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性

    • volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。

    • volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化

$ReentrantLock(可重入 )如何实现非公平锁,和“公平锁”有什么区别?

JUC包图

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HDFlnkdG-1681014581788)(【精】各大厂问题汇总_files/Image [7].jpg)]

如果一个锁是公平的,那么锁的获取顺序就应该符合请求的绝对时间顺序,FIFO。对于非公平锁,只要 CAS 比较并替换
设置同步状态成功,则表示当前线程获取了锁,而公平锁还需要判断当前节点是否有前驱节点,如果有,则表示有线程比当前线程更早请求获取锁,因此需要等待前驱线程获取并释放锁之后才能继续获取锁。

并发编程中,ReentrantLock的使用是比较多的,包括之前讲的LinkedBlockingQueue和ArrayBlockQueue的内部都是使用的ReentrantLock,谈到它又不能的不说AQS,
AQS的全称是AbstractQueuedSynchronizer
,这个类也是在java.util.concurrent.locks下面,提供了一个FIFO的队列,可以用于构建锁的基础框架,内部通过原子变量state来表示锁的状态,当state大于0的时候表示锁被占用,如果state等于0时表示没有占用锁,ReentrantLock是一个重入锁,表现在state上,如果持有锁的线程重复获取锁时,它会将state状态进行递增,也就是获得一个信号量,当释放锁时,同时也是释放了信号量,信号量跟随减少,如果上一个线程还没有完成任务,则会进行入队等待操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HV7qaasy-1681014581788)(【精】各大厂问题汇总_files/Image [9].png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JJC2ts1d-1681014581788)(【精】各大厂问题汇总_files/Image [10].png)]

通过构造函数与三个内置类,我们就能知道ReentrantLock是怎么实现“公平锁”与“非公平锁“的。

公平锁会获取锁时会判断阻塞队列里是否有线程再等待,若有获取锁就会失败,并且会加入阻塞队列。

非公平锁获取锁时不会判断阻塞队列是否有线程再等待,所以对于已经在等待的线程来说是不公平的,但如果是因为其它原因没有竞争到锁,它也会加入阻塞队列。

进入阻塞队列的线程,竞争锁时都是公平的,因为队列为先进先出(FIFO)。

并发(Thread)中的常用方法

常用方法

start()

用来启动一个线程,当调用start方法后,系统才会开启一个新的线程来执行用户定义的子任务,在这个过程中,会为相应的线程分配需要的资源。

run()

该方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。

isAlive()

该方法是不需要用户来调用的,当通过start方法启动一个线程之后,当线程获得了CPU执行时间,便进入run方法体去执行具体的任务。注意,继承Thread类必须重写run方法,在run方法中定义具体要执行的任务。

Thread.sleep(long millis)

sleep相当于让线程睡眠,交出CPU,让CPU去执行其他的任务。sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。当前线程进入TIMED_WAITING状态

yield()

使当前线程从执行状态(运行状态)变为可执行态(就绪状态)。cpu会从众多的可执行态里选择,也就是说,当前也就是刚刚的那个线程还是有可能会被再次执行到的,并不是说一定会执行其他线程而该线程在下一次中不会执行到了。

wait()

该方法为Object的方法;当一个线程执行到wait()方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁(暂时失去机锁,wait(long
timeout)超时时间到后还需要返还对象锁);其他线程可以访问。即让当前线程阻塞,并且当前线程必须拥有此对象的monitor(即锁)

notify()

唤醒一个正在等待这个对象的monitor的线程,如果有多个线程都在等待这个对象的monitor,则只能唤醒其中一个线程;

notifyAll()

唤醒所有正在等待这个对象的monitor(监视器)的线程;

join()

启动线程后直接调用,即join()的作用是:“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了join()方法后面的代码,只有等到子线程结束了才能执行。join是基于wait实现的。先start(),再join()。

interrupt()

Thread.interrupt 的作用其实也不是中断线程,而是「通知线程应该中断了」,具体到底中断还是继续运行,应该由被通知的线程自己处理。
具体来说,当对一个线程,调用 interrupt() 时,① 如果线程处于被阻塞状态(例如处于sleep, wait, join
等状态),那么线程将立即退出被阻塞状态,并抛出一个InterruptedException异常。仅此而已。②
如果线程处于正常活动状态,那么会将该线程的中断标志设置为 true,仅此而已。被设置中断标志的线程将继续正常运行,不受影响。

过时的几个方法

stop()

强行终止线程(这个方法不推荐使用,因为stop和suspend、resume一样,也可能发生不可预料的结果)。
因为它在终止一个线程时会强制中断线程的执行,不管run方法是否执行完了,并且还会释放这个线程所持有的所有的锁对象。这一现象会被其它因为请求锁而阻塞的线程看到,使他们继续向下执行。这就会造成数据的不一致,我们还是拿银行转账作为例子,我们还是从A账户向B账户转账500元,我们之前讨论过,这一过程分为三步,第一步是从A账户中减去500元,假如到这时线程就被stop了,那么这个线程就会释放它所取得锁,然后其他的线程继续执行,这样A账户就莫名其妙的少了500元而B账户也没有收到钱。这就是stop方法的不安全性。

suspend()

suspend被弃用的原因是因为它会造成死锁。suspend方法和stop方法不一样,它不会破换对象和强制释放锁,相反它会一直保持对锁的占有,一直到其他的线程调用resume方法,它才能继续向下执行。
假如有A,B两个线程,A线程在获得某个锁之后被suspend阻塞,这时A不能继续执行,线程B在或者相同的锁之后才能调用resume方法将A唤醒,但是此时的锁被A占有,B不能继续执行,也就不能及时的唤醒A,此时A,B两个线程都不能继续向下执行而形成了死锁。这就是suspend被弃用的原因。

resume()

Thread.suspend很容易死锁。如果目标线程挂起来,他将给监听器上锁用以保护重要的系统资源,其他线程将不能访问该资源直到目标线程恢复工作。如果线程在恢复一个企图给监听器加锁的线程前调用了resume方法,则导致死锁。这种死锁称之为冰冻过程。

Thread使用注意

  • 线程执行的业务逻辑,放在run()方法中

  • 使用 thread.start() 启动线程

  • wait方法需要和notify方法配套使用

  • 守护线程必须在线程启动之前设置

  • 如果需要等待线程执行完毕,可以调用 join()方法

$多线程的四种写法

1实现runable接口,实现重写run方法 implements Runable()

2实现callable接口,implements Callable,实现重写call()方法,多线程

Callable oneCallable =new Tickets();

FutureTask oneTask =newFutureTask(oneCallable);

Thread t =new Thread(oneTask);

System.out.println(Thread.currentThread().getName());

t.start();

3.多线程

ExecutorService service=Executors.newFixedThreadPool(5);

Future result1=service.submit(zhiBoJian1);

4继承thread类,重写run方法,run()

public class ttt extend Thread{@Override public void run(){}}

分布式

Nginx做了负载均衡,那你知道Spring

cloud(Dubbo)也有负载均衡吗,那你说一下这两个负载均衡有什么区别,为什么两个地方都有负载均衡。

1.服务器端负载均衡Nginx

nginx是客户端所有请求统一交给nginx,由nginx进行实现负载均衡请求转发,属于服务器端负载均衡。

既请求有nginx服务器端进行转发。

2.客户端负载均衡Ribbon

Ribbon是从eureka注册中心服务器端上获取服务注册信息列表,缓存到本地,让后在本地实现轮训负载均衡策略。

既在客户端实现负载均衡。

应用场景的区别:

Nginx适合于服务器端实现负载均衡 比如Tomcat
,Ribbon适合与在微服务中RPC远程调用实现本地服务负载均衡,比如Dubbo、SpringCloud中都是采用本地负载均3.

衡。

Ribbon是Spring Cloud (本地)客户端负载均衡器

$$了解Spring cloud(Dubbo)框架不,看过源码没,了解实现原理不+1

Spring Cloud是一个全家桶式的技术栈,包含了很多组件:Eureka、Ribbon、Feign、Hystrix、Zuul

dubbo由于是二进制的传输,占用带宽会更少

springCloud是http协议传输,带宽会比较多,同时使用http协议一般会使用JSON报文,消耗会更大

dubbo的开发难度较大,原因是dubbo的jar包依赖问题很多大型工程无法解决

springcloud的接口协议约定比较自由且松散,需要有强有力的行政措施来限制接口无序升级

dubbo的注册中心可以选择zk,redis等多种,springcloud的注册中心只能用eureka或者自研

  • Eureka是微服务架构中的注册中心,专门负责服务的注册与发现。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wurVSORm-1681014581789)(【精】各大厂问题汇总_files/Image [11].png)]

Feign的一个关键机制就是使用了动态代理

* 首先,如果你对某个接口定义了@FeignClient注解,Feign就会针对这个接口创建一个动态代理

* 接着你要是调用那个接口,本质就是会调用 Feign创建的动态代理,这是核心中的核心

* Feign的动态代理会根据你在接口上的@RequestMapping等注解,来动态构造出你要请求的服务的地址

* 最后针对这个地址,发起请求、解析响应

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3y5vIPVr-1681014581789)(【精】各大厂问题汇总_files/Image [12].png)]

Ribbon就是专门解决这个问题的。它的作用是 负载均衡,默认使用的最经典的Round
Robin轮询算法Ribbon是和Feign以及Eureka紧密协作,完成工作的,具体如下:

*  **首先Ribbon会从 Eureka Client里获取到对应的服务注册表,也就知道了所有的服务都部署在了哪些机器上,在监听哪些端口号。**

*  **然后Ribbon就可以使用默认的Round Robin算法,从中选择一台机器**

*  **Feign就会针对这台机器,构造并发起请求。**

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZJVUZ3eh-1681014581790)(【精】各大厂问题汇总_files/Image.webp)]

Hystrix微服务架构中恐怖的服务雪崩问题但是如果积分服务都挂了,每次调用都要去卡住几秒钟干啥呢?有意义吗?当然没有!所以我们直接对积分服务熔断不就得了,比如在5分钟内请求积分服务直接就返回了,不要去走网络请求卡住几秒钟,这个过程,就是所谓的熔断!那人家又说,兄弟,积分服务挂了你就熔断,好歹你干点儿什么啊!别啥都不干就直接返回啊?没问题,咱们就来个降级:每次调用积分服务,你就在数据库里记录一条消息,说给某某用户增加了多少积分,因为积分服务挂了,导致没增加成功!这样等积分服务恢复了,你可以根据这些记录手工加一下积分。这个过程,就是所谓的降级。

为帮助大家更直观的理解,接下来用一张图,梳理一下Hystrix隔离、熔断和降级的全流程:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kQJausiY-1681014581790)(【精】各大厂问题汇总_files/Image [1].webp)]

Zuul,也就是微服务网关。 这个组件是负责网络路由的。可以做统一的降级、限流、认证授权、安全

总结:

最后再来总结一下,上述几个Spring Cloud核心组件,在微服务架构中,分别扮演的角色:

  • Eureka :各个服务启动时,Eureka Client都会将服务注册到Eureka Server,并且Eureka Client还可以反过来从Eureka Server拉取注册表,从而知道其他服务在哪里

  • Ribbon :服务间发起请求的时候,基于Ribbon做负载均衡,从一个服务的多台机器中选择一台

  • Feign :基于Feign的动态代理机制,根据注解和选择的机器,拼接请求URL地址,发起请求

  • Hystrix :发起请求是通过Hystrix的线程池来走的,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题

@FeignClient(name= “spring-cloud-producer”,fallback =
HelloRemoteHystrix.class)public interface HelloRemote { @RequestMapping(value
= “/hello”) public String hello(@RequestParam(value = “name”) String name); }

  • Zuul :如果前端、移动端要调用后端系统,统一从Zuul网关进入,由Zuul网关转发请求给对应的服务

  • zuul过滤器

以上就是我们通过一个电商业务场景,阐述了Spring Cloud微服务架构几个核心组件的底层原理。

文字总结还不够直观?没问题! 我们将Spring Cloud的5个核心组件通过一张图串联起来,再来直观的感受一下其底层的架构原理:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ss5TjMpx-1681014581791)(【精】各大厂问题汇总_files/Image [2].webp)]

ZooKeeper在这一套体系中起到什么作用?

简单的说,zookeeper= 文件系统+通知机制。

ZooKeeper 是一个开源的分布式协调服务,由雅虎创建,是 Google Chubby 的开源实现。 分布式应用程序可以基于 ZooKeeper
实现诸如数据发布/订阅、负载均衡、命名服务、分布式协 调/通知、集群管理、Master 选举、配置维护,名字服务、分布式同步、分布式锁和分布式队列 等功能。

  • Zookeeper对节点的watch:

  • Zookeeper的选举机制:

  • Client对于Zookeeper的ServerList的轮询机制:

zookeeper的实际运用场景:

场景一:有一组服务器向客户端提供某种服务(例如:我前面做的分布式网站的服务端,就是由四台服务器组成的集群,向前端集群提供服务),我们希望客户端每次请求服务端都可以找到服务端集群中某一台服务器,这样服务端就可以向客户端提供客户端所需的服务。

场景二:分布式锁服务。当分布式系统操作数据,例如:读取数据、分析数据、最后修改数据。

场景三:配置管理。在分布式系统里,我们会把一个服务应用分别部署到n台服务器上,这些服务器的配置文件是相同的。统一修改配置文件

场景四:为分布式系统提供故障修复的功能。集群管理是很困难的,在分布式系统里加入了zookeeper服务,能让我们很容易的对集群进行管理。

分布式锁有哪些实现方式?都有哪些优缺点?

简单的来说就是对资源操作时的一种控制策略

何为资源:能被程序访问的所有信息或是媒介;比如常见的文件,数据库,内存中的数据等

何为操作:像读,写,修改等

在单体应用或是单实例部署的情况下,所有的请求处理都是在同一个JVM中。可借助java
中的锁机制来控制对资源的访问比如:synchronized关键字,java.util.concurrent.*
中的api但是如果应用程序是分布式部署或是集群部署,请求会别分发到不同的实例上来处理。这样就会带来一个问题:如果资源是被一个JVM给锁定了(比如说通过synchronized),那么其它的JVM是如何知道资源的锁定状态呢?

实际上java在处理请求时没有办法直接跨越JVM的,这个时候只能是借用一个"第三方"来饰演资源的公共角色?这个“第三方”,就可以是我们常见的文件,数据库等外部的一切可访问资源

分布式锁的基本原则

1.互斥性:在同一时刻只能由一个客户端持久

2.独立性:在正常情况下,只有锁的拥有者才可以释放锁即可识别锁拥有者的身份

3.可用性:即在使用端出现异常的情况下,锁还是可以正常被使用的,即不会存在死锁或是长时间无资源可用的状态

常见实现有三种 1.基于数据库
2.基于Redis(一定要设置Key的超时时间,防止客户端在获取到了锁之后没有释放锁或是客户端在获致到锁之后宕机,锁不能被释放) 3.基于Zookeeper
4.其它第三实现(redisson / https://github.com/redisson/redisson)

推荐系统,考虑过分布式系统嘛,QPS怎么测试出来

jmeter吞吐量,压测工具 Throughput:吞吐量。默认表示每秒完成的请求数(Request per second); 若使用Transaction
Controller,则表示每秒处理的事务数(Transaction per second)

$数据结构

integer和int的自动装箱和拆箱以及为什么要用integer类

int是JAVA八大基本数据类型(baibyte,shor,int,long,char,boolean,float,double)之一。

  • Integer是int的包装类,int则是java的一种基本数据类型

  • Integer变量必须实例化后才能使用,而int变量不需要

  • Integer实际是对象的引用,当new一个Integer时,实际上是生成一个指针指向此对象;而int则是直接存储数据值 。

  • Integer的默认值是null,int的默认值是0

自动装箱Integer b = 88;拆箱int d = b;

Java中绝大部分方法或类都是用来处理类类型对象的, 如ArrayList集合类就只能以类作为他的存储对象
,而这时如果想把一个int型的数据存入list是不可能的,必须把它包装成类,也就是Integer才能被List所接受。

string类的用法

1、int length():获取长度

2、char charAt(int index);根据位置获取位置上某个字符。

3、int indexOf(int ch):返回的是ch在字符串中第一次出现的位置。

4、int indexOf(int ch,int fromIndex):从fromIndex指定位置开始,获取ch在字符串中出现的位置。

5、int indexOf(String str):返回的是str在字符串中第一次出现的位置。

6、int indexOf(String str,int fromIndex):从fromIndex指定位置开始,获取str在字符串中出现的位置。

7、int lastIndexOf(String str):反向索引。

8、boolean contains(str);字符串中是否包含某一个子串

9、boolean isEmpty():原理就是判断长度是否为0。

10、boolean startsWith(str);字符串是否以指定内容开头。

11、boolean endsWith(str);字符串是否以指定内容结尾。

12、boolean equals(str);判断字符内容是否相同

13、boolean.equalsIgnorecase();判断内容是否相同,并忽略大小写。

14、String trim();将字符串两端的多个空格去除

15、int compareTo(string);对两个字符串进行自然顺序的比较

16、String toUpperCsae() 大转小 String toLowerCsae() 小转大

17、 String subString(begin); String subString(begin,end);获取字符串中子串

18、String replace(oldchar,newchar);将字符串指定字符替换。

JDK1.8都有什么新特性

  • Lambda表达式,这意味着java也开始承认了函数式编程,并且尝试引入其中。代码更少

  • @FunctionalInterface注解来定义函数式接口

  • 接口中可以定义默认实现方法和静态方法,在接口中可以使用default和static关键字来修饰接口中定义的普通方法

  • default关键字

  • Optional容器,可以快速的定位NPE,并且在一定程度上可以减少对参数非空检验的代码量。最大化减少空指针异常

  • 新的日期API LocalDate | LocalTime | LocalDateTime

      • 之前使用的java.util.Date月份从0开始,我们一般会+1使用,很不方便,java.time.LocalDate月份和星期都改成了enum
      • java.util.Date和SimpleDateFormat都不是线程安全的,而LocalDate和LocalTime和最基本的String一样,是不变类型,不但线程安全,而且不能修改。
      • java.util.Date是一个“万能接口”,它包含日期、时间,还有毫秒数,更加明确需求取舍
      • 新接口更好用的原因是考虑到了日期时间的操作,经常发生往前推或往后推几天的情况。用java.util.Date配合Calendar要写好多代码,而且一般的开发人员还不一定能写对。
  • Stream 的并行操作依赖于 Java7 中引入的 Fork/Join 框架(JSR166y)来拆分任务和加速处理过程。(关键字:递归分合、分而治之。)

    • Fork/Join 框架:就是在必要的情况下,将一个大任务,进行拆分(fork)成若干个小任务(拆到不可再拆时),再将一个个的小任务运算的结果进行 join 汇总。

    • Stream操作的三个步骤:创建stream,中间操作(过滤、map),终止操作

  • 方法与构造函数引用

  • 局部变量限制

$ConcurrentHashMap原理。concurrentHashmap是安全的吧,那你知道concurrentHashmap的size()怎么求吗,在并发场景下,需要怎么设定锁?

  • 在JDK1.7版本中,ConcurrentHashMap的数据结构是由一个Segment数组和多个HashEntry组成。

JDK1.8的实现已经摒弃了Segment的概念,而是直接用 Node数组+链表+红黑树的数据结构
来实现,并发控制使用Synchronized和CAS来操作, 整个看起来就像是优化过且线程安全的HashMap

* Node是ConcurrentHashMap存储结构的基本单元,继承于HashMap中的Entry,用于存储数据,Node数据结构很简单,就是一个链表,但是只允许对数据进行查找,不允许进行修改

* TreeNode继承与Node,但是数据结构换成了二叉树结构,它是红黑树的数据的存储结构,用于红黑树中存储数据,当链表的节点数大于8时会转换成红黑树的结构,他就是通过TreeNode作为存储结构代替Node来转换成黑红树 

* TreeBin从字面含义中可以理解为存储树形结构的容器,而树形结构就是指TreeNode,所以TreeBin就是封装TreeNode的容器,它提供转换黑红树的一些条件和锁的控制
  • 在JDK1.8版本中,对于size的计算,在扩容和addCount()方法就已经有处理了,JDK1.7是在调用size()方法才去计算,其实在并发集合中去计算size是没有多大的意义的,因为size是实时在变的,只能计算某一刻的大小,但是某一刻太快了,人的感知是一个时间段,所以并不是很精确

JDK 1.8的ConcurrentHashMap又采用了哪些技术呢?

悲观锁策略(阻塞同步)互斥同步最主要的问题就是线程阻塞和唤醒带来的性能问题,因此这种同步也称阻塞同步。从处理方式来讲,互斥同步是一种悲观的并发策略,总是认为只要不去做正确的的同步措施(列入加锁),那肯定会出现问题,无论共享数据是否出现竞争,都需要加锁。(需要线程挂起)

乐观锁策略(非阻塞同步)基于冲突检测的乐观并发策略就是先进行操作,如果没有其他线程争用共享资源,那么操作就成功了。如果共享数据有争用,出现冲突,那就再采取其他的不就措施。(不需要线程挂起)

$jdk hashmap 底层存储?mysql用的啥引擎?为啥数据库底层用B+树不用红黑树?

  • 在Jdk1.8中HashMap的实现方式做了一些改变,但是基本思想还是没有变得,只是在一些地方做了优化,下面来看一下这些改变的地方,数据结构的存储由数组+链表的方式,变化为数组+链表+红黑树的存储方式,当链表长度超过阈值(8)时,将链表转换为红黑树。在性能上进一步得到提升。

  • MyISAM存储引擎 InnoDB存储引擎(MySQL5.5后的默认) MEMORY存储引擎

1MyISAM 更适合读密集的表,不支持事务,用的是表锁 而 InnoDB 更适合写密集的的表,支持事务,行级锁(并发效率高)
在数据库做主从分离的情况下,经常选择MyISAM 作为主库的存储引擎。

2可使用 MEMORY 来存储非永久需要的数据,或能够从基于磁盘的表中重新生成的数据。 MEMORY(记忆)

1.红黑树必须存在内存里的,数据库表太大了,存不进去。

2.即使你找到了把红黑树存进硬盘的方法,红黑树查找一个节点最多要查logN层,每一层都是一个内存页(虽然你只是想找一个节点,但硬盘必须一次读一个页。。),那么一共logN次IO,伤不起阿!

讲讲java容器(吹了半天HashMap和ConcurrentHashMap)

hashmap扩容是需要重新哈希吗?如果二次哈希后还是哈希冲突呢?

需要

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YcvYs5d0-1681014581792)(【精】各大厂问题汇总_files/1036837-20200510205147888-1261461043.gif)]

Java在处理hash冲突的时候使用了链表

图中的0到10号
的方块就是entry(键值对),如果发生hashcode的冲突,就会像4号方块那样,开始向后追加,注意看4号方块的next的属性,那个属性不是null,而是指向了一个方块

什么是哈希冲突?

哈希冲突就是指当插入一个Entry时,将key经过hash计算出的下标中已经存在另一个Entry,这时就会产生哈希冲突。

2、如何解决?

解决哈希冲突的方式有四种: 1、开放地址法(包括线性探测、二次探测、伪随机探测等) 2、链地址法 3、再哈希发 4、建立一个公共的溢出区

众所周知,HasMap的底层是通过 数组 + 链表 实现的。此处的“ 链表
”就是HashMap为我们提供的解决哈希冲突的方法。也就是 连地址法

HashMap扩容时限插入还是先扩容?

当然是先扩容!!!

①HashMap的初始容量默认为16,负载因子默认为0.75。当插入的数据数量大于二者之积时就要考虑进行扩容。

②HashMap的扩容是需要将其扩至2倍,再哈希计算将所有数据移至新的HashMap中,如果先插入,那我岂不是要多计算一次哈希值,多一次插入吗?

hashmap和hashtable的区别?

我们都知道HashMap在多线程情况下,在put的时候,插入的元素超过了容量(由负载因子决定)的范围就会触发 扩容操作,就是rehash
,这个会重新将原数组的内容重新hash到新的扩容数组中,在多线程的环境下,存在同时其他的元素也在进行put操作,如果hash值相同,可能出现同时在同一数组下用链表表示,造成闭环,导致在get时会出现死循环,所以HashMap是线程不安全的。

我们来了解另一个键值存储集合HashTable,它是线程安全的,它在所有涉及到多线程操作的都加上了synchronized关键字来锁住整个table,这就意味着所有的线程都在竞争一把锁,在多线程的环境下,它是安全的,但是无疑是效率低下的。

其实HashTable有很多的优化空间,锁住整个table这么粗暴的方法可以变相的柔和点,比如在多线程的环境下,对不同的数据集进行操作时其实根本就不需要去竞争一个锁,因为他们不同hash值,不会因为rehash造成线程不安全,所以互不影响,
这就是锁分离技术,将锁的粒度降低,利用多个锁来控制多个小的table

链表和数组有什么区别?

1)数组在内存中是逐个存放的,也就是说倘若数组的第一个元素在地址A,则数组第二个元素就在地址A+1。

而链表则不是,链表每个节点没有相对固定的位置关系。某个节点在地址A其后的节点不一定是A+1,而在内存的其他空闲区域,呈现一种随机的状态。

2)数组一旦显式的被申明后,其 大小就固定 了,不能 动态进行扩充 。而链表则可以,可以动态生成节点并且添加到已有的链表后面。

3)链表灵活,但是空间和时间额外耗费较大;数组大小固定,元素位置固定,但是操作不灵活,且容易浪费空间,但是时间耗费较小,尤其是元素变化不大的时候效率很高。双向链表比单向的更灵活,但是空间耗费也更大

链表的特性是在中间任意位置添加删除元素的都非常的快,不需要移动其它的元素

链表顾名思义,要把各个元素链接起来才算撒。

通常链表每一个元素都要保存一个指向下一个元素的指针(单链表)。

双链表的化每个元素即要保存到下一个元素的指针,还要保存一个上一个元素的指针。

循环链表则把最后一个元素中保存下一个元素指针指向第一个元素。

数组是一组具有相同类型和名称的变量的集合。这些变量称为数组的元素,每个数组元素都有一个编号,这个编号叫做下标,我们可以通过下标来区别这些元素。数组元素的个数有时也称之为数组的长度。

数组查改方便,链表增删效率高。

写一个list删除目标元素的函数,然后写个测试用例测试一下能不能通,为什么不能正向遍历

声明一个新的list存放符合要求的数据,迭代器删除,使用JDK1.8的List过滤功能,代码最简洁

list = list.stream().filter(a -> a%2==0).collect(Collectors.toList());

//查找身高在1.8米及以上的男生 List boys = studentList .stream()
.filter(s->s.getGender() && s.getHeight() >=
1.8).collect(Collectors.toList());

因为注释部分的代码将指针指向了最后,所以可以反向遍历

三,lterator和Listlterator的区别

(1)ListIterator有add()方法,可以向List中添加对象,而Iterator不能

(2)ListIterator和Iterator都有hasNext()和next()方法,可以实现顺序向后遍历,但是ListIterator有hasPrevious()和previous()方法,可以实现逆向(顺序向前)遍历。Iterator就不可以。

(3)ListIterator可以定位当前的索引位置,nextIndex()和previousIndex()可以实现。Iterator没有此功能。

(4)都可实现删除对象,但是ListIterator可以实现对象的修改,set()方法可以实现。Iierator仅能遍历,不能修改。

因为ListIterator的这些功能,可以实现对LinkedList等List数据结构的操作。其实,数组对象也可以用迭代器来实现。

二叉树镜像,时间复杂度,空间复杂度

遍历二叉树的无论按照哪一种次序进行遍历,对n个结点时间复杂度为O(n),所需要的辅助空间为树的深度O(d).

二叉树的四种遍历方式分别是:先序、中序、后序和层次。 它们的时间复杂度都是O(n),因为它们只访问每个节点一次,不存在多余的访问。
三种深度优先遍历方法(先序、中序和后序)的时间复杂度是O(h),其中h是二叉树的深度,额外空间是函数递归的调用栈产生的,而不是显示的额外变量。层次遍历的时间复杂度是O(w),其中w是二叉树的宽度(拥有最多节点的层的节点数),因为层次遍历通常是用一个queue来实现的。

请评估一下程序的执行结果?

blockngqueue
BlockingQueue

A. true true true 1 3 B. true true true (阻塞) C. false false false null 0 D.
false false false (阻塞) 参考答案:D

public class SynchronousQueueQuiz { public static void main(String[] args)
throws Exception { BlockingQueue queue = new SynchronousQueue<>();
System. out .print(queue.offer(1) + " "); System. out .print(queue.offer(2) +
" "); System. out .print(queue.offer(3) + " "); System. out
.print(queue.take() + " "); System. out .println(queue.size()); } }

算法

按数量级递增排列,常见的时间复杂度有:常数阶 O(1) ,对数阶 **O(log 2 n),**线性阶 O(n), 线性对数阶
**O(nlog 2 n),**平方阶 **O( n 2),**立方阶 O( n 3),…, k次方阶 **O( n
k),**指数阶 O( 2 n)

当一个算法的空间复杂度为一个常量,即不随被处理数据量n的大小而改变时,可表示为O(1);当一个算法的空间复杂度与以2为底的n的对数成正比时,可表示为0(10g2n);当一个算法的空I司复杂度与n成线性比例关系时,可表示为0(n).若形参为数组,则只需要为它分配一个存储由实参传送来的一个地址指针的空间,即一个机器字长空间;若形参为引用方式,则也只需要为其分配存储一个地址的空间,用它来存储对应实参变量的地址,以便由系统自动引用实参变量。

给定一个二叉搜索树(BST),找到树中第 K 小的节点

在二叉查找树种:

(1)若任意结点的左子树不空,则左子树上所有结点的值均小于它的根结点的值。

(2)任意结点的右子树不空,则右子树上所有结点的值均大于它的根结点的值。

(3)任意结点的左、右子树也分别为二叉查找树。

(4)没有键值相等的结点。

二叉树的遍历次序:

前序顺序是根节点排最先,然后同级先左后右;中序顺序是先左后根最后右;后序顺序是先左后右最后根。

树相关的题目,第一眼就想到递归求解,左右子树分别遍历。联想到二叉搜索树的性质,root 大于左子树,小于右子树,如果左子树的节点数目等于 K-1,那么
root 就是结果,否则如果左子树节点数目小于 K-1,那么结果必然在右子树,否则就在左子树。因此在搜索的时候同时返回节点数目,跟 K
做对比,就能得出结果了。 public class TreeNode { int val; TreeNode left; TreeNode right;
TreeNode(int x) { val = x; }} class Solution { private class ResultType {
boolean found; // 是否找到 int val; // 节点数目 ResultType(boolean found, int val) {
this.found = found; this.val = val; } } public int kthSmallest(TreeNode root,
int k) { return kthSmallestHelper(root, k).val; } private ResultType
kthSmallestHelper(TreeNode root, int k) { if (root == null) { return new
ResultType(false, 0); } ResultType left = kthSmallestHelper(root.left, k); //
左子树找到,直接返回 if (left.found) { return new ResultType(true, left.val); } //
左子树的节点数目 = K-1,结果为 root 的值 if (k - left.val == 1) { return new
ResultType(true, root.val); } // 右子树寻找 ResultType right =
kthSmallestHelper(root.right, k - left.val - 1); if (right.found) { return new
ResultType(true, right.val); } // 没找到,返回节点总数 return new ResultType(false,
left.val + 1 + right.val); } }

给定一个链表,删除链表的倒数第 N 个节点,并且返回链表的头结点

public class Num19 { public static void main(String[] args) { int[] arr = {3,
2, 5, 8, 4, 7, 6, 9}; ListNode head = new ListNode(arr); head =
removeNthFromEnd(head, 8); System.out.println(head.toString()); } public
static ListNode removeNthFromEnd(ListNode head, int n) {
//当链表为空或者要删除的节点小于等于0的时候,直接返回head if (head == null || n <= 0) return head;
//建立一个虚拟的表头结点,因为需要删除的节点有可能是头结点, // 所以建立虚拟头结点可以不用分是否是头结点两种情况 ListNode tempHead
= new ListNode(0); tempHead.next = head; ListNode p = tempHead, q = tempHead;
//p指针比q指针先跑n次 for (int i = 0; i < n; i++) { //如果p为空的时候,说明这个节点的长度不足n,返回head if
(p == null ) return head; else { p = p.next; } } //p,q一起往前跑,直到p的next为空, //
q所指向的下一个结点就是要删除的元素的位置 while (p.next != null) { p = p.next; q = q.next; }
//删除q指向的节点的下一个元素 q.next = q.next.next; //删除虚拟头结点 return tempHead.next; }
static class ListNode { int val; ListNode next; ListNode(int x) { val = x; }
public ListNode(int[] arr) { if (arr == null || arr.length == 0) throw new
IllegalArgumentException(“arr can to be empty”); this.val = arr[0]; ListNode
cur = this; for (int i = 1; i < arr.length; i++) { cur.next = new
ListNode(arr[i]); cur = cur.next; } } @Override public String toString() {
StringBuilder res = new StringBuilder(); ListNode cur = this; while (cur !=
null) { res.append(cur.val + “->”); cur = cur.next; } res.append(“NULL”);
return res.toString(); } } }

反转链表按k,最长重复子串

给定一个整数数组和一个整数,返回两个数组的索引,这两个索引指向的数字

的加和等于指定的整数。需要最优的算法,分析算法的空间和时间复杂度

public int[] twoSum(int[] nums, int target) { if(nums==null || nums.length<2)
return new int[]{0,0}; HashMap map = new HashMap Integer>(); for(int i=0; i return new int[]{map.get(nums[i]), i}; }else{ map.put(target-nums[i], i); } }
return new int[]{0,0};} 分析:空间复杂度和时间复杂度均为 O(n)

已知 sqrt (2)约等于 1.414,要求不用数学库,求 sqrt (2)精确到小数点后 10 位。

public class Main { public static void main(String[] args) { double l = 1.41;
double r = 1.42; double mid = (r+l)/2; double flag = 0.0000000001; while(r-l >
flag){ mid = (r+l)/2; if(mid*mid < 2) l = mid; else r = mid; }
System.out.println(mid); } }

如何实现一个高效的单向链表逆序输出?

直接递归(简单,但O(n)空间复杂度不支持大数据量)

// 直接递归实现核心代码片段 public void reverse(head){ // 递归终止条件 if(head.next == null){
print(head); return; } // 下一层需要做的事儿 reverse(head.next); // 本层需要做的事儿
print(head); }

JAVA算法:最长重复子串问题(JAVA解题)

package com.bean.algorithm.basic; public class LongestRepeatingSubstring { //
Returns the longest repeating non-overlapping // substring in str static
String longestRepeatedSubstring(String str) { int n = str.length(); int
LCSRe[][] = new int[n + 1][n + 1]; String res = “”; // To store result int
res_length = 0; // To store length of result // building table in bottom-up
manner int i, index = 0; for (i = 1; i <= n; i++) { for (int j = i + 1; j <=
n; j++) { // (j-i) > LCSRe[i-1][j-1] to remove // overlapping if (str.charAt(i

    1. == str.charAt(j - 1) && LCSRe[i - 1][j - 1] < (j - i)) { LCSRe[i][j] =
      LCSRe[i - 1][j - 1] + 1; // updating maximum length of the // substring and
      updating the finishing // index of the suffix if (LCSRe[i][j] > res_length) {
      res_length = LCSRe[i][j]; index = Math.max(i, index); } } else { LCSRe[i][j] =
      0; } } } // If we have non-empty result, then insert all // characters from
      first character to last // character of String if (res_length > 0) { for (i =
      index - res_length + 1; i <= index; i++) { res += str.charAt(i - 1); } }
      return res; } // Driver program to test the above function public static void
      main(String[] args) { String str = “aabaabaaba”;
      System.out.println(longestRepeatedSubstring(str)); } }

给一个数组,找出出现次数大于数组长度一半的那个数。(我直接无脑用哈希表,面试官皱着眉头说能优化不,我想了半天没想出来)。

package test; public class Search { public static void main(String[] args) {
//System.out.println(“Hello World!”); Integer []a={4,5,5,2,3,5,2,5,5,5};
Integer len= a.length; Integer result = search(a,len);
System.out.println(result); } public static Integer search(Integer A[],Integer
len){ if(Anull || len<=0) { return -1; } Integer k=null, j=0; for(Integer
i=0;i0) { k=A[i]; } if(k==A[i]) { ++j; }else { --j; } }
return k; } }

给一个链表1->2->3->4->5->6->7 和 一个数字n每n个反转一次链表。如 n = 2时,2->1->4->3->6->5->7;n =

3时,3->2->1>6->5->4->7

罗马数字转整数 leetcode13

class Solution { public int romanToInt(String s) { Map map
= new HashMap<>(); map.put(‘I’,1); map.put(‘V’,5); map.put(‘X’,10);
map.put(‘L’,50); map.put(‘C’,100); map.put(‘D’,500); map.put(‘M’,1000);
//用map结构来存储字母与带表的值 char[] temp = s.toCharArray();//将其转化为数组会索引时会更快 int length =
temp.length;//现记录长度,避免循环的时候每次都要去计算 int sum = 0;//原来保存最后的值 for(int
i=0;i map.get(temp[i]);//获取当前字母的值 int nextTemp = map.get(temp[i+1]);//获取相邻字母的值 sum
+= curTemp < nextTemp ? -curTemp : curTemp;//比较,如果当前值小于右边,就应该减,否则加 } return
sum+ map.get(temp[length-1]);//之前循环的时候并没有循环最后一个,因为最后一个无论如何都是会被加上的,所以此时加上 } }

反转链表,删除排好序数组中重复元素

/** * 单链表相关操作 * * @author yinhr * / public class LinkedListUtil { /* *
反转链表(3指针pre head headNext) * * @param head * @return / ListNode
reverse(ListNode head) { ListNode pre = null; ListNode headNext; while (head
!= null) { headNext = head.next; head.next = pre; pre = head; head = headNext;
} return pre; } /
* * 删除有序链表中重复值,重复值不保留(头结点+三指针pre head end) * * @param head *
@return / ListNode deleteRepeat(ListNode head) { // 创建头节点用于返回链表 ListNode
headNode = new ListNode(0, head); ListNode pre = headNode; // 用于寻找第一个不同值节点
ListNode end; while (head != null && head.next != null) { // 当前值和它的下个值不等则移动两指针
if (head.value != head.next.value) { pre = head; head = head.next; } else {
end = head.next.next; while (end != null && end.value == head.value) { end =
end.next; } pre.next = end; head = end; } } return headNode.next; } /
* *
删除有序链表中重复值,重复值保留一个(1)两指针listHead head,相邻两个相等删除后一个(2)三指针listHead head end * *
@param head * @return */ ListNode deleteRepeatKeepOne(ListNode head) { //
用于返回链表 ListNode listHead = head; // (1)用于寻找第一个不同值节点 ListNode end; while (head
!= null && head.next != null) { // 当前值和它的下个值不等则移动指针 if (head.value !=
head.next.value) { head = head.next; } else { // (2)也可只用list.next =
list.next.next; end = head.next.next; while (end != null && end.value ==
head.value) { end = end.next; } head.next = end; head = end; } } return
listHead; } }

数据库

大数据

HBase底层数据存储的结构

HBase底层存储原理——我靠,和cassandra本质上没有区别啊!都是kv
列存储,只是一个是p2p另一个是集中式而已!

首先HBase不同于一般的关系 数据库,

  • 它是一个适合于 非结构化数据 存储的数据库.

  • 另一个不同的是HBase 基于列的 而不是基于行的模式.

什么是BigTable:

  • Bigtable是一个 疏松的分布式的持久的多维排序的map ,

  • 这个map被 行键,列键,和时间戳索引.

  • 每一个值都是连续的byte数组.

Hadoop wiki的 **
HBase架构** 页面提到:

  • HBase使用和Bigtable非常相同的数据模型.

  • 用户存储数据行在一个表里. 一个数据行拥有一个可选择的键和任意数量的列.

  • 表是疏松的 存储的,因此 用户可以 给行定义各种不同的列.

关系型数据库

sql执行顺序

(1)from

(2) on

(3) join

(4) where

(5)group by(开始使用select中的别名,后面的语句中都可以使用)

(6) avg,sum…

(7)having

(8) select

(9) distinct

(10) order by

$MySQL 的数据如何恢复到任意时间点?

恢复到任意时间点以定时的做全量备份,以及备份增量的 binlog 日志为前提。恢复到任意时间点首先将全量备份恢复之后,再此基础上回放增加的 binlog
直至指定的时间点。

从 innodb 的索引结构分析,为什么索引的 key 长度不能太长?

key 太长会导致一个页当中能够存放的 key 的数目变少,间接导致索引树的页数目变多,索引层次增加,从而影响整体查询变更的效率。

MySQL用的什么索引?

btree或哈希hash

在计算机数据结构(不懂数据结构的自行充电)体系中,为了加速查找的速度,常见的数据结构有两种:

  • Hash哈希结构,例如Java中的HashMap,这种数据组织结构可以让查询/插入/修改/删除的平均时间复杂度都为O(1);

  • Tree 树 结构 , 这种数据组织结构可以让查询/插入/修改/删除的平均时间复杂度都为O(log(n));

确实用HASH索引更快,因为每次都只查询一条信息(重名的雇员姓名也才几条而已),但实际上业务对于SQL的应用场景是:

  • orderby 需要排个序

  • groupby 还要分个组

  • 还要比较大小 大于或小于等等

这种情况下如果继续用HASH类型做索引结构,其时间复杂度会从O(1)直接退化为O(n),相当于全表扫描了,而Tree的特性保证了不管是哪种操作,依然能够保持O(log(n))的高效率,有种我自岿然不动的赶脚!所以抛开应用场景谈设计其实是耍流浪(比如很多java程序员被安利阿里的fastjson比jackson快,故而抛弃jackson一样),实际上MySQL中也不把HASH类型的索引作为主流索引。

为什么MySQL不能支撑高并发,你有做过测试吗?

由于 mysql是一个连接给一个线程
,当并发高的时候,每秒需要几百个甚至更多的线程,其中创建和销毁线程还好说,大不了多耗费点内存,线程缓存命中率下降还有创建销毁线程的性能增加问题—
这个问题不是特别大,
重点是mysql底层瞬间处理这几百个线程提交的sql(有时候一个页面会有10多条sql,cpu一次只能处理一条sql)会导致cpu的
上下文切换
,性能抖动,然后性能下降
,虽然mysql对多核cpu有做了优化,但是也并不完美,
也仅限于24核心上下 (也许是64以内,有做过测试,超过24再增加核心效果不明显)。 所以mysql企业版本推出了线程池技术
,另外percona的mysql有免费的线程池提供。可以极大提高高并发下mysql的性能(也就比没有大量并发的时候低20-30%左右这样子),如果要讲解线程池
我一句半句也说不清楚,你可以百度下mysql线程池去了解相关原理。

大量数据迁移

四个步骤:存量,增量,校验,切流

增量数据迁移

  • DTS: 阿里云的DTS算是一条龙服务了,在提供存量数据迁移的同时也提供了增量数据迁移,只不过需要按量收费。

  • 服务双写:比较适合于系统没有切换的迁移,也就是只换了存储但是系统还是同一个,比如说分库分表,redis数据同步等,这个的做法比较简单直接在代码里面同步的去写入需要迁移的数据,但是由于不是同一个数据库就不能保证事务,有可能导致迁移数据的时候会出现数据丢失,这个过程通过后续的数据校验会进行解决。

  • MQ异步写入:这个可以适用于所有的场景,当有数据修改的时候发送一个MQ消息,消费者收到这个消息之后再进行数据更新。这个和上面的双写有点类似,但是他把数据库的操作变成了MQ异步了出问题的概率就会小很多

  • 监听binlog: 我们可以使用之前说过的canal或者其他的一些开源的如databus去进行binlog监听,监听binlog的方式 就和上面的消息MQ方式一样,只是发送消息的这一步被我们省略了。这个方式的一个开发量来说基本是最小的。

这么多种方式我们应该使用哪种呢?我个人来说是比较推荐监听binlog的做法的,监听binlog减少开发成本,我们只需要实现consumer逻辑即可,数据能保证一致性,因为是监听的binlog这里不需要担心之前双写的时候不是一个事务的问题。

Nosql

$redis的使用场景

1、Redis底层ZSet跳表是如何设计与实现的

zset的两种实现方式

ziplist(压缩链表):满足以下两个条件的时候

元素数量少于128的时候

每个元素的长度小于64字节

skiplist(跳跃链表):不满足上述两个条件就会使用跳表,具体来说是组合了map和skiplist

map用来存储member到score的映射,这样就可以在O(1)时间内找到member对应的分数

skiplist按从小到大的顺序存储分数

skiplist每个元素的值都是[score,value]对

ziplist原理

每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个元素保存元素的分值。压缩列表中的元素按照分数从小到大依次紧挨着排列,有效减少了内存空间的使用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cszcv5ZI-1681014581792)(【精】各大厂问题汇总_files/Image [13].png)]

skiplist原理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JXgTOM37-1681014581793)(【精】各大厂问题汇总_files/Image [14].png)]

上图用a,b,c,d,e五种有序链表说明了跳跃表的motivation.

[a]单链表:查询时间复杂度O(n)

[b]level-2单链表:每隔一个节点为一个level-2节点,每个level-2节点有2个后继指针,分别指向单链表中的下一个节点和下一个level-2节点。查询时间复杂度为O(n/2)

[c]level-3单链表:每隔一个节点为一个level-2节点,每隔4个节点为一个level-3节点,查询时间复杂度O(n/4)

[d]指数式单链表:每隔一个节点为一个level-2节点,每隔4个节点为一个level-3节点,每隔8个节点为一个level-4节点(每2^i个节点的level为i+1),查询时间复杂度为O(log2N)

[e]跳跃表:各个level的节点个数同指数式单链表,但出现的位置随机,查询复杂度是O(logN)

2、Redis底层ZSet实现压缩列表和跳表如何选择

ziplist(压缩链表):满足以下两个条件的时候

元素数量少于128的时候

每个元素的长度小于64字节

skiplist(跳跃链表):不满足上述两个条件就会使用跳表

3、Redis高并发场景热点缓存如何重建

1.解决Redis把内存爆满的三种方法

1.1 定期删除

让程序员均匀设置过期时间,Redis定时去扫描数据是否有过期的过期的删除掉,全盘扫描太浪费资源了采用的是随机选取一部分缓解内存压力,但是有一部分键值对每次都没有被随机选择算法选中。在定期删除的基础上添加了惰性删除

1.2 惰性删除

那些逃过随机选择算法的,一但被查询的时候先查看是否过期,过期就直接删除,,存在问题,还是有很多数据到了过期时间但是没有去查询删除而存在内存中,造成内存爆满。为解决内存不足时该如何解决最后出现了内存淘汰策略

1.3 内存淘汰策略

以下8种策略,给出了在内存不足时,该如果决策

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AKr7Nq4u-1681014581793)(【精】各大厂问题汇总_files/Image [15].png)]

  1. 缓存穿透——缓存击穿——缓存雪崩

缓存穿透是某个热点数据访问缓存和数据库中都不存在(访问数据库中不存在的数据)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bx9QzEaQ-1681014581794)(【精】各大厂问题汇总_files/Image [16].png)]

缓存击穿
是某个热点数据访问缓存中不存在 数据库中存在(访问缓存中不存在的数据)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WoxUzNJj-1681014581794)(【精】各大厂问题汇总_files/Image [17].png)]

缓存雪崩
是大量数据访问缓存都失效,大量请求去访问数据库把数据库给怼宕机了

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HfNrNR7v-1681014581795)(【精】各大厂问题汇总_files/Image [18].png)]

3. 如何解决线上缓存穿透问题

过期时间均匀分布+热点数据永不过期

3.1 缓存击穿(缓存失效)

解决思路:

只需要加上过期时间随机值,能够保证上万条数据不同时过期,减少对数据库的压力,同一时刻缓存中还是有数据的

redisUtil.set(key, JSON.toJSONString(cookies), 24 * 60 * 60 ,
TimeUnit.SECONDS);

new Random().nextInt(30)60; 随机产生一个[0~3060)int类型的数值

redisUtil.set(key, JSON.toJSONString(cookies), 24 * 60 * 60 + new
Random().nextInt(30)*60, TimeUnit.SECONDS);

3 .2 缓存穿透(数据删除)

上万条数据同时请求数据,缓存中没有数据,去数据库中数据库中也没有数据,穿透所有的持久层

解决思路:

为了防止缓存穿透,在第一条数据进行请求的时候发现缓存和数据库中都没有要求请的数据,就在缓存中放一个空串{},下次再次请求的时,读取到缓存中的值,判断值如果是空串则返回null
,否则返回对应的值。

4. 基于DCL机制解决热点缓存并发重建问题实战

上万条请求同时请求某一条数据

方法一:

添加同步锁机制synchronized,,,,

(双重检测锁)解决突发热点并发重建导致DB压力暴增

让第一个请求先查Redis缓存,若不存在查DB,查到写入Redis缓存

后续的请求再访问Redis时,直接取值

synchronized(this){

//Redis缓存中取数据,

//缓存中不存在该条数据,请求DB,存到Redis中

}

方法二:

使用Redis分布式锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Xr0rfIIH-1681014581795)(【精】各大厂问题汇总_files/Image [19].png)]

引入依赖

org.redisson

redissson

3.6.5

@Autowired

private Redisson redisson;

RLock hotCacheCreateLock = redisson.getLock(key);

hotCacheCreateLock.locak(); //相当于执行了setnx(k,v) // 加锁

try{

//Redis缓存中取数据,

//缓存中不存在该条数据,请求DB,存到Redis中

} finally {

hotCacheCreateLock.unlock(); //删除锁 del k 删除key

}

  1. Redis分布式锁解决缓存与数据库双写不一致问题实战

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MaaNILAF-1681014581796)(【精】各大厂问题汇总_files/Image [20].png)]

解决办法:Redis 分布式锁

@Autowired

private Redisson redisson;

RLock hotCacheCreateLock = redisson.getLock(key);

hotCacheCreateLock.locak(); //相当于执行了setnx(k,v) // 加锁

try{

//Redis缓存中取数据,

RLock updateCreateLock = redisson.getLock(key2);

updateCreateLock.locak(); //相当于执行了setnx(k,v) // 加锁

try{

//缓存中不存在该条数据,读取DB中数据,并存到Redis中

} finally {

updateCreateLock.unlock(); //删除锁 del k 删除key2

}

} finally {

hotCacheCreateLock.unlock(); //删除锁 del k 删除key

}

优化:可以用读写锁

对读多写少进行优化——所有的读操作并行执行,读和写互斥串行执行

读数据库中的数据

@Autowired

private Redisson redisson;

RLock hotCacheCreateLock = redisson.getLock(key);

hotCacheCreateLock.locak(); //相当于执行了setnx(k,v) // 加锁

try{

//Redis缓存中取数据,

//RLock updateCreateLock = redisson.getLock(key2);

RReadWriteLock readWriteLock = redisson.getReadWriteLock(ke2)

RLock rLock = readWriteLock.readLock();

rLock.locak(); // 加读锁 setnx(k,v)

try{

//缓存中不存在该条数据,读取DB中数据,并存到Redis中

} finally {

rLock.unlock(); //删除锁

}

} finally {

hotCacheCreateLock.unlock(); //删除锁 del k 删除key

}

更新数据库中数据

RReadWriteLock readWriteLock = redisson.getReadWriteLock(ke2)

RLock wLock = readWriteLock.writeLock();

wLock.locak(); // 加写锁 setnx(k,v)

try{

//修改数据库中的数据操作

} finally {

wLock.unlock();

}

读数据缓存锁优化

@Autowired

private Redisson redisson;

RLock hotCacheCreateLock = redisson.getLock(key);

//hotCacheCreateLock.locak(); //相当于执行了setnx(k,v) // 加锁

hotCacheCreateLock.tryLock(2,TimeUnit.SECONDS); //等待2秒后锁失效(设置合适的时间)

try{

//Redis缓存中取数据,

//RLock updateCreateLock = redisson.getLock(key2);

RReadWriteLock readWriteLock = redisson.getReadWriteLock(ke2)

RLock rLock = readWriteLock.readLock();

rLock.locak(); // 加读锁 setnx(k,v)

try{

//缓存中不存在该条数据,读取DB中数据,并存到Redis中

} finally {

rLock.unlock(); //删除锁

}

} finally {

hotCacheCreateLock.unlock(); //删除锁 del k 删除key

}

  1. 利用多级缓存架构解决Redis线上集群缓存雪崩问题

Redis单节点高并发最高10W+,,,瞬间来了几十W上百W的请求来查Redis中数据,Redis直接宕机了,,,缓存雪崩问题

web应用分流,,,

多级缓存——JVM进程级别缓存

JVM进程级别的缓存一般来说都是有容量上的限制

原文链接:https://blog.csdn.net/qq_45896330/article/details/125975423

4、高并发场景缓存穿透 &失效&雪崩如何解决

5、Redis集群架构如何抗住双十一的洪峰流量

6、Redis缓存与数据库双写不一致如何解决

7、Redis分布式锁主从架构锁失效问题如何解决

8、从CAP角度解释下Redis &Zookeeper锁架构异同

9、超大并发的分布式锁架构该如何设计

10、双十一亿级用户日活统计如何用Redis快速计算

11、双十一电商推荐系统如何用Redis实现

12、双十一电商购物车系统如何用Redis实现

13、类似微信的社交App朋友圈关注模型如何设计实现

14、美团单车如何基于Redis快速找到附近的车

15、Redis 6.0 多线程模型比单线程优化在哪里了

缓存,session,数据库,

redis应用场景总结redis平时我们用到的地方蛮多的,下面就了解的应用场景做个总结:

1、热点数据的缓存

由于redis访问速度块、支持的数据类型比较丰富,所以redis很适合用来存储热点数据,另外结合expire,我们可以设置过期时间然后再进行缓存更新操作,这个功能最为常见,我们几乎所有的项目都有所运用。

2、限时业务的运用

redis中可以使用 expire
命令设置一个键的生存时间,到时间后redis会删除它。利用这一特性可以运用在限时的优惠活动信息、手机验证码等业务场景。

3、计数器相关问题

redis由于 incrby
命令可以实现原子性的递增,所以可以运用于高并发的秒杀活动、分布式序列号的生成、具体业务还体现在比如限制一个手机号发多少条短信、一个接口一分钟限制多少请求、一个接口一天限制调用多少次等等。

4、排行榜相关问题

关系型数据库在排行榜方面查询速度普遍偏慢,所以可以借助redis的 SortedSet 进行热点数据的排序。

在奶茶活动中,我们需要展示各个部门的点赞排行榜,
所以我针对每个部门做了一个SortedSet,然后以用户的openid作为上面的username,以用户的点赞数作为上面的score,
然后针对每个用户做一个hash,通过zrangebyscore就可以按照点赞数获取排行榜,然后再根据username获取用户的hash信息,这个当时在实际运用中性能体验也蛮不错的。

5、分布式锁

这个主要利用redis的 setnx 命令进行,setnx:"set if not exists"就是如果不存在则成功设置缓存同时返回1,否则返回0
,这个特性在俞你奔远方的后台中有所运用,因为我们服务器是集群的,定时任务可能在两台机器上都会运行,所以在定时任务中首先
通过setnx设置一个lock,如果成功设置则执行,如果没有成功设置,则表明该定时任务已执行。
当然结合具体业务,我们可以给这个lock加一个过期时间,比如说30分钟执行一次的定时任务,那么这个过期时间设置为小于30分钟的一个时间
就可以,这个与定时任务的周期以及定时任务执行消耗时间相关。

当然我们可以将这个特性运用于其他需要分布式锁的场景中,结合过期时间主要是防止死锁的出现。

Redis Setnx( SET if N ot e X ists )命令在指定的 key 不存在时,为 key
设置指定的值,这种情况下等同 SET 命令。当
key存在时,什么也不做。

6、延时操作

这个目前我做过相关测试,但是还没有运用到我们的实际项目中,下面我举个该特性的应用场景。
比如在订单生产后我们占用了库存,10分钟后去检验用户是够真正购买,如果没有购买将该单据设置无效,同时还原库存。
由于redis自2.8.0之后版本提供Keyspace
Notifications功能,允许客户订阅Pub/Sub频道,以便以某种方式接收影响Redis数据集的事件。
所以我们对于上面的需求就可以用以下解决方案,我们在订单生产时,设置一个key,同时设置10分钟后过期,
我们在后台实现一个监听器,监听key的实效,监听到key失效时将后续逻辑加上。
当然我们也可以利用rabbitmq、activemq等消息中间件的延迟队列服务实现该需求。

7、分页、模糊搜索

redis的set集合中提供了一个zrangebylex方法,语法如下:

ZRANGEBYLEX key min max [LIMIT offset count]

通过ZRANGEBYLEX zset - + LIMIT 0 10 可以进行分页数据查询,其中- +表示获取全部数据

zrangebylex key min max
这个就可以返回字典区间的数据,利用这个特性可以进行模糊查询功能,这个也是目前我在redis中发现的唯一一个支持对存储内容进行模糊查询的特性。

前几天我通过这个特性,对学校数据进行了模拟测试,学校数据60万左右,响应时间在700ms左右,比mysql的like查询稍微快一点,但是由于它可以避免大量的数据库io操作,所以总体还是比直接mysql查询更利于系统的性能保障。

8、点赞、好友等相互关系的存储(sdd 添加 smember查看)

Redis
set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。
又或者在微博应用中,每个用户关注的人存在一个集合中,就很容易实现求两个人的共同好友功能。

这个在奶茶活动中有运用,就是利用set存储用户之间的点赞关联的,另外在点赞前判断是否点赞过就利用了sismember方法,当时这个接口的响应时间控制在10毫秒内,十分高效。

9、队列

由于redis有list push和list pop这样的命令,所以能够很方便的执行队列操作。

redis单线程多线程?原因有什么好处?如何实现高效?

单线程,使用内存,使用多路I/O复用模型,非阻塞IO;数据结构简单

$redis能否当消息队列,用过哪些中间件消息队列,有什么不同?

在项目中用到了redis作为缓存,再学习了ActiveMq之后想着用redis实现简单的消息队列,下面做记录。

Redis的列表类型键可以用来实现队列,并且支持阻塞式读取,可以很容易的实现一个高性能的优先队列。同时在更高层面上,Redis还支持
"发布/订阅"的消息模式,可以基于此构建一个聊天系统。

一、redis的列表类型天生支持用作消息队列。 (类似于MQ的队列模型–任何时候都可以消费,一条消息只能消费一次)

在Redis中,List类型是按照插入顺序排序的字符串链表。

将redis发布订阅模式用做消息队列和rabbitmq的区别:(redis由订阅端实现监听)

可靠性

  • .redis :没有相应的机制保证消息的可靠消费,如果发布者发布一条消息,而没有对应的订阅者的话,这条消息将丢失,不会存在内存中;

  • rabbitmq:具有消息消费确认机制,如果发布一条消息,还没有消费者消费该队列,那么这条消息将一直存放在队列中,直到有消费者消费了该条消息,以此可以保证消息的可靠消费,;

实时性

  • redis:实时性高,redis作为高效的缓存服务器,所有数据都存在在服务器中,所以它具有更高的实时性

消费者负载均衡:

  • rabbitmq队列可以被多个消费者同时监控消费,但是每一条消息只能被消费一次,由于rabbitmq的消费确认机制,因此它能够根据消费者的消费能力而调整它的负载;

  • redis发布订阅模式,一个队列可以被多个消费者同时订阅,当有消息到达时,会将该消息依次发送给每个订阅者;

持久性

  • redis:redis的持久化是针对于整个redis缓存的内容,它有RDB和AOF两种持久化方式(持久化介绍:

持久化(persistence) — Redis
命令参考http://doc.redisfans.com/topic/persistence.html

  • ),可以将整个redis实例持久化到磁盘,以此来做数据备份,防止异常情况下导致数据丢失。

  • rabbitmq:队列,消息都可以选择性持久化,持久化粒度更小,更灵活;

队列监控

  • rabbitmq实现了后台监控平台,可以在该平台上看到所有创建的队列的详细情况,良好的后台管理平台可以方面我们更好的使用;

  • redis没有所谓的监控平台。

总结 redis: 轻量级,低延迟,高并发,低可靠性;

rabbitmq:重量级,高可靠,异步,不保证实时;

rabbitmq是一个专门的AMQP协议(即Advanced Message Queuing
Protocol,一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计)队列,他的优势就在于提供可靠的队列服务,并且可做到异步,而redis主要是用于缓存的
,redis的发布订阅模块,可用于实现及时性,且可靠性低的功能。

redis中的哈希槽你知道吧?(用于均匀存值至集群)

Redis 哈希槽 - 孤独信徒 - 博客园Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-
value时,redis 先对 key 使用 crc16 算法算出一个结果,然后把结果对 16384
求余data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAzMiAzMiI+PGRlZnM+PGNsaXBQYXRoIGlkPSJwcmVmaXhfX2NsaXAtcGF0aCI+PHBhdGggY2xhc3M9InByZWZpeF9fY2xzLTEiIGQ9Ik0yOS40OCAyNS4yNGMtLjE2LTEuODktLjA4LTEuNS0uMjQtMy4xNmE3Mi4yNCA3Mi4yNCAwIDAwLTItMTAuMzdMMjYgNi4yNWwtMi42IDUuNDEtLjQ0Ljg5cS0uMjQuNDgtLjU0IDFhMjAuNDMgMjAuNDMgMCAwMS0xLjI0IDEuODQgMjAuMDggMjAuMDggMCAwMS0zLjA4IDMuMjQgMTkuNzIgMTkuNzIgMCAwMS0zLjg3IDIuNTNjLS43Mi4zMy0xLjQ1LjcxLTIuMjMgMS0uMzQuMTQtMS4xOS40NC0xLjkzLjY2YTIuNTUgMi41NSAwIDAxLTEuMDctLjNjLS4zNC0xLS44My0yLjI0LTEuMi0zLjE5LS41LTEuMjUtMS0yLjUtMS41MS0zLjczYTU5LjExIDU5LjExIDAgMDAtMy42Mi03LjI1IDU5LjgyIDU5LjgyIDAgMDAxLjUxIDcuOTRjLjMgMS4zLjcgMi41OCAxLjA1IDMuODdTNiAyMi43MSA2LjQ0IDI0YTEuNCAxLjQgMCAwMDEuMjkuOTNjMS4yOCAwIDIuNTcuMDYgMy44Ni4wNXMyLjU5IDAgMy44OC0uMTFhNTYgNTYgMCAwMDcuNzktLjg3Yy0xLjkyLS40OS03LjUtLjgxLTEwLjc5LTEuMDZhNDMgNDMgMCAwMDYuNTMtMS43IDI0LjA2IDI0LjA2IDAgMDA1LjM4LTMuNDVjLjcxLS42MiAxLjIxIDEuODggMi4zOSA1LjI3LjYyIDEuNjkuMzQgMSAxIDIuNjZzMS4zNyA0LjM4IDIuMTcgNmMtLjA3LTEuODItLjMxLTQuNjctLjQ2LTYuNDh6Ii8+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9InByZWZpeF9fY2xpcC1wYXRoLTIiPjxwYXRoIGNsYXNzPSJwcmVmaXhfX2Nscy0xIiBkPSJNMjAuOTIgMy40NmE0LjI1IDQuMjUgMCAwMS4zMSA1Ljg2IDMuOTEgMy45MSAwIDAxLTUuNjYuMjQgNC4yNSA0LjI1IDAgMDEtLjMxLTUuODYgMy45MSAzLjkxIDAgMDE1LjY2LS4yNG0xLjM1LTEuNTRhNS44OSA1Ljg5IDAgMDAtOC41My4zNiA2LjQxIDYuNDEgMCAwMC40OCA4LjgzIDUuOTEgNS45MSAwIDAwOC41My0uMzYgNi40MSA2LjQxIDAgMDAtLjQ4LTguODN6Ii8+PC9jbGlwUGF0aD48c3R5bGU+LnByZWZpeF9fY2xzLTF7ZmlsbDojM2UzYTM5fUBtZWRpYSAocHJlZmVycy1jb2xvci1zY2hlbWU6ZGFyayl7LnByZWZpeF9fY2xzLTF7ZmlsbDojZWZlZmVmfX08L3N0eWxlPjwvZGVmcz48ZyBzdHlsZT0iaXNvbGF0aW9uOmlzb2xhdGUiPjxnIGlkPSJwcmVmaXhfX2xheWVyXzEiIGRhdGEtbmFtZT0ibGF5ZXIgMSI+PHBhdGggY2xhc3M9InByZWZpeF9fY2xzLTEiIGQ9Ik0yOS40OCAyNS4yNGMtLjE2LTEuODktLjA4LTEuNS0uMjQtMy4xNmE3Mi4yNCA3Mi4yNCAwIDAwLTItMTAuMzdMMjYgNi4yNWwtMi42IDUuNDEtLjQ0Ljg5cS0uMjQuNDgtLjU0IDFhMjAuNDMgMjAuNDMgMCAwMS0xLjI0IDEuODQgMjAuMDggMjAuMDggMCAwMS0zLjA4IDMuMjQgMTkuNzIgMTkuNzIgMCAwMS0zLjg3IDIuNTNjLS43Mi4zMy0xLjQ1LjcxLTIuMjMgMS0uMzQuMTQtMS4xOS40NC0xLjkzLjY2YTIuNTUgMi41NSAwIDAxLTEuMDctLjNjLS4zNC0xLS44My0yLjI0LTEuMi0zLjE5LS41LTEuMjUtMS0yLjUtMS41MS0zLjczYTU5LjExIDU5LjExIDAgMDAtMy42Mi03LjI1IDU5LjgyIDU5LjgyIDAgMDAxLjUxIDcuOTRjLjMgMS4zLjcgMi41OCAxLjA1IDMuODdTNiAyMi43MSA2LjQ0IDI0YTEuNCAxLjQgMCAwMDEuMjkuOTNjMS4yOCAwIDIuNTcuMDYgMy44Ni4wNXMyLjU5IDAgMy44OC0uMTFhNTYgNTYgMCAwMDcuNzktLjg3Yy0xLjkyLS40OS03LjUtLjgxLTEwLjc5LTEuMDZhNDMgNDMgMCAwMDYuNTMtMS43IDI0LjA2IDI0LjA2IDAgMDA1LjM4LTMuNDVjLjcxLS42MiAxLjIxIDEuODggMi4zOSA1LjI3LjYyIDEuNjkuMzQgMSAxIDIuNjZzMS4zNyA0LjM4IDIuMTcgNmMtLjA3LTEuODItLjMxLTQuNjctLjQ2LTYuNDh6Ii8+PGcgY2xpcC1wYXRoPSJ1cmwoI3ByZWZpeF9fY2xpcC1wYXRoKSI+PHBhdGggY2xhc3M9InByZWZpeF9fY2xzLTEiIGQ9Ik0tLjg3LS43OGgzNC40MnYzMy4wN0gtLjg3eiIvPjwvZz48cGF0aCBjbGFzcz0icHJlZml4X19jbHMtMSIgZD0iTTIwLjkyIDMuNDZhNC4yNSA0LjI1IDAgMDEuMzEgNS44NiAzLjkxIDMuOTEgMCAwMS01LjY2LjI0IDQuMjUgNC4yNSAwIDAxLS4zMS01Ljg2IDMuOTEgMy45MSAwIDAxNS42Ni0uMjRtMS4zNS0xLjU0YTUuODkgNS44OSAwIDAwLTguNTMuMzYgNi40MSA2LjQxIDAgMDAuNDggOC44MyA1LjkxIDUuOTEgMCAwMDguNTMtLjM2IDYuNDEgNi40MSAwIDAwLS40OC04LjgzeiIvPjxnIGNsaXAtcGF0aD0idXJsKCNwcmVmaXhfX2NsaXAtcGF0aC0yKSI+PHBhdGggY2xhc3M9InByZWZpeF9fY2xzLTEiIGQ9Ik0tLjg3LS43OGgzNC40MnYzMy4wN0gtLjg3eiIvPjwvZz48L2c+PC9nPjwvc3ZnPg==https://www.cnblogs.com/unqiang/p/10524177.html

Redis 集群中内置了 16384 个哈希槽,当需要在 Redis 集群中放置一个 key-value时,redis 先对 key 使用 crc16
算法算出一个结果,然后把结果对 16384 求余数,

这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,redis 会根据节点数量大致均等的将哈希槽映射到不同的节点。

Redis 集群没有使用一致性hash, 而是引入了哈希槽的概念。

Redis 集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽.集群的每个节点负责一部分hash槽。

这种结构很容易添加或者删除节点,并且无论是添加删除或者修改某一个节点,都不会造成集群不可用的状态。

使用哈希槽的好处就在于可以方便的添加或移除节点。

当需要增加节点时,只需要把其他节点的某些哈希槽挪到新节点就可以了;

当需要移除节点时,只需要把移除节点上的哈希槽挪到其他节点就行了;

在这一点上,我们以后新增或移除节点的时候不用先停掉所有的 redis 服务。

"用了哈希槽的概念,而没有用一致性哈希算法,不都是哈希么?这样做的原因是为什么呢?"Redis
Cluster是自己做的crc16的简单hash算法,没有用一致性hash。Redis的作者认为它的crc16(key) mod
16384的效果已经不错了,虽然没有一致性hash灵活,但实现很简单,节点增删时处理起来也很方便。

"为了动态增删节点的时候,不至于丢失数据么?"节点增删时不丢失数据和hash算法没什么关系,不丢失数据要求的是一份数据有多个副本。

“还有集群总共有2的14次方,16384个哈希槽,那么每一个哈希槽中存的key 和 value是什么?”当你往Redis
Cluster中加入一个Key时,会根据crc16(key) mod 16384计算这个key应该分布到哪个hash slot中,一个hash
slot中会有很多key和value。你可以理解成表的分区,使用单节点时的redis时只有一个表,所有的key都放在这个表里;改用Redis
Cluster以后会自动为你生成16384个分区表,你insert数据时会根据上面的简单算法来决定你的key应该存在哪个分区,每个分区里有很多key。

介绍一下redis有哪些过期策略?

面试题 redis 的过期策略都有哪些?内存淘汰机制都有哪些?手写一下 LRU 代码实现?

常见的有两个问题:

• 往 redis 写入的数据怎么没了? 可能有同学会遇到,在生产环境的 redis
经常会丢掉一些数据,写进去了,过一会儿可能就没了。我的天,同学,你问这个问题就说明 redis 你就没用对啊。redis 是缓存,你给当存储了是吧?
啥叫缓存?用内存当缓存。内存是无限的吗,内存是很宝贵而且是有限的,磁盘是廉价而且是大量的。可能一台机器就几十个 G 的内存,但是可以有几个 T
的硬盘空间**。redis 主要是基于内存来进行高性能、高并发的读写操作的**。 那既然内存是有限的,比如 redis 就只能用 10G,你要是往里面写了
20G 的数据,会咋办?当然会干掉 10G 的数据,然后就保留 10G 的数据了。那干掉哪些数据?保留哪些数据?当然是干掉不常用的数据,保留常用的数据了。

• 数据明明过期了,怎么还占用着内存? 这是由 redis 的过期策略来决定。

面试题剖析 redis 过期策略 redis 过期策略是:定期删除+惰性删除。 所谓定期删除,指的是 redis 默认是每隔 100ms
就随机抽取一些设置了过期时间的 key,检查其是否过期,如果过期就删除。 假设 redis 里放了 10w 个
key,都设置了过期时间,你每隔几百毫秒,就检查 10w 个 key,那 redis 基本上就死了,cpu 负载会很高的,消耗在你的检查过期 key
上了。注意,这里可不是每隔 100ms 就遍历所有的设置过期时间的 key,那样就是一场性能上的灾难。实际上 redis 是每隔 100ms 随机抽取一些
key 来检查和删除的。

但是问题是,定期删除可能会导致很多过期 key 到了时间并没有被删除掉,那咋整呢?所以就是惰性删除了。这就是说,在你获取某个 key 的时候,redis
会检查一下 ,这个 key 如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西。 获取 key 的时候,如果此时 key
已经过期,就删除,不会返回任何东西。 但是实际上这还是有问题的,如果定期删除漏掉了很多过期
key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期 key 堆积在内存里,导致 redis 内存块耗尽了,咋整?

答案是:走内存淘汰机制。

内存淘汰机制 redis 内存淘汰机制有以下几个:

• noeviction: 当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了。

• allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。

• allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的
key 给干掉啊。

• volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key(这个一般不太合适)。

• volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key。

• volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除。

项目中为什么要用redis来缓存赞和踩?数据不一致怎么办?如何保证数据的持久性?

取数快,有set

散列hash:Redis中的散列可以看成具有String key和String value的map容器,可以将多个key-
value存储到一个key中。每一个Hash可以存储4294967295个键值对。

因此,最终的结论是,需要解决的不一致,产生的原因是更新数据库成功,但是删除缓存失败。

解决方案大概有以下几种:

1. 对删除缓存进行重试,数据的一致性要求越高,我越是重试得快。

2. 定期全量更新,简单地说,就是我定期把缓存全部清掉,然后再全量加载。

3. 给所有的缓存一个失效期。

task-blog-
BlogCommendFromMachineLearnPai2-1.control&depth_1-utm_source=distribute.pc_relevant.none-
task-blog-
BlogCommendFromMachineLearnPai2-1.control](https://blog.csdn.net/xinbumi/article/details/89742930?utm_medium=distribute.pc_relevant.none-
task-blog-
BlogCommendFromMachineLearnPai2-1.control&depth_1-utm_source=distribute.pc_relevant.none-
task-blog-BlogCommendFromMachineLearnPai2-1.control)

怎么设置一个key的过期时间?

EXPIRE 和 PERSIST 命令。Redis PERSIST 命令用于移除给定 key 的过期时间,使得 key 永不过期。

EXPIRE key seconds

为给定 key 设置生存时间,当 key 过期时(生存时间为 0 ),它会被自动删除。

在 Redis 中,带有生存时间的 key 被称为『易失的』(volatile)。

$redis可能存在的坑

如何保证缓存和数据库同步

应用建设方案

假设有一亿个用户每个用户订阅了一个手机闹铃闹铃类型不同,都是7点钟的闹铃,如何保证7点时数据库的负载最小,简述你的方案

数据存储redis缓存

主从数据库

编程题:一个二维矩阵,矩阵中标0的位置是道路,标1的位置是墙壁,矩阵的边界也是墙壁,然后有一个起始点p和终点q,问:一个小球起始位于p,小球停止时可以选择一个方向一直滚动,碰到墙壁就停下来,问:小球能否从p滚到q,即最终要在q静止,滚过去不行。

编程题:给定一颗二叉树,实现一个方法让每个节点新增一个next,next指向当前节点右边的第一个兄弟节点

$场景题:有A、B、C三个方法,分别是循环输出A,输出B,输出C 10次,使用多线程实现按照“ABC”的顺序输出10次

package thread1; public class Main_thread2 { static boolean isThreadA = true;
static boolean isThreadB = false; static boolean isThreadC = false; public
static void main(String[] args) throws Exception{ byte []print = new byte[0];
Thread t1 = new Thread(new Runnable(){ public void run(){ synchronized
(print){ for(int i=0;i<10; i++){ while(!isThreadA){ try{ print.wait(); } catch
(InterruptedException e){ e.printStackTrace(); } } System.out.print(“A”);
isThreadA = false; isThreadB = true; isThreadC = false; print.notifyAll(); } }
} }); Thread t2 = new Thread(new Runnable(){ public void run(){ synchronized
(print){ for(int i=0;i<10; i++){ while(!isThreadB){ try{ print.wait(); } catch
(InterruptedException e){ e.printStackTrace(); } } System.out.print(“B”);
isThreadA = false; isThreadB = false; isThreadC = true; print.notifyAll(); } }
} }); Thread t3 = new Thread(new Runnable(){ public void run(){ synchronized
(print){ for(int i=0;i<10; i++){ while(!isThreadC){ try{ print.wait(); } catch
(InterruptedException e){ e.printStackTrace(); } } System.out.print(“C”);
isThreadA = true; isThreadB = false; isThreadC = false; print.notifyAll(); } }
} }); t1.start(); t2.start(); t3.start(); } }

$推荐算法-排名算法,权值计算

探究 Stack Overflow
网站的热点问题排名算法

$微信红包实现

1.红包算法,给定总金额,个数等参数,事先计算好随机金额的红包;

红包算法:每个人当前能抢到的金额,服从一个0.01到当前剩余均值两倍的左开右闭区间的均匀分布

random.nextInt(剩余平均红包金额*2)+1

高并发问题,一个人只能抢一个,一个红包不能被多人抢:

查询用户是否已参与过活动:可以使用Set的特性,集合中不能出现重复的数据,每个用户发起抢的动作就将用户标识放入Set中,如果Set中已存在这个用户标识,则说明用户不可再抢了

获取一个可抢的红包:可以使用队列的数据结构,每次获取红包都是一个出队的动作,解决了多人获取同一个红包和超发的问题

建立红包与用户的关系:同样使用队列的数据结构,每次建立都是一个入队动作,使用单独线程每秒尝试出队多个,批量存储到数据库中

  1. 实现功能
* 准备工作:生成可抢红包,入队redis的list结构,命令为RPUSH

* 用户发出抢的请求:用户标识写入redis的Set中,命令SADD,返回1标识插入成功,可继续获取红包,返回0则为已抢,直接返回(一个人只能抢一个)

* 获取红包:list出队一条红包数据,命令为LPOP,如果返回不为nil时,代表获取成功,继续下一步,反之则说明已抢完,返回个红包不能被多人抢)

* 建立红包与用户的关系:构建红包与用户的关系对象,入队Redis的list,使用单独线程每秒尝试出队1000个(举例),批量存储到数据库中

lpush(name,values) --> 实际上是左添加

在name对应的list中添加元素,每个新的元素都添加到列表的最左边

如: # r.lpush(‘oo’, 11,22,33) # 保存顺序为: 33,22,11

扩展:# rpush(name,values) 表示从右向左操作

lpop(name) -->列表的左侧获取第一个元素并在列表中移除,返回值是第一个元素

更多: # rpop(name) 表示从右向左操作

Redis Incr 命令将 key 中储存的数字值增一。

如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。

如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。

redis 127.0.0.1:6379> INCR KEY_NAME

如何把一个文件快速下发到100个服务器

想一想迅雷的下载方式就可以了。下载好的主机同样可以作为上传服务器。不断扩散,速度很快

保证发送消息的有序性,消息处理的有序性

3.2.1 rabbitmq

拆分多个queue,每个queue一个consumer 就是多一些queue而已,确实麻烦点
或者就一个queue但是对应一个consumer,然后这个consumer内部用内存队列做排队,然后分发给底层不同的worker来处理

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HlIBvjLx-1681014581797)(【精】各大厂问题汇总_files/Image [21].png)]

10亿个数最大的10个

局部淘汰法,分治法,Hash法,最小堆法

redis2.6.12过期时间设置

EXPIRE cache_page 30000 # 更新过期时间 TTL cache_page #查看剩余生存时间 PERSIST
message#移除过期时间设置 从 Redis 2.6.12 版本开始, 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 :只在键已经存在时,才对键进行设置操作。

分布式集群保证线程的安全性

避免并发,
相比于单一部署的服务器来说,分布式架构同一个模块的系统部署了多台;对于单一服务来说,只要保证一台机器上的对于共享资源的访问是同步进行的就能保证线程安全了;但是对于分布式系统而已,保证一台服务器的同步,并不能保证访问共享资源是同步的;所以可以考虑使用分布式锁的方式来保证分布式中的线程的安全线,这样不同的服务不同的线程通过竞争分布式锁来获取共享资源的操作权限;例如redis的分布式锁、zookeeper锁,都可以作为分布式线程安全的手段。

十万个数输出从小到大

位图法 bitmap

在海量数据中查找出重复出现的元素或者去除重复出现的元素是面试中常考的文图。针对此类问题,可以使用位图法来解决。例如:已知某个文件内包含若干个电话号码,要求统计不同的号码的个数,甚至在O(n)时间复杂度内对这些号码进行排序。

位图法需要的空间很少(依赖于数据分布,但是我们也可以通过一些放啊发对数据进行处理,使得数据变得密集),在数据比较密集的时候效率非常高。例如:8位整数可以表示的最大十进制数值为99999999,如果每个数组对应于一个bit位,那么把所有的八进制整数存储起来只需要:99Mbit
= 12.375MB.

十万个单词,找出重复次数最高十个

先遍历十万个单词 统计每个单词出现次数,然后局部淘汰法排序取前十

多个电脑存储几亿搜索日志,你有一台2G的电脑,找出搜索热度最高的10个词

分别统计每台电脑搜索日志的关键词的重复次数,然后排序

原理篇

基础

NIO

NIO 主要有三大核心部分:Channel(通道),Buffer(缓冲区), Selector。传统 IO 基于字节流和字 符流进行操作,而 NIO 基于
Channel 和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区 中,或者从缓冲区写入到通道中。单个线程可以监听多个数据通道

JMM

Java内存模型(即Java Memory
Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行。

首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。

RabbitMQ 高级消息队列协议 AMQP 的开源实现

首先来看看RabbitMQ里的几个重要概念:

  • 生产者(Producer):发送消息的应用。

  • 消费者(Consumer):接收消息的应用。

  • 队列(Queue):存储消息的缓存。

  • 消息(Message):由生产者通过RabbitMQ发送给消费者的信息。

  • 连接(Connection):连接RabbitMQ和应用服务器的TCP连接。

  • 通道(Channel):连接里的一个虚拟通道。当你通过消息队列发送或者接收消息时,这个操作都是通过通道进行的。

  • 交换机(Exchange):交换机负责从生产者那里接收消息,并根据交换类型分发到对应的消息列队里。要实现消息的接收,一个队列必须到绑定一个交换机。

  • 绑定(Binding):绑定是队列和交换机的一个关联连接。

  • 路由键(Routing Key):路由键是供交换机查看并根据键来决定如何分发消息到列队的一个键。路由键可以说是消息的目的地址。

生产者(Producer) 发送/发布消息到代理-> 消费者(Consumer)
从代理那里接收消息。哪怕生产者和消费者运行在不同的机器上, RabbitMQ 也能扮演代理中间件的角色。

当生产者发送消息时,它并不是直接把消息发送到队列里的,而是使用交换机(Exchange)来发送。下面的设计图简单展示了这三个主要的组件之间是如何连接起来的。

交换机代理(exchange
agent)负责把消息分发到不同的队列里。这样的话,消息就能够从生产者发送到交换机,然后被分发到消息队列里。这就是常见的“发布”方法。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GdAh6ThT-1681014581798)(【精】各大厂问题汇总_files/Image [22].png)]

往多个队列里发送消息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-arCxzTpa-1681014581798)(【精】各大厂问题汇总_files/Image [23].png)]

给带有多个队列的交换机发送的消息是通过绑定和路由键来进行分发的。绑定是你设置的用来连接一个队列和交换机的连接。路由键是消息的一个属性。交换机会根据路由键来决定消息分发到那个队列里(取决于交换机的类型)。

交换机(Exchange)

消息并不是直接发布到队里里的,而是被生产者发送到一个交换机上。交换机负责把消息发布到不同的队列里。交换机从生产者应用上接收消息,然后根据绑定和路由键将消息发送到对应的队列里。绑定是交换机和队列之间的一个关系连接。

rabbitmq和kafka区别

RabbitMQ采用AMQP(Advanced Message Queuing Protocol,高级消息队列协议)是一个进程间传递 异步消息

网络协议

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-efvbSlvb-1681014581799)(【精】各大厂问题汇总_files/Image [24].png)]

RabbitMQ的broker由Exchange,Binding,queue组成

kafka采用mq结构:broker 有part 分区的概念

rabbitMQ的负载均衡需要单独的loadbalancer进行支持。

kafka采用zookeeper对集群中的broker、consumer进行管理

RabbitMQ 采用push的方式

kafka采用pull的方式

rabbitMQ支持对消息的可靠的传递,支持事务,不支持批量的操作;基于存储的可靠性的要求存储可以采用内存或者硬盘。金融场景中经常使用

kafka具有高的吞吐量,内部采用消息的批量处理,zero-
copy机制,数据的存储和获取是本地磁盘顺序批量操作,具有O(1)的复杂度(与分区上的存储大小无关),消息处理的效率很高。(大数据)

RabbitMQ里的消息流程

  • 生产者(producer) 把消息发送给交换机。当你创建交换机的时候,你需要指定类型。交换机的类型接下来会讲到。

  • 交换机(exchange) 接收消息并且负责对消息进行路由。根据交换机的类型,消息的多个属性会被使用,例如路由键。

  • 绑定(binding) 需要从交换机到队列的这种方式来进行创建。在这个例子里,我们可以看到交换机有到两个不同队列的绑定。交换机根据消息的属性来把消息分发到不同的队列上。

  • 消息(message) 消息会一直留在队列里直到被消费。

  • 消费者(consumer) 处理消息。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-smWtnCak-1681014581800)(【精】各大厂问题汇总_files/Image [25].png)]

  • 直接(Direct) :直接交换机通过消息上的路由键直接对消息进行分发。 最简单模式

  • 扇出(Fanout) :一个扇出交换机会将消息发送到所有和它进行绑定的队列上。 广播模式

  • 主题(Topic) :这个交换机会将路由键和绑定上的模式进行通配符匹配。 最能契合复杂应用

  • 消息头(Headers) :消息头交换机使用消息头的属性进行消息路由。

生产者 private final static String QUEUE_NAME = “hello”; public static void
main(String[] args) throws Exception{ // TODO Auto-generated method stub
ConnectionFactory factory = new ConnectionFactory();
factory.setHost(“localhost”);//因为两个进程在同一个机器上 Connection connection = null;
Channel channel = null; connection = factory.newConnection(); channel =
connection.createChannel(); channel.queueDeclare(QUEUE_NAME, false, false,
false, null); String message = “Hello World!”; channel.basicPublish(“”,
QUEUE_NAME, null, message.getBytes(“UTF-8”)); System.out.println(" [Producer]
Sent ‘" + message + "’“); channel.close(); connection.close(); } 消费者 private
final static String QUEUE_NAME = “hello”; public static void main(String[]
args) throws Exception{ ConnectionFactory factory = new ConnectionFactory();
factory.setHost(“localhost”); Connection connection = factory.newConnection();
Channel channel = connection.createChannel(); channel.queueDeclare(QUEUE_NAME,
false, false, false, null); System.out.println(” [*] Waiting for messages. To
exit press CTRL+C"); Consumer consumer = new DefaultConsumer(channel) {
@Override public void handleDelivery(String consumerTag, Envelope envelope,
AMQP.BasicProperties properties, byte[] body) throws IOException { String
message = new String(body, “UTF-8”); System.out.println(" [Consumer] Received
‘" + message + "’"); } }; channel.basicConsume(QUEUE_NAME, true, consumer); }

Kafka 高吞吐量、分布式、基于发布/订阅的消息系统

消息中间件,作用: 解耦、异步、削峰

一、 点对点模式

如上图所示,点对点模式通常是基于拉取或者轮询的消息传送模型,这个模型的特点是发送到队列的消息被一个且只有一个消费者进行处理。生产者将消息放入消息队列后,由消费者主动的去拉取消息进行消费。点对点模型的的优点是消费者拉取消息的频率可以由自己控制。但是消息队列是否有消息需要消费,在消费者端无法感知,所以在消费者端需要额外的线程去监控。

二、 发布订阅模式

如上图所示,发布订阅模式是一个基于消息送的消息传送模型,改模型可以有多种不同的订阅者。生产者将消息放入消息队列后,队列会将消息推送给订阅过该类消息的消费者(类似微信公众号)。由于是消费者被动接收推送,所以无需感知消息队列是否有待消费的消息!但是consumer1、consumer2、consumer3由于机器性能不一样,所以处理消息的能力也会不一样,但消息队列却无法感知消费者消费的速度!所以推送的速度成了发布订阅模模式的一个问题!假设三个消费者处理速度分别是8M/s、5M/s、2M/s,如果队列推送的速度为5M/s,则consumer3无法承受!如果队列推送的速度为2M/s,则consumer1、consumer2会出现资源的极大浪费!

上面简单的介绍了为什么需要消息队列以及消息队列通信的两种模式,接下来就到了我们本文的主角——kafka闪亮登场的时候了!Kafka是一种高吞吐量的分布式发布订阅消息系统,它可以处理消费者规模的网站中的所有动作流数据,具有高性能、持久化、多副本备份、横向扩展能力

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Up08swhD-1681014581800)(【精】各大厂问题汇总_files/Image [26].png)]

如果看到这张图你很懵逼,木有关系!我们先来分析相关概念

Producer :Producer即生产者,消息的产生者,是消息的入口。

kafka cluster

Broker
:Broker是kafka实例,每个服务器上有一个或多个kafka的实例,我们姑且认为每个broker对应一台服务器。每个kafka集群内的broker都有一个
不重复 的编号,如图中的broker-0、broker-1等……

Topic :消息的主题,可以理解为消息的分类,kafka的数据就保存在topic。在每个broker上都可以创建多个topic。

Partition
:Topic的分区,每个topic可以有多个分区,分区的作用是做负载,提高kafka的吞吐量。同一个topic在不同的分区的数据是不重复的,partition的表现形式就是一个一个的文件夹!

Replication
:每一个分区都有多个副本,副本的作用是做备胎。当主分区(Leader)故障的时候会选择一个备胎(Follower)上位,成为Leader。在kafka中默认副本的最大数量是10个,且副本的数量不能大于Broker的数量,follower和leader绝对是在不同的机器,同一机器对同一个分区也只可能存放一个副本(包括自己)。

Message :每一条发送的消息主体。。

Consumer :消费者,即消息的消费方,是消息的出口。

Consumer Group
:我们可以将多个消费组组成一个消费者组,在kafka的设计中同一个分区的数据只能被消费者组中的某一个消费者消费。同一个消费者组的消费者可以消费同一个topic的不同分区的数据,这也是为了提高kafka的吞吐量!

Zookeeper :kafka集群依赖zookeeper来保存集群的的元信息,来保证系统的可用性。

为什么要用 Partition :Topic的分区

1、 方便扩展 。因为一个topic可以有多个partition,所以我们可以通过扩展机器去轻松的应对日益增长的数据量。

2、 提高并发 。以partition为读写单位,可以多个消费者同时消费数据,提高了消息的处理效率。

import java.util.Properties; import
org.apache.kafka.clients.producer.KafkaProducer; import
org.apache.kafka.clients.producer.ProducerRecord; import
org.apache.kafka.common.serialization.StringSerializer; /** * * Title:
KafkaProducerTest * Description: * kafka 生产者demo * Version:1.0.0 * @author
pancm * @date 2018年1月26日 / public class KafkaProducerTest implements Runnable
{ private final KafkaProducer producer; private final String
topic; public KafkaProducerTest(String topicName) { Properties props = new
Properties(); props.put(“bootstrap.servers”,
“master:9092,slave1:9092,slave2:9092”); props.put(“acks”, “all”);
props.put(“retries”, 0); props.put(“batch.size”, 16384);
props.put(“key.serializer”, StringSerializer.class.getName());
props.put(“value.serializer”, StringSerializer.class.getName()); this.producer
= new KafkaProducer(props); this.topic = topicName; }
@Override public void run() { int messageNo = 1; try { for(; { String
messageStr=“你好,这是第”+messageNo+“条数据”; producer.send(new ProducerRecord String>(topic, “Message”, messageStr));// IMPORTANT// IMPORTANT //生产了100条就打印
if(messageNo%1000){ System.out.println(“发送的信息:” + messageStr); }
//生产1000条就退出 if(messageNo%1000
0){ System.out.println(“成功发送了”+messageNo+“条”);
break; } messageNo++; } } catch (Exception e) { e.printStackTrace(); } finally
{ producer.close(); } } public static void main(String args[]) {
KafkaProducerTest test = new KafkaProducerTest(“KAFKA_TEST”); Thread thread =
new Thread(test); thread.start(); } } import java.util.Arrays; import
java.util.Properties; import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.clients.consumer.ConsumerRecords; import
org.apache.kafka.clients.consumer.KafkaConsumer; import
org.apache.kafka.common.serialization.StringDeserializer; /
* * * Title:
KafkaConsumerTest * Description: * kafka消费者 demo * Version:1.0.0 * @author
pancm * @date 2018年1月26日 */ public class KafkaConsumerTest implements Runnable
{ private final KafkaConsumer consumer; private
ConsumerRecords msgList; private final String topic; private
static final String GROUPID = “groupA”; public KafkaConsumerTest(String
topicName) { Properties props = new Properties();
props.put(“bootstrap.servers”, “master:9092,slave1:9092,slave2:9092”);
props.put(“group.id”, GROUPID); props.put(“enable.auto.commit”, “true”);
props.put(“auto.commit.interval.ms”, “1000”); props.put(“session.timeout.ms”,
“30000”); props.put(“auto.offset.reset”, “earliest”);
props.put(“key.deserializer”, StringDeserializer.class.getName());
props.put(“value.deserializer”, StringDeserializer.class.getName());
this.consumer = new KafkaConsumer(props);// IMPORTANT//
IMPORTANT this.topic = topicName;
this.consumer.subscribe(Arrays.asList(topic)); } @Override public void run() {
int messageNo = 1; System.out.println(“---------开始消费---------”); try { for
(; { msgList = consumer.poll(1000);// IMPORTANT// IMPORTANT
if(null!=msgList&&msgList.count()>0){ for (ConsumerRecord
record : msgList) { //消费100条就打印 ,但打印的数据不一定是这个规律的 if(messageNo%100==0){
System.out.println(messageNo+"===receive: key = " + record.key() + “,
value = " + record.value()+” offset
="+record.offset()); } //当消费了1000条就退出
if(messageNo%1000
0){ break; } messageNo++; } }else{ Thread.sleep(1000); } }
} catch (InterruptedException e) { e.printStackTrace(); } finally {
consumer.close(); } } public static void main(String args[]) {
KafkaConsumerTest test1 = new KafkaConsumerTest(“KAFKA_TEST”); Thread thread1
= new Thread(test1); thread1.start(); } }

Netty(NIO框架)

是一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端

JDK NIO 编程的特点是功能开发相对容易,但是可靠性能力补齐工作量和难度都非常大。

Netty 对 JDK 自带的 NIO 的 API 进行封装,解决上述问题,主要特点有:

  • 设计优雅,适用于各种传输类型的统一 API 阻塞和非阻塞 Socket;基于灵活且可扩展的事件模型,可以清晰地分离关注点;高度可定制的线程模型 - 单线程,一个或多个线程池;真正的无连接数据报套接字支持(自 3.1 起)。

  • 使用方便,详细记录的 Javadoc,用户指南和示例;没有其他依赖项,JDK 5(Netty 3.x)或 6(Netty 4.x)就足够了。

  • 高性能,吞吐量更高,延迟更低;减少资源消耗;最小化不必要的内存复制。

  • 安全,完整的 SSL/TLS 和 StartTLS 支持。

  • 社区活跃,不断更新,社区活跃,版本迭代周期短,发现的 Bug 可以被及时修复,同时,更多的新功能会被加入。

Netty 常见的使用场景如下:互联网行业。在分布式系统中,各个节点之间需要远程服务调用,高性能的 RPC 框架必不可少,Netty
作为异步高性能的通信框架,往往作为基础通信组件被这些 RPC 框架使用。

  • 游戏行业。无论是手游服务端还是大型的网络游戏,Java 语言得到了越来越广泛的应用。Netty 作为高性能的基础通信组件,它本身提供了 TCP/UDP 和 HTTP 协议栈。 非常方便定制和开发私有协议栈,账号登录服务器,地图服务器之间可以方便的通过 Netty 进行高性能的通信。

传统阻塞IO

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iitbbSIx-1681014581801)(【精】各大厂问题汇总_files/Image [8].jpg)]

IO复用模式

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lBVCDYE9-1681014581801)(【精】各大厂问题汇总_files/Image [9].jpg)]

在 I/O 复用模型中,会用到 Select,这个函数也会使进程阻塞,但是和阻塞 I/O 所不同的是这两个函数可以同时阻塞多个 I/O 操作。

IO多路复用

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b2yJBZxq-1681014581802)(【精】各大厂问题汇总_files/Image [10].jpg)]

传统的 I/O 是面向字节流或字符流的,以流式的方式顺序地从一个 Stream 中读取一个或多个字节, 因此也就不能随意改变读取指针的位置。

在 NIO 中,抛弃了传统的 I/O 流,而是引入了 Channel 和 Buffer 的概念。在 NIO 中,只能从 Channel 中读取数据到
Buffer 中或将数据从 Buffer 中写入到 Channel。

基于 Buffer 操作不像传统 IO 的顺序操作,NIO 中可以随意地读取任意位置的数据。

Redis

单线程为什么快

原因1: 单线程,避免线程之间的竞争

原因2 :是内存中的,使用内存的,可以减少磁盘的io

原因3:多路复用模型,用了缓冲区的概念,selector模型来进行的

mybaties

一级缓存和二级缓存

Mybatis 中有一级缓存和二级缓存,默认情况下一级缓存是开启的,而且是不能关闭的。一级缓存 是指 SqlSession 级别的缓存,当在同一个
SqlSession 中进行相同的 SQL 语句查询时,第二次以 后的查询不会从数据库查询,而是直接从缓存中获取,一级缓存最多缓存 1024 条
SQL。二级缓存 是指可以跨 SqlSession 的缓存。是 mapper 级别的缓存,对于 mapper 级别的缓存不同的 sqlsession
是可以共享的。

Spring

IOC

IOC(Inversion of
Control,控制反转),所有的Java类(不管是JDK库中Java类,还是你自己定义Java类)都可以利用Spring框架来new它们的实例化对象

Spring框架完成对Java类的初始化对象工作

Spring来负责控制对象的生命周期和对象间的关系

DI(Dependency Injection,依赖注入),它主要作用是动态的向某个对象提供它所需要的其他对象

Ioc容器原理Inversion of controller 控制反转给框架:可以理解为将某个类依赖的成员注入到这个类中

  1. beanhelper:定义bean助手,用于存放bean和 bean实例的映射关系

  2. ClassHelper:获取想要类的集合,但是无法实例化对象。这时需要 ReflectionUtil反射工具类

  3. IocHelper依赖注入助手类(实现依赖注入功能):获取所有Bean类和bean实例的映射关系,先通过beanhelper获取所有 Bean Map然后遍历这个映射关系,分别取出bean类和bean实例,通过反射获取类中所有的成员变量。继续遍历这些成员变量,再循环判断是否有Inject注解,若带有该注解,则从Bean Map中根据Bean类取出Bean实例,然后通过反射修改当前成员变量的值

  4. 利用注解区分 controller、server等类

  5. 实现将所有类加载到一个容器中

  6. ControllerHelper:封装 Action Map 存放request和Handler对应关系

IOC容器管理的那些组成你应用程序的对象我们就叫它Bean, Bean就是由Spring容器初始化、装配及管理的对象

BeanFactory和FactoryBean接口

1、 BeanFactory(实例化对象及依赖)

BeanFactory定义了IOC容器的最基本形式,并提供了IOC容器应遵守的的最基本的接口,也 就是Spring
IOC所遵守的最底层和最基本的编程规范。

****它的职责包括:实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。

****在Spring代码中,BeanFactory只是个接口,并不是IOC容器的具体实现,但是Spring容器给出了很多种实现,如
DefaultListableBeanFactory、XmlBeanFactory、ApplicationContext等,都是附加了某种功能的实现。

2、FactoryBean (实例化过程的规范及细节的封装)

一般情况下,Spring通过反射机制利用的class属性指定实现类实例化Bean,在某些情况下,实例化Bean过程比较复杂,如果按照传统的方式,则需要在中提供大量的配置信息。配置方式的灵活性是受限的,这时采用编码的方式可能会得到一个简单的方案。Spring为此提供了一个org.springframework.bean.factory.FactoryBean的工厂类接口,用户可以通过实现该接口定制实例化Bean的逻辑。

FactoryBean接口对于Spring框架来说占用重要的地位,Spring自身就提供了70多个FactoryBean的实现。

它们隐藏了实例化一些复杂Bean的细节
,给上层应用带来了便利。从Spring3.0开始,FactoryBean开始支持泛型,即接口声明改为FactoryBean的形式。

Spring提供两种Ioc容器,分别是BeanFactory和ApplicationContext

BeanFactory:基础类型的Ioc容器,采用懒加载(lazy-load),对象只有在用的时候才初始化和依赖注入。所以启动容器比较快。

ApplicationContext(所谓上下文,它是用来存储系统的一些初始化信息):较高级类型的IOC容器,基于BeanFactory,在启动容器的时候就会初始化并注入依赖。并且还提供其他高级特性。

AOP

AOP: Aspect Oriented Programming 面向切面编程。 面向切面编程(也叫面向方面):Aspect Oriented
Programming(AOP),是目前软件开发中的一个热点。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
AOP是OOP的延续,是(Aspect Oriented Programming)的缩写,意思是面向切面(方面)编程。
主要的功能是:日志记录,性能统计,安全控制,事务处理,异常处理等等。

JDK implements InvocationHandler invoke

CGLIB implements MethodInterceptor intercept

AOP(Aspect Oriented Programming 面向方面编程)横向拦截操作,比如性能分析、日志收集、安全控制等代理模式-
静态代理:写一个接口,和实现类,用代理类实例化 接口的实现类,并写代理方法调用实现类具体方法,并可在方法前后加before和after操作(这里before
和after是每个代理类都要写的,不易 )

动态代理1-JDK动态代理(原理反射):实现InvocationHandler,重写invoke方法。(before 和after方法都是在 动态代理类中,
易于代码维护) 前置增强,后置增强

CGlib动态代理:实现接口MethodInterceptor 重写 intercept方法。快速生产 代理对象。与JDK代理不同可以代理没有接口的类

spring aop可以理解为拦截器框架

Spring +aspectJ 基于注解的 表达式拦截方法@Aspect,方面

@pointCut【切点】(execution【拦截器】(。。。))

@Around 环绕增强 @Before前置增强 @After后置增强

security 过滤器

Spring Security 安全框架:继承 WebSecurityConfigurerAdapter 底层是Filter过滤器

MVC

rest服务发布原理:首先是注解@Controller、@RestController、@RequestMapping,启动的时候,spring
会自动加载解析相关的bean以及bean的方法,然后包装成HttpMthod对象,存储在AbstactMappingHandler抽象类里面的一个MappingRegistery对象里面,该对象以map的对象维持着所有的HttpMethod
. (这一块的加载原理还需要看下).
AbstactMappingHandler实现了MappingHandler接口,真正的核心实现类是:RequestMappingHandlerMapping

2.http请求接入的时候,首先走的是tomcat提供的servlet接口,然后在DispatchServlet里面完成寻找

首先是解析请求,这里会对请求进行warp操作,以及判断是否是mulit请求

然后根据请求从MappingRegistery查找httpMethod

根据httpMethod构建HandleExecutionChain,其内部包含了拦截器HandleInterceptor

然后根据定义的参数类型找到合适的ArgumentResovler,由ArgumentResovler来完成参数的解析和转换,ArgumentResovler持有做HttpMessageConverter,然后根据不同的类型进行转换

ArgumentResovler的入口实现累是ArgumentResovlerCompise,组合了所有的Resovler,每一个注解都有对应的Resolver,@RequestBody的Resovler的注册器是RequestMappingBodyProcessoer

spring scheduled 定时任务

Spring Security 安全框架:继承 WebSecurityConfigurerAdapter 底层是Filter过滤器

Spring 事务

ACID 原子性,一致隔离 持久

@Transactional是spring中声明式事务管理的注解配置方式,相信这个注解的作用大家都很清楚。@Transactional注解可以帮助我们把事务开启、提交或者回滚的操作,通过aop的方式进行管理。通过@Transactional注解就能让spring为我们管理事务,免去了重复的事务管理逻辑,减少对业务代码的侵入,使我们开发人员能够专注于业务层面开发

@Transactional注解标注的方法需要被代理。切面逻辑类似于@Around,在spring中是实现一种类似代理逻辑。

spring boot核心自动配置

通常搭建一个基于spring的web应用,我们需要做以下工作:

1、pom文件中引入相关jar包,包括spring、springmvc、redis、mybaits、log4j、mysql-connector-java
等等相关jar …

2、配置web.xml,Listener配置、Filter配置、Servlet配置、log4j配置、error配置 …

3、配置数据库连接、配置spring事务

4、配置视图解析器

5、开启注解、自动扫描功能

6、配置完成后部署tomcat、启动调试

用springboot后,一切都变得很简便快速。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DyGkJPgw-1681014581803)(【精】各大厂问题汇总_files/Image [27].png)]

其他

Linux常用

reboot 立刻重启

shutdown -r 5 5分钟后重启

ifconfig --help:查看网卡信息

cd / 切换到根目录

ls 查看当前目录下的所有目录和文件

mkdir aaa 在当前目录下创建一个名为aaa的目录

rm 文件 删除当前目录下的文件

rm -rf * 将当前目录下的所有目录和文件全部删除

mv aaa bbb 将目录aaa改为bbb

将/usr/tmp目录下的aaa目录剪切到 /usr目录下面 mv /usr/tmp/aaa /usr

find /usr/tmp -name ‘a*’ 查找/usr/tmp目录下的所有以a开头的目录或文件

打开当前目录下的aa.txt文件 vi aa.txt 或者 vim aa.txt

使用vi编辑器打开文件后,并不能编辑,因为此时处于命令模式,点击键盘i/a/o进入编辑模式。

【3】保存并退出: :wq

grep命令是一种强大的文本搜索工具

ps -ef |grepsshd |grep-vgrep

查找指定服务进程,排除gerp身

tar [-zxvf] 压缩文件

tar -zcvf 打包压缩后的文件名 要打包的文件

Linux进程间通信的方式

1.管道

2.信号量

信号量是一个计数器,可以用来控制多个线程对共享资源的访问.,它不是用于交换大批数据,而用于多线程之间的同步.它常作为一种锁机制,防止某进程在访问资源时其它进程也访问该资源.因此,主要作为进程间以及同一个进程内不同线程之间的同步手段.

3.信号(信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生.)

4.消息队列

5.共享内存

6.套接字

进程通信的应用场景

数据传输:一个进程需要将它的数据发送给另一个进程,发送的数据量在一个字节到几兆字节之间。

共享数据:多个进程想要操作共享数据,一个进程对共享数据的修改,别的进程应该立刻看到。

通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。

资源共享:多个进程之间共享同样的资源。为了作到这一点,需要内核提供锁和同步机制。

进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wSVDRlMR-1681014581803)(【精】各大厂问题汇总_files/Image [28].png)]

Linux里用来查看CPU状态都有哪些指令?

Linux中常用的监控CPU整体性能的工具有:

 mpstat: mpstat 不但能查看所有 CPU的平均信息 ,还能查看指定CPU的信息。

 vmstat:只能查看所有CPU的平均信息;查看cpu队列信息;

 iostat: 只能查看所有CPU的平均信息。

 sar: 与mpstat 一样,不但能查看CPU的平均信息,还能查看指定CPU的信息。

 top:显示的信息同ps接近,但是top可以了解到CPU消耗,可以根据用户指定的时间来更新显示。

衡量CPU性能的指标: 1,用户使用CPU的情况; CPU运行常规用户进程 CPU运行niced process CPU运行实时进程
2,系统使用CPU情况; 用于I/O管理:中断和驱动 用于内存管理:页面交换 用户进程管理:进程开始和上下文切换
3,WIO:用于进程等待磁盘I/O而使CPU处于空闲状态的比率。 4,CPU的空闲率,除了上面的WIO以外的空闲时间 5,CPU用于上下文交换的比率
6,nice 7,real-time 8,运行进程队列的长度 9,平均负载 Linux中常用的监控CPU整体性能的工具有:  mpstat: mpstat
不但能查看所有CPU的平均信息,还能查看指定CPU的信息。  vmstat:只能查看所有CPU的平均信息;查看cpu队列信息;  iostat:
只能查看所有CPU的平均信息。  sar: 与mpstat 一样,不但能查看CPU的平均信息,还能查看指定CPU的信息。 
top:显示的信息同ps接近,但是top可以了解到CPU消耗,可以根据用户指定的时间来更新显示。 下面一一介绍: 一,vmstat
[root@localhost ~]# vmstat -n 3 (每个3秒刷新一次) procs -----------memory
--------------------swap-- ----io---- --system---- ------cpu-------- r b swpd
free buff cache si so bi bo in cs us sy id wa 1 0 144 186164 105252 2386848 0
0 18 166 83 2 48 21 31 0 2 0 144 189620 105252 2386848 0 0 0 177 1039 1210 34
10 56 0 0 0 144 214324 105252 2386848 0 0 0 10 1071 670 32 5 63 0 0 0 144
202212 105252 2386848 0 0 0 189 1035 558 20 3 77 0 2 0 144 158772 105252
2386848 0 0 0 203 1065 2832 70 14 15 0 红色内容标示CPU相关的参数 PROC(ESSES)
–r:如果在processes中运行的序列(process r)是连续的大于在系统中的CPU的个数表示系统现在运行比较慢,有多数的进程等待CPU.
如果r的输出数大于系统中可用CPU个数的4倍的话,则系统面临着CPU短缺的问题,或者是CPU的速率过低,系统中有多数的进程在等待CPU,造成系统中进程运行过慢.
SYSTEM --in:每秒产生的中断次数 --cs:每秒产生的上下文切换次数 上面2个值越大,会看到由内核消耗的CPU时间会越大 CPU
-us:用户进程消耗的CPU时间百分
us的值比较高时,说明用户进程消耗的CPU时间多,但是如果长期超50%的使用,那么我们就该考虑优化程序算法或者进行加速(比如PHP/PERL)
-sy:内核进程消耗的CPU时间百分比(sy的值高时,说明系统内核消耗的CPU资源多,这并不是良性表现,我们应该检查原因)
-wa:IO等待消耗的CPU时间百分比 wa的值高时,说明IO等待比较严重,这可能由于磁盘大量作随机访问造成,也有可能磁盘出现瓶颈(块操作)。
-id:CPU处于空闲状态时间百分比,如果空闲时间(cpu id)持续为0并且系统时间(cpu sy)是用户时间的两倍(cpu us)
系统则面临着CPU资源的短缺. 解决办法:
当发生以上问题的时候请先调整应用程序对CPU的占用情况.使得应用程序能够更有效的使用CPU.同时可以考虑增加更多的CPU.
关于CPU的使用情况还可以结合mpstat, ps aux top prstat
–a等等一些相应的命令来综合考虑关于具体的CPU的使用情况,和那些进程在占用大量的CPU时间.一般情况下,应用程序的问题会比较大一些.比如一些SQL语句不合理等等都会造成这样的现象.
二,sar sar [options] [-A] [-o file] t [n] 在命令行中,n 和t
两个参数组合起来定义采样间隔和次数,t为采样间隔,是必须有 的参数,n为采样次数,是可选的,默认值是1,-o file表示将命令结果以二进制格式
存放在文件中,file 在此处不是关键字,是文件名。options 为命令行选项,sar命令 的选项很多,下面只列出常用选项: -A:所有报告的总和。
-u:CPU利用率 -v:进程、I节点、文件和锁表状态。 -d:硬盘使用报告。 -r:内存和交换空间的使用统计。 -g:串口I/O的情况。
-b:缓冲区使用情况。 -a:文件读写情况。 -c:系统调用情况。 -q:报告队列长度和系统平均负载 -R:进程的活动情况。 -y:终端设备活动情况。
-w:系统交换活动。 -x { pid | SELF | ALL
}:报告指定进程ID的统计信息,SELF关键字是sar进程本身的统计,ALL关键字是所有系统进程的统计。 用sar进行CPU利用率的分析 #sar -u 2
10 Linux 2.6.18-53.el5PAE (localhost.localdomain) 03/28/2009 07:40:17 PM CPU
%user %nice %system %iowait %steal %idle 07:40:19 PM all 12.44 0.00 6.97 1.74
0.00 78.86 07:40:21 PM all 26.75 0.00 12.50 16.00 0.00 44.75 07:40:23 PM all
16.96 0.00 7.98 0.00 0.00 75.06 07:40:25 PM all 22.50 0.00 7.00 3.25 0.00
67.25 07:40:27 PM all 7.25 0.00 2.75 2.50 0.00 87.50 07:40:29 PM all 20.05
0.00 8.56 2.93 0.00 68.46 07:40:31 PM all 13.97 0.00 6.23 3.49 0.00 76.31
07:40:33 PM all 8.25 0.00 0.75 3.50 0.00 87.50 07:40:35 PM all 13.25 0.00 5.75
4.00 0.00 77.00 07:40:37 PM all 10.03 0.00 0.50 2.51 0.00 86.97 Average: all
15.15 0.00 5.91 3.99 0.00 74.95 在显示内容包括: %user:CPU处在用户模式下的时间百分比。
%nice:CPU处在带NICE值的用户模式下的时间百分比。 %system:CPU处在系统模式下的时间百分比。
%iowait:CPU等待输入输出完成时间的百分比。 %steal:管理程序维护另一个虚拟处理器时,虚拟CPU的无意识等待时间百分比。
%idle:CPU空闲时间百分比。
在所有的显示中,我们应主要注意%iowait和%idle,%iowait的值过高,表示硬盘存在I/O瓶颈,%idle值高,表示CPU较空闲,如果%idle值高但系统响应慢时,有可能是CPU等待分配内存,此时应加大内存容量。%idle值如果持续低于10,那么系统的CPU处理能力相对较低,表明系统中最需要解决的资源是CPU。
用sar进行运行进程队列长度分析: #sar -q 2 10 Linux 2.6.18-53.el5PAE (localhost.localdomain)
03/28/2009 07:58:14 PM runq-sz plist-sz ldavg-1 ldavg-5 ldavg-15 07:58:16 PM 0
493 0.64 0.56 0.49 07:58:18 PM 1 491 0.64 0.56 0.49 07:58:20 PM 1 488 0.59
0.55 0.49 07:58:22 PM 0 487 0.59 0.55 0.49 07:58:24 PM 0 485 0.59 0.55 0.49
07:58:26 PM 1 483 0.78 0.59 0.50 07:58:28 PM 0 481 0.78 0.59 0.50 07:58:30 PM
1 480 0.72 0.58 0.50 07:58:32 PM 0 477 0.72 0.58 0.50 07:58:34 PM 0 474 0.72
0.58 0.50 Average: 0 484 0.68 0.57 0.49 runq-sz 准备运行的进程运行队列。 plist-sz
进程队列里的进程和线程的数量 ldavg-1 前一分钟的系统平均负载(load average) ldavg-5 前五分钟的系统平均负载(load
average) ldavg-15 前15分钟的系统平均负载(load average) 顺便说一下load avarage的含义 load
average可以理解为每秒钟CPU等待运行的进程个数. 在Linux系统中,sar -q、uptime、w、top等命令都会有系统平均负载load
average的输出,那么什么是系统平均负载呢?
系统平均负载被定义为在特定时间间隔内运行队列中的平均任务数。如果一个进程满足以下条件则其就会位于运行队列中: - 它没有在等待I/O操作的结果 -
它没有主动进入等待状态(也就是没有调用’wait’) - 没有被停止(例如:等待终止) 例如: # uptime 20:55:40 up 24 days,
3:06, 1 user, load average: 8.13, 5.90, 4.94
命令输出的最后内容表示在过去的1、5、15分钟内运行队列中的平均进程数量。
一般来说只要每个CPU的当前活动进程数不大于3那么系统的性能就是良好的,如果每个CPU的任务数大于5,那么就表示这台机器的性能有严重问题。对
于上面的例子来说,假设系统有两个CPU,那么其每个CPU的当前任务数为:8.13/2=4.065。这表示该系统的性能是可以接受的。 三,iostat
#iostat -c 2 10 Linux 2.6.18-53.el5PAE (localhost.localdomain) 03/28/2009 avg-
cpu: %user %nice %system %iowait %steal %idle 30.10 0.00 4.89 5.63 0.00 59.38
avg-cpu: %user %nice %system %iowait %steal %idle 8.46 0.00 1.74 0.25 0.00
89.55 avg-cpu: %user %nice %system %iowait %steal %idle 22.06 0.00 11.28 1.25
0.00 65.41 四,mpstat mpstat是Multiprocessor
Statistics的缩写,是实时系统监控工具。其报告与CPU的一些统计信息,这些信息存放在/proc/stat文件中。在多CPUs系统里,其不但能查看所有CPU的平均状况信息,而且能够查看特定CPU的信息。下面只介绍
mpstat与CPU相关的参数,mpstat的语法如下: mpstat [-P {|ALL}] [internal [count]] 参数的含义如下: 参数
解释 -P {|ALL} 表示监控哪个CPU, cpu在[0,cpu个数-1]中取值 internal 相邻的两次采样的间隔时间 count
采样的次数,count只能和delay一起使用
当没有参数时,mpstat则显示系统启动以后所有信息的平均值。有interval时,第一行的信息自系统启动以来的平均信息。从第二行开始,输出为前一个interval时间段的平均信息。与CPU有关的输出的含义如下:
参数 解释 从/proc/stat获得数据 CPU 处理器ID user 在internal时间段里,用户态的CPU时间(%) ,不包含 nice值为负
进程 usr/total100 nice 在internal时间段里,nice值为负进程的CPU时间(%) nice/total100
system 在internal时间段里,核心时间(%) system/total100 iowait
在internal时间段里,硬盘IO等待时间(%) iowait/total
100 irq 在internal时间段里,软中断时间(%)
irq/total100 soft 在internal时间段里,软中断时间(%) softirq/total100 idle
在internal时间段里,CPU除去等待磁盘IO操作外的因为任何原因而空闲的时间闲置时间 (%) idle/total100 intr/s
在internal时间段里,每秒CPU接收的中断的次数 intr/total
100
CPU总的工作时间=total_cur=user+system+nice+idle+iowait+irq+softirq
total_pre=pre_user+ pre_system+ pre_nice+ pre_idle+ pre_iowait+ pre_irq+
pre_softirq user=user_cur – user_pre total=total_cur-total_pre 其中_cur
表示当前值,_pre表示interval时间前的值。上表中的所有值可取到两位小数点。 #mpstat -P ALL 2 10 Linux
2.6.18-53.el5PAE (localhost.localdomain) 03/28/2009 10:07:57 PM CPU %user
%nice %sys %iowait %irq %soft %steal %idle intr/s 10:07:59 PM all 20.75 0.00
10.50 1.50 0.25 0.25 0.00 66.75 1294.50 10:07:59 PM 0 16.00 0.00 9.00 1.50
0.00 0.00 0.00 73.50 1000.50 10:07:59 PM 1 25.76 0.00 12.12 1.52 0.00 0.51
0.00 60.10 294.00

java 成员变量 局部变量 静态变量

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NcCS2jdR-1681014581804)(【精】各大厂问题汇总_files/Image [11].jpg)]

高并发架构

消息队列

如何实现高可用性

RabbitMQ 镜像集群模式,RabbitMQ
有很好的管理控制台,就是在后台新增一个策略,这个策略是镜像集群模式的策略,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建
queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。

Kafka 一个最基本的架构认识:由多个 broker 组成,每个 broker 是一个节点;你创建一个 topic,这个 topic 可以划分为多个
partition,每个 partition 可以存在于不同的 broker 上,每个 partition 就放一部分数据。

这就是天然的分布式消息队列,就是说一个 topic 的数据,是分散放在多个机器上的,每个机器就放一部分数据。、

Kafka 0.8 以后,提供了 HA 机制,就是 replica(复制品) 副本机制。每个 partition 的数据都会同步到其它机器上,形成自己的多个
replica 副本。如果某个 broker 宕机了,没事儿,那个 broker 上面的 partition 在其他机器上都有副本的。如果这个宕机的
broker 上面有某个 partition 的 leader,那么此时会从 follower 中重新选举一个新的 leader 出来,大家继续读写那个新的
leader 即可。这就有所谓的高可用性了。

写数据的时候,生产者就写 leader,然后 leader 将数据落地写本地磁盘,接着其他 follower 自己主动从 leader 来 pull
数据。一旦所有 follower 同步好数据了,就会发送 ack 给 leader,leader 收到所有 follower 的 ack
之后,就会返回写成功的消息给生产者。(当然,这只是其中一种模式,还可以适当调整这个行为)

保证可靠性传输,防止数据丢失

RabbitMQ

此时可以选择用 RabbitMQ 提供的事务功能,就是生产者发送数据之前开启 RabbitMQ 事务
channel.txSelectt,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务
但是问题是,RabbitMQ 事务机制(同步)一搞,基本上吞吐量会下来,因为太耗性能

所以一般在生产者这块避免数据丢失,都是用 confirm 机制的。

在生产者那里设置开启 confirm 模式之后,你每次写的消息都会分配一个唯一的 id,然后如果写入了 RabbitMQ 中,RabbitMQ
会给你回传一个 ack 消息,告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消息,会回调你的一个

nack 接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息 id
的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。

设置持久化有两个步骤:

  • 创建 queue 的时候将其设置为持久化

这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是它是不会持久化 queue 里的数据的。

  • 第二个是发送消息的时候将消息的 deliveryMode 设置为 2

就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。

必须要同时设置这两个持久化才行,RabbitMQ 哪怕是挂了,再次重启,也会从磁盘上重启恢复 queue,恢复这个 queue 里的数据。

注意,哪怕是你给 RabbitMQ 开启了持久化机制,也有一种可能,就是这个消息写到了 RabbitMQ 中,但是还没来得及持久化到磁盘上,结果不巧,此时
RabbitMQ 挂了,就会导致内存里的一点点数据丢失。

所以,持久化可以跟生产者那边的 confirm 机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者 ack
了,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢了,生产者收不到 ack ,你也是可以自己重发的。

消费端弄丢了数据(关闭自动 )

RabbitMQ 如果丢失了数据,主要是因为你消费的时候,刚消费到,还没处理,结果进程挂了,比如重启了,那么就尴尬了,RabbitMQ
认为你都消费了,这数据就丢了。

这个时候得用 RabbitMQ 提供的 ack 机制,简单来说,就是你必须关闭 RabbitMQ 的自动 ack ,可以通过一个 api
来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里 ack 一把。这样的话,如果你还没处理完,不就没有 ack 了?那 RabbitMQ
就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。

Kafka 消费端弄丢了数据

唯一可能导致消费者弄丢数据的情况,就是说,你消费到了这个消息,然后消费者那边自动提交了 offset,让 Kafka
以为你已经消费好了这个消息,但其实你才刚准备处理这个消息,你还没处理,你自己就挂了,此时这条消息就丢咯。

这不是跟 RabbitMQ 差不多吗,大家都知道 Kafka 会自动提交 offset,那么只要关闭自动提交 offset,在处理完之后自己手动提交
offset,就可以保证数据不会丢。但是此时确实还是可能会有重复消费,比如你刚处理完,还没提交
offset,结果自己挂了,此时肯定会重复消费一次,自己保证幂等性就好了。

所以此时一般是要求起码设置如下 4 个参数:

  • 给 topic 设置 replication.factor 参数:这个值必须大于 1,要求每个 partition 必须有至少 2 个副本。

  • 在 Kafka 服务端设置 min.insync.replicas 参数:这个值必须大于 1,这个是要求一个 leader 至少感知到有至少一个 follower 还跟自己保持联系,没掉队,这样才能确保 leader 挂了还有一个 follower 吧。

  • 在 producer 端设置 acks=all :这个是要求每条数据,必须是写入所有 replica 之后,才能认为是写成功了。

  • 在 producer 端设置 retries=MAX (很大很大很大的一个值,无限次重试的意思):这个是要求一旦写入失败,就无限重试,卡在这里了。

[如何保证消息不被重复消费?(如何保证消息消费的幂等性)](https://github.com/doocs/advanced-

java/blob/main/docs/high-concurrency/how-to-ensure-that-messages-are-not-
repeatedly-consumed.md)

  • 比如你拿个数据要写库,你先根据主键 一下,如果这数据都有了,你就别插入了,update 一下好吧。

  • 比如你是写 Redis,那没问题了,反正每次都是 set, 天然幂等性

  • 比如你不是上面两个场景,那做的稍微复杂一点,你需要让生产者发送每条数据的时候,里面加一个全局唯一的 id,类似订单 id 之类的东西,然后你这里消费到了之后,先根据这个 id 去比如 Redis 里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个 id 写 Redis 。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。

  • 比如基于数据库的 唯一键 来保证重复数据不会重复插入多条。因为有唯一键约束了,重复数据插入只会报错,不会导致数据库中出现脏数据。

执行顺序问题

RabbitMQ

拆分多个 queue,每个 queue 一个 consumer,就是多一些 queue 而已,确实是麻烦点;或者就一个 queue 但是对应一个
consumer,然后这个 consumer 内部用内存队列做排队,然后分发给底层不同的 worker 来处理。

Kafka

  • 一个 topic,一个 partition,一个 consumer,内部单线程消费,单线程吞吐量太低,一般不会用这个。

  • 写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。

[如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决?](https://github.com/doocs/advanced-
java/blob/main/docs/high-concurrency/mq-time-delay-and-expired-failure.md)

大量消息在 mq 里积压了几个小时了还没解决

几千万条数据在 MQ 里积压了七八个小时,从下午 4 点多,积压到了晚上 11
点多。这个是我们真实遇到过的一个场景,确实是线上故障了,这个时候要不然就是修复 consumer
的问题,让它恢复消费速度,然后傻傻的等待几个小时消费完毕。这个肯定不能在面试的时候说吧。

一个消费者一秒是 1000 条,一秒 3 个消费者是 3000 条,一分钟就是 18
万条。所以如果你积压了几百万到上千万的数据,即使消费者恢复了,也需要大概 1 小时的时间才能恢复过来。

一般这个时候,只能临时紧急扩容了,具体操作步骤和思路如下:

  • 先修复 consumer 的问题,确保其恢复消费速度,然后将现有 consumer 都停掉。

  • 新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。

  • 然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的 10 倍数量的 queue。

  • 接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。

  • 等快速消费完积压数据之后,得恢复原先部署的架构,重新用原先的 consumer 机器来消费消息。

mq 中的消息过期失效了

假设你用的是 RabbitMQ,RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ
给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq 里,而是大量的数据会直接搞丢。

这个情况下,就不是说要增加 consumer
消费积压的消息,因为实际上没啥积压,而是丢了大量的消息。我们可以采取一个方案,就是批量重导,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上
12 点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq
里面去,把白天丢的数据给他补回来。也只能是这样了。

假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。

mq 都快写满了

某些业务流程如果支持批量方式消费,

发生消息堆积时,如果消费速度一直追不上发送速度,如果业务对数据要求不高的话,可以选择丢弃不重要的消息。

优化每条消息消费过程

搜索引擎

现在很多项目都是直接用基于 lucene 的分布式搜索引擎—— ElasticSearch,简称为 ES。ElasticSearch
设计的理念就是分布式搜索引擎,底层其实还是基于 lucene 的。核心思想就是在多台机器上启动多个 ES 进程实例,组成了一个 ES 集群。

ES 中存储数据的基本单位是索引,比如说你现在要在 ES 中存储一些订单数据,你就应该在 ES 中创建一个索引 order_idx
,所有的订单数据就都写到这个索引里面去,一个索引差不多就是相当于是 mysql 里的一张表。

index -> type -> mapping -> document -> field。

你可以认为 index 是一个类别的表,具体的每个 type 代表了 mysql 中的一个表。每个 type 有一个 mapping,如果你认为一个
type 是具体的一个表,index 就代表多个 type 同属于的一个类型,而 mapping 就是这个 type 的表结构定义,你在 mysql
中创建一个表,肯定是要定义表结构的,里面有哪些字段,每个字段是什么类型。实际上你往 index 里的一个 type 里面写的一条数据,叫做一条
document,一条 document 就代表了 mysql 中某个表里的一行,每个 document 有多个 field,每个 field 就代表了这个
document 中的一个字段的值。

你搞一个索引,这个索引可以拆分成多个 shard ,每个 shard 存储部分数据。拆分多个 shard 是有好处的,一是支持横向扩展
二是提高性能,数据分布在多个 shard,即多台服务器上,所有的操作,都会在多台机器上并行分布式执行,提高了吞吐量和性能。

缓存

为什么要用缓存?

用缓存,主要有两个用途:高性能、高并发。

所以要是你有个系统,高峰期一秒钟过来的请求有 1 万,那一个 mysql 单机绝对会死掉。你这个时候就只能上缓存,把很多数据放缓存,别放
mysql。缓存功能简单,说白了就是 key-value 式操作,单机支撑的并发量轻松一秒几万十几万,支撑高并发 so easy。单机承载并发量是
mysql 单机的几十倍。

为啥 Redis 单线程模型也能效率这么高?

  • 纯内存操作。

  • 核心是基于非阻塞的 IO 多路复用机制。

  • C 语言实现,一般来说,C 语言实现的程序“距离”操作系统更近,执行速度相对会更快。

  • 单线程反而避免了多线程的频繁上下文切换问题,预防了多线程可能产生的竞争问题。

Redis 主要有以下几种数据类型:

  • Strings

  • Hashes

  • Lists

  • Sets

  • Sorted Sets

Redis 除了这 5 种数据类型之外,还有 Bitmaps、HyperLogLogs、Streams 等。

Strings这是最简单的类型,就是普通的 set 和 get,做简单的 KV 缓存。 set college szu Hashes这个是类似 map
的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在 Redis 里,然后每次读写缓存的时候,可以就操作
hash 里的某个字段。 hset person name bingo hset person age 20 hset person id 1 hget
person name (person = { “name”: “bingo”, “age”: 20, “id”: 1 }) Lists
是有序列表,这个可以玩儿出很多花样。比如可以通过 list 存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的东西。比如可以通过 lrange
命令,读取某个闭区间内的元素,可以基于 list 实现分页查询,这个是很棒的一个功能,基于 Redis
实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走。 比如可以搞个简单的消息队列,从 list 头怼进去,从 list
尾巴那里弄出来。lpush mylist 1 lpush mylist 2 lpush mylist 3 4 5 # 1 rpop mylist Sets
是无序集合,自动去重。直接基于 set 将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于 jvm
内存里的 HashSet 进行去重,但是如果你的某个系统部署在多台机器上呢?得基于 Redis 进行全局的 set 去重。 可以基于 set
玩儿交集、并集、差集的操作,比如交集吧,可以把两个人的粉丝列表整一个交集,看看俩人的共同好友是谁?对吧。把两个大 V 的粉丝都放在两个 set 中,对两个
set 做交集。 #-------操作一个set-------# 添加元素 sadd mySet 1 # 查看全部元素 smembers mySet #
判断是否包含某个值 sismember mySet 3 # 删除某个/些元素 srem mySet 1 srem mySet 2 4 # 查看元素个数
scard mySet # 随机删除一个元素 spop mySet #-------操作多个set-------# 将一个set的元素移动到另外一个set
smove yourSet mySet 2 # 求两set的交集 sinter yourSet mySet # 求两set的并集 sunion
yourSet mySet # 求在yourSet中而不在mySet中的元素 sdiff yourSet mySet Sorted SetsSorted
Sets 是排序的 set,去重但可以排序,写进去的时候给一个分数,自动根据分数排序。zadd board 85 zhangsan zadd board
72 lisi zadd board 96 wangwu zadd board 63 zhaoliu # 获取排名前三的用户(默认是升序,所以需要 rev
改为降序) zrevrange board 0 3 # 获取某用户的排名 zrank board zhaoliu

内存淘汰机制

Redis 内存淘汰机制有以下几个:

  • noeviction: 当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了。

  • allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。

  • allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的 key 给干掉啊。

  • volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key(这个一般不太合适)。

  • volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key。

  • volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除。

实现高并发

redis 实现高并发主要依靠主从架构,一主多从,一般来说,很多项目其实就足够了,单主用来写入数据,单机几万
QPS,多从用来查询数据,多个从实例可以提供每秒 10w 的 QPS。

如果想要在实现高并发的同时,容纳大量的数据,那么就需要 redis 集群,使用 redis 集群之后,可以提供每秒几十万的读写并发。

redis 高可用,如果是做主从架构部署,那么加上哨兵就可以了,就可以实现,任何一个实例宕机,可以进行主备切换。

单机的 Redis,能够承载的 QPS 大概就在上万到几万不等。对于缓存来说,一般都是用来支撑读高并发的。因此架构做成主从(master-
slave)架构,一主多从,主负责写,并且将数据复制到其它的 slave
节点,从节点负责读。所有的读请求全部走从节点。这样也可以很轻松实现水平扩容,支撑读高并发

Redis replication 的核心机制

  • Redis 采用异步方式复制数据到 slave 节点,不过 Redis2.8 开始,slave node 会周期性地确认自己每次复制的数据量;

  • 一个 master node 是可以配置多个 slave node 的;

  • slave node 也可以连接其他的 slave node;

  • slave node 做复制的时候,不会 block master node 的正常工作;

  • slave node 在做复制的时候,也不会 block 对自己的查询操作,它会用旧的数据集来提供服务;但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了;

  • slave node 主要用来进行横向扩容,做读写分离,扩容的 slave node 可以提高读的吞吐量。

Redis 主从复制的核心原理

当启动一个 slave node 的时候,它会发送一个 PSYNC 命令给 master node。

如果这是 slave node 初次连接到 master node,那么会触发一次 full resynchronization 全量复制。此时
master 会启动一个后台线程,开始生成一份 RDB 快照文件,同时还会将从客户端 client 新收到的所有写命令缓存在内存中。 RDB
文件生成完毕后, master 会将这个 RDB 发送给 slave,slave 会先写入本地磁盘,然后再从本地磁盘加载到内存中,接着 master
会将内存中缓存的写命令发送到 slave,slave 也会同步这些数据。slave node 如果跟 master node
有网络故障,断开了连接,会自动重连,连接之后 master node 仅会复制给 slave 部分缺少的数据。

哨兵的介绍

sentinel,中文名是哨兵。哨兵是 Redis 集群架构中非常重要的一个组件,主要有以下功能:

  • 集群监控:负责监控 Redis master 和 slave 进程是否正常工作。

  • 消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。

  • 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。

  • 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。

哨兵用于实现 Redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。

  • 故障转移时,判断一个 master node 是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题。

  • 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了。

哨兵的核心知识

  • 哨兵至少需要 3 个实例,来保证自己的健壮性。

  • 哨兵 + Redis 主从的部署架构,是不保证数据零丢失的,只能保证 Redis 集群的高可用性。

  • 对于哨兵 + Redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。

Redis 持久化的两种方式

  • RDB:RDB 持久化机制,是对 Redis 中的数据执行周期性的持久化。

  • AOF:AOF 机制对每条写入命令作为日志,以 append-only 的模式写入一个日志文件中,在 Redis 重启的时候,可以通过回放 AOF 日志中的写入指令来重新构建整个数据集。

通过 RDB 或 AOF,都可以将 Redis 内存中的数据给持久化到磁盘上面来,然后可以将这些数据备份到别的地方去,比如说阿里云等云服务。

如果 Redis 挂了,服务器上的内存和磁盘上的数据都丢了,可以从云服务上拷贝回来之前的数据,放到指定的目录中,然后重新启动 Redis,Redis
就会自动根据持久化数据文件中的数据,去恢复内存中的数据,继续对外提供服务。

如果同时使用 RDB 和 AOF 两种持久化机制,那么在 Redis 重启的时候,会使用 AOF 来重新构建数据,因为 AOF 中的数据更加完整。

RDB 和 AOF 到底该如何选择

  • 不要仅仅使用 RDB,因为那样会导致你丢失很多数据;

  • 也不要仅仅使用 AOF,因为那样有两个问题:第一,你通过 AOF 做冷备,没有 RDB 做冷备来的恢复速度更快;第二,RDB 每次简单粗暴生成数据快照,更加健壮,可以避免 AOF 这种复杂的备份和恢复机制的 bug;

  • Redis 支持同时开启开启两种持久化方式,我们可以综合使用 AOF 和 RDB 两种持久化机制,用 AOF 来保证数据不丢失,作为数据恢复的第一选择;用 RDB 来做不同程度的冷备,在 AOF 文件都丢失或损坏不可用的时候,还可以使用 RDB 来进行快速的数据恢复。

[Redis 集群模式的工作原理能说一下么?在集群模式下,Redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash

算法吗?如何动态增加和删除一个节点?](https://github.com/doocs/advanced-
java/blob/main/docs/high-concurrency/redis-cluster.md)

Redis cluster 介绍

  • 自动将数据进行分片,每个 master 上放一部分数据

  • 提供内置的高可用支持,部分 master 不可用时,还是可以继续工作的

在 Redis cluster 架构下,每个 Redis 要放开两个端口号,比如一个是 6379,另外一个就是 加 1w 的端口号,比如 16379。

16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus
的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus 用了另外一种二进制的协议, gossip
协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。

一致性 hash 算法

一致性 hash 算法将整个 hash 值空间组织成一个虚拟的圆环,整个空间按顺时针方向组织,下一步将各个 master 节点(使用服务器的 ip
或主机名)进行 hash。这样就能确定每个节点在其哈希环上的位置。

来了一个 key,首先计算 hash 值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,遇到的第一个 master 节点就是 key 所在位置。

在一致性哈希算法中,如果一个节点挂了,受影响的数据仅仅是此节点到环空间前一个节点(沿着逆时针方向行走遇到的第一个节点)之间的数据,其它不受影响。增加一个节点也同理。

燃鹅,一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成缓存热点的问题。为了解决这种热点问题,一致性 hash
算法引入了虚拟节点机制,即对每一个节点计算多个 hash,每个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布,负载均衡。

缓存雪崩的事前事中事后的解决方案如下:

  • 事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。

  • 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。

  • 事后:Redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。

缓存穿透

解决方式很简单,每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,比如 set -999 UNKNOWN
。然后设置一个过期时间,这样的话,下次有相同的 key 来访问的时候,在缓存失效之前,都可以直接从缓存中取数据。

缓存击穿

缓存击穿,就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key
在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。

不同场景下的解决方式可如下:

  • 若缓存的数据是基本不会发生更新的,则可尝试将该热点数据设置为永不过期。

  • 若缓存的数据更新不频繁,且缓存刷新的整个流程耗时较少的情况下,则可以采用基于 Redis、zookeeper 等分布式中间件的分布式互斥锁,或者本地互斥锁以保证仅少量的请求能请求数据库并重新构建缓存,其余线程则在锁释放后能访问到新缓存。

  • 若缓存的数据更新频繁或者在缓存刷新的流程耗时较长的情况下,可以利用定时线程在缓存过期前主动地重新构建缓存或者延后缓存的过期时间,以保证所有的请求能一直访问到对应的缓存。

分库分表

如何保证缓存与数据库的双写一致性?

Cache Aside Pattern

最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。

  • 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。

  • 更新的时候,先更新数据库,然后再删除缓存。

举个栗子,一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;但是这个缓存在 1
分钟内只被读取了 1 次,有大量的冷数据。实际上,如果你只是删除缓存的话,那么在 1
分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。用到缓存才去算缓存。

最初级的缓存不一致问题及解决方案

解决思路:先删除缓存,再更新数据库。如果数据库更新失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。因为读的时候缓存没有,所以去读了数据库中的旧数据,然后更新到缓存中。

Redis 的并发竞争问题是什么?如何解决这个问题?了解 Redis 事务的 CAS 方案吗?

某个时刻,多个系统实例都去更新某个 key。可以基于 zookeeper 实现分布式锁。每个系统通过 zookeeper
获取分布式锁,确保同一时间,只能有一个系统实例在操作某个 key,别人都不允许读和写。

你要写入缓存的数据,都是从 mysql 里查出来的,都得写入 mysql 中,写入 mysql 中的时候必须保存一个时间戳,从 mysql
查出来的时候,时间戳也查出来。

每次要写之前,先判断一下当前这个 value 的时间戳是否比缓存里的 value 的时间戳要新。如果是的话,那么可以写,否则,就不能用旧的数据覆盖新的数据。

生产环境中的 Redis 是怎么部署的?

看看你了解不了解你们公司的 Redis 生产集群的部署架构,如果你不了解,那么确实你就很失职了,你的 Redis
是主从架构?集群架构?用了哪种集群方案?有没有做高可用保证?有没有开启持久化机制确保可以进行数据恢复?线上 Redis 给几个 G
的内存?设置了哪些参数?压测后你们 Redis 集群承载多少 QPS?

Redis cluster,10 台机器,5 台机器部署了 Redis 主实例,另外 5 台机器部署了 Redis 的从实例,每个主实例挂了一个从实例,5
个节点对外提供读写服务,每个节点的读写高峰 QPS 可能可以达到每秒 5 万,5 台机器最多是 25 万读写请求每秒。

机器是什么配置?32G 内存+ 8 核 CPU + 1T 磁盘,但是分配给 Redis 进程的是 10g 内存,一般线上生产环境,Redis
的内存尽量不要超过 10g,超过 10g 可能会有问题。

5 台机器对外提供读写,一共有 50g 内存。

因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,Redis 从实例会自动变成主实例继续提供读写服务。

你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200
万条商品数据,占用内存是 20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。

其实大型的公司,会有基础架构的 team 负责缓存集群的运维。

数据迁移双写迁移方案

简单来说,就是在线上系统里面,之前所有写库的地方,增删改操作,除了对老库增删改,都加上对新库的增删改,这就是所谓的双写,同时写俩库,老库和新库。

然后系统部署之后,新库数据差太远,用之前说的导数工具,跑起来读老库数据写新库,写的时候要根据 gmt_modified
这类字段判断这条数据最后修改的时间,除非是读出来的数据在新库里没有,或者是比新库的数据新才会写。简单来说,就是不允许用老数据覆盖新数据。

读写分离

MySQL 主从复制原理的是啥?

主库将变更写入 binlog 日志,然后从库连接到主库之后,从库有一个 IO 线程,将主库的 binlog 日志拷贝到自己本地,写入一个 relay
中继日志中。接着从库中有一个 SQL 线程会从中继日志读取 binlog,然后执行 binlog 日志中的内容,也就是在自己本地再次执行一遍
SQL,这样就可以保证自己跟主库的数据是一样的。

这里有一个非常重要的一点,就是从库同步主库数据的过程是串行化的,也就是说主库上并行的操作,在从库上会串行执行。所以这就是一个非常重要的点了,由于从库从主库拷贝日志以及串行执行
SQL
的特点,在高并发场景下,从库的数据一定会比主库慢一些,是有延时的。所以经常出现,刚写入主库的数据可能是读不到的,要过几十毫秒,甚至几百毫秒才能读取到。

所以 MySQL 实际上在这一块有两个机制,一个是半同步复制,用来解决主库数据丢失问题;一个是并行复制,用来解决主从同步延时问题。

这个所谓半同步复制,也叫 semi-sync 复制,指的就是主库写入 binlog 日志之后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的
relay log 之后,接着会返回一个 ack 给主库,主库接收到至少一个从库的 ack 之后才会认为写操作完成了。

所谓并行复制,指的是从库开启多个线程,并行读取 relay log 中不同库的日志,然后并行重放不同库的日志,这是库级别的并行。

一般来说,如果主从延迟较为严重,有以下解决方案:

  • 分库,将一个主库拆分为多个主库,每个主库的写并发就减少了几倍,此时主从延迟可以忽略不计。

  • 打开 MySQL 支持的并行复制,多个库并行复制。如果说某个库的写入并发就是特别高,单库写并发达到了 2000/s,并行复制还是没意义。

  • 重写代码,写代码的同学,要慎重,插入数据时立马查询可能查不到。

  • 如果确实是存在必须先插入,立马要求就查询到,然后立马就要反过来执行一些操作,对这个查询设置直连主库。不推荐这种方法,你要是这么搞,读写分离的意义就丧失了。

高并发系统

实现高并发。可以分为以下 6 点:

  • 系统拆分

  • 缓存

  • MQ

  • 分库分表

  • 读写分离

  • ElasticSearch

系统拆分

将一个系统拆分为多个子系统,用 dubbo 来搞。然后每个系统连一个数据库,这样本来就一个库,现在多个数据库,不也可以扛高并发么。

缓存

缓存,必须得用缓存。大部分的高并发场景,都是读多写少,那你完全可以在数据库和缓存里都写一份,然后读的时候大量走缓存不就得了。毕竟人家 redis
轻轻松松单机几万的并发。所以你可以考虑考虑你的项目里,那些承载主要请求的读场景,怎么用缓存来抗高并发。

MQ

MQ,必须得用 MQ。可能你还是会出现高并发写的场景,比如说一个业务操作里要频繁搞数据库几十次,增删改增删改,疯了。那高并发绝对搞挂你的系统,你要是用
redis 来承载写那肯定不行,人家是缓存,数据随时就被 LRU 了,数据格式还无比简单,没有事务支持。所以该用 mysql 还得用 mysql
啊。那你咋办?用 MQ 吧,大量的写请求灌入 MQ 里,排队慢慢玩儿,后边系统消费后慢慢写,控制在 mysql
承载范围之内。所以你得考虑考虑你的项目里,那些承载复杂写业务逻辑的场景里,如何用 MQ 来异步写,提升并发性。MQ 单机抗几万并发也是 ok
的,这个之前还特意说过。

分库分表

分库分表,可能到了最后数据库层面还是免不了抗高并发的要求,好吧,那么就将一个数据库拆分为多个库,多个库来扛更高的并发;然后将一个表拆分为多个表,每个表的数据量保持少一点,提高
sql 跑的性能。

读写分离

读写分离,这个就是说大部分时候数据库可能也是读多写少,没必要所有请求都集中在一个库上吧,可以搞个主从架构,主库写入,从库读取,搞一个读写分离。读流量太多的时候,还可以加更多的从库。

ElasticSearch

Elasticsearch,简称 es。es
是分布式的,可以随便扩容,分布式天然就可以支撑高并发,因为动不动就可以扩容加机器来扛更高的并发。那么一些比较简单的查询、统计类的操作,可以考虑用 es
来承载,还有一些全文搜索类的操作,也可以考虑用 es 来承载。

上面的 6
点,基本就是高并发系统肯定要干的一些事儿,大家可以仔细结合之前讲过的知识考虑一下,到时候你可以系统的把这块阐述一下,然后每个部分要注意哪些问题,之前都讲过了,你都可以阐述阐述,表明你对这块是有点积累的。

分布式服务架构

分布式锁

大致来说,zookeeper 的使用场景如下,我就举几个简单的,大家能说几个就好了:

  • 分布式协调

  • 分布式锁

  • 元数据/配置信息管理

  • HA 高可用性

redis 分布式锁和 zk 分布式锁的对比

  • redis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能。

  • zk 分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。

另外一点就是,如果是 Redis 获取锁的那个客户端 出现 bug 挂了,那么只能等待超时时间之后才能释放锁;而 zk 的话,因为创建的是临时
znode,只要客户端挂了,znode 就没了,此时就自动释放锁。

Redis 分布式锁大家没发现好麻烦吗?遍历上锁,计算时间等等…zk 的分布式锁语义清晰实现简单。

所以先不分析太多的东西,就说这两点,我个人实践认为 zk 的分布式锁比 Redis 的分布式锁牢靠、而且模型简单易用。

Redis 最普通的分布式锁

第一个最普通的实现方式,就是在 Redis 里使用 SET key value [EX seconds] [PX milliseconds] NX 创建一个
key,这样就算加锁。其中:

  • NX:表示只有 key 不存在的时候才会设置成功,如果此时 redis 中存在这个 key,那么设置失败,返回 nil。

  • EX seconds:设置 key 的过期时间,精确到秒级。意思是 seconds 秒后锁自动释放,别人创建的时候如果发现已经有了就不能加锁了。

  • PX milliseconds:同样是设置 key 的过期时间,精确到毫秒级。

比如执行以下命令:

SETresource_namemy_random_valuePX30000NX

分布式事务

TCC 方案

TCC 的全称是: Try 、 Confirm 、 Cancel 。

  • Try 阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行锁定或者预留。

  • Confirm 阶段:这个阶段说的是在各个服务中执行实际的操作。

  • Cancel 阶段:如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,就是执行已经执行成功的业务逻辑的回滚操作。(把那些执行成功的回滚)

这种方案说实话几乎很少人使用,我们用的也比较少,但是也有使用的场景。因为这个事务回滚实际上是严重依赖于你自己写代码来回滚和补偿了,会造成补偿代码巨大,非常之恶心。

比如说我们,一般来说跟钱相关的,跟钱打交道的,支付、交易相关的场景,我们会用
TCC,严格保证分布式事务要么全部成功,要么全部自动回滚,严格保证资金的正确性,保证在资金上不会出现问题。

而且最好是你的各个业务执行的时间都比较短。

但是说实话,一般尽量别这么搞,自己手写回滚逻辑,或者是补偿逻辑,实在太恶心了,那个业务代码是很难维护的。

最大努力通知方案

这个方案的大致意思就是:

  1. 系统 A 本地事务执行完之后,发送个消息到 MQ;

  2. 这里会有个专门消费 MQ 的最大努力通知服务,这个服务会消费 MQ 然后写入数据库中记录下来,或者是放入个内存队列也可以,接着调用系统 B 的接口;

  3. 要是系统 B 执行成功就 ok 了;要是系统 B 执行失败了,那么最大努力通知服务就定时尝试重新调用系统 B,反复 N 次,最后还是不行就放弃。

分布式会话

完全不用 Session

使用 JWT Token 储存用户身份,然后再从数据库或者 cache 中获取其他的信息。这样无论请求分配到哪个服务器都无所谓。

Tomcat + Redis

这个其实还挺方便的,就是使用 Session 的代码,跟以前一样,还是基于 Tomcat 原生的 Session 支持即可,然后就是用一个叫做 Tomcat
RedisSessionManager 的东西,让所有我们部署的 Tomcat 都将 Session 数据存储到 Redis 即可。

Spring Session + Redis

上面所说的第二种方式会与 Tomcat 容器重耦合,如果我要将 Web 容器迁移成 Jetty,难道还要重新把 Jetty 都配置一遍?

因为上面那种 Tomcat + Redis 的方式好用,但是会严重依赖于 Web 容器,不好将代码移植到其他 Web
容器上去,尤其是你要是换了技术栈咋整?比如换成了 Spring Cloud 或者是 Spring Boot 之类的呢?

所以现在比较好的还是基于 Java 一站式解决方案,也就是 Spring。人家 Spring 基本上承包了大部分我们需要使用的框架,Spirng Cloud
做微服务,Spring Boot 做脚手架,所以用 Spring Session 是一个很好的选择。

高可用架构

大型电商网站的商品详情页系统架构

大型电商网站商品详情页的系统设计中,当商品数据发生变更时,会将变更消息压入 MQ
消息队列中。缓存服务从消息队列中消费这条消息时,感知到有数据发生变更,便通过调用数据服务接口,获取变更后的数据,然后将整合好的数据推送至 redis
中。Nginx 本地缓存的数据是有一定的时间期限的,比如说 10 分钟,当数据过期之后,它就会从 redis 获取到最新的缓存数据,并且缓存到自己本地。

用户浏览网页时,动态将 Nginx 本地数据渲染到本地 html 模板并返回给用户。

虽然没有直接返回 html 页面那么快,但是因为数据在本地缓存,所以也很快,其实耗费的也就是动态渲染一个 html 页面的性能。如果 html
模板发生了变更,不需要将所有的页面重新静态化,也不需要发送请求,没有网络请求的开销,直接将数据渲染进最新的 html 页面模板后响应即可。

如果系统访问量很高,Nginx 本地缓存过期失效了,redis 中的缓存也被 LRU
算法给清理掉了,那么会有较高的访问量,从缓存服务调用商品服务。但如果此时商品服务的接口发生故障,调用出现了延时,缓存服务全部的线程都被这个调用商品服务接口给耗尽了,每个线程去调用商品服务接口的时候,都会卡住很长时间,后面大量的请求过来都会卡在那儿,此时缓存服务没有足够的线程去调用其它一些服务的接口,从而导致整个大量的商品详情页无法正常显示。

高可用系统

微服务架构

海量数据处理

如何从大量的 URL 中找出相同的 URL?

题目描述

给定 a、b 两个文件,各存放 50 亿个 URL,每个 URL 各占 64B,内存限制是 4G。请找出 a、b 两个文件共同的 URL。

解答思路

1. 分治策略

每个 URL 占 64B,那么 50 亿个 URL 占用的空间大小约为 320GB。

5, 000, 000, 000 _ 64B ≈ 5GB _ 64 = 320GB

由于内存大小只有 4G,因此,我们不可能一次性把所有 URL 加载到内存中处理。对于这种类型的题目,一般采用分治策略,即:把一个文件中的 URL
按照某个特征划分为多个小文件,使得每个小文件大小不超过 4G,这样就可以把这个小文件读到内存中进行处理了。

思路如下:

首先遍历文件 a,对遍历到的 URL 求 hash(URL) % 1000 ,根据计算结果把遍历到的 URL 存储到 a0, a1, a2, …,
a999,这样每个大小约为 300MB。使用同样的方法遍历文件 b,把文件 b 中的 URL 分别存储到文件 b0, b1, b2, …, b999
中。这样处理过后,所有可能相同的 URL 都在对应的小文件中,即 a0 对应 b0, …, a999 对应 b999,不对应的小文件不可能有相同的
URL。那么接下来,我们只需要求出这 1000 对小文件中相同的 URL 就好了。

接着遍历 ai( i∈[0,999] ),把 URL 存储到一个 HashSet 集合中。然后遍历 bi 中每个 URL,看在 HashSet
集合中是否存在,若存在,说明这就是共同的 URL,可以把这个 URL 保存到一个单独的文件中。

分治策略

  1. 分而治之,进行哈希取余;

  2. 对每个子文件进行 HashSet 统计。

1Socket和http的区别

http: 如何封装数据;基于TCP协议,简单的对象访问协议,对应于应用层;(货物)

tcp协议:数据在网络中的传输;对应于传输层;(卡车)

IP协议:对应网络层;数据在网络中的传输(高速公路)

TCP/IP:解决数据如何在网络中传输,HTTP:如何包装数据

Socket是对TCP/IP协议的封装,Socket本身并不是协议,而是一个调用接口(API),通过Socket,我们才能使用TCP/IP协议。

Http连接:http连接就是所谓的短连接,及客户端向服务器发送一次请求,服务器端相应后连接即会断掉。

socket连接:socket连接及时所谓的长连接,理论上客户端和服务端一旦建立连接,则不会主动断掉;但是由于各种环境因素可能会是连接断开,比如说:服务器端或客户端主机down了,网络故障,或者两者之间长时间没有数据传输,网络防火墙可能会断开该链接已释放网络资源。所以当一个socket连接中没有数据的传输,那么为了位置连续的连接需要发送心跳消息,具体心跳消息格式是开发者自己定义的

三、注解的原理:

注解本质是一个继承了Annotation 的特殊接口,其具体实现类是Java 运行时生成的动态代理类。而我们通过反射获取注解时,返回的是Java
运行时生成的动态代理对象$Proxy1。通过代理对象调用自定义注解(接口)的方法,会最终调用AnnotationInvocationHandler
的invoke 方法。该方法会从memberValues 这个Map 中索引出对应的值。而memberValues 的来源是Java 常量池。

2. FULL gc排查问题时能使用哪些工具

公司的监控系统:大部分公司都会有,可全方位监控JVM的各项指标。

JDK的自带工具,包括jmap、jstat等常用命令:

查看堆内存各区域的使用率以及GC情况

jstat -gcutil -h20 pid 1000

查看堆内存中的存活对象,并按空间排序

jmap -histo pid | head -n20

dump堆内存文件

jmap -dump:format=b,file=heap pid

可视化的堆内存分析工具:JVisualVM、MAT等

Dump文件分析

dump下来的文件大约1.8g,用jvisualvm查看,发现用char[]类型的数据占用了41%内存,同时另外一个com.alibaba.druid.stat.JdbcSqlStat类型的数据占用了35%的内存,也就是说整个堆中几乎全是这两类数据。如下图:

查看char[]类型数据,发现几乎全是sql语句。

JVM调优建议

  • (单服务器单应用)最大堆的设置建议在物理内存的1/2 到 2/3 之间

  • survivor和伊甸园的最优比例为1:8。年轻代=eden+2survivor

  • 年轻代和老年代的最优比例为1:2。

类加载机制

类从被加载到虚拟机内存开始到卸载出内存,整个生命周期包括以下七个阶段,其中加载,验证,准备,初始化,卸载这5个阶段的顺序是确定的。

加载

验证:确保Class文件中的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

准备:正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配

解析 :将符号引用转换成直接引用的过程

初始化:初始化阶段才真正开始执行类中定义的java程序代码(字节码)

什么时候加载

遇到new(使用new关键字创建实例对象的时候)、getstatic(读取一个静态变量,被final修饰,已在编译把结果放入常量池的静态变量除外)、putstatic(设置一个静态变量,被final修饰,已在编译把结果放入常量池的静态变量除外)或invokestatic(调用一个类的静态方法)这四条字节码指令的时候,如果类没有进行初始化,则需要先触发其初始化。

使用java.lang.reflect方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发器初始化

初始化一个类时,发现其父类没有进行初始化,则需要先触发父类的初始化。

虚拟机启动的时候,用户需要指定一个执行的主类(包含main方法的类),虚拟机会先对初始化这个主类

当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法的句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。(使用场景未知)

热部署

devtools

问:redis缓存会考虑到什么因素

雪崩、穿透和击穿

缓存雪崩

对于系统 A,假设每天高峰期每秒 5000 个请求,本来缓存在高峰期可以扛住每秒 4000 个请求,但是缓存机器意外发生了全盘宕机。缓存挂了,此时 1 秒
5000 个请求全部落数据库,数据库必然扛不住,它会报一下警,然后就挂了。此时,如果没有采用什么特别的方案来处理这个故障,DBA
很着急,重启数据库,但是数据库立马又被新的流量给打死了。

这就是缓存雪崩。

缓存雪崩的事前事中事后的解决方案如下:

  • 事前:Redis 高可用,主从+哨兵,Redis cluster,避免全盘崩溃。

  • 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。

  • 事后:Redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。

用户发送一个请求,系统 A 收到请求后,先查本地 ehcache 缓存,如果没查到再查 Redis。如果 ehcache 和 Redis
都没有,再查数据库,将数据库中的结果,写入 ehcache 和 Redis 中。

限流组件,可以设置每秒的请求,有多少能通过组件,剩余的未通过的请求,怎么办?走降级!可以返回一些默认的值,或者友情提示,或者空值。

好处:

  • 数据库绝对不会死,限流组件确保了每秒只有多少个请求能通过。

  • 只要数据库不死,就是说,对用户来说,2/5 的请求都是可以被处理的。

  • 只要有 2/5 的请求可以被处理,就意味着你的系统没死,对用户来说,可能就是点击几次刷不出来页面,但是多点几次,就可以刷出来了。

缓存穿透

对于系统 A,假设一秒 5000 个请求,结果其中 4000 个请求是黑客发出的恶意攻击。

黑客发出的那 4000 个攻击,缓存中查不到,每次你去数据库里查,也查不到。

举个栗子。数据库 id 是从 1 开始的,结果黑客发过来的请求 id
全部都是负数。这样的话,缓存中不会有,请求每次都“视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。

concurrency/images/redis-caching-penetration.png>

解决方式很简单,每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,比如 set -999 UNKNOWN
。然后设置一个过期时间,这样的话,下次有相同的 key 来访问的时候,在缓存失效之前,都可以直接从缓存中取数据。

缓存击穿

缓存击穿,就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key
在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。

不同场景下的解决方式可如下:

  • 若缓存的数据是基本不会发生更新的,则可尝试将该热点数据设置为永不过期。

  • 若缓存的数据更新不频繁,且缓存刷新的整个流程耗时较少的情况下,则可以采用基于 Redis、zookeeper 等分布式中间件的分布式互斥锁,或者本地互斥锁以保证仅少量的请求能请求数据库并重新构建缓存,其余线程则在锁释放后能访问到新缓存。

  • 若缓存的数据更新频繁或者在缓存刷新的流程耗时较长的情况下,可以利用定时线程在缓存过期前主动地重新构建缓存或者延后缓存的过期时间,以保证所有的请求能一直访问到对应的缓存。

问:数据一致性的实现原理

final不可变

作用于类、方法、成员变量、局部变量。初始化完成后的不可变对象,其它线程可见。常量不会改变不会因为其它线程产生影响。Final修饰的引用类型的地址不变,同时需要保证引用类型各个成员和操作的线程安全问题。因为引用类型成员可能是可变的。

synchronized同步

作用域代码块、方法上。通过线程互斥,同一时间的同样操作只允许一个线程操作。通过字节码指令实现。

Volatile

  1. volatile 修饰的变量的变化保证对其它线程立即可见。


volatile变量的写,先发生于读。每次使用volatile修饰的变量个线程都会刷新保证变量一致性。但同步之前各线程可能仍有操作。如:各个根据volatile变量初始值分别进行一些列操作,然后再同步写赋值。每个线程的操作有先后,当一个最早的线程给线程赋值时,其它线程同步。但这时其它线程可能根据初始值做了改变,同步的结果导致其它线程工作结果丢失。

根据volatile的语意使用条件:运算结果不依赖变量的当前值。

  1. volatile禁止指令重排优化。

这个语意导致写操作会慢一些。因为读操作跟这个没关系。

并发包概述

java.util.concurrent 包含许多线程安全、测试良好、高性能的并发构建块。不客气地说,创建java.util.concurrent
的目的就是要实现 Collection
框架对数据结构所执行的并发操作。通过提供一组可靠的、高性能并发构建块,开发人员可以提高并发类的线程安全、可伸缩性、性能、可读性和可靠性。

此包包含locks,concurrent,atomic 三个包。

Atomic:原子数据的构建。

Locks:基本的锁的实现,最重要的AQS框架和lockSupport

Concurrent:构建的一些高级的工具,如线程池,并发队列等。

其中都用到了CAS(compare-and-swap)操作。CAS
是一种低级别的、细粒度的技术,它允许多个线程更新一个内存位置,同时能够检测其他线程的冲突并进行恢复。它是许多高性能并发算法的基础。在 JDK 5.0
之前,Java 语言中用于协调线程之间的访问的惟一原语是同步,同步是更重量级和粗粒度的。公开 CAS 可以开发高度可伸缩的并发 Java 类。这些更改主要由
JDK 库类使用,而不是由开发人员使用。

CAS操作都封装在java
不公开的类库中,sun.misc.Unsafe。此类包含了对原子操作的封装,具体用本地代码实现。本地的C代码直接利用到了硬件上的原子操作。

问:redis怎么保证不被击穿

永久有效,分布式部署,

若缓存的数据更新频繁或者在缓存刷新的流程耗时较长的情况下,可以利用定时线程在缓存过期前主动地重新构建缓存或者延后缓存的过期时间,以保证所有的请求能一直访问到对应的缓存。

问:mysql怎么实现他的一致性,涉及到哪些锁,是怎么实现的

上一篇文章 MySQL-
InnoDB行锁
中,提到过一致性锁定读和一致性非锁定读,这篇文章会详细分析一下在事务中时,具体是如何实现一致性的。

一致性读原理

start
transaction和begin语句,并不是立即开启一个事务,事务是在第一条读语句执行时才建立的。如果需要立即开启事务,可以使用这个语句:start
transaction with comsistent snapshot。

每一个事务都有一个唯一的事务id,在mysql系统中,这个事务id是唯一且递增的。每一条数据库记录也有一个版本号,这个版本号记录了修改记录的事务id,如图:

最新的版本是V4,修改它的事务id为25,依次往前为V3,事务id17,一直到V1,事务id为10。
数据库中并不是真的有这些V1~V4的物理实体,是根据当前最新版本号和undolog往前计算出来每一个版本的。另外,数据库记录中除了保存修改它的事务id以外,还会记录这条修改是否已经提交。

在事务建立的一瞬间,当前事务会生成一个数组,保存了当前时刻系统中所有的活跃事务id(未提交事务),按照从小到大顺序排列,其中最小的id为低水位,最大的id为高水位。

那么在读操作和更新操作的时候,具体是如何使用这个版本号的呢?

我们知道,读分为一致性锁定读和一致性非锁定读;更新操作,其实可以拆解为两步,一步是一致性锁定读,一步是更新。我们只需要分析
一致性锁定读和一致性非锁定读就可以了。

  • 如果是一致性非锁定读,能读到的是低水位下的最近一个事务更新后的记录。

  • 如果是一致性锁定读,如果当前记录被锁定,需要等待锁释放;如果没被锁定,能读到 最新一个已提交记录 或者 当前事务版本号对记录的修改。

问:乐观锁和悲观锁是怎么理解的

悲观锁假设最坏的情况,即每次拿数据时都认为别人会修改,所以每次拿数据的时候都会上锁,这样别人拿着笔数据的时候就会阻塞,直到他拿到锁。关系型数据库里有这种锁表的机制,
都是在做操作之前先上锁。Java中synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁总是假设最好的情况,拿数据的时候不会上锁,但是在更新数据的时候会判断数据在此期间是不是更改了,比如使用版本号机制和CAS机制,适用于多读的场景,再多写的场景时反而会降低性能。

问:分布式锁用的多吗

一, 基于数据库实现分布式锁

1、悲观锁

利用select … where … for update 排他锁

注意: 其他附加功能与实现一基本一致,这里需要注意的是“where name=lock
”,name字段必须要走索引,否则会锁表。有些情况下,比如表不大,mysql优化器会不走这个索引,导致锁表问题。

2、乐观锁

所谓乐观锁与前边最大区别在于基于CAS思想,是不具有互斥性,不会产生锁等待而消耗资源,操作过程中认为不存在并发冲突,只有update
version失败后才能觉察到。我们的抢购、秒杀就是用了这种实现以防止超卖。 通过增加递增的版本号字段实现乐观锁

二, 基于缓存(Redis等)实现分布式锁

1、使用命令介绍:

(1)SETNX

SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。

(2)expire

expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。

(3)delete

delete key:删除key

在使用Redis实现分布式锁的时候,主要就会使用到这三个命令。

2、实现思想:

(1)获取锁的时候,使用setnx加锁,并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。

(2)获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。

(3)释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放。

基于zookeeper

问:线上死锁排查

JVM调优工具

Jconsole,jProfile,VisualVM

Jconsole : jdk自带,功能简单,但是可以在系统有一定负荷的情况下使用。对垃圾回收算法有很详细的跟踪。详细说明参考这里

JProfiler:商业软件,需要付费。功能强大。详细说明参考这里

VisualVM:JDK自带,功能强大,与JProfiler类似。推荐。

生产内存溢出,通过jprofiler对dump文件进行分析

一.Java VisualVM 概述

对于使用命令行远程监控jvm 太麻烦 。 在jdk1.6 中 Oracle 提供了一个新的可视化的。 JVM 监控工具 Java VisualVM
。jvisualvm.exe 在JDK 的 bin 目录下。

双击启动 Java VisualVM 后可以看到窗口左侧 “应用程序 ”栏中有“ 本地 ”、“远程 ” 、“快照 ”三个项目。

“本地 ”下显示的是在 localhost 运行的 Java 程序的资源占用情况,如果本地有 Java 程序在运行的话启动 Java VisualVM
即可看到相应的程序名,点击程序名打开相应的资源监控菜单,以图形的形式列出程序所占用的 CPU 、 Heap 、 PermGen 、类、线程的 统计信息。

“远程” 项下列出的远程主机上的 Java 程序的资源占用情况,但需要在远程主机上运行 jstatd 守护程序

VisualVM分为 3 类, 本地 它会自动侦测到,并显示出来

双击Local 下的任一节点,看到右边的变化 ,你可以监控 CPU ,内存,类,线程等运行状况,实时监控服务器性能。

如何调优 VisualVM

观察内存释放情况、集合类检查、对象树

上面这些调优工具都提供了强大的功能,但是总的来说一般分为以下几类功能

堆信息查看

可查看堆空间大小分配(年轻代、年老代、持久代分配)

提供即时的垃圾回收功能

垃圾监控(长时间监控回收情况)

查看堆内类、对象信息查看:数量、类型等

有了堆信息查看方面的功能,我们一般可以顺利解决以下问题:

–年老代年轻代大小划分是否合理

–内存泄漏

–垃圾回收算法设置是否合理

线程监控

Dump线程详细信息:查看线程内部运行情况

死锁检查

CPU热点 :检查系统哪些方法占用的大量CPU时间

内存热点 :检查哪些对象在系统中数量最大(一定时间内存活对象和销毁对象一起统计)

问:equals和hashcode问题

笔试: 判断数组是否相等工具类/三数和为3问题

排序+双指针:

①如果数组为空或数组元素小于3,返回数组;

②排序,

③遍历排序后的数组,如果nums[i] > 0,即后面不可能有三个数加起来为0,跳出循环,直接返回结果;

④判断数组元素与上一个元素是否相同,如果相同,跳过,避免出现重复的解;

⑤左右指针left、right,left = i + 1; right = nums.length - 1;当left < right,执行以下循环:

⑥当sum = nums[i] + nums[left] + nums[right] ==
0,判断左右指针left、right分别与其下个位置是否重复,去除重复解,并将left、right同时向下个位置移动;

⑦当sum = nums[i] + nums[left] + nums[right] >
0,说明nums[right]太大了,因此将右指针right向下个位置移动(左移);

⑧当sum = nums[i] + nums[left] + nums[right] <
0,说明nums[left]太小了,因此将左指针left向下个位置移动(右移)。

问:hashmap内存及结构,什么时候触发扩容,index位置怎么算,怎么做的hash

HashMap是基于底层叫Entry[]数组实现的一种哈希表

数组,链表,红黑树。 达到阈值时触发扩容一倍,数组扩容阈值,即:HashMap数组总容量 * 负载因子

当hashmap中的元素个数超过数组大小loadFactor时,就会进行数组扩容,loadFactor的默认值为0.75,也就是说,默认情况下,数组大小为16,那么当hashmap中元素个数超过160.75=12的时候,就把数组的大小扩展为2*16=32,即扩大一倍,然后重新计算每个元素在数组中的位置,而这是一个非常消耗性能的操作,所以如果我们已经预知hashmap中元素的个数,那么预设元素的个数能够有效的提高hashmap的性能。

https://zhuanlan.zhihu.com/p/74349470

hashCode:HashMap的Key对象里面的方法hashCode产生的值

hash:通过hashCode的值,通过一定算法产生的hash值

index:通过hash值计算产生的,HashMap底层数组Entry[]的偏移值

那计算index的方法是什么呢?

Entry[]数组的长度在初始化的时候会被指定,假定这个值为length。

那index的值就从 0 ~ length-1。所以index需要尽可能的平衡,也就是分布均匀,不能某些位置上存储特别多的数据,某些位置上又特别少。

1.3 index的计算方法

1.3.1 取模运算

hash值为int,index需要映射到0 ~ length -1,最直观的使用取模运算,也就是:

1.3.2 位运算(为什么长度一定是2的n次方)

因为:一个数num除以2n,相当于右移n位,那么移出去的那些数自然就是余数了,举个例子158除以8:

问:hashcode和equals是干什么的,一定需要重写吗

equals()作用:用于判断其他对象是否与该对象相同;

很显然,在Object类原生代码中比较的是引用地址,但是需要提醒的一点是在String、Math、Integer、Double等封装类中都对equals()进行了不同程度的重写以满足其不同需要

显然,在String类中的equals()比较的不再是引用对象的地址而是内容,在Java8种基本数据类型中equals()比较的都是内容,其实就是数值。

HashCode()作用:给不同对象返回不同的hash code值,相当于识别码;

使用HashCode()时应当符合以下三点:

  1. 在一个Java应用的执行期间,如果一个对象提供给equals做比较的信息没有被修改的话,该对象无论调用多少次hashCode(),必须始终返回同一个integer;

  2. 如果两个对象根据equals(Object)方法是相等的,那么调用二者各自的hashCode()必须产生同一个integer结果;

  3. 调用equals(java.lang.Object)方法结果不相等的两个对象,调用二者各自hashCode()不一定不相同,可能相同,可能不同。

在集合查找时,使用hashcode无疑能大大降低对象比较次数,提高查找效率!

Java对象的eqauls方法和hashCode方法是这样规定的:

1、相等(相同)的对象必须具有相等的哈希码(或者散列码)。

2、如果两个对象的hashCode相同,它们并不一定相同

1 什么情况需要重写

1 自定义类 需要判断对象在业务逻辑上是否相等,需要重写hashCode和equals。

如果不被重写(原生)的hashCode和equals是什么样的?

不被重写(原生)的hashCode值是根据内存地址换算出来的一个值。

不被重写(原生)的equals方法是严格判断一个对象是否相等的方法(object1 == object2)。

为什么需要重写equals和hashCode方法?

SQL执行顺序

(1)from

(3) join

(2) on

(4) where

(5)group by(开始使用select中的别名,后面的语句中都可以使用)

(6) avg,sum…

(7)having

(8) select

(9) distinct

(10) order by

问:mysql慢sql怎么排查,什么原因,用什么工具

mysql排查线上数据库问题,经常会用到 show processlist和show full processlist这两条命令

processlist命令的输出结果显示了有哪些线程在运行,不仅可以查看当前所有的连接数,还可以查看当前的连接状态帮助识别出有问题的查询语句等。

如果是root帐号,能看到所有用户的当前连接。如果是其他普通帐号,则只能看到自己占用的连接。showprocesslist只能列出当前100条。如果想全部列出,可以使用SHOW
FULL PROCESSLIST命令

索引

  • 索引是存储引擎用于快速找到记录的一种数据结构

  • B-Tree,适用于全键值,键值范围或键最左前缀:(A,B,C): A, AB, ABC,B,C,BC

  • 哪些列建议创建索引:WHERE, JOIN , GROUP BY, ORDER BY等语句使用的列

  • 如何选择索引列的顺序:

    1. 经常被使用到的列优先

    2. 选择性高的列优先:选择性=distinct(col)/count(col)

    3. 宽度小的列优先:宽度 = 列的数据类型

慢查询

原因

  1. 未使用索引

  2. 索引不优

  3. 服务器配置不佳

  4. 死锁

命令

看版本

mysql -V 客户端版本 select version 服务器版本

explain 执行计划,慢查询分析神器

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vIAa4z45-1681014581806)(【精】各大厂问题汇总_files/Image [29].png)]

  • type

    • const,system: 最多匹配一个行,使用主键或者unique进行索引

    • eq_ref: 返回一行数据,通常在联接时出现,使用主键或者unique索引(内表索引连接类型)

    • ref: 使用key的最左前缀,且key不是主键或unique键

    • range: 索引范围扫描,对索引的扫面开始于某一点,返回匹配的行

    • index:以索引的顺序进行全表扫描,优点是不用排序,缺点是还要全表扫描

    • all: 全表扫描 no no no

  • extra

    • using index : 索引覆盖,只用到索引,可以避免访问表

    • using where: 在存储引擎检索行后再做过滤

    • using temporary:使用临时表,通常在使用GROUP BY,ORDER BY 时出现(严禁)

    • using filesort: 到非索引顺序的额外排序,当order by col未使到索引时发生(严禁)

  • possible_keys: 显示查询可能使用的索引

  • key:优化器决定采用哪个索引来优化对该表的访问

  • rows:MySQL估算的为了找到所需行要检索的数,优化选择key的参考 (不是结果集的行数)

  • key_len: 使用的索引左前缀的长度(字节数),亦可理解为使用了索引中哪些字段

    • 定长字段,int占4个字节、date占3个字节、timestamp占4个字节,char(n)占n个字节

    • NULL的字段:需要加1个字节,因此建议尽亮设计为NOT NULL

    • 变长字段varchar(n),则需要 (n * 编码字符所占字节数 + 2 、)个字节,如utf8编码的, 个字符 占 3个字节,则 度为 n * 3 + 2

  • 强制使用索引: USE INDEX (建议)或 FORCE_INDEX (强制)

show 命令

  • show status

    • 查看select语句的执行数 show global status like ‘Com_select’;

    • 查看慢查询的个数 show global status like ‘Slow_queries’;

    • 表扫描情况 show global status like ‘Handler_read%’; Handler_read_rnd_next / com_select > 4000 需要考虑优化索引

  • show variables

    • 查看慢查询相关的配置 show variables like ‘long_query_time’;

    • 将慢查询时间线设置为2s set global long_query_time=2;

    • 查看InnoDB缓存 show variables like ‘innodb_buffer_pool_size’;

    • 查看InnoDB缓存的使用状态 show status like ‘Innodb_buffer_pool_%’; 缓存命中率=(1-Innodb_buffer_pool_reads/ Innodb_buffer_pool_read_requests) * 100%;缓存率=(Innodb_buffer_pool_pages_data/ Innodb_buffer_pool_pages_total) * 100%

    • SHOW PROFILES;该命令可以trace在整个执行过程中各资源消耗情况(会话级)

    • SHOW PROCESSLIST; 查看当前有哪些线程正在运行,并且处在何种状态

    • SHOW ENGINE INNODB STATUS; 可用于分析死锁,但需要super权限

问:mysql行级锁和表级锁

三、行级锁定

行级锁定不是MySQL自己实现的锁定方式,而是由其他存储引擎自己所实现的,如广为大家所知的InnoDB存储引擎,以及MySQL的分布式存储引擎NDBCluster等都是实现了行级锁定。考虑到行级锁定由各个存储引擎自行实现,而且具体实现也各有差别,而InnoDB是目前事务型存储引擎中使用最为广泛的存储引擎,所以这里我们就主要分析一下InnoDB的锁定特性。

1.InnoDB锁定模式及实现机制

总的来说,InnoDB的锁定机制和Oracle数据库有不少相似之处。InnoDB的行级锁定同样分为两种类型,共享锁和排他锁,而在锁定机制的实现过程中为了让行级锁定和表级锁定共存,InnoDB也同样使用了意向锁(表级锁定)的概念,也就有了意向共享锁和意向排他锁这两种。

MySQL的表级锁有两种模式:表共享读锁(Table Read Lock)和表独占写锁(Table Write Lock)。锁模式的兼容性:

对MyISAM表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求;

对MyISAM表的写操作,则会阻塞其他用户对同一表的读和写操作;

什么时候使用表锁

对于InnoDB表,在绝大部分情况下都应该使用行级锁,因为事务和行锁往往是我们之所以选择InnoDB表的理由。但在个另特殊事务中,也可以考虑使用表级锁。

  • 第一种情况是:事务需要更新大部分或全部数据,表又比较大,如果使用默认的行锁,不仅这个事务执行效率低,而且可能造成其他事务长时间锁等待和锁冲突,这种情况下可以考虑使用表锁来提高该事务的执行速度。

  • 第二种情况是:事务涉及多个表,比较复杂,很可能引起死锁,造成大量事务回滚。这种情况也可以考虑一次性锁定事务涉及的表,从而避免死锁、减少数据库因事务回滚带来的开销。

在InnoDB下 ,使用表锁要注意以下两点。

(1)使用LOCK TALBES虽然可以给InnoDB加表级锁,但必须说明的是,表锁不是由InnoDB存储引擎层管理的,而是由其上一层MySQL
Server负责的,仅当autocommit=0、innodb_table_lock=1(默认设置)时,InnoDB层才能知道MySQL加的表锁,MySQL
Server才能感知InnoDB加的行锁,这种情况下,InnoDB才能自动识别涉及表级锁的死锁;否则,InnoDB将无法自动检测并处理这种死锁。

(2)在用LOCAK TABLES对InnoDB锁时要注意,要将AUTOCOMMIT设为0,否则MySQL不会给表加锁;事务结束前,不要用UNLOCAK
TABLES释放表锁,因为UNLOCK TABLES会隐含地提交事务;COMMIT或ROLLBACK产不能释放用LOCAK
TABLES加的表级锁,必须用UNLOCK TABLES释放表锁,正确的方式见如下语句。

问:如果发现没有用到索引应该怎么办,命中部分索引该怎么办

1、 如果条件中有 or ,即使其中有条件带索引也不会使用(这也是为什么尽量少用or的原因)

注意:要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引

如果出现OR的一个条件没有索引时,建议使用 union ,拼接多个查询语句

2.、 like查询是以%开头 ,索引不会命中

只有一种情况下,只查询索引列,才会用到索引,但是这种情况下跟是否使用%没有关系的,因为查询索引列的时候本身就用到了索引

  1. 如果列类型是字符串,那一定要在条件中将数据使用引号引用起来,否则不使用索引

  2. 没有查询条件,或者查询条件没有建立索引

  3. 查询条件中,在索引列上使用函数(+, - ,*,/), 这种情况下需建立函数索引

  4. 采用 not in, not exist

  5. B-tree 索引 is null 不会走, is not null 会走

  6. 链表时,如果关联字段的的编码不同,也不会走索引。如一个表时 utf8,另外一个表是utf8mb4

问:为什么加索引会快

索引数据少,加载遍历快, 有排序,二叉树查找时间快

问:构建索引有什么要注意的地方吗

索引:提高数据查询的速度,一般创建索引的列为很少改动的列。

1、例句: select * from user where userId = ‘123456789’;

userId很少改变且是唯一值,可以对userID创建索引;

2、例句:select * from user where name = ‘李明’ and age = 26;

如果name和age是不能为null的列,那么我们可以对这两列单独建立索引,但是如果经常使用这两列进行查询,就要创建复合索引(name, age);

3、例句:select * from user where name = ‘李明’ and age = 26 and height = 175;

此时我们既想满足上面的条件又想满足下面的查询语句的条件的索引,可以会创建两个索引(name,age)和(name,age,height),但是通常可以创建复合索引(name,age,height)即可,因为复合索引满足最佳左前缀原则,会创建以下三个索引(name,age,height)、(name,age)、(name);

4、例句:select * from user where number like ‘2017%’;

学号是按年月日+四位数字,此时想查学号的年份是2017的学生,为了提高查询速度,如果对number建立索引使用,模糊查询(%123%)索引会失效,此时可以使用短索引,也就是只对number的前缀,前四位创建索引即可。

5、select * from user where grade = ‘二年级’ order by number;

查询二年级的学生并对其进行排序,此时如果对grade和number分别创建了索引,采用上面的方式进行查询时,索引是没有效果的,因为where子句中已经有了索引,此时order
by中的索引不会生效,导致整个sql语句的索引都不会生效。

6、select * from user where year(time) < 2017 and grade not in (‘二年级’,‘三年级’);

此时无论是建立在time还是grade的索引都会失效,索引的列是不能进行计算的,采用计算该列的索引就会失效,使用not in
语句也会失效,不过可以使用not exists代替。

通常索引的列可以进行<,>,=,<=,>=,between,in操作。

如果sql语句中包含or,<>,like ‘%123’,所在列不能null,尽量不要在重复值过多的列创建索引

7、in和not in在主键上的唯一索引是可以使用的,但是在其他不是唯一索引列索引是失效的。

问:一致性hash实现原理

https://www.jianshu.com/p/528ce5cd7e8f

笔试:猴子分桃

问:图数据库的查询原理

问:abtest原理

问:为什么要做熔断

当我们的系统的访问量突然剧增,大量的请求涌入过来,最典型的就是秒杀业务了,我们可能会知道会有一波高峰,这时候该如何处理?

而且现在很多情况我们还需要调用第三方接口例如支付等,因此我们还得考虑如果第三方那边出问题了,返回异常的慢,我们系统该如何处理。

常见的处理方式有三种:降级、熔断、限流。

降级

降级也就是服务降级,当我们的服务器压力剧增为了 保证核心功能的可用性 ,而 选择性的降低一些功能的可用性,或者直接关闭该功能 。这就是典型的
丢车保帅 了。
就比如贴吧类型的网站,当服务器吃不消的时候,可以选择把发帖功能关闭,注册功能关闭,改密码,改头像这些都关了,为了确保登录和浏览帖子这种核心的功能。

一般而言都会建立一个独立的降级系统,可以灵活且批量的配置服务器的降级功能。当然也有用代码自动降级的,例如接口超时降级、失败重试多次降级等。具体失败几次,超时设置多久,由你们的业务等其他因素决定。开个小会,定个值,扔线上去看看情况。根据情况再调优。

熔断

降级一般而言指的是我们自身的系统出现了故障而降级。而熔断一般是指依赖的外部接口出现故障的情况断绝和外部接口的关系。

例如你的A服务里面的一个功能依赖B服务,这时候B服务出问题了,返回的很慢。这种情况可能会因为这么一个功能而拖慢了A服务里面的所有功能,因此我们这时候就需要熔断!即当发现A要调用这B时就直接返回错误(或者返回其他默认值啊啥的),就不去请求B了。我这还是举了两个服务的调用,有些那真的是一环扣一环,出问题不熔断,那真的是会雪崩。

当然也有人认为熔断不就是降级的一种的,我觉得你非要说熔断也属于一种降级我也没法反驳,但是它们本质上的突出点和想表达的意思还是有一些不同的。

那什么时候熔断合适呢?也就是到达哪个阈值进行熔断,5分钟内50%的请求都超过1秒?还是啥?参考降级。

限流

上面说的两个算是请求过来我们都受理了,这个限流就更狠了,直接跟请求说对不起再见!也就是系统规定了多少承受能力,只允许这么些请求能过来,其他的请求就说再见了。

一般限制的指标有: 请求总量或某段时间内请求总量

请求总量:比如秒杀的,秒杀100份产品,我就放5000名进来,超过的直接拒绝请求了。

某段时间内请求总量:比如规定了每秒请求的峰值是1W,这一秒内多的请求直接拒绝了。咱们下一秒再见。

问:hashcode和equals是不是必须同时改,为什么?

是,为了满足不同需求下的equals比较,我们在必要时需要重写equals()。为了不影响Object中hashCode()对equals()结果,我们必须也要同时重写hashCode()来保证我们达到想要的结果

问:猴子偷桃为什么要用synchronized?

问:怎么启动一个线程?怎么把他停下来?标志位这变量声明需要有什么注意的吗?

启动一个线程用start()方法,

在java中有以下3种方法可以终止正在运行的线程:

  1. 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止。

  2. 使用stop方法强行终止,但是不推荐这个方法,因为stop和suspend及resume一样都是过期作废的方法。

  3. 使用interrupt方法中断线程。

如果我们在面试中被问到“你知不知道如何正确停止线程”这样的问题,我想你一定可以完美地回答了,首先,从原理上讲应该用 interrupt
来请求中断,而不是强制停止,因为这样可以避免数据错乱,也可以让线程有时间结束收尾工作。

如果我们是子方法的编写者,遇到了 interruptedException,应该如何处理呢?

我们可以把异常声明在方法中,以便顶层方法可以感知捕获到异常,或者也可以在 catch
中再次声明中断,这样下次循环也可以感知中断,所以要想正确停止线程就要求我们停止方,被停止方,子方法的编写者相互配合,大家都按照一定的规范来编写代码,就可以正确地停止线程了。

最后我们再来看下有哪些方法是不够好的,比如说已经被舍弃的 stop()、suspend() 和
resume(),它们由于有很大的安全风险比如死锁风险而被舍弃,而 volatile
这种方法在某些特殊的情况下,比如线程被长时间阻塞的情况,就无法及时感受中断,所以 volatile 是不够全面的停止线程的方法

1. 停止不了的线程

interrupt()方法的使用效果并不像for+break语句那样,马上就停止循环。调用interrupt方法是在当前线程中打了一个停止标志,并不是真的停止线程。

由于stop()方法以及在JDK中被标明为“过期/作废”的方法,显然它在功能上具有缺陷,所以不建议在程序张使用stop()方法。

Java wait 和 sleep 的区别

一、区别

  1. sleep 来自 Thread 类,和 wait 来自 Object 类

  2. sleep 方法没有释放锁,而wait方法释放了锁,使得其他线程可以使用同步控制块或方法

  3. wait,notify和 notifyAll 只能在同步控制方法或者同步控制块里面使用,而 sleep 可以在任何地方使用

  4. sleep 必须捕获异常,而 wait , notify 和 notifyAll 不需要捕获异常

二、概念

sleep

:sleep 方法属于 Thread
类中方法,表示让一个线程进入睡眠状态,等待一定的时间之后,自动醒来进入到可运行状态,不会马上进入运行状态,因为线程调度机制恢复线程的运行也需要时间,一个线程对象调用了
sleep方法之后,并不会释放他所持有的所有对象锁,所以也就不会影响其他进程对象的运行。但在 sleep 的过程中过程中有可能被其他对象调用它的
interrupt() ,产生 InterruptedException 异常,如果你的程序不捕获这个异常,线程就会异常终止,进入 TERMINATED
状态,如果你的程序捕获了这个异常,那么程序就会继续执行catch语句块(可能还有 finally 语句块)以及以后的代码。

wait

: wait 属于 Object 的成员方法,一旦一个对象调用了wait方法,必须要采用 notify() 和 notifyAll()
方法唤醒该进程;如果线程拥有某个或某些对象的同步锁,那么在调用了 wait() 后,这个线程就会释放它持有的所有同步资源,而不限于这个被调用了 wait()
方法的对象。 wait() 方法也同样会在 wait 的过程中有可能被其他对象调用 interrupt() 方法而产生 。

标签: java

问:什么叫线程可见性?为什么猴子偷桃不用volatile?

jmm的每个线程有独立的工作内存,他们的工作方式是从主内存将变量读取到自己的工作内存,然后在工作内存中进行逻辑或者自述运算再把变量写回到主内存中。正常情况下各线程的工作内存之间是相互隔离的、不可见的。

内存屏障

可见性和有序性是基于各种内存屏障(禁止指令重排序)来实现的,先来看下有哪些内存屏障类型,以及可以解决那些因重排序引起的有序性问题。对有序性不太理解的同学可以先看下前这篇文章面试官:你知道happens-
before规则吗

本文的重点来了,对于volatile修复的变量,在它的读/写操作前后都会加上不同的内存屏障。

在每个volatile写操作的前面插入一个StoreStore屏障,后面插入一个StoreLoad屏障,保证volatile写与之前的写操作指令不会重排序,写完数据之后立即执行flush处理器缓存操作将所有写操作刷到内存,对所有处理器可见。

在每个volatilie读操作的前面插入一LoadLoad屏障,保证在该变量读操作时,如果其他处理修改过,必须从其他处理器高速缓存(或者主内存)加载到自己本地高速缓存,保证读取到的值是最新的。然后在该变量读操作后面插入一个LoadStore屏障,禁止volatile读操作与后面任意读写操作重排序。

问:可见性和同步有什么区别?每次用的时候都会去主存读吗?工作内存在哪?

(1)同步性:同步性就是一个事物要么一起成功,要么一起失败,可谓是有福同享有难同当,就像A有10000去银行转5000给身无分文的B,这个事物有两个操作,1.A扣去5000
即剩下10000-5000=5000;2.B增加0+5000=5000;一起成功的情况就是1和2都成功执行,一起失败的情况是,如果A扣除的时候机器刚好坏了,那么事物就应该回滚,不然A就只剩5000,B还是0;这里没写例子

(2).可见性:就是一个线程的操作可以及时被其他线程更新到;要做到线程可见性必须满足两个条件,这里要谈到JMM(java memory
model),1.线程的工作内存副本的共享变量要更新到主存,2.其他线程要及时读取主存的共享变量

工作内存是私有区域,所以工作内存可以对应着JVM运行时数据区的线程私有部分,包括虚拟机栈,本地方法栈,程序计数器。

主内存是共享区域,所以主内存可以对应着JVM运行时数据区的线程共享部分,包括堆和方法区

Hystrix的使用

@SpringBootApplication @EnableEurekaClient//声明为eureka 客户端 @EnableFeignClients
//启动Feign @EnableCircuitBreaker //启动Hystrix

/使用feign package com.futurecloud.hystrix.feignClient; import
com.futurecloud.hystrix.bean.User; import
org.springframework.cloud.openfeign.FeignClient; import
org.springframework.web.bind.annotation.GetMapping; import
org.springframework.web.bind.annotation.PathVariable; import
org.springframework.web.bind.annotation.RequestMapping; import
org.springframework.web.bind.annotation.RequestMethod; @FeignClient(name =
“futurecloud-user”) //通过注解指定依赖服务 public interface FeignClientInterfaces {
@RequestMapping(value = “/user/{id}”,method = RequestMethod.GET) User
getUserById(@PathVariable(“id”) Long id); @GetMapping(“/getUser/{id}”) User
getUser(@PathVariable(“id”) Long id); @RequestMapping(value =
“/find/user/{id}”,method = RequestMethod.POST) User
findUserById(@PathVariable(“id”) Long id); } /使用熔断器 package
com.futurecloud.hystrix.controller; import com.futurecloud.hystrix.bean.User;
import com.futurecloud.hystrix.feignClient.FeignClientInterfaces; import
com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import
feign.Param; import feign.RequestLine; import
org.springframework.beans.factory.annotation.Autowired; import
org.springframework.web.bind.annotation.*; import java.util.Date;
@RestController public class FuturecloudHystrixController { @Autowired private
FeignClientInterfaces feignClientInterfaces; @RequestMapping(value =
“/user/{id}”,method = RequestMethod.GET) public User
getUserById(@PathVariable(“id”) Long id){ User user =
feignClientInterfaces.getUserById(id); return user; }
@GetMapping(“/getUser/{id}”) public User getUser(@PathVariable(“id”) Long id){
User user = feignClientInterfaces.getUser(id); return user; } /** *
使用Hystrix熔断器,当调用findUserById失败后,调用forbackFindUserById方法 * @param id * @return
/ @HystrixCommand(fallbackMethod = “forbackFindUserById”)
@RequestMapping(value = “/find/user/{id}”,method = RequestMethod.GET) public
User findUserById(@PathVariable(“id”) Long id){ User user =
feignClientInterfaces.findUserById(id); int a = 4/0; return user; } public
User forbackFindUserById(Long id){ User user = new User(); user.setId(-400L);
user.setUsername(“hystrix-fallback”); user.setMail(“hystrix-
[email protected]”); user.setPhone(“13838384381”); user.setCreateDate(new
Date()); return user; } } /hystrix的两种降级方式:信号量,线程池 @HystrixCommand( groupKey =
“timeline-group-rcmd”, fallbackMethod = “callback”, commandProperties = {
@HystrixProperty(name=“execution.isolation.strategy”, value=“SEMAPHORE”), //
信号量隔离,因为业务方法用了ThreadLocal @HystrixProperty(name =
“execution.isolation.thread.timeoutInMilliseconds”, value = “100”), //超时时间
@HystrixProperty(name = “circuitBreaker.requestVolumeThreshold”,
value=“50”),//触发熔断最小请求数量 @HystrixProperty(name =
“circuitBreaker.errorThresholdPercentage”, value=“30”),//触发熔断的错误占比阈值
@HystrixProperty(name = “circuitBreaker.sleepWindowInMilliseconds”,
value=“3000”),//熔断器回复时间 @HystrixProperty(name =
“execution.isolation.semaphore.maxConcurrentRequests”, value=“300”),// 单机最高并发
@HystrixProperty(name = “fallback.isolation.semaphore.maxConcurrentRequests”,
value=“100”)// fallback单机最高并发 } ) public void call() { count++; if(0 == 0) {
try { Thread.sleep(1000); } catch (InterruptedException e) { } } } /
* *
callback 方法 */ public void callback() { System.out.println(“callback”); }
使用这个注解@HystrixCommand来定义Hystrix的熔断器,查看HystrixCommand源码,这个注解有10个属性,我们使用fallbackMethod属性,来指定接口调用失败后执行的方法。HystrixCommand源码如下
package com.netflix.hystrix.contrib.javanica.annotation; import
java.lang.annotation.Documented; import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import
java.lang.annotation.Target; @Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface
HystrixCommand { String groupKey() default “”; String commandKey() default “”;
String threadPoolKey() default “”; String fallbackMethod() default “”;
HystrixProperty[] commandProperties() default {}; HystrixProperty[]
threadPoolProperties() default {}; Class[]
ignoreExceptions() default {}; ObservableExecutionMode
observableExecutionMode() default ObservableExecutionMode.EAGER;
HystrixException[] raiseHystrixExceptions() default {}; String
defaultFallback() default “”; }

线程隔离(Isolation)

  • execution.isolation.strategy : 配置请求隔离的方式,有 threadPool(线程池,默认)和 semaphore(信号量)两种,信号量方式高效但配置不灵活,我们一般采用 Java 里常用的线程池方式。

  • execution.timeout.enabled :是否给方法执行设置超时,默认为 true。

  • execution.isolation.thread.timeoutInMilliseconds :方法执行超时时间,默认值是 1000,即 1秒,此值根据业务场景配置。

  • execution.isolation.thread.interruptOnTimeoutexecution.isolation.thread.interruptOnCancel :是否在方法执行超时/被取消时中断方法。需要注意在 JVM 中我们无法强制中断一个线程,如果 Hystrix 方法里没有处理中断信号的逻辑,那么中断会被忽略。

  • execution.isolation.semaphore.maxConcurrentRequests:默认值是 10,此配置项要在 execution.isolation.strategy 配置为 semaphore 时才会生效,它指定了一个 Hystrix 方法使用信号量隔离时的最大并发数,超过此并发数的请求会被拒绝。信号量隔离的配置就这么一个,也是前文说信号量隔离配置不灵活的原因。

熔断器(Circuit Breaker)

  • circuitBreaker.enabled :是否启用熔断器,默认为 true;

  • circuitBreaker.forceOpencircuitBreaker.forceClosed :是否强制启用/关闭熔断器,强制启用关闭都想不到什么应用的场景,保持默认值,不配置即可。

  • circuitBreaker.requestVolumeThreshold :启用熔断器功能窗口时间内的最小请求数。试想如果没有这么一个限制,我们配置了 50% 的请求失败会打开熔断器,窗口时间内只有 3 条请求,恰巧两条都失败了,那么熔断器就被打开了,5s 内的请求都被快速失败。此配置项的值需要根据接口的 QPS 进行计算,值太小会有误打开熔断器的可能,值太大超出了时间窗口内的总请求数,则熔断永远也不会被触发。建议设置为 QPS * 窗口秒数 * 60%。

  • circuitBreaker.errorThresholdPercentage :在通过滑动窗口获取到当前时间段内 Hystrix 方法执行的失败率后,就需要根据此配置来判断是否要将熔断器打开了。 此配置项默认值是 50,即窗口时间内超过 50% 的请求失败后会打开熔断器将后续请求快速失败。

  • circuitBreaker.sleepWindowInMilliseconds :熔断器打开后,所有的请求都会快速失败,但何时服务恢复正常就是下一个要面对的问题。熔断器打开时,Hystrix 会在经过一段时间后就放行一条请求,如果这条请求执行成功了,说明此时服务很可能已经恢复了正常,那么会将熔断器关闭,如果此请求执行失败,则认为服务依然不可用,熔断器继续保持打开状态。此配置项指定了熔断器打开后经过多长时间允许一次请求尝试执行,默认值是 5000。

缓存怎么和数据库一致

队列里数据更新了怎么处理

设计模式 懒加载

只不过相对于主从模式在主节点宕机导致不可写的情况下,多了一个竞选机制——从所有的从节点竞选出新的主节点。竞选机制的实现,是依赖于在系统中启动一个sentinel进程。
3.2 sentinel特点 监控:它会监听主服务器和从服务器之间是否在正常工作。 通知:它能够通过API告诉系统管理员或者程序,集群中某个实例出了问题。
故障转移:它在主节点出了问题的情况下,会在所有的从节点中竞选出一个节点,并将其作为新的主节点。
提供主服务器地址:它还能够向使用者提供当前主节点的地址。这在故障转移后,使用者不用做任何修改就可以知道当前主节点地址。
sentinel,也可以集群,部署多个哨兵,sentinel可以通过发布与订阅来自动发现Redis集群上的其它sentinel。sentinel在发现其它sentinel进程后,会将其放入一个列表中,这个列表存储了所有已被发现的sentinel。
集群中的所有sentinel不会并发着去对同一个主节点进行故障转移。故障转移只会从第一个sentinel开始,当第一个故障转移失败后,才会尝试下一个。当选择一个从节点作为新的主节点后,故障转移即成功了(而不会等到所有的从节点配置了新的主节点后)。
当竞选出新的主节点后,被选为新的主节点的从节点的配置信息会被sentinel改写为旧的主节点的配置信息。完成改写后,再将新主节点的配置广播给所有的从节点。如果原master重连,并不会成为master。

3.3 Sentinel的工作方式:
1):每个Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他Sentinel实例发送一个PING命令

2):如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds
选项所指定的值,则这个实例会被 Sentinel 标记为主观下线。

3):如果一个Master被标记为主观下线,则正在监视这个Master的所有 Sentinel 要以每秒一次的频率确认Master的确进入了主观下线状态。

4):当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认Master的确进入了主观下线状态,
则Master会被标记为客观下线 5):在一般情况下, 每个 Sentinel 会以每10 秒一次的频率向它已知的所有Master,Slave发送 INFO
命令 6):当Master被 Sentinel 标记为客观下线时,Sentinel 向下线的 Master 的所有 Slave 发送 INFO
命令的频率会从 10 秒一次改为每秒一次 7):若没有足够数量的 Sentinel 同意Master 已经下线, Master 的客观下线状态就会被移除。
若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。 四 Sentinel 搭建 4.1
修改配置文件 每一台服务器都需要修改 [root@master ~]# vi redis-5.0.7/sentinel.conf
17行/protected-mode no #关闭保护模式 26行/daemonize yes #指定sentinel为后台启动 36行/logfile
“/var/log/sentinel.log” #指定日志存放路径 65行/dir “/var/lib/redis/6379” #指定数据库存放路径
84行/sentinel monitor mymaster 20.0.0.10 6379 2
#至少几个哨兵检测到主服务器故障了,才会进行故障迁移,全部指向masterIP 113行/sentinel down-after-milliseconds
mymaster 30000 #判定服务器down掉的时间周期,默认30000毫秒(30秒) 146行/sentinel failover-timeout
mymaster 180000 #故障节的的最大超时时间为180000(180秒) 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
4.2开启哨兵模式 先开启master,在开启slave [root@localhost opt]# redis-sentinel
redis-5.0.7/sentinel.conf & 1 1  4.3查看哨兵信息 [root@localhost opt]# redis-cli -h
20.0.0.10 -p 26379 info Sentinel 1 1    4.4 模拟故障 查看进程号,并杀死master上的进程号
[root@localhost opt]# ps -ef | grep redis 1 1  4.5 查看信息验证结果  [root@localhost
opt]# redis-cli -p 26379 INFO Sentinel 1 1 

红黑树

mybaties 只有DAO没有实现类

原理:动态代理

总结:

1.mybatis注解方式通过没有实现类的dao接口进行数据库操作的原理,一句话概括,就是jdk proxy,就是jdk代理

2.spring+mybatis注解方式,也是没有实现类的,但是spring会默认返回MapperFactoryBean对象作为实现类的替换,但是这个只是被spring使用的,mybatis本身还是通过jdk代理来运行的。

mybatis中提供了基于注解的配置和基于xml的配置,除此之外在mybatis中还可以使用dao实现类来进行操作

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tzBIsOeb-1681014581807)(【精】各大厂问题汇总_files/Image [30].png)]

所以,咱们在spring中使用

  1. @autowired

  2. private UserMapper userMapper;

来注入对象的时候,其实是经历了 cglib --> mapperfactorybean --> sqlsessiontemplate -->
mapperproxy --> 原生dao接口 的包装过程,才获取的。

所以咱们在使用spring来调用没有实现类的mybatis的dao接口的时候,并不是像看起来那么简单,而是经过多层代理包装的一个代理对象,对方法的执行也跳转到mybatis框架中的mappermethod中了。

2021年5月11日 分界线----------------------------

tomcat原理

功能组件结构

Tomcat 的核心功能有两个,分别是负责接收和反馈外部请求的连接器 Connector,和负责处理请求的容器
Container。其中连接器和容器相辅相成,一起构成了基本的 web 服务 Service。每个 Tomcat 服务器可以管理多个 Service。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6GelJBdA-1681014581807)(【精】各大厂问题汇总_files/Image [31].png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FNGN5bql-1681014581808)(【精】各大厂问题汇总_files/Image [32].png)]

连接器核心功能

一、监听网络端口,接收和响应网络请求。

二、网络字节流处理。将收到的网络字节流转换成 Tomcat Request 再转成标准的 ServletRequest 给容器,同时将容器传来的
ServletResponse 转成 Tomcat Response 再转成网络字节流。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8JyVHt5a-1681014581808)(【精】各大厂问题汇总_files/Image [33].png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NoQFxbZQ-1681014581809)(【精】各大厂问题汇总_files/Image [34].png)]

容器结构分析

每个 Service 会包含一个容器。容器由一个引擎可以管理多个虚拟主机。每个虚拟主机可以管理多个 Web 应用。每个 Web 应用会有多个 Servlet
包装器。Engine、Host、Context 和 Wrapper,四个容器之间属于父子关系。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G8V54pAi-1681014581809)(【精】各大厂问题汇总_files/Image [35].png)][外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RxZnpBNt-1681014581809)(【精】各大厂问题汇总_files/Image [36].png)]

容器请求处理

容器的请求处理过程就是在 Engine、Host、Context 和 Wrapper 这四个容器之间层层调用,最后在 Servlet
中执行对应的业务逻辑。各容器都会有一个通道 Pipeline,每个通道上都会有一个 Basic Valve(如StandardEngineValve),
类似一个闸门用来处理 Request 和 Response 。其流程图如下。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NPOCVlp1-1681014581810)(【精】各大厂问题汇总_files/Image [37].png)]

Tomcat 请求处理流程

上面的知识点已经零零碎碎地介绍了一个 Tomcat 是如何处理一个请求。简单理解就是连接器的处理流程 + 容器的处理流程 = Tomcat
处理流程。哈!那么问题来了,Tomcat 是如何通过请求路径找到对应的虚拟站点?是如何找到对应的 Servlet 呢?

映射器功能介绍

这里需要引入一个上面没有介绍的组件
Mapper。顾名思义,其作用是提供请求路径的路由映射。根据请求URL地址匹配是由哪个容器来处理。其中每个容器都会它自己对应的Mapper,如
MappedHost。不知道大家有没有回忆起被 Mapper class not found 支配的恐惧。在以前,每写一个完整的功能,都需要在
web.xml 配置映射规则,当文件越来越庞大的时候,各个问题随着也会出现

HTTP请求流程

打开 tomcat/conf 目录下的 server.xml 文件来分析一个http://localhost:8080/docs/api 请求。

第一步:连接器监听的端口是8080。由于请求的端口和监听的端口一致,连接器接受了该请求。

第二步:因为引擎的默认虚拟主机是 localhost,并且虚拟主机的目录是webapps。所以请求找到了 tomcat/webapps 目录。

第三步:解析的 docs 是 web 程序的应用名,也就是 context。此时请求继续从 webapps 目录下找 docs
目录。有的时候我们也会把应用名省略。

第四步:解析的 api 是具体的业务逻辑地址。此时需要从 docs/WEB-INF/web.xml 中找映射关系,最后调用具体的函数。

SpringBoot 如何启动内嵌的 Tomcat

SpringBoot 一键启动服务的功能,让有很多刚入社会的朋友都忘记 Tomcat 是啥。随着硬件的性能越来越高,普通中小项目都可以直接用内置
Tomcat 启动。但是有些大一点的项目可能会用到 Tomcat 集群和调优,内置的 Tomcat 就不一定能满足需求了。

代码从 main 方法开始,执行 run 方法启动项目。

SpringApplication.run

从 run 方法点进去,找到刷新应用上下文的方法。

this.prepareContext(context, environment, listeners, applicationArguments,
printedBanner);this.refreshContext(context);this.afterRefresh(context,
applicationArguments);

从 refreshContext 方法点进去,找 refresh 方法。并一层层往上找其父类的方法。

this.refresh(context);

在 AbstractApplicationContext 类的 refresh 方法中,有一行调用子容器刷新的逻辑。

this.postProcessBeanFactory(beanFactory);this.invokeBeanFactoryPostProcessors(beanFactory);this.registerBeanPostProcessors(beanFactory);this.initMessageSource();this.initApplicationEventMulticaster();this.onRefresh();this.registerListeners();this.finishBeanFactoryInitialization(beanFactory);this.finishRefresh();

从 onRefresh 方法点进去,找到 ServletWebServerApplicationContext 的实现方法。在这里终于看到了希望。

从 createWebServer 方法点进去,找到从工厂类中获取 WebServer的代码。

从 getWebServer 方法点进去,找到 TomcatServletWebServerFactory 的实现方法,与之对应的还有 Jetty 和
Undertow。这里配置了基本的连接器、引擎、虚拟站点等配置。

根据接口的 QPS 进行计算,值太小会有误打开熔断器的可能,值太大超出了时间窗口内的总请求数,则熔断永远也不会被触发。建议设置为 QPS * 窗口秒数 * 60%。

  • circuitBreaker.errorThresholdPercentage :在通过滑动窗口获取到当前时间段内 Hystrix 方法执行的失败率后,就需要根据此配置来判断是否要将熔断器打开了。 此配置项默认值是 50,即窗口时间内超过 50% 的请求失败后会打开熔断器将后续请求快速失败。

  • circuitBreaker.sleepWindowInMilliseconds :熔断器打开后,所有的请求都会快速失败,但何时服务恢复正常就是下一个要面对的问题。熔断器打开时,Hystrix 会在经过一段时间后就放行一条请求,如果这条请求执行成功了,说明此时服务很可能已经恢复了正常,那么会将熔断器关闭,如果此请求执行失败,则认为服务依然不可用,熔断器继续保持打开状态。此配置项指定了熔断器打开后经过多长时间允许一次请求尝试执行,默认值是 5000。

缓存怎么和数据库一致

队列里数据更新了怎么处理

设计模式 懒加载

只不过相对于主从模式在主节点宕机导致不可写的情况下,多了一个竞选机制——从所有的从节点竞选出新的主节点。竞选机制的实现,是依赖于在系统中启动一个sentinel进程。
3.2 sentinel特点 监控:它会监听主服务器和从服务器之间是否在正常工作。 通知:它能够通过API告诉系统管理员或者程序,集群中某个实例出了问题。
故障转移:它在主节点出了问题的情况下,会在所有的从节点中竞选出一个节点,并将其作为新的主节点。
提供主服务器地址:它还能够向使用者提供当前主节点的地址。这在故障转移后,使用者不用做任何修改就可以知道当前主节点地址。
sentinel,也可以集群,部署多个哨兵,sentinel可以通过发布与订阅来自动发现Redis集群上的其它sentinel。sentinel在发现其它sentinel进程后,会将其放入一个列表中,这个列表存储了所有已被发现的sentinel。
集群中的所有sentinel不会并发着去对同一个主节点进行故障转移。故障转移只会从第一个sentinel开始,当第一个故障转移失败后,才会尝试下一个。当选择一个从节点作为新的主节点后,故障转移即成功了(而不会等到所有的从节点配置了新的主节点后)。
当竞选出新的主节点后,被选为新的主节点的从节点的配置信息会被sentinel改写为旧的主节点的配置信息。完成改写后,再将新主节点的配置广播给所有的从节点。如果原master重连,并不会成为master。

3.3 Sentinel的工作方式:
1):每个Sentinel以每秒钟一次的频率向它所知的Master,Slave以及其他Sentinel实例发送一个PING命令

2):如果一个实例(instance)距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds
选项所指定的值,则这个实例会被 Sentinel 标记为主观下线。

3):如果一个Master被标记为主观下线,则正在监视这个Master的所有 Sentinel 要以每秒一次的频率确认Master的确进入了主观下线状态。

4):当有足够数量的 Sentinel(大于等于配置文件指定的值)在指定的时间范围内确认Master的确进入了主观下线状态,
则Master会被标记为客观下线 5):在一般情况下, 每个 Sentinel 会以每10 秒一次的频率向它已知的所有Master,Slave发送 INFO
命令 6):当Master被 Sentinel 标记为客观下线时,Sentinel 向下线的 Master 的所有 Slave 发送 INFO
命令的频率会从 10 秒一次改为每秒一次 7):若没有足够数量的 Sentinel 同意Master 已经下线, Master 的客观下线状态就会被移除。
若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。 四 Sentinel 搭建 4.1
修改配置文件 每一台服务器都需要修改 [root@master ~]# vi redis-5.0.7/sentinel.conf
17行/protected-mode no #关闭保护模式 26行/daemonize yes #指定sentinel为后台启动 36行/logfile
“/var/log/sentinel.log” #指定日志存放路径 65行/dir “/var/lib/redis/6379” #指定数据库存放路径
84行/sentinel monitor mymaster 20.0.0.10 6379 2
#至少几个哨兵检测到主服务器故障了,才会进行故障迁移,全部指向masterIP 113行/sentinel down-after-milliseconds
mymaster 30000 #判定服务器down掉的时间周期,默认30000毫秒(30秒) 146行/sentinel failover-timeout
mymaster 180000 #故障节的的最大超时时间为180000(180秒) 1 2 3 4 5 6 7 8 1 2 3 4 5 6 7 8
4.2开启哨兵模式 先开启master,在开启slave [root@localhost opt]# redis-sentinel
redis-5.0.7/sentinel.conf & 1 1  4.3查看哨兵信息 [root@localhost opt]# redis-cli -h
20.0.0.10 -p 26379 info Sentinel 1 1    4.4 模拟故障 查看进程号,并杀死master上的进程号
[root@localhost opt]# ps -ef | grep redis 1 1  4.5 查看信息验证结果  [root@localhost
opt]# redis-cli -p 26379 INFO Sentinel 1 1 

红黑树

mybaties 只有DAO没有实现类

原理:动态代理

总结:

1.mybatis注解方式通过没有实现类的dao接口进行数据库操作的原理,一句话概括,就是jdk proxy,就是jdk代理

2.spring+mybatis注解方式,也是没有实现类的,但是spring会默认返回MapperFactoryBean对象作为实现类的替换,但是这个只是被spring使用的,mybatis本身还是通过jdk代理来运行的。

mybatis中提供了基于注解的配置和基于xml的配置,除此之外在mybatis中还可以使用dao实现类来进行操作

[外链图片转存中…(img-tzBIsOeb-1681014581807)]

所以,咱们在spring中使用

  1. @autowired

  2. private UserMapper userMapper;

来注入对象的时候,其实是经历了 cglib --> mapperfactorybean --> sqlsessiontemplate -->
mapperproxy --> 原生dao接口 的包装过程,才获取的。

所以咱们在使用spring来调用没有实现类的mybatis的dao接口的时候,并不是像看起来那么简单,而是经过多层代理包装的一个代理对象,对方法的执行也跳转到mybatis框架中的mappermethod中了。

2021年5月11日 分界线----------------------------

tomcat原理

功能组件结构

Tomcat 的核心功能有两个,分别是负责接收和反馈外部请求的连接器 Connector,和负责处理请求的容器
Container。其中连接器和容器相辅相成,一起构成了基本的 web 服务 Service。每个 Tomcat 服务器可以管理多个 Service。

[外链图片转存中…(img-6GelJBdA-1681014581807)][外链图片转存中…(img-FNGN5bql-1681014581808)]

连接器核心功能

一、监听网络端口,接收和响应网络请求。

二、网络字节流处理。将收到的网络字节流转换成 Tomcat Request 再转成标准的 ServletRequest 给容器,同时将容器传来的
ServletResponse 转成 Tomcat Response 再转成网络字节流。

[外链图片转存中…(img-8JyVHt5a-1681014581808)][外链图片转存中…(img-NoQFxbZQ-1681014581809)]

容器结构分析

每个 Service 会包含一个容器。容器由一个引擎可以管理多个虚拟主机。每个虚拟主机可以管理多个 Web 应用。每个 Web 应用会有多个 Servlet
包装器。Engine、Host、Context 和 Wrapper,四个容器之间属于父子关系。

[外链图片转存中…(img-G8V54pAi-1681014581809)][外链图片转存中…(img-RxZnpBNt-1681014581809)]

容器请求处理

容器的请求处理过程就是在 Engine、Host、Context 和 Wrapper 这四个容器之间层层调用,最后在 Servlet
中执行对应的业务逻辑。各容器都会有一个通道 Pipeline,每个通道上都会有一个 Basic Valve(如StandardEngineValve),
类似一个闸门用来处理 Request 和 Response 。其流程图如下。

[外链图片转存中…(img-NPOCVlp1-1681014581810)]

Tomcat 请求处理流程

上面的知识点已经零零碎碎地介绍了一个 Tomcat 是如何处理一个请求。简单理解就是连接器的处理流程 + 容器的处理流程 = Tomcat
处理流程。哈!那么问题来了,Tomcat 是如何通过请求路径找到对应的虚拟站点?是如何找到对应的 Servlet 呢?

映射器功能介绍

这里需要引入一个上面没有介绍的组件
Mapper。顾名思义,其作用是提供请求路径的路由映射。根据请求URL地址匹配是由哪个容器来处理。其中每个容器都会它自己对应的Mapper,如
MappedHost。不知道大家有没有回忆起被 Mapper class not found 支配的恐惧。在以前,每写一个完整的功能,都需要在
web.xml 配置映射规则,当文件越来越庞大的时候,各个问题随着也会出现

HTTP请求流程

打开 tomcat/conf 目录下的 server.xml 文件来分析一个http://localhost:8080/docs/api 请求。

第一步:连接器监听的端口是8080。由于请求的端口和监听的端口一致,连接器接受了该请求。

第二步:因为引擎的默认虚拟主机是 localhost,并且虚拟主机的目录是webapps。所以请求找到了 tomcat/webapps 目录。

第三步:解析的 docs 是 web 程序的应用名,也就是 context。此时请求继续从 webapps 目录下找 docs
目录。有的时候我们也会把应用名省略。

第四步:解析的 api 是具体的业务逻辑地址。此时需要从 docs/WEB-INF/web.xml 中找映射关系,最后调用具体的函数。

SpringBoot 如何启动内嵌的 Tomcat

SpringBoot 一键启动服务的功能,让有很多刚入社会的朋友都忘记 Tomcat 是啥。随着硬件的性能越来越高,普通中小项目都可以直接用内置
Tomcat 启动。但是有些大一点的项目可能会用到 Tomcat 集群和调优,内置的 Tomcat 就不一定能满足需求了。

代码从 main 方法开始,执行 run 方法启动项目。

SpringApplication.run

从 run 方法点进去,找到刷新应用上下文的方法。

this.prepareContext(context, environment, listeners, applicationArguments,
printedBanner);this.refreshContext(context);this.afterRefresh(context,
applicationArguments);

从 refreshContext 方法点进去,找 refresh 方法。并一层层往上找其父类的方法。

this.refresh(context);

在 AbstractApplicationContext 类的 refresh 方法中,有一行调用子容器刷新的逻辑。

this.postProcessBeanFactory(beanFactory);this.invokeBeanFactoryPostProcessors(beanFactory);this.registerBeanPostProcessors(beanFactory);this.initMessageSource();this.initApplicationEventMulticaster();this.onRefresh();this.registerListeners();this.finishBeanFactoryInitialization(beanFactory);this.finishRefresh();

从 onRefresh 方法点进去,找到 ServletWebServerApplicationContext 的实现方法。在这里终于看到了希望。

从 createWebServer 方法点进去,找到从工厂类中获取 WebServer的代码。

从 getWebServer 方法点进去,找到 TomcatServletWebServerFactory 的实现方法,与之对应的还有 Jetty 和
Undertow。这里配置了基本的连接器、引擎、虚拟站点等配置。

你可能感兴趣的:(JAVA,java,面试)