该博文主要收录了本人面试过程中频率较高的面试题,在整理归纳后贡献出来,其中基础模块没有做过多的整理,一般在面试宝典都会有最基础的面试的整理,并且有些模块需要读者自己收集资料。
基础
1. == 和 equals 的区别是什么?
2. 深拷贝和浅拷贝
深拷贝和浅拷贝就是指对象的拷贝,一个对象中存在两种类型的属性,一种是基本数据类型,一种是实例对象的引用。
- 浅拷贝是指,只会拷贝基本数据类型的值,以及实例对象的引用地址,并不会复制一份引用地址所指向的对象,也就是浅拷贝出来的对象,内部的类属性指向的是同一个对象
- 深拷贝是指,既会拷贝基本数据类型的值,也会针对实例对象的引用地址所指向的对象进行复制,深拷贝出来的对象,内部的属性指向的不是同一个对象
3. Java反射机制的原理
4. 重写和重载的区别
5. 抽象类和接口的区别
6. io流
7. String,StringBuilder,StringBuffer的区别
网上太多关于基础的面试题,我这边不多整理
集合
1. ArrayList和LinkedList的区别
- 对ArrayList和LinkedList而言,在列表末尾增加一个元素所花的开销都是固定的。对ArrayList而言,主要是在内部数组中增加一项,指向所添加的元素,偶尔可能会导致对数组重新进行分配;而对LinkedList而言,这个开销是统一的,分配一个内部Entry对象。
- 在ArrayList的中间插入或删除一个元素意味着这个列表中剩余的元素都会被移动;而在LinkedList的中间插入或删除一个元素的开销是固定的。
- LinkedList不支持高效的随机元素访问。
- ArrayList的空间浪费主要体现在在list列表的结尾预留一定的容量空间,而LinkedList的空间花费则体现在它的每一个元素都需要消耗相当的空间
2. ArrayList的扩容机制并且手写
-
创建新的数组
-
复制到新的数组
-
1.5倍扩容
-
无参初始值0, 初始值10
-
copyof()方法
3. CopyOnWriteArrayList的底层原理是怎样的
-
CopyOnWriteArrayList内部也是用过数组来实现的,在向CopyOnWriteArrayList添加元素时,会复制一个新的数组,写操作在新数组上进行,读操作在原数组上进行
-
并且,写操作会加锁,防止出现并发写入丢失数据的问题
-
写操作结束之后会把原数组指向新数组
-
CopyOnWriteArrayList允许在写操作时来读取数据,大大提高了读的性能,因此适合读多写少的应用场景,但是CopyOnWriteArrayList会比较占内存,同时可能读到的数据不是实时最新的数据,所以不适合实时性要求很高的场景
4. HashMap原理
1.7和1.8的区别
1.7
数组+链表 O(n)
1.8
数组+链表+红黑树 O(log(n))
5. HashMap的put原理
6. HashMap的扩容
1.7版本
-
先生成新数组
-
遍历老数组中的每个位置上的链表上的每个元素
-
取每个元素的key,并基于新数组长度,计算出每个元素在新数组中的下标
-
将元素添加到新数组中去
-
所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
1.8版本
-
先生成新数组
-
遍历老数组中的每个位置上的链表或红黑树
-
如果是链表,则直接将链表中的每个元素重新计算下标,并添加到新数组中去
-
如果是红黑树,则先遍历红黑树,先计算出红黑树中每个元素对应在新数组中的下标位置
(a)统计每个下标位置的元素个数
(b)如果该位置下的元素个数超过了8,则生成一个新的红黑树,并将根节点的添加到新数组的对应位置
(c)如果该位置下的元素个数没有超过8,那么则生成一个链表,并将链表的头节点添加到新数组的对应位置
-
所有元素转移完了之后,将新数组赋值给HashMap对象的table属性
7. HashMap1.7扩容死循环的问题
链接: pan.baidu.com/s/11TM3FDYZ… 提取码: plj1
8. HashMap线程不安全的问题
1.7
扩容链表循环
1.8
Java8 中的 HashMap 多线程情况下可能出错的一个例子。
HashMap中的 size 表示总的map存储的node的个数。在每次进行put方法的的最后。存在这样的代码进行累加
if (++size > threshold)
resize();
因为 ++size 并不是原子操作,在多个线程都执行这行代码,最后的结果大概率时不正确的。会存在丢失数据的情况。
9. ConcurrentHashMap原理
JDK1.7版本: 容器中有多把锁,每一把锁锁一段数据,这样在多线程访问时不同段的数据时,就不会存在锁竞争了,这 样便可以有效地提高并发效率。这就是ConcurrentHashMap所采用的"分段锁"思想
1.8中放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现
JDK1.8版本:做了2点修改
-
取消segments字段,直接采用transient volatile HashEntry<K,V>[] table保存数据,采用table数组元素作为锁,从而实现了对每一行数据进行加锁,并发控制使用Synchronized和CAS来操作
-
将原先table数组+单向链表的数据结构,变更为table数组+单向链表+红黑树的结构.
ConcurrentHashMap保证线程安全主要有三个地方:
- 一、使用volatile保证当Node中的值变化时对于其他线程是可见的
- 二、使用table数组的头结点作为synchronized的锁来保证写操作的安全
- 三、当头结点为null时,使用CAS操作来保证数据能正确的写入。
10. ConcurrentHashMap扩容
1.7版本
- 1.7版本的ConcurrentHashMap是基于Segment分段实现的
- 每个Segment相对于一个小型的HashMap
- 每个Segment内部会进行扩容,和HashMap的扩容逻辑类似
- 先生成新的数组,然后转移元素到新数组中
- 扩容的判断也是每个Segment内部单独判断的,判断是否超过阈值
1.8版本
- 1.8版本的ConcurrentHashMap不再基于Segment实现
- 当某个线程进行put时,如果发现ConcurrentHashMap正在进行扩容那么该线程一起进行扩容
- 如果某个线程put时,发现没有正在进行扩容,则将key-value添加到ConcurrentHashMap中,然后判断是否超过阈值,超过了则进行扩容
- ConcurrentHashMap是支持多个线程同时扩容的
- 扩容之前也先生成一个新的数组
- 在转移元素时,先将原数组分组,将每组分给不同的线程来进行元素的转移,每个线程负责一组或多组的元素转移工作
多线程并发
1. start()和run()的区别
start()方法被用来启动新创建的线程,而且start()内部调用了run()方法;
当你调用run()方法的时候,只会是在原来的线程中调用,没有新的线程启动;
start()方法才会启动新的线程。
2. wait()和sleep()的区别
-
对于 sleep()方法,我们首先要知道该方法是属于 Thread 类中的。而 wait()方法,则是属于Object 类中的。
-
sleep()方法导致了程序暂停执行指定的时间,让出 cpu 该其他线程,但是他的监控状态依然 保持者,当指定的时间到了又会自动恢复运行状态。
-
在调用 sleep()方法的过程中,线程不会释放对象锁。
-
而当调用 wait()方法的时候,线程会放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象调用 notify()方法后本线程才进入对象锁定池准备获取对象锁进入运行状态。
3. 创建线程的方式
1)继承Thread类创建线程
2)实现Runnable接口创建线程
3)使用Callable和Future创建线程(有返回值)
4)使用线程池例如用Executor框架
4. 线程池的原理
线程池内部是通过队列+线程实现的,当我们利用线程池执行任务时:
- 如果此时线程池中的线程数量小于corePoolSize,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。
- 如果此时线程池中的线程数量等于corePoolSize,但是缓冲队列workQueue未满,那么任务被放入缓冲队列。
- 如果此时线程池中的线程数量大于等于corePoolSize,缓冲队列workQueue满,并且线程池中的数量小于maximumPoolSize,建新的线程来处理被添加的任务。
- 如果此时线程池中的线程数量大于corePoolSize,缓冲队列workQueue满,并且线程池中的数量等于maximumPoolSize,那么通过 handler所指定的策略来处理此任务。
- 当线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止。这样,线程池可以动态的调整池中的线程数
5. 线程池的好处
线程池提供了一种限制和管理资源(包括执行一个任务)。 每个线程池还维护一些基本统计信息,例如已完成任务的数量。
这里借用《Java并发编程的艺术》提到的来说一下使用线程池的好处:
- 降低资源消耗。 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。 当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性, 使用线程池可以进行统一的分配,调优和监控。
6. 线程池excute / addworker流程
借鉴博文:blog.csdn.net/Double2hao/…
addWorker总结
由源码解析得,addWorker的几种调用场景如下:
- addWorker(command, true)
当线程数小于corePoolSize时,创建核心线程并且运行task。
- addWorker(command, false)
当核心线程数已满,阻塞队列已满,并且线程数小于maximumPoolSize时,创建非核心线程并且运行task。
- addWorker(null, false)
如果工作线程为0是,创建一个核心线程但是不运行task。(主要是避免工作队列中还有任务,但是工作线程为0,导致工作队列中的任务一直没有执行)
execute总结
-
如果正在工作线程小于corePoolSize,创建核心线程并且运行task。
-
如果正在工作线程等于corePoolSize,会尝试将task加入工作队列。
(加入队列之后,会再次确认下状态,如果线程池已经关闭了,那么会移除这个任务并执行拒绝策略,如果所有的工作线程正好都运行完了,那么会再创建一个,避免工作队列中的任务没有线程去执行)
- 如果工作线程等于corePoolSize,并且工作队列已满。会创建非工作线程来执行这个任务,如果执行失败,即线程数已达maximumPoolSize,那么会执行拒绝策略。
7. 有哪些线程池以及创建线程池的方式
阿里巴巴规范:
FixedThreadPool 和 SingleThreadPool : 允许的请求队列长度为 Integer.MAX_VALUE ,可能会堆积大量的请求,从而导致 OOM 。
这两个线程池是线程池大小是固定的。SingleThreadPool是单个线程的线程池。FixedThreadPool在应对平稳流量的时候,能有效的处理,缺点就是可能无法应付突发性大流量。
两个方法,都通过了 LinkedBlockingQueue来接收来不及处理的任务。关键点就在这个队列里,默认的构造器容量是Integer.MAX_VALUE。
那就是说,当流量突然变得非常大时,线程池满,等候队列变得非常庞大,内存和CPU都告急,这样无疑对服务器造成非常大的压力。
CachedThreadPool 和 ScheduledThreadPool : 允许的创建线程数量为 Integer.MAX_VALUE ,可能会创建大量的线程,从而导致 OOM 。
从工厂方法可以看到,这两种线程池,线程池大小是不固定的,虽然newScheduledThreadPool传如一个线程数,但是这个数字只是核心线程数,可能还会扩容,直至Integer.MAX_VALUE。而他们使用的队列是SynchronousQueue和DelayedWorkQueue。
8. 线程池参数配置,一般创建多少个线程合适(io密集型与计算密集型)
参数
1.corePoolSize:核心线程数大小:不管它们创建以后是不是空闲的。线程池需要保持 corePoolSize 数量的线程,除非设置了 allowCoreThreadTimeOut。
2.maximumPoolSize:最大线程数:线程池中最多允许创建 maximumPoolSize 个线程。
3.keepAliveTime:存活时间:如果经过 keepAliveTime 时间后,超过核心线程数的线程还没有接受到新的任务,那就回收。
4.unit:keepAliveTime 的时间单位。
5.workQueue:存放待执行任务的队列:当提交的任务数超过核心线程数大小后,再提交的任务就存放在这里。它仅仅用来存放被 execute 方法提交的 Runnable 任务。所以这里就不要翻译为工作队列了,好吗?不要自己给自己挖坑。
6.threadFactory:线程工程:用来创建线程工厂。比如这里面可以自定义线程名称,当进行虚拟机栈分析时,看着名字就知道这个线程是哪里来的,不会懵逼。
7.handler :拒绝策略:当队列里面放满了任务、最大线程数的线程都在工作时,这时继续提交的任务线程池就处理不了,应该执行怎么样的拒绝策略。
拒绝策略
(1)AbortPolicy
ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
这是线程池默认的拒绝策略,在任务不能再提交的时候,抛出异常,及时反馈程序运行状态。如果是比较关键的业务,推荐使用此拒绝策略,这样子在系统不能承载更大的并发量的时候,能够及时的通过异常发现。
(2)DiscardPolicy
ThreadPoolExecutor.DiscardPolicy:丢弃任务,但是不抛出异常。如果线程队列已满,则后续提交的任务都会被丢弃,且是静默丢弃。
使用此策略,可能会使我们无法发现系统的异常状态。建议是一些无关紧要的业务采用此策略。例如,本人的博客网站统计阅读量就是采用的这种拒绝策略。
(3)DiscardOldestPolicy
ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
此拒绝策略,是一种喜新厌旧的拒绝策略。是否要采用此种拒绝策略,还得根据实际业务是否允许丢弃老任务来认真衡量。
(4)CallerRunsPolicy
ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
如果任务被拒绝了,则由调用线程(提交任务的线程)直接执行此任务
io密集型与计算密集型
借鉴博文:segmentfault.com/a/119000002…
9. synchronized 关键字的作用
修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁
修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁 。 也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员( static表明这是该类的一个静态资源,不管new了多少个对象,只有一份,所以对该类的所有对象都加了锁)。所以如果一个线程A调用一个实 例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁 。
修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 和 synchronized 方 法一样,synchronized(this)代码块也是锁定当前对象的。synchronized 关键字加到 static 静态方法和synchronized(class)代码块上都是是给 Class 类上锁。这里再提一下:synchronized关键字加到非 static 静态 方法上是给对象实例上锁。另外需要注意的是:尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓冲功能!
10. Sychronized的偏向锁、轻量级锁、重量级锁
- 偏向锁:在锁对象的对象头中记录一下当前获取到该锁的线程ID,该线程下次如果又来获取该锁就可以直接获取到了
- 轻量级锁:由偏向锁升级而来,当一个线程获取到锁后,此时这把锁是偏向锁,此时如果有第二个线程来竞争锁,偏向锁就会升级为轻量级锁,之所以叫轻量级锁,是为了和重量级锁区分开来,轻量级锁底层是通过自旋来实现的,并不会阻塞线程
- 如果自旋次数过多仍然没有获取到锁,则会升级为重量级锁,重量级锁会导致线程阻塞
- 自旋锁:自旋锁就是线程在获取锁的过程中,不会去阻塞线程,也就无所谓唤醒线程,阻塞和唤醒这两个步骤都是需要操作系统去进行的,比较消耗时间,自旋锁是线程通过CAS获取预期的一个标记,如果没有获取到,则继续循环获取,如果获取到了则表示获取到了锁,这个过程线程一直在运行中,相对而言没有使用太多的操作系统资源,比较轻量。
11. volatile 关键字的作用
借鉴博文:blog.csdn.net/u010255818/…
在并发领域中,存在三大特性:原子性、有序性、可见性。volatile关键字用来修饰对象的属性,在并发环境下可以保证这个属性的可见性,对于加了volatile关键字的属性,在对这个属性进行修改时,会直接将CPU高级缓存中的数据写回到主内存,对这个变量的读取也会直接从主内存中读取,从而保证了可见性,底层是通过操作系统的内存屏障来实现的,由于使用了内存屏障,所以会禁止指令重排,所以同时也就保证了有序性,在很多并发场景下,如果用好volatile关键字可以很好的提高执行效率。
1.变量可见性
其一是保证该变量对所有线程可见,这里的可见性指的是当一个线程修改了变量的值,那么新的 值对于其他线程是可以立即获取的。
2.禁止重排序
volatile 禁止了指令重排。
3.轻量级
比 sychronized 更轻量级的同步锁
volatile 适合这种场景: 一个变量被多个线程共享,线程直接给这个变量赋值。
12. synchronized 和 volatile 关键字的区别:
-
volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。
-
多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
-
volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
-
volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。
13. ReentrantLock中的公平锁和非公平锁的底层实现
首先不管是公平锁和非公平锁,它们的底层实现都会使用AQS来进行排队,它们的区别在于:线程在使用lock()方法加锁时,如果是公平锁,会先检查AQS队列中是否存在线程在排队,如果有线程在排队,则当前线程也进行排队,如果是非公平锁,则不会去检查是否有线程在排队,而是直接竞争锁。
不管是公平锁还是非公平锁,一旦没竞争到锁,都会进行排队,当锁释放时,都是唤醒排在最前面的线程,所以非公平锁只是体现在了线程加锁阶段,而没有体现在线程被唤醒阶段。
另外,ReentrantLock是可重入锁,不管是公平锁还是非公平锁都是可重入的。
14. AQS原理、公平锁原理、非公平锁原理
AQS核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。
AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列来完成获取资源线程的排队工作。AQS使用CAS对该同步状态进行原子操作实现对其值的修改。
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
状态信息通过procted类型的getState,setState,compareAndSetState进行操作
//返回同步状态的当前值
protected final int getState() {
return state;
}
// 设置同步状态的值
protected final void setState(int newState) {
state = newState;
}
//原子地(CAS操作)将同步状态值设置为给定值update如果当前同步状态的值等于expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
15.ReentrantLock中tryLock()和lock()方法的区别
- tryLock()表示尝试加锁,可能加到,也可能加不到,该方法不会阻塞线程,如果加到锁则返回true,没有加到则返回false
- lock()表示阻塞加锁,线程会阻塞直到加到锁,方法也没有返回值
16. Sychronized和ReentrantLock的区别
-
sychronized是一个关键字,ReentrantLock是一个类
-
sychronized会自动的加锁与释放锁,ReentrantLock需要程序员手动加锁与释放锁
-
sychronized的底层是JVM层面的锁,ReentrantLock是API层面的锁
-
sychronized是非公平锁,ReentrantLock可以选择公平锁或非公平锁
-
sychronized锁的是对象,锁信息保存在对象头中,ReentrantLock通过代码中int类型的state标识来标识锁的状态
-
sychronized底层有一个锁升级的过程
17. 共享锁和独占锁 锁优化
借鉴博文:www.jianshu.com/p/39628e118…
18. CAS原理以及产生的问题
概念
CAS(Compare And Swap/Set)比较并交换,CAS 算法的过程是这样:它包含 3 个参数CAS(V,E,N)。V 表示要更新的变量(内存值),E 表示预期值(旧的),N 表示新值。当且仅当 V 值等于 E 值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,则当 前线程什么都不做。最后,CAS 返回当前 V 的真实值。
CAS 操作是抱着乐观的态度进行的(乐观锁),它总是认为自己可以成功完成操作。当多个线程同时 使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会被挂 起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。基于这样的原理,CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
ABA
线程C、D;线程D将A修改为B后又修改为A,此时C线程以为A没有改变过,java的原子类AtomicStampedReference,通过控制变量值的版本号来保证CAS的正确性。具体解决思路就是在变量前追加上版本号,每次变量更新的时候把版本号加一,那么A - B - A就会变成1A - 2B - 3A。
缺点
- ABA
- 自旋时间过长,消耗CPU资源,如果资源竞争激烈,多线程自旋长时间消耗资源
19. 如何在两个线程之间共享数据
20. ThreadLocal原理
-
ThreadLocal是Java中所提供的线程本地存储机制,可以利用该机制将数据缓存在某个线程内部,该线程可以在任意时刻、任意方法中获取缓存的数据
-
ThreadLocal底层是通过ThreadLocalMap来实现的,每个Thread对象(注意不是ThreadLocal对象)中都存在一个ThreadLocalMap,Map的key为ThreadLocal对象,Map的value为需要缓存的值
-
如果在线程池中使用ThreadLocal会造成内存泄漏,因为当ThreadLocal对象使用完之后,应该要把设置的key,value,也就是Entry对象进行回收,但线程池中的线程不会回收,而线程对象是通过强引用指向ThreadLocalMap,ThreadLocalMap也是通过强引用指向Entry对象,线程不被回收,Entry对象也就不会被回收,从而出现内存泄漏,解决办法是,在使用了ThreadLocal对象之后,手动调用ThreadLocal的remove方法,手动清除Entry对象
-
ThreadLocal经典的应用场景就是连接管理(一个线程持有一个连接,该连接对象可以在不同的方法之间进行传递,线程之间不共享同一个连接)
21. ThreadLocal 作用(线程本地存储)
ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
ThreadLocalMap(线程的一个属性)
-
每个线程中都有一个自己的 ThreadLocalMap 类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。
-
将一个共用的 ThreadLocal 静态实例作为 key,将不同对象的引用保存到不同线程的ThreadLocalMap 中,然后在线程执行的各处通过这个静态 ThreadLocal 实例的 get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。
-
ThreadLocalMap 其实就是线程里面的一个属性,它在 Thread 类中定义
ThreadLocal.ThreadLocalMap threadLocals = null;
22. ThreadLocal是怎么可见的?
如果InheritableThreadLocal存储的是不变性(immutable)的对象,如String,对于主线程设置的值子线程可以通过get函数获取,但子线程调用set函数设置新值后,对主线程没有影响,对其它子线程也没有影响,只对自己可见,(请参考代码例子中的stringItl变量);如果主线程还没有获取(get)或者设置(set)过ThreadLocal变量,而子线程先获取(get)或者设置(set)了ThreadLocal变量,那么这个份值只属于那个子线程,对主线程和其它子线程都不可见,
如果InheritableThreadLocal存储的是可变性(mutable)的对象,如StringBuffer,对于主线程设置的值子线程可以通过get函数获取,但子线程调用set函数设置新值后,对主线程没有影响,对其它子线程也没有影响,只对自己可见,但如果子线程先get获取再修改对象的属性,那么这个修改对主线程和其它子线程是可见的,即他们还是共享一个引用(请参考代码例子中的stringBufferItl变量);如果主线程还没有获取(get)或者设置(set)过ThreadLocal变量,而子线程先获取(get)或者设置(set)了ThreadLocal变量,那么这份值只属于那个子线程,对主线程和其它子线程都不可见,
所以子线程只能通过修改可变性(Mutable)对象对主线程才是可见的,即才能将修改传递给主线程,但这不是一种好的实践,不建议使用,为了保护线程的安全性,一般建议只传递不可变(Immuable)对象,即没有状态的对象。
23. ThreadLocal线程在什么情况下造成内存泄漏?
原因
ThreadLocalMap 维护 ThreadLocal 变量与具体实例的映射,当 ThreadLocal 变量被回收后,该映射的键变为 null,该 Entry 无法被移除。从而使得实例被该 Entry 引用而无法被回收造成内存泄漏。
防止内存泄漏
对于已经不再被使用且已被回收的 ThreadLocal 对象,它在每个线程内对应的实例由于被线程的 ThreadLocalMap 的 Entry 强引用,无法被回收,可能会造成内存泄漏。
针对该问题,ThreadLocalMap 的 set 方法中,通过 replaceStaleEntry 方法将所有键为 null 的 Entry 的值设置为 null,从而使得该值可被回收。另外,会在 rehash 方法中通过 expungeStaleEntry 方法将键和值为 null 的 Entry 设置为 null 从而使得该 Entry 可被回收。通过这种方式,ThreadLocal 可防止内存泄漏。
24. 如何实现幂等性
25. 多线程场景
多线程处理后台任务
一般来说,我们需要在后台处理的任务,通常会使用定时器来开启后台线程处理,比如有些数据表的状态我需要定时去修改、我们搜索引擎里面的数据需要定时去采集、定时生成统计信息、定时清理上传的垃圾文件等。
多线程异步处理任务
当我们需要处理一个耗时操作并且不要立刻知道处理结果时,我们可以开启后台线程异步处理该耗时操作,这样可以提高用户体验。比如我之前做的一个项目需要上传简历附件,后台需要对简历附件进行解析,保存到数据表中,因为涉及多种格式的处理,所以我们开启多线程异步处理此操作,这样用户就不用等到我们的简历解析完就能看到服务端的响应了。再比如用户下载简历时,我们需要将数据表中的数据生成简历附件并且通过邮件发送到用户邮箱,该操作也可以开启多线程异步处理。
多线程分布式计算
当我们处理一个比较大的耗时任务时,我们可以将该任务切割成多个小的任务,然后开启多个线程同时处理这些小的任务,切割的数量一般根据我们服务器CPU的核数,合理利用多核CPU的优势。比如下载操作可以使用多线程下载提高下载速度;清理文件时,开启多个线程,按目录并行处理等等。
26. 守护线程和非守护线程理解
Daemon的作用是为其他线程的运行提供服务,比如说GC线程。
其实User Thread线程和Daemon Thread守护线程本质上来说去没啥区别的,唯一的区别之处就在虚拟机的离开:
如果User Thread全部撤离,那么Daemon Thread也就没啥线程好服务的了,所以虚拟机也就退出了。
27. 如何查看线程死锁
- 可以通过jstack命令来进行查看,jstack命令中会显示发生了死锁的线程
- 或者两个线程去操作数据库时,数据库发生了死锁,这是可以查询数据库的死锁情况
1、查询是否锁表
show OPEN TABLES where In_use > 0;
2、查询进程
show processlist;
3、查看正在锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
4、查看等待锁的事务
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
28. 死锁如何避免
造成死锁的几个原因:
-
一个资源每次只能被一个线程使用
-
一个线程在阻塞等待某个资源时,不释放已占有资源
-
一个线程已经获得的资源,在未使用完之前,不能被强行剥夺
-
若干线程形成头尾相接的循环等待资源关系
这是造成死锁必须要达到的4个条件,如果要避免死锁,只需要不满足其中某一个条件即可。而其中前3个条件是作为锁要符合的条件,所以要避免死锁就需要打破第4个条件,不出现循环等待锁的关系。
在开发过程中:
-
要注意加锁顺序,保证每个线程按同样的顺序进行加锁
-
要注意加锁时限,可以针对所设置一个超时时间
-
要注意死锁检查,这是一种预防机制,确保在第一时间发现死锁并进行解决
29. 死锁怎么解决
1)资源剥夺法。挂起某些死锁进程,并抢占它的资源,将这些资源分配给其他的死锁进程。但应防止被挂起的进程长时间得不到资源时,而处于资源匮乏的状态。
2)进程撤销法。强制撤销一个或一部分进程并剥夺这些进程的资源。撤销的原则可以按进程的优先级和撤销进程代价的高低进行。
3)进程回退法。让一个或多个进程回退到足以回避死锁的地步,进程回退时资源释放资源而不是被剥夺。要求系统保持进程的历史信息,设置还原点。
Spring
1. Spring源码了解多少
自由发挥
2. Spring Boot自动装配
@SpringBootConfiguration——>@Configuration 配置
@ComponentScan:没有指定扫描包,加载同类级别或者该类级别下面的
@EnableAutoConfiguration:只要加了注解就会开启自动装配,就会去扫描所有的bean
@AutoConfigurationPackage
Register:主配置类所在的包作为自动配置包进行管理
-
@Import({AutoConfigurationImportSelector.class}) @Import的作用就是导入一个类到IOC容器
-
AutoConfigurationImportSelector
-
selectImports()
-
SpringFactoriesLoader.loadFactoryNames()
-
spring-boot-autoconfigure.jar从指定的配置文件META-INF/spring.factories加载配置
AutoConfigurationImportSelector:(selectImports()-》getAutoConfigurationEntry()-》getCandidateConfigurations())-》SpringFactoriesLoader:(loadFactoryNames()-》loadSpringFactories().getResources("META-INF/spring.factories"))
3. Spring的事物隔离级别
4. Spring中的事务是如何实现的
- Spring事务底层是基于数据库事务和AOP机制的
- 首先对于使用了@Transactional注解的Bean,Spring会创建一个代理对象作为Bean
- 当调用代理对象的方法时,会先判断该方法上是否加了@Transactional注解
- 如果加了,那么则利用事务管理器创建一个数据库连接
- 并且修改数据库连接的autocommit属性为false,禁止此连接的自动提交,这是实现Spring事务非常重要的一步
- 然后执行当前方法,方法中会执行sql
- 执行完当前方法后,如果没有出现异常就直接提交事务
- 如果出现了异常,并且这个异常是需要回滚的就会回滚事务,否则仍然提交事务
- Spring事务的隔离级别对应的就是数据库的隔离级别
- Spring事务的传播机制是Spring事务自己实现的,也是Spring事务中最复杂的
- Spring事务的传播机制是基于数据库连接来做的,一个数据库连接一个事务,如果传播机制配置为需要新开一个事务,那么实际上就是先建立一个数据库连接,在此新数据库连接上执行sql
5. Spring中什么时候@Transactional会失效
1、@Transactional注解的方法内部调用了当前类中的方法,事务失效
原因:当通过在同一个类的内部方法直接调用带有@Transactional的方法时,@Transactional将失效,例如:
public void saveAB(A a, B b)
{
saveA(a);
saveB(b);
}
@Transactional
public void saveA(A a)
{
dao.saveA(a);
}
@Transactional
public void saveB(B b)
{
dao.saveB(b);
}
在saveAB中调用saveA和saveB方法,两者的@Transactional都将失效。这是因为Spring事务的实现基于代理类,当在内部直接调用方法时,将不会经过代理对象,而是直接调用目标对象的方法,无法被TransactionInterceptor拦截处理。
解决方案:
(1)ApplicationContextAware通过ApplicationContextAware注入的上下文获得代理对象。
public void saveAB(A a, B b)
{
Test self = (Test) applicationContext.getBean("Test");
self.saveA(a);
self.saveB(b);
}
(2)AopContext
通过AopContext获得代理对象。
public void saveAB(A a, B b)
{
Test self = (Test)AopContext.currentProxy();
self.saveA(a);
self.saveB(b);
}
(3)@Autowired
通过@Autowired注解注入代理对象。
@Component
public class Test {
@Autowired
Test self;
public void saveAB(A a, B b)
{
self.saveA(a);
self.saveB(b);
}
// ...
}
(4)拆分
将saveA、saveB方法拆分到另一个类中。
public void saveAB(A a, B b)
{
txOperate.saveA(a);
txOperate.saveB(b);
}
2、@Transactional注解只对方法的访问控制权限为pubic级别的才生效,其他级别的访问控制权限的方法事务失效
原因:@Transactional只有标注在public级别的方法上才能生效,对于非public方法将不会生效。这是由于Spring AOP不支持对private、protect方法进行拦截。声明 @Transactional 的目标方法时,Spring Framework 默认使用 AOP 代理,在代码运行时生成一个代理对象,再由这个代理对象来统一管理。声明式事务原理是Spring事务会为@Transaction标注的方法的类生成AOP增强的动态代理类对象,并且在调用目标方法的拦截链中加入TransactionInterceptor进行环绕增加,实现事务管理。从原理上来说,动态代理是通过接口实现,所以自然不能支持private和protect方法的。而CGLIB是通过继承实现,其实是可以支持protect方法的拦截的,但Spring AOP中并不支持这样使用。如果需要对protect或private方法拦截则建议使用AspectJ。
解决方案: @Transactional注解的方法声明为public的访问控制权限。
3、@Transactional注解的方法内部捕获并处理了异常时,事务失效
原因: 只有抛出非检查异常或是rollbackFor中指定的异常才能触发回滚。如果我们把异常catch住,而且没抛出,则会导致无法触发回滚,这也是开发中常犯的错误。
解决方案: 将异常throw到@Transactional注解的方法的上级调用方法中处理。
4、抛出的异常不是Spring的事务支持的异常
Spring的事务只支持未检查异常(unchecked),不支持已检查异常(checked)。
Q:什么是未检查异常、已检查异常?
A:异常的继承结构:Throwable为基类,Error和Exception继承Throwable。Error和RuntimeException及其子类称为未检查异常(unchecked),其它异常称为已检查异常(checked)。
即下图中的蓝色部分是未检查异常,红色部分中Exception的除RuntimeException之外的其他子类是已检查异常。
需要注意的是,Spring事务只支持上面说的未检查异常,其他所有的异常的抛出都不会回滚。其中,开发过程中比较常见的就是SQLException。通过查看SQLException的继承关系可以看出,SQLException不属于未检查异常,所以SQLException的抛出不会导致事务回滚。
解决方案:
1、捕获异常,手动回滚。
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
参考:spring 事务控制 设置手动回滚
2、捕获异常,然后抛出一个事务支持回滚的异常。
常用的就是我们项目中会自定义异常继承自RuntimeException类,可以抛出这种自定义异常。
5、数据库不支持事务
MySQL数据库常用的引擎有MyISAM和InnoDB。
其中,MyISAM是不支持事务的,InnoDB是支持事务的。
6. Spring传播机制及Spring事物中传播级别
7. Spring Bean的加载过程,如何创建和销毁的
-
推断构造方法
-
实例化
-
填充属性,也就是依赖注入
-
处理Aware回调
-
初始化前,处理@PostConstruct注解
-
初始化,处理InitializingBean接口
-
初始化后,进行AOP
8. bean的注入方式
-
构造方法注入
-
setter注入
-
静态工厂注入
-
实例工厂注入
9. Spring中的Bean创建的生命周期有哪些步骤
Spring中一个Bean的创建大概分为以下几个步骤:
-
推断构造方法
-
实例化
-
填充属性,也就是依赖注入
-
处理Aware回调
-
初始化前,处理@PostConstruct注解
-
初始化,处理InitializingBean接口
-
初始化后,进行AOP
10. Spring中Bean是线程安全的吗
Spring本身并没有针对Bean做线程安全的处理,所以:
-
如果Bean是无状态的,那么Bean则是线程安全的
-
如果Bean是有状态的,那么Bean则不是线程安全的
另外,Bean是不是线程安全,跟Bean的作用域没有关系,Bean的作用域只是表示Bean的生命周期范围,对于任何生命周期的Bean都是一个对象,这个对象是不是线程安全的,还是得看这个Bean对象本身。
11. ApplicationContext和BeanFactory有什么区别
BeanFactory是Spring中非常核心的组件,表示Bean工厂,可以生成Bean,维护Bean,而ApplicationContext继承了BeanFactory,所以ApplicationContext拥有BeanFactory所有的特点,也是一个Bean工厂,但是ApplicationContext除开继承了BeanFactory之外,还继承了诸如EnvironmentCapable、MessageSource、ApplicationEventPublisher等接口,从而ApplicationContext还有获取系统环境变量、国际化、事件发布等功能,这是BeanFactory所不具备的
12.BeanFactory 简介以及它 和FactoryBean的区别
借鉴博文:www.cnblogs.com/aspirant/p/…
13. Spring AOP和IOC
借鉴博文: www.jianshu.com/p/78ba8bafb…
14. Spring容器启动流程是怎样的
-
在创建Spring容器,也就是启动Spring时
-
首先会进行扫描,扫描得到所有的BeanDefinition对象,并存在一个Map中
-
然后筛选出非懒加载的单例BeanDefinition进行创建Bean,对于多例Bean不需要在启动过程中去进行创建,对于多例Bean会在每次获取Bean时利用BeanDefinition去创建
-
利用BeanDefinition创建Bean就是Bean的创建生命周期,这期间包括了合并BeanDefinition、推断构造方法、实例化、属性填充、初始化前、初始化、初始化后等步骤,其中AOP就是发生在初始化后这一步骤中
-
单例Bean创建完了之后,Spring会发布一个容器启动事件
-
Spring启动结束
-
在源码中会更复杂,比如源码中会提供一些模板方法,让子类来实现,比如源码中还涉及到一些BeanFactoryPostProcessor和BeanPostProcessor的注册,Spring的扫描就是通过BenaFactoryPostProcessor来实现的,依赖注入就是通过BeanPostProcessor来实现的
-
在Spring启动过程中还会去处理@Import等注解
15.Spring用到了哪些设计模式
(1)工厂模式:BeanFactory就是简单工厂模式的体现,用来创建对象的实例;
(2)单例模式:Bean默认为单例模式。
(3)代理模式:Spring的AOP功能用到了JDK的动态代理和CGLIB字节码生成技术;
(4)模板方法:用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。
(5)观察者模式:定义对象键一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都会得到通知被制动更新,如Spring中listener的实现--ApplicationListener。*
(6)策略模式:
第一个地方,加载资源文件的方式,使用了不同的方法,比如:ClassPathResourece,FileSystemResource,ServletContextResource,UrlResource但他们都有共同的借口Resource;
第二个地方就是在Aop的实现中,采用了两种不同的方式,JDK动态代理和CGLIB代理;
16. Spring MVC 流程
(1) 客户端请求提交到 DispatcherServlet 。HandlerMapping 寻找处理器
(2) 由 DispatcherServlet 控制器查询一个或多个 HandlerMapping,找到处理请求的 Controller。调用处理器 Controller
(3) DispatcherServlet 将请求提交到 ****Controller。Controller 调用业务逻辑处理后,返回 ModelAndView
(4)(5)调用业务处理和返回结果:Controller 调用业务逻辑处理后,返回 ModelAndView。
DispatcherServlet 查询 ModelAndView
(6)(7)处理视图映射并返回模型: DispatcherServlet 查询一个或多个 ViewResoler 视图解析器,找到 ModelAndView 指定的视图。
ModelAndView 反馈浏览器 HTTP
(8) Http 响应:视图负责将结果显示到客户端。
17. SpringBoot中常用注解及其底层实现
- @SpringBootApplication注解:这个注解标识了一个SpringBoot工程,它实际上是另外三个注解的组合,这三个注解是:
(a)@SpringBootConfiguration:这个注解实际就是一个@Configuration,表示启动类也是一个配置类
(b)@EnableAutoConfiguration:向Spring容器中导入了一个Selector,用来加载ClassPath下SpringFactories中所定义的自动配置类,将这些自动加载为配置Bean
(c)@ComponentScan:标识扫描路径,因为默认是没有配置实际扫描路径,所以SpringBoot扫描的路径是启动类所在的当前目录
-
@Bean注解:用来定义Bean,类似于XML中的标签,Spring在启动时,会对加了@Bean注解的方法进行解析,将方法的名字做为beanName,并通过执行方法得到bean对象
-
@Controller、@Service、@ResponseBody、@Autowired都可以说
18. SpringBoot是如何启动Tomcat的
-
首先,SpringBoot在启动时会先创建一个Spring容器
-
在创建Spring容器过程中,会利用@ConditionalOnClass技术来判断当前classpath中是否存在Tomcat依赖,如果存在则会生成一个启动Tomcat的Bean
-
Spring容器创建完之后,就会获取启动Tomcat的Bean,并创建Tomcat对象,并绑定端口等,然后启动Tomcat
19. SpringBoot中配置文件的加载顺序是怎样的
优先级从高到低,高优先级的配置覆盖低优先级的配置,所有配置会形成互补配置。
-
命令行参数。所有的配置都可以在命令行上进行指定;
-
Java系统属性(System.getProperties());
-
操作系统环境变量 ;
-
jar包外部的application-{profile}.properties或application.yml(带spring.profile)配置文件
-
jar包内部的application-{profile}.properties或application.yml(带spring.profile)配置文件 再来加载不带profile
-
jar包外部的application.properties或application.yml(不带spring.profile)配置文件
-
jar包内部的application.properties或application.yml(不带spring.profile)配置文件
-
@Configuration注解类上的@PropertySource
20. Spring中后置处理器的作用
Spring中的后置处理器分为BeanFactory后置处理器和Bean后置处理器,它们是Spring底层源码架构设计中非常重要的一种机制,同时开发者也可以利用这两种后置处理器来进行扩展。BeanFactory后置处理器表示针对BeanFactory的处理器,Spring启动过程中,会先创建出BeanFactory实例,然后利用BeanFactory处理器来加工BeanFactory,比如Spring的扫描就是基于BeanFactory后置处理器来实现的,而Bean后置处理器也类似,Spring在创建一个Bean的过程中,首先会实例化得到一个对象,然后再利用Bean后置处理器来对该实例对象进行加工,比如我们常说的依赖注入就是基于一个Bean后置处理器来实现的,通过该Bean后置处理器来给实例对象中加了@Autowired注解的属性自动赋值,还比如我们常说的AOP,也是利用一个Bean后置处理器来实现的,基于原实例对象,判断是否需要进行AOP,如果需要,那么就基于原实例对象进行动态代理,生成一个代理对象。
21. 循环依赖的问题
借鉴博文:topic.kaikeba.com/p/100001247…
24. SPI机制
借鉴博文:www.pdai.tech/md/java/adv…
Redis
1. redis与memcached 的区别
-
redis支持更丰富的数据类型(支持更复杂的应用场景):Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。
-
Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而Memecache把数据全部存在内存之中。
-
集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 redis 目前是原生支持 cluster 模式的.
-
Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。
2. redis常见数据结构及使用场景
3. redis持久化
4. 缓存雪崩/缓存穿透/缓存击穿
缓存雪崩:
简介:
缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩
掉。
解决办法:
事前: 尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。
事中: 本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉
事后: 利用 redis 持久化机制保存的数据尽快恢复缓存
缓存击穿:
和缓存雪崩类似,缓存雪崩是大批热点数据失效,而缓存击穿是指某一个热点key突然失效,也导致了大量请求直接访问Mysql数据库,这就是缓存击穿,解决方案就是考虑这个热点key不设过期时间
缓存穿透:
简介:
一般是黑客故意去请求缓存中不存在的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决办法:
有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压 力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数据不存 在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
布隆过滤器原理
借鉴博文:zhuanlan.zhihu.com/p/43263751
5. redis的内部淘汰机制
redis 提供 6种数据淘汰策略:
-
volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
-
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
-
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
-
allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的).
-
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
-
no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
6. 如何解决 Redis 的并发竞争 Key 问题
所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺 序不同,这样也就导致了结果的不同!
推荐一种方案:分布式锁(zookeeper和redis都可以实现分布式锁)。(如果不存在 Redis 的并发竞争Key问题,不要使用分布式锁,这样会影响性能)
基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。
7. 如何保证缓存与数据库双写时的数据一致性
8. 用redis实现队列,你准备用什么redis
借鉴博文:blog.csdn.net/qq_31965925…
9. redis集群了解吗?谈谈什么是redis的主从,哨兵,
借鉴博文:www.jianshu.com/p/5de2ab291…
10. redis的分区存储
借鉴博文:www.yisu.com/zixun/14369…
11. 分布式锁
-
首先利用setnx来保证:如果key不存在才能获取到锁,如果key存在,则获取不到锁
-
然后还要利用lua脚本来保证多个redis操作的原子性
-
同时还要考虑到锁过期,所以需要额外的一个看门狗定时任务来监听锁是否需要续约
-
同时还要考虑到redis节点挂掉后的情况,所以需要采用红锁的方式来同时向N/2+1个节点申请锁,都申请到了才证明获取锁成功,这样就算其中某个redis节点挂掉了,锁也不能被其他客户端获取到
红锁
具体的红锁算法分为以下五步:
-
获取当前的时间(单位是毫秒)。
-
使用相同的key和随机值在N个节点上请求锁。这里获取锁的尝试时间要远远小于锁的超时时间,防止某个masterDown了,我们还在不断的获取锁,而被阻塞过长的时间。
-
只有在大多数节点上获取到了锁,而且总的获取时间小于锁的超时时间的情况下,认为锁获取成功了。
-
如果锁获取成功了,锁的超时时间就是最初的锁超时时间进去获取锁的总耗时时间。
-
如果锁获取失败了,不管是因为获取成功的节点的数目没有过半,还是因为获取锁的耗时超过了锁的释放时间,都会将已经设置了key的master上的key删除。
12.redis 主从复制原理
Redis的主从复制是提高Redis的可靠性的有效措施,主从复制的流程如下:
-
集群启动时,主从库间会先建立连接,为全量复制做准备
-
主库将所有数据同步给从库。从库收到数据后,在本地完成数据加载,这个过程依赖于内存快照RDB
-
在主库将数据同步给从库的过程中,主库不会阻塞,仍然可以正常接收请求。否则,redis的服务就被中断了。但是,这些请求中的写操作并没有记录到刚刚生成的RDB文件中。为了保证主从库的数据一致性,主库会在内存中用专门的replication buffer,记录RDB文件生成收到的所有写操作。
-
最后,也就是第三个阶段,主库会把第二阶段执行过程中新收到的写命令,再发送给从库。具体的操作是,当主库完成RDB文件发送后,就会把此时replocation buffer中修改操作发送给从库,从库再执行这些操作。这样一来,主从库就实现同步了
-
后续主库和从库都可以处理客户端读操作,写操作只能交给主库处理,主库接收到写操作后,还会将写操作发送给从库,实现增量同步
13. redis通信协议了解过吗
借鉴博文:zhuanlan.zhihu.com/p/91270536
14. Redis过期策略及实现原理
定时删除
-
含义:在设置 key 的过期时间的同时,为该 key 创建一个定时器,让定时器在 key 的过期时间来临时,对 key 进行删除
-
优点:保证内存被尽快释放
-
缺点:若过期 key 很多,删除这些 key 会占用很多的 CPU 时间,在 CPU 时间紧张的情况下, CPU 不能把所有的时间用来做要紧的事儿,还需要去花时间删除这些 key,定时器的创建耗时,若为每一个设置过期时间的 key 创建一个定时器(将会有大量的定时器产生),性能影响严重。
懒汉式式删除
-
含义:key 过期的时候不删除,每次通过 key 获取值的时候去检查是否过期,若过期,则删除,返回 null。
-
优点:删除操作只发生在通过 key 取值的时候发生,而且只删除当前 key,所以对 CPU 时间的占用是比较少的,而且此时的删除是已经到了非做不可的地步(如果此时还不删除的话,我们就会获取到了已经过期的 key 了)
-
缺点:若大量的 key 在超出超时时间后,很久一段时间内,都没有被获取过,那么可能发生内存泄露(无用的垃圾占用了大量的内存)
定期删除
-
含义:每隔一段时间执行一次删除过期 key 操作
-
优点:通过限制删除操作的时长和频率,来减少删除操作对 CPU 时间的占用–处理 “定时删除” 的缺点
-
缺点:在内存友好方面,不如 ”定时删除”(会造成一定的内存占用,但是没有懒汉式那么占用内存) 在 CPU 时间友好方面,不如 ”懒汉式删除”(会定期的去进行比较和删除操作,cpu 方面不如懒汉式,但是比定时好)
-
难点:合理设置删除操作的执行时长(每次删除执行多长时间)和执行频率(每隔多长时间做一次删除)(这个要根据服务器运行情况来定了),每次执行时间太长,或者执行频率太高对 cpu 都是一种压力。每次进行定期删除操作执行之后,需要记录遍历循环到了哪个标志位,以便下一次定期时间来时,从上次位置开始进行循环遍历
-
说明:memcached 只是用了惰性删除,而 redis 同时使用了惰性删除与定期删除,这也是二者的一个不同点(可以看做是 redis 优于 memcached 的一点);对于懒汉式删除而言,并不是只有获取 key 的时候才会检查 key 是否过期,在某些设置 key 的方法上也会检查(eg.setnx key2 value2:该方法类似于 memcached 的 add 方法,如果设置的 key2 已经存在,那么该方法返回false,什么都不做;如果设置的 key2 不存在,那么该方法设置缓存 key2-value2。假设调用此方法的时候,发现 redis 中已经存在了 key2,但是该key2已经过期了,如果此时不执行删除操作的话,setnx方法将会直接返回false,也就是说此时并没有重新设置 key2-value2 成功,所以对于一定要在 setnx 执行之前,对 key2 进行过期检查)。
Redis 采用的过期策略
- 懒汉式删除+定期删除
懒汉式删除流程:
-
在进行 get 或 setnx 等操作时,先检查 key 是否过期;
-
若过期,删除 key,然后执行相应操作;
-
若没过期,直接执行相应操作;
-
定期删除流程(简单而言,对指定个数个库的每一个库随机删除小于等于指定个数个过期 key)
-
遍历每个数据库(就是 redis.conf 中配置的 ”database” 数量,默认为 16)
-
检查当前库中的指定个数个 key(默认是每个库检查 20 个 key,注意相当于该循环执行 20 次,循环体是下边的描述)
-
如果当前库中没有一个 key 设置了过期时间,直接执行下一个库的遍历
-
随机获取一个设置了过期时间的 key,检查该key是否过期,如果过期,删除 key
-
判断定期删除操作是否已经达到指定时长,若已经达到,直接退出定期删除。
对于定期删除,在程序中有一个全局变量 current_db 来记录下一个将要遍历的库,假设有 16 个库,我们这一次定期删除遍历了 10 个,那此时的 current_db 就是 11,下一次定期删除就从第11个库开始遍历,假设 current_db 等于 15 了,那么之后遍历就再从 0 号库开始(此时 current_db==0)
15. 为什么单线程Redis能那么快?
借鉴博文:zhuanlan.zhihu.com/p/296484467
16. redis的高可用
借鉴博文:www.jianshu.com/p/5de2ab291…
17. 设计LRU
public class LRUCache {
class DLinkedNode {
int key;
int value;
DLinkedNode prev;
DLinkedNode next;
public DLinkedNode() {}
public DLinkedNode(int _key, int _value) {key = _key; value = _value;}
}
private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();
private int size;
private int capacity;
private DLinkedNode head, tail;
public LRUCache(int capacity) {
this.size = 0;
this.capacity = capacity;
// 使用伪头部和伪尾部节点
head = new DLinkedNode();
tail = new DLinkedNode();
head.next = tail;
tail.prev = head;
}
public int get(int key) {
DLinkedNode node = cache.get(key);
if (node == null) {
return -1;
}
// 如果 key 存在,先通过哈希表定位,再移到头部
moveToHead(node);
return node.value;
}
public void put(int key, int value) {
DLinkedNode node = cache.get(key);
if (node == null) {
// 如果 key 不存在,创建一个新的节点
DLinkedNode newNode = new DLinkedNode(key, value);
// 添加进哈希表
cache.put(key, newNode);
// 添加至双向链表的头部
addToHead(newNode);
++size;
if (size > capacity) {
// 如果超出容量,删除双向链表的尾部节点
DLinkedNode tail = removeTail();
// 删除哈希表中对应的项
cache.remove(tail.key);
--size;
}
}
else {
// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
node.value = value;
moveToHead(node);
}
}
private void addToHead(DLinkedNode node) {
node.prev = head;
node.next = head.next;
head.next.prev = node;
head.next = node;
}
private void moveToHead(DLinkedNode node) {
removeNode(node);
addToHead(node);
}
private DLinkedNode removeTail() {
DLinkedNode res = tail.prev;
removeNode(res);
return res;
}
private void removeNode(DLinkedNode node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
}
MongoDB
1. 使用场景
-
缓存数据实时更新
-
生产数据异构备份
-
生产数据实时全量抽取到报表库
-
模糊查询数据实时同步到搜索引擎
2. 特点
MySQL
1. 什么是索引
索引是关系数据库中对某一列或多个列的值进行预排序的数据结构。通过使用索引,可以让数据库系统不必扫描整个表,而是直接定位到符合条件的记录,这样就大大加快了查询速度。
2. 存储引擎
zhuanlan.zhihu.com/p/158978012
MyISAM存储引擎
-
不支持事务
-
缓冲池只缓存索引文件,不缓冲数据文件
-
由MYD和MYI文件组成,MYD用来存放数据文件,MYI用来存放索引文件,
InnoDB存储引擎
-
独立表空间,支持MVCC,行锁设计,提供一致性非锁定读
-
支持外键,插入缓冲,二次写,自适应哈希索引,预读
-
使用聚集的方式存储数据,每张表的存储都是按主键顺序存放。
InnoDB存储引擎(推荐)
InnoDB是事务型数据库的首选引擎,支持事务安全表(ACID),支持行锁定和外键,InnoDB是默认的MySQL引擎。
InnoDB主要特性
-
为MySQL提供了具有提交、回滚和崩溃恢复能力的事物安全(ACID兼容)存储引擎。InnoDB锁定在行级并且也在
SELECT语句中提供一个类似Oracle的非锁定读。这些功能增加了多用户部署和性能。在SQL查询中,可以自由地将InnoDB类型的表和其他MySQL的表类型混合起来,甚至在同一个查询中也可以混合 -
InnoDB存储引擎为在主内存中缓存数据和索引而维持它自己的缓冲池。InnoDB将它的表和索引在一个逻辑表空间中,表空间可以包含数个文件(或原始磁盘文件)。这与MyISAM表不同,比如在MyISAM表中每个表被存放在分离的文件中。InnoDB表可以是任何尺寸,即使在文件尺寸被限制为2GB的操作系统上
-
InnoDB支持外键完整性约束,存储表中的数据时,每张表的存储都按主键顺序存放,如果没有显示在表定义时指定主键,InnoDB会为每一行生成一个6字节的ROWID,并以此作为主键
使用 InnoDB存储引擎 MySQL将在数据目录下创建一个名为 ibdata1 的10MB大小的自动扩展数据文件,以及两个名为ib_logfile0和ib_logfile1的5MB大小的日志文件
3. 事物的特性
ACID
4. 事物的隔离级别
5. 事物的隔离如何实现的
借鉴博文: MVCC机制
6. MySQL的底层数据结构
借鉴博文:
7. 分库分表
借鉴博文:zhuanlan.zhihu.com/p/137368446
8. CAP理论
9. Base理论
由于不能同时满足CAP,所以出现了BASE理论:
-
BA:Basically Available,表示基本可用,表示可以允许一定程度的不可用,比如由于系统故障,请求时间变长,或者由于系统故障导致部分非核心功能不可用,都是允许的
-
S:Soft state:表示分布式系统可以处于一种中间状态,比如数据正在同步
-
E:Eventually consistent,表示最终一致性,不要求分布式系统数据实时达到一致,允许在经过一段时间后再达到一致,在达到一致过程中,系统也是可用的
10. SELECT执行顺序
-
FROM
-
WHERE
-
GROUP BY
-
HAVING
-
SELECT
-
ORDER BY
即:FROM和WHERE子句子句最先执行,在他们执行的时候,产生别名的SELECT子句还根本就没有执行,所以FROM和WHERE根本就没有别名可用。
11. 数据库性能调优
Explain SQL
-
type ALL
-
rows
-
Extra Using temporary:临时表,using filesort:文件排序
-
explain优先执行id更大的一行,全表查询:创建索引
STRAIGHT_JOIN只适用于内连接,因为left join、right join已经知道了哪个表作为驱动表,哪个表作为被驱动表,比如left join就是以左表为驱动表,right join反之,而STRAIGHT_JOIN就是在内连接中使用,而强制使用左表来当驱动表,所以这个特性可以用于一些调优,强制改变mysql的优化器选择的执行计划
-
尽量使用右模糊,避免全模糊,like使用concat()防止SQL注入
-
彻底使用冗余优化SQL,如果字段较大,慎重考虑,是否有必要,比如url,比较激进,增删改的时候会很麻烦
-
考虑使用非关系型数据库里面,也不是万能的,增大学习成本,使用成本以及运维成本
查看SQL具体的执行步骤的性能
PERFORMANCE_SCHEMA
-
查看是否开启性能监控 select * from performance_schema.setup_actors;
-
开启监控项
-
使用开启监控项的用户,执行SQL语句
-
执行SQL,获取event_id
-
获取性能分析
12. 慢查询优化
-
检查是否走了索引,如果没有则优化SQL利用索引
-
检查所利用的索引,是否是最优索引
-
检查所查字段是否都是必须的,是否查询了过多字段,查出了多余数据
-
检查表中数据是否过多,是否应该进行分库分表了
-
检查数据库实例所在机器的性能配置,是否太低,是否可以适当增加资源
13. 索引覆盖是什么
索引覆盖就是一个SQL在执行时,可以利用索引来快速查找,并且此SQL所要查询的字段在当前索引对应的字段中都包含了,那么就表示此SQL走完索引后不用回表了,所需要的字段都在当前索引的叶子节点上存在,可以直接作为结果返回了
14. 最左前缀原则是什么
当一个SQL想要利用索引是,就一定要提供该索引所对应的字段中最左边的字段,也就是排在最前面的字段,比如针对a,b,c三个字段建立了一个联合索引,那么在写一个sql时就一定要提供a字段的条件,这样才能用到联合索引,这是由于在建立a,b,c三个字段的联合索引时,底层的B+树是按照a,b,c三个字段从左往右去比较大小进行排序的,所以如果想要利用B+树进行快速查找也得符合这个规则
15. Innodb是如何实现事务的
Innodb通过Buffer Pool,LogBuffer,Redo Log,Undo Log来实现事务,以一个update语句为例:
-
Innodb在收到一个update语句后,会先根据条件找到数据所在的页,并将该页缓存在Buffer Pool中
-
执行update语句,修改Buffer Pool中的数据,也就是内存中的数据
-
针对update语句生成一个RedoLog对象,并存入LogBuffer中
-
针对update语句生成undolog日志,用于事务回滚
-
如果事务提交,那么则把RedoLog对象进行持久化,后续还有其他机制将Buffer Pool中所修改的数据页持久化到磁盘中
-
如果事务回滚,则利用undolog日志进行回滚
16. Mysql锁有哪些,如何理解
按锁粒度分类:
-
行锁:锁某行数据,锁粒度最小,并发度高
-
表锁:锁整张表,锁粒度最大,并发度低
-
间隙锁:锁的是一个区间
还可以分为:
- 共享锁:也就是读锁,一个事务给某行数据加了读锁,其他事务也可以读,但是不能写
- 排它锁:也就是写锁,一个事务给某行数据加了写锁,其他事务不能读,也不能写
还可以分为:
- 乐观锁:并不会真正的去锁某行记录,而是通过一个版本号来实现的
- 悲观锁:上面所的行锁、表锁等都是悲观锁
在事务的隔离级别实现中,就需要利用锁来解决幻读
17. 表的左连接,右连接,内连接,笛卡尔积
18. 一级索引和二级索引
一级索引:索引和数据存储在一起,都存储在同一个B+tree中的叶子节点。一般主键索引都是一级索引。
二级索引:二级索引树的叶子节点存储的是主键而不是数据。也就是说,在找到索引后,得到对应的主键,再回到一级索引中找主键对应的数据记录。
19. MySQL的主从复制
Mybatis
1. Mybatis中#{}和${}的区别是什么?
-
#{}是预编译处理、是占位符, ${}是字符串替换、是拼接符
-
Mybatis 在处理#{}时,会将 sql 中的#{}替换为?号,调用 PreparedStatement 来赋值
-
Mybatis 在处理{}替换成变量的值,调用 Statement 来赋值
-
使用#{}可以有效的防止SQL注入,提高系统安全性
-- 假设
name="zhouyu"
password="1 or 1=1"
select * from user where name = #{name} and password = #{password} 将转为
select * from user where name = 'zhouyu' and password = '1 or 1=1'
select * from user where name = ${name} and password = ${password} 将转为
select * from user where name = zhouyu and password = 1 or 1=1
消息队列
1. 消息队列有哪些作用
-
解耦:使用消息队列来作为两个系统之间的通讯方式,两个系统不需要相互依赖了
-
异步:系统A给消息队列发送完消息之后,就可以继续做其他事情了
-
流量削峰:如果使用消息队列的方式来调用某个系统,那么消息将在队列中排队,由消费者自己控制消费速度
2. 死信队列是什么?延时队列是什么?
-
死信队列也是一个消息队列,它是用来存放那些没有成功消费的消息的,通常可以用来作为消息重试
-
延时队列就是用来存放需要在指定时间被处理的元素的队列,通常可以用来处理一些具有过期性操作的业务,比如十分钟内未支付则取消订单
3. 消息队列如何保证消息可靠传输
消息可靠传输代表了两层意思,既不能多也不能少。
-
为了保证消息不多,也就是消息不能重复,也就是生产者不能重复生产消息,或者消费者不能重复消费消息
a. 首先要确保消息不多发,这个不常出现,也比较难控制,因为如果出现了多发,很大的原因是生产者自己的原因,如果要避免出现问题,就需要在消费端做控制
b. 要避免不重复消费,最保险的机制就是消费者实现幂等性,保证就算重复消费,也不会有问题,通过幂等性,也能解决生产者重复发送消息的问题
-
消息不能少,意思就是消息不能丢失,生产者发送的消息,消费者一定要能消费到,对于这个问题,就要考虑两个方面
a. 生产者发送消息时,要确认broker确实收到并持久化了这条消息,比如RabbitMQ的confirm机制,Kafka的ack机制都可以保证生产者能正确的将消息发送给broker
b. broker要等待消费者真正确认消费到了消息时才删除掉消息,这里通常就是消费端ack机制,消费者接收到一条消息后,如果确认没问题了,就可以给broker发送一个ack,broker接收到ack后才会删除消息
4. 消息堆积
如何避免消息堆积和延迟
为了避免在业务使用时出现非预期的消息堆积和延迟问题,您需要在前期设计阶段对整个业务逻辑进行完善的排查和梳理。整理出正常业务运行场景下的性能基线,才能在故障场景下迅速定位到阻塞点。其中最重要的就是梳理消息的消费耗时和消息消费的并发度。
-
梳理消息的消费耗时通过压测获取消息的消费耗时,并对耗时较高的操作的代码逻辑进行分析。查询消费耗时,请参见 获取消息消费耗时。梳理消息的消费耗时需要关注以下信息:
a 消息消费逻辑的计算复杂度是否过高,代码是否存在无限循环和递归等缺陷。 b 消息消费逻辑中的I/O操作(如:外部调用、读写存储等)是否是必须的,能否用本地缓存等方案规避。 c 消费逻辑中的复杂耗时的操作是否可以做异步化处理,如果可以是否会造成逻辑错乱(消费完成但异步操作未完成)。 -
设置消息的消费并发度
a 逐步调大线程的单个节点的线程数,并观测节点的系统指标,得到单个节点最优的消费线程数和消息吞吐量。 b 得到单个节点的最优线程数和消息吞吐量后,根据上下游链路的流量峰值计算出需要设置的节点数,节点数=流量峰值/单线程消息吞吐量。
5. 异常消息处理
RabbitMQ-消费异常处理
Kafka
1. Kafka的原理
借鉴博文
2. Kafka的使用场景
3. Offset的使用
4. 如何设置 Kafka 能接收的最大消息的大小?
除了要回答消费者端的参数设置之外,一定要加上 Broker 端的设置,这样才算完整。毕竟,如果 Producer 都不能向 Broker 端发送数据很大的消息,又何来消费一说呢? 因此,你需要同时设置 Broker 端参数和 Consumer 端参数。
- Broker 端参数:message.max.bytes、max.message.bytes(主题级别)和 replica.fetch.max.bytes。
- Consumer 端参数:fetch.message.max.bytes。
Broker 端的最后一个参数比较容易遗漏。我们必须调整 Follower 副本能够接收的最大消息的大小,否则,副本同步就会失败。因此,把这个答出来的话,就是一个加分项。
5. Leader 总是 -1,怎么破
-
重启Kafka,Kafka集群部署,不建议
-
讲Leader杀掉,触发Kafka的重新选举Leader的操作
6. 分区 leader 的选举
分区的 Leader 副本选举对用户是完全透明的,它是由 Controller 独立完成的。你需要回答的是,在哪些场景下,需要执行分区 Leader 选举。每一种场景对应于一种选举策略。当前,Kafka 有 4 种分区 Leader 选举策略。
-
OfflinePartition Leader 选举:每当有分区上线时,就需要执行 Leader 选举。所谓的分区上线,可能是创建了新分区,也可能是之前的下线分区重新上线。这是最常见的分区 Leader 选举场景。
-
ReassignPartition Leader 选举:当你手动运行 kafka-reassign-partitions 命令,或者是调用 Admin 的 alterPartitionReassignments 方法执行分区副本重分配时,可能触发此类选举。假设原来的 AR 是[1,2,3],Leader 是 1,当执行副本重分配后,副本集 合 AR 被设置成[4,5,6],显然,Leader 必须要变更,此时会发生 Reassign Partition Leader 选举。
-
PreferredReplicaPartition Leader 选举:当你手动运行 kafka-preferred-replica- election 命令,或自动触发了 Preferred Leader 选举时,该类策略被激活。所谓的 Preferred Leader,指的是 AR 中的第一个副本。比如 AR 是[3,2,1],那么, Preferred Leader 就是 3。
-
ControlledShutdownPartition Leader 选举:当 Broker 正常关闭时,该 Broker 上 的所有 Leader 副本都会下线,因此,需要为受影响的分区执行相应的 Leader 选举。
7. 分区分配策略
8. __consumer_offsets 是做什么用的
它是一个内部主题,无需手动干预,由 Kafka 自行管理。当然,我们可以创建该主题。
它的主要作用是负责注册消费者以及保存位移值。可能你对保存位移值的功能很熟悉, 但其实该主题也是保存消费者元数据的地方。千万记得把这一点也回答上。另外,这里 的消费者泛指消费者组和独立消费者,而不仅仅是消费者组。
Kafka 的 GroupCoordinator 组件提供对该主题完整的管理功能,包括该主题的创建、 写入、读取和 Leader 维护等。
9. Kafka 的哪些场景中使用了零拷贝(Zero Copy)
零拷贝指的是,应用程序在需要把内核中的一块区域数据转移到另外一块内核区域去时,不需要经过先复制到用户空间,再转移到目标内核区域去了,而直接实现转移。
在 Kafka 中,体现 Zero Copy 使用场景的地方有两处:基于 mmap 的索引和日志文件读写所用的 TransportLayer。
- mmap 就是在用户态直接引用文件句柄,也就是用户态和内核态共享内核态的数据缓冲区,此时数据不需要复制到用户态空间。当应用程序往 mmap 输出数据时,此时就直接输出到了内核态数据,如果此时输出设备是磁盘的话,会直接写盘(flush间隔是30秒)。
- 再说第二个。对于sendfile 而言,数据不需要在应用程序做业务处理,仅仅是从一个 DMA 设备传输到另一个 DMA设备。 此时数据只需要复制到内核态,用户态不需要复制数据,并且也不需要像 mmap 那样对内核态的数据的句柄(文件引用)。
10. Kafka 为什么不支持读写分离
Leader/Follower 模型并没有规定 Follower 副本不可以对外提供读服务。很多框架都是允 许这么做的,只是 Kafka 最初为了避免不一致性的问题,而采用了让 Leader 统一提供服 务的方式。
不过,在开始回答这道题时,你可以率先亮出观点:自 Kafka 2.4 之后,Kafka 提供了有限度的读写分离,也就是说,Follower 副本能够对外提供读服务。
说完这些之后,你可以再给出之前的版本不支持读写分离的理由。
-
场景不适用。读写分离适用于那种读负载很大,而写操作相对不频繁的场景,可 Kafka 不属于这样的场景。
-
同步机制。Kafka 采用 PULL 方式实现 Follower 的同步,因此,Follower 与 Leader 存 在不一致性窗口。如果允许读 Follower 副本,就势必要处理消息滞后(Lagging)的问题。
11. LEO、LSO、AR、ISR、HW 都表示什么含义?
-
LEO:Log End Offset。日志末端位移值或末端偏移量,表示日志下一条待插入消息的 位移值。举个例子,如果日志有 10 条消息,位移值从 0 开始,那么,第 10 条消息的位 移值就是 9。此时,LEO = 10。
-
LSO:Log Stable Offset。这是 Kafka 事务的概念。如果你没有使用到事务,那么这个 值不存在(其实也不是不存在,只是设置成一个无意义的值)。该值控制了事务型消费 者能够看到的消息范围。它经常与 Log Start Offset,即日志起始位移值相混淆,因为 有些人将后者缩写成 LSO,这是不对的。在 Kafka 中,LSO 就是指代 Log Stable Offset。
-
AR:Assigned Replicas。AR 是主题被创建后,分区创建时被分配的副本集合,副本个数由副本因子决定。
-
ISR:In-Sync Replicas。Kafka 中特别重要的概念,指代的是 AR 中那些与 Leader 保持同步的副本集合。在 AR 中的副本可能不在 ISR 中,但 Leader 副本天然就包含在 ISR 中。关于 ISR,还有一个常见的面试题目是如何判断副本是否应该属于 ISR。目前的判断 依据是:Follower 副本的 LEO 落后 Leader LEO 的时间,是否超过了 Broker 端参数 replica.lag.time.max.ms 值。如果超过了,副本就会被从 ISR 中移除。
-
HW:高水位值(High watermark)。这是控制消费者可读取消息范围的重要字段。一 个普通消费者只能“看到”Leader 副本上介于 Log Start Offset 和 HW(不含)之间的 所有消息。水位以上的消息是对消费者不可见的。关于 HW,问法有很多,我能想到的 最高级的问法,就是让你完整地梳理下 Follower 副本拉取 Leader 副本、执行同步机制 的详细步骤。这就是我们的第 20 道题的题目,一会儿我会给出答案和解析。
12. Kafka为什么吞吐量高
Kafka的生产者采用的是异步发送消息机制,当发送一条消息时,消息并没有发送到Broker而是缓存起来,然后直接向业务返回成功,当缓存的消息达到一定数量时再批量发送给Broker。这种做法减少了网络io,从而提高了消息发送的吞吐量,但是如果消息生产者宕机,会导致消息丢失,业务出错,所以理论上kafka利用此机制提高了性能却降低了可靠性。
13. Kafka的Pull和Push分别有什么优缺点
-
pull表示消费者主动拉取,可以批量拉取,也可以单条拉取,所以pull可以由消费者自己控制,根据自己的消息处理能力来进行控制,但是消费者不能及时知道是否有消息,可能会拉到的消息为空
-
push表示Broker主动给消费者推送消息,所以肯定是有消息时才会推送,但是消费者不能按自己的能力来消费消息,推过来多少消息,消费者就得消费多少消息,所以可能会造成网络堵塞,消费者压力大等问题
14. 重复消费&&消息丢失
一、消息丢失和重复消费
生产者:
1、生产者丢失消息
(1)、丢失场景
配置文件里面,ack设置为0,也就是生产者发送之后,
不管分区副本(leader和follower)是否收到都不管了,
如果发送失败,就会消息丢失。
ack设置为1,也就是生产者发送之后,只要leader接收到了,就会返回成功,
follower没来及同步的时候leader挂掉,就会消息丢失。
(2)、解决办法:
设置ack=all / -1,保证leader和follower分区都收到之后,
再返回给生产者成功。
如果其中有一个步骤异常,都会触发kafka的重试机制。
2、生产者重复消费
(1)、重复消息场景:
生产发送的消息没有收到正确的broke响应,导致producer重试。
producer发出一条消息,broke落盘以后因为网络等种种原因
发送端得到一个发送失败的响应或者网络中断,然后producer收到
一个可恢复的Exception重试消息导致消息重复。
(2)、解决办法:
启动kafka的幂等性
enable.idempotence=true 同时要求 ack=all 且 retries>1。
幂等原理:
每个生产者producer都有一个唯一id,producer每发送一条数据,
都会带上一个sequence,当消息落盘,sequence就会递增1。
那么只需要判断当前消息的sequence是否大于当前最大sequence,
大于就代表此条数据没有落盘过,可以正常消费。
不大于就代表落盘过,这个时候重发的消息会被服务端拒掉从而避免消息重复。
broker:
1、broker丢失数据
(1)、丢失场景
<1>、ack=1,follower没来及同步的时候leader挂掉也不好重试,
当follower被选举为新的leader时,这部分没同步的数据就丢失了。
<2>、分区副本数小于2个,导致没有足够数量的副本参与新leader选举,
无法保证数据的高可用,当原leader挂了之后,
没有follower被选举为leader。
(2)、解决办法:
<1>、ack=-1,保证leader和follower分区的数据可以落盘。
<2>、保证分区副本数大于2,保证数据的高可用性
<3>、设置重试次数等。
消费者
1、消费者丢失消息
(1)、丢失场景
<1>、设置的自动提交offset,当消费者已经消费到了消息,
也记录了新的偏移量offset,但是后面的业务失败了或者
没来得及处理就挂了。
这时候因为offset已经更新了,这条消息也再消费不到了。
(2)、解决办法:
<1>、设置为手动提交成功,当业务代码都执行完成之后,
再进行手动提交,确保消息被真正处理到。
2、消费者消息重复
(1)、丢失场景
<1>、数据消费完没有及时提交offset到broke。
消息消费端在消费过程中挂掉没有及时提交offset到broke,
另一个消费端启动拿之前记录的offset开始消费,
由于offset的滞后性可能会导致新启动的客户端有少量重复消费。
(2)、解决办法
<1>、设置为手动提交成功
<2>、在下游程序里面做幂等,
幂等实际上就两种方法:
(1)、将唯一键存入第三方介质,
要操作数据的时候先判断第三方介质(数据库或者缓存)
有没有这个唯一键。
(2)、将版本号(offset)存入到数据里面,
然后再要操作数据的时候用这个版本号做乐观锁,
当版本号大于原先的才能操作。
设计模式
常用的设计模式,能结合业务场景讲解
1. 单例模式
2. 工厂模式
3. 观察者模式
4. 模板模式:
一个抽象类公开定义了执行它的方法的方式/模板。它的子类可以按需要重写方法实现,但调用将以抽象类中定义的方式进行。
5. 适配器模式
转接头的作用,拉数据的时候拉不同医院的数据,系统表是一样的,但是中间的转换过程是不一样的,根据适配器模式,选择合适的转换
6. 代理模式
代理类和真实的类都是实现同一个接口,使用代理类来获取真实类的对象
排序(自行百度)
1. 冒泡排序算法
2. 二分法排序
3. 快速排序
4. 插入排序
5. 归并排序
分布式
1. 分布式事务
在分布式系统中,一次业务处理可能需要多个应用来实现,比如用户发送一次下单请求,就涉及到订单系统创建订单、库存系统减库存,而对于一次下单,订单创建与减库存应该是要同时成功或同时失败的,但在分布式系统中,如果不做处理,就很有可能出现订单创建成功,但是减库存失败,那么解决这类问题,就需要用到分布式事务。常用解决方案有:
-
本地消息表:创建订单时,将减库存消息加入在本地事务中,一起提交到数据库存入本地消息表,然后调用库存系统,如果调用成功则修改本地消息状态为成功,如果调用库存系统失败,则由后台定时任务从本地消息表中取出未成功的消息,重试调用库存系统
-
消息队列:目前RocketMQ中支持事务消息,它的工作原理是:
a 生产者订单系统先发送一条half消息到Broker,half消息对消费者而言是不可见的 b 再创建订单,根据创建订单成功与否,向Broker发送commit或rollback c 并且生产者订单系统还可以提供Broker回调接口,当Broker发现一段时间half消息没有收到任何操作命令,则会主动调此接口来查询订单是否创建成功 d 一旦half消息commit了,消费者库存系统就会来消费,如果消费成功,则消息销毁,分布式事务成功结束 e 如果消费失败,则根据重试策略进行重试,最后还失败则进入死信队列,等待进一步处理 -
Seata:阿里开源的分布式事务框架,支持AT、TCC等多种模式,底层都是基于两阶段提交理论来实现的
2. 分布式锁
在单体架构中,多个线程都是属于同一个进程的,所以在线程并发执行时,遇到资源竞争时,可以利用ReentrantLock、synchronized等技术来作为锁,来控制共享资源的使用。
而在分布式架构中,多个线程是可能处于不同进程中的,而这些线程并发执行遇到资源竞争时,利用ReentrantLock、synchronized等技术是没办法来控制多个进程中的线程的,所以需要分布式锁,意思就是,需要一个分布式锁生成器,分布式系统中的应用程序都可以来使用这个生成器所提供的锁,从而达到多个进程中的线程使用同一把锁。
目前主流的分布式锁的实现方案有两种:
- zookeeper:利用的是zookeeper的临时节点、顺序节点、watch机制来实现的,zookeeper分布式锁的特点是高一致性,因为zookeeper保证的是CP,所以由它实现的分布式锁更可靠,不会出现混乱
- redis:利用redis的setnx、lua脚本、消费订阅等机制来实现的,redis分布式锁的特点是高可用,因为redis保证的是AP,所以由它实现的分布式锁可能不可靠,不稳定(一旦redis中的数据出现了不一致),可能会出现多个客户端同时加到锁的情况
3. 分布式id
在开发中,我们通常会需要一个唯一ID来标识数据,如果是单体架构,我们可以通过数据库的主键,或直接在内存中维护一个自增数字来作为ID都是可以的,但对于一个分布式系统,就会有可能会出现ID冲突,此时有以下解决方案:
-
uuid,这种方案复杂度最低,但是会影响存储空间和性能
-
利用单机数据库的自增主键,作为分布式ID的生成器,复杂度适中,ID长度较之uuid更短,但是受到单机数据库性能的限制,并发量大的时候,此方案也不是最优方案
-
利用redis、zookeeper的特性来生成id,比如redis的自增命令、zookeeper的顺序节点,这种方案和单机数据库(mysql)相比,性能有所提高,可以适当选用
-
雪花算法,一切问题如果能直接用算法解决,那就是最合适的,利用雪花算法也可以生成分布式ID,底层原理就是通过某台机器在某一毫秒内对某一个数字自增,这种方案也能保证分布式架构中的系统id唯一,但是只能保证趋势递增。业界存在tinyid、leaf等开源中间件实现了雪花算法。
JVM
1. JVM内存区域
线程私有:
程序计数器 本地方法栈 虚拟机栈
线程共享:
堆 方法区
2. JVM运行内存
3. JVM中哪些可以作为gc root
什么是gc root,JVM在进行垃圾回收时,需要找到“垃圾”对象,也就是没有被引用的对象,但是直接找“垃圾”对象是比较耗时的,所以反过来,先找“非垃圾”对象,也就是正常对象,那么就需要从某些“根”开始去找,根据这些“根”的引用路径找到正常对象,而这些“根”有一个特征,就是它只会引用其他对象,而不会被其他对象引用,例如:栈中的本地变量、方法区中的静态变量、本地方法栈中的变量、正在运行的线程等可以作为gc root。
4. jvm怎么判断哪些对象应该回收呢(可达性分析)
引用计数法的算法
在对象中添加一个引用计数器,每当一个地方引用它时,计数器就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。但是这样的算法有个问题,就是不能解决循环依赖的问题。
Object 1和Object 2其实都可以被回收,但是它们之间还有相互引用,所以它们各自的计数器为1,则还是不会被回收。
所以,Java虚拟机没有采用引用计数法。它采用的是可达性分析算法。
可达性分析算法
通过一系列的“GC Roots”,也就是根对象作为起始节点集合,从根节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为引用链,如果某个对象到GC Roots间没有任何引用链相连。
用图论的话来说就是从GC Roots到这个对象不可达时,则证明此对象是不可能再被使用的。所以此对象就是可以被回收的对象。
5. 垃圾回收算法
6. GC垃圾收集器
CMS
G1
7. JVM类加载机制
8. 双亲委派
9. 四种引用类型
JVM调优
1.你们项目如何排查JVM问题
对于还在正常运行的系统:
-
可以使用jmap来查看JVM中各个区域的使用情况
-
可以通过jstack来查看线程的运行情况,比如哪些线程阻塞、是否出现了死锁
-
可以通过jstat命令来查看垃圾回收的情况,特别是fullgc,如果发现fullgc比较频繁,那么就得进行调优了
-
通过各个命令的结果,或者jvisualvm等工具来进行分析
-
首先,初步猜测频繁发送fullgc的原因,如果频繁发生fullgc但是又一直没有出现内存溢出,那么表示fullgc实际上是回收了很多对象了,所以这些对象最好能在younggc过程中就直接回收掉,避免这些对象进入到老年代,对于这种情况,就要考虑这些存活时间不长的对象是不是比较大,导致年轻代放不下,直接进入到了老年代,尝试加大年轻代的大小,如果改完之后,fullgc减少,则证明修改有效
-
同时,还可以找到占用CPU最多的线程,定位到具体的方法,优化这个方法的执行,看是否能避免某些对象的创建,从而节省内存
对于已经发生了OOM的系统:
- 一般生产系统中都会设置当系统发生了OOM时,生成当时的dump文件
(-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/usr/local/base)
-
我们可以利用jsisualvm等工具来分析dump文件
-
根据dump文件找到异常的实例对象,和异常的线程(占用CPU高),定位到具体的代码
-
然后再进行详细的分析和调试
2. cpu占满的情况以及解决方式
top+jstack
-
top 查看CPU占用率最高的进程
-
top -Hp PID 显示该进程里面线程信息
-
printf &x PID 把线程号转换成16进制
-
jstack cpu过高的进程
-
jstack PID > 1.txt 将信息输出到文件
-
cat 1.txt |grep -A 30 16进制 查看到问题
可能导致CPU占用率过高的问题
-
无线while循环
尽量避免无限循环
让循环执行的慢一点
-
频繁的GC
降低GC频率
-
频繁的创建对象
合理的使用单例
-
序列化和反序列化
选择合理的API实现功能
选择好用的序列化/反序列化类库
-
正则
2. 定位项目越来越慢的问题
可能性
-
Stop the World过长,GC回收过多
-
项目依赖的资源导致变慢
-
Code Cache满了
-
线程争抢过于激烈,导不能致线程老是抢不到CPU资源去执行
-
服务器出问题了,比方说操作系统(重启),其他进程抢占资源(中了木马,成了肉机,找到抢占资源的进程,直接kill)
3. 堆内存溢出
如:查询的时候直接返回很多的数据,导致堆内存溢出。
4. 方法区溢出
5. 工具
jps:查看jvm进程状态
jstat:监控JVM各种运行状态
jinfo:查看与调整JVM参数
jmap:展示对象内存映射或堆内存详细信息
jstack:打印线程快照
jhat:分析堆Dump
jstate:
一致性算法
1. Paxos
2. 一致性Hash
Zookeeper
1. 为啥需要Zookeeper
借鉴博文:zhuanlan.zhihu.com/p/69114539
2. 什么是ZAB协议
ZAB协议是Zookeeper用来实现一致性的原子广播协议,该协议描述了Zookeeper是如何实现一致性的,分为三个阶段:
-
领导者选举阶段:从Zookeeper集群中选出一个节点作为Leader,所有的写请求都会由Leader节点来处理
-
数据同步阶段:集群中所有节点中的数据要和Leader节点保持一致,如果不一致则要进行同步
-
请求广播阶段:当Leader节点接收到写请求时,会利用两阶段提交来广播该写请求,使得写请求像事务一样在其他节点上执行,达到节点上的数据实时一致
但值得注意的是,Zookeeper只是尽量的在达到强一致性,实际上仍然只是最终一致性的。
3. 为什么Zookeeper可以用来作为注册中心
可以利用Zookeeper的临时节点和watch机制来实现注册中心的自动注册和发现,另外Zookeeper中的数据都是存在内存中的,并且Zookeeper底层采用了nio,多线程模型,所以Zookeeper的性能也是比较高的,所以可以用来作为注册中心,但是如果考虑到注册中心应该是注册可用性的话,那么Zookeeper则不太合适,因为Zookeeper是CP的,它注重的是一致性,所以集群数据不一致时,集群将不可用,所以用Redis、Eureka、Nacos来作为注册中心将更合适。
4. Zookeeper中的领导者选举的流程是怎样的?
对于Zookeeper集群,整个集群需要从集群节点中选出一个节点作为Leader,大体流程如下:
-
集群中各个节点首先都是观望状态(LOOKING),一开始都会投票给自己,认为自己比较适合作为leader
-
然后相互交互投票,每个节点会收到其他节点发过来的选票,然后pk,先比较zxid,zxid大者获胜,zxid如果相等则比较myid,myid大者获胜
-
一个节点收到其他节点发过来的选票,经过PK后,如果PK输了,则改票,此节点就会投给zxid或myid更大的节点,并将选票放入自己的投票箱中,并将新的选票发送给其他节点
-
如果pk是平局则将接收到的选票放入自己的投票箱中
-
如果pk赢了,则忽略所接收到的选票
-
当然一个节点将一张选票放入到自己的投票箱之后,就会从投票箱中统计票数,看是否超过一半的节点都和自己所投的节点是一样的,如果超过半数,那么则认为当前自己所投的节点是leader
-
集群中每个节点都会经过同样的流程,pk的规则也是一样的,一旦改票就会告诉给其他服务器,所以最终各个节点中的投票箱中的选票也将是一样的,所以各个节点最终选出来的leader也是一样的,这样集群的leader就选举出来了
5. Zookeeper集群中节点之间数据是如何同步的
-
首先集群启动时,会先进行领导者选举,确定哪个节点是Leader,哪些节点是Follower和Observer
-
然后Leader会和其他节点进行数据同步,采用发送快照和发送Diff日志的方式
-
集群在工作过程中,所有的写请求都会交给Leader节点来进行处理,从节点只能处理读请求
-
Leader节点收到一个写请求时,会通过两阶段机制来处理
-
Leader节点会将该写请求对应的日志发送给其他Follower节点,并等待Follower节点持久化日志成功
-
Follower节点收到日志后会进行持久化,如果持久化成功则发送一个Ack给Leader节点
-
当Leader节点收到半数以上的Ack后,就会开始提交,先更新Leader节点本地的内存数据
-
然后发送commit命令给Follower节点,Follower节点收到commit命令后就会更新各自本地内存数据
-
同时Leader节点还是将当前写请求直接发送给Observer节点,Observer节点收到Leader发过来的写请求后直接执行更新本地内存数据
-
最后Leader节点返回客户端写请求响应成功
-
通过同步机制和两阶段提交机制来达到集群中节点数据一致
6. Zookeeper 工作原理(原子广播)
-
Zookeeper 的核心是原子广播,这个机制保证了各个 server 之间的同步。实现这个机制的协议叫做 Zab 协议。Zab 协议有两种模式,它们分别是恢复模式和广播模式。
-
当服务启动或者在领导者崩溃后,Zab 就进入了恢复模式,当领导者被选举出来,且大多数 server 的完成了和 leader 的状态同步以后,恢复模式就结束了。
-
状态同步保证了 leader 和 server 具有相同的系统状态
-
一旦 leader 已经和多数的 follower 进行了状态同步后,他就可以开始广播消息了,即进
入广播状态。这时候当一个 server 加入 zookeeper 服务中,它会在恢复模式下启动,发 现 leader,并和 leader 进行状态同步。待到同步结束,它也参与消息广播。Zookeeper服务一直维持在 Broadcast 状态,直到 leader 崩溃了或者 leader 失去了大部分的followers 支持。 -
广播模式需要保证 proposal 被按顺序处理,因此 zk 采用了递增的事务 id 号(zxid)来保 证。所有的提议(proposal)都在被提出的时候加上了 zxid。
-
实现中 zxid 是一个 64 为的数字,它高 32 位是 epoch 用来标识 leader 关系是否改变, 每次一个 leader 被选出来,它都会有一个新的 epoch。低 32 位是个递增计数。
-
当 leader 崩溃或者 leader 失去大多数的 follower,这时候 zk 进入恢复模式,恢复模式 需要重新选举出一个新的 leader,让所有的 server 都恢复到一个正确的状态。
Dubbo
1. 什么是RPC
RPC,表示远程过程调用,对于Java这种面试对象语言,也可以理解为远程方法调用,RPC调用和HTTP调用是有区别的,RPC表示的是一种调用远程方法的方式,可以使用HTTP协议、或直接基于TCP协议来实现RPC,在Java中,我们可以通过直接使用某个服务接口的代理对象来执行方法,而底层则通过构造HTTP请求来调用远端的方法,所以,有一种说法是RPC协议是HTTP协议之上的一种协议,也是可以理解的。
2. RPC原理
-
服务消费方(client)调用以本地调用方式调用服务;
-
client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体;
-
client stub找到服务地址,并将消息发送到服务端;
-
server stub收到消息后进行解码;
-
server stub根据解码结果调用本地的服务;
-
本地服务执行并将结果返回给server stub;
-
server stub将返回结果打包成消息并发送至消费方;
-
client stub接收到消息,并进行解码;
-
服务消费方得到最终结果。
3. 为什么要用dubbo
负载均衡——同一个服务部署在不同的机器时该调用那一台机器上的服务
服务调用链路生成——随着系统的发展,服务越来越多,服务间依赖关系变得错踪复杂,甚至分不清哪个应用
要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。Dubbo 可以为我们解决服务之间互相是如
何调用的。
服务访问压力以及时长统计、资源调度和治理——基于访问压力实时管理集群容量,提高集群利用率。
服务降级——某个服务挂掉之后调用备用服务
4. Dubbo 提供的负载均衡策略
-
随机:从多个服务提供者随机选择一个来处理本次请求,调用量越大则分布越均匀,并支持按权重设置随机概率
-
轮询:依次选择服务提供者来处理请求, 并支持按权重进行轮询,底层采用的是平滑加权轮询算法
-
最小活跃调用数:统计服务提供者当前正在处理的请求,下次请求过来则交给活跃数最小的服务器来处理
-
一致性哈希:相同参数的请求总是发到同一个服务提供者
5. Dubbo是如何完成服务导出的?
-
首先Dubbo会将程序员所使用的@DubboService注解或@Service注解进行解析得到程序员所定义的服务参数,包括定义的服务名、服务接口、服务超时时间、服务协议等等,得到一个ServiceBean。
-
然后调用ServiceBean的export方法进行服务导出
-
然后将服务信息注册到注册中心,如果有多个协议,多个注册中心,那就将服务按单个协议,单个注册中心进行注册
-
将服务信息注册到注册中心后,还会绑定一些监听器,监听动态配置中心的变更
-
还会根据服务协议启动对应的Web服务器或网络框架,比如Tomcat、Netty等
6. Dubbo是如何完成服务引入的?
-
当程序员使用@Reference注解来引入一个服务时,Dubbo会将注解和服务的信息解析出来,得到当前所引用的服务名、服务接口是什么
-
然后从注册中心进行查询服务信息,得到服务的提供者信息,并存在消费端的服务目录中
-
并绑定一些监听器用来监听动态配置中心的变更
-
然后根据查询得到的服务提供者信息生成一个服务接口的代理对象,并放入Spring容器中作为Bean
7. Dubbo的架构设计是怎样的?
Dubbo中的架构设计是非常优秀的,分为了很多层次,并且每层都是可以扩展的,比如:
-
Proxy服务代理层,支持JDK动态代理、javassist等代理机制
-
Registry注册中心层,支持Zookeeper、Redis等作为注册中心
-
Protocol远程调用层,支持Dubbo、Http等调用协议
-
Transport网络传输层,支持netty、mina等网络传输框架
-
Serialize数据序列化层,支持JSON、Hessian等序列化机制
各层说明
●config 配置层:对外配置接口,以 ServiceConfig, ReferenceConfig 为中心,可以直接初始化配置类,也可以通过 spring 解析配置生成配置类
●proxy 服务代理层:服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton, 以 ServiceProxy 为中心,扩展接口为 ProxyFactory
●registry 注册中心层:封装服务地址的注册与发现,以服务 URL 为中心,扩展接口为 RegistryFactory, Registry, RegistryService
●cluster 路由层:封装多个提供者的路由及负载均衡,并桥接注册中心,以 Invoker 为中心,扩展接口为 Cluster, Directory, Router, LoadBalance
●monitor 监控层:RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactory, Monitor, MonitorService
●protocol 远程调用层:封装 RPC 调用,以 Invocation, Result 为中心,扩展接口为 Protocol, Invoker, Exporter
●exchange 信息交换层:封装请求响应模式,同步转异步,以 Request, Response 为中心,扩展接口为 Exchanger, ExchangeChannel, ExchangeClient, ExchangeServer
●transport 网络传输层:抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel, Transporter, Client, Server, Codec
●serialize 数据序列化层:可复用的一些工具,扩展接口为 Serialization, ObjectInput, ObjectOutput, ThreadPool
关系说明
●在 RPC 中,Protocol 是核心层,也就是只要有 Protocol + Invoker + Exporter 就可以完成非透明的 RPC 调用,然后在 Invoker 的主过程上 Filter 拦截点。
●图中的 Consumer 和 Provider 是抽象概念,只是想让看图者更直观的了解哪些类分属于客户端与服务器端,不用 Client 和 Server 的原因是 Dubbo 在很多场景下都使用 Provider, Consumer, Registry, Monitor 划分逻辑拓普节点,保持统一概念。
●而 Cluster 是外围概念,所以 Cluster 的目的是将多个 Invoker 伪装成一个 Invoker,这样其它人只要关注 Protocol 层 Invoker 即可,加上 Cluster 或者去掉 Cluster 对其它层都不会造成影响,因为只有一个提供者时,是不需要 Cluster 的。
●Proxy 层封装了所有接口的透明化代理,而在其它层都以 Invoker 为中心,只有到了暴露给用户使用时,才用 Proxy 将 Invoker 转成接口,或将接口实现转成 Invoker,也就是去掉 Proxy 层 RPC 是可以 Run 的,只是不那么透明,不那么看起来像调本地服务一样调远程服务。
●而 Remoting 实现是 Dubbo 协议的实现,如果你选择 RMI 协议,整个 Remoting 都不会用上,Remoting 内部再划为 Transport 传输层和 Exchange 信息交换层,Transport 层只负责单向消息传输,是对 Mina, Netty, Grizzly 的抽象,它也可以扩展 UDP 传输,而 Exchange 层是在传输层之上封装了 Request-Response 语义。
●Registry 和 Monitor 实际上不算一层,而是一个独立的节点,只是为了全局概览,用层的方式画在一起。
Spring Cloud
1. Spring Cloud组件
-
Eureka:注册中心 各个服务启动时,Eureka Client都会将服务注册到Eureka Server,并且Eureka Client还可以反过来从Eureka Server拉取注册表,从而知道其他服务在哪里
-
Nacos:注册中心、配置中心
-
Consul:注册中心、配置中心
-
Spring Cloud Config:配置中心
-
Feign/OpenFeign:RPC调用 基于Feign的动态代理机制,根据注解和选择的机器,拼接请求URL地址,发起请求
-
Kong:服务网关
-
Zuul:如果前端、移动端要调用后端系统,统一从Zuul网关进入,由Zuul网关转发请求给对应的服务
-
Spring Cloud Gateway:服务网关
-
Ribbon:负载均衡
-
Spring CLoud Sleuth:链路追踪
-
Zipkin:链路追踪
-
Seata:分布式事务
-
Dubbo:RPC调用
-
Sentinel:服务熔断
-
Hystrix:服务熔断 发起请求是通过Hystrix的线程池来走的,不同的服务走不同的线程池,实现了不同服务调用的隔离,避免了服务雪崩的问题
-
Ribbon:服务间发起请求的时候,基于Ribbon做负载均衡,从一个服务的多台机器中选择一台
2. 熔断器 Hystrix 的原理与使用
借鉴博文:segmentfault.com/a/119000000…
3. Feign原理
Feign远程调用,核心就是通过一系列的封装和处理,将以JAVA注解的方式定义的远程调用API接口,最终转换成HTTP的请求形式,然后将HTTP的请求的响应结果,解码成JAVA Bean,放回给调用者。Feign远程调用的基本流程,大致如下图所示。
从上图可以看到,Feign通过处理注解,将请求模板化,当实际调用的时候,传入参数,根据参数再应用到请求上,进而转化成真正的 Request 请求。通过Feign以及JAVA的动态代理机制,使得Java 开发人员,可以不用通过HTTP框架去封装HTTP请求报文的方式,完成远程服务的HTTP调用。
4. Seata
借鉴博文:www.dreamwu.com/post-1741.h…
ES
1. Elasticsearch是如何实现Master选举的?
-
Elasticsearch的选主是ZenDiscovery模块负责的,主要包含Ping(节点之间通过这个RPC来发现彼此)和Unicast(单播模块包含一个主机列表以控制哪些节点需要ping通)这两部分;
-
对所有可以成为master的节点(node.master: true)根据nodeId字典排序,每次选举每个节点都把自己所知道节点排一次序,然后选出第一个(第0位)节点,暂且认为它是master节点。
-
如果对某个节点的投票数达到一定的值(可以成为master节点数n/2+1)并且该节点自己也选举自己,那这个节点就是master。否则重新选举一直到满足上述条件。
-
补充:master节点的职责主要包括集群、节点和索引的管理,不负责文档级别的管理;data节点可以关闭http功能。
2. Elasticsearch中的节点(比如共20个),其中的10个选了一个master,另外10个选了另一个master,怎么办?
-
当集群master候选数量不小于3个时,可以通过设置最少投票通过数量(discovery.zen.minimum_master_nodes)超过所有候选节点一半以上来解决脑裂问题;
-
当候选数量为两个时,只能修改为唯一的一个master候选,其他作为data节点,避免脑裂问题。
3. 倒排索引
把文件ID对应到关键词的映射转换为关键词到文件ID的映射,每个关键词都对应着一系列的文件,这些文件中都出现这个关键词。
得到倒排索引的结构如下:
“关键词1”:“文档1”的ID,“文档2”的ID,…………。
“关键词2”:带有此关键词的文档ID列表。
从词的关键字,去找文档。
4. es索引优化
借鉴博文:www.elastic.co/guide/cn/el…
网络
1. TCP的三次握手和四次挥手
TCP协议是7层网络协议中的传输层协议,负责数据的可靠传输。 在建立TCP连接时,需要通过三次握手来建立,过程是:
-
客户端向服务端发送一个SYN
-
服务端接收到SYN后,给客户端发送一个SYN_ACK
-
客户端接收到SYN_ACK后,再给服务端发送一个ACK
在断开TCP连接时,需要通过四次挥手来断开,过程是:
-
客户端向服务端发送FIN
-
服务端接收FIN后,向客户端发送ACK,表示我接收到了断开连接的请求,客户端你可以不发数据了,不过服务端这边可能还有数据正在处理
-
服务端处理完所有数据后,向客户端发送FIN,表示服务端现在可以断开连接
-
客户端收到服务端的FIN,向服务端发送ACK,表示客户端也会断开连接了
2. TCP和UDP协议区别
借鉴博文:www.jiangmen.gov.cn/bmpd/jmszwf…
3. TCP拆包粘包
借鉴博文:zhuanlan.zhihu.com/p/77275039
4. BIO/NIO/AIO
借鉴博文:www.huaweicloud.com/articles/e3…
场景题
1. 库存扣减表设计
2. 多数据源
3. 阻塞队列的实现
4. 设计下哈罗是怎么扫码开锁的流程
5. 重复支付如何解决
6. 高并发类
设计一个能亿级并发的系统
借鉴博文: blog.csdn.net/wenlin_xie/…
7. 数据处理类
7.1 海量数据场景处理思路
-
分而治之/hash映射 + hash统计 + 堆/快速/归并排序
-
双层桶划分
-
Bloom filter / Bitmap
-
Trie树/数据库/倒排索引
-
外排序
-
分布式处理
借鉴博文:www.cnblogs.com/GarrettWale…
7.2 海量数据排序
1TB的数据需要排序,限定使用32GB的内存如何处理?
例如,考虑一个 1G 文件,只可用内存 100M 的排序方法。首先将文件分成 10 个 100M ,并依次载入内存中进行排序,最后结果存入硬盘。得到的是 10 个分别排序的文件。接着从每个文件载入 9M 的数据到输入缓存区,输出缓存区大小为 10M 。对输入缓存区的数据进行归并排序,输出缓存区写满之后写在硬盘上,缓存区清空继续写接下来的数据。对于输入缓存区,当一个块的 9M 数据全部使用完,载入该块接下来的 9M 数据,一直到所有的 9 个块的所有数据都已经被载入到内存中被处理过。最后我们得到的是一个 1G 的排序好的存在硬盘上的文件。
解法:
-
把磁盘上的 1TB 数据分割为 40 块,每份 25GB。注意,要留一些系统空间!
-
顺序将每份 25GB 数据读入内存,使用 quick sort 算法排序。
-
把排序好的数据存放回磁盘。
-
循环 40 次,现在,所有的 40 个块都已经各自排序了。
-
从 40 个块中分别读取 25G/40 = 0.625G入内存
-
执行 40 路合并,并将合并结果临时存储于2GB 基于内存的输出缓冲区中。当缓冲区写满 2GB 时,写入硬盘上最终文件,并清空输出缓冲区当
-
40 个输入缓冲区中任何一个处理完毕时,写入该缓冲区所对应的块中的下一个 0.625GB ,直到全部处理完成。
继续优化:
-
使用并发:如多磁盘(并发I/O提高)、多线程、使用异步 I/O 、使用多台主机集群计算
-
提升硬件性能:如更大内存、更高 RPM 的磁盘、升级为 SSD 、 Flash 、使用更多核的 CPU
-
提高软件性能:比如采用 radix sort 、压缩文件(提高I/O效率)等
7.3 海量数据查询
海量日志数据,提取出某日访问百度次数最多的那个IP
首先过滤日志文件,将某日的 ip 过滤出来,保存在一个大文件内。然后对这些 ip 求 Hash 值,得到 Hash 值后再对 1000 取模,然后将计算后的值作为该 ip 写入的目标文件下标。上述操作中,每个 IP 仅会保存在 1000 个小文件中的某一个文件中。每个小文件中的数据以键值对的形式保存。最后可从这 1000 个小文件中找到一个或几个出现概率最大的 IP 。
海量日志数据,提取出搜索量前十的查询串,每个查询串的长度为1-255字节,要求内存不能超过1G(Top-K)
虽然有一千万 个Query,但是由于重复度比较高,因此事实上只有 300 万的 Query ,每个 Query 最大 255 Byte,因此我们可以考虑把他们都放进内存中去,而现在只是需要一个合适的数据结构,在这里, Hash 表是优先的选择。所以摒弃分而治之或 hash 映射的方法,直接上 hash 统计,然后排序。
一个1G大小的一个文件,里面每一行是一个词,词的大小不超过16字节,内存限制大小是1M。返回频数最高的100个词。
顺序读取文件,对于每个词 x,取 Hash(x)%5000 ,存放到对应的 5000 个小文件中。这样,每个小文件大概为 200k 左右。然后加载每一个小文件并做Hash统计。最后使用堆排序取出前 100 个高频词,使用归并排序进行总排序。
给定a、b两个文件,各存放50亿个url,每个url各占64字节,内存限制是4G,让你找出a、b文件共同的url?
顺序读取文件 a ,对每个 url 做如下操作:求 Hash(url)%1000 ,然后根据该值将 url 存放到 1000 个小文件中的某一个小文件并记录出现次数。按照这样划分,每个小文件的大小大约 300M 。同理,对文件 b 做上述操作。此时,如果存在相同的 url ,那么 url 一定会出现在同一个下标内的文件中。接下来只需要遍历这 1000 个文件即可。
2.5亿个整数中找出不重复的整数的个数,内存空间不足以容纳这2.5亿个整数
采用 2-Bitmap(每个数分配2bit,00表示不存在,01表示出现一次,10表示多次,11无意义)进行,共需内存 2^32*2bit=1GB 内存,还可以接受。然后扫描这 2.5 亿个整数,查看 Bitmap 中相对应位,如果是 00 变 01 , 01 变 10 , 10 保持不变。扫描后,查看 bitmap ,把对应位是 01 的整数输出即可。
5亿个int找它们的中位数
首先我们将 int 划分为 2^16 个区域,然后读取数据统计落到各个区域里的数的个数,之后我们根据统计结果就可以判断中位数落到那个区域,同时知道这个区域中的第几大数刚好是中位数。然后第二次扫描我们只统计落在这个区域中的那些数就可以了。
实际上,如果不是 int 是 int64 ,我们可以经过 3 次这样的划分即可降低到可以接受的程度。即可以先将 int64 分成 2^24 个区域,然后确定区域的第几大数,在将该区域分成 2^20 个子区域,然后确定是子区域的第几大数,然后子区域里的数的个数只有 2^20 ,就可以直接利用direct addr table 进行统计了。
已知某个文件内包含一些电话号码,每个号码为8位数字,统计不同号码的个数
8 位最多99 999 999,大概需要 99m 个bit,大概 10m 字节的内存即可。 (可以理解为从0-99 999 999的数字,每个数字对应一个Bit位,所以只需要 99m 个Bit==1.2MBytes,这样,就用了小小的 1.2M 左右的内存表示了所有的8位数的电话)
给40亿个不重复的unsigned int的整数,没排过序的,然后再给一个数,如何快速判断这个数是否在那40亿个数当中?
申请 512M 的内存,一个 bit 位代表一个 unsigned int 值。读入 40 亿个数,设置相应的 bit 位,读入要查询的数,查看相应 bit 位是否为 1 ,为 1 表示存在,为 0 表示不存在。