面试总结最终版
JAVA基础
面向过程和面向对象的区别
面向过程
面向过程是以过程为中心的编程思想,是把解决问题的步骤通过函数去实现。
优点:性能比面向对象高,因为类调用时需要实例化,开销比较大。
面向对象
面向对象是将事物高度抽象化为对象,不同对象进行调用、组合去解决问题。
优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态的特点,可以设计出低耦合的系统,使系统更加灵活、易于维护
java语言有哪些特点
JVM、JDK、JRE的解答
JVM
JRE
java运行时环境,主要由JVM+基础类库组成(IO、日期、线程、集合类等)
JDk
Java开发工具包,主要由JVM+基础类库+编译工具,编译器(javac)和工具(javadoc和jdb)。它能创建和编译程序
Java和C++的区别
- 都是面向对象语言,都支持封装、继承和多态
- java不提供指针来直接访问内存,程序内存更加安全
- java的类是单继承,C++支持多重继承,虽然java的类不可以多继承,但是接口可以多继承
- java有自动内存管理机制,不需要程序员手动释放无用内存
字符型常量和字符串常量的区别
- 形式上:字符型常量是单引号引起的一个字符,字符串常量是双引号引起的若干个字符
- 含义上:字符常量相当于一个整型值(ASCII值),可以参加表达式运算,字符串常量代表一个地址值(该字符串在内存中存放的位置)
- 占内存大小,字符常量只占2个字节,字符串常量占若干个字节(至少一个字符结束标志)
构造器Constructor是否可被override
父类的私有属性和构造方法并不能被继承,所以Constructor也就不能被override(重写)。但是可以overload(重载),所以你可以看到一个类中有多个构造函数的情况
重载和重写的区别
重载
发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同、方法返回值和访问修饰符可以不同,发生在编译时
重写
发生在父子类中,方法名、参数列表必须相同、返回值范围小于等于父类,抛出异常范围小于等于父类,访问修饰符范围大于父类,如果父类方法访问修饰符为private,则子类就不能重写该方法。
封装、继承、多态
封装
封装就是把一个对象的属性私有化,同时提供一些可以被外界访问属性的方法,如果属性不想被外界访问,我们也可以不提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
继承
是java对功能进行扩展的一种方式,通过继承创建的类被称为子类,被继承的类称为父类。
多态
就是同一操作,作用于不同的对象,可以有不同的解释,并且产生不同的执行结果。
在java中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)
String为什么是不可变的
因为String类中使用final关键字字符数组保存字符串,private final char value[],所以String对象是不可变的。
String、StringBuffer和StringBuilder的区别
可变性
String是不可变的,StringBuffer和StringBuilder都继承字AbstractStringBuilder,没有使用final关键字修饰,所以这两种对象是可变的。
线程安全性
String中的对象是不可变的,也就可以理解为常量,线程安全。
StringBuffer对方法加了同步锁,所以是线程安全的。
StringBuilder并没有对方法进行加同步锁,所以是线程不安全的。
性能
每次对String类型进行改变时,都会生成一个新的String对象,然后将地址指向新的String对象。
StringBuffer和StringBuilder每次都会对对象本身进行操作,而不会生成新的对象并改变对象的引用。
在相同的情况下,StringBuilder比StringBuffer仅能获得10%~15%左右的性能提升,但却要冒多线程不安全的风险。
总结
自动拆装箱
包装类
自动拆装箱
自动拆装箱原理
自动装箱都是通过包装类的valueOf()方法来实现的,自动拆箱都是通过包装类对象的xxxValue()来实现的。
那些地方会自动拆装箱
自动拆装箱带来的问题
- 包装对象的数值比较,不能简单的使用==,虽然-128到127之间的数字可以,但是这个范围之外还是需要使用equals比较
- 由于自动拆箱,如果包装类对象为null,那么自动拆箱是就有可能抛异常
- 如果一个for循环中有大量的拆装箱操作,会浪费很多资源
在一个静态方法内调用一个非静态成员为什么是非法的
由于静态方法可以不通过对象进行调用,因此在静态方法里,不能调用其他非静态变量,也不可以访问非静态变量成员。
接口和抽象类的区别
- 抽象类要被子类继承,接口要被子类实现
- 抽象类可以有构造方法,接口中不能有构造方法
- 抽象类中可以有普通成员变量,接口中没有普通成员变量,它的变量只能是公共的静态的常量
- 一个类可以实现多个接口,但是只能继承一个父类,这个父类可以是抽象类
- 接口只能做方法声明,抽象类中可以做方法声明,也可以做方法实现
- 抽象级别(从高到低):接口>抽象类>实现类
- 抽象类主要是用来抽象类别,接口主要是用来抽象方法功能
- 抽象类的关键字是abstract,接口关键字是interface
成员变量与局部变量的区别
- 从语法形式上,成员变量是属于类的,而局部变量是在方法中定义的变量或者是方法的参数,成员变量可以被public,private、static等修饰符所修饰,而局部变量不能被访问控制修饰符及static所修饰,但是成员变量和局部变量都能被final所修饰
- 成员变量是对象的一部分,存储在堆内存,局部变量在方法中,存在于栈内存
- 从生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失
- 成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值,而局部变量则不会自动赋值。
创建一个对象用什么运算符,对象实体与对象引用有何不同
什么是方法返回值?返回值的作用
方法的返回值是指我们获取到某个方法体中的代码执行后产生的结果。返回值的作用:接收出结果,使得它可以用于其他操作
一个类的构造方法的作用,若一个类没有声明构造方法,该程序能正常执行吗
可以执行,因为一个类即使没有声明构造方法也会有默认的不带参的构造方法。
静态方法和实例方法有何不同
- 使用静态方法时,可以使用类名.方法名的方式,也可以使用对象名.方法名的方式,而实例方法只有后面这种方法,也就是说调用静态方法可以无需创建对象
- 静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法,实例方法则无此限制
==与equals
==
它的作用是判断两个对象的地址是不是相等,即判断两个对象是不是同一个对象(基本数据类型= =比较的是值,引用数据类型= =比较的是内存地址)
equals()
- 情况1:类没有覆盖equals()方法。则通过equals()比较该类的两个对象时,等价于通过”==”比较这两个对象
- 情况2:类覆盖了equals()方法,一般,我们都覆盖equals方法来判断两个对象的内容相等,若它们的内容相等,则返回true
hashCode()与equals()
- 如果两个对象相等,则hashcode一定也是相同的
- 两个对象相等,对两个对象分别调用equals方法都返回true(a.equals(b)返回true)
- 两个对象有相同的hashcode值,它们也不一定是相等的
- 因此,equals方法被覆盖过,则hashcode方法也必须被覆盖
final关键字
- 作用于变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改,如果是引用类型的变量,则在对其初始化后便不能让其指向另一个对象
- 作用于方法,表示该该方法不能被重写
- 作用于类上,表示该类不能被继承
使用原因
java异常处理
所有异常都有一个共同的类:Throwable类,该类有两个重要的子类:Exception(异常)和Error(错误)
Error(错误)
是程序无法处理的错误,表示运行应用程序中较严重的问题,大多数错误与代码编写者执行的操作无关,而表示代码运行时JVM出现的问题
Exception(异常)
是程序本身可以处理的异常,Exception类有一个重要的子类RuntimeException,该异常由java虚拟机抛出
Throwable类常用方法
- public string getMessage()返回异常发生时的详细信息
- public stirng toString() 返回异常发生时的简要描述
- public String getLocalizedMessage() 返回异常对象的本地化信息
- public void printStackTrace()在控制台上打印Throwable对象封装的异常信息
异常处理
- try块:用于捕获异常。其后可以接零个或多个catch块,如果没有catch块,则必须跟一个finally块
- catch块:用于处理try捕获到的异常
- finally块:无论是否捕获到异常处理,finally块的语句都会被执行。
- 当try块或catch块中遇到return语句时,finally语句块将在方法返回之前被执行
-
以下四种情况,finally块不会被执行
- 在finally语句块发生了异常
- 在前面的代码中使用了System.exit()退出程序
- 程序所在的线程死亡
- 关闭CPU
java序列化中如果那些字段不想进行序列化,怎么办
transient关键字作用:阻止实例中那些用此关键字修饰的变量序列化,当对象被反序列化时,被transient修饰的变量值不会被持久化和恢复。transient只能修饰变量,不能修饰类和方法。
获取键盘输入常用的类两种方法
-
1
2
3Scanner input = new Scanner(System.in);
String s = input.nextLine();
input.close(); -
1
2BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
String s = input.readLine();
线程和锁
https://www.processon.com/mindmap/629c72761e08531a4012cfd7
线程基础知识
并行和并发的区别
线程和进程的区别
- 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
- 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
- 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)
Java中创建线程的方式
runnable和callable的区别
在实际开发中,如果需要拿到执行的结果,需要使用Callalbe接口创建线程,调用FutureTask.get()得到可以得到返回值,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞。
线程状态和状态之间的变化
在JDK中的Thread类中的枚举State里面定义了6中线程的状态分别是:新建、可运行、终结、阻塞、等待和有时限等待六种。
- 如果线程获取锁失败后,由 可运行 进入 Monitor 的阻塞队列 阻塞 ,只有当持锁线程释放锁时,会按照一定规则唤醒阻塞队列中的 阻塞 线程,唤醒后的线程进入 可运行 状态
- 如果线程获取锁成功后,但由于条件不满足,调用了 wait() 方法,此时从 可运行 状态进入释放锁 等待 状态,当其它持锁线程调用 notify() 或 notifyAll() 方法,会恢复为 可运行 状态
- 还有一种情况是调用 sleep(long) 方法也会从 可运行 状态进入 有时限等待 状态,不需要主动唤醒,超时时间到自然恢复为 可运行 状态
wait和sleep方法的区别
它们两个的相同点是都可以让当前线程暂时放弃 CPU 的使用权,进入阻塞状态。
-
线程执行 sleep(long) 会在等待相应毫秒后醒来,而 wait() 需要被 notify 唤醒,wait() 如果不唤醒就一直等下去
-
wait 方法的调用必须先获取 wait 对象的锁,而 sleep 则无此限制
新建T1、T2、T3三个线程,如何保证顺序执行
在多线程中有多种方法让线程按特定顺序执行,可以用线程类的 join ()方法在一个线程中启动另一个线程,另外一个线程完成该线程继续执行。
使用join方法,T3调用T2,T2调用T1,这样就能确保T1就会先完成而T3最后完成
start方法和run方法的区别
如何停止一个正在运行的线程
- 可以使用退出标志,使线程正常退出,也就是当run方法完成后线程终止,一般我们加一个标记
- 可以使用线程的stop方法强行终止,不过一般不推荐,这个方法已作废
- 可以使用线程的interrupt方法中断线程,内部其实也是使用中断标志来中断线程
notify()和notifyAll()有什么区别
线程中并发锁
synchronized关键字的底层原理
synchronized 因为需要依赖于JVM级别的Monitor ,相对性能也比较低。
具体说下Monitor
monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因
只有一个线程获取到的标志就是在monitor中设置成功了Owner,一个monitor中只能有一个Owner
synchronized 的锁升级
Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。
重量级锁
底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
轻量级锁
线程加锁的时间是错开的(也就是没有竞争),可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性
偏向性
synchronized它在高并发量的情况下,性能不高,在项目该如何控制使用锁呢?
ReentrantLock使用方式和底层原理
使用方式
ReentrantLock是一个可重入锁,调用 lock 方 法获取了锁之后,再次调用 lock,是不会再阻塞,内部直接增加重入次数 就行了,标识这个线程已经重复获取一把锁而不需要等待锁的释放。
ReentrantLock是属于juc包下的类,属于api层面的锁,跟synchronized一样,都是悲观锁。通过lock()用来获取锁,unlock()释放锁。
底层原理
主要利用 CAS+AQS队列 来实现。它支持公平锁和非公平锁,两者的实现类似
构造方法接受一个可选的公平参数( 默认非公平锁 ),当设置为true时,表示公平锁,否则为非公平锁。公平锁的效率往往没有非公平锁的效率高。
CAS
CAS的全称是: Compare And Swap(比较再交换);它体现的一种乐观锁的思想,在无锁状态下保证线程操作数据的原子性。
AQS
其实就一个jdk提供的类。AbstractQueuedSynchronizer,是阻塞式锁和相关的同步器工具的框架。
内部有一个属性 state 属性来表示资源的状态,默认state等于0,表示没有获取锁,state等于1的时候才标明获取到了锁。通过cas 机制设置 state 状态
在它的内部还提供了基于 FIFO 的等待队列,是一个双向列表,其中
synchronized和Lock有什么区别
-
语法层面
- synchronized 是关键字,源码在 jvm 中,用 c++ 语言实现,退出同步代码块锁会自动释放
- Lock 是接口,源码由 jdk 提供,用 java 语言实现,需要手动调用 unlock 方法释放锁
-
功能层面
- 二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
- Lock 提供了许多 synchronized 不具备的功能,例如获取等待状态、公平锁、可打断、可超时、多条件变量,同时Lock 可以实现不同的场景,如 ReentrantLock, ReentrantReadWriteLock
-
性能层面
- 在没有竞争时,synchronized 做了很多优化,如偏向锁、轻量级锁,性能不赖
- 在竞争激烈时,Lock 的实现通常会提供更好的性能
死锁产生的条件
这个时候t1线程和t2线程都在互相等待对方的锁,就产生了死锁
死锁诊断
volatile理解
volatile 是一个关键字,可以修饰类的成员变量、类的静态成员变量,主要有两个功能
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的,volatile关键字会强制将修改的值立即写入主存
- 禁止进行指令重排序,可以保证代码执行有序性。底层实现原理是,添加了一个 内存屏障 ,通过插入内存屏障禁止在内存屏障 前后 的指令执行重排序优化
线程池
线程池核心参数
拒绝策略
线程池种类
如何确定核心线程池
我们公司当时有一些规范,为了减少线程上下文的切换,要根据当时部署的服务器的CPU核数来决定。
线程池执行原理
- 首先判断线程池里的核心线程是否都在执行任务,如果不是则创建一个新的工作线程来执行任务
-
如果核心线程都在执行任务,则线程池判断工作队列是否已满
- 如果工作队列没有满,则将新提交的任务存储在这个工作队 列里
- 如果工作队列满了,则判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任 务。如果已经满了,则交给拒绝策略来处理这个任务。
为什么不建议使用Executors创建线程池
其实这个事情在阿里提供的最新开发手册《Java开发手册-嵩山版》中也提到了
主要原因是如果使用Executors创建线程池的话,它允许的请求队列默认长度是Integer.MAX_VALUE,这样的话,有可能导致堆积大量的请求,从而导致OOM(内存溢出)。
所以,我们一般推荐使用ThreadPoolExecutor来创建线程池,这样可以明确规定线程池的参数,避免资源的耗尽。
线程使用场景问题
如何控制某一个方法允许并发访问线程的数量
在jdk中提供了一个Semaphore[seməfɔːr]类(信号量),它提供了两个方法
如何保证Java程序在多线程的情况下执行安全
jdk中也提供了很多的类帮助我们解决多线程安全的问题,比如:
- JDK Atomic开头的原子类、synchronized、LOCK,可以解决原子性问题
- synchronized、volatile、LOCK,可以解决可见性问题
- Happens-Before 规则可以解决有序性问题
项目中哪里使用了多线程
es数据批量导入
- 获取当前表的总条数,固定分页条数,可以得出分页数
- 创建对应的倒计时锁,参数为分页数
- 遍历页数,每次都能得到对应limit开始和结束的下标,获取对应的数据,创建线程,在用线程池去执行
- 在创建线程的逻辑方法中,去插入数据和将CountDownLatch锁计数减1
- 在循环外调用CountDownLatch.await方法等待计数归零
数据汇总
异步调用
其他
ThreadLocal的理解
ThreadLocal的底层原理实现
- 在ThreadLocal内部维护了一个一个 ThreadLocalMap 类型的成员变量,用来存储资源对象
- 当我们调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线程的 ThreadLocalMap 集合中
- 当调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中查找关联的资源值
- 当调用 remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值
ThreadLocal会导致内存溢原因
是因为ThreadLocalMap 中的 key 被设计为弱引用,它是被动的被GC调用释放key,不过关键的是只有key可以得到内存释放,而value不会,因为value是一个强引用。
在使用ThreadLocal 时都把它作为静态变量(即强引用),因此无法被动依靠 GC 回收,建议主动的remove 释放 key,这样就能避免内存溢出。
IO
IO流分为几种
字符流、字节流
输入流、输出流
输入、输出,有一个参照物,参照物就是存储数据的介质。如果·是把对象读入到介质中,这就是输入。从介质中向外读数据,这就是输出、所以,输入流是把数据写入存储介质的,输出流是从存储介质中把数据读取出来
同步、异步
如果是同步,B在接到A的调用后,会立即执行要做的事。A的本次调用可以得到结果
如果是异步,B在接到A的调用后,不保证会立即执行要做的事,但是保证会去做,B在做好了之后会通知A。A的本次调用得不到结果,但是B执行完之后会通知A
阻塞、非阻塞
如果是非阻塞,A在发出调用后,不需要等待,可以去做自己的事情
同步,异步和阻塞。非阻塞之间的区别
IO模型 | |
---|---|
阻塞IO模型 | 同步阻塞 |
非阻塞IO模型 | 同步非阻塞 |
IO多路复用模型 | 同步阻塞 |
信号驱动IO模型 | 同步非阻塞 |
异步IO模型(AIO) | 异步非阻塞 |
一个例子读懂BIO、NIO、AIO
- 同步阻塞(blocking-io)简称BIO:线程发起IO请求,不管内核是否准备好IO操作,从发起请求,线程一直阻塞,直到操作完成
- 同步非阻塞(non-blocking-IO)简称NIO:线程发起IO请求,立即返回,内核在做好IO操作的准备之后,通过调用注册的回调函数通知线程做IO操作,线程开始阻塞,直到操作完成
- 异步非阻塞(asynchronous-non-blocking-io)简称AIO:线程发起IO请求,立即返回,内存做好IO操作的准备之后,做IO操作,直到操作完成或者是失败,通过调用注册的回调函数通知线程做IO操作完成或失败
- 小明去吃椰子鸡,就这样在那里排队,等了一个小时,然后才开始吃(BIO)
- 小红去吃椰子鸡,她一看要等很久,于是去逛商场,每次逛一下,就跑回来看看,是不是轮到她了,最后她既购物了,又吃上了椰子鸡(NIO)
- 小华,去吃椰子鸡,由于他是会员,所以店长说,你去商场随便逛逛,等下有位置,我立马打电话给你,于是不需要干巴巴的等着,也不用一会跑过来看看有没有等到,最后也吃上了椰子鸡(AIO)
五种IO模型
阻塞IO模型
假设程序的进程发起IO调用,但是内核的数据还没准备好,那么应用程序进程一直在阻塞等待,一直等到内核数据准备好了,从内核拷贝到用户空间,才返回成功提示,此次IO操作,称之为阻塞IO
非阻塞IO模型
如果内核数据还没准备好,可以先返回错误信息给用户进程,让它不需要等待,而是通过轮询的方式再来请求,这就是非阻塞IO。
- 应用进程向操作系统内核,发起recvfrom读取数据
- 操作系统内核数据没准备好,立即返回ewouldblock错误码
- 应用程序进程轮询调用,继续向操作系统内核发起recvfrom读取数据、
- 操作系统内核数据准备好了,从内核缓冲区拷贝到用户空间
- 完成调用,返回成功提示
非阻塞IO模型,简称NIO,相对于阻塞IO,虽然大幅提升性能,但是它依然存在性能问题,即频繁的轮询,导致频繁的系统调用,同样会消耗大量的cpu资源,可以考虑使用IO复用模型,去解决这个问题
IO多路复用模型
既然NIO无效的轮询会导致cpu资源消耗,我们等到内核数据准备好了,主动通知应用程序进程再去进行调用,不就好了吗
-
select
的IO多路复用模型,只需要发起一次询问就够了,大大优化了性能。但是几个缺点因为存在连接数限制,所以后来提出poll,与select相比,poll解决了连接数限制问题,但是还是一样需要通过遍历文件描述符来获取已就绪的socket
IO模型之信号驱动模型
IO模型之异步IO(AIO)
异步IO的优化思路很简单,只需要向内核发送一次请求,就可以完成数据状态询问和数据拷贝的所有操作,并且不用阻塞等待结果。
集合
Java常见的集合类
-
Collection单列集合
-
List
- ArrayList
- LinkedList
-
Set
- HashSet
- TreeSet
-
List
-
Map双列集合
- HashMap
- TreeMap
- 线程安全map:ConcurrentHashMap
List
Java的List是非常常用的数据类型。List是有序的Collection。Java List一共三个实现类:分别是ArrayList、Vector和LinkedList
ArrayList(动态数组)
底层实现
- 判断数组已使用长度+1之后是否能足够存下下一个数据
- 如果当前数组已使用长度+1后的值大于当前数组长度,则调用grow方法扩容(原来的1.5倍)
- 确保新增的数据有地方存储之后,则将新元素添加到对应的位置上
- 返回成功的布尔值
数组的的寻址公式
1 |
|
代入到寻址公式,获取下标为1的元素,首地址为10,存入的是int类型,占4个字节
1 |
|
总结
因为ArrayList底层结构是数组,数组可以通过索引根据寻址公式直接找到元素,时间复杂度为O(1),所以它的随机查询和遍历是很快的。
如何实现数组和List之间的转换
1 |
|
-
List 转数组,可以直接调用list中的toArray方法,需要给一个参数,指定数组的类型,需要指定数组的长度。
1
String[] array = list.toArray(new String[list.size()]);
-
用Arrays.asList转List后,如果修改了数组内容,list受影响吗
数组转List受影响,因为它的底层使用的Arrays类中的一个内部类ArrayList来构造集合,在这个集合的构造器中,把我们传入的这个集合进行了包装而已,最终指向的都是同一个内存地址。
-
List用toArray转数组后,如果修了List内容,数组受影响吗
List转数组不受影响,当调用了toArray以后,在底层时它是进行了数组的拷贝,更原来的元素就没啥关系了,所以即使list修改了以后,数组也不受影响。
Vector(数组实现、线程同步)
LinkedList(双向链表)
ArrayList和LinkedList
Set
HashSet(Hash表)
HashSet通过hashCode值来确定元素在内存中的位置。一个hashCode位置上可以存放多个元素。
TreeSet(二叉树)
- TreeSet()是使用二叉树原理对新add()的对象按照指定顺序排序(升序、降序),每增加一个对象都会进行排序,将对象插入的二叉树指定的位置。
- Integer和String对象都可以进行默认的TreeSet排序,而自定义类的对象时不可以的,自己定义的类必须实现Comparable接口,并且覆盖相应的compareTo()函数,才可以正常使用。
- 在覆写compare()函数时,要返回相应的值才能使TreeSet按照一定的规则来排序
- 比较此对象与指定对象的顺序。如果该对象小于、等于或大于指定对象,则分别返回负整数、零和正整数。
LikedHashSet(HashSet+LinkedHashMap)
Map
HashMap(数组+链表+红黑树)
JAVA7实现HashMap结构
- capacity:当前数组容量,始终保持2^n,可以扩容,扩容后数组大小为当前的两倍。
- loadFactor:负载因子,默认为0.75
- threshold:扩容的阈值,等于capacity*loadFactor
JAVA8实现
java8对HashMap进行了一些修改,最大的不同就是利用了红黑树,所以其由数组+链表+红黑树组成。
HashMap的put方法具体流程
- 判断键值对数组table是否为空或为null,否则执行resize()进行扩容(初始化)
- 根据键值key计算hash值得到数组索引
- 判断table[i] == null,条件成立,直接新建节点添加
-
如果table[i] == null不成立
- 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value
- 判断table[i]是否为treeNode,即table[i]是否为红黑树,如果是红黑树,则直接在树中插入键值对
- 遍历rable[i],链表的尾部插入数据,然后判断链表长度是否大于8,如果大于8的话把链表转换为红黑树,在红黑树中执行插入操作,遍历过程中,若发现key已经存在则直接覆盖value
- 插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold(数组长度*0.75),如果超过,进行扩容
hashMap扩容机制
- 在添加元素或初始化的时候需要调用resize方法进行扩容,第一次添加数据初始化数组长度为16,以后每次每次扩容都是达到了扩容阈值(数组长度 * 0.75)
- 每次扩容的时候,都是扩容之前容量的2倍;
- 扩容之后,会新创建一个数组,需要把老数组中的数据挪动到新的数组中
- 没有hash冲突的节点,则直接使用 e.hash & (newCap - 1) 计算新数组的索引位置
- 如果是红黑树,走红黑树的添加
- 如果是链表,则需要遍历链表,可能需要拆分链表,判断(e.hash & oldCap)是否为0,该元素的位置要么停留在原始位置,要么移动到原始位置+增加的数组大小这个位置上
HashMap的寻址算法
这个哈希方法首先计算出key的hashCode值,然后通过这个hash值右移16位后的二进制进行按位 异或运算 得到最后的hash值。
为何HashMap的数组长度一定是2的次幂?
hashmap在1.7情况下的多线程死循环问题
在数组进行扩容的时候,因为链表是 头插法 ,在进行数据迁移的过程中,有可能导致死循环
线程一: 读取 到当前的hashmap数据,数据中一个链表,在准备扩容时,线程二介入
线程二也读取hashmap,直接进行扩容。因为是头插法,链表的顺序会进行颠倒过来。比如原来的顺序是AB,扩容后的顺序是BA,线程二执行结束。
线程一先将A移入新的链表,再将B插入到链头,由于另外一个线程的原因,B的next指向了A,所以B->A->B,形成循环。
当然,JDK 8 将扩容算法做了调整,不再将元素加入链表头(而是保持与扩容前一样的顺序), 尾插法 ,就避免了jdk7中死循环的问题
ConcurrentHashMap
Segment段
线程安全(Segment继承ReentrantLock加锁)
Java 7 ConcurrentHashMap结构
并行度(默认16)
JAVA8实现(引入红黑树)
Java8对ConcurrentHashMap进行了比较大的改动,java8也引入了红黑树
HashTable(线程安全)
TreeMap(可排序)
TreeMap实现SortedMap接口,能够把它保存的记录根据键排序,默认是按键值的升序排序,也可以指定排序的比较器,当Iterator遍历TreeMap时,得到的记录是排过序的。
LinkedHashMap(记录插入顺序)
LinkedHashMap是HashMap的一个子类,保存了记录的插入顺序,在用Iterator遍历LinkedHashMap时,先得到的记录肯定是先插入的,也可以在构造时带参数,按照访问顺序排序。
HashSet与HashMap的区别
HashSet底层其实是用HashMap实现存储的, HashSet封装了一系列HashMap的方法,依靠HashMap来存储元素值,(利用hashMap的key键进行存储)
而value值默认为Object对象. 所以HashSet也不允许出现重复值, 判断标准和HashMap判断标准相同, 两个元素的hashCode相等并且通过equals()方法返回true.
HashTable与HashMap的区别
- 数据结构不一样,hashtable是数组+链表,hashmap在1.8之后改为了数组+链表+红黑树
- hashtable存储数据的时候都不能为null,而hashmap是可以的
- hash算法不同,hashtable是用本地修饰的hashcode值,而hashmap经常了二次hash
- 扩容方式不同,hashtable是当前容量翻倍+1,hashmap是当前容量翻倍
- hashtable是线程安全的,操作数据的时候加了锁synchronized,hashmap不是线程安全的,效率更高一些
- 在实际开中不建议使用HashTable,在多线程环境下可以使用ConcurrentHashMap类
SpringMVC
什么是SpringMvc
SpringMvc是spring的一个模块,基于MVC的一个框架,无需中间整合层来整合。
SpringMvc的优点
- 是spring框架的一部分,可以方便的利用spring所提供的的其他功能
- 提供了一个前端控制器DispatchServlet,使开发人员无需额外开发控制器对象
- 可以自动绑定用户输入,并能正确的转换数据类型
- 内置了常见的校验器,可以校验用户输入
- 支持国际化,可以根据用户区域显示多国语言
- 支持多种视图技术,它支持JSP、FreeMarker等视图技术
- 使用基于xml的配置文件,在编辑后,不需要重新编译应用程序
SpringMvc工作原理
- 客户端发送请求到DispatchServlet
- DispatchServlet查询handlerMapping找到处理请求的Controller
- Controller调用业务逻辑后,返回ModelAndView
- DispatchServlet查询ModelAndView,找到指定视图
- 视图将结果返回客户端
SpringMVC流程
- 用户发送请求到前端控制器DispatchServlet
- DispatchServlet收到请求调用HandlerMapping处理器映射器
- 处理器映射器找到具体的处理器(可以根据xml配置,注解进行查找),生成处理器对象及处理器拦截器(如果有则生成)一并返回给DispatchServlet
- DispatchServlet调用HandlerAdapter处理器适配器
- HandlerAdapter经过适配调用具体的处理器(controller,也叫后端控制器)
- Controller执行完成返回ModelAndView
- HandlerAdapter将controller执行结果返回给DispatchServlet
- DispatchServlet将ModelAndView传给ViewReslover视图解析器
- ViewReslover解析后返回具体View
- DispatchServlet根据View进行渲染视图(即将模型数据填充至视图中)
- DispatchServlet响应用户
SpringMVc的控制器是不是单例模式,如果是,有什么问题,怎么解决
是单例模式,所以在多线程访问的时候有线程安全问题,不要用同步,会影响性能的,解决方法是在控制器里面不能写成员变量。
SpringMVC控制器的注解一般用哪个,有没别的注解可以替代
一般用@Controller注解,表示是表现层,不能用别的注解替代
@RequestMapping注解用在类上面有什么作用
是一个用来处理请求地址映射的注解,可以用于类或者方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径
怎样把某个请求方法映射到特定的方法上面
直接在方法上加上注解@RequestMapping,并且在这个注解里面写上要拦截的路径
如果在拦截请求中,我想拦截get方式提交的方法,怎么配置
可以在@RequestMapping中加上method=RequestMethod.GET
怎么样在方法里得到Request或者Session
直接在方法的形参中声明request,springmvc就自动把request对象传入
我想在拦截的方法里面得到从前台传入的参数,怎么得到
直接在形参里面声明这个参数就可以,但名字必须要和传过来的参数一样
如果前台有很多个参数传入,并且这些参数都是一个对象
直接在方法中声明这个对象,springmvc就自动会把属性到这个对象里面。
SpringMvc怎样设定重定向和转发的
在返回值加forward,比如forward:user.do?name=method4
再返回值加redirect,比如redirect: http://www.baidu.com
当以一个方法向ajax返回List,Object,Map等,要怎么做
sprinmvc拦截器怎么写的
- 实现接口 HandlerInterceptor ,重写preHandle方法(处理前)、postHandle方法(处理后)、afterCompletion方法(清理操作)
- 继承适配器类,然后在springmvc的配置文件配置拦截器即可
1 |
|
拦截器和过滤器的区别
过滤器
拦截器
如何解决POST请求中文乱码,GET的呢
解决post中文乱码
1 |
|
解决Get请求中文参数乱码有两个方法
-
修改tomcat配置文件添加编码
URIEncoding="utf-8"
与工程编码一致1
<Connector URIEncoding="utf-8" connectionTimeout="20000" port="8080" protocol="HTTP/1.1" redirectPort="8443"/>
-
方法对参数进行重新编码:ISO8859-1是tomcat默认编码,需要将tomcat编码后的内容按utf-8编码
1
String userName = new String(request.getParamter("userName").getBytes("ISO8859-1"),"utf-8")
Springmvc异常如何处理
定义一个异常处理器,就是自己写一个类,实现SpringMVC的一个异常处理器HandlerExceptionResolver(接口),并在springmvc配置中配置
1 |
|
Spring
什么是Spring框架
Spring就是一个轻量级的控制反转(IOC)和面向切面编程(AOP)的框架,主要是针对javaBean的生命周期进行管理的轻量级容器,最大的优点就是解耦,减少程序的耦合性。
什么是控制反转,什么是依赖注入
IOC:把对象的创建、初始化、销毁交给spring来管理,而不是由开发者控制,实现控制反转。
DI:依赖注入是控制反转的基础,所谓依赖注入就是上层控制底层,底层类作为参数传入上层类
SpringAOP的理解
- 切面:类是对物体特征的抽象,切面就是对横切关注点的抽象
- 横切关注点:对哪些方法进行拦截,拦截后怎么处理,这些关注点称为横切关注点
- 连接点:被拦截到的点,在spring中指的是被拦截到的方法,实际上连接点还可以是字段或者构造器
- 切入点:对连接点进行拦截的定义
- 通知:指拦截到连接点之后要执行的代码,分为前置、后置、异常、最终、环绕通知五类
- 目标对象:代理的目标对象
- 织入:将切面应用到目标对象并导致代理对象创建的过程
- 引入:在不修改代码的前提下,引入可以在运行期为类动态地添加一些方法或字段
BeanFactory和ApplicationContext有什么区别
BeanFactory
BeanFactory可以理解为含有bean集合的工厂类。BeanFactory包含了各种bean的定义,以便在接收到客户端请求时将对应的bean实例化。
ApplicationContext
ApplicationContext是spring提供的高级的IOC容器,面向使用spring框架的开发者,由BeanFactory派生而来,并且提供了很多面向实际应用的功能,比如
以下是三种较常见的ApplicationContext实现方式
- ClassPathXMLApplicationContext:默认从类路径加载配置文件
- FileSystemXmlApplicationContext:默认从文件系统中加载配置文件
- XmlWebApplicationContext:从web应用的文件中读取上下文
Spring有几种配置方式
-
在spring框架中,依赖和服务需要在专门的配置文件来实现,这些配置文件的格式通常用
开头,然后一系列的bean定义和专门的应用配置选项组成。 Spring的xml配置方式是使用被Spring命名空间的所支持的一些系列的xml标签来实现的,主要有context、beans、jdbc、tx、aop、mvc等
1
2
3
4
5
6
7
8
9
10<beans>
<!-- JSON Support -->
<bean name="viewResolver"
class="org.springframework.web.servlet.view.BeanNameViewResolver"/>
<bean name="jsonTemplate"
class="org.springframework.web.servlet.view.json.MappingJackson2JsonV
iew"/>
<bean id="restTemplate"
class="org.springframework.web.client.RestTemplate"/>
</beans> -
1
2
3<beans>
<context:annotation-config/>
</beans> -
被@Configuration所注解的类则表示这个类的主要目的是作为bean定义的资源,被@Configuration声明的类可以通过在同一个类的内部调用@bean方法来设置嵌入bean的依赖关系。
1
2
3
4
5
6
7
8
9@Configuration//这个也会被Spring容器托管,注册到容器中,因为他本来就是一个@Component
//@Configuration代表这是一个配置类就和我们之前看的beans.xml一样
public class AppConfig{
@Bean
// @Bean—注册一个bean,就相当于我们之前写的一个bean标签,这个方法的名字就相当于bean标签中的id属性,这个方法的返回值就相当于bean标签中的class属性
public MyService myService(){
return new MyServiceImpl()
}
}1
2
3<beans>
<bean id="myService" class="com.xxx.xxx.MyserviceImpl"></bean>
</beans>上述配置方式的实例化方法如下:利用AnnotationConfigApplication类进行实例化
1
2
3
4
5public static void main(String[] args){
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
MyService myService = ctx.getBean(MyService.class);
myService.doSomeThing();
}
简述Spring Bean的生命周期
首先会通过一个非常重要的类,叫做BeanDefinition获取bean的定义信息,这里面就封装了bean的所有信息,比如,类的全路径,是否是延迟加载,是否是单例等等这些信息
- 在创建bean的时候,第一步是调用构造函数实例化bean
- bean的依赖注入,比如一些set方法注入,像平时开发用的@Autowire都是这一步完成的
- 处理Aware接口,如果某一个bean实现了Aware接口,就会重写方法执行
- bean的后置处理器BeanPostProcessor,这个是前置处理器
- 初始化方法,比如实现接口InitializingBean或者自定义了方法init-method标签或@PostContruct
- 执行了bean的后置处理器BeanPostProcessor,主要是对bean进行增强,有可能在这里产生代理对象
- 最后一步是调用destory方法销毁bean
Spring Bean的作用域
singleton(单列)
这种bean范围是默认的,每个容器中只有一个bean实例,只有一个就避免对象的频繁创建与销毁,达到了bean对象的复用,性能高。
单例bean是线程安全的吗
Spring框架并没有对单例bean进行任何多线程的封装处理,关于单例bean的线程安全和并发问题需要开发者自行去搞定
比如:我们通常在项目中使用的Spring bean都是不可变的状态(比如Service类和Dao类),所以在某种程度上说Spring的单例bean是线程安全的
如果你的bean有多种状态的话(比如View Model对象,可以理解为实体类),就需要自行保证线程安全。最浅显的解决办法就是将多态bean的范围由singleton变更为prototype。
prototype(多例)
request
为每一个来自客户端的网络请求创建一个实例,在请求完成以后,bean会失效并被垃圾回收器回收
Session
确保每个session中有一个bean的实例,在session过期后,bean会随之失效
global-session
举例说明如何在spring中注入一个java collection
请解释Spring Bean的自动装配
spring可以通过向bean factory中注入的方式自动搞定bean之间的依赖关系,自动装配可以配置在每一个bean上,也可以设定在特定的bean上
-
1
<bean id="employDao" class="com.xxx.EmployDaoImp" autowire="byName"></bean>
-
还可以使用@Autowired注解来自动装配指定的bean,但是需要spring配置中加上以下配置
1
<context:annotation-config />
- no:默认的设置,在该设置下自动装配是关闭的,开发者需要自行在bean定义中用标签明确的设置依赖关系
- byName:可以根据bean名称设置依赖关系
- byType:根据bean类型设置依赖关系
- constructor:仅适用于有构造器相同参数的bean
- autodetect:该模式自动探测使用构造器自动装配或者byType自动装配
Spring都用到了那些设计模式
- 代理模式—-Spring的Aop用到了JDK动态代理和CGLIB字节码生成技术
- 单例模式—bean默认为单例模式
- 模板方法—-用来解决代码重复的问题,比如restTemplate,jdbcTempalte
- 工厂模式—–BeanFactory用来创建对象的实例
- 观察者模式—-定义对象一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被自动更新,如Spring中listener的实现ApplicationListener。
开发中主要使用Spring什么技术
spring中如何配置bean
IOC容器对Bean的生命周期
- 通过构造器或工厂方法创建bean实例
- 为bean的属性设置值和对其他bean的引用
- 将bean实例传递给bean前置处理器的postProcessBeforeIniiialization方法
- 调用bean的初始化方法(init-method)
- 将bean实例传递给bean后置处理器的postProcessAfterInitialization方法
- bean可以使用了
- 当容器关闭时,调用bean的销毁方法(destory-method)
Spring的事务隔离级别,实现原理
- 读未提交 read uncommitted(允许另外一个事务可以看到这个事务未提交的数据)
- 读提交 read committed(保证一个事务修的数据提交后才能被另一事务读取,而且能看到该事务对已有记录的更新)
- 可重复读 repeatable read(保证每一个是事务修改的数据提交后才能被另一事务读取,但是不能看到该事务对已有记录的更新)
- 串行化 serializable(一个事务在执行的过程中完全看不到其他事务对数据库所做的更新)
spring实现的事务本质就是aop完成,对方法前后进行拦截,在执行方法之前开启事务,在执行完目标方法之后根据执行情况提交或者回滚事务。
-
声明式事务管理其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前加入一个事务,在执行完目标方法之后根据执行的情况提交或者回滚事务。
-
在某个方法上增加@Transaction注解,就可以开始事务,这个方法所有的sql都会在一个事务中执行,统一成功或失败。
Spring中事务失效场景
- 如果方法上异常捕获处理,自己处理了异常,没有抛出,就会导致事务失效,所以一般处理了异常以后,别忘了跑出去就行了
- 如果方法抛出检查异常,也会导致事务失效,最后在spring事务的注解上,就是@Transactional上配置rollbackFor属性为Exception,这样别管是什么异常,都会回滚事务,Spring默认只回滚非检查异常(只会回滚runtimeException)
- 我之前还遇到过一个,如果方法上不是public修饰的,也会导致事务失效,因为spring为方法创建代理、添加事务通知、前提条件都是该方法是public的
循环依赖
就是两个或两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于A
循环依赖在spring中是允许存在,spring框架依据三级缓存已经解决了大部分的循环依赖
解决流程
如果想要打破循环依赖,就需要一个中间人的参与,这个中间人就是二级缓存或三级缓存
- 先实例A对象,同时会创建ObjectFactory对象存入三级缓存(singletonFactories)
- A在初始化的时候需要B对象,这个走B的创建的逻辑
- B实例化完成,也会创建ObjectFactory对象存入三级缓存(singletonFactories)
- B需要注入A,通过三级缓存中获取ObjectFactory来生成一个A的对象同时存入二级缓存,这个是有两种情况,一个是可能是A的普通对象,另外一个是A的代理对象,都可以让ObjectFactory来生产对应的对象,这也是三级缓存的关键
- B通过从通过二级缓存(earlySingletonObjects )获得到A的对象后可以正常注入,B创建成功,存入一级缓存(singletonObjects)
- 回到A对象初始化,因为B对象已经创建完成,则可以直接注入B,A创建成功存入一次缓存(singletonObjects)
- 二级缓存中的临时对象A清除
构造方法出现了循环依赖怎么解决
由于bean的生命周期中构造函数是第一个执行的,spring框架并不能解决构造函数的的依赖注入,可以使用@Lazy懒加载,什么时候需要对象再进行bean对象的创建
常用注解
Spring注解
声明bean
依赖注入
设置作用域
Spring配置相关
AOP相关增强
SpringMVC注解
SpringBoot注解
@SpringBootApplication核心注解
SpringBoot
SpringBoot的优点
什么是JavaConfig
它提供了配置spring ioc容器的纯java方法,有助于避免使用xml配置,
如何在定义定端口上运行springboot程序
在application.properties中指定端口:server.port=8090
什么是YAML
yaml是一种人类可读的数据序列化语言,它通常用于配置文件,与属性文件相比,如果我们想在配置文件中添加复杂的属性,yaml文件就更加结构化,而且更少混淆,可以看出yaml具有分层配置的数据。
springboot实现异常处理
我们通过实现一个ControlerAdvice,来处理控制器类抛出的所有异常
什么是CSRF攻击
CSRF代表跨站请求伪造,这是一种攻击,迫使最终用户在当前通过身份验证的web应用程序执行不需要的操作,CSRF攻击专门针对状态改变请求,而不是数据窃取,因为攻击者无法查看对伪造请求的响应。
SpringBoot运行原理
-
xxxApplication启动类有@springbootApplication注解标注它是一个springboot启动类。点进去有三个注解
Mybatis
什么是mybatis
Mybatis的执行流程
- 读取Mybatis配置文件:Mybatis-config.xml加载运行环境和映射文件
- 构造会话工厂sqlSessionFactory,一个项目只需要一个,单例的,一般由spring进行管理
- 会话工厂创建sqlSession对象,这里面就含了执行SQL语句的所有方法
- 操作数据库的接口,Executor执行器,同时负责查询缓存的维护
- Executor接口的执行方法中有一个MappedStatement类型的参数,封装了映射信息
- 输入参数映射
- 输出结果映射
Mybatis与Hibernate有哪些不同
#{}和${}的区别
mybatis在处理#{}时,会将sql中的#{}替换为?号,调用PreparedStatement的set方法来赋值。可以有效的防止sql注入,提高系统的安全性
Mybatis是如何进行分页的,分页插件原理是什么?
分页插件的基本原理是使用Mybatis提供的插件接口,实现自定义插件,在插件的拦截方法内拦截待执行的sql,然后重写sql,根据dialect方言,添加对应的物理分页语句和物理分页参数。
Mybatis是如何将sql执行结果封装为目标对象并返回
-
使用
标签,逐一定义数据库列名和对象属性名之间的映射关系 - 使用sql列的别名功能,将类的别名书写为对象的属性名。有了列名和属性名的映射关系后,mybatis通过反射创建对象,同时使用反射给对象的属性逐一赋值并返回,那些找不到映射关系的属性,是无法完成赋值的。
Mybatis动态sql用什么?执行原理
xml映射文件中,除了select、insert、update、delete标签,还有那些标签
mybatis的xml映射文件中,不同的xml映射文件,id是否可以重复
不同的xml映射文件,如果配置了namespace,那么id可以重复,如果没有配置namespace,那么id不能重复、
Mybatis是否支持延迟加载,如果支持,它的实现原理是什么
底层原理
- 使用CGLIB创建目标对象的代理对象,这里的目标对象就是开启了延迟加载的mapper
- 当调用目标方法时,进入拦截器invoke方法,发现目标方法时null值,再执行sql查询
- 获取数据以后,调用set方法设置属性值,在继续查询目标方法,就有值了
Mybatis缓存
缓存的读取顺序:先会读取二级缓存,如果二级缓存没有,则会去一级缓存找,如果一级缓存也没有,在连接数据库去查找
使用Mabatis的mapper接口调用时有哪些要求
- Mapper接口方法名和mapper.xm中定义的每个sql的id相同
- Mapper接口方法的输入参数类型和mapper.xml中定义每个sql的parameterType的类型相同
- Mapper接口方法的输出参数类型和mapper.xml中定义的每个sql的resultType的类型相同
- Mapper.xml文件中的namespace即是mapper接口的类路径
SpringCloud
微服务架构
如果人过于多的访问一个模块,一台服务器解决不了,在增加服务器(属于横向解决)。
假设A服务器占用98%的资源,而B服务器只占用10%(负载均衡解决)
微服务架构会遇到的问题
springcloud是一套生态,用来解决以上分布式的四个问题
-
spring cloud netflix(一站式解决方案)
- API网关:zuul组件,解决服务问题
- Fegin:基于HttpClient,http的通信方式,同步并阻塞
- 服务注册与发现:Eureka
- 熔断机制:Hystrix
-
Apache Dubbo Zookeeper(第二套解决方案)
- API:没有,要么找第三方组件,要么自己解决
- Doubbo:(RPC框架)高性能的基于java实现的RPC通信框架
- 服务注册与发现:(交给第三方管理)Zookeeper
- 熔断机制:没有,借助了Hystrix
-
Springcloud Alibaba(一站式解决方案)
-
注册中心/配置中心 Nacos
-
负载均衡 Ribbon
-
服务调用 Feign
-
服务保护 sentinel
-
服务网关 Gateway
-
SpringBoot和springcloud谈谈你的理解
- springboot可以用来开发单个应用。springcloud是用于多个个体的开发,也就是分布式开发
- Springcloud是基于springboot的一套实现微服务的框架,springboot可以独立开发项目,但是springcloud离不开springboot,属于依赖关系
- springboot专注于快速、方便的开发单个微服务个体,springcloud关注全局的服务治理框架
注册中心
常见的注册中心:Eureka、nocos、zookeeper
Eureka
Nacos
Eureka与Nacos对比
相同点
不同点
- Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
- 临时实例心跳不正常会被剔除、非临时实例则不会被剔除
- Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
- Nacos集群默认采用AP(高可用)方式,当集群中存在非临时实例时,采用CP模式(强一致性);eureka采用AP方式
- Nacos还支持了配置中心,eureka则只有注册中心,也是选择使用Nacos的一个重要原因
Ribbon负载均衡
当服务之间发起远程调用Feign就会使用到Ribbon实现请求的负载均衡
负载均衡的流程
负载均衡策略
自定义负载均衡策略
可以自己创建类实现IRule接口,然后通过配置类或者配置文件配置即可,通过定义IRule实现可以修改负载均衡规则,有以下规则
-
全局生效(创建类实现IRule接口,将自己的负载均衡策略交给Spring容器管理)
1
2
3
4@Bean
public IRule randomRule(){
return new RandomRule();
} -
局部生效(在服务消费者的配置文件中,配置需要消费的服务设置对应的负载均衡规则)
1
2
3
4userservice:
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule# 负载均衡规则
服务雪崩
一个服务失败,导致整条链路的服务都失败的情形。可以通过服务的熔断降级去解决或者限流去预防
Hystix熔断、降级(解决)
服务降级
服务降级是服务自我保护的一种方式,或者保护下游服务的一种方式,用与确保服务不会受请求突增影响变得不可用,确保服务不会崩溃,一般在实际开发中与Feign接口整合,编写降级逻辑
1 |
|
服务熔断
用于监控微服务调用情况,默认是关闭的,如果需要开启,需要再引导类上添加注解@EnableCircuitBreaker。
如果检测到10秒内请求失败率超过50%,就触发熔断机制。
之后每隔5秒重新尝试请求微服务,如果微服务不能响应,继续走熔断机制。如果微服务可达,则关闭熔断机制,恢复正常请求。
因为发起请求是通过Hystrix的线程池来走的,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题
服务熔断和降级的区别
- 触发原因不太一样,服务熔断一般是某个服务(下游服务)故障引起,而服务降级一般是从整体负荷考虑
- 管理目标的层次不太一样,熔断其实是一个框架级的处理,每个微服务都需要(无层级区分),而降级一般需要对业务有层级之分(比如降级一般都是从最外围服务开始的)
- 实现方式不太一样,服务降级具有代码侵入性(由控制器完成/或自动降级),熔断一般称为自我熔断。
服务监控
skywalking
一个分布式系统的应用程序性能监控工具(ApplicationContext Performance Managment),提供了完善的链路追踪能力,Apache的顶级项目
- skywalking主要可以监控接口、服务、物理实例的一些状态。特别是在压测的时候可以看到众多服务中那些服务和接口比较慢,我们可以针对性的分析和优化
- 我们还在skywalking设置了告警规则,特别是在项目上线以后,如果报错,我们分别设置了可以给相关负责人发送短信和邮件,第一时间知道项目的bug情况,第一时间修复。
微服务的优缺点,说下你在项目中遇到的坑
限流
-
1
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" maxThreads="150" redirectPort="8443"></Connector>
Nginx限流
nginx使用的漏桶算法来实现过滤,让请求以固定的速率处理请求,可以应对突发流量,我们控制的速率是按照ip进行限流,限制的流量是每秒20
网关限流
漏桶算法和令牌桶算法
漏桶算法
漏桶算法是把请求存入到桶中,以固定速率从桶中流出,可以让我们的服务做到绝对的平均,起到很好的限流效果
令牌桶算法
令牌桶算法在桶中存储的是令牌,按照一定的速率生成令牌,每个请求都要先申请令牌,申请到令牌以后才能正常请求,也可以起到很好的限流作用
CAP理论
CAP主要是在分布式项目下的一个理论。包含了三项,一致性、可用性、分区容错性
一致性(Consistency)
是指更新操作成功并返回客户端完成后,所有节点在同一时间的数据完全一致(强一致性),不能存在中间状态。
可用性(Availability)
是指系统提供的服务必须一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。
分区容错性(Partition tolerance)
是指分布式系统在遇到任何网络分区故障时,仍然需要能够保证对外提供满足一致性和可用性的服务,除非是整个网络环境都发生了故障。
为什么分布式系统中无法同时保证一致性和可用性
首先一个前提,对于分布式系统而言,分区容错性是一个最基本的要求,因此基本上我们在设计分布式系统的时候只能从一致性(C)和可用性(A)之间进行取舍。
如果保证了可用性(A):那就不能暂停N2的读写操作,但同时N1在写数据的话,这就违背了一致性的要求。
BASE理论
Basically Available(基本可用)
基本可用是指分布式系统在出现不可预知的故障的时候,允许损失部分可用性,但不等于系统不可用。
Soft state(软状态)
即是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统的整体可用性,即允许系统在不同节点的数据副本之间进行数据同步的过程存在延时。
Eventually consistent(最终一致性)
强调系统中所有的数据副本,在经过一段时间的同步后,最终能够达到一个一致的状态。其本质是需要系统保证最终数据能够达到一致,而不需要实时保证系统数据的强一致性。
Ribbon与Feign的区别
Ribbon添加maven依赖spring-starter-ribbon,使用@RibbonClient(value=”服务名称”),使用RestTemplate调用远程服务对应的方法。
Feign添加maven依赖spring-starter-feign,服务提供方提供对外接口,调用方使用在接口上使用@FeignClient(“指定服务名”)
- Ribbon和Feign都是用于调用其他服务的,不过方式不同
- 启动类使用的注解不同,Ribbon用的是@RibbonClient,Feign用的是@EnableFeighClients
- 服务的指定位置不同,Ribbon是在@RibbonClient注解上声明,Feign则是在定义抽象方法的接口中使用FeignClient声明
- 调用方式不同,Ribbon需要自己构建http请求,模拟http请求然后使用RestTemplate发送给其他服务,步骤繁琐。Feign则是在Ribbon的基础上进行了一次改进,采用接口的方式,将需要调用的其他服务的方法定义成抽象方法即可,不需要自己构建http请求。不过要注意的是抽象方法的注解、方法签名要和提供服务的方法完全一致。
Eureka和Zookeeper的区别
- Zookeeper保证了CP(C:一致性,P:分区容错性),但是不保证可用性,ZK的leader选举期间,是不可用的
- Eureka保证了AP(A:高可用),它优先保证了可用性,几个节点挂掉不会影响正常节点的工作。
分布式事务解决方案
Seata架构
XA模式
AT模式
AT模式同样是分阶段提交的事务模式,不过弥补了XA模型中资源锁定周期过长的缺陷。
TCC模式
总结
- seata的XA模式,需要互相等待各个分支事务提交,可以保证强一致性,性能差(CP,适用于银行业务)
- seata的AT模式,底层使用undo-log实现,性能好(AP,适用于互联网业务)
- seata的TCC模式,性能较好,不过需要人工编码实现(AP,适用于银行业务)
接口幂等性
多次调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致。
XXl-JOB
路由策略
xxl-job提供了很多的路由策略,我们平时用的较多就是:轮询、故障转移、分片广播…
任务执行失败怎么解决
大数据量的任务同时都需要执行,怎么解决
我们会让部署多个实例,共同去执行这些批量的任务,其中任务的路由策略是分片广播
在任务执行的代码中可以获取分片总数和当前分片,按照取模的方式分摊到各个实例执行就可以了
Redis
什么是Redis?主要用来干什么的
Redis,由C语言编写的远程字典服务,可基于内存亦可持久化的日志型key-value数据库。
Redis的数据结构类型
五大基本类型
String(字符串)
- String是Redis最基础的数据结构类型,它是二进制安全的,可以存储图片或者序列化对象,值最大存储为512M
- 有以下方法set、get、exist(是否存在)、incr(获取值自增)、decr(自减少)、setex(设置过期时间)、setnx(不存在在设置,在分布式锁会常用)
- 应用场景:共享session、分布式锁、计数器、限流
Hash(哈希)
List(列表)
-
1
2
3
4lpush+lpop=stack(栈)
lpush+rpop=Queue(队列)
lpush+ltrim=Capped Collection(有限集合)
lpush+brpop=message Queue(消息队列)
Set(集合)
zset(有序集合)
三种特殊数据结构类型
Geospatial
Hyperloglog
Bitmap
什么是缓存穿透、缓存雪崩、缓存击穿
缓存穿透
缓存穿透是指查询一个一定 不存在 的数据,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。这种情况大概率是遭到了攻击。。
- 如果是非法请求,我们在api入口,对参数进行校验,过滤非法值
- 如果查询数据库为空,我们可以给缓存设置一个空值或者默认值。但是如果有请求进来的话,需要更新缓存,以保证缓存一致性,同时,最后给缓存设置适当的过期时间。(业务上比较常用,简单有效)
- 布隆过滤器
布隆过滤器
布隆过滤器主要是用于检索一个元素是否在一个集合中。我们当时使用的是redisson实现的布隆过滤器。
缓存雪崩问题
缓存雪崩意思是设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB 瞬时压力过重雪崩。与缓存击穿的区别:雪崩是很多key,击穿是某一个key缓存。
- 缓存雪崩一般是由于大量数据同时过期造成的,对于这个原因,可以通过均匀设置过期时间解决。如采用一个较大固定值+一个较小的随机值。(5小时+0到18000秒)
- Redis故障宕机也可能引起缓存雪崩,这就需要构造Redis高可用集群
缓存击穿问题
指热点key在某个时间点过期的时候,而恰好在这个时间点对这个key有大量并发请求,从而大量的请求到数据库。
缓存击穿看着有点像:其实两者区别是:缓存雪崩是指数据库压力过大甚至down机,缓存击穿只是大量并发请求到了数据库层面。可以认为击穿是缓存雪崩的子集
解决方法:
- 使用互斥锁方案:缓存失效时,不是立即加载数据库数据,而是先使用某些带成功返回的原子操作,如(redis的setnx)去操作,成功的时候,再去加载db数据库数据和设置缓存。否则就去重试获取缓存
-
设置当前key逻辑过期
- 在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前key设置过期时间
- 当查询的时候,从redis取出数据后判断时间是否过期
- 如果过期则开通另外一个线程进行数据同步,当前线程正常返回数据,但是这个数据不是最新的数据
如果选择数据的强一致性,建议使用分布式锁的方案,性能上可能没那么高,锁需要等,也有可能产生死锁的问题
如果选择key的逻辑删除,则优先考虑的高可用性,性能比较高,但是数据同步这块做不到强一致。
什么是热key问题,如何解决
在redis中,我们把访问频率高的key,称为热点key,如果某一热点key的请求到服务器主机时,由于请求量特别大,可能会导致主机资源不足,甚至宕机,从而影响正常的服务。
- 用户消费的数据远大于生成产的数据,如秒杀,热点新闻等读多写少的场景
- 请求分片集中,超过redis服务器性能,比如固定名称key。hash落入同一台服务器,瞬间访问量极大,超过机器瓶颈,产生热点key问题
Mysql的数据如何与redis进行同步的
场景要求强一致性
可以采用redisson实现的读写锁,在读的时候添加共享锁,可以保证读读不互斥,读写互斥。
当我们更新数据的时候,添加排他锁,它是读写,读读都互斥,这样就能保证在写数据的同时是不会让其他线程读数据的,避免了脏数据。
这里面需要注意的是读方法和写方法上需要使用同一把锁才行。
排他锁是如何保证读写、读读互斥的呢
其实排他锁底层使用也是setnx,保证了同时只能有一个线程操作锁住的方法
延迟双删
延迟双删,如果是写操作,我们先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据,其中这个延时多久不太好确定,在延时的过程中可能会出现脏数据,并不能保证强一致性,所以没有采用它。
场景要求最终一致性
MQ异步通知
当去修改数据库数据时,会发送一个异步通知到消息队列,在根据消息队列的数据去更新缓存中的数据,这样去保证数据的最终一致性。
Canal异步通知
Redis过期策略和内存淘汰策略
过期策略
定期删除
就是说每隔一段时间,我们就对一些key进行检查,删除里面过期的key
- SLOW模式是定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf 的 hz 选项来调整这个次数
- FAST模式执行频率不固定,每次事件循环会尝试执行,但两次间隔不低于2ms,每次耗时不超过1ms
惰性删除
在设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。
==Redis中同时是用来惰性过期和定时过期两种过期策略==
- 假设redis存放30万key,并都设置了过期时间,如果你每隔100ms去检查全部的key,cpu负载会很高
- 因此采取定期过期,每隔100ms随机抽取一定数量的key来检查和删除
- 但是最后可能有很多过期的key没被删除,redis采用惰性删除,在你获取某个key时,redis会检查是否key已经过了过期时间,过期了就会删除
内存淘汰策略
这个在redis中提供了很多种,默认是noeviction,不删除任何数据,内部不足直接报错
是可以在redis的配置文件中进行设置的,里面有两个非常重要的概念,一个是LRU,另外一个是LFU
LRU的意思就是最近最少使用,用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。
LFU的意思是最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高
我们在项目设置的allkeys-lru,挑选最近最少使用的数据淘汰,把一些经常访问的key留在redis中(这种留下来的数据都是经常访问的热点数据)
redis内存用完了会发生什么
这个要看redis的数据淘汰策略是什么,如果是默认的配置,redis内存用完以后则直接报错。我们当时设置的 allkeys-lru 策略。把最近最常访问的数据留在缓存中。
redis应用场景
共享session
分布式锁
分布式服务下,就会遇到对同一个资源的并发访问的技术难题,如秒杀,下单减库存等场景
- 用sychronize或者ReentrantLock本地锁肯定不行
- 如果并发量不大的话,使用数据库的悲观锁、乐观锁来实现没啥问题
- 但是在并发量高的场合中,利用数据库锁来控制资源的并发访问,会影响数据库的性能
- 实际上可以使用redis的setnx来实现分布式锁
消息队列
消息对列是大型网站的必用中间件,主要用于业务解耦,流量削峰以及异步处理实时性低的业务。redis提供了发布/订阅及阻塞队列功能,能实现一个简单的消息队列系统,另外,这个不能和专业的消息中间件相比
Redis持久化
AOF
采用日志的形式来记录每一个写操作,追加到AOF文件的末尾。Redis默认是不开启AOF的,重启时再重新执行AOF文件中的命令来恢复数据。它主要解决数据持久化的实时性的问题。
AOF是执行命令完后才记录的日志,这是因为redis在向AOF记录日志时,不会先对这些命令进行检查,如果先记录日志在执行命令,日志中可能记录错误命令,redis使用日志恢复数据时,可能出错。
- always:同步写回,每个子命令执行完,都立即将日志写回磁盘
- everysec:每个命令执行完,只是先把日志写到AOF内存缓存区,每隔一秒同步到磁盘
- no:只是先把日志写到AOF内存缓存区,由操作系统去决定何时写入磁盘
==always同步写回,基本保证数据不丢失,no则性能高但是数据可能会丢失,一般可以考虑这种选择everysec==
优点:
缺点:
RDB
RDB是一个快照文件,它是把redis内存存储的数据写到磁盘上,当redis实例宕机恢复数据的时候,方便从RDB的快照文件中恢复数据。
如何选择RDB和AOF
Redis集群方案
为了实现高可用,通常的做法是,将数据库复制多个副本以部署在不同的服务器上,其中一台挂了也可以继续提供服务。redis实现高可用有三种部署模式:主从模式,哨兵模式、分片集群模式
redis高可用回答包括两个层面:一个就是数据不能丢失,或者是说尽量减少丢失,另一个就是保证redis服务不中断
Redis主从
- 就是部署多台redis服务器,有主库和从库,它们之间通过主从复制,以保证数据的一致
- 主从库之间采用的是读写分离的方式,其中主库负责读操作和写操作,从库负责读操作
- 如果Redis主库挂了,切换其中的从库成为主库
主从同步原理
主从同步数据的流程
-
全量同步是指从节点第一次与主节点建立连接的时候使用全量同步,流程是这样的
- 从节点请求主节点同步数据,其中从节点会携带自己的replication id和offset偏移量。
- 主节点判断是否是第一次请求,主要判断的依据就是,主节点与从节点是否是同一个replication id,如果不是,就说明是第一次同步,那主节点就会把自己的replication id和offset发送给从节点,让从节点与主节点的信息保持一致
- 在同时主节点会执行bgsave,生成rdb文件后,发送给从节点去执行,从节点先把自己的数据清空,然后执行主节点发送过来的rdb文件,这样就保持了一致
- 当然,如果在rdb生成执行期间,依然有请求到了主节点,而主节点会以命令的方式记录到缓冲区,缓冲区是一个日志文件,最后把这个日志文件发送给从节点,这样就能保证主节点与从节点完全一致了,后期再同步数据的时候,都是依赖于这个日志文件
Redis哨兵
哨兵作用
哨兵其实是一个运行在特殊模式下的redis进程,它有三个作用,分别是:监控、自动选主切换、通知.
哨兵模式
其实哨兵之间是通过发布订阅机制组成集群的,同时哨兵又通过info命令,获得了从库连接信息,也能和从库建立连接,从而进行监控
哨兵如何判定主库下线
==假设我们有 N 个哨兵实例,如果有 N/2+1 个实例判断主库 主观下线 ,此时就可 以把节点标记为 客观下线 ,就可以做主从切换了==
哨兵的工作模式
-
如果一个实例节点距离最后一次有效回复 PING 命令的时间超过 down-after-milliseconds 选项所指定的值, 则这个实例会被哨兵标记为主观下线。
-
当有足够数量的哨兵( 大于等于配置文件指定的值 )在指定的时间范围内确认主库的确进入了主观下线状态, 则主库会被标记为 客观下线 。
-
若没有足够数量的哨兵同意主库已经进入主观下线, 主库的 主观下线状态就会被 移除 ;若主库重新向哨兵的 PING 命令返回有效回复,主库的主观下线状态就会被 移除。
哨兵是如何选主的
- 首先判断主节点与从节点断开时间次数,如果超过指定阈值就排除该从节点(筛选)
- 然后判断从节点的slave-priority值,值越小优先级越高(打分)
-
如果slave-priority一样,则判断slave节点的offset值,offset值越大优先级越高
- 最后是判断slave节点的运行id大小,id值越小优先级越高
由那个哨兵执行主从切换
假设如果因为网络故障等原因,导致这轮投票就不会产生Leader,哨兵集群会等待一端时间(一般是哨兵故障转移超时时间的2倍),再进行重新选举。
故障转移
当哨兵检测到redis主库出现故障,那么哨兵需要对集群进行故障转移。流程如下
分片集群
主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决
- 集群中有多个master,每个master保存不同数据(解决海量数据存储问题,有过个master也能解决高并发写的问题)
- 每个master都可以有多个slave节点(解决高并发读)
- master之间通过ping监测彼此健康状态(监控状态,类似哨兵模式)
- 客户端请求可以访问集群任意节点,最终都会被转发到正确节点。(因为有路由规则,引入哈希槽概念。)
分片集群中数据是怎么存储和读取的
项目中使用redis是单点还是集群
一般单节点不超过10G内存,如果Redis内存不足则可以给不同服务分配独立的Redis主从节点。
尽量不做分片集群。因为集群维护起来比较麻烦,并且集群之间的心跳检测和数据通信会消耗大量的网络带宽,也没有办法使用lua脚本和事务
Redis脑裂该如何解决
脑裂的问题是这样的,出现在redis的哨兵模式集群,有的时候由于网络等原因可能会出现脑裂的情况
使用过redis分布式锁吗。有哪些注意点
分布式锁是控制分布式系统不同进程共同访问共享资源的一种锁的实现。redis分布式锁的几种实现方法
-
1
2
3
4
5
6
7
8
9
10
11if(jedis.setnx(key.lockValue)==1){
//加锁
expire(key,100);//设置过期时间
try{
do something;
}catch(){
}finally{
jedis.del(key);//释放锁
}
}如果执行完setnx加锁,正要执行expire设置过期时间,进程crash或者服务器重启,那么这个锁就一直存在,别的线程永远获取不到锁,所以分布式锁不能这样实现。
-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20long expires = System.currentTimeMillis()+expireTime;//系统时间+设置过期时间
String expiresStr = String.valueOf(expires);
//如果当前锁不存在就返回加锁成功
if(jedis.setnx(key,expiresStr)==1){
return true;
}
//如果所已经存在,获取锁的过期日期
String currentValueStr = jedis.get(key);
//如果获取到的过期时间,小于系统当前时间,表示已经过期
if(currentValueStr!=null&&Long.parseLong(currentValueStr)<System.currentTImeMillis()){
//锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间
String oldValue = jedis.getSet(key_resource_id.expiresStr);
if(oldValueStr!=null&&oldValueStr.equals(currentValueStr)){
//考虑多线程并发的情况,只有一个线程的设置值和当前值相同,他才可以加锁
return true;
}
}
//其他情况,均返回加锁失败
return false;
} -
key不存在的时候才进行设置选用NX,过期时间设置为10s,将SETNX和EXPIRE合二为一
1
2
3
4
5
6
7
8
9
10
11
12
13if(jedis.set(key,uni_request_id,"NX","EX",100s)==1){
//加锁
try{
do something;
}catch(){
}finally{
//判断是不是当前线程假的锁,是才释放
if(uni_request_id.equals(jedis.get(key))){
jedis.del(key);//释放锁
}
}
}在这里,判断当前线程加的锁和释放锁是不是一个原子操作。如果调用jedis.del()释放锁的时候,可能这把锁已经不属于当前客户端, 会解除他人加的锁
使用过Redisson嘛。说说它的原理
redis的setnx指令不好控制这个问题,我们当时采用的redis的一个框架redisson实现的。
还有一个好处就是,在高并发下,一个业务有可能会执行很快,先客户1持有锁的时候,客户2来了以后并不会马上拒绝,它会自旋不断尝试获取锁,如果客户1释放之后,客户2就可以马上持有锁,性能也得到了提升
redisson实现的分布式锁是可重入的吗
是可以重入的。这样做是为了避免死锁的产生。这个重入其实在内部就是判断是否是当前线程持有的锁,如果是当前线程持有的锁就会计数,如果释放锁就会在计算上减一。
在存储数据的时候采用的hash结构,大key可以按照自己的业务进行定制,其中小key是当前线程的唯一标识,value是当前线程重入的次数
redisson实现的分布式锁能解决主从一致性的问题吗
但是,如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变的很低了,并且运维维护成本也非常高,所以,我们一般在项目中也不会直接使用红锁,并且官方也暂时废弃了这个红锁
如果业务非要保证数据的强一致性,这个该怎么解决呢?
redis本身就是支持高可用的,做到强一致性,就非常影响性能,所以,如果有强一致性要求高的业务,建议使用zookeeper实现的分布式锁,它是可以保证强一致性的。
Redis是单线程,为什么那么快
I/O多路复用模型
I/O多路复用是指利用单个线程来同时监听多个Socket ,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源
目前的I/O多路复用都是采用的epoll模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。
其中Redis的网络模型就是使用I/O多路复用结合事件的处理器来应对多个Socket请求,比如,提供了连接应答处理器、命令回复处理器,命令请求处理器;
为什么Redis6.0之后改为多线程呢
redis 使用多线程并非是完全摒弃单线程,redis 还是使用单线程模型来处理客 户端的请求
只是在命令回复处理器使用了多线程来处理回复事件,在命令请求处理器中,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程
Redis事务机制
redis通过multi、exec、watch等一组命令集合,来实现事务机制。简言之,redis就是顺序性,一次性、排他性的执行一个队列中的一系列命令
还存在一个watch,监视key,如果在事务执行之前,该key被其他命令所改动,那么事务将被打断
消息队列
什么是消息队列
你可以把消息队列理解为一个使用队列来通信的组件。它的本质,就是个转发器,包含发消息、存消息、消费消息的过程。最简单的消息队列模型如下:
我们通常说的消息队列,简称MQ(Message Queue),它其实就指消息中间件,当前业界比较流行的开源消息中间件包括:RabbitMQ、RocketMQ、Kafka
消息队列有哪些使用场景(或者为什么使用消息队列)
应用解耦
常见业务场景:下单扣库存,用户下单后,订单系统去通知库存系统扣减。传统做法就是订单系统直接调用库存系统:
流量削峰
流量削峰也是消息队列的常用场景。我们做秒杀实现的时候,需要避免流量暴涨,打垮应用系统的风险。可以在应用前面加入消息队列。
假设秒杀系统每秒最多可以处理2k个请求,每秒却有5k的请求,可以引入消息队列,秒杀系统每秒从消息队列拉2k请求处理得了。
异步提速
消息通讯
消息队列内置了高效的通信机制,可用于消息通讯,如实现点对点消息队列、聊天室等
远程调用
MQ的劣势
消息队列如何解决消息丢失问题
如果是RocketMQ消息中间件,Producer生产者提供了三种发送消息的方式,分别是:
- 采用同步方式发送,send消息方法返回成功状态,就表示消息正常到达了存储端Broker
- 如果send消息异常或者返回非成功状态,可以重试
- 可以使用事务消息,RocketMQ的事务消息机制就是为了保证零丢失来设计的
- 开启生产者确认机制,确保生产者的消息能到达队列,如果报错可以先记录到日志中,再去修复数据
- 开启持久化功能,确保消息未消费前在队列中不会丢失,其中的交换机、队列、和消息都要做持久化
- 开启消费者确认机制为auto,由spring确认消息处理成功后完成ack,当然也需要设置一定的重试次数,我们当时设置了3次,如果重试3次还没有收到消息,就将失败后的消息投递到异常交换机,交由人工处理
在发送消息到消费者接收消息,在每个阶段都有可能会丢失消息,所以我们解决的话也是从多个方面考虑
- 生产者发送消息的时候,可以使用异步回调发送,如果消息发送失败,我们可以通过回调获取失败后的消息信息,可以考虑重试或记录日志,后边再做补偿都是可以的。同时在生产者这边还可以设置消息重试,有的时候是由于网络抖动的原因导致发送不成功,就可以使用重试机制来解决
- 在broker中消息有可能会丢失,我们可以通过kafka的复制机制来确保消息不丢失,在生产者发送消息的时候,可以设置一个acks,就是确认机制。我们可以设置参数为all,这样的话,当生产者发送消息到了分区之后,不仅仅只在leader分区保存确认,在follwer分区也会保存确认,只有当所有的副本都保存确认以后才算是成功发送了消息,所以,这样设置就很大程度了保证了消息不会在broker丢失
- 有可能是在消费者端丢失消息,kafka消费消息都是按照offset进行标记消费的,消费者默认是自动按期提交已经消费的偏移量,默认是每隔5s提交一次,如果出现重平衡的情况,可能会重复消费或丢失数据。我们一般都会禁用掉自动提价偏移量,改为手动提交,当消费成功以后再报告给broker消费的位置,这样就可以避免消息丢失和重复消费了
消息队列如何保证消息的顺序性
消息的有序性,就是指可以按照消息的发送顺序来消费,比如先下单在付款,最后再完成订单。
为了保证消息的顺序性,可以将将M1、M2发送到一个Server上,当M1发送完收到ack后,M2在发送。如图:
但是可能存在网络延迟,虽然M1先发送,但是它比M2晚到。那可以这样处理
Kafka保证消费的顺序性
消息队列有可能发生重复消费,如何避免,如何做到幂等
消息队列是可能发生重复消费的。
我们当时消费者是设置了自动确认机制,当服务还没来得及给MQ确认的时候,服务宕机了,导致服务重启之后,又消费了一次消息。这样就重复消费了
如何幂等处理重复消息
Kafka消息的重复消费问题解决
kafka消费消息都是按照offset进行标记消费的,消费者默认是自动按期提交已经消费的偏移量,默认是每隔5s提交一次,如果出现重平衡的情况,可能会重复消费或丢失数据。
我们一般都会禁用掉自动提价偏移量,改为手动提交,当消费成功以后再报告给broker消费的位置,这样就可以避免消息丢失和重复消费了
如何处理消息队列的消息积压问题
消息积压是因为生产者的生产速度,大于消费者的消费者速度,首先需要先排查,是不是有bug。
-
- 先修复consumer消费者的问题,以确保其恢复消费速度,然后将现用consumer都停掉
- 新建一个topic,partition是原来的10,临时建立好原来10倍的queue数量
- 然后写一个临时的分发数据的consumer程序,这个程序部署上去消费积压的数据,消费之后不做耗时的处理,直接均匀轮询写入临时建立好的10倍数量的queue
- 接着临时征用10倍的机器来部署consumer,每一批consumer消费一个临时queue的数据,这种做法相当于临时将queue资源和consumer资源扩大10倍,以正常的10倍速度来消费数据
- 等快速消费玩积压数据之后,得恢复原来先部署的架构,重新用原来的consumer机器来消费
消息队列技术选型
Kafka | RocketMQ | RabbitMQ | |
---|---|---|---|
单机吞吐量 | 17.3w/s | 11.6w/s | 2.62/s(消息做持久化) |
开发语言 | Scala/java | java | Erlang |
主要维护者 | Apache | Alibaba | Mozilla/Spring |
订阅形式 | 基于topic,按照topic进行正则匹配的发布订阅模式 | 基于topic/messageTag,按照消息类型、属性进行正则匹配的发布订阅模式 | 提供了4种:direct,topic,Headers和fanout。fanout就是广播模式 |
持久化 | 支持大量堆积 | 支持大量堆积 | 支持少量堆积 |
顺序消息 | 支持 | 支持 | 不支持 |
集群方式 | 天然的Leader-Slave,无状态集群,每台服务器即是Master也是Slave | 常用多对Master-Slave模式,开源版本需要手动切换Slave变成Master | 支持简单集群,复制模式,对高级集群模式支持不好 |
性能稳定性 | 较差 | 一般 | 好 |
如何保证数据一致性,事务消息如何实现
- 生产者产生消息,发送一条半事务消息到MQ服务器
- MQ收到消息后,将消息持久化到存储系统,这条消息的状态就是待发送状态
- MQ服务器返回ACK,确认到生产者,此时MQ不会触发消息推送事件
- 生产者执行本地事务
- 如果本地事务执行成功,即commit执行结果到MQ服务器;如果执行失败,发送rollback
- 如果是正常commit,MQ服务器更新消息状态为可发送,如果是rollback,即删除消息
- 如果消息状态更新为可发送,则MQ服务器会push消息给消费者,消费者消费完就返回ACK
- 如果MQ服务器长时间没有收到生产者的commit或者rollback,它会反查生成者,然后根据查询到的结果执行最终状态。
RabbitMQ高可用机制
我们当时项目在生产环境下,使用的集群,当时搭建是镜像模式集群,使用了3台机器。
镜像队列结构是一主多从,所有操作都是主节点完成,然后同步给镜像节点,如果主节点宕机后,镜像节点会替代成新的主节点,不过在主从同步完成前,主节点就已经宕机,可能出现数据丢失
RabbitMQ出现丢数据如何解决
我们可以采用仲裁队列,与镜像队列一样,都是主从模式,支持主从数据同步,主从同步基于Raft协议,强一致。
并且使用起来也非常简单,不需要额外的配置,在声明队列的时候只要指定这个是仲裁队列即可
Kafka高可用机制
- kafka集群指的是由多个broker实例组成,即使某一台宕机,也不耽误其他broker继续对外提供服务
- 复制机制是可以保证kafka的高可用的,一个topic有多个分区,每个分区有多个副本,有一个leader,其余的是follower,副本存储在不同的broker中;所有的分区副本的内容是都是相同的,如果leader发生故障时,会自动将其中一个follower提升为leader,保证了系统的容错性、高可用性
复制机制中的ISR
ISR的意思是in-sync replica,就是需要同步复制保存的follower
Kafka数据清理机制
Kafka中topic的数据存储在分区上,分区如果文件过大会分段存储segment
每个分段都在磁盘上以索引(xxxx.index)和日志文件(xxxx.log)的形式存储,这样分段的好处是,第一能够减少单个文件内容的大小,查找数据方便,第二方便kafka进行日志清理。
- 根据消息的保留时间,当消息保存的时间超过了指定的时间,就会触发清理,默认是168小时( 7天)
- 根据topic存储的数据大小,当topic所占的日志文件大小大于一定的阈值,则开始删除最久的消息。这个默认是关闭的
这两个策略都可以通过kafka的broker中的配置文件进行设置
Kafka中实现高性能的设计有了解过嘛
Kafka 高性能,是多方面协同的结果,包括宏观架构、分布式存储、ISR 数据同步、以及高效的利用磁盘、操作系统特性等。主要体现有这么几点:
- 消息分区:不受单台服务器的限制,可以不受限的处理更多的数据
- 顺序读写:磁盘顺序读写,提升读写效率
- 页缓存:把磁盘中的数据缓存到内存中,把对磁盘的访问变为对内存的访问
- 零拷贝:减少上下文切换及数据拷贝
- 消息压缩:减少磁盘IO和网络IO
- 分批发送:将消息打包批量发送,减少网络开销
RabbitMQ
相关概念
-
当多个不同的用户使用同一个RabbitMQ Server提供的服务时,可以划分出多个vhost,每个用户在自己的vhost创建exchange/queue等
-
exchange和queue之间的虚拟连接,binding中可以包含routing key。Binding信息被保存到exchange中的查询表中,用于message的分发依据
工作模式
简单模式
- 生产者产生消息,将消息放入队列
- 消息的消费者监听消息队列,如果队列中有消息,就消费掉,消息被拿走后,自动从队列中删除,这样可能造成消息处理失败后,造成消息的丢失,可以设置手动的ack,但是如果设置为手动的ack,处理完后要即时ack消息给队列,否则会造成内存溢出
工作队列模式(work queues)
Publish/Subscribe(发布与订阅模式)
Routing路由模式
Topics主题模式
可以里理解为路由模式的一种,不是有路由的key去精确匹配,而是根据key去模糊匹配,星号和井号代表通配符,星号代表多个单词,井号代表一个单词
消息确认机制(确保消息的发送和接收)
事务机制
confirm模式
发送方确认模式是异步的,生产者在等待确认的同时,可以继续发送消息,当确认消息到达生产者,生产者的回调方法会被触发
ConfirmCallback接口:只确认是否正确到达Exchange中,成功到达时回调
消息延时发送机制(死信交换机)
延迟队列就是用到了死信交换机和TTL(消息存活时间)实现的。
Zookeeper
Zookeeper有什么用途
- 当做dubbo的注册中心
- 它是一个开放源码的分布式协调服务,它是一个集群的管理者,它将简单易用的接口提供给用户
- 可以基于Zookeeper实现诸如数据发布/订阅、负载均衡,命名服务,分布式协调、集群管理、Master选举、分布式锁和分布式队列等功能
- Zookeeper用途:命名服务、配置管理、集群管理等。
什么是命名服务、配置管理、集群管理
znode有几种类型,Zookeeper数据模型是怎样的
Zookeeper的视图数据结构,很像unix文件系统,也是树状的,这样可以确定每个路径都是唯一的。Zookeeper的节点统一叫做znode,它是可以通过路径来标识的
znode的4中类型
根据节点的生命周期,znode可以分为4中类型,分别是持久节点、持久顺序节点、临时节点、临时顺序节点
- 持久节点(这类节点被创建后,就会一直存在于zk服务器上,直到手动删除)
- 持久顺序节点(它的基本特性和持久节点差不多,不同的是增加了顺序性。父节点会维护一个自增整型数字,用于子节点的创建的先后顺序)
- 临时节点(临时节点的生命周期与客户端的会话绑定,一旦客户端会话失效,那么这个节点就会被自动清理掉。zk规定临时节点只能作为叶子节点)
- 临时顺序节点(基本特性同临时节点,添加了顺序的特性)
znode节点存储什么,节点数据最大是多少
znode节点存储
1 |
|
所以znode包含了:存储数据,访问权限,子节点的引用、节点状态信息。
- data:znode存储的业务数据信息
- acl:记录客户端对znode节点的访问权限,如IP等
- child:当前节点的子节点引用
- stat:包含znode节点的状态信息,比如事务id、版本号、时间戳等
节点数据最大范围
为了保证高吞吐和低延迟,以及数据的一致性,znode只适合存储非常小的数据,不能超过1M,最好都小于1k。
你知道Znode节点的监听机制吗?讲下zk的watch机制
watcher监听机制
可以把watcher理解成客户端注册在某个znode上的触发器,当这个znode节点发生变化时(增删改查),就会触发znode对应的注册事件,注册的客户端就会收到异步通知,然后做出业务的改变
watch监听机制工作原理
- Zookeeper的watcher机制主要包括客户端线程,客户端watcherManager、Zookeeper服务器三部分
- 客户端向Zookeeper服务器注册watcher的同时,会将watcher对象存储在客户端的watchManager中
- 当Zookeeper服务器触发watcher事件后,会向客户端发送通知,客户端线程从watchManager中取出对应的watcher对象来执行回调逻辑。
Zookeeper的特性
- 顺序一致性:从同一客户端发起的事务请求,最终将会严格的按照顺序被应用到Zookeeper中去
- 原子性:要么整个集群中所有机器都成功应用了某一个服务,要么都没有应用
- 单一视图:无论客户端连到哪一个Zookeeper服务器上,其看到的服务端数据模型都是一致的
- 可靠性:一旦服务端成功应用了一个事务,并完成对客户端的响应,那么该事务所引起的服务端状态变更将会被一直保留下来
- 实时性(最终一致性):Zookeeper仅仅能保证在一定时间段内,客户端最终一定能够从服务端上读取到最新的数据状态
Zookeeper如何保证事务的顺序一致性
zxid的低32位是计数器,所以统一任期内,zxid是连续的,每个节点又都保存着自身最新生效的zxid,通过对比新提案的zxid与自身最新的zxid是否相差1,来保证事务严格按照顺序生效的。
Zookeeper服务器的角色,server工作的状态
服务器角色
-
Leader服务器是整个Zookeeper集群工作机制中的核心,其主要工作是:事务请求的唯一调度和处理者,保证集群事务处理的顺序性。集群内部各服务的调度者
-
充当一个观察者角色,观察Zookeeper集群的最新状态变化并将这些状态变更同步过来,其工作:处理客户端的非事务请求,转发事务请求给Leader服务器,不参与任何形式的投票
Server工作状态
- Looking:寻找Leader状态,当服务器处于该状态时,它会认为当前急群众没有Leader,因此需要进入Leader选举状态
- Following:跟随者状态,表明当前服务器角色是Follower
- Leading:领导者状态:表明当前服务器角色是Leader
- Observing:观察者状态:表明当前服务器角色是Observer
Zookeeper集群部署图
Zookeeper如何保证主从节点数据一致性
Zookeeper是采用ZAB(原子广播协议)来保证主从节点数据一致性的,ZAB协议支持崩溃恢复和消息广播两种模式
简述Zookeeper的选举机制
就是半数机制(集群中半数以上机器存活,集群可用,所以Zookeeper适合安装奇数台服务器),只要达到半数,它就会选择myid当中id号最大的那个。选举其实细分两类
启动时选举
- 服务器启动时选举,在未选取完leader的情况下,每台服务器启动时都会发起leader的选取,当服务器1发起选举时,会投给自己一票,但是服务器得票未超过半数,服务器1状态将会保持为loking状态
- 服务2启动时发起选举,首先会投自己一票,当服务器1和服务器2比较myid,发现服务器2的id大于服务器1,则服务器1将把自己的票投给服务器2,票未够半数,服务器2继续保持looking状态
- 服务器3启动,发起选举,按上述投票规则投完票,发现得票数大于半数,服务器1和2的状态就改为following,服务器3状态变为leading
- 服务器4启动发现服务器1和2状态为following,服务器1和2不会再次投票,而服务器4不管myid是否比服务器3大,而是遵从半数原则,直接将自己的票投给服务器3,依次类推,直到所有的服务起来
崩溃恢复时选举
Zookeeper常用命令
Zookeeper的部署方式有几种,角色有哪些,集群最少需要几台服务器
Dubbo
什么是dubbo
Apache dubbo是一款高性能、轻量级的开源java RPC框架,主要有三大能力
dubbo基本概念
- 服务容器启动负责启动、加载、运行服务提供者
- 服务提供者在启动时,向注册中心注册自己提供的服务
- 服务消费者在启动时,向注册中心订阅自己所需要的服务
- 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接(NIO异步通讯,适用于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况)推送变更数据给消费者
- 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,在选另一台调用
- 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心
dubbo如何做负载均衡
在提供者和消费者的配置文件中设置loadbalance属性的值,有以下四种内置策略
- RandomLoadBalance:随机负载均衡,按权重设置随机概率,是dubbo的默认负载均衡策略
- RoundRobinLoadBalance:轮询负载均衡,轮询依次,根据公约后的权重(几分之几,比如一个是七分之一,表示在七次访问中会有一次访问该服务器)进行轮询访问
- LeastActiveLoadBalance:最少活跃调用数,相同活跃数的随机
- ConsistentHashLoadBalance:一致性哈希负载均衡:相同参数的请求总是落在同一台机器上
dubbo如何做服务降级
dubbo提供了mock配置,可以很好的实现了dubbo服务降级
1 |
|
dubbo如何集群容错
-
失败自动切换,当出现失败时,重试其他服务器,通常用于读操作,但重试会带来更长的延迟,可通过设置retries=“2”来设置重试次数(不含第一次)
-
并行调用多个服务器,只要一个成功即返回,通常用于实时性要求较高的读操作,但需要浪费更多资源,可通 过fork=“2”来设置最大并行数
Dubbo如何实现异步调用的
- api注入是添加异步调用标识@Reference(interfaceClass=xxx.class,async-true)
- 启动类开启异步调用 @EnableAsycn
- 异步调用的接口添加异步调用代码
- RPCContext.getContext.future()
在provide上可配置的consumer端的属性有哪些
dubbo默认使用的是什么通信框架
Dubbo默认使用Netty框架(推荐),另外内容还集成Mina、Grizzly
Doubbo服务之间的调用时阻塞的吗
Dubbo的管理控制台可以做什么
说说dubbo服务暴露的过程
Nignx
概述
是一款高性能的HTTP和反向代理服务器,同时也提供了IMAP/POP3/SMTP服务。其特点是占有内存少,并发能力强。
Nginx代码完全用C语言从头写成,官方数据测试表明能够支持高达50000个并发连接数响应。
Nginx作用(下列图中云可以不需要考虑是什么意思)
Nginx负载均衡策略
Nginx常用命令
1 |
|
Nginx安装一些注意的问题
Mysql
Mysql中如何定位慢查询
- 如果部署了运维监控系统Skywalking,在展示的报表中可以看到哪一个接口比较慢,并且分析这个接口那部分比较慢,可以看到sql的具体的执行时间,所以可以定位是那个sql出了问题
- MySQL中也提供了慢日志查询的功能,可以在MySQL的系统配置文件中开启这个慢日志的功能,并且也可以设置SQL执行超过多少时间来记录到一个日志文件中
索引的优缺点以及索引的类型
优缺点
优点:
缺点
索引类型
- 主键索引:数据列不允许重复,不允许为Null,一个表只能有一个主键
- 唯一索引:数据列不允许重复,允许为null值,一个表允许多个列创建唯一索引
- 普通索引:基本的索引类型,没有唯一性的限制,允许为null值
- 全文索引:是目前搜索引擎使用的一种关键技术,对文本的内容进行分词、搜索
- 覆盖索引:查询列要被所建的索引覆盖,不必取数据行
- 组合索引:多列值组成一个索引,用于组合搜索,效率大于索引合并
创建索引有什么原则
这个情况有很多,不过都有一个大前提,就是表中的数据要超过10万以上,我们才会创建索引,并且添加索引的字段是查询比较频繁的字段,一般也是像作为查询条件,排序字段或分组的字段这些。
还有就是,我们通常创建索引的时候都是使用复合索引来创建,一条sql的返回值,尽量使用覆盖索引,如果字段的区分度不高的话,我们也会把它放在组合索引后面的字段。
如果某一个字段的内容较长,我们会考虑使用前缀索引来使用,当然并不是所有的字段都要添加索引,这个索引的数量也要控制,因为添加索引也会导致新增改的速度变慢。
- 最左前缀匹配原则
- 频繁作为查询条件的字段才去创建索引
- 索引列不能参与计算,不能有函数操作
- 频繁更新的字段不适合创建索引
- 优先考虑扩展索引,而不是新建索引,避免不必要的索引
- 在order by或者group by子句中,创建索引需要注意顺序
- 区分度低的数据列不适合做索引列(比如性别)
- 定义有外键的数据列一定要建立索引
- 对于定义为text、image数据类型的类不要建立索引
- 删除不在使用或者很少使用的索引
Mysql索引有哪些注意事项
索引那些情况会失效
- 查询条件包含or,可能导致索引失效
- 如果字段类型是字符串,where时一定要用引号括起来,否则索引失效
- 模糊查询,如果%号在前面也会导致索引失效
- 联合索引,查询时的条件列不是联合索引中的第一个列,索引失效(没遵循最左匹配法则)
- 在索引列上使用mysql内置函数,索引失效
- 对索引列运算(如+、-、*),索引失效
- 索引字段上使用(!=或者<>,not in)时,可能会导致索引失效
- 索引字段上使用is null,in not null,可能导致索引失效
- 左连接查询或者右连接查询关联字段编码格式不一样,可能导致索引失效
- mysql估计使用全表扫描要比索引快,则不使用索引
索引不适合那些场景
索引的一些潜规则
Mysql遇到死锁问题吗,如何解决
如何优化sql
- 加索引
- 避免返回不必要的数据(SELECT语句务必指明字段名称,不要直接使用select * )
- 适当分批量进行
-
优化sql结构
- 如果是聚合查询,尽量用union all代替union ,union会多一次过滤,效率比较低;
- 如果是表关联的话,尽量使用innerjoin ,不要使用用left join right join,如必须使用 一定要以小表为驱动
- 分库分表
- 读写分离
说说分库与分表的设计
分库分表方案
常用分库分表中间件
分库分表可能遇到的问题
InnoDB和MyISAM区别
- InnoDB支持事务,MyISAM不支持事务
- InnoDB支持外键,MyISAM不支持外键
- InnoDB支持MVCC(多版本并发控制),MyISAM不支持
- InnoDB支持表、行级锁,而MyISAM支持表级锁
- InnoDB必须有主键,而MyISAM可以没有主键
- InnoDB按主键大小有序插入,MyISAM按记录插入顺序保存
limit 1000000加载很慢,你怎么解决
-
如果id是连续的,可以返回上次查询的最大记录(偏移量),在往下limit
1
select id,name from employee where id>1000000 limit 10
-
1
select id,name from employee order by id limit 1000000,10
-
利用延迟关联或者子查询优化超多分页场景(先快速定位需要获取的id段,然后在关联)
1
select a.* from employee a,(select id from employee where 条件 limit 1000000,10)b where a.id=b.id
如何选择合适的分布式主键方案
事务的隔离级别有哪些,Mysql默认隔离级别是什么
什么是脏读、不可重复读、幻读
- 事务A,B交替执行,事务A读取到了事务B未提交的数据,这就是脏读
- 在一个事务范围内,两个相同的查询,读取同一条数据,却返回了不同的数据,这就是不可重复读
- 事务A查询一个范围的结果集,另一个并发事务B往这个范围中插入/删除了数据,并提交了,然后事务A再次查询相同的范围,两次读取的到的结果集不一样了,这就是幻读
在高并发的情况下,如何做到安全的修改同一行数据
要安全的修改同一行数据,就要保证一个线程再修改时其他线程无法更新这行记录,一般有悲观锁和乐观锁两种方案
使用悲观锁
就是当前线程要进来修改数据时,别的线程都得拒之门外,比如使用select…..for update
1 |
|
以上这条sql语句会锁定user表中所有符合检索条件name=’jay’的记录,本次事务提交之前,别的线程都无法修改这些记录
使用乐观锁
有线程过来,先放过去修改,如果看到别的线程没修改过,就可以修改成功,如果别的线程修改过,就修改失败或者重试。实现方式:乐观锁一般会使用版本号机制或CAS算法实现。
SQL优化的一般步骤是什么,怎么看执行计划(explain)
背这个回答:如果一条sql执行很慢的话,我们通常会使用mysql自动的执行计划explain来去查看这条sql的执行情况。
比如在这里面可以通过key和key_len检查是否命中了索引,如果本身已经添加了索引,也可以判断索引是否有失效的情况。
第二个,可以通过type字段查看sql是否有进一步的优化空间,是否存在全索引扫描或全盘扫描
第三个可以通过extra建议来判断,是否出现了回表的情况,如果出现了,可以尝试添加索引或修改返回字段来修复
-
id(表示查询中执行select子句或者操作表的顺序,id的值越大,代表优先级越高,越先执行)
- id相同:可以理解成具有同样的优先级,执行顺序由上而下,具体顺序由优化器决定
- id不同:如果sql中存在子查询,那么id序号会递增,id值越大优先级越高,越先被执行。当三个表依次嵌套,发现最里层的子查询id最大,最先执行
- 以上两种同时存在,增加一个子查询,发现id以上两种同时存在。相同的id划分为一组,这样就有三个组,同组的从上往下顺序执行。不同组id值越大,优先级越高,越先执行
-
select_type:表示select查询类型,主要用于区分各种复杂的查询,例如普通查询,联合查询,子查询等
- simple:表示最简单的select查询语句
- primary:当查询语句中包含任何复杂的子部分,最外层查询则被标记为primary
- subquery:当select或where列表中包含子查询
- derived:表示包含在from子句中的子查询的select
- union:如果union后边又出现的select语句
- union result:代表从union的临时表中读取数据
- table:查询的表明,并不一定是真实存在的表,也可能是临时表
- partitions:查询时匹配到的分区信息
-
type:查询使用了何种类型,它在sql优化中是一个非常重要的指标,从好到坏的顺序==system > const > eq_ref > ref > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > all==
- system:当表仅有一行记录时(系统表),数据量很少,往往不需要进行磁盘IO,速度非常快
- const:表示查询时命中primary key主键或者unique唯一索引,或者被连接的部分是一个常量值(const)。这类扫描效率极高,返回数据量少,速度非常快
- eq_ref:查询时命中主键primary_key或者unique_key索引
- ref:区别eq_ref,ref表示使用非唯一性索引,会找到很多符合条件的行
- ref_or_null:这种连接类型类似于ref,区别在于mysql会额外搜索包含null值的行
- index_merge:使用了索引合并优化方法,查询使用了两个以上的索引
- unique_subquery:替换下面的in子查询,子查询返回不重复的集合
- index_subquery:区别unique_subquery,用于非唯一索引,可以返回重复值
- range:使用索引选择行,仅检索给定范围内的行,简单的说就是针对一个有索引的字段,给定范围检索数据。在where语句中使用bettween..and、<、>、<=、in等条件查询type都是range
- index:index与ALL其实都是读全表,区别在于index是遍历索引树读取,而all是从硬盘中读取
- all:将遍历全表以找到匹配的行,性能最差
- possible_keys:表示在mysql中通过那些索引,能让我们在表中找到想要的记录,一旦查询涉及到的某个字段上存在索引,则索引将被列出。
- key:区别于possible_keys,key是查询中实际使用的索引,若没有使用索引,显示为NULL
- key_len:表示查询用到的索引长度,原则上长度越短越好
-
ref
- 当使用常量等值查询,显示const
- 当关联查询时,会显示相应关联表的关联字段
- 如果查询条件使用了表达式、函数、或者条件列发生内部隐式转换,可能显示为func
- 其他情况null
- rows:以表的统计信息和索引使用情况,估计要找到它们所需的记录,需要读取的行数,这是评估sql想能的一个比较重要的数据,mysql需要扫描的行数,很直观的显示sql性能的好坏,一般rows值越小越好
- filtered:这个字段表示存储引擎返回的数据在经过过滤后,剩下满足条件的记录数量的比例
- Extra:不适合在其他列中显示的信息,很多额外的信息会在exrea字段显示
select for update有什么含义,会锁表还是锁行还是其他
select for update含义
Mysql事务的四大特性以及实现原理
- 原子性:事务作为一个整体被执行,包含在其中的对数据库的操作要么全部执行,要么都不执行
- 一致性:指在事务开始之前和事务结束以后,数据不会被破坏,假如A账户给B账户转10块钱,不管成功与否,A和B的总金额是不变的
- 隔离性:多个事务并发访问,事务之间是相互隔离的,即一个事务不影响其他事务运行效果,简而言之,就是事务之间是井水不犯河水
- 持久性:表示事务完成以后,该事务对数据库所作的操作更改,将持久的保存在数据库之中
- 原子性:是使用undo log来实现的,如果事务执行过程中出错或者用户执行了rollback,系统通过undo log日志返回事务开始的状态
- 持久性:使用redo log 来实现,只要redo log日志持久化了,当系统崩溃,即可通过redo log把数据恢复
- 隔离性:通过锁以及mvvc,使事务相互隔离开
- 一致性:通过回滚、恢复、以及并发情况下的隔离性,从而实现一致性。
如果某个表有仅千万数据,curd比较慢,如何优化
如何写sql能够有效的使用到复合索引
数据库自增可能遇到什么问题
MVVC的底层原理
mvcc的意思是多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,它的底层实现主要是分为了三个部分,第一个是隐藏字段,第二个是undo log日志,第三个是readView读视图
-
对于InnoDB存储引擎,每一行记录都有两个隐藏列trx_id,roll_point,如果表中没有主键和非null唯一键时,则还会有第三个隐藏的主键列row_id
列名 是否必须 描述 row_id 否 单调递增的行id,不是必须的,占用6字节 trx_id 是 记录操作该数据事务的事务id roll_pointer 是 这个隐藏列相当于一个指针,指向回滚段的undo日志 -
它就是事务执行sql语句时,产生的读视图,在InnoDB中,每个sql语句执行前都会得到一个read view,用来判断当前事务可见那个版本的数据。
不同的隔离级别快照读是不一样的,最终的访问的结果不一样。如果是rc隔离级别,每一次执行快照读时生成ReadView,如果是rr隔离级别仅在事务中第一次执行快照读时生成ReadView,后续复用
查询一条sql,基于MVVC的流程
- 获取事务自己的版本号,即事务id
- 获取read view
- 查询得到的数据,然后和read view中的事务版本号进行比较
- 如果不符合read view的可见性规则,即就需要undo log中历史快照
- 最后返回符合规则的数据
==InnoDB 实现MVCC,是通过
Read View+ Undo Log
实现的,Undo Log 保存了历史快照,Read View可见性规则帮助判断当前版本的数据是否可见。==
MySql的主从延迟,怎么解决
主从复制的步骤
- 主库的更新操作(update、insert、delete)被写到binlog
- 从库发起连接,连接到主库
- 此时主库创建一个binlog dump thread,把binlog的内容发送到从库
- 从库启动后,创建一个IO线程。读取主库传过来的binlog内容并写入到relay log
- 还会创建一个sql线程,从relay log里面读取内容,从Exec_Master_Log_Pos位置开始执行读取到的更新事件,将更新内容写入到slave的db
主从同步延迟的原因
主从同步延迟的解决办法
- 主服务器要负责更新操作,对安全性的要求比从服务器高,所以有些设置参数可以修改,比如sync_binlog=1等
- 选择更好的硬件设备作为备份服务器
- 把一台从服务器当做为备份使用,而不提供查询,那他的负载就下来了,执行relay log里面的sql效率就高了起来
- 增加从服务器,这个目的还是分散读的压力,从而降低服务器负载
说说大表查询的优化方案
InnoDB索引策略
一条sql执行时间过长,如何优化
- 查看是否涉及多表和子查询,优化sql结构,比如去除冗余字段,是否可拆表等
- 优化索引结构,看是否可以适当的添加索引
- 数据量大的表,可以考虑进行分表
- 数据库主从分离,读写分离
- explain分析sql语句,查看执行计划,优化sql
Blob和text的区别
- Blob用于存储二进制数据,而Text用于存储大字符串
- Blob值被视为二进制字符串(字节字符串),它们没有字符集,并且排序和比较基于列值中的字节的数值
- text值被视为非二进制字符串(字符字符串)。它们有一个字符集,并根据字符集的排序规则对值进行排序和比较
Mysql锁
- 表锁:开销小,锁定粒度大,发生锁冲突概率高,并发度最低;不会出现死锁
- 行锁:开销大,加锁慢,会出现死锁,锁定粒度小,发生锁冲突的概率低,并发读高
- 页锁:开销和加锁速度介于表锁和行锁之间,会出现死锁,锁定粒度介于表锁和行锁之间,并发度一般
Hash索引和B+树区别是什么?如何选择
- B+树可以进行范围查询,Hash索引不能
- B+树支持联合索引的最左侧原则,Hash索引不支持
- B+树支持order by排序,Hash索引不支持
- Hash索引在等值查询上比B+树效率更高
- B+树使用like进行模糊查询的时候,like后面(比如%开头)的话可以起到优化作用,而Hash索引根本无法进行模糊查询
Mysql的内连接、做连接、右连接
- inner join:在两张表进行连接查询时,只保留两张表中完全匹配的结果集
- left join:在两张表进行连接查询时,会返回左表所有的行,即使在右表中没有匹配的记录
- right join:在两张表进行连接查询时,会返回右表所有的行,即使在坐标中没有匹配的记录
数据库三大范式
- 第一范式:数据表中的每个字段都不可拆分
- 第二范式:在第一范式基础上,分主键列完全依赖于主键,而不能是依赖于主键的一部分
- 第三范式:在满足第二范式的基础上,表中的非主键只依赖于主键、而不依赖于其他非主键
Mysql的binlog有几种录入格式
- statement:记录的是sql的原文。不需要记录每一行的变化,减少了binlog日志量,太高了性能
- row,不记录sql语句上下文相关关系,仅保存那条记录被修改。记录单元为每一行的改动,基本是可以全部记下来,但是由于很多批量操作会导致当量行的改动,因此这种模式的文件保存的信息太多。
- mixed:一种折中的方案,普通操作使用statement记录,当无法使用statement的时候使用row
百万级别或以上数据如何删除
覆盖索引、回表是什么
- 覆盖索引:查询列要被所建的索引覆盖,不必从数据表中读取,换取话说查询列要被所使用的的索引覆盖
- 回表:二级索引无法直接查询所有列的数据,所以通过二级索引查询到聚簇索引后,再查询到想到的数据,这种通过二级索引查询出来的过程,叫做回表
InnoDB锁
什么是死锁,怎么解决
死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象
死锁有四个必要条件:互斥条件、请求和保持条件、环路等待条件、不剥夺条件
- 如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以降低死锁机会
- 在同一个事务中,尽可能做到一次锁定所需要的的所有资源,减少死锁产生的概率
- 对于容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率
- 如果业务处理不好可以用分布式事务锁或者乐观锁
- 死锁与索引密不可分,解决索引问题,需要合理优化索引
什么是存储过程,优缺点是什么
存储过程
就是一些编译好了的sql语句,这些sql语句代码像一个方法一样实现一些功能(对单表或多表的增删改查),然后给这些代码块取一个名字,在用到这个功能的时候调用即可
优点
- 存储过程是一个预编译的代码块,执行效率比较高
- 存储过程在服务器端运行,减少客户端的压力
- 可以一定程度确保数据安全性
- 允许模块化程序设计,只需要创建一次过程,以后在程序中就可以调用该过程任意次,类似方法的复用
缺点
varchar(50)中50的含义
mysql中int(20)、char(20)、varchar(20)的区别
union与union all的区别
undo log 和redo log的区别
Tomcat
Tomcat的缺省端口是多少,这么去修改?
-
1
<Connector connectionTimeout="20000" port="8080" protocol="HTTP/1/1" redirectPort="8443" uriEncoding="utf-8"></Connector>
tomcat有哪几种Connector运行模式(优化)
-
tomcat使用线程来处理接收的每个请求,这个值表示tomcat可创建的最大线程数,默认值200,可以根据机器的性能和大小调整,一般可以在400-500.最大可以在800左右
-
tomcat初始化时创建的线程数,默认值4.如果当前没有空闲线程,且没有超过maxThreads,一次性创建的空闲线程数量,tomcat初始化创建的线程数量也由此值设置
-
一旦创建的线程超过这个值,tomcat就会关闭不在需要的socket线程,默认值50.线程数大致上用同时在线人数每秒用户操作次数系统平均操作时间来计算
-
tomcat将以JNI形式调用Apache,htto服务器的核心动态链接库来处理文件读取或网络传输操作,从而大大地提高tomcat对静态文件的处理性能
Tomcat有几种部署方式
- 直接把web项目放在webapps下,Tomcat会自动将其部署
-
在server.xml文件上配置
节点,设置相关属性即可 - 通过Catalina来进行配置,进入到config\Catalina\localhost文件下,创建一个xml文件,该文件的名字就是站点的名字。
Tomcat如何优化
-
优化连接配置,需要修改conf/server.xml文件,修改连接数,关闭客户端dns查询
- URIEncoding=”utf-8”:使得tomcat可以解析含有中文名的url
- maxSpareThreads:如果空闲状态的线程数多于设置的数目,则将这些线程终止,减少这个池中的线程总数
- minSpareThreads:最小备用线程数,tomcat启动时的初始化的线程数
- enableLookups:设置为关闭
- connectionTimeout:connectionTimeout为网络连接超时时间毫秒数
- maxThreads:tomcat使用线程来处理接收的每个请求,这个值表示Tomcat可创建的最大的线程数,即最大并发数
- acceptCount:当线程数达到maxThreads后,后续请求会被放入一个等待队列,这个acceptCount是这个对列的大小,如果这个对列也满了,就直接refuse connection
- maxProcessors 与 minProcessors :在java中线程是程序运行时的路径,是在一个程序中与其他控制线程无关的,能够独立运行的代码段,它们共享相同的地址空间。多线程帮助程序员写出cpu最大利用效率的高效程序,使空闲时间保持最低,从而接受更多的请求。通常windows是1000个左右,Linux是2000个左右
- userURIValidationHack:如果把 useURIValidationHack 设成”false”,可以减少它对一些 url的不必要的检查从而减省开销。
- enableLookups=”false”:为了消除DNS查询对性能的影响我们可以关闭DNS查询,把值改为false
- disableUploadTimeout:类似于Apache中的keeyalive一样
- compression=”on”:打开压缩功能
内存调优
内存方式的设置是在catalina.sh中,调整JAVA_OPTS变量既可,因为后面的启动参数会把JAVA_OPTS作为JVM的启动参数来处理
JAVA_OPTS=”$JAVA_OPTS -Xmx3500m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4”
-
-Xmn2g:设置年轻代大小为2G。整个堆大小=年轻代大小+老年代大小+持久代大小。持久代大小一般固定位64m,所以增大年轻代后,将会减少老年代大小,推荐配置为整个堆的3/8
-
-XX:SurvivorRatio=4:设置年轻代中Eden区与Surivior区的大小比值。设置为4,则两个survive区与一个eden区比值为2:4,一个survivor区占整个年轻代的1/6
-
-XX:MaxTenuringThreshold=0;设置垃圾最大年龄,如果设置为0的话,则年轻代对象不经过Survivor去,直接进入老年代
垃圾回收策略调优
垃圾回收的设置也是在 catalina.sh 中,调整 JAVA_OPTS 变量。
共享Session处理
-
目前用的比较多的是memcached,借助于memcached-session-manager来进行Tomcat的session管理
-
对于会话要求不太强(不涉及到计费,失败了允许重新请求)的场合,同一个session可以用Nginx或者apache交个同一个Tomcat处理。
Tomcat工作模式
Tomcat是一个jsp/servlet容器。其作为Servlet容器,有三种工作模式:独立的Servlet容器、进程内的Servlet容器和进程外的Servlet容器
进入Tomcat的请求可以根据Tomcat的工作模式分为如下两类:
Tomca作为应用程序服务器:请求来自前端的web服务器,这可能是Apache。Nginx等
JVM
https://www.processon.com/mindmap/629b77a17d9c08070f9854e7a
JVM组成
JVM由那些部分组成,运行流程是什么
- ClassLoader(类加载器)
- Runtime Data Area(运行时数据区,内存分区)
- Execution Engine(执行引擎)
- Native Method Library(本地库接口)
- 类加载器(ClassLoader)把Java代码转换为字节码
- 运行时数据区(Runtime Data Area)把字节码加载到内存中,而字节码文件只是JVM的一套指令集规范,并不能直接交给底层系统去执行,而是有执行引擎运行
- 执行引擎(Execution Engine)将字节码翻译为底层系统指令,再交由CPU执行去执行,此时需要调用其他语言的本地库接口(Native Method Library)来实现整个程序的功能
JVM运行时数据区
运行时数据区包含了堆、方法区、栈、本地方法栈、程序计数器这几部分,每个功能作用不一样。
- 堆解决的是对象实例存储的问题,垃圾回收器管理的主要区域。
- 方法区可以认为是堆的一部分,用于存储已被虚拟机加载的信息,常量、静态变量、即时编译器编译后的代码。
- 栈解决的是程序运行的问题,栈里面存的是栈帧,栈帧里面存的是局部变量表、操作数栈、动态链接、方法出口等信息。
- 本地方法栈与栈功能相同,本地方法栈执行的是本地方法,一个Java调用非Java代码的接口。
- 程序计数器(PC寄存器)程序计数器中存放的是当前线程所执行的字节码的行数。JVM工作时就是通过改变这个计数器的值来选取下一个需要执行的字节码指令。
程序计数器作用
java虚拟机对于多线程是通过线程轮流切换并且分配线程执行时间。在任何的一个时间点上,一个处理器只会处理执行一个线程。
如果当前被执行的这个线程它所分配的执行时间用完了【挂起】。处理器会切换到另外的一个线程上来进行执行。并且这个线程的执行时间用完了,接着处理器就会又来执行被挂起的这个线程。
这时候程序计数器就起到了关键作用,程序计数器在来回切换的线程中记录他上一次执行的行号,然后接着继续向下执行。
JAVA堆
Java中的堆术语线程共享的区域。主要用来保存 对象实例,数组 等,当堆中没有内存空间可分配给实例,也无法再扩展时,则抛出OutOfMemoryError异常。
- Young区被划分为三部分,Eden区(伊甸园区)和两个大小严格相同的Survivor区(幸存区),其中,Survivor区间中,某一时刻只有其中一个是被使用的,另外一个留做垃圾收集时复制对象用。在Eden区变满的时候, GC就会将存活的对象移到空闲的Survivor区间中,根据JVM的策略,在经过几次垃圾收集后,任然存活于Survivor的对象将被移动到Tenured区间(老年代)。
- Tenured区主要保存生命周期长的对象,一般是一些老的对象,当一些对象在Young复制转移一定的次数以后,对象就会被转移到Tenured区。
解释下方法区
与虚拟机栈类似。本地方法栈是为虚拟机 执行本地方法时提供服务的 。不需要进行GC。本地方法一般是由其他语言编写。
你听过直接内存吗
所以在 Java 8 中就把方法区的实现移到了本地内存中的元空间中,这样方法区就不受 JVM 的控制了,也就不会进行 GC,也因此提升了性能。
什么是虚拟机栈
堆栈的区别
- 栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的的。堆会GC垃圾回收,而栈不会。
- 栈内存是线程私有的,而堆内存是线程共有的。
-
两者异常错误不同,但如果栈内存或者堆内存不足都会抛出异常。
- 栈空间不足:java.lang.StackOverFlowError。
- 堆空间不足:java.lang.OutOfMemoryError。
类加载器
什么是类加载器,类加载器有哪些
JVM只会运行二进制文件,而类加载器(ClassLoader)的主要作用就是将 字节码文件加载到JVM中 ,从而让Java程序能够启动起来。
- 启动类加载器(BootStrap ClassLoader):其是由C++编写实现。用于加载JAVA_HOME/jre/lib目录下的类库
- 扩展类加载器(ExtClassLoader):该类是ClassLoader的子类,主要加载JAVA_HOME/jre/lib/ext目录中的类库。
- 应用类加载器(AppClassLoader):该类是ClassLoader的子类,主要用于加载classPath下的类,也就是加载开发者自己编写的Java类。
- 自定义类加载器:开发者自定义类继承ClassLoader,实现自定义类加载规则。
类装载的执行过程
类从加载到虚拟机中开始,直到卸载为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用和卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)
双亲委派机制
采用原因
垃圾回收
Java垃圾回收机制
为了让程序员更专注于代码的实现,而不用过多的考虑内存释放的问题,所以,在Java语言中,有了自动的垃圾回收机制,也就是我们熟悉的GC(Garbage Collection)。
有了垃圾回收机制后,程序员只需要关心内存的申请即可,内存的释放由系统自动识别完成。
在进行垃圾回收时,不同的对象引用类型,GC会采用不同的回收时机
强引用、软引用、弱引用、虚引用的区别
强引用
最为普通的引用方式,表示一个对象处于 有用且必须 的状态,如果一个对象具有强引用,则GC并不会回收它。即便堆中内存不足了,宁可出现OOM,也不会对其进行回收
软引用
弱引用
虚引用
表示一个对象处于 无用 的状态。在任何时候都有可能被垃圾回收。虚引用的使用必须和引用队列Reference Queue联合使用
对象什么时候可以被垃圾器回收
如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收。
如果要定位什么是垃圾,有两种方式来确定,第一个是引用计数法,第二个是可达性分析算法
引用计数法
一个对象被引用了一次,在当前的对象头上递增一次引用次数,如果这个对象的引用次数为0,代表这个对象可回收,当对象间出现了循环引用的话,则引用计数法就会失效。
可达性分析法
会存在一个根节点【GC Roots】,引出它下面指向的下一个节点,再以下一个节点节点开始找出它下面的节点,依次往下类推。直到所有的节点全部遍历完毕。
判断某对象是否与根对象有直接或间接的引用,如果没有被引用,则可以当做垃圾回收
可以当做根对象的有以下几种(肯定不能当做垃圾回收的对象)
JVM 垃圾回收算法
一共有四种,分别是标记清除算法、复制算法、标记整理算法、分代回收
标记清除算法
复制算法
复制算法的核心就是,将原有的内存空间一分为二,每次只用其中的一块,在垃圾回收时,将正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾回收。
如果内存中的垃圾对象较多,需要复制的对象就较少,这种情况下适合使用该方式并且效率比较高,反之,则不适合。
分配的2块内存空间,在同一时刻,只能使用一半,内存使用率较低
标记整理算法
分代收集算法
在java8时,堆被分为了两份:新生代和老年代,它们默认空间占用比例是1:2
对于新生代,内部又被分为了三个区域。Eden区,S0区,S1区默认空间占用比例是8:1:1
- 当创建一个对象的时候,那么这个对象会被分配在新生代的Eden区。当Eden区要满了时候,触发YoungGC。
- 当进行YoungGC后,此时在Eden区存活的对象被移动到S0区,并且 当前对象的年龄会加1 ,清空Eden区。
- 当再一次触发YoungGC的时候,会把Eden区中存活下来的对象和S0中的对象,移动到S1区中,这些对象的年龄会加1,清空Eden区和S0区。
- 当再一次触发YoungGC的时候,会把Eden区中存活下来的对象和S1中的对象,移动到S0区中,这些对象的年龄会加1,清空Eden区和S1区。
- 对象的年龄达到了某一个限定的值( 默认15岁 ),那么这个对象就会进入到老年代中。
- 当然也有特殊情况,如果进入Eden区的是一个大对象,在触发YoungGC的时候,会直接存放到老年代
- 当老年代满了之后, 触发FullGC 。 FullGC同时回收新生代和老年代 ,当前只会存在一个FullGC的线程进行执行,其他的线程全部会被挂起。 我们在程序中要尽量避免FullGC的出现。
新生代、老年代、永久代的区别
JVM垃圾回收器
在jvm中,实现了多种垃圾收集器,包括:串行垃圾收集器、并行垃圾收集器(JDK8默认)、CMS(并发)垃圾收集器、G1垃圾收集器(JDK9默认)
串行垃圾收集器
Serial和Serial Old串行垃圾收集器,是指使用单线程进行垃圾回收,堆内存较小,适合个人电脑
垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成
并行垃圾收集器
Parallel New和Parallel Old是一个并行垃圾回收器,JDK8默认使用此垃圾回收器
垃圾回收时,多个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成
CMS(并发)垃圾收集器
MS全称Concurrent Mark Sweep。是一款并发的、使用标记清除算法的垃圾回收器。
该回收器是针对老年代垃圾进行回收的,是一款以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。
最后需要重新标记的原因是:有可能会出现新的关联节点,所以需要重新标记
G1垃圾回收器
- 应用于新生代和老年代,在jdk9之后默认使用G1
- 划分成多个区域,每个区域都可以充当eden,survivor,old,humongous,其中humongous专为大对象准备
- 采用复制算法
- 响应时间与吞吐量兼顾
-
分成三个阶段
- 新生代回收
- 并发标记
- 混合收集
- 如果并发失败(即回收速度赶不上创建新对象的速度),就会出发Full GC
Young Collection(年轻代垃圾回收)
Young Collection + Concurrent Mark(年轻代垃圾回收+并发标记)
-
这些都完成后就知道了老年代有哪些存活对象,随后进行混合收集阶段。此时不会对所有老年代区域进行回收,而是根据暂停时间目标优先回收价值高(存活对象少)的区域(这也是Gabage First名称的由来)
Mixed Collection (混合垃圾回收)
复制完成,内存得到释放。进入下一轮的新生代回收、并发标记、混合收集
其中H叫做巨型对象,如果对象非常大,会开辟一块连续的空间存储巨型对象
Minor GC、Major GC、Full GC是什么
JVM调优
JVM 调优的参数可以在哪里设置参数值
我们当时的项目是springboot项目,可以在项目启动的时候,java -jar中加入参数就行了
JVM 调优的参数都有哪些
调试 JVM都用了哪些工具
产生了java内存泄露,你说一下你的排查思路
- 可以通过jmap指定打印他的内存快照 dump文件,不过有的情况打印不了,我们会设置vm参数让程序自动生成dump文件
- 可以通过工具去分析 dump文件,jdk自带的VisualVM就可以分析
- 通过查看堆信息的情况,可以大概定位内存溢出是哪行代码出了问题
- 找到对应的代码,通过阅读上下文的情况,进行修复即可
服务器CPU持续飙高,你的排查方案与思路?
- 可以使用使用top命令查看占用cpu的情况
- 通过top命令查看后,可以查看是哪一个进程占用cpu较高,记录这个进程id
- 可以通过ps 查看当前进程中的线程信息,看看哪个线程的cpu占用较高
- 可以jstack命令打印进行的id,找到这个线程,就可以进一步定位问题代码的行号
设计模式
工厂模式
设计一个咖啡类(Coffee),并定义两个子类美式咖啡和拿铁咖啡,在设计一个咖啡店类,咖啡店具有点咖啡的功能。
-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18/**
* @Title: Coffee
* @Author huan
* @Package com.zhuixun.test.designPatterns
* @Date 2024/3/11 9:54
* @description: 咖啡接口
*/
public interface Coffee {
//获取咖啡名称
String getName();
//添加牛奶方法
void addMilk();
//添加糖
void addSugar();
} -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55package com.zhuixun.test.designPatterns;
/**
* @Title: LatteCoffee
* @Author huan
* @Package com.zhuixun.test.designPatterns
* @Date 2024/3/11 10:28
* @description: 拿铁咖啡类
*/
public class LatteCoffee implements Coffee{
@Override
public String getName() {
return "拿铁咖啡";
}
@Override
public void addMilk() {
System.out.println("拿铁咖啡添加牛奶");
}
@Override
public void addSugar() {
System.out.println("拿铁咖啡添加糖");
}
}
package com.zhuixun.test.designPatterns;
/**
* @Title: AmericanCoffee
* @Author huan
* @Package com.zhuixun.test.designPatterns
* @Date 2024/3/11 9:53
* @description: 美式咖啡类
*/
public class AmericanCoffee implements Coffee{
@Override
public String getName() {
return "美式咖啡";
}
@Override
public void addMilk() {
System.out.println("美式咖啡添加牛奶");
}
@Override
public void addSugar() {
System.out.println("美式咖啡添加糖");
}
} -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34package com.zhuixun.test.designPatterns;
/**
* @Title: CoffeeStore
* @Author huan
* @Package com.zhuixun.test.designPatterns
* @Date 2024/3/11 10:29
* @description: 咖啡店类
*/
public class CoffeeStore {
public static void main(String[] args) {
createCoffee("拿铁");
}
public static Coffee createCoffee(String coffeeType){
Coffee coffee = null;
if (coffeeType.equals("拿铁")){
coffee = new LatteCoffee();
String name = coffee.getName();
System.out.println("创建"+name+"成功");
}else if(coffeeType.equals("美式")){
coffee = new AmericanCoffee();
String name = coffee.getName();
System.out.println("创建"+name+"成功");
}
//添加配料
coffee.addMilk();
coffee.addSugar();
return coffee;
}
}
在java中,万物皆对象,这些对象都需要创建,如果创建的时候,直接new该对象,就会对该对象耦合严重,假如我们要更换对象,所有new对象的地方都需要修改一遍, 这显然违背了软件设计的开闭原则
[^开闭原则:对扩展开放,对修改关闭。在程序需要进行拓展的时候,不能去修原有代码,实现一个热插拔的效果,简而言之,是为了使程序的扩展性好,易于维护和升级。]:
如果我们使用工厂来生产创建对象,我们就只和工厂打交道就可以了,彻底和对象解耦,如果需要更换对象,直接在工厂里面更换该对象即可,达到了与对象解耦的目的,所以说工厂模式最大的优点是 解耦
简单工厂模式
结构
实现
1 |
|
1 |
|
后期如果再加新品种的咖啡,我们势必要修改CoffeeFactory的代码,违反了开闭原则。
优点
缺点
工厂方法模式
定义一个用于创建对象的接口,让子类决定实例化哪个产品类对象。工厂方法使一个产品类的实例化延迟到其他工厂的子类
结构
实现
1 |
|
1 |
|
1 |
|
从以上的编写的代码可以看到,要增加产品类时也要相应地增加工厂类,不需要修改工厂类的代码了,这样就解决了简单工厂模式的缺点。
工厂方法模式是简单工厂模式的进一步抽象。由于使用了多态性,工厂方法模式保持了简单工厂模式的优点,而且克服了它的缺点。
优点
缺点
抽象工厂模式
前面介绍的工厂方法模式中考虑的是一类产品的生产,如畜牧场只养动物、电视机厂只生产电视机等
同种类产品称为同等级产品,也就是说:工厂方法模式只考虑生产同等级的产品。
在现实生活中许多工厂是综合型的工厂,能生产多等级(种类) 的产品,如电器厂既生产电视机又生产洗衣机或空调,大学既有软件专业又有生物专业等。
抽象工厂模式将考虑多等级产品的生产,将同一个具体工厂所生产的位于不同等级的一组产品称为一个产品族,下图所示
概念
是一种为访问类提供一个创建一组组或相关依赖对象的接口,且访问类无需指定所要产品的具体类就能得到同族的不同等级的产品的模式结构
抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产一个等级的产品,而抽象工厂模式可以生产多个等级的产品
结构
实现
要是按照工厂方法模式,需要定义提拉米苏类、抹茶慕斯类、提拉米苏工厂、抹茶慕斯工厂、甜点工厂类,很容易发生爆炸情况。
优点
当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象
缺点
当产品族中需要增加一个新的产品时,所有的工厂类都需要进行修改。
使用场景
- 当需要创建的对象是一系列相互关联或相互依赖的产品族时,如电器工厂中的电视机、洗衣机、空调等
- 系统中有多个产品族,但每次只使用其中的某一族产品。如有人只喜欢穿某一个品牌的衣服和鞋。
- 系统中提供了产品的类库,且所有产品的接口相同,客户端不依赖产品实例的创建细节和内部结构。
- 输入法换皮肤,一整套一起换。生成不同操作系统的程序。
策略模式
我们去旅游选择出行模式有很多种,可以骑自行车、可以坐汽车、可以坐火车、可以坐飞机。
结构
- 抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口。
- 具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。
- 环境(Context)类:持有一个策略类的引用,最终给客户端调用。
案例实现
一家百货公司在定年度的促销活动。针对不同的节日(春节、中秋节、圣诞节)推出不同的促销活动,由促销员将促销活动展示给客户。类图如下
1 |
|
定义具体策略角色(Concrete Strategy):每个节日具体的促销活动
1 |
|
定义环境角色(Context):用于连接上下文,即把促销活动推销给客户,这里可以理解为销售员
1 |
|
综合案例
一般自己的代码都是使用前端传递过来的登录类型去if else判断去走对应的登录方式,这样会出现一个问题,如果业务发生变更,需要新增一个登录方式,这个时候就需要修改业务层代码,违反了开闭原则
-
改造之后,不在service中写业务逻辑,让service调用工厂,然后通过service传递不同的参数来获取不同的登录策略(登录方式)
-
-
1
2
3
4
5
6
7
8
9
10
11
12/**
* 抽象策略类
*/
public interface UserGranter{
/**
* 获取数据
* @param loginReq 传入的参数
* @return map值
*/
LoginResp login(LoginReq loginReq);
} -
具体的策略:AccountGranter、SmsGranter、WeChatGranter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48/**
* 策略:账号登录
**/
@Component
public class AccountGranter implements UserGranter{
@Override
public LoginResp login(LoginReq loginReq) {
System.out.println("登录方式为账号登录" + loginReq);
// TODO
// 执行业务操作
return new LoginResp();
}
}
/**
* 策略:短信登录
*/
@Component
public class SmsGranter implements UserGranter{
@Override
public LoginResp login(LoginReq loginReq) {
System.out.println("登录方式为短信登录" + loginReq);
// TODO
// 执行业务操作
return new LoginResp();
}
}
/**
* 策略:微信登录
*/
@Component
public class WeChatGranter implements UserGranter{
@Override
public LoginResp login(LoginReq loginReq) {
System.out.println("登录方式为微信登录" + loginReq);
// TODO
// 执行业务操作
return new LoginResp();
}
} -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42/**
* 操作策略的上下文环境类 工具类
* 将策略整合起来 方便管理
*/
@Component
public class UserLoginFactory implements ApplicationContextAware {
private static Map<String, UserGranter> granterPool = new ConcurrentHashMap<>();
@Autowired
private LoginTypeConfig loginTypeConfig;
/**
* 从配置文件中读取策略信息存储到map中
* {
* account:accountGranter,
* sms:smsGranter,
* we_chat:weChatGranter
* }
*
* @param applicationContext
* @throws BeansException
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
loginTypeConfig.getTypes().forEach((k, y) -> {
granterPool.put(k, (UserGranter) applicationContext.getBean(y));
});
}
/**
* 对外提供获取具体策略
*
* @param grantType 用户的登录方式,需要跟配置文件中匹配
* @return 具体策略
*/
public UserGranter getGranter(String grantType) {
UserGranter tokenGranter = granterPool.get(grantType);
return tokenGranter;
}
} -
1
2
3
4
5login:
types:
account: accountGranter
sms: smsGranter
we_chat: weChatGranter -
1
2
3
4
5
6
7
8
9
10Getter
@Setter
@Configuration
@ConfigurationProperties(prefix = "login")
public class LoginTypeConfig {
private Map<String,String> types;
} -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18@Service
public class UserService {
@Autowired
private UserLoginFactory factory;
public LoginResp login(LoginReq loginReq){
UserGranter granter = factory.getGranter(loginReq.getType());
if(granter == null){
LoginResp loginResp = new LoginResp();
loginResp.setSuccess(false);
return loginResp;
}
LoginResp loginResp = granter.login(loginReq);
return loginResp;
}
}
-
举一反三
其实像这样的需求,在日常开发中非常常见,场景有很多,以下的情景都可以使用工厂模式+策略模式解决比如:
-
订单的支付策略
- 支付宝支付
- 微信支付
- 银行卡支付
- 现金支付
-
解析不同类型excel
- xls格式
- xlsx格式
-
打折促销
- 满300元9折
- 满500元8折
- 满1000元7折
-
物流运费阶梯计算
- 5kg以下
- 5kg-10kg
- 10kg-20kg
- 20kg以上
一句话总结: 只要代码中有冗长的 if-else 或 switch 分支判断都可以采用策略模式优化
责任链模式
概述
在现实生活中,常常会出现这样的事例:一个请求有多个对象可以处理,但每个对象的处理条件或权限不同。
这样的例子还有很多,如找领导出差报销、生活中的“击鼓传花”游戏等。
定义
又名职责链模式,为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。
比较常见的springmvc中的拦截器,web开发中的filter过滤器
结构
- 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
- 具体处理者(Concrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
- 客户类(Client)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。
案例实现
-
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17/**
* 抽象处理者
*/
public abstract class Handler {
protected Handler handler;
public void setNext(Handler handler) {
this.handler = handler;
}
/**
* 处理过程
* 需要子类进行实现
*/
public abstract void process(OrderInfo order);
} -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33import java.math.BigDecimal;
public class OrderInfo {
private String productId;
private String userId;
private BigDecimal amount;
public String getProductId() {
return productId;
}
public void setProductId(String productId) {
this.productId = productId;
}
public String getUserId() {
return userId;
}
public void setUserId(String userId) {
this.userId = userId;
}
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
} -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48/**
* 订单校验
*/
public class OrderValidition extends Handler {
@Override
public void process(OrderInfo order) {
System.out.println("校验订单基本信息");
//校验
handler.process(order);
}
}
/**
* 补充订单信息
*/
public class OrderFill extends Handler {
@Override
public void process(OrderInfo order) {
System.out.println("补充订单信息");
handler.process(order);
}
}
/**
* 计算金额
*/
public class OrderAmountCalcuate extends Handler {
@Override
public void process(OrderInfo order) {
System.out.println("计算金额-优惠券、VIP、活动打折");
handler.process(order);
}
}
/**
* 订单入库
*/
public class OrderCreate extends Handler {
@Override
public void process(OrderInfo order) {
System.out.println("订单入库");
}
} -
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22public class Application {
public static void main(String[] args) {
//检验订单
Handler orderValidition = new OrderValidition();
//补充订单信息
Handler orderFill = new OrderFill();
//订单算价
Handler orderAmountCalcuate = new OrderAmountCalcuate();
//订单落库
Handler orderCreate = new OrderCreate();
//设置责任链路
orderValidition.setNext(orderFill);
orderFill.setNext(orderAmountCalcuate);
orderAmountCalcuate.setNext(orderCreate);
//开始执行
orderValidition.process(new OrderInfo());
}
}
优点
缺点
- 不能保证每个请求一定被处理。由于一个请求没有明确的接收者,所以不能保证它一定会被处理,该请求可能一直传到链的末端都得不到处理。
- 对比较长的职责链,请求的处理可能涉及多个处理对象,系统性能将受到一定影响。
- 职责链建立的合理性要靠客户端来保证,增加了客户端的复杂性,可能会由于职责链的错误设置而导致系统出错,如可能会造成循环调用。
举一反三
常见技术场景
单点登录
概述
单点登录的英文名叫做:Single Sign On(简称 SSO ),只需要登录一次,就可以访问所有信任的应用系统
在 以前 的时候,一般我们就 单系统 ,所有的功能都在同一个系统上。
后来,我们为了 合理利用资源和降低耦合性 ,于是把单系统 拆分 成多个子系统。
多系统即可能有多个Tomcat,而Session是依赖当前系统的Tomcat,所以系统A的Session和系统B的Session是 不共享 的。
JWT解决单点登录
回答要点
权限认证
概述
后台的管理系统,更注重权限控制,最常见的就是RBAC模型来指导实现权限
RBAC(Role-Based Access Control)基于角色的访问控制
RBAC权限模型
流程:张三登录系统—> 查询张三拥有的角色列表—>再根据角色查询拥有的权限
在实际的开发中,也会使用权限框架完成权限功能的实现,并且设置多种粒度,常见的框架有:
上传数据安全如何控制
这里的安全性,主要说的是,浏览器访问后台,需要经过网络传输,有可能会出现安全的问题
解决方案:使用非对称加密(或对称加密),给前端一个公钥让他把数据加密后传到后台,后台负责解密后处理数据
对称加密
常见的对称加密算法有
:
AES、DES、3DES、Blowfish、IDEA、RC4、RC5、RC6、HS256
优点
对称加密算法的优点是算法公开、计算量小、加密速度快、加密效率高。
缺点
非对称加密
两个密钥:公开密钥(publickey)和私有密钥,公有密钥加密,私有密钥解密
一般用于签名和认证。私钥服务器保存, 用来加密, 公钥客户拿着用于对于令牌或者签名的解密或者校验使用.
常见的非对称加密算法有: RSA、DSA(数字签名用)、ECC(移动设备用)、RS256 (采用SHA-256 的 RSA 签名
解释: 同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端.
优点
缺点
非对称加密的缺点是加密和解密花费时间长、速度慢,只适合对少量数据进行加密。
回答要点
使用非对称加密(或对称加密),给前端一个公钥让他把数据加密后传到后台,后台解密后处理数据
项目遇到那些棘手问题
设计模式
线上bug
调优
组件封装
项目中日志怎么采集
日志是定位系统问题的重要手段,可以根据日志信息快速定位系统中的问题
日志采集方式
ELK基本架构
ELK即Elasticsearch、Logstash和Kibana三个开源软件的缩写
-
Elasticsearch
Elasticsearch 全文搜索和分析引擎,对大容量的数据进行接近实时的存储、搜索和分析操作。 -
Logstash
Logstash是一个数据收集引擎,它可以动态的从各种数据源搜集数据,并对数据进行过滤、分析和统一格式等操作,并将输出结果存储到指定位置上 -
Kibana
Kibana是一个数据分析和可视化平台,通常与Elasticsearch配合使用,用于对其中的数据进行搜索、分析,并且以统计图标的形式展示。
参考回答
linux查看日志的命令
-
实时监控某一个日志文件的变化:tail -f xx.log;实时监控日志最后100行日志: tail –n 100 -f xx.log
-
sed -n ‘/2023-05-18 14:22:31.070/,/ 2023-05-18 14:27:14.158/p’xx.log
生产问题怎么排查
- 先分析日志,通常在业务中都会有日志的记录,或者查看系统日志,或者查看日志文件,然后定位问题
- 远程debug(通常公司的正式环境(生产环境)是不允许远程debug的。一般远程debug都是公司的测试环境,方便调试代码)
远程debug配置
-
远程代码需要配置启动参数,把项目打包放到服务器后启动项目的参数
1
java -jar -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 project-1.0-SNAPSHOT.jar