工作两年多,XX 征信 面试,offer已到手
回复“000”获取程序员必备电子书
大家好,我是老田,今天给大家分享的是一位两年多工作经验的小伙伴面试经历,恭喜他成功上岸,收到了offer!本文大部分内容是这位朋友所写,我对一小部分内容进行修正和调整,话不多说,咱们直入主题。
面试官到了后,看着简历,然后来一句千年不变的:先做一个自我介绍
,然后我就大致做了一个简短的自我介绍。随后面试官,随便问了为什么离职?还问了项目情况,最后开始技术面试。
1、说说你对HashMap的理解
这道题,有点泛,所以只能把自己知道都回答一遍,能回答越多越好。
关于这道题,我们可以从几个方面去回答:
数据结构
JDK1.7之前采用的是数组+链表。
JDK1.8后采用的是数组+链表(红黑树),当链表的长度大于8,并且数组长度为64时,如果再往此链表上添加数据,那么该链表就会转为红黑树。
put方法过程
看流程图,这样印象更深刻:
线程安全问题
在多线程环境下,1.7 会产生死循环、数据丢失、数据覆盖的问题,1.8 中会有数据覆盖的问题,以 1.8 为例,当 A 线程判断 index 位置为空后正好挂起,B 线程开始往 index 位置的写入节点数据,这时 A 线程恢复现场,执行赋值操作,就把 A 线程的数据给覆盖了;还有++size 这个地方也会造成多线程同时扩容等问题。
2、hash冲突解决方案有哪些
这个题是知道就知道,不知道就是不知道,理论性很强,说白了背就得了。
一共有四种方法:
1、再哈希法:如果hash出的index已经有值,就再hash,不行继续hash,直至找到空的index位置,要相信瞎猫总能碰上死耗子。这个办法最容易想到。但有2个缺点:
比较浪费空间,消耗效率。根本原因还是数组的长度是固定不变的,不断hash找出空的index,可能越界,这时就要创建新数组,而老数组的数据也需要迁移。随着数组越来越大,消耗不可小觑。 get不到,或者说get算法复杂。进是进去了,想出来就没那么容易了。
2、开放地址方法:如果hash出的index已经有值,通过算法在它前面或后面的若干位置寻找空位,这个和再hash算法差别不大。
3、建立公共溢出区: 把冲突的hash值放到另外一块溢出区。
4、链式地址法: 把产生hash冲突的hash值以链表形式存储在index位置上。HashMap用的就是该方法。优点是不需要另外开辟新空间,也不会丢失数据,寻址也比较简单。但是随着hash链越来越长,寻址也是更加耗时。好的hash算法就是要让链尽量短,最好一个index上只有一个值。也就是尽可能地保证散列地址分布均匀,同时要计算简单。
3、说说你对Spring IOC的理解
传说中的傻屌面试题,但是又不得不问的面试题,所以一定要回答的好,给面试官一个好的印象。
IOC就是控制反转,是指创建对象的控制权的转移。以前创建对象的主动权和时机是由自己把控的,而现在这种权力转移到Spring容器中,并由容器根据配置文件去创建实例和管理各个实例之间的依赖关系。对象与对象之间松散耦合,也利于功能的复用。DI依赖注入,和控制反转是同一个概念的不同角度的描述,即 应用程序在运行时依赖IoC容器来动态注入对象需要的外部资源。
最直观的表达就是,IOC让对象的创建不用去new了,可以由spring自动生产,使用java的反射机制,根据配置文件在运行时动态的去创建对象以及管理对象,并调用对象的方法的。
Spring的IOC有三种注入方式 :构造器注入、setter方法注入、根据注解注入。
IoC让相互协作的组件保持松散的耦合,而AOP编程允许你把遍布于应用各层的功能分离出来形成可重用的功能组件。
4、Spring AOP在工作中有用过吗?
应该是后端开发必备,不管是吹牛逼,还是实战,这都是必备的。
有用过。
AOP(Aspect-Oriented Programming,面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可扩展性和可维护性。
Spring AOP是基于动态代理的,如果要代理的对象实现了某个接口,那么Spring AOP就会使用JDK动态代理去创建代理对象;而对于没有实现接口的对象,就无法使用JDK动态代理,转而使用CGlib动态代理生成一个被代理对象的子类来作为代理。
当然也可以使用AspectJ,Spring AOP中已经集成了AspectJ,AspectJ应该算得上是Java生态系统中最完整的AOP框架了。使用AOP之后我们可以把一些通用功能抽象出来,在需要用到的地方直接使用即可,这样可以大大简化代码量。我们需要增加新功能也方便,提高了系统的扩展性。日志功能、事务管理和权限管理等场景都用到了AOP。
5、@Controller和@RestController有什么区别?
这是个基础性的问题,但天天都在用,却没有关心过这两者的区别。
@RestController
为spring 4.0.1
版本后新增的内容,@Controller
为spring 2.5.0
版本后新增的内容。两者在实际使用中都用于定义控制层,用于控制业务逻辑层的跳转。
对比源码可知@RestController
相对@Controller
增加了@RestponseBody
注释。
@RestController
相对于@Controller
增加了@ResponseBody
返回机制。相当于@Controller+@ResponseBody
两个注解的结合,返回json
数据不需要在方法前面加@ResponseBody
注解了,但使用@RestController
这个注解,就不能返回jsp,html
页面,视图解析器无法解析jsp,html
页面。
6、熟悉Bean的生命周期吗?
造轮子必备,写码业务代码的基本上不太关心这种,但是面试被问的概率却是相当的高。
首先说一下Servlet的生命周期:实例化,初始init,接收请求service,销毁destroy;
Spring上下文中的Bean生命周期也类似,如下:
(1)实例化Bean:
对于BeanFactory容器,当客户向容器请求一个尚未初始化的bean时,或初始化bean的时候需要注入另一个尚未初始化的依赖时,容器就会调用createBean进行实例化。对于ApplicationContext容器,当容器启动结束后,通过获取BeanDefinition对象中的信息,实例化所有的bean。
(2)设置对象属性(依赖注入):
实例化后的对象被封装在BeanWrapper对象中,紧接着,Spring根据BeanDefinition中的信息 以及 通过BeanWrapper提供的设置属性的接口完成依赖注入。
(3)处理Aware接口:
接着,Spring会检测该对象是否实现了xxxAware接口,并将相关的xxxAware实例注入给Bean:
①如果这个Bean已经实现了BeanNameAware接口,会调用它实现的setBeanName(String beanId)方法,此处传递的就是Spring配置文件中Bean的id值;
②如果这个Bean已经实现了BeanFactoryAware接口,会调用它实现的setBeanFactory()方法,传递的是Spring工厂自身。
③如果这个Bean已经实现了ApplicationContextAware接口,会调用setApplicationContext(ApplicationContext)方法,传入Spring上下文;
(4)BeanPostProcessor:
如果想对Bean进行一些自定义的处理,那么可以让Bean实现了BeanPostProcessor接口,那将会调用postProcessBeforeInitialization(Object obj, String s)方法。
(5)InitializingBean 与 init-method:
如果Bean在Spring配置文件中配置了 init-method 属性,则会自动调用其配置的初始化方法。
(6)如果这个Bean实现了BeanPostProcessor接口,将会调用postProcessAfterInitialization(Object obj, String s)方法;由于这个方法是在Bean初始化结束时调用的,所以可以被应用于内存或缓存技术;
以上几个步骤完成后,Bean就已经被正确创建了,之后就可以使用这个Bean了。
(7)DisposableBean:
当Bean不再需要时,会经过清理阶段,如果Bean实现了DisposableBean这个接口,会调用其实现的destroy()方法;
(8)destroy-method:
最后,如果这个Bean的Spring配置中配置了destroy-method属性,会自动调用其配置的销毁方法。
7、说说Synchronized和ReentrantLock的区别
看样子是在考察一个很平常的题目,但是面试官可能会因为你回答了两者区别后,引发其他相关问题,也有可能葬送了大好机会。
相似点
这两种同步方式有很多相似之处,它们都是加锁方式同步,而且都是阻塞式的同步,也就是说当如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的.
区别
这两种方式最大区别就是对于Synchronized来说,它是java语言的关键字,是原生语法层面的互斥,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。
synchronized经过编译,会在同步块的前后分别形成monitorenter和monitorexit这个两个字节码指令。在执行monitorenter指令时,首先要尝试获取对象锁。如果这个对象没被锁定,或者当前线程已经拥有了那个对象锁,把锁的计算器加1,相应的,在执行monitorexit指令时会将锁计算器就减1,当计算器为0时,锁就被释放了。如果获取对象锁失败,那当前线程就要阻塞,直到对象锁被另一个线程释放为止。
由于ReentrantLock是java.util.concurrent包下提供的一套互斥锁,相比Synchronized,ReentrantLock类提供了一些高级功能,主要有以下3项:
1.等待可中断,持有锁的线程长期不释放的时候,正在等待的线程可以选择放弃等待,这相当于Synchronized来说可以避免出现死锁的情况。
2.公平锁,多个线程等待同一个锁时,必须按照申请锁的时间顺序获得锁,Synchronized锁非公平锁,ReentrantLock默认的构造函数是创建的非公平锁,可以通过参数true设为公平锁,但公平锁表现的性能不是很好。
3.锁绑定多个条件,一个ReentrantLock对象可以同时绑定对个对象。
8、了解volatile关键字吗?
这种题目,简单也可以说,详细点也可以说,深层次的也可以说。就看面试官需要你回答那个层次。
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1.保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。2.禁止进行指令重排序。 volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。 volatile仅能使用在变量级别;synchronized则可以使用在变量、方法、和类级别的。 volatile仅能实现变量的修改可见性,并不能保证原子性;synchronized则可以保证变量的修改可见性和原子性。 volatile不会造成线程的阻塞;synchronized可能会造成线程的阻塞。
volatile标记的变量不会被编译器优化;synchronized标记的变量可以被编译器优化。
9、说说你对并发编程中CAS的理解
千万别把并发编程中的CAS和单点登录搞混了哈
CAS叫做CompareAndSwap,比较并交换,主要是通过处理器的指令来保证操作的原子性,它包含三个操作数:
变量内存地址,V表示 旧的预期值,A表示 准备设置的新值,B表示
当执行CAS指令时,只有当V等于A时,才会用B去更新V的值,否则就不会执行更新操作。
CAS的缺点主要有3点
ABA问题:ABA的问题指的是在CAS更新的过程中,当读取到的值是A,然后准备赋值的时候仍然是A,但是实际上有可能A的值被改成了B,然后又被改回了A,这个CAS更新的漏洞就叫做ABA。只是ABA的问题大部分场景下都不影响并发的最终效果。
Java中有AtomicStampedReference
来解决这个问题,他加入了预期标志和更新后标志两个字段,更新时不光检查值,还要检查当前的标志是否等于预期标志,全部相等的话才会更新。
循环时间长开销大:自旋CAS的方式如果长时间不成功,会给CPU带来很大的开销。
只能保证一个共享变量的原子操作:只对一个共享变量操作可以保证原子性,但是多个则不行,多个可以通过AtomicReference
来处理或者使用锁synchronized实现。
9、线程有哪些状态?
这个题目网上答案都有些差别,有的说是5个有的是6个。很多是跟着别人写,但是写偏了。五种有五种的说法,六种有六种的说法,我们是Java开发,肯定关注的是Java中线程的状态,并且在Thread类中有个内部类State就是表示线程状态的,注释里还有每个状态的相关解释。
Java中线程的状态分为6种。
初始(NEW):新创建了一个线程对象,但还没有调用start()方法。 运行(RUNNABLE):Java线程中将就绪(ready)和运行中(running)两种状态笼统的称为“运行”。线程对象创建后,其他线程(比如main线程)调用了该对象的start()方法。该状态的线程位于可运行线程池中,等待被线程调度选中,获取CPU的使用权,此时处于就绪状态(ready)。就绪状态的线程在获得CPU时间片后变为运行中状态(running)。 阻塞(BLOCKED):表示线程阻塞于锁。 等待(WAITING):进入该状态的线程需要等待其他线程做出一些特定动作(通知或中断)。 超时等待(TIMED_WAITING):该状态不同于WAITING,它可以在指定的时间后自行返回。 终止(TERMINATED):表示该线程已经执行完毕。
想获取面试官的青睐,还是得说说线程的状态流转,可以根据下面这张图来描述:
10、有用过线程池吗?是怎么用的?
这个线程池在面试中也基本上是必问的题目。
有用过,
创建线程有两种方式
ThreadPoolExecutor Executors
使用ThreadPoolExecutor是JDK原生态创建线程池,也可以使用Executors工具类来创建线程池,并Executors大多数都是基于ThreadPoolExecutor进行二次封装。
以下是Executors方式创建线程池的几种方式:
newSingleThreadExecutor:创建一个单线程的线程池,此线程池保证所有任务的执行顺序按照任务的提交顺序执行。 newFixedThreadPool:创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。 newCachedThreadPool:创建一个可缓存的线程池,此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。 newScheduledThreadPool:创建一个大小无限的线程池,此线程池支持定时以及周期性执行任务的需求。 newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。 newWorkStealingPoo0l:Java 8 新增创建线程池的方法,创建时如果不设置任何参数,则以当前机器CPU 处理器数作为线程个数,此线程池会并行处理任务,不能保证执行顺序。
通常不建议使用Executors来创建线程池,因为该方式中很多参数都已经给你设置好了,所以在使用的时候,如果使用不当或者对参数没有认证考察可能会产生很多意想不到的问题:比如队列多大,造成OOM
等。所以,通常都建议使用ThreadPoolExecutor创建线程池。
11、说说线程池中那几个核心参数和含义
这道题目,基本上快成必考面试题了,不过我还是记住了,还有就是在某些面试中,喜欢问线程池的原理,其实也是侧面的让你讲解线程池的这些核心参数的含义。
corePoolSize:核心线程数
线程池维护的最小线程数量,核心线程创建后不会被回收(注意:设置allowCoreThreadTimeout=true后,空闲的核心线程超过存活时间也会被回收)。
大于核心线程数的线程,在空闲时间超过keepAliveTime后会被回收。
线程池刚创建时,里面没有一个线程,当调用 execute() 方法添加一个任务时,如果正在运行的线程数量小于
corePoolSize,则马上创建新线程并运行这个任务。
maximumPoolSize:最大线程数
线程池允许创建的最大线程数量。
当添加一个任务时,核心线程数已满,线程池还没达到最大线程数,并且没有空闲线程,工作队列已满的情况下,创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。
keepAliveTime:空闲线程存活时间
当一个可被回收的线程的空闲时间大于keepAliveTime,就会被回收。
可被回收的线程:
设置allowCoreThreadTimeout=true的核心线程。 大于核心线程数的线程(非核心线程)。
unit:时间单位
keepAliveTime的时间单位:
TimeUnit.NANOSECONDS
TimeUnit.MICROSECONDS
TimeUnit.MILLISECONDS // 毫秒
TimeUnit.SECONDS
TimeUnit.MINUTES
TimeUnit.HOURS
TimeUnit.DAYS
workQueue:工作队列
新任务被提交后,会先添加到工作队列,任务调度时再从队列中取出任务。工作队列实现了BlockingQueue接口。
JDK默认的工作队列有五种:
ArrayBlockingQueue 数组型阻塞队列:数组结构,初始化时传入大小,有界,FIFO,使用一个重入锁,默认使用非公平锁,入队和出队共用一个锁,互斥。 LinkedBlockingQueue 链表型阻塞队列:链表结构,默认初始化大小为Integer.MAX_VALUE,有界(近似无解),FIFO,使用两个重入锁分别控制元素的入队和出队,用Condition进行线程间的唤醒和等待。 SynchronousQueue 同步队列:容量为0,添加任务必须等待取出任务,这个队列相当于通道,不存储元素。 PriorityBlockingQueue 优先阻塞队列:无界,默认采用元素自然顺序升序排列。 DelayQueue 延时队列:无界,元素有过期时间,过期的元素才能被取出。
threadFactory:线程工厂
创建线程的工厂,可以设定线程名、线程编号等。
12、有了解过JVM吗?
没有了解过JVM的,这道题是吱吱呜呜的,瞎说一通,然后尴尬的结束。
JVM是Java Virtual Machine(Java虚拟机)的缩写 ,JVM在执行Java程序时,会把它管理的内存划分为若干个的区域,每个区域都有自己的用途和创建销毁时间。如下图所示,可以分为两大部分,线程私有区和共享区。
下图是根据自己理解画的一个JVM内存模型架构图:
JVM内存分为线程私有区和线程共享区。
线程私有区
1、程序计数器
当同时进行的线程数超过CPU数或其内核数时,就要通过时间片轮询分派CPU的时间资源,不免发生线程切换。这时,每个线程就需要一个属于自己的计数器来记录下一条要运行的指令。如果执行的是JAVA方法,计数器记录正在执行的java字节码地址,如果执行的是native方法,则计数器为空。
2、虚拟机栈
线程私有的,与线程在同一时间创建。管理JAVA方法执行的内存模型。每个方法执行时都会创建一个桢栈来存储方法的的变量表、操作数栈、动态链接方法、返回值、返回地址等信息。栈的大小决定了方法调用的可达深度(递归多少层次,或嵌套调用多少层其他方法,-Xss
参数可以设置虚拟机栈大小),栈的大小可以是固定也可以动态扩展。
如果请求的栈深度大于最大可用深度,则抛出stackOverflowError
;
如果栈是可动态扩展的,但没有内存空间支持扩展,则抛出OutofMemoryError
。
使用jclasslib工具可以查看class类文件的结构。下图为栈帧结构图:
一个线程对应一个虚拟机栈,一个虚拟机栈对应多个栈帧,每个栈帧的的入栈和出栈表示一个方法的调用。
3、本地方法栈
与虚拟机栈作用相似。但它不是为Java方法服务的,而是本地方法(C语言)。由于规范对这块没有强制要求,不同虚拟机实现方法不同。
线程共享区
1、方法区
线程共享的,用于存放被虚拟机加载的类的元数据信息,如常量、静态变量和即时编译器编译后的代码。若要分代,算是永久代(老年代),以前类大多“static”的,很少被卸载或收集,现回收废弃常量和无用的类。其中运行时常量池存放编译生成的各种常量。(如果hotspot虚拟机确定一个类的定义信息不会被使用,也会将其回收。回收的基本条件至少有:所有该类的实例被回收,而且装载该类的ClassLoader被回收)。
2、堆
存放对象实例和数组,是垃圾回收的主要区域,分为新生代和老年代。刚创建的对象在新生代的Eden区中,经过GC后进入新生代的S0区中,再经过GC进入新生代的S1区中,15次GC后仍存在就进入老年代。这是按照一种回收机制进行划分的,不是固定的。若堆的空间不够实例分配,则OutOfMemoryError
。
13、类加载机制是什么?
更偏向于类装载,因为在描述这个过程的时候。,第一步就是加载,这样更容易辨别。
JVM类加载分为5个过程:加载,验证,准备,解析,初始化,使用,卸载,如下图所示:
下面来看看加载,验证,准备,解析,初始化这5个过程的具体动作。
加载
加载主要是将.class文件(并不一定是.class。可以是ZIP包,网络中获取)中的二进制字节流读入到JVM中。在加载阶段,JVM需要完成3件事:1)通过类的全限定名获取该类的二进制字节流;2)将字节流所代表的静态存储结构转化为方法区的运行时数据结构;3)在内存中生成一个该类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
连接
验证
验证是连接阶段的第一步,主要确保加载进来的字节流符合JVM规范。验证阶段会完成以下4个阶段的检验动作:1)文件格式验证 2)元数据验证(是否符合Java语言规范) 3)字节码验证(确定程序语义合法,符合逻辑) 4)符号引用验证(确保下一步的解析能正常执行)
准备
主要为静态变量在方法区分配内存,并设置默认初始值。
解析
是虚拟机将常量池内的符号引用替换为直接引用的过程。
初始化
初始化阶段是类加载过程的最后一步,主要是根据程序中的赋值语句主动为类变量赋值。注:1)当有父类且父类为初始化的时候,先去初始化父类;2)再进行子类初始化语句。
14、垃圾回收算法有哪些?
我见过有的面试官,上来啥也不问,直接问垃圾回收算法有哪些,随便看看,学学就知道了。
GC最基础的算法有三种:标记 -清除算法、复制算法、标记-压缩算法。
我们常用的垃圾回收器一般都采用分代收集算法,然后针对不同的代进行使用不同的算法。
标记 -清除算法,“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。 复制算法,“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。 标记-压缩算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存 分代收集算法,“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
15、熟悉哪些JVM调优参数?
不能说我们要把下面的全部背下来,但是最起码的堆相关参数的应该有所知道,肯定知道的越多越好。
「堆栈内存相关」
-Xms 设置初始堆的大小 -Xmx 设置最大堆的大小 -Xmn 设置年轻代大小,相当于同时配置-XX:NewSize和-XX:MaxNewSize为一样的值 -Xss 每个线程的堆栈大小 -XX:NewSize 设置年轻代大小(for 1.3/1.4) -XX:MaxNewSize 年轻代最大值(for 1.3/1.4) -XX:NewRatio 年轻代与年老代的比值(除去持久代) -XX:SurvivorRatio Eden区与Survivor区的的比值 -XX:PretenureSizeThreshold 当创建的对象超过指定大小时,直接把对象分配在老年代。 -XX:MaxTenuringThreshold设定对象在Survivor复制的最大年龄阈值,超过阈值转移到老年代
「垃圾收集器相关」
-XX:+UseParallelGC:选择垃圾收集器为并行收集器。
-XX:ParallelGCThreads=20:配置并行收集器的线程数 -XX:+UseConcMarkSweepGC:设置年老代为并发收集。 -XX:CMSFullGCsBeforeCompaction=5 由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行5次GC以后对内存空间进行压缩、整理。 -XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片
「辅助信息相关」
-XX:+PrintGCDetails 打印GC详细信息 -XX:+HeapDumpOnOutOfMemoryError让JVM在发生内存溢出的时候自动生成内存快照,排查问题用 -XX:+DisableExplicitGC禁止系统System.gc(),防止手动误触发FGC造成问题. -XX:+PrintTLAB 查看TLAB空间的使用情况
16、熟悉分布式锁吗?有哪些实现方案?
变向的喜欢问,你们项目中有用到分布式锁吗?是怎么使用的,为什么这么使用?这么使用会不会有问题,有什么问题,遇到这问题了怎么解决?
项目中有用到分布式锁,使用Zookeeper
的实现方案。
分布式锁实现方案,常见有如下几种:
基于表主键唯一做分布式锁 通过数据库 mvcc
实现乐观锁基于 Redis
实现分布式锁基于 Redlock
算法实现分布式锁基于 Zookeeper
实现分布式锁
17、哪一种方案是最好的?
没有最好的方案,只有相对最优方案。
1、Redis
的分布式锁中redisson
一般为单实例,当单实例不可用时,会阻塞业务流程。主从方式、主从数据异步,会存在锁失效的问题。RedLock
一般要求至少3台以上的redis
主从实例,维护成本相对来说比较高。
2、ZK锁具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。但是因为需要频繁的创建和删除节点,性能上不如Redis方式。
3、 ETCD
分布式锁的实现原理与zk锁类似,但是ETCD
分布式锁更加可靠强大。其Lease功能保证分布式锁的安全性;watch功能支持监听某个固定的key,也支持watch一个范围的key(前缀机制);revision功能可通过 Revision 的大小就可以知道进行写操作的顺序。可以避免 “羊群效应” (也称 “惊群效应”),实现公平锁。前缀机制与watch功能配合使用解决了死锁问题。总之ETCD
的灵感来源于Zookeeper
,但实现的时候做了很多的改进,如:高负载下的稳定读写、数据模型的多版本并发控制、稳定的watch功能,通知订阅者监听值得变化、可以容忍脑裂现场的发生、客户端的协议使用gRPC
协议,支持go
、c++
、Java
等。
4、数据库实现分布式锁,对数据库表侵入较大,每个表需要增加version等字段,高并发下存在很多更新失败。数据库写入是磁盘io,性能方面差一些。数据库能支持的最大QPS
也有限制,很难满足高并发的需要。
总结
小伙伴本次面试中发挥的还是挺好的,最终收获offer,恭喜这位朋友。
最后,希望大家平时就算不面试,也要为日后的面试做准备,做一个能进能退的人。
机会总是留给有准备的人,加油!