# 总结的题目

# 0.简历上写的一定要会

# 1.Spring事务是怎么实现的,事务失效的情况分析

在进行事务支持时,Spring只需要使用者在需要事务支持的bean上使用@Transactional注解即可,如果需要修改事务的隔离级别和传播特性的属性,则使用该注解中的属性进行指定。

失效情况

  1. 注解不是在public上
    1. 基于动态代理实现的,创建代理对象时候,如果不是public的,就认为Transactional信息为空,就不会进行代理了.
  2. 在一个类内部调用事务方法
    1. 如果是内部调用,那么也不是通过代理对象来调用,而是通过this对象进行调用.绕过了代理对象.肯定没有代理逻辑了.
  3. 事务内部捕捉了异常,没有抛出新的异常
    1. 通过源码解析看到,事务回滚必须捕捉到异常,但是异常中途被捕捉,事务将不会回滚

事务的隔离级别

1. @Transactional(isolation = Isolation.READ_UNCOMMITTED):读取未提交数据(会出现脏读,不可重复读) 基本不使用
2. @Transactional(isolation = Isolation.READ_COMMITTED):读取已提交数据(会出现不可重复读和幻读)
3. @Transactional(isolation = Isolation.REPEATABLE_READ):可重复读(会出现幻读)
4. @Transactional(isolation = Isolation.SERIALIZABLE):串行化

# 2.zookeeper的锁如何应用

Zookeeper 分布式锁的原理

Zookeeper 分布式锁恰恰应用了临时顺序节点。

  1. 首先,在 Zookeeper 当中创建一个持久节点 ParentLock。
  2. 每个客户端来了后,需要在这个节点下创建一个有序临时节点,并且判断他这个节点是不是最小的一个,如果是,那么代表他获取锁成功.
  3. 剩下的客户端创建的有序临时节点,按顺序排好,并设置一个watch来观察前一个节点,这个时候,可以有序维持一个队列.

释放锁分为两种情况:

  1. 任务完成,客户端显示释放

    当任务完成时,Client1 会显示调用删除节点 Lock1 的指令。

  2. 任务执行过程中,客户端崩溃

    获得锁的 Client1 在任务执行过程中,如果崩溃,则会断开与 Zookeeper 服务端的链接。根据临时节点的特性,相关联的节点 Lock1 会随之自动删除。

Zookeeper 和 Redis 分布式锁的比较

zookeeper:****

优点. zookeeper有封装好的框架,容易实现,有等待锁队列,抢锁效率高.

缺点,频繁创建删除节点性能较低.

redis

优点.

缺点,轮询setnx.处理死锁问题等等.

# 3.MySql和Oracle的区别

# 一、宏观上:

1、Oracle是大型的数据库而Mysql是中小型数据库;Mysql是开源的,Oracle是收费的,且价格昂贵。

2、Oracle支持大并发,大访问量,是OLTP的最好的工具。

3、安装占用的内存也是有差别,Mysql安装完成之后占用的内存远远小于Oracle所占用的内存,并且Oracle越用所占内存也会变多。

# 二、微观上:

1、对于事务的支持

Mysql对于事务默认是不支持的,只是有某些存储引擎中如:innodb可以支持;而Oracle对于事物是完全支持的。

2、并发性

什么是并发性?并发性是OLTP(On-Line Transaction Processing联机事务处理过程)数据库最重要的特性,并发性涉及到资源的获取、共享与锁定。

Mysql以表锁为主,对资源锁定的力度很大,如果一个session对一个表加锁时间过长,会让其他session无法更新此表的数据。

Oracle使用行级锁,对资源锁定的力度要小很多,只是锁定sql需要的资源,并且加锁是在数据库中的数据行上,不依赖于索引。所以oracle对并发性的支持要好很多。

3、数据的持久性

Oracle保证提交的事务均可以恢复,因为Oracle把提交的sql操作线写入了在线联机日志文件中,保存到磁盘上,如果出现数据库或者主机异常重启,重启Oracle可以靠联机在线日志恢复客户提交的数据。

Mysql默认提交sql语句,但是如果更新过程中出现db或者主机重启的问题,也可能会丢失数据。

4、事务隔离级别

MySQL是repeatable read的隔离级别,而Oracle是read commited的隔离级别,同时二者都支持serializable串行化事务隔离级别,可以实现最高级别的。

读一致性。每个session提交后其他session才能看到提交的更改。Oracle通过在undo表空间中构造多版本数据块来实现读一致性,每个session 查询时,如果对应的数据块发生变化,Oracle会在undo表空间中为这个session构造它查询时的旧的数据块。

MySQL没有类似Oracle的构造多版本数据块的机制,只支持read commited的隔离级别。一个session读取数据时,其他session不能更改数据,但可以在表最后插入数据。session更新数据时,要加上排它锁,其他session无法访问数据

5、提交方式

Oracle默认不自动提交,需要手动提交。Mysql默认自动提交。

6、逻辑备份

Mysql逻辑备份是要锁定数据,才能保证备份的数据是一致的,影响业务正常的DML(数据操纵语言Data Manipulation Language)使用;Oracle逻辑备份时不锁定数据,且备份的数据是一致的。

7、sql语句的灵活性

mysql对sql语句有很多非常实用而方便的扩展,比如limit功能(分页),insert可以一次插入多行数据;Oracle在这方面感觉更加稳重传统一些,Oracle的分页是通过伪列和子查询完成的,插入数据只能一行行的插入数据。

8、数据复制

MySQL:复制服务器配置简单,但主库出问题时,丛库有可能丢失一定的数据。且需要手工切换丛库到主库。

Oracle:既有推或拉式的传统数据复制,也有dataguard的双机或多机容灾机制,主库出现问题是,可以自动切换备库到主库,但配置管理较复杂。

9、分区表和分区索引

MySQL的分区表还不太成熟稳定;Oracle的分区表和分区索引功能很成熟,可以提高用户访问db的体验。

10、售后与费用

Oracle是收费的,出问题找客服;Mysql是免费的的,开源的,出问题自己解决。

11、权限与安全

Oracle的权限与安全概念比较传统,中规中矩;MySQL的用户与主机有关,感觉没有什么意义,另外更容易被仿冒主机及ip有可乘之机。

12、性能诊断方面

Oracle有各种成熟的性能诊断调优工具,能实现很多自动分析、诊断功能。比如awr、addm、sqltrace、tkproof等 ;MySQL的诊断调优方法较少,主要有慢查询日志。

# 4.Weblogic和Tomcat的区别

1、Weblogic具有不同的功能,例如事务管理,消息队列,数据库连接处理和许多其他功能,而Tomcat具有轻量级,开源,灵活,稳定,servlet,JSP,将应用程序嵌入到tomcat异步日志记录中等等。

2、Weblogic是具有EJB支持的完全加载的容器,而Tomcat是Servlet和JSP支持的容器。

3、Weblogic是许可版本和基于Java EE的商业Web服务器,具有高度可扩展,快速,安全和高性能的Web服务器,而Tomcat是开源的,基于Servlet和JSP规范的参考实现。

4、Weblogic是市场上最好的服务器,具有数据库池功能,而且购买许可证的价格有点昂贵,而Tomcat是免费的,并且在大型应用程序的功能方面存在局限性。

5、Weblogic具有正在使用该服务器的顶级客户和客户,并且被业内的顶级客户所使用,而Tomcat也被许多客户使用,但是与Weblogic相比,它受到限制并且用户数量更少。

6、Weblogic提供了对部署业务逻辑以及Web应用程序的支持,而Tomcat仅允许基于http或基于Web的应用程序的部署。

7、与WebLogic相比,Weblogic的性能良好,而Tomcat的性能则较差。

8、Weblogic具有良好的服务管理功能以及自动化的可能性,而Tomcat具有较少的IT服务管理可能性。

9、Weblogic具有针对任何新功能或新JDK版本的更快更新,而Tomcat不会立即提供JDK版本更新或任何其他功能更新,但需要花费较长时间。

10、Weblogic具有应用程序版本控制功能,消息传递引擎,故障排除,数据库调优,数据源池,而Apache Tomcat Server没有此功能。

11、Weblogic具有EJB集群,JMS集群,故障转移,迁移,升级,补丁,安全更新和恢复功能,而Tomcat没有这些功能。

结论

Weblogic是需要许可证的企业和商业软件,具有用于大型工业应用程序的多种功能,可简化开发人员的生活,而Tomcat是轻量级的免费开源软件,适用于小型Web应用程序或公司在许可版本上投资的成本效益很高的地方。选择软件的选择取决于项目的要求和规模以及所需的功能和客户的要求,并且在设计应用程序体系结构时,可以采用相同的方法来选择任何应用程序服务器或Web服务器。

# 5.gc有哪些回收器.

# 一、串行垃圾回收器

在JDK1.3之前,单线程回收器是唯一的选择。它的单线程意义不仅仅是说它只会使用一个CPU或一个手机线程去完成垃圾收集工作。而且它进行垃圾回收的时候,必须暂停其它所有的工作线程(Stop The World,STW),直到它收集完成。它适合Client模式的应用,在单CPU环境下,它效率高效,由于没有线程交互的开销,专心垃圾收集自然可以获得最高的单线程效率。

串行的垃圾收集器有两种,Serial和Serial Old,一般两者搭配使用。

新生代采用Serial,是利用复制算法;老年代使用Serial Old采用标记-整理算法。Client应用或者命令行程序可以通过

-XX:+UseSerialGC开启串行垃圾回收器。

# 二、并行垃圾回收器

并行垃圾回收器是通过多线程进行垃圾收集的。也会暂停其它所有的工作线程(Stop The World,STW)。适合Server模式以及多CPU环境。一般会和JDK1.5之后出现的CMS搭配使用。并行的垃圾回收器有以下几种:

ParNew:Serial收集器的多线程版本,默认开启的收集线程数和CPU数量一样,运行数量可以通过修改ParallelGCThreads设定。用于新生代手机,复制算法。用-XX:+UseParNewGC,和Serial Old收集器组合进行内存回收。

Parallel Scavenge: 关注吞吐量,吞吐量优先,吞吐量=代码运行时间/(代码运行时间+垃圾收集时间),也就是高效率利用CPU时间,尽快完成程序的运算任务可以升值最大停顿时间MaxGCPauseMillis以及,吞吐量大小GCTimeRatio。如果设置了-XX:+UseAdaptiveSizePolicy参数,则随着GC,会动态调整新生代的大小,Eden,Survivor比例等,以提供最合适的停顿时间或者最大的吞吐量。用于新生代收集,复制算法。通过-XX:+UseParallelGC参数,Server模式下默认提供了其和SerialOld进行搭配的分代收集方式。

Parllel Old:Parallel Scavenge的老年代版本。JDK 1.6开始提供的。在此之前Parallel Scavenge的地位也很尴尬,而有了Parllel Old之后,通过-XX:+UseParallelOldGC参数使用Parallel Scavenge + Parallel Old器组合进行内存回收。

# 三、CMS收集器

CMS(Concurrent Mark Sweep)收集器是一种以获得最短回收停顿时间为目标的收集器。从名字就能知道它是标记-清除算法的。但是它比一般的标记-清除算法要复杂一些,分为以下4个阶段:

  1. 初始标记:标记一下GC Roots能直接关联到的对象,会"Stop The World"。
  2. 并发标记:GC Roots Tracing,可以和用户线程并发执行。
  3. 重新标记:标记期间产生的对象存活的再次判断,修正对这些对象的标记,执行时间相对并发标记短,会“Stop The World”。
  4. 并发清除:清除对象,可以和用户线程并发执行。

由于垃圾回收线程可以和用户线程同时运行,也就是说它是并发的,那么它会对CPU的资源非常敏感,CMS默认启动的回收线程数是(CPU数量+3)/ 4,当CPU<4个时,并发回收是垃圾收集线程就不会少于25%,而且随着CPU减少而增加,这样会影响用户线程的执行。而且由于它是基于标记-清除算法的,那么就无法避免空间碎片的产生。CMS收集器无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。

​ 所谓浮动垃圾,在CMS并发清理阶段用户线程还在运行着,伴随程序运行自然还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只能留待下一次GC时再清理掉。

# 四、G1垃圾收集器

# 1、G1垃圾收集器

把G1单独拿出来的原因是其比较复杂,在JDK 1.7确立是项目目标,在JDK 7u2版本之后发布,并在JDK 9中成为了默认的垃圾回收器。通过“-XX:+UseG1GC”启动参数即可指定使用G1 GC。

G1从整体看还是基于标记-清除算法的,但是局部上是基于复制算法的。这样就意味者它空间整合做的比较好,因为不会产生空间碎片。G1还是并发与并行的,它能够充分利用多CPU、多核的硬件环境来缩短“stop the world”的时间。G1还是分代收集的,但是G1不再像上文所述的垃圾收集器,需要分代配合不同的垃圾收集器,因为G1中的垃圾收集区域是“分区”(Region)的。G1的分代收集和以上垃圾收集器不同的就是除了有年轻代的ygc,全堆扫描的full GC外,还有包含所有年轻代以及部分老年代Region的Mixed GC。G1还可预测停顿,通过调整参数,制定垃圾收集的最大停顿时间。

G1收集器的运作大致可以分为以下步骤:初始标记、并发标记、最终标记、筛选回收。其中初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Set)的值,让下一个阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这个阶段需要STW,但耗时很短。并发标记阶段是从GC Roots开始对堆中对象进行可达性分析,找到存活的对象,这阶段耗时较长,但是可以和用户线程并发运行。最终标记阶段则是为了修正在并发标记期间因用户程序继续运行而导致标记产生变化的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记需要把Remembered Set Logs的数据合并到Remembered Sets中,这阶段需要暂停线程,但是可并行执行。最后的筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来确定回收计划。G1收集器运行示意图如下图所示。

img

# 2、G1分区的概念

G1的堆区在分代的基础上,引入分区的概念。G1将堆分成了若干Region,以下和”分区”代表同一概念。(这些分区不要求是连续的内存空间)Region的大小可以通过G1HeapRegionSize参数进行设置,其必须是2的幂,范围允许为1Mb到32Mb。 JVM的会基于堆内存的初始值和最大值的平均数计算分区的尺寸,平均的堆尺寸会分出约2000个Region。分区大小一旦设置,则启动之后不会再变化。如下图简单画了下G1分区模型。

img

Eden regions(年轻代-Eden区)

Survivor regions(年轻代-Survivor区)

Old regions(老年代)

Humongous regions(巨型对象区域)

Free regions(未分配区域,也会叫做可用分区)-上图中空白的区域

G1中的****巨型对象****是指,占用了Region容量的50%以上的一个对象。Humongous区,就专门用来存储巨型对象。如果一个H区装不下一个巨型对象,则会通过连续的若干H分区来存储。因为巨型对象的转移会影响GC效率,所以并发标记阶段发现巨型对象不再存活时,会将其直接回收。ygc也会在某些情况下对巨型对象进行回收。

分区可以有效利用内存空间,因为收集整体是使用“标记-整理”,Region之间基于“复制”算法,GC后会将存活对象复制到可用分区(未分配的分区),所以不会产生空间碎片。

# 3、G1 GC的分类和过程

JDK10 之前的G1中的GC只有Young GC,Mixed GC。Full GC处理会交给单线程的Serial Old垃圾收集器。

# 4、Young GC年轻代收集

在分配一般对象(非巨型对象)时,当所有Eden region使用达到最大阀值并且无法申请足够内存时,会触发一次Young GC。每次Young GC会回收所有Eden以及Survivor区,并且将存活对象复制到Old区以及另一部分的Survivor区。到Old区的标准就是在PLAB中得到的计算结果。因为Young GC会进行根扫描,所以会stop the world。

Young GC的回收过程如下:

1、根扫描,跟CMS类似,Stop the world,扫描GC Roots对象。

2、处理Dirty card,更新RSet.

3、扫描RSet,扫描RSet中所有old区对扫描到的young区或者survivor去的引用。

4、拷贝扫描出的存活的对象到survivor2/old区

5、处理引用队列,软引用,弱引用,虚引用

# 5、Mix GC混合收集

Mixed GC是G1 GC特有的,跟Full GC不同的是Mixed GC只回收部分老年代的Region。哪些old region能够放到CSet里面,有很多参数可以控制。比如G1HeapWastePercent参数,在一次young GC之后,可以允许的堆垃圾百占比,超过这个值就会触发mixed GC。

G1MixedGCLiveThresholdPercent参数控制的,old代分区中的存活对象比,达到阀值时,这个old分区会被放入CSet。

Mixed GC一般会发生在一次Young GC后面,为了提高效率,Mixed GC会复用Young GC的全局的根扫描结果,因为这个Stop the world过程是必须的,整体上来说缩短了暂停时间。

Mix GC的回收过程可以理解为Young GC后附加的全局concurrent marking,全局的并发标记主要用来处理old区(包含H区)的存活对象标记,过程如下:

\1. 初始标记(Initial Mark)。标记GC Roots,会STW,一般会复用Young GC的暂停时间。如前文所述,初始标记会设置好所有分区的NTAMS值。

\2. 根分区扫描(Root Region Scan)。这个阶段GC的线程可以和应用线程并发运行。其主要扫描初始标记以及之前Young GC对象转移到的Survivor分区,并标记Survivor区中引用的对象。所以此阶段的Survivor分区也叫根分区(Root Region)。

\3. 并发标记(Concurrent Mark)。会并发标记所有非完全空闲的分区的存活对象,也即使用了SATB算法,标记各个分区。

\4. 最终标记(Remark)。主要处理SATB缓冲区,以及并发标记阶段未标记到的漏网之鱼(存活对象),会STW,可以参考上文的SATB处理。

\5. 清除阶段(Clean UP)。上述SATB也提到了,会进行bitmap的swap,以及PTAMS,NTAMS互换。整理堆分区,调整相应的RSet(比如如果其中记录的Card中的对象都被回收,则这个卡片的也会从RSet中移除),如果识别到了完全空的分区,则会清理这个分区的RSet。这个过程会STW。

清除阶段之后,还会对存活对象进行转移(复制算法),转移到其他可用分区,所以当前的分区就变成了新的可用分区。复制转移主要是为了解决分区内的碎片问题。

# 6、Full GC

G1在对象复制/转移失败或者没法分配足够内存(比如巨型对象没有足够的连续分区分配)时,会触发Full GC。Full GC使用的是stop the world的单线程的Serial Old模式,所以一旦触发Full GC则会STW应用线程,并且执行效率很慢。JDK 8版本的G1是不提供Full GC的处理的。对于G1 GC的优化,很大的目标就是没有Full GC。

# 6.内存新生代.老年代等

# JVM--对象什么情况会进入到老年代

  1. 大对象:所谓的大对象是指需要大量连续内存空间的java对象,最典型的大对象就是那种很长的字符串以及数组,大对象对虚拟机的内存分配就是坏消息,尤其是一些朝生夕灭的短命大对象,写程序时应避免。

  2. 长期存活的对象:虚拟机给每个对象定义了一个对象年龄(Age)计数器,如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1,。对象在Survivor区中每熬过一次Minor GC,年龄就增加1,当他的年龄增加到一定程度(默认是15岁), 就将会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。

  3. 动态对象年龄判定:为了能更好地适应不同程度的内存状况,虚拟机并不是永远地要求对象的年龄必须达到了MaxTenuringThreshold才能晋升到老年代,如果在Survivor空间中相同年龄的所有对象大小的总和大于Survivor空间的一半,年龄大于或等于年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。

  4. 在一次安全Minor GC 中,仍然存活的对象不能在另一个Survivor 完全容纳,则会通过担保机制进入老年代。

# 7.为什么需要分布式锁

# 什么是分布式锁

分布式锁是控制分布式系统之间同步访问共享资源的一种方式。在分布式系统中,常常需要协调他们的动作。如果不同的系统或是同一个系统的不同主机之间共享了一个或一组资源,那么访问这些资源的时候,往往需要互斥来防止彼此干扰来保证一致性,在这种情况下,便需要使用到分布式锁。

# 为什么需要分布式锁

为了保证一个方法或属性在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLock或Synchronized)进行互斥控制。

在单机环境中,Java中提供了很多并发处理相关的API。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力。为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

举个例子:

机器A , 机器B是一个集群, A, B两台机器上的程序都是一样的, 具备高可用性能.

A, B机器都有一个定时任务, 每天晚上凌晨2点需要执行一个定时任务, 但是这个定时任务只能执行一遍, 否则的话就会报错, 那A,B两台机器在执行的时候, 就需要抢锁, 谁抢到锁, 谁执行, 谁抢不到, 就不用执行了!

为什么需要分布式锁 (opens new window)

# 8.工厂,代理模式,好好看看.

# 9.HashMap是线程安全吗,为啥

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null) // 如果没有hash碰撞则直接插入元素
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

其中第六行代码是判断是否出现hash碰撞,假设两个线程A、B都在进行put操作,并且hash函数计算出的插入下标是相同的,当线程A执行完第六行代码后由于时间片耗尽导致被挂起,而线程B得到时间片后在该下标处插入了元素,完成了正常的插入,然后线程A获得时间片,由于之前已经进行了hash碰撞的判断,所有此时不会再进行判断,而是直接进行插入,这就导致了线程B插入的数据被线程A覆盖了,从而线程不安全。

# 10.gc线上调优经验(如果写了会被问)

# 11.说说如果你想用zookeeper怎么用.

多节点的分布式架构,分布式锁.

# 12.redis和zookeeper的分布式锁有什么区别

对于redis的分布式锁而言,它有以下缺点:

  • 它获取锁的方式简单粗暴,获取不到锁直接不断尝试获取锁,比较消耗性能。
  • 另外来说的话,redis的设计定位决定了它的数据并不是强一致性的,在某些极端情况下,可能会出现问题。锁的模型不够健壮
  • 即便使用redlock算法来实现,在某些复杂场景下,也无法保证其实现100%没有问题,关于redlock的讨论可以看How to do distributed locking

但是另一方面使用redis实现分布式锁在很多企业中非常常见,而且大部分情况下都不会遇到所谓的“极端复杂场景”

所以使用redis作为分布式锁也不失为一种好的方案,最重要的一点是redis的性能很高,可以支撑高并发的获取、释放锁操作。

对于zk分布式锁而言:

  • zookeeper天生设计定位就是分布式协调,强一致性。锁的模型健壮、简单易用、适合做分布式锁。
  • 如果获取不到锁,只需要添加一个监听器就可以了,不用一直轮询,性能消耗较小。

但是zk也有其缺点:如果有较多的客户端频繁的申请加锁、释放锁,对于zk集群的压力会比较大。

# zookeeper实现分布式锁的原理

理解了锁的原理后,就会发现,Zookeeper 天生就是一副分布式锁的胚子。

首先,Zookeeper的每一个节点,都是一个天然的顺序发号器。

在每一个节点下面创建子节点时,只要选择的创建类型是有序(EPHEMERAL_SEQUENTIAL 临时有序或者PERSISTENT_SEQUENTIAL 永久有序)类型,那么,新的子节点后面,会加上一个次序编号。这个次序编号,是上一个生成的次序编号加一

比如,创建一个用于发号的节点“/test/lock”,然后以他为父亲节点,可以在这个父节点下面创建相同前缀的子节点,假定相同的前缀为“/test/lock/seq-”,在创建子节点时,同时指明是有序类型。如果是第一个创建的子节点,那么生成的子节点为/test/lock/seq-0000000000,下一个节点则为/test/lock/seq-0000000001,依次类推,等等。

image.png

其次,Zookeeper节点的递增性,可以规定节点编号最小的那个获得锁。

一个zookeeper分布式锁,首先需要创建一个父节点,尽量是持久节点(PERSISTENT类型),然后每个要获得锁的线程都会在这个节点下创建个临时顺序节点,由于序号的递增性,可以规定排号最小的那个获得锁。所以,每个线程在尝试占用锁之前,首先判断自己是排号是不是当前最小,如果是,则获取锁。

第三,Zookeeper的节点监听机制,可以保障占有锁的方式有序而且高效。

每个线程抢占锁之前,先抢号创建自己的ZNode。同样,释放锁的时候,就需要删除抢号的Znode。抢号成功后,如果不是排号最小的节点,就处于等待通知的状态。等谁的通知呢?不需要其他人,只需要等前一个Znode 的通知就可以了。当前一个Znode 删除的时候,就是轮到了自己占有锁的时候。第一个通知第二个、第二个通知第三个,击鼓传花似的依次向后。

Zookeeper的节点监听机制,可以说能够非常完美的,实现这种击鼓传花似的信息传递。具体的方法是,每一个等通知的Znode节点,只需要监听linsten或者 watch 监视排号在自己前面那个,而且紧挨在自己前面的那个节点。 只要上一个节点被删除了,就进行再一次判断,看看自己是不是序号最小的那个节点,如果是,则获得锁。

为什么说Zookeeper的节点监听机制,可以说是非常完美呢?

一条龙式的首尾相接,后面监视前面,就不怕中间截断吗?比如,在分布式环境下,由于网络的原因,或者服务器挂了或则其他的原因,如果前面的那个节点没能被程序删除成功,后面的节点不就永远等待么?

其实,Zookeeper的内部机制,能保证后面的节点能够正常的监听到删除和获得锁。在创建取号节点的时候,尽量创建临时znode 节点而不是永久znode 节点,一旦这个 znode 的客户端与Zookeeper集群服务器失去联系,这个临时 znode 也将自动删除。排在它后面的那个节点,也能收到删除事件,从而获得锁。

说Zookeeper的节点监听机制,是非常完美的。还有一个原因。

Zookeeper这种首尾相接,后面监听前面的方式,可以避免羊群效应。所谓羊群效应就是每个节点挂掉,所有节点都去监听,然后做出反映,这样会给服务器带来巨大压力,所以有了临时顺序节点,当一个节点挂掉,只有它后面的那一个节点才做出反映。

# 13.Thread和Runnable的顶级父类是什么

Thread也是实现了Runnable,new Thread也是间接实现了Runnable接口.

# 14.spring中用了哪些设计模式

  • 代理模式—在AOP中被用的比较多。
  • 单例模式—在spring配置文件中定义的bean默认为单例模式。
  • 模板方法—用来解决代码重复的问题。比如. RestTemplate, JmsTemplate, JpaTemplate。
  • 工厂模式—BeanFactory用来创建对象的实例。
  • 适配器--spring aop
  • 装饰器--spring data hashmapper
  • 观察者-- spring 事件驱动模型
  • 回调--Spring Aware回调接口

# 15.ConcurrentHashMap的链表为什么8个就转为红黑树

红黑树插入为O(lgn),查询为O(lgn),链表插入为O(1),查询为O(n)。个数少时,插入删除成本高,用链表;个数多时,查询成本高,用红黑树。需要定一个值,比这个值大就转红黑树,比这个值小就转链表,而且要避免频繁的转换。根据泊松分布,在负载因子0.75(HashMap默认)的情况下,单个hash槽内元素个数为8的概率小于百万分之一,将7作为一个分水岭,等于7时不做转换,大于等于8才转红黑树,小于等于6才转链表。

# 16.Innodb和myisam的区别和使用

  1. InnoDB 支持事务,MyISAM 不支持事务。这是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;

  2. InnoDB 支持外键,而 MyISAM 不支持。对一个包含外键的 InnoDB 表转为 MYISAM 会失败;

  3. InnoDB 是聚集索引,MyISAM 是非聚集索引。聚簇索引的文件存放在主键索引的叶子节点上,因此 InnoDB 必须要有主键,通过主键索引效率很高。但是辅助索引需要两次查询,先查询到主键,然后再通过主键查询到数据。因此,主键不应该过大,因为主键太大,其他索引也都会很大。而 MyISAM 是非聚集索引,数据文件是分离的,索引保存的是数据文件的指针。主键索引和辅助索引是独立的。

  4. InnoDB 不保存表的具体行数,执行 select count(*) from table 时需要全表扫描。而MyISAM 用一个变量保存了整个表的行数,执行上述语句时只需要读出该变量即可,速度很快;

  5. InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁。一个更新语句会锁住整张表,导致其他查询和更新都会被阻塞,因此并发访问受限。这也是 MySQL 将默认存储引擎从 MyISAM 变成 InnoDB 的重要原因之一;

如何选择:

  1. 是否要支持事务,如果要请选择 InnoDB,如果不需要可以考虑 MyISAM;

  2. 如果表中绝大多数都只是读查询,可以考虑 MyISAM,如果既有读写也挺频繁,请使用InnoDB。

  3. 系统奔溃后,MyISAM恢复起来更困难,能否接受,不能接受就选 InnoDB;

  4. MySQL5.5版本开始Innodb已经成为Mysql的默认引擎(之前是MyISAM),说明其优势是有目共睹的。如果你不知道用什么存储引擎,那就用InnoDB,至少不会差。

# 17.map是如何实现的

list,set,map的底层实现 (opens new window)

HashMap实现原理要点概括

参考文献:http://zhangshixi.iteye.com/blog/672697 参考文献:http://blog.csdn.net/lizhongkaide/article/details/50595719

1. HashMap是基于哈希表的Map接口的非同步实现,允许使用null值和null键,但不保证映射的顺序。
2. 底层使用数组实现,数组中每一项是个单向链表,即数组和链表的结合体;当链表长度大于一定阈值时,链表转换为红黑树,这样减少链表查询时间。
3. HashMap在底层将key-value当成一个整体进行处理,这个整体就是一个Node对象。HashMap底层采用一个Node[]数组来保存所有的key-value对,当需要存储一个Node对象时,会根据key的hash算法来决定其在数组中的存储位置,在根据equals方法决定其在该数组位置上的链表中的存储位置;当需要取出一个Node时,也会根据key的hash算法找到其在数组中的存储位置,再根据equals方法从该位置上的链表中取出该Node。
4. HashMap进行数组扩容需要重新计算扩容后每个元素在数组中的位置,很耗性能
5. 采用了Fail-Fast机制,通过一个modCount值记录修改次数,对HashMap内容的修改都将增加这个值。迭代器初始化过程中会将这个值赋给迭代器的expectedModCount,在迭代过程中,判断modCount跟expectedModCount是否相等,如果不相等就表示已经有其他线程修改了Map,马上抛出异常

# 18.红黑树

有了(bst)二分查找树,avl(二分平衡树,为什么还需要红黑树) (opens new window)

五分钟学会红黑树 (opens new window)

红黑树没有你想象的那么难 (opens new window)

红黑树变色规则 (opens new window)

#

# 19.mysql事务隔离级别

事务的 四个特征(ACID)

事务具有四个特征:原子性( Atomicity )、一致性( Consistency )、隔离性( Isolation )和持续性( Durability )。这四个特性简称为 ACID 特性。

1 、原子性。事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做

2 、一致性。事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说不一致的状态。

3 、隔离性。一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。

4 、持续性。也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。

Mysql的四种隔离级别 SQL标准定义了4类隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。

Read Uncommitted(读取未提交内容)

在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。

Read Committed(读取提交内容)

这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。

Repeatable Read(可重读)

这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。

Serializable(可串行化)

这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。

出现问题

这四种隔离级别采取不同的锁类型来实现,若读取的是同一个数据的话,就容易发生问题。例如:

脏读(Drity Read):某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。

不可重复读(Non-repeatable read):在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。

幻读(Phantom Read):在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的。

在MySQL中,实现了这四种隔离级别,分别有可能产生问题如下所示:

​ 各种隔离级别和数据库异常情况对应情况如下:

隔离级别 脏读 不可重复 读 幻读
READ- UNCOMMITTED 读未提交
READ-COMMITTED 读已提交 ×
REPEATABLE- READ 可重读 × ×
SERIALIZABLE 可串行化 × × ×

img

# 20.索引失效的情况,详见sql.md

​ 1、组合索引不遵循最左匹配原则

​ 2、组合索引的前面索引列使用范围查询(<,>,like),会导致后续的索引失效

​ 3、不要在索引上做任何操作(计算,函数,类型转换)

​ 4、is null和is not null无法使用索引

​ 5、尽量少使用or操作符,否则连接时索引会失效

​ 6、字符串不添加引号会导致索引失效

​ 7、两表关联使用的条件字段中字段的长度、编码不一致会导致索引失效

​ 8、like语句中,以%开头的模糊查询

​ 9、如果mysql中使用全表扫描比使用索引快,也会导致索引失效

# 21.tcp三次握手,四次挥手

  1. 为什么要三次握手
    1. 为了防止服务器端开启一些无用的连接增加服务器开销
      1. 如果A向服务发送一次握手,B就返回ack消息,并且打开链接,但是A并没收到,那么就不会有后续了,但是打开的连接就一直在这里耗费资源.
    2. 防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误。
      1. 如果A向服务端发送了一次握手,然后卡住了,过了一会超时了,A觉得这个事情结束了,然后服务端突然收到了,如果没有后续确认就开始认为有效,那么可能出现问题.
  2. 为什么是四次挥手
    1. 断开连接的过程,包含主动方A和被动方B,
    2. A发送断开请求,B收到并且回复ack消息.表明收到了请求,但是可能还没完事.这个过程表明A已经没有事情了.
    3. 接着B处理完了自己的事,向A发送断开消息,A回复ack消息给B.
    4. 至此四次挥手结束,

# 22.一致性哈希原理

# 23.Redis数据类型.及实现原理

结构类型 结构存储的类型 结构读写能力
STRING 整数、字符串或者浮点数 对整个字符串或者字符串的其中一部分执行操作;对整数和浮点数执行自增或者自减操作。
LIST 一个链表,链表上的每个节点都包含了一个字符串 从链表的两端推入或者弹出元素;根据偏移量对链表进行修剪;读取单个或者多个元素;根据值查找或者移除元素。
SET 包含字符串的无序收集器,并且被包含的每个字符串都是独一无二、各不相同的 添加、获取、移除单个元素;检查一个元素是否存在于集合中;计算交集、并集、差集;从集合里面随机获取元素。
HASH 包含键值对的无序散列表 添加、获取、移除单个键值对;获取所有键值对
ZSET(SORT SET) 字符串成员与浮点数分值之间的有序映射,元素的排列顺序由分值的大小决定 添加、获取、删除单个元素;根据分值范围(range)或者成员来获取元

# 24.lambda并行的线程池和java线程池的区别

基于服务器内核的限制,如果你是八核,每次线程只能起八个,不能自定义线程池;
适用于对list密集计算操作充分利用CPU资源,如果需要调用远端服务不建议使用;

# 24.1.java线程池7个参数,四个实现方式,队列和最大线程数执行顺序.

# 25.java中解决了ABA问题的类

?

# 26.CAS全拼是什么.他的set的实现细节

compare and swap

compare and set

# 27.redis的原子setnx是在哪个版本出现的

?

# 28.redis的set kv nx ex 是怎么保证原子的.

# 29.mysql如何排查死锁问题.

# 30.innodb什么时候表锁,什么时候行锁.

InnoDB基于索引的行锁

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

# 31.java如何排查死锁.

jps,jstack,jconsole

# 32.向hashset中插入自定义的类有什么需要注意的.

# 33.hashMap和hashSet的底层

在这里插入图片描述

# 34.HashSet类是如何实现添加元素保证不重复的

如何不重复 (opens new window)

# 35.redis在你的项目中你是怎么用的.redis锁的注意事项.