笔记

Java并发篇

相关演示代码地址:hello-github-ui/interview: 面试知识点汇总 (github.com)

一、Java如何开启线程?怎么保证线程安全?

首先说一下线程和进程的区别:进程是操作系统进行资源分配的最小单元;线程是操作系统进行任务分配的最小单元。线程隶属于进程。 如何开启线程? 1.继承 Thread类,重写run方法。 2.实现Runnable接口,实现run方法。 延伸到start方法和run方法的区别,调用start方法开启一个线程,而在start方法内部呢会调用run方法去实现一个线程的逻辑。而如果是我们主观地直接调用run方法,那就是一个普通的方法调用,并不会开启线程。 3.实现Callable接口,实现 call 方法。传统的Runnable和Thread的run方法是没有返回值的,如果你想返回一下线程的执行结果,就需要通过 Callable接口来实现。通过 FutureTask创建一个线程,获取到线程执行的返回值。 4.通过线程池来开启线程(不熟悉线程池的话就不要提这种方式) 怎么保证线程安全? 什么是线程安全:多个资源对同一个线程进行操作就会出现线程安全的问题。 加锁: 1. JVM提供的锁,也就是Synchronized关键字 2. JDK提供的各种锁 Lock 注意:既然你回答到了上面的锁,那面试官就会顺其自然地问你你了解哪些锁?所以,自己需要补充这块知识。 锁分类:公平锁和非公平锁 公平锁:是指多个线程按照申请锁的顺序来获取锁 非公平锁:是指多个线程获取的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。 ReentrantLock:可以指定构造方法的boolean类型来指定是公平锁还是非公平锁,默认是非公平锁 synchronized:是一种非公平锁。 可重入锁(又名递归锁):指的是同一线程外层方法获得锁之后,内层递归方法仍然能够获得该锁的代码 示例如下: class MyClass { ​ public synchronized void method1() { ​ method2(); ​ } ​ public synchronized void method2() { ​ } } method1和method2都是synchronized修饰的方法,在method1里面调用method2的时候,不需要重新申请锁,可以直接调用就行了(其实可以反过来想一想,如果synchronized不具有重入性,当我调用了method1的时候,得申请锁,申请好了之后那么method1就拥有了这个锁,那么调用method2的时候,又要重新申请锁,而锁在method1的手上,这时候又要重新申请锁,显然是不可能得到的,这不科学。所以,synchronize和lock都是具有可重入性的) 具体锁的内容可以参考:https://zhuanlan.zhihu.com/p/85511613

二、Volatile和Synchronized有什么区别?Volatile能不能保证线程安全?DCL(Double Check Lock)单例为什么要加 Volatile?

1. Synchronized关键字,用来加锁。Volatile只是保持变量的线程可见性。通常适用于一个线程写,多个线程读的场景。 2. Volatile不能保证线程安全。Volatile关键字只能保证线程可见性,不能保证原子性。 3. Volatile防止指令重排。在DCL中,防止高并发情况下,指令重排造成的线程安全问题。

三、Java线程锁机制是怎样的?偏向锁、轻量级锁、重量级锁有什么区别?锁机制是如何升级的?

1. Java的锁就是在对象的MarkWord中记录一个锁状态。无锁、偏向锁、轻量级锁、重量级锁对应不同的锁状态。 2. Java的锁机制就是根据资源竞争的激烈程度不断进行锁升级的过程。

四、谈谈你对AQS的理解。AQS=如何实现可重入锁?

1. AQS(AbstractQueuedSynchronizer)是一个java线程同步的框架。是jdk中很多锁工具的核心实现框架。 2. 在AQS中,维护了一个信号量state和一个线程组成的双向链表队列。其中,这个线程队列就是用来给线程排队的,而state就像是一个红绿灯,用来控制线程排队或者放行的。在不同的场景下,有不同的意义。 3. 在可重入锁场景下,state就用来表示加锁的次数。0表示无锁,每加一次锁,state就加1.释放锁state就减1。

五、有A,B,C三个线程,如何保证三个线程同时执行?如何在并发情况下保证三个线程依次执行?如何保证三个线程有序交错运行?

当面试官提到这个问题时,一定要想到其考察的是以下三个并发工具:CountDownLatch, CylicBarrier, Semaphore CountDownLatch就像是百米赛跑,由指挥官统一发射开始信号,然后多个线程同时开始执行。 CylicBarrier就像是做核酸检测时一样,一组10人,当一个人来了后,先等待,只有当够了10人后,才会进行统一检测;也就说它保证了多个线程的有序依次执行。 相关代码参看:threadSafe.ThreadSafeDemo

六、如何对一个字符串快速进行排序?

首先如果是面试官在并发篇中间插入这个问题,那么面试官很可能问你的是你对 Fork/Join 框架的理解,不是传统的冒泡,快排,交换等排序算法 基本模式是 拆分,汇总 参看代码:forkjoin.MergeTest

Java网络通信篇

一、TCP和UDP有什么区别?TCP为什么是三次握手,而不是两次?

TCP Transfer Control Protocol是一种面向连接的、可靠的、传输层通信协议。 特点:好比是打电话,面向连接的,点对点的通信,高可靠的,效率比较低,占用的系统资源比较多。 UDP User Datagram Protocol 是一种无连接的、不可靠的、传输层通信协议。 特点:好比是广播,不需要连接,发送方不管接收方有没有准备好,直接发消息;可以进行广播发送的;传输不可靠,有可能丢失消息;效率比较高,占用的系统资源便较少。 TCP建立连接三次握手,断开连接四次挥手。主要从网络的不稳定性方面考虑,网络的不稳定性是确定的,但是我们必须保证会话的准确性,因此至少需要三次握手来保证会话的可靠性。 如果是两次握手,可能造成连接资源浪费的情况。

二、Java有哪几种IO模型?有什么区别?

BIO 同步阻塞IO,传统型 web 服务就是这种模型:

特点:可靠性差,吞吐量低,适用于连接比较少且比较固定的场景。JDK1.4之前唯一的选择。

NIO 同步非阻塞IO

在 BIO 的基础上,增加一个 selector ,减少 Thread 对 Server 的压力,就是 NIO:

特点:可靠性比较好,吞吐量较高,适用于连接比较多并且连接比较短(因为对于客户端来说还是一个轮询,查看是否服务端响应了没),适用于轻操作,例如聊天室

AIO 异步非阻塞IO

在 NIO 的基础上,让 Server 处理完成后,主动推送响应到 客户端就是 AIO 了:

特点:可靠性是最好的,吞吐量也是非常高。适用于连接比较多,并且连接也比较长(重操作)。例如:相册服务器。JDK7版本才开始支持

同步、异步和阻塞、非阻塞:同步,异步针对的是请求;阻塞,非阻塞针对的是响应。 在一个网络请求中,客户端会发一个请求到服务端 1. 客户端发了请求后,就一直等着服务端响应。客户端:阻塞。请求:同步 2. 客户端发了请求后,就去干别的事情了。时不时的过来检查服务端是否给出了响应。客户端:非阻塞。请求:同步。 3. 换成异步请求。客户端发了请求后,就坐在椅子上,等着服务端返回响应。客户端:阻塞。请求:异步。 4. 客户端发了请求后,就去干别的事情了。等到服务端给出响应后,再过来处理业务逻辑。客户端:非阻塞。请求:异步。 具体内容参考文章:https://blog.csdn.net/qq_42216791/article/details/107316926

三、Java NIO的几个核心组件是什么?分别有什么用?

三大组件: Channel Buffer Selector Channel类似于流,每个channel对应一个buffer缓冲区。channel会注册到selector上。 selector会根据channel上发生的读写事件,将请求交由某个空闲的线程去处理。selector对应一个或多个线程。 buffer和channel都是可读可写的。

四、select, poll 和 epoll 有什么区别?

这个问题就很深了,与操作系统相关。 这三个东西呢,其实就是上面的核心组件Selector在操作系统中的具体实现机制。 他们是NIO中多路复用的三种实现机制,是由Linux操作系统提供的。 用户空间和内核空间:操作系统为了保护系统安全,将内核划分为两个部分,一个是用户空间,一个是内核空间。用户空间能直接访问底层的硬件设备,必须通过内核空间。 文件描述符File Descriptor(FD):是一个抽象的概念,形式上是一个整数,实际上是一个索引值。指向内核中为每个进程维护进程所打开的文件的记录表。当程序打开一个文件或者创建一个文件时,内核就会向进程返回一个FD。只存在于 Unix,Linux中。 select机制:会维护一个FD的集合fd_set。将fd_set从用户空间复制到内核空间,激活socket。 poll机制:和select机制是差不多的,把fd_set结构进行了优化,fd集合的大小就突破了操作系统的限制。 epoll:event poll。epoll不再扫描所有的FD,只将用户关心的FD的事件存放到内核的一个事件表当中。这样,可以减少用户空间和内核空间之间需要拷贝的数据。

五、描述下 Http 和 Https 的区别

HTTP:是互联网上应用最为广泛的一种网络通信协议,基于TCP,可以使浏览器工作更为高效,减少网络传输。 HTTPS:是http的加强版,可以认为是http+ssl(secure socket layer)。在http的基础上增加了一些列的安全机制。一方面保证数据传输安全,另一方面对访问者增加了验证机制。是目前现行架构下,最为安全的解决方案。 主要区别: 1. HTTP的连接是简单无状态的,HTTPS的数据传输是经过证书加密的,安全性更高。 2. HTTP是免费的,而HTTPS需要申请证书,而证书通常是需要收费的,并且费用不低。 3. 他们的传输协议不同,所以他们使用的端口也是不一样的,HTTP默认是80端口,而HTTPS默认是443端口。 HTTPS的缺点: 1. HTTPS的握手协议比较费时,所以会影响服务的响应速度以及吞吐量。 2. HTTPS也并不是完全安全的。它的证书体系其实也并不是完全安全的。并且HTTPS在面对DDOS这样的攻击时,几乎起不到任何作用。

JVM调优篇

一、说一说JVM的内存模型?

二、Java类加载的全过程是怎样的?什么是双亲委派机制?有什么作用?一个对象从加载到JVM,再到被GC清除,都经历了什么过程?

Java的类加载器:AppClassLoader -> ExtClassLoader -> BootstrapClassLoader 每种类加载器都有他自己的加载目录。 Java中的类加载器:AppClassLoader,ExtClassLoader -> URLClassLoader -> SecureClassLoader -> ClassLoader 每个类加载器对它加载过的类,都是有一个缓存的。 双亲委派:向上委托查找,向下委托加载。作用:保护了Java底层的类不会被应用程序覆盖。(例如:String,List等类不会被你自定义的同名String等给覆盖掉) 类加载过程:加载 -> 连接 -> 初始化 加载:把Java的字节码文件加载到JVM内存当中,并映射成JVM认可的数据结构。 连接:分为三个小的阶段:1. 验证:检查加载到的字节信息是否符合JVM规范;2. 准备:创建类或接口的静态变量,并赋初始值,半初始化状态;3.解析:把符号引用转为直接引用。 初始化: 1. 用户创建一个对象,JVM首先需要到方法区去找对象的类型信息,然后再创建对象。 2. JVM要实例化一个对象,首先要在堆当中先创建一个对象。 -> 半初始化状态 3. 对象首先会分配在堆内存中新生代的Eden。然后经过一次Minor GC,对象如果存活,就会进入S区。在后续的每次GC中,如果对象一直存活,就会在S区来回拷贝,每移动一次,年龄加1. -> 多大年龄才会移入老年代?年龄是4个bit,最大为15,超过一定年龄后,对象转入老年代。 4. 当方法执行结束后,栈中的指针会先移除掉。 5. 堆中的对象,经过Full GC,就会被标记为垃圾,然后被GC线程清理掉。

三、怎么确定一个对象到底是不是垃圾?什么是GC Root?

有两种定位垃圾的方式: 1. 引用计数:这种方式是给堆内存当中的每个对象记录一个引用计数。引用个数为0的就认为是垃圾。这是早期JDK中使用的方式。引用计数无法解决循环引用的问题。 2. 根可达算法:这种方式是在内存中,从引用根对象向下一直找引用,找不到的对象就是垃圾。 哪些是GC Root? stack -> JVM stack, Native stack, class类, run-time constant pool常量池,static reference 静态变量

四、JVM有哪些垃圾回收算法?

MarkSweep 标记清除算法

这个算法分为两个阶段,标记阶段:把垃圾内存标记出来,清除阶段:直接将垃圾内存回收。

这种算法是比较简单的,但是有个很严重的问题,会产生很多内存碎片。

Copying 拷贝算法

为了解决标记清除算法的内存碎片问题,就产生了拷贝算法。

拷贝算法将内存分为大小相等的两半,每次只使用其中一半。垃圾回收时,将当前这一块的存活对象全部拷贝到另一半,然后当前这一半内存就可以直接清除。

这种算法没有内存碎片,但是它的问题就在于浪费空间。而且,它的效率跟存活对象的个数有关。

MarkCompack 标记压缩算法

为了解决拷贝算法的空间利用缺陷,就提出了标记压缩算法。这种算法在标记阶段跟标记清除算法是一样的,但是在完成标记后,不是直接清理垃圾内存,而是将存活对象往一端移动,然后将端边界以外的所有内存直接清除。

这三种算法各有利弊,各自有各自的适合场景。

五、JVM有哪些垃圾回收器?他们都是怎么工作的?什么是STW?它都发生在哪些阶段?什么是三色标记?如何解决错标记和漏标记的问题?为什么要设计这么多的垃圾回收器?

STW:Stop-The-World,是在垃圾回收算法执行过程当中,需要将JVM内存冻结的一种状态。

在STW状态下,Java的所有线程都是停止执行的-GC线程除外,native方法可以执行,但是,不能与JVM交互。GC各种算法优化的重点,就是减少STW,同时这也是JVM调优的重点。

核心思想,就是将STW打散,让一部分GC线程与用户线程并发执行。整个GC过程分为四个阶段。

1、初始标记阶段:STW只标记出根对象直接引用的对象。

2、并发标记:继续标记其它对象,与应用程序是并发执行。

3、重新标记:STW对并发执行阶段的对象进行重新标记。

4、并发清除:并行。将产生的垃圾清除。清除过程中,应用程序又会不断的产生新的垃圾,叫做浮动垃圾。这些垃圾就要留到下一次GC过程中清除。

六、 如何进行JVM调优?JVM参数有哪些?怎么查看一个Java进程的JVM参数?谈谈你了解的JVM参数。如果一个Java程序每次运行一段时间后,就变得非常卡顿,你准备如何对他进行优化?

JVM调优主要就是通过定制JVM运行参数来提高Java应用程序的运行数据

JVM参数大致可以分为三类:

1、标准指令:-开头,这些是所有的HotSpot(虚拟机)都支持的参数。可以用java -help打印出来。

2、非标准指令:-X开头,这些指令通常是跟特定的HotSpot版本对应的。可以用java -X打印出来。

3、不稳定参数:-XX开头,这一类参数是跟特定HotSpot版本对应的,并且变化非常大。详细的文档资料非常少。在JDK1.8版本下,有几个常用的不稳定指令:

java -XX:+PrintCommandLineFlags:查看当前命令的不稳定指令

java -XX:+PrintFlagsInitial:查看所有不稳定指令的默认值

消息队列篇

1、MQ有什么用?有哪些具体的使用场景?

MQ:MessageQuene消息队列,队列是一种FIFO先进先出的数据结构,消息是由生产者发送到MQ进行排队,然后由消费者对消息进行处理。QQ,微信就是典型的MQ场景。 MQ的作用主要有三个方面: 1、异步: 例子:快递,快递员-》菜鸟驿站《-客户 作用:异步能提高系统的响应速度和吞吐量。 2、解耦: 例子:《Thinking in java》-> 编辑社 作用:服务之间解耦,可以减少服务之间的影响,提高系统的稳定性和可扩展性。 另外,解耦之后可以实现数据分发,生产者发送一个消息后,可以由多个消费者来处理。 3、削峰: 例子:长江涨水->三峡大坝 作用:以稳定的系统资源应对突发的流量冲击。 MQ的缺点: 1、系统的可用性降低:一旦MQ宕机,整个业务就会产生影响。高可用。 2、系统的复杂度提高:引入MQ之后,数据链路就会变得很复杂,如何保证消息不丢失?消息不会重复调用?怎么保证消息的顺序性?...... 3、数据一致性:A系统发消息,需要由B、C两个系统一同处理,如果B系统处理成功,C系统处理失败,这就会造成数据一致性的问题。

2、如何进行产品选型?

Kafka 优点:吞吐量非常大,性能非常好,集群高可用。 缺点:会丢失数据,功能比较单一。 使用场景:比如日志分析,大数据采集;适用于数据量大,允许丢失一部分消息的场景 RabbitMQ(退役老牌) 优点:消息可靠性高,功能全面。 缺点:吞吐量比较低,消息积累会严重影响性能。(即并发性低)erlang语言不好定制。 使用场景:小规模场景。 RocketMQ(阿里出品) 优点:高吞吐、高性能、高可用,功能非常全面 缺点:开源版功能不如云上商业版。官方文档和周边生态还不够成熟。客户端只支持Java。 使用场景:几乎是全场景。

3、如何保证消息不丢失?

1、哪些环节会造成消息丢失?

2、怎么去防止消息丢失?

2.1 生产者发送消息不丢失

Kafka:消息发送+回调

RocketMQ:1、消息发送+回调。2、事务消息

RabbitMQ:1、消息发送+回调 2、手动事务:channel.txSelect()开启事务,channeltxCommit()提交事务,channel.txRollback()回滚事务。这种方式对channel是会产生阻塞的,造成吞吐量下降。3、Publisher Confirm,整个处理流程跟RocketMQ的事务消息,基本是一样的。

2.2 MQ主从消息同步不丢失

RocketMQ:1、普通集群中,同步同步、异步同步。异步同步效率更高,但是有丢失消息的风险。同步同步就不会丢消息。2、Dledger集群-两阶段提交

RabbitMQ:

  • 普通集群:消息是分散存储的,节点之间不会主动进行消息同步,是有可能丢失消息的。
  • 镜像集群:镜像集群会在节点之间主动进行数据同步,这样数据安全性得到提高。

Kafka:允许消息丢失的,通常都是用在允许消息少量丢失的场景下。

2.3 MQ消息存盘不丢失

RocketMQ:同步刷盘,异步刷盘:异步刷盘效率更高,但是有可能丢失消息。同步刷盘消息安全性更高,但是效率会降低。

RabbitMQ:将队列配置成持久化队列。新增的Quorum类型的队列,会采用Raft协议来进行消息同步。

Kafka:

2.4 MQ消费者消费消息不丢失

RocketMQ:使用默认的方式消费就行,不要采用异步方式。

RabbitMQ:autoCommit -> 手动提交 offset

Kafka:手动提交 offset

4、如何保证消息消费的幂等性?

其实就是要防止消费者重复消费消息的问题。

所有MQ产品并没有提供主动解决幂等性的机制,需要由消费者自行控制。

RocketMQ:给每个消息分配了一个MessageID,这个MessageID就可以作为消费者判断幂等的依据。这种方式不太建议(因为当消息量过多之后,这个MessageID并不能保证全局唯一)。最好的方式就是自己带一个有业务标识的ID,来进行幂等性判断。比如OrderID。

统一分配ID方式。

5、如何保证消息的顺序?

全局有序和局部有序:MQ只需要保证局部有序,不需要保证全局有序。

生产者把一组有序的消息放到同一个队列当中,而消费者一次消费整个队列(先锁住该队列)当中的消息。

RocketMQ中有完整的设计(已经定义好的api来实现上述要求),但是在RabbitMQ和Kafka中并没有完整的设计,需要自行设计。

RabbitMQ:要保证目标exchange只有一个队列。并且一个队列只对应一个消费者。

Kafka:生产者通过定制partition分配规则,将消息分配到同一个partition 。Topic下只对应一个消费者。

6、如何保证消息的高效读写?

零拷贝:Kafka和RocketMQ都是通过零拷贝技术来优化文件读写。

传统拷贝是需要在内核空间和用户空间之间进行四次拷贝。

零拷贝:有两种方式,mmap 和 transfile

Java当中对零拷贝进行了封装,Mmap方式通过MappedByteBuffer对象进行操作,而transfile通过FileChannel来进行操作。

Mmap适合比较小的文件,通常文件大小不要超过1.5G~2G之间。

Transfile没有文件大小限制。

RocketMQ当中使用Mmap方式来对它的文件进行读写。commitlog大小1G。

在Kafka当中,它的index日志文件也是通过mmap的方式来读写的。在其它的日志文件中,并没有使用零拷贝的方式。Kafka使用transfile的方式将硬盘数据加载到网卡

7、使用MQ如何保证分布式事务的最终一致性?

分布式事务:业务相关的多个操作(比如,电商订单中,用户下单后,一边需要等待支付,另一边需要生成物流单,这两个操作是分布式的,但必须同时成功或失败),保证他们同时成功或同时失败。

事务的最终一致性:与之对应的就是强一致性,就是只需要保证多个事务的最终时候是一致性即可(言外之意,中间某个过程中可能不一致)。

MQ中要保证事务的最终一致性,就需要做到两点:

1、生产者要保证100%的消息投递。

2、消费者这一端要保证幂等消费。唯一ID+业务自己实现幂等。

8、让你设计一个MQ,你会如何设计?

两个误区:1、放飞自我,漫无边际。2、纠结技术细节。

好的方式:1、从整体到细节,从业务场景到技术实现。2、以现有产品为基础。RocketMQ(本身就吸收了Kafka和RabbitMQ的精华)为模型。

答题思路:MQ的作用、项目大概的样子。

1、实现一个单机的队列数据结构。高效、可扩展。

2、将单机队列扩展成为分布式队列。分布式集群管理。

3、基于Topic定制消息路由策略。-发送者路由策略,消费者与队列对应关系,消费者路由策略。

4、实现高效的网络通信。-Netty Http

5、规划日志文件,实现文件高效读写。-零拷贝,顺序写。服务重启后,快速还原运行现场。

缓存篇

一、为什么使用缓存?

1、高性能:数据库的性能不是那么理想。

2、高可用:防止高并发下瞬间大量的访问造成数据库崩溃。

二、什么是缓存穿透?缓存击穿?缓存雪崩?怎么解决?

1、缓存穿透:缓存中查不到,数据库中也查不到。

解决方案:①对参数进行合法性校验。②将数据库中没有查到结果的数据也写入缓存中。这时要注意防止Redis被无用的key占满,这一类缓存的有效期要设置的短一点。③引入布隆过滤器,在访问Redis之前判断数据是否存在。要注意布隆过滤器存在一定的误判率,并且,布隆过滤器只能加数据不能减数据。

2、缓存击穿:缓存中没有,数据库中有。

一般是出现在缓存数据初始化以及缓存Key过期了的情况下。他的问题在于,重新写入缓存需要一定的时间,如果是在高并发的场景下,过多的请求就会瞬间写到DB上,给DB造成很大的压力。

解决方案:①设置这个热点缓存永不过期(永不过期意味着数据可能不是最新的,所以需要解决更新问题)。这时要注意在value当中包含一个逻辑上的过期时间,然后另起一个线程,定期重建这些缓存。②加载DB的时候,要防止并发。

3、缓存雪崩:缓存大面积过期,导致请求都被转发到DB。

解决方案:①把缓存的失效时间分散开。例如,在原有的统一失效时间基础上,增加一个随机值。②对热点数据设置永不过期。③把redis的key分散到不同的集群上等方案。

三、 如何保证Redis与数据库的数据一致?

当我们对数据进行修改的时候,到底是先删除缓存,还是先写数据库?

1、如果先删除缓存,再写数据库:在高并发场景下,当第一个线程删除了缓存,还没有来得及写数据库,第二个线程来读取数据,会发现缓存中的数据为空,那就会去读数据库中的数据(旧值,脏数据),读完之后,把读到的数据写入缓存(此时,第一个线程已经将新的值写到缓存里面了),这样缓存中的值就会被覆盖为修改前的脏数据。

总结:在这种方式,通常要求写操作不会太频繁。

解决方案:①先操作缓存,但是不删除缓存。将缓存修改为一个特殊值(-999)。客户端读缓存时,发现是默认值,就休眠一小会,再去查一次Redis。(缺点是特殊值对业务有侵入,休眠时间,可能会多次重复,对性能有影响)②延时双删。先删除缓存,然后再写数据库,休眠一小会,再次删除缓存。如果写操作很频繁,同样还是会有脏数据的问题。

2、先写数据库,再删缓存:如果数据库写完了之后,缓存删除失败,数据就会不一致。

解决方案:①给缓存设置一个过期时间。问题:过期时间内,缓存数据不会更新。

②引入MQ,保证原子操作。

四、 如何设计一个分布式锁?如何对锁性能进行优化?

分布式锁的本质:就是在所有进程都能访问到的一个地方,设置一个锁资源,让这些进程都来竞争锁资源。数据库、zookeeper、redis。通常对于分布式锁,会要求响应快、性能高、与业务无关。

Redis实现分布式锁:

SETNX key value:当key不存在时,就将key设置为value,并返回1。如果key存在,就返回0。

EXPIRE key locktime:设置key的有效时长。

DEL key:删除。

GETSET key value:先GET,再SET,先返回key对应的值,如果没有就返回空。然后再将key设置成value。

1、最简单的分布式锁:SETNX加锁,DEL解锁。问题:如果获取到锁的进程执行失败,他就永远不会主动解锁,那这个锁就被锁死了。

2、给锁设置过期时长:问题:SETNX和EXPIRE并不是原子性的,所以获取到锁的进程有可能还没有执行EXPIRE指令就挂了,这时锁还是会被锁死。

3、将锁的内容设置为过期时间(客户端时间+过期时长),SETNX获取锁失败时,拿这个时间跟当前时间对比,如果是过期的锁,就先删除锁,再重新上锁。

5、分析一下,上面各种优化的根本问题在于SETNX和EXPIRE两个指令无法保证原子性。Redis2.6提供了直接执行lua脚本的方式,通过lua脚本来保证原子性。

伪代码:

public boolean tryLock(RedisConnection connn){
	if(connn.SETNX("mykey", "1") == 1){
        conn.EXPIRE("mykey", 1000); // 给锁设置过期时长
        return true; // 表明获取到锁
    }else{
        return false;
    }
}

DEL

五、Redis如何配置key的过期时间?他的实现原理是什么?

redis设置key的过期时间方式:1、EXPIRE,2、SETEX

实现原理:

1、定期删除:每隔一段时间,执行一次删除过期key的操作。

2、懒汉式删除:当使用get,getset等指令去获取数据时,判断key是否过期。过期后,就先把key删除,再执行后面的操作。

Redis是将两种方式结合来使用。

懒汉式删除

定期删除:平衡执行频率和执行时长。

定期删除时会遍历每个database(默认16个),检查当前库中指定个数的key(默认是20个)。随机抽查这些key,如果有过期的,就删除。

程序中有一个全局变量记录删除到了哪个数据库。

注意redis中的这种删除策略,可以拓展自己的思维,将这种方式延伸到其它数据库中。

六、海量数据下,如何快速查找一条记录?

1、使用布隆过滤器,快速过滤不存在的记录。

使用redis的bitmap结构来实现布隆过滤器

2、在redis中建立数据缓存。将我们对redis使用场景的理解尽量表达出来。

以普通字符串的形式来存储,(userId->user.json)。以一个hash来存储一条记录(userId key -> username field -> ,userAge -> )。但是这样会存储海量的userId,有点浪费空间,所以可以使用一个整的hash来存储所有的数据,UserInfo -> field 就用userId,value就用user.json 。一个hash最多能支持2^32-1(40多个亿)个键值对。

缓存击穿:对不存在的数据也建立key,这些key都是经过布隆过滤器过滤的,所以一般不会太多。

缓存过期:将热点数据设置成永不过期,定期重建缓存。使用分布式锁重建缓存。

3、查询优化

redis是按槽位分配数据的。

自己实现槽位计算,找到记录应该分配到哪台机器上,然后直接去目标机器上找。

微服务篇

一、谈谈你对微服务的理解,微服务有哪些优缺点?

微服务是由Martin Fowler大师提出的。微服务是一种架构风格,通过将大型的单体应用拆分为比较小的服务单元,从而降低整个系统的复杂度。

优点:

1、服务部署更灵活:每个应用都可以是一个独立的项目,可以独立部署,不依赖于其它服务,耦合性降低。

2、技术更新灵活:在大型单体应用中,技术要进行更新,往往是困难的。而微服务可以根据业务特点,灵活选择技术栈。

3、应用的性能得到提高:大型单体应用中,往往启动就会成为一个很大的难关。而采用微服务之后,整个系统的性能是能够提高的。

4、更容易组合专门的团队:在单体应用时,团队成员往往需要对系统的各个部分都要有深入的了解,门槛是很高的。

5、代码复用:很多底层服务可以以Rest API的方式对外提供统一的服务,所有基础服务可以在整个微服务系统中调用。

缺点:

1、服务之间的调用的复杂性提高了:网络问题,容错问题,负载问题,高并发问题。。。

2、分布式事务:尽量不要使用微服务事务。

3、测试的难度提升了。

4、运维难度提升了:单体架构只要维护一个环境,而到了微服务是很多个环境,并且运维方式都还不一样。所以对部署,监控,告警等要求就会变得很困难。

二、SpringCloud和SpringCloudAlibaba都有哪些组件?都解决了什么问题?

SpringCloud:提供了构建微服务系统所需要的一组通用开发模式以及一系列快速实现这些开发模式的工具。

通常所说的SpringCloud是指SpringCloud NetFlix,他和SpringCloud Alibaba都是SpringCloud这一系列开发模式的具体实现。

SpringCloud NetFlix:

SpringCloud Alibaba:

三、分布式事务如何处理?怎么保证事务一致性?

误区:分布式事务 = Seata

分布式事务:就是要将不同节点上的事务操作,提供操作原子性保证。同时成功或同时失败。

分布式事务第一个要点就是要在原本没有直接关联的事务之间建立联系。

1、HTTP连接:最大努力通知。 -- 事后补偿。

2、MQ:事务消息机制。

3、Redis:也可以定制出分布式事务机制。

4、Seata:是通过TC来在多个事务之间建立联系。

两阶段:AT XA 就在于要锁资源。

三阶段:TCC 在两阶段的基础上增加一个准备阶段。在准备阶段是不锁资源的。

SAGA模式:类似于熔断,业务自己实现正向操作和补偿操作的逻辑。

四、怎么拆分微服务?怎样设计出高内聚、低耦合的微服务?有没有了解过DDD领域驱动设计?什么是中台?中台和微服务有什么关系?

拆分微服务的时候,为了尽量保证微服务的稳定,会有一些基本的准则:

1、微服务之间尽量不要有业务交叉。

2、微服务之间只能通过接口进行服务调用,而不能绕过接口直接访问对方的数据。

3、高内聚、低耦合。

什么是DDD,在2004年,由Eric Evans提出的,DDD是面向软件复杂之道。Domain-Driven-Design。

中台这个概念是由阿里在2015年提出的“小前台,大中台”战略思想。

所谓中台就是将各个业务线中可以复用的一些功能抽取出来,剥离个性,提取共性,形成一些可复用的组件。

五、你的项目中是怎么保证微服务敏捷开发的?微服务的链路追踪、持续集成、AB发布要怎么做?

开发运维一体化。

敏捷开发:目的就是为了提高团队的交付效率,快速迭代,快速试错。

每个月固定发布新的版本,以分支的形式保存到代码仓库中。快速入职,任务面板,站立会议。团队人员灵活流动,同时形成各个专家代表。测试环境-生产环境-开发测试环境SIT,集成测试环境、压测环境STR、预投产环境、生产环境PRD。文档优先。晨会,周会,需求拆分会。

链路追踪:1、基于日志。形成全局事务ID,落地到日志文件。filebeat-logstash-Elasticsearch 形成大型报表。

2、基于MQ,往往需要架构支持。经过流式计算形成一些可视化的结果。

持续集成:springboot maven pom -> build -> shell ; Jenkins。

AB发布:1、蓝绿发布、红黑发布。老版本和新版本是同时存在的。2、灰度发布,金丝雀发布。

Spring 篇

三、Spring框架中Bean的创建过程是怎样的?

先得说明一下Spring中的Bean是什么?在spring中,构成应用程序主干并由spring IoC容器管理的对象就被成为bean,bean是一个由spring ioc容器实例化、组装和管理的对象。(关于spring ioc参考文章:spring bean是什么 - 歪麦博客 (awaimai.com)

spring中的bean的三种定义方式:①使用构造型@Component注释你的类;②编写在自定义Java配置类中使用@Bean注释的bean工厂方法;③在xml配置文件中声明bean的定义。

首先简单来说,Spring框架中的Bean经过四个阶段:实例化 - 属性赋值 - 初始化 - 销毁。

然后具体来说,Spring中Bean经过了以下几个步骤:

1、实例化:new xxx();两个时机:① 当客户端向容器申请一个Bean时,② 当容器在初始化一个Bean时发现还需要依赖另一个Bean。BeanDefinition 对象保存。到底是new一个对象还是创建一个动态代理?

2、设置对象属性(依赖注入):spring通过BeanDefinition找到对象依赖的其它对象,并将这些对象赋予当前对象。

3、处理Aware接口:spring会检测对象是否实现了xxAware接口,如果实现了,就会调用对应的方法。

BeanNameAware、BeanClassLoaderAware、BeanFactoryAware、ApplicationContextAware

4、BeanPostProcessor前置处理:调用BeanPostProcessor的postProcessBeforeInitialization方法

5、BeanPostProcessor后置处理。

四、Spring框架中的Bean是线程安全的吗?如果线程不安全,要如何处理?

Spring容器本身没有提供Bean的线程安全策略,因此也可以说Spring容器中的Bean不是线程安全的。

要如何处理线程安全问题,就要分情况来分析。

Spring中的作用域:

1、singleton 单例:spring中默认是单例的,所以是线程不安全的。

2、prototype:为每个Bean请求创建一个实例。

3、request:为每个request请求创建一个实例,请求完成后失效。

4、session:与request是类似的。

5、global-session:全局作用域。

对于线程安全问题:

1、对于prototype作用域,每次都是生成一个新的对象,所以不存在线程安全问题。

2、singleton作用域,默认就是线程不安全的。但是对于开发中大部分的bean,其实是无状态的,不需要保证线程安全。所以在平常的MVC开发中,是不会有线程安全问题的。

无状态表示这个实例没有属性对象,是不能保存数据的,是不变的类。比如:controller、service、dao

有状态表示实例是有属性对象,可以保存数据,是线程不安全的,比如pojo

但是如果要保证线程安全,可以将bean的作用域改为 prototype,比如像Model,View等(因为这类对象每次返回时都是new出来的)。

另外还可以采用ThreadLocal来解决线程安全问题。ThreadLocal为每个线程保存一个副本变量,每个线程只操作自己的副本变量。

五、Spring如何处理循环依赖问题?

循环依赖:多个对象之间存在循环引用的关系,在初始化过程当中,就会出现“先有蛋还是先有鸡”的问题。

一种是使用@Lazy注解:解决构造方法造成的循环依赖问题。

另一种是使用三级缓存:如果引用的对象配置了AOP,那在单例池中最终就会需要注入动态代理对象,而不是原对象。而生成动态代理是要在对象初始化完成之后才开始的。于是Spring增加了三级缓存,保存所有对象的动态代理配置信息。在发现有循环依赖时,将这个对象的动态代理信息获取出来,提前进行AOP,生成动态代理。

六、Spring如何处理事务?

Spring当中支持编程式事务管理和声明式事务管理两种方式:

1、编程式事务管理可以使用TransactionTemplate。

2、声明式事务:是Spring在AOP基础上提供的事务实现机制(在需要回滚的方法上使用@Transactional注解)。他的最大优点就是不需要在业务代码中添加事务管理的代码,只需要在配置文件中做相关的事务规则声明就可以了。但是声明式事务只能作用于方法级别,无法控制代码级别的事务管理。

七、SpringMVC中的控制器是不是单例模式?如果是,如何保证线程安全?

posted @ 2021-10-20 21:17  LoremMoon  阅读(31)  评论(0编辑  收藏  举报