面试题学习记录初稿

并发:

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

    如何开启线程?

        1、继承Thread类,重写run方法,调用start方法。

        2、实现Runnable接口,实现run方法。

        3、实现Callable接口,实现call方法。通过FutureTask创建一个线程,获取到线程执行的返回值。

        4、通过线程池来开启线程。

    怎么保证线程安全?

        加锁:1、使用JVM提供的锁,Synchronized关键字。2、JDK提供的各种锁 Lock。

        线程池的使用:   

new ThreadPoolExecutor(int corePoolSize,
                       int maximumPoolSize,
                       long keepAliveTime,
                       TimeUnit unit,
                       BlockingQueue<Runnable> workQueue,
                       ThreadFactory threadFactory,
                       RejectedExecutionHandler handler);




ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2,
                10, 5000, TimeUnit.MILLISECONDS, new SynchronousQueue(),
                new ThreadPoolExecutor.CallerRunsPolicy());
List<AlgorithmEntity> list = new Vector<>();    //多线程中使用的list,普通list的长度不好控制
threadPoolExecutor.submit(() -> {
                System.out.println("第" + (finalI + 1) + "个线程开始");
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    return null;
                }
                a[finalI] = finalI + 1;
                aqqq.add(new AlgorithmEntity());
//                aqqq.add("第" + (finalI + 1) + "个线程成功");
                return "第" + (finalI + 1) + "个线程成功";
            });

Future<R> future = threadPoolExecutor.submit(new Callable<T>() {});    //可自定义返回的类型
future.get();    //get()方法将阻塞当前线程,直到子线程完成并返回结果
List<Future> futures = new ArrayList<>();

        参数解释:

        corePoolSize:指定了线程池中的线程数量,它的数量决定了添加的任务是开辟新的线程去执行,还是放到workQueue任务队列中去;

        maximumPoolSize:指定了线程池中的最大线程数量,这个参数会根据你使用的workQueue任务队列的类型,决定线程池会开辟的最大线程数量;

        keepAliveTime:当线程池中空闲线程数量超过corePoolSize时,多余的线程会在多长时间内被销毁;

        unit:keepAliveTime的单位,如:TimeUnit.MILLISECONDS;

        workQueue:任务队列,被添加到线程池中,但尚未被执行的任务;它一般分为直接提交队列(new SynchronousQueue<>())、有界任务队列、无界任务队列、优先任务队列几种;

        threadFactory:线程工厂,用于创建线程,一般用默认即可,如:Executors.defaultThreadFactory();

        handler:拒绝策略;当任务太多来不及处理时,如何拒绝任务;

        相关链接:java线程池ThreadPoolExecutor类使用详解 - DaFanJoy - 博客园 (cnblogs.com)

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

    1、Synchronized关键字,用来加锁。Volatile只是保持变量的线程可见性。通常适用于一个线程写,多个线程读的场景。

    2、不能。Volatile关键字只能保证线程可见性,不能保证原子性。

    3、Volatile防止指令重排。在DCL中,防止高并发情况下,指令重排造成的线程安全问题。

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

    1、JAVA的锁就是在对象的Markword中记录一个锁状态。偏向锁、轻量级锁、重量级锁对应不同的锁状态。

    2、JAVA的锁机制就是根据资源竞争的激烈程度不断进行锁升级的过程

    3、 偏向锁:有线程占用了资源,告知其他线程有锁;

        轻量级锁:轻度竞争,其他线程不断请求资源,消耗CPU资源,但是效率较高;

        重量级锁:重度竞争(耗时过长,wait()等),由操作系统进行通知,会降低效率;

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

    1、AQS(队列同步器AbstractQueuedSynchronizer)是一个JAVA线程同步的框架。是JDK中很多锁工具的核心实现框架。

    2、在AQS中,维护了一个信号量state和一个线程组成的双向链表队列。其中,这个线程队列,就是用来给线程排队的,而state就像是一个红绿灯,用来控制线程排队或者放行的。在不同场景下,有不同的意义。

    3、在可重入锁这个场景下,state就用来表示加锁的次数。0标识无锁,每加一次锁,state就加1,释放锁state就减1。

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

    CountDownLatch:定义了一个计数器(计数器只能使用一次),和一个阻塞队列, 当计数器的值递减为0之前,阻塞队列里面的线程处于挂起状态,当计数器递减到0时会唤醒阻塞队列所有线程。

        CountDownLatch(int count); //构造方法,创建一个值为count 的计数器。

        await();//阻塞当前线程,将当前线程加入阻塞队列。

        await(long timeout, TimeUnit unit);//在timeout的时间之内阻塞当前线程,时间一过则当前线程可以执行,

        countDown();//对计数器进行递减1操作,当计数器递减至0时,当前线程会去唤醒阻塞队列里的所有线程。

    CylicBarrier:可以实现一组线程相互等待,当所有线程都到达某个屏障点后再进行后续的操作。

        CyclicBarrier(int parties); //表示屏障拦截的线程数量

        CyclicBarrier(int parties, Runnable barrierAction); //线程到达屏障时,优先执行barrierAction

        await();    //告诉CyclicBarrier自己已经到达同步点,然后当前线程被阻塞

        reset();  //重置计数器

    Semaphore:控制同时访问特定资源的线程数量,通过协调各个线程,以保证合理的使用资源。

        Semaphore semaphore=new Semaphore(10);  //限制为10

        acquire();  //获取一个令牌,在获取到令牌、或者被其他线程调用中断之前线程一直处于阻塞状态。

        release();  //释放一个令牌,唤醒一个获取令牌不成功的阻塞线程。

        availablePermits(); //返回可用的令牌数量

       

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

    Fork/Join框架


 

网络:

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

    TCP(Transfer Control Protocol,传输控制协议):是一种面向连接、可靠的、传输层协议。

        特点:面向连接的,点对点的通信,高可靠的,效率比较低,占用的系统资源比较多。

    UDP(User Datagram Protocol):是一种无连接的、不可靠的、传输层协议。

        特点:不需要连接,发送方不管接收方有没有准备好,直接发消息;可以进行广播发送;传输不可靠,有可能丢失消息;效率比较高;协议比较简单,占用的系统资源比较少。

    TCP建立连接三次握手,断开连接四次挥手

    如果是两次握手,可能会造成连接资源浪费的情况。

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

    BIO 同步阻塞IO:每有一个客户端请求,服务端则生成一个线程去处理。

        特点:可靠性差(线程可能会丢失),吞吐量低,适用于连接比较少且比较固定的场景。JDK1.4版本之前唯一的选择。编程模型最简单。

    NIO 同步非阻塞IO:无论多少个客户端请求,服务端只有一个线程去处理,服务端通过selector去控制响应哪一个客户端的请求。

        特点:可靠性比较好,吞吐量比较高,适用于连接比较多并且连接比较短(轻操作),例如聊天室。编程模型最复杂。

    AIO 异步非阻塞IO:在NIO的基础上,在服务端处理完客户端请求之后会主动去通知客户端。JDK1.4版本才开始支持。

        特点:可靠性最好,吞吐量非常高,适用于连接比较多,并且连接比较长(重操作)。例如相册服务器。JDK7版本才开始支持。编程模型比较简单,但需要操作系统来支持(服务是不稳定的,服务无法解决异步通知问题)。

    在一个网络请求中,客户端会发一个请求到服务端。

    1、客户端发了请求后,就一直等着服务端响应。客户端:阻塞。请求:同步。

    2、客户端发了请求后,就去干别的事情。时不时过来检查服务端是否给出了响应。客户端:非阻塞。请求:同步。

    3、客户端发了请求后,就去干别的事情。等到服务端给出响应后,再过来处理业务逻辑。客户端:非阻塞。请求:异步。

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

    buffer:和channel都是可读可写的。

       ↓

    channel:类似于流。每个channel对应一个buffer缓冲区。channel会注册到seelector。

       ↓

    selector:会根据channel上发生的读写事件,将请求交给某个空闲的线程处理。selector对应一个或多个线程。

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

    他们是NIO中多路复用的三种实现机制,是由Linux操作系统提供的。

    用户空间和内核空间:操作系统为了保护系统安全,将内核划分为两个部分,一个是用户空间,一个是内核空间。用户空间不能直接访问底层的硬件设备,必须通过内核空间。

    文件描述符 File Descriptor(FD):是一个抽象的概念,形式上是一个整数,实际上是一个索引值。指向内核中为每个进程维护进程所打开的文件的记录表。当程序打开一个文件或者创建一个文件时,内核就会向进程返回一个FD。Unix,Linux。

    select机制:会维护一个FD的集合fd_set。将fd_set从用户空间复制到内核空间,激活socket。会受fd_set大小的影响性能,在x64位机器上大小为2048,fd_set是一个数组结构。

    poll机制:和select机制相似,把fd_set结构进行了优化,FD集合的大小突破了操作系统的限制。pollfd结构代替fd_set,通过链表实现。

    epoll(event poll):epoll中不再扫描所有的FD,只将用户关心的FD的事件存放到内核的一个事件中。这样可以减少用户空间与内核空间之间需要拷贝的数据。

    总结:

                            操作方式        底层实现        最大连接数          IO效率

        select:            遍历            数组            受限于内核          一般

          poll:              遍历            链表            无上限              一般

         epoll:        事件回调        红黑树          无上限              高

    java的NIO当中是用的哪种机制?

        可以查看DefaultSelectorProvider源码。

        在Windows下,WindowsSelectorProvider。

        而Linux下,根据Linux的内核版本,2.6版本以上,就是EPollSelectorProvider,否则就是默认的PollSelectorProvider。

    select 1984年出现,poll  1997年出现,epoll  2002年出现

五、描述下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这样的攻击时,几乎起不到任何作用。

        3、证书需要费钱,并且功能越强大的证书费用越高。

JVM:

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

        

 

  

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

        JAVA的类加载器:AppClassloader → ExtClassLoader → BootStrap ClassLoader

        每种类加载器都有他自己的加载目录

        JAVA中的类加载器:AppClassloader,ExtClassLoader → URLClassLoader → SecureClassloader → ClassLoader

        每个类加载器对他加载过的类,都是有一个缓存的。

        双亲委派:向上委托查找,向下委托加载。作用:保护JAVA底层的类不会被应用程序覆盖。

       

        类加载过程:加载 → 连接 → 初始化

                加载:把JAVA的字节码数据加载到JVM内存当中,并映射成JVM认可的数据结构。

                连接:分为三个小的阶段:

                        1、验证:检查加载到的字节信息是否符合JVM规范。

                        2、准备:创建类或接口的静态变量,并赋予初始值(半初始化状态)。

                        3、解析:把符号引用转为直接引用。

                初始化:

        1、用户创建一个对象,JVM首先需要到方法区去找对象的类型信息,然后再创建对象。

        2、JVM要实例化一个对象,首先要在堆当中先创建一个对象(半初始化状态)。

        3、对象首先会分配在堆内存中新生代的Eden区。然后经过一次Minor GC,对象如果存活,就会进入S区。在后续的每次GC中,如果对象一直存活,就会在S区来回拷贝,每移动一次,年龄加1(年龄最大为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调优的重点。

        JVM的垃圾回收器:

                

        Serial (串行):整体过程比较简单,需要GC时,直接暂停,GC完了再继续。这个垃圾回收器,是早期的垃圾回收器,只有一个线程执行GC,在多CPU架构下,性能就会严重下降。只适用于几十兆的空间。

                

        Parallel(并行):在串行的基础上,增加多线程GC。PS+PO这种组合是JDK1.8默认的垃圾回收器。在多CPU的架构下,性能会比 Serial 高很多。

                

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

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

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

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

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

                 

        G1(Garbage First 垃圾优先):他的内存模型中,对于堆内存不再分老年代和新生代,而是划分成一个一个的小内存块,叫做Region。每个Region可以隶属于不同的年代。GC分为四个阶段:

        第一:初始标记,标记出GC Root 直接引用的对象,STW。

        第二:标记Region,通过RSet标记出上一个阶段标记的Region引用到的Old区Region。

        第三:并发标记阶段:跟CMS的步骤是差不多的,只是遍历的范围不再是整个Old区,而是只需要遍历第二步标记出来的Region。

        第四:重新标记:跟CMS中的重新标记过程是差不多的。

        第五:垃圾清理:与CMS不同的是,G1采用拷贝算法,直接将整个Region中的对象拷贝到另一个Region。而这个阶段,G1只选择垃圾较多的Region来清理,并不是完全清理。

                 

        

        CMS的核心算法就是三色标记。

        三色标记:是一种逻辑上的抽象。将每个内存对象分成三种颜色:黑色:表示自己和成员变量都已经标记完毕。灰色:自己已经标记完了,但是成员变量还没有完全标记完。白色:自己未标记完。

        错标,漏标主要发生在并发标记阶段,是由于在程序运行过程中对象之间的引用发生了变化而导致的。

        CMS通过增量标记(increment update:把黑色再标为灰色)的方式来解决漏标记的问题。

        G1通过快照(SATB Snapshot At The Begining)的方式来解决漏标记的问题。

        为什么要设计这么多的垃圾回收算法:因为使用的内存逐渐变大 。

六、如何进行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 :查看所有不稳定指令的默认值。

        java -XX:+PrintFlagsFinal :查看所有不稳定指令最终生效的实际值。

消息队列:

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

        MQ:MessageQueue,消息队列。队列是一种FIFO先进先出的数据结构。消息由生产者发送到MQ进行排队,然后由消费者对消息进行处理。QQ、微信就是典型的MQ场景。

        MQ的作用主要有三个方面:

        1、异步:能够提高系统的响应速度和吞吐量。

        2、解耦:服务之间进行解耦,可以减少服务之间的影响,提高系统的稳定性和可扩展性。另外,解耦之后可以实现数据分发。生产者发送一个消息后,可以有多个消费者来处理。

        3、削峰:以稳定的系统资源应对突发的流量冲击。

        MQ的缺点:

        1、系统的可用性降低:一旦MQ宕机,整个业务就会产生影响。高可用。

        2、系统的复杂度提高:引入MQ之后,数据链路就会变得很复杂。如何保证消息不丢失?消息不会重复调用?怎么保证消息的顺序?

        3、数据一致性:如果A发消息,需B和C一同处理,但B成功,C失败,数据的一致性无法保证。

二、如何进行产品选型?

        Kafka:

                优点:吞吐量非常大,性能非常好,集群高可用。

                缺点:会丢失数据,功能比较单一。

                使用场景:日志分析、大数据采集。

        RabbitMQ:

                优点:消息可靠性高,功能全面。

                缺点:吞吐量比较低,消息积累会严重影响性能。erlang语言不好定制。

                使用场景:小规模场景。

        RocketMQ:

                优点:高吞吐,高性能,高可用,功能非常全面。

                缺点:开源版功能不如云上商业版。官方文档和周边生态还不够成熟。客户端只支持JAVA。

                使用场景:几乎是全场景。

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

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

                

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

        2.1 生产者发送消息不丢失

        Kafka:消息发送+回调

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

            

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

        2.2 MQ主从消息同步不丢失

        RocketMQ:1、普通集群中,同步同步、异步同步。异步同步效率更高,但是有丢失消息的风险。同步同步就不会丢失消息。2、Dledger集群-两阶段提交(需要至少三台服务器)。

        RabbitMQ:1、普通集群(erlang.cookie文件相同):消息是分散存储的,节点之间不会主动进行消息同步,是有可能丢失消息的。2、镜像集群(在普通集群的基础上再进行配置):镜像集群会在节点之间主动进行数据同步,这样数据安全性得到提高。

        Kafka:通常都是用在允许消息少量丢失的场景。可配置acks参数(0,1,all)。

        2.3  MQ消息存盘不丢失

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

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

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

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

        RabbitMQ:关闭autoCommit,改为手动提交offset。

        Kafka:关闭autoCommit,改为手动提交offset。

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

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

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

        RocketMQ:给每个消息分配了个MessageID。这个MessageID就可以作为消费者判断幂等的依据,但是在RabbitMQ和Kafka中不存在MessageID,而且在大数据量的情况下生成的MessageID也无法保证唯一性,所以这种方式不太建议。

        最好的方式就是自己带一个有业务标识的ID或统一分配唯一的ID,来进行幂等判断。

        

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

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

RocketMQ中的设计

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

        RocketMQ中有完整的设计,但是在RabbitMQ和Kafka当中,并没有完整的设计,需要自己进行设计。

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

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

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

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

        传统文件复制方式:需要对文件在内存中进行四次拷贝。

                

         零拷贝:有两种方式,Mmap和Transfile

                

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

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

        Transfile 没有大小限制。

        RocketMQ当中使用Mmap方式来对他的文件进行读写。如:commitlog。

        kafka当中,他的index日志文件也是通过Mmap的方式来读写的。在其他日志文件当中,并没有使用零拷贝的方式。

        kafka使用Transfile方式将硬盘数据加载到网卡。

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

        分布式事务:业务相关的多个操作,保证他们同时成功或者同时失败。

        最终一致性:只要最终多个操作的结果一致就行,时间上要求不严谨,与之对应的就是强一致性,需一起完成操作。

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

        1、生产者要保证100%的消息投递:事务消息机制。

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

        分布式MQ的三种语义:

        at least once:需最少发送成功一次并消费成功一次。

        at most once:只需要发送一次。

        exactly once:刚好发送一次。

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

        1、从整体到细节,从业务场景到技术实现。

        2、以现有的产品为基础。RocketMQ

缓存:

一、为什么使用缓存?

        高性能、高可用,为数据库缓解查询压力。

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

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

        解决方案:

        1.1 对参数进行合法性校验。

        1.2 将数据库中没有查到结果的数据也写入缓存。这时要注意为了防止Redis被无用的key占满,这一类缓存的有效期要设置得短一点。

        1.3 引用布隆过滤器(BloomFilter),在访问Redis之间判断数据是否存在。要注意布隆过滤器存在一定的误判率,并且,布隆过滤器只能加数据不能减数据。

                

         2、缓存击穿:缓存中没有,数据库中有。一般是出现在缓存数据初始化或者key过期了的情况。他的问题在于,重新写入缓存需要一定的时间,如果是在高并发场景下,过多的请求就会瞬间写到DB上,给DB造成很大的压力。

        解决方案:

        2.1 设置这个热点缓存永不过期。这时要注意在value当中包含一个逻辑上的过期时间,然后另起一个线程,定期重建这些缓存。

        2.2 加载DB的时候,要防止并发。

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

        解决方案:

        3.1 把缓存的失效时间分散开。如:在原有的统一失效时间的基础上,增加一个随机值。

        3.2 对热点数据设置永不过期。

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

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

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

        解决方案:

                1.1 先操作缓存,但是不删除缓存。将缓存修改为一个特殊值(-999)。客户端读缓存时,发现是默认值,就休眠一小会,再去查一次Redis。但这个方案对业务有侵入,休眠时间可能会多次重复,对性能有影响。

                1.2 延时双删。先删除缓存,然后再写数据库,休眠一会,再次删除缓存。如果数据写操作很频繁,同样还是会有脏数据的问题。

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

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

        解决方案:

                2.1 给缓存设置一个过期时间。但是过期时间内,缓存数据不会更新。

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

                2.3 将热点数据缓存设置为永不过期,但是在value当中写入一个逻辑上的过期时间,另外起一个后台进程,扫描这些key,对于逻辑上已过期的缓存,进行删除。

        总结:始终只能保证一定时间内的最终一致性。 

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

        分布式锁的本质:就是在所有进程都能访问到的一个地方,设置一个锁资源,让这些进程都来竞争锁资源。如:数据库、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获取锁失败时,拿这个时间跟当前时间比对,如果是过期的锁,就先删除锁,再重新上锁。在高并发场景下,会产生多个进程同时拿到锁的情况。

        4、SETNX 失败后,获取锁上的时间戳,然后用GETSET,将自己的过期时间更新上去,并获取旧值。如果这个旧值,跟之前获取的时间戳是不一致的,就表示这个锁已经被其他进程占用了,自己就要放弃竞争锁。

        5、以上就形成了一个比较高效的分布式锁。但上面各种优化的根本原因在于 SETNX 和 EXPIRE 两个指令无法保证原子性。Redis2.6 提供了直接执行 Lua 脚本的方式,通过 Lua 脚本来保证原子性。redission。

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

        Redis 设置key的过期时间:1、EXPIRE。2、SETEX。

        实现原理:

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

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

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

        定期删除:平衡执行频率和执行时长。会遍历每个database(默认16个),检查当前库中指定个数的key(默认是20个)。随机抽查这些key,如果有过期的,就删除。程序中有一个全局变量记录遍历到了哪个数据库。

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

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

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

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

             以普通字符串的形式来存储。以一个 hash 来存储一条记录。一个 hash 最多能支持2^32-1(40多亿)个键值对。

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

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

        3、查询优化。按槽位分配数据,自己实现槽位计算,找到记录应该分配在哪台机器上,然后直接去目标机器上找。

微服务:

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

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

        优点:

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

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

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

        4、更容易组合专门的团队:在单体应用时,团队成员往往需要对系统的各个部分都要有深入的了解,门槛是很高的。而采用微服务之后,可以给每个微服务组建专门的团队。

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

        缺点:

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

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

        3、测试的难度提升了。

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

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

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

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

        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。

        Martin Fowler - 贫血模型 ——》贫血失忆症。充血模型

        MVC架构——》领域优先

                                

        大泥团:不利于微服务的拆分。大泥团结构拆分出来的微服务依然是泥团结构,当服务业务逐渐复杂,这个泥团又会膨胀成为大泥团。

        DDD只是一种方法论,没有一个稳定的技术框架。DDD要求领域是跟技术无关、跟存储无关、跟通信无关。

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

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

        大体上,中台可以分为三类:业务中台、数据中台和技术中台。

        中台和DDD结合:DDD会通过限界上下文将系统拆分成一个一个的领域,而这种限界上下文,天生就成了中台之间的逻辑屏障。

        DDD在技术与资源调度方面都能够给中台建设提供不错的指导。

        DDD分为战略设计和战术设计。上层的战略设计能够很好的指导中台划分,下层的战术设计能够很好的指导微服务搭建。

        在目前阶段,DDD还大都处在小范围实验的阶段。

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

Spring:

一、什么是Spring?谈谈你对IOC和AOP的理解。

        Spring:是一个企业级Java应用框架,他的作用主要是简化软件的开发以及配置过程,简化项目部署环境。

        Spring的优点:

        1、Spring低侵入设计,对业务代码的污染非常低。

        2、Spring的DI机制将对象之间的关系交由框架处理,减少组件的耦合。

        3、Spring提供了AOP技术,支持将一些通用的功能进行集中式管理,从而提供更好的复用。

        4、Spring对于主流框架提供了非常好的支持。

        IOC就是控制反转,指创建对象的控制权转移给Spring进行管理。简单来说,就是应用不用去new对象了,而全部交由Spring自动生成。

        IOC有三种注入方式:1、构造器注入。        2、setter方式注入。        3、根据注解注入。

        AOP面向切面。用于将那些与业务无关,但却对多个对象产生影响的公共行为逻辑,抽取并封装成一个可重用的模块。AOP的核心就是动态代理。JDK的动态代理 和 CGLIB动态代理。

二、Spring容器的启动流程是怎么样的?

        使用 AnnotationConfigApplicationContext 来跟踪一下启动流程:

        this();        初始化 reader 和 scanner  

        scan(basePackages);        使用scanner组件扫描basePackage下的所有对象,将配置类的BeanDefinition注册到容器中。

        refresh();        刷新容器。

                prepareRefresh: 刷新前的预处理。

                obtainFreshBeanFactory: 获取在容器初始化时创建得 BeanFactory。

                prepareBeanFactory:BeanFactory 的预处理工作,会向容器中添加一些组件。

                postProcessBeanFactory:子类重写该方法,可以实现在 BeanFactory 创建并预处理完成后做进一步的设置。

                invokeBeanFactoryPostProcessors:在 BeanFactory 初始化之后执行 BeanFactory 的后处理器。

                registerBeanPostProcessors:向容器中注册 Bean 的后处理器,他的主要作用就是干预Spring 初始化 Bean 的流程,完成代理、自动注入、循环依赖等这些功能。

                initMessageSource:初始化 messageSource 组件,主要用于国际化。

                initApplicationEventMulticaster:初始化事件分发器。

                onRefresh:留给子容器,子类重写的方法,在容器刷新的时候可以自定义一些逻辑。

                registerListeners:注册监听器。

                finishBeanFactorylnitialization:完成 BeanFactory 的初始化,主要作用是初始化所有剩下的单例 Bean。

                finishRefresh:完成整个容器的初始化,发布 BeanFactory 容器刷新完成的事件。

                

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

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

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

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

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

                3、处理 Aware 接口:Spring 会检测对象是否实现了 xxxAware 接口,如果实现了,就会调用对应的方法。 BeanNameAware、BeanClassLoaderAware、BeanFactoryAware、ApplicationContextAware。

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

                5、lnitializingBean: Spring 检测对象如果实现了这个接口,就会执行他的 afterPropertiesSet() 方法,定制初始化逻辑。

                6、init-method: <bean init-method=xxx> 如果 Spring 发现 Bean 配置了这个属性,就会调用他的配置方法,执行初始化逻辑。 @PostConstruct

                7、BeanPostProcessor 后置处理:调用 BeanPostProcessor 的 postProcessAfterlnitialization 方法。

        到这里,这个 Bean 的创建过程就完成了,Bean 就可以正常使用了。

                8、DisposableBean:当 Bean 实现了这个接口,在对象销毁前就会调用 destory() 方法。

                9、destroy-method: <bean destroy-method=xxx> @PreDestroy。

                

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

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

六、Spring如何处理事务?







 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值